1

前言

前端工程化主要解决效率质量的问题;webpack是一种实现前端工程化的有效方案。
基本配置思路图

基本概念

webpack类似一个加工处理器,通过灵活的配置文件实现复杂需求;主要配置有入口、出口、装在器、插件
资料
  • entry/output: 出口/入口
  • loader/plugin: 装载器/插件 加工处理
  • moudle/chunk/bundle:

    • moudle: 源码每个文件可称为一个模块
    • chunk: 通过webpack处理后的文件为chunk代码块,一般由多个模块构成
    • bundle: 最终产品,一般为chunk处理后的代码块

主要任务

自动化处理js,css,html,图片等静态资源

脚本处理

主要让javascript适应宿主环境,竟可能的压缩体积和复用
es6

ployfill : 全局垫片,注入全局对象,污染全局
runtime : 局部垫片,按需局部注入,不会污染全局
presets

// .babelrc
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage|entry|false" // usage 按需加载polyfill
    }]
  ]
}

browerslist : 兼容到目标浏览器标准

ts

ts / tsconfig

// ts.config.json
{  
  "compilerOptions": {  
    "module": "commonjs",  
    "target": "es5"  
  },  
  "exclude": \["./node\_modules"\]  
}

样式处理

css处理

style-loader (合并style标签,transform插入页面前对其修改)
css-loader (css模块化功能)(importLoaders解决css中@import未能处理loader的问题)
mini-css-extract-plugin(提取css至单独文件)

css预处理

less-loader
sass-loader
stylus-loader

css后处理

postcss
autoprefixer : 兼容浏览器样式
postcss-preset-env : 可设置目标css规范
cssnano是一个强大的 PostCss 插件,在 CSS 压缩优化中会经常被用到,它有别于常规的 CSS 压缩工具只是去除空格注释,还支持根据 CSS 语法解析结果智能压缩代码

静态资源处理

html处理

html-webpack-plugin : 打包并处理html文件至目标文件夹

文件处理

file-loader : 允许js加载其他资源;如, 图片,字体,媒体 等
url-loader : 在file-loader基础上封装;如, 允许将图片转成base64格式

tips: 注意3.0版本 esMoudle属性
图片

svg-url-loader 的工作原理类似于 url-loader,除了它利用 URL encoding 而不是 Base64 对文件编码。
可以借助img-webpack-loader来对使用到的图片进行优化。它支持 JPG、PNG、GIF 和 SVG 格式的图片。

// webpack.config.js
module.exports = {
    module: {
        rules: [
            {
                test: /\.(jpe?g|png|gif|svg)$/,
                loader: 'image-webpack-loader',
                // 这会应用该 loader,在其它之前
                enforce: 'pre'
            }
        ]
    }
};

通过enforce: 'pre'我们提高了 img-webpack-loader 的优先级,保证在url-loadersvg-url-loader之前就完成了图片的优化。
另外img-webpack-loader默认的配置就已经适用于日常开发图片的压缩优化需求了,但是如果你想更进一步去配置它,参考插件选项。要选择指定选项,请查看国外牛人写的一个图像优化指南

字体/媒体
{
    // 文件解析
    test: /\.(eot|woff|ttf|woff2|appcache|mp4|pdf)(\?|$)/,
    loader: 'url-loader',
    query: {
        // 这么多文件,ext不同,所以需要使用[ext]
        name: 'assets/[name].[hash:7].[ext]'
    }
},
数据

要导入 CSV,TSV 和 XML,可以使用csv-loaderxml-loader

 {
    test: /\.(csv|tsv)$/,
    use: [
    'csv-loader'
    ]
},
{
    test: /\.xml$/,
    use: [
    'xml-loader'
    ]
}
CDN部署
一般静态资源上线的时候都会放到 CDN,假设我们的 CDN 域名和路径为:http://bd.xxx.com/img/,这时候只需要修改output.publicPath即可
module.exports = {
    //..
    output: {
        publicPath: 'http://bd.xxx.com/img/'
    }
    //..
};

框架处理

vue

vue-loader / vue-style-loader

react

jsx
hot-module-replacement-plugin

// babelrc
presets: [
    // 添加 preset-react
    require.resolve('@babel/preset-react'),
    [require.resolve('@babel/preset-env'), {modules: false}]
],
// index.js
if (module.hot) {
    module.hot.accept(err => {
        if (err) {
            console.error('Cannot apply HMR update.', err);
        }
    });
}

辅助任务

本地开发调试

