乐趣区

关于tree-shaking:CSS-Tree-Shaking

我的项目引入 bricks 根底组件库,并不是独自引入每一个所依赖的根底组件款式,而是在入口文件全局引入所有款式import '@casstime/bricks/lib/styles/bricks.scss';,这就导致一些没有被应用的组件款式被打包到最终产物中,须要对款式做树摇解决。

接下来就该 PurgeCSS 上场了。PurgeCSS 是一个用来删除未应用的 CSS 代码的工具。能够将它作为你的开发流程中的一个环节。当你构建一个网站时,你可能会决定应用一个 CSS 框架,例如 TailwindCSS、Bootstrap、MaterializeCSS、Foundation 等,然而,你所用到的也只是框架的一小部分而已,大量 CSS 款式并未被应用。PurgeCSS 通过剖析你的内容和 CSS 文件,首先它将 CSS 文件中应用的选择器与内容文件中的选择器进行匹配,而后它会从 CSS 中删除未应用的选择器,从而生成更小的 CSS 文件。

webpack对应插件purgecss-webpack-plugin,该插件的应用依赖款式抽离插件mini-css-extract-plugin,只有先将款式抽离成独立文件后能力将 CSS 文件中应用的选择器与内容文件中的选择器进行匹配,而后它会从 CSS 中删除未应用的选择器,从而生成更小的 CSS 文件。

插件 purgecss-webpack-plugin 的应用须要指定 paths 属性,通知 purgecss 须要剖析的文件列表,这些文件中应用的选择器与抽离的款式文件中的选择器进行匹配,从而剔除未应用的选择器。

因为我的项目中应用到款式的文件有 src 目录和引入的业务组件以及 bricks 根底组件,所以须要将这些文件目录指定为被剖析的列表,这些文件中应用到的款式选择器不会被剔除掉

// config-overrides.js
const fs = require('fs');
const path = require('path');
const glob = require('glob-all');
const webpack = require('webpack');
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const paths = require('react-scripts/config/paths');

