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"

}

}

以上是 CSS Tree Shaking 的全部内容, 来源链接: www.h5w3.com/qa/757639.html

回到顶部