webapck-dev-server(错误遮罩,代理请求,热更新,重定向)
webpack.HotModuleReplacementPlugin 插件来开启全局的 HMR 能力
// webpack.config.js
const path = require('path');
module.exports = {
    entry: './src/index.js',
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        port: 9000,
        // 开启 hmr 支持
        hot: true,
        overlay: true, // 错误遮罩
        proxy: { // 接口代理
            ...
        }
    },
    plugins: [
        // 添加 hmr plugin
        new webpack.HotModuleReplacementPlugin()
    ]
};
// 在入口文件index.js最后添加如下代码
if (module.hot) {
    // 通知 webpack 该模块接受 hmr
    module.hot.accept(err => {
        if (err) {
            console.error('Cannot apply HMR update.', err);
        }
    });
}

错误定位

devtool开启sourceMap方便定位错误 : evl-source-map(开发) / source-map(生产)
module.exports = {
 ...
 devtool: 'evl'
}

代码风格检查

{
    test: /\.js$/,
    loader: 'eslint-loader',
    enforce: 'pre',
    include: [path.resolve(__dirname, 'src')], // 指定检查的目录
    options: { // 这里的配置项参数将会被传递到 eslint 的 CLIEngine
        formatter: require('eslint-friendly-formatter') // 指定错误报告的格式规范
    }
}
const StyleLintPlugin = require('stylelint-webpack-plugin');

module.exports = {
    // ...
    plugins: [new StyleLintPlugin(options)]
    // ...
};

优化方案

代码质量方向

代码分割

  • 多入口分割思路: 主业务代码 + 公共依赖 + 第三方依赖 + webapck运行代码
  • 单入口分割思路: 主业务代码 + 异步依赖 + 第三方依赖 + webpack运行代码
splitChunks

由于 Webpack 做到了开箱即用,所以splitChunks是有默认配置的:

module.exports = {
    // ...
    optimization: {
        splitChunks: {
            chunks: 'async', // 三选一: "initial" | "all" | "async" (默认)
            minSize: 30000, // 最小尺寸,30K,development 下是10k,越大那么单个文件越大,chunk 数就会变少(针对于提取公共 chunk 的时候,不管再大也不会把动态加载的模块合并到初始化模块中)当这个值很大的时候就不会做公共部分的抽取了
            maxSize: 0, // 文件的最大尺寸,0为不限制,优先级:maxInitialRequest/maxAsyncRequests < maxSize < minSize
            minChunks: 1, // 默认1,被提取的一个模块至少需要在几个 chunk 中被引用,这个值越大,抽取出来的文件就越小
            maxAsyncRequests: 5, // 在做一次按需加载的时候最多有多少个异步请求,为 1 的时候就不会抽取公共 chunk 了
            maxInitialRequests: 3, // 针对一个 entry 做初始化模块分隔的时候的最大文件数,优先级高于 cacheGroup,所以为 1 的时候就不会抽取 initial common 了
            automaticNameDelimiter: '~', // 打包文件名分隔符
            name: true, // 拆分出来文件的名字,默认为 true,表示自动生成文件名,如果设置为固定的字符串那么所有的 chunk 都会被合并成一个
            /** =========核心配置cacheGroups======= **/
            cacheGroups: { 
                vendors: {
                    test: /[\\/]node_modules[\\/]/, // 正则规则,如果符合就提取 chunk
                    priority: -10 // 缓存组优先级,当一个模块可能属于多个 chunkGroup,这里是优先级
                },
                default: {
                    minChunks: 2,
                    priority: -20, // 优先级
                    reuseExistingChunk: true // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的chunk,不会再重新产生一个
                }
            }
        }
    }
};

splitChunks默认配置对应的就是 chunk 生成的第二种情况:通过写代码时主动使用import()或者require.ensure来动态加载。

Tips:除了 JavaScript,splitChunks也适用于使用mini-css-extract-plugin插件的 css 配置。
runtimeChunk
Webpack 打包时,除了模块代码之外,Webpack 的 bundle 中还包含了 Runtime(运行时),这部分代码是一小段用来管理模块执行和加载的代码。
// webpack.config.js
module.exports = {
    optimization: {
        runtimeChunk: true
    }
};
// 分离webapck 运行时代码
打包分析

体积优化

js压缩