/** css tree shaking */
function CssTreeShaking(config) {
  /** 开发环境下不做非凡解决 */
  if (process.env.NODE_ENV === 'development') return;
  /** 有一些动静款式须要手动匹配保留,形如 classNames=`icon-${type}` classNames=`mall-sidebar__${name}-icon` */
  function collectSafelist() {
    return {standard: ['icon', /^icon-/, /^mall-sidebar__/, /^mall-right-trial-modal/],
      deep: [/^icon-/, /^mall-sidebar__/, /^mall-right-trial-modal/],
      greedy: [/^icon-/, /^mall-sidebar__/, /^mall-right-trial-modal/],
    };
  }
  /** 收集我的项目中应用到的 bricks 组件(引入的业务组件中可能也有应用 bricks 组件)*/
  var files = glob.sync([path.join(paths.appSrc, '/**/*.{ts,tsx}'),
    path.resolve(__dirname, `./node_modules/@casstime/bre-*/**/*.tsx`),
    path.resolve(__dirname, `./node_modules/@casstime/mall-*/**/*.tsx`),
  ]);
  /** 正则匹配我的项目中应用了哪些 bricks 组件 */
  let usedComps = [];
  files.forEach((filePath) => {const code = fs.readFileSync(filePath, 'utf-8');
    const reg = new RegExp(/import\s+\{(.*)\}\s+from\s+'@casstime\/bricks'/);
    const ret = code.match(reg);
    if (ret) {const comps = ret[1].replace(/\s+/g, '').toLowerCase().split(',');
      usedComps.push(...comps);
    }
  });
  /** 应用到 bricks 组件的文件列表 */
  const usedBrsPaths = [...new Set(usedComps)].map((comp) => {return path.resolve(__dirname, `./node_modules/@casstime/bricks/lib/components/${comp}/*.js`);
  });

  /** 收集我的项目中应用到业务组件的门路 */
  const usedBrePaths = [path.resolve(__dirname, `./node_modules/@casstime/bre-*/**/*.tsx`),
    path.resolve(__dirname, `./node_modules/@casstime/mall-*/**/*.tsx`),
  ];

  /** 款式树摇 */
  const purgeCSSPlugin = new PurgeCSSPlugin({
    /** paths 示意这些文件中应用的款式须要保留,没有应用的款式将会被剔除 */
    paths: glob.sync([paths.appHtml, path.join(paths.appSrc, '/**/*.{ts,tsx}', ...usedBrsPaths, ...usedBrePaths],
      {nodir: true},
    ),
    // fontFace: true,
    safelist: collectSafelist,
  });
  config.plugins.push(purgeCSSPlugin);
}

module.exports = function override(config, env) {CssTreeShaking(config);
  return config;
};

应用上述 purgecss-webpack-plugin 解决会有一个问题,那就是没有思考到 css modules 的影响,因为款式选择器被哈希化,与剖析文件中应用的款式选择器不能匹对,导致被剔除掉了

// src/page/components/Main.tsx
import styles from './index.module.scss';

const Main = () => {return <div className={styles.container}></div>
}

// 如果构建后款式产物如下(main.5d986f58.chunk.css)
.container__3PRC3 {}

// main.5d986f58.chunk.css 在与 Main.tsx 款式选择器匹配时发现选择器 container 与 container__3PRC3 不统一,就把 container__3PRC3 款式选择器剔除掉了

所以,在 css modules 场景下应用 purgecss-webpack-plugin 做款式荡涤会有问题。PurgeCSS 官网思考到此问题,给出了相干 loader 做款式荡涤计划。css modules是在 css-loader 解决后的后果,为了防止 css modules 对款式荡涤的影响,能够在 css-loader 之前,sass-loader之后引入 postCSS 插件 @fullhuman/postcss-purgecss 解决

// config-overrides.js
const fs = require('fs');
const path = require('path');
const glob = require('glob-all');
const webpack = require('webpack');
const paths = require('react-scripts/config/paths');

/** css tree shaking */
function CssTreeShaking(config) {
  /** 开发环境下不做非凡解决 */
  if (process.env.NODE_ENV === 'development') return;
  /** 有一些动静款式须要手动匹配保留,形如 classNames=`icon-${type}` classNames=`mall-sidebar__${name}-icon` */
  function collectSafelist() {
    return {standard: ['icon', /^icon-/, /^mall-sidebar__/, /^mall-right-trial-modal/],
      deep: [/^icon-/, /^mall-sidebar__/, /^mall-right-trial-modal/],
      greedy: [/^icon-/, /^mall-sidebar__/, /^mall-right-trial-modal/],
    };
  }
  /** 收集我的项目中应用到的 bricks 组件(引入的业务组件中可能也有应用 bricks 组件)*/
  var files = glob.sync([path.join(paths.appSrc, '/**/*.{ts,tsx}'),
    path.resolve(__dirname, `./node_modules/@casstime/bre-*/**/*.tsx`),
    path.resolve(__dirname, `./node_modules/@casstime/mall-*/**/*.tsx`),
  ]);
  let usedComps = [];
  files.forEach((filePath) => {const code = fs.readFileSync(filePath, 'utf-8');
    const reg = new RegExp(/import\s+\{(.*)\}\s+from\s+'@casstime\/bricks'/);
    const ret = code.match(reg);
    if (ret) {const comps = ret[1].replace(/\s+/g, '').toLowerCase().split(',');
      usedComps.push(...comps);
    }
  });
  const usedBrsPaths = [...new Set(usedComps)].map((comp) => {return path.resolve(__dirname, `./node_modules/@casstime/bricks/lib/components/${comp}/*.js`);
  });

  /** 收集我的项目中应用到的业务组件 */
  const usedBrePaths = [path.resolve(__dirname, `./node_modules/@casstime/bre-*/**/*.tsx`),
    path.resolve(__dirname, `./node_modules/@casstime/mall-*/**/*.tsx`),
  ];

  const pureCSSPaths = [...usedBrsPaths, ...usedBrePaths];

  const targetRules = config.module.rules.find((rule) => !!rule.oneOf);
  if (targetRules && targetRules.oneOf) {
    const sassModuleRule = targetRules.oneOf.find((rule) => rule.test && new RegExp(rule.test).test('.module.scss'),
    );
    if (sassModuleRule) {const postCssLoader = sassModuleRule.use.find((item) =>
        /postcss-loader/.test(item.loader || ''),
      );
      if (postCssLoader) {postCssLoader.options.plugins = () =>
          [require('postcss-flexbugs-fixes'),
            require('postcss-preset-env')({
              autoprefixer: {flexbox: 'no-2009',},
              stage: 3,
            }),
            require('@fullhuman/postcss-purgecss')({
              content: [
                paths.appHtml,
                ...glob.sync([path.join(paths.appSrc, '/**/*.{ts,tsx}')].concat(pureCSSPaths), {nodir: true,}),
              ],
              safelist: collectSafelist(),}),
            require('postcss-normalize'),
          ].filter(Boolean);
      }
    }
  }
}

module.exports = function override(config, env) {CssTreeShaking(config);
  return config;
};

优化前后产物体积比照,减小体积还是很主观的~

在理论优化过程中,因为生产环境 node@10.18.0 版本太低,如果装置的 @fullhuman/postcss-purgecsspurgecss-webpack-plugin版本过高,其外部依赖的 postcss@8.0.0 在低版本 node 环境下装置不了,须要升高版本 @fullhuman/postcss-purgecss@3.1.3purgecss-webpack-plugin@3.1.3,必要时可能还需配置 resolutions 对立 postcss 版本

{
    "resolutions": {
      "commander": "7.2.0",
      "postcss": "7.0.39"
    }
}
退出移动版