美文网首页
webpack从配置到跑路v3

webpack从配置到跑路v3

作者: hellomyshadow | 来源:发表于2020-05-24 17:48 被阅读0次

    区分不同环境

    目前我们的webpack配置都定义在webpack.config.js中,我们可以通过 process.env.NODE_ENV 区分不同的环境,从而加载不同的配置。
    创建多个配置文件:

    • webpack.base.js 公共配置
    • webpack.dev.js 开发环境的配置
    • webpack.prod.js 生产环境的配置

    webpack-merge 专为webpack设计,提供了一个 merge 函数,用于连接数组、合并对象,根据不同的环境生成不同的配置文件。

    npm i webpack-merge -D
    //合并示例
    merge({
        devtool: 'cheap-module-eval-source-map',
        module: { rules: [{a: 1}] },
        plugins: [1,2,3]
    }, {
        devtool: 'none',
        mode: "production",
        module: { rules: [{a: 2}, {b: 1}] },
        plugins: [4,5,6],
    });
    --> 合并后:
    {
        devtool: 'none',
        mode: "production",
        module: { rules: [{a: 1}, {a: 2}, {b: 1}] },
        plugins: [1,2,3,4,5,6]
    }
    

    创建配置文件的目录config,存放配置文件webpack.base.config.js(公共配置)、webpack.dev.config.js(开发环境)、webpack.prod.config.js(生产环境)

    • webpack.dev.config.js
      const merge = require('webpack-merge');
      const baseConfig = require('./webpack.base.config.js');
      module.exports = merge(baseConfig, {
          mode: 'development',
          devtool: "cheap-module-eval-source-map",
          devServer: {
              //...
          }
          //...
      })
      
    • package.json 启动时指定配置文件
      "scripts": {
          "dev": "cross-env NODE_ENV=development webpack-dev-server --config=webpack.config.dev.js",
          "build": "cross-env NODE_ENV=production webpack --config=webpack.config.prod.js"
      },
      

    可以使用 merge 合并,也可以使用 merge.smart 合并。merge.smart 在合并loader时,会将同一匹配规则的进行合并。

    定义环境变量

    DefinePluginwebpack的一个内置插件,允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和生产模式的构建允许不同的行为非常有用。
    如果在预发布构建中执行日志记录,而不在线上构建中执行,则可以使用全局常量来决定是否记录日志。设置DefinePlugin,就可以忘记开发环境和生产环境构建的规则。

    每个传进DefinePlugin的键值都是一个标志符,或者多个用 . 连接起来的标志符。

    • 如果值是一个字符串,它会被当作一个代码片段来使用;
    • 如果值不是字符串,它会被转化为字符串(包括函数);
    • 如果值是一个对象,正常对象定义即可;
    • 如果在一个 key 前面加了 typeof,它会被定义为 typeof 调用。
    plugins: [
        new webpack.DefinePlugin({
            PRODUCTION: JSON.stringify(true),
            DEV: JSON.stringify('dev'),  // 字符串
            BROWSER_SUPPORTS_HTML5: true,
            FLAG: 'true',  // 这是个布尔类型
            TWO: '1+1',   // 这是个数值型
            'typeof window': JSON.stringify('object'),
            'process.env.firstName': JSON.stringify("hello webpack")
        })
    ]
    

    注意:因为这个插件直接执行文本替换,给定的值必须包含字符串本身内的实际引号。通常有两种方式来达到这个效果,使用 '"xxxx"', 或者使用 JSON.stringify('xxxx')。

    src/index.js 中使用这些变量

    console.log(PRODUCTION)  // true
    console.log(DEV)         // "dev"
    console.log(BROWSER_SUPPORTS_HTML5)   // true
    console.log(FLAG)         // true
    console.log(TWO)         // 2
    console.log(typeof window)         // "object"
    console.log(process.env.firstName)    // "hello webpack"
    if(PRODUCTION) {
       // ...
    } else {
       // ...
    }
    

    webpack解决跨域

    webpack可以通过配置代理 devServer.proxy 在前端解决跨域。

    //webpack.config.js
    module.exports = {
        //...
        devServer: {
            proxy: "http://xxx.yyy.zzz:4000"
        }
    }
    

    proxy 可以是一个字符串、数组、对象,有多种配置方式,适用各种场景。

    模拟数据

    模拟请求数据不再仅仅是后端的独家工作,前端一样可以!

    • 简单的数据模拟
    // webpack.config.js
    module.exports = {
        devServer: {
            before(app) {
                app.get('/user', (req, res) => {
                    res.json({info: 'hello webpack'})
                })
            }
        }
    }
    
    // src/index.js
    fetch("user")
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(err => console.log(err));
    // npm run dev  --> {info: 'hello webpack'}
    
    • mocker-api
      mocker-apiREST API 创建模拟 API。在没有实际 REST API 服务器的情况下测试应用程序时,它会很有用。

    mocker-api 的安装与使用

    npm i mocker-api -D
    
    • 创建 mock/mocker.js
    module.exports = {
        'GET /user': {name: 'webpack4'},
        'POST /login/account': (req, res) => {
            const { password, username } = req.body
            if (password === '888888' && username === 'admin') {
                return res.send({
                    status: 'ok',
                    code: 0,
                    token: 'sdfsdfsdfdsf',
                    data: { id: 1, name: 'webpack4' }
                })
            } else {
                return res.send({ status: 'error', code: 403 })
            }
        }
    }
    
    • webpack.config.js
    const apiMocker = require('mocker-api');
    module.export = {
        //...
        devServer: {
            before(app){
                apiMocker(app, path.resolve('./mock/mocker.js'))
            }
        }
    }
    
    • src/index.js
    // get
    fetch("user")
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(err => console.log(err));
    // post
    fetch("/login/account", {
        method: "POST",
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            username: "admin", password: "888888"
        })
    })
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(err => console.log(err));
    
    • npm run dev 查看控制台

    配置优化

    量化

    有时候我们以为的优化可能是负优化,希望能有一个量化的指标可以看出前后对比。
    speed-measure-webpack-plugin插件可以做速度分析,测量各个插件和loader所花费的时间;

    npm i -D speed-measure-webpack-plugin
    
    // webpack.config.js
    const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
    const smp = new SpeedMeasurePlugin();
    module.exports = smp.wrap({
        // ... webpack配置
    });
    

    输出信息:

    量化指标.png

    exclude/include

    exclude/include 配置loader时使用,确保loader尽可能少的处理文件。

    rules: [
        {
            test: /\.js[x]?$/,
            use: ['babel-loader'],
            include: [path.resolve(__dirname, 'src')]
        }
    ]
    

    exclude(排除)的优先级高于include(仅包含),在include/exclude中使用绝对路径的数组。
    结合量化指标,可以查看加上include/exclude和不加的构建速度。

    cache-loader

    对于一些性能开销比较大的loader,可以使用cache-loader,将结果缓存到磁盘中,默认保存在node_modueles/.cache/cache-loader目录下。
    cache-loader的配置很简单,只需要放在其他loader之前即可;

    npm i cache-loader -D
    
    // webpack.config.js
    rules: [
        {
            test: /\.jsx?$/,
            use: ['cache-loader', 'babel-loader']
        }
    ]
    

    如果只打算给babel-loader配置cache的话,可以不用cache-loader,给babel-loader增加选项cacheDirectory

    use: {
        loader: 'babel-loader',
        options: {
            cacheDirectory: true,  // 开启缓存
            // ...
        }
    }
    // 或者
    loader: 'babel-loader?cacheDirectory=true'
    

    cacheDirectory 默认为false。当有设置时,指定的目录将用来缓存loader的执行结果。之后的Webpack构建将会尝试读取缓存,来避免在每次执行时可能产生的、高性能消耗的Babel重新编译过程。设置true时,使用默认缓存目录node_modules/.cache/babel-loader

    happyPack

    在webpack构建过程中,有大量jscss、图片、字体等文件需要loader解析和处理,这些转换操作还不能并发处理文件,而是一个个文件进行处理;随着文件越来越多,构建速度必然会变慢。
    happyPack的基本原理是,将转换任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间;

    npm i happypack -D
    
    // webpack.config.js
    const happyPack = require('happypack');
    module.exports = {
        module: {
            rules: [
                {
                    test: /\.js$/,
                    exclude: /node_modules/,
                    loader: 'happypack/loader?id=js'
                },
                {
                    test: /\.css$/,
                    use: 'happypack/loader?id=css',
                    include: [
                        path.resolve(__dirname, 'src'),
                        path.resolve(__dirname, 'static')
                    ]
                }
            ],
        },
        plugins: [
            new HappyPack({
                id: 'js',  // 和rules中的id=js对应,标识Happypack处理哪类文件
                use: ['babel-loader'] //必须是数组,指定所使用的处理器Loader
                // loaders: ['babel-loader']
            }),
            new HappyPack({
                id: 'css',
                use: ['style-loader', 'css-loader','postcss-loader'],
            }),
        ]
    }
    

    happypack默认开启 CPU核数 - 1 个进程,当然也可以threadsHappypack

    注意:postcss-loader配置在Happypack中,必须要在项目中创建postcss.config.js,否则会抛出错误: Error: No PostCSS Config found

    module.exports = {
        plugins: [
            require('autoprefixer')()
        ]
    }
    

    thread-loader

    除了使用Happypack外,也可以使用thread-loader,把thread-loader放置在其他loader之前,那么后面的loader就会在一个单独的worker池中运行。
    worker(worker pool)中运行的loader是受到限制的,比如:

    • 这些loader不能产生新的文件;
    • 这些loader不能使用定制的loader API
    • 这些loader无法获取webpack的选项设置。
    npm i thread-loader -D
    
    // webpack.config.js
    rules: [
        {
            test: /\.jsx?$/,
            // babel-loader耗时比较长,所以给它配置 thread-loader
            use: ['thread-loader', 'babel-loader']
        }
    ]
    

    开启JS多进程压缩

    虽然很多webpack优化的文章上会提及多进程压缩的优化,不管是webpack-parallel-uglify-plugin或是uglifyjs-webpack-plugin配置parallel。其实没必要单独安装这些插件,它们并不会让Webpack构建速度提升。
    当前Webpack默认使用的是terser-webpack-pluginuglifyjs-webpack-plugin不支持ES6语法),默认就开启了多进程和缓存。构建时,项目中可以看到terser的缓存文件node_modules/.cache/terser-webpack-plugin

    HardSourceWebpackPlugin

    HardSourceWebpackPlugin为模块提供中间缓存,缓存默认存放路径是node_modules/.cache/hard-source
    配置hard-source-webpack-plugin,首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约80%

    npm i hard-source-webpack-plugin -D
    
    // webpack.config.js
    const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
    module.exports = {
        //...
        plugins: [
            new HardSourceWebpackPlugin()
        ]
    }
    

    HardSourceWebpackPlugin文档 中列出了一些可能会遇到的问题以及如何解决,例如热更新失效,或者某些配置不生效等。

    noParse

    如果一些第三方模块如jquery 、lodash 没有AMD/CommonJS规范版本,可以使用noParse来标识这个模块,这样Webpack会引入这些模块,但是不进行转化和解析,从而提升Webpack的构建性能。简而言之,忽略大型的library以提高构建性能。

    noParse属性的值是一个正则表达式或是一个function

    //webpack.config.js
    module.exports = {
        //...
        module: {
            noParse: /jquery|lodash/
        }
    }
    

    如果使用到了不需要解析的第三方依赖,那么配置 noParse 可以起到一定的优化作用。

    IgnorePlugin

    webpack的内置插件,作用是忽略第三方包指定的目录。
    比如 moment v2.24.0会将所有本地化内容和核心功能一起打包,那么就可以使用IgnorePlugin在打包时忽略本地化内容。

    //webpack.config.js
    module.exports = {
        //...
        plugins: [
            // 忽略 moment 下的 ./locale 目录
            new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
        ]
    }
    

    在使用时,如果需要指定语言,那么要手动引入语言包

    import moment from 'moment';
    import 'moment/locale/zh-cn'; // 手动引入中文语言
    

    index.js中只引入moment,打包出来的bundle大小为263KB;配置了IgnorePlugin,单独引入moment/locale/zh-cn,构建的包大小为55KB

    externals

    我们可以将一些JS文件存储在CDN上(减少webpack打包出来的js体积),在index.html中通过<script>标签引入。

    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    

    我们希望在使用时仍然可以通过 import 的方式去引用(import $ from 'jquery'),并且希望webpack不会对其进行打包,那么配置externals就好了。

    //webpack.config.js
    module.exports = {
        //...
        externals: {
            // jquery通过script引入之后,全局中即有了 jQuery 变量
            'jquery': 'jQuery'
        }
    }
    

    DllPlugin

    如果将所有的JS文件都打包成一个JS文件,必然会导致最终生成的JS文件很大,这个时候就需要考虑拆分bundles
    DllPluginDLLReferencePlugin 可以实现拆分 bundles,并且可以大大提升构建速度,而且它们都是webpack的内置模块。
    DllPlugin 可以把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下(版本号没变),动态库不需要重新打包,每次构建只重新打包业务代码,而不再把所有代码都重新构建一次。
    新建一个webpack配置文件webpack.config.dll.js,专门用于编译动态链接库,比如将reactreact-dom单独打包成一个动态链接库

    //webpack.config.dll.js
    const webpack = require('webpack');
    const path = require('path');
    
    module.exports = {
        entry: {
            react: ['react', 'react-dom']
        },
        mode: 'production',
        output: {
            filename: '[name].dll.[hash:6].js',
            path: path.resolve(__dirname, 'dist', 'dll'),
            library: '[name]_dll'  // 暴露给外部使用
            //libraryTarget  指定如何暴露内容,缺省时就是 var
        },
        plugins: [
            new webpack.DllPlugin({
                //name和library一致
                name: '[name]_dll', 
                 // manifest.json 的生成路径
                path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
            })
        ]
    }
    

    package.jsonscripts 中增加:

    "scripts": {
        // ...
        "build:dll": "webpack --config webpack.config.dll.js"
    },
    

    执行npm run build:all,生成的dist目录:

    dist
    └── dll
        ├── manifest.json
        └── react.dll.9dcd9d.js
    

    之所以将动态链接库单独放在 dll 目录下,主要是为了使用 CleanWebpackPlugin 更为方便的过滤掉动态链接库。
    manifest.json 用于让 DLLReferencePlugin 映射到相关依赖上。

    配置 webpack.config.js

    //webpack.config.js
    const webpack = require('webpack');
    const path = require('path');
    module.exports = {
        //...
        devServer: {
            contentBase: path.resolve(__dirname, 'dist')
        },
        plugins: [
            new webpack.DllReferencePlugin({
                manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
            }),
            new CleanWebpackPlugin({
                 // 不删除dll目录
                cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**']
            }),
            //...
        ]
    }
    

    使用 npm run build 构建,可以看到bundle.js的体积大大减少,构建速度也明显加快。
    修改 index.html,手动引入react.dll.js

    <script src="/dll/react.dll.9dcd9d.js"></script>
    

    抽离公共代码

    如果多个页面引入了一些公共模块,那么可以把这些公共模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来,避免重复下载。
    抽离公共代码对于对于单页面应用和多页应用在配置上没什么区别,都是配置在optimization.splitChunks中。

    //webpack.config.js
    module.exports = {
        optimization: {
            splitChunks: {  //分割代码块
                cacheGroups: {
                    vendor: {
                        // 第三方依赖,如 lodash
                        priority: 1,  //设置优先级,首先抽离第三方模块
                        name: 'vendor',
                        test: /node_modules/,
                        chunks: 'initial',
                        minSize: 0,
                        minChunks: 1 //最少引入了1次
                    },
                    //缓存组
                    common: {
                        // 公共模块,比如 utils.js
                        chunks: 'initial',
                        name: 'common',
                        minSize: 100,  //大小超过100个字节
                        minChunks: 3  //最少引入了3次
                    }
                }
            }
        }
    }
    

    如果打包出来的bundle.js体积过大,则可以将一些依赖打包成动态链接库,然后将剩下的一些第三方依赖拆出来。
    optimization.runtimeChunk 的作用是将包含chunk映射关系的列表从main.js中抽离出来,在配置splitChunk时,还应该配置runtimeChunk

    optimization: {
        runtimeChunk: {
            name: 'manifest'
        },
        splitChunks: {
            // ...
        }
    }
    

    最终构建出来的文件中会生成一个 manifest.js

    webpack-bundle-analyzer

    在做webpack构建优化时,vendor的体积超过1Mreactreact-dom 已经打包成了DLL
    所以需要借助分析插件 webpack-bundle-analyzer 查看下哪些包的体积较大

    npm install webpack-bundle-analyzer -D
    
    //webpack.config.js
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    module.exports = {
        //....
        plugins: [
            //...
            new BundleAnalyzerPlugin(),
        ]
    }
    

    执行 npm run build 构建,会默认打开 http://127.0.0.1:8888/,可以看到各个包的体积

    analyzer

    进一步对 vendor 进行拆分,将 vendor 拆分成了4个(使用 splitChunks 进行拆分即可)

    // webpack.config.js
        optimization: {
            concatenateModules: false,
            // ...
            splitChunks: {//分割代码块
                maxInitialRequests:6, //默认是5
                cacheGroups: {
                    vendor: {
                        //第三方依赖
                        priority: 1,
                        name: 'vendor',
                        test: /node_modules/,
                        chunks: 'initial',
                        minSize: 100,
                        minChunks: 1 //重复引入了几次
                    },
                    'lottie-web': {
                        name: "lottie-web", // 单独将 react-lottie 拆包
                        priority: 5, // 权重需大于`vendor`
                        test: /[\/]node_modules[\/]lottie-web[\/]/,
                        chunks: 'initial',
                        minSize: 100,
                        minChunks: 1 //重复引入了几次
                    },
                    // ...
                }
            },
        },
    // 重新构建
    npm run build
    

    webpack自身的优化

    • tree-shaking 如果使用ES6import语法,那么在生产环境下,会自动移除没有使用到的代码;
      //math.js
      const add = (a, b) => {
          console.log('aaaaaa')
          return a + b;
      }
      const minus = (a, b) => {
          console.log('bbbbbb')
          return a - b;
      }
      export { add, minus }
      
      //index.js
      import {add, minus} from './math';
      add(2,3);
      
      构建的最终代码里,minus 函数不会被打包进去。
    • scope hosting 作用域提升
      变量提升,可以减少一些变量声明。在生产环境下,默认开启。
      另外,大家测试的时候注意一下,speed-measure-webpack-pluginHotModuleReplacementPlugin 不能同时使用,否则会报错。

    相关文章

      网友评论

          本文标题:webpack从配置到跑路v3

          本文链接:https://www.haomeiwen.com/subject/tlzdahtx.html