mode=production下,Webpack 会自动压缩代码,我们可以自定义自己的压缩工具,这里推荐 terser-webpack-plugin,它是 Webpack 官方维护的插件

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [new TerserPlugin({
            // 使用 cache,加快二次构建速度
            cache: true,
            terserOptions: {
                comments: false,
                compress: {
                    // 删除无用的代码
                    unused: true,
                    // 删掉 debugger
                    drop_debugger: true, // eslint-disable-line
                    // 移除 console
                    drop_console: true, // eslint-disable-line
                    // 移除无用的代码
                    dead_code: true, // eslint-disable-line
                    parallel: true   // 多线程
                }
            }
        })]
    }
};

作用域提升(Scope Hoisting)是指 webpack 通过 ES6 语法的静态分析,分析出模块之间的依赖关系,尽可能地把模块放到同一个函数中。

Tips:其实 webpack 4 中,在 production 模式下已经根据大多数项目的优化经验做了通用的配置,类似 Tree-Shaking、Scope Hoisting 都是默认开启的,而且最新版本的 Webpack 使用的压缩工具就是 terser-webpack-plugin。
tree-shaking

在 Webpack 中,Tree-Shaking 是需要配合mode=production来使用的,这是因为 Webpack 的 Tree-Shaking 实际分了两步来实现

  1. Webpack 自己来分析 ES6 Modules 的引入和使用情况,去除不使用的import引入;
  2. 借助工具(如 uglifyjs-webpack-pluginterser-webpack-plugin)进行删除,这些工具只在mode=production中会被使用。
// babelrc
presets: [
    [require.resolve('@babel/preset-env'), {modules: false}] // 防止babel转义模块,导致tree-shaking实效
],
css压缩

首先我们的 CSS 文件应该是导出到单独的 CSS 文件中,而不要直接打包到 JavaScript 文件中,然后通过style-loaderaddStyles方法添加进去,导出 CSS 文件就需要使用mini-css-extract-plugin这个插件

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[name].[contenthash:8].css'
        })
    ],
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: '../',
                            hmr: process.env.NODE_ENV === 'development'
                        }
                    },
                    'css-loader'
                ]
            }
        ]
    }
};

cssnano是基于 postcss 的一款功能强大的插件包,它集成了 30 多个插件,只需要执行一个命令,就可以对我们的 CSS 做多方面不同类型的优化,
在 Webapck 中,css-loader 已经集成了 cssnano,我们还可以使用optimize-css-assets-webpack-plugin来自定义 cssnano 的规则。optimize-css-assets-webpack-plugin 是一个 CSS 的压缩插件,默认的压缩引擎就是 cssnano。我们来看下怎么在 Webpack 中使用这个插件:

// webpack.config.js
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
    plugins: [
        new OptimizeCssAssetsPlugin({
            assetNameRegExp: /\.optimize\.css$/g,
            cssProcessor: require('cssnano'), // 这里制定了引擎,不指定默认也是 cssnano
            cssProcessorPluginOptions: {
                preset: ['default', {discardComments: {removeAll: true}}]
            },
            canPrint: true
        })
    ]
};
图片资源压缩

如果我们的项目中小图片特别多,例如有很多 icon 类的图标,这时候则推荐使用雪碧图(CSS Sprite)来合并这些小图到一张大图中,然后使用background-position来设置图片的位置,通过这样的方式可以节省多次小图片的请求。

// postcss.config.js
const postcssSprites = require('postcss-sprites');

module.exports = {
    plugins: [
        postcssSprites({
            // 在这里制定了从哪里加载的图片被主动使用css sprite
            // 可以约定好一个目录名称规范,防止全部图片都被处理
            spritePath: './src/assets/img/'
        })
    ]
};
// tips: 雪碧图的位置时根据图片原图的位置而确定的,在多端适配问题上可能会有问题

对于大图片来说,可以使用image-webpack-loader来压缩图片,image-webpack-loader 它支持 JPG、PNG、GIF 和 SVG 格式的图片,因此我们在碰到所有这些类型的图片都会使用它。

缓存优化

webpack.mode介绍
浏览器只会在文件名发生改变(或者浏览器缓存策略失效)时才会请求网络。在使用 Webpack 构建项目的时候,同样可以做到自动更新,但 Webpack 使用的不是版本号,而是指定哈希值(hash),Webpack 的 hash 值有三种:

  • hash:每次编译 Compilation 对象的 hash,全局一致,跟单次编译有关,跟单个文件无关,不推荐使用
  • chunkhash:chunk 的 hash,根据不同的 chunk 及其包含的模块计算出来的 hash,chunk 中包含的任意模块发生变化,则 chunkhash 发生变化,推荐使用
  • contenthash:CSS 文件特有的 hash 值,是根据 CSS 文件内容计算出来的,CSS 发生变化则其值发生变化,推荐 CSS 导出中使用

Javascript运行时优化

const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
 
const configuration = {};
 
module.exports = {
  // ...
  plugins: [
    new PrepackWebpackPlugin(configuration)
  ]
};

开发效率方向

项目本身相关

  • 减少依赖嵌套深度
  • 合理使用 polyfill,防止多余的代码
  • 使用 ES6 语法,尽量不使用具有副作用的代码,以加强 Tree-Shaking 的效果

Webpack相关

减少查找过程

使用 resolve.alias减少查找过程
使用 resolve.extensions 优先查找
合理配置 rule 的查找范围,设置inclue,exclude范围

module.exports = {
    resolve: {  
      // 顺序从前到后查找
      extensions: \['.js', '.jsx', '.ts', '.tsx', '.vue'\],  
      // 使用 alias 把导入 react 的语句换成直接使用单独完整的 react.min.js 文件,
      // 减少耗时的递归解析操作
      alias: {  
         react: path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
        '@assets': resolve('./src/assets')  
      }  
    },
    // 排除不需要解析的模块
    module: {
        noParse: /node_modules\/(jquey\.js)/;
    }
};
利用多线程提升构建速度

thread-loader : 是针对 loader 进行优化的,它会将 loader 放置在一个 worker 池里面运行,以达到多线程构建

// webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                include: path.resolve('src'),
                use: [
                    'thread-loader'
                    // 你的高开销的loader放置在此 (e.g babel-loader)
                ]
            }
        ]
    }
};

happypack : 利用多线程模型来提高构建速度 / 支持列表

// webpack.config.js
const os = require('os');
const HappyPack = require('happypack');
// 根据 cpu 数量创建线程池
const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                use: 'happypack/loader?id=jsx'
            },

            {
                test: /\.less$/,
                use: 'happypack/loader?id=styles'
            }
        ]
    },
    plugins: [
        new HappyPack({
            id: 'jsx',
            // 多少个线程
            threads: happyThreadPool,
            loaders: ['babel-loader']
        }),

        new HappyPack({
            id: 'styles',
            // 自定义线程数量
            threads: 2,
            loaders: ['style-loader', 'css-loader', 'less-loader']
        })
    ]
};
预编译处理

预先编译和打包不会变动存在的文件,在业务代码中直接引入,加快 Webpack 编译打包的速度,但是并不能减少最后生成的代码体积。
wepack.DllPlugin
add-asset-html-webpack-plugin可将dll处理文件自动插入html中

// webpack.config.dll.js
const webpack = require('webpack');
// 这里是第三方依赖库
const vendors = ['react', 'react-dom'];

module.exports = {
   mode: 'production',
   entry: {
       // 定义程序中打包公共文件的入口文件vendor.js
       vendor: vendors
   },
   output: {
       filename: '[name].[chunkhash].js',
       // 这里是使用将 verdor 作为 library 导出,并且指定全局变量名字是[name]_[chunkhash]
       library: '[name]_[chunkhash]'
   },
   plugins: [
       new webpack.DllPlugin({
           // 这里是设置 mainifest.json 路径
           path: 'manifest.json',
           name: '[name]_[chunkhash]',
           context: __dirname
       })
   ]
};
// webpack.config.js
const webpack = require('webpack');

module.exports = {
   output: {
       filename: '[name].[chunkhash].js'
   },
   entry: {
       app: './src/index.js'
   },
   plugins: [
       new webpack.DllReferencePlugin({
           context: __dirname,
           // 这里导入 manifest配置内容
           manifest: require('./manifest.json')
       })
   ]
};
压缩优化

terser-webpack-plugin : 开启多线程(parallel)和缓存(cache)

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    optimization: {
        minimizer: [
            new TerserPlugin({
                cache: true, // 开启缓存
                parallel: true // 多线程
            })
        ]
    }
};
缓存优化
如babel-loader中cacheDirectory
rules: [
    {
        test: /\.js$/,
        loader: 'babel-loader',
        options: {
            cacheDirectory: true
        },
        // 排除路径
        exclude: /node_modules/,
        // 查找路径
        include: [path.resolve('.src')]
    }
];
其他优化
适当选择sourceMap的devtool值
切换一些 loader 或者插件,比如:fast-sass-loader可以并行处理 sass 文件,要比 sass-loader 快 5~10 倍;

参考


JTR354
21 声望1 粉丝

读书点亮生活