美文网首页
Webpack4.0进阶

Webpack4.0进阶

作者: nimw | 来源:发表于2019-04-08 17:45 被阅读0次

    1. Tree Shaking

    1.1 JS Tree Shaking

    1.1.1 本地代码Tree Shaking

    1. 一个简单的打包示例

    (1) 打包入口代码
    src/index.js

    import { add } from './math'
    add(1,5)
    

    src/math.js

    export const add = (a, b) => {
      console.log(a + b)
    }
    
    export const minus = (a, b) => {
      console.log(a - b)
    }
    

    (2) 打包输出
    npm run bundle

    //...
    /*! exports provided: add, minus */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; });
    /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "minus", function() { return minus; });
    const add = (a, b) => {
      console.log(a + b);
    };
    const minus = (a, b) => {
      console.log(a - b);
    };
    //...
    

    (3) 问题分析
    src/index.js仅引入了add方法,但是却打包了add方法和minus方法。

    1. Tree Shaking
      tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。

    webpack 2.0及之后版本支持Tree Shaking
    webpack 3.X版本开启Tree Shaking方式与 webpack 4.X不同。
    Tree Shaking只支持ES Module模块引入方式。不支持commonjs模块引入方式。

    1. development模式开启Tree Shaking

    (1) 编辑打包配置文件
    webpack.dev.config.js

    optimization: {
       usedExports: true
    }
    

    (2) 将文件标记为side-effect-free(无副作用)
    编辑package.json

    "sideEffects": ["*.css"]
    

    side-effect-free数组中标记的文件即使没有通过ES Module,也会被打包输出。如果没有文件设置为side-effect-free,则sideEffects值设置为false

    (3) 打包输出

    /*! exports provided: add, minus */
    /*! exports used: add */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    
    "use strict";
    /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return add; });
    /* unused harmony export minus */
    const add = (a, b) => {
      console.log(a + b);
    };
    const minus = (a, b) => {
      console.log(a - b);
    };
    
    /***/
    
    1. production模式开启Tree Shaking
      生产模式自动开启Tree Shaking,无需设置optimization

    Tree Shaking开启的关键在于JavaScript代码压缩。在webpack3.X版本中,通过UglifyJsPlugin插件进行JavaScript代码压缩。在webpack4.X版本中,mode: production生产模式默认进行JavaScript代码压缩。

    1. 结论
      你可以将应用程序想象成一棵树。绿色表示实际用到的 source code(源码) 和library(库),是树上绿色的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。

    在以import { add } from './math'的方式引入模块时,Tree Shaking能够将'./math'中未被引入的模块过滤掉。

    1.1.2 Lodash Tree Shaking

    1. 编辑打包入口文件
      src/index.js
    import { join } from 'lodash';
    console.log(_.join(['1','2', '3'], '-'))
    
    1. 打包输出
         Asset       Size  Chunks             Chunk Names
    index.html  199 bytes          [emitted]  
       main.js   70.3 KiB       0  [emitted]  app
    

    只用到了lodash中的join方法,main.js包大小为0.3 KiB。很明显。Tree Shaking并没有生效。

    1. 安装依赖
      npm i babel-plugin-lodash -D
    2. 编辑打包配置文件
      webapck.dev.config.js
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader",
            options: {
              presets: [
                [ "@babel/preset-env", {"useBuiltIns": "usage", "corejs": 2}]
              ],
              plugins: [
                "lodash" //对lodash进行Tree Shaking
              ]
            }
          }
    
    1. 打包输出
         Asset       Size  Chunks             Chunk Names
    index.html  199 bytes          [emitted]  
       main.js   1.08 KiB       0  [emitted]  app
    

    经过Tree Shaking后,main.js包大小为1.08 KiB

    使用babel-plugin-lodash插件后,即使使用import lodash from 'lodash'方式引入lodashTree Shaking仍然生效。

    1.2 CSS Tree Shaking

    1. 安装依赖
      npm i -D purifycss-webpack purify-css glob-all
    2. 编辑打包配置文件
      webpack.dev.config.js
    const PurifyCSS = require('purifycss-webpack');
    const glob = require('glob-all');
    module.exports = {
    //...
      plugins: [
        new PurifyCSS({
          paths: glob.sync([
            path.join(__dirname, './src/*.js')
          ])
        })
      ]
    }
    
    1. 打包输出
      生成的css文件不包含./src/*.js中使用不到的样式。

    purify-csscss modules不可以同时使用。

    2. webpack-merge

    1. 安装依赖
      npm i webpack-merge -D
    2. 打包配置文件
      (1) build/webpack.base.config.js
    const path = require('path')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const CleanWebpackPlugin = require('clean-webpack-plugin')
    
    module.exports = {
      entry: {
        app: path.resolve(__dirname, '../src/index.js')
      },
      output: {
        publicPath: '',
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, '../dist')
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: "babel-loader"
          },
          {
            test: /\.css$/,
            use: [
              'style-loader',
              {
                loader: 'css-loader',
                options: {
                  modules: true,
                  localIdentName: '[path][name]__[local]--[hash:base64:5]'
                }
              },
              {
                loader: 'postcss-loader',
                options: {
                  ident: 'postcss',
                  plugins: [ require('autoprefixer')]
                }
              }
            ]
          },
          {
            test: /\.scss$/,
            use: [
              "style-loader",
              "css-loader",
              "sass-loader",
              {
                loader: 'postcss-loader',
                options: {
                  ident: 'postcss',
                  plugins: [ require('autoprefixer')]
                }
              }
            ]
          },
          {
            test: /\.html$/,
            use: [
              {
                loader: "html-loader",
                options: {
                  attrs: [':src', ':data-src']
                }
              }
            ]
          },
          {
            test: /\.(eot|ttf|svg|woff)$/,
            use: {
              loader: "file-loader",
              options: {
                name: '[name]-[hash:5].[ext]',
                outputPath: 'font/'
              }
            }
          },
          {
            test: /\.(png|svg|jpg|gif)$/,
            use: {
              loader:'url-loader',
              options: {
                name: '[name]-[hash:5].[ext]',
                outputPath: 'images/',
                limit: 4096
              }
            }
          }
        ]
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: path.resolve(__dirname, '../src/index.html')
        }),
        new CleanWebpackPlugin()
      ]
    }
    

    (2) build/webpack.dev.config.js

    const webpack = require('webpack');
    const merge = require('webpack-merge')
    const baseConfig = require('./webpack.base.config')
    
    const devConfig = {
      mode: "development",
      devtool: 'cheap-module-eval-source-map',
      optimization: {
        usedExports: true
      },
      devServer: {
        open: true, //浏览器自动打开
        port: 9000,
        contentBase: './dist',
        hot: true,
        //hotOnly: true
      },
      plugins: [
        new webpack.HotModuleReplacementPlugin()
      ]
    }
    
    module.exports = merge(baseConfig, devConfig)
    

    (3) build/webpack.prod.config.js

    const merge = require('webpack-merge')
    const baseConfig = require('./webpack.base.config')
    
    const prodConfig = {
      mode: "production",
      devtool: 'cheap-module-source-map',
    }
    
    module.exports = merge(baseConfig, prodConfig)
    

    webpack-merge可以对module.rules进行合并,但无法对单个rule中的loader进行合并。

    1. 创建打包命令
      package.json
      "scripts": {
        "build": "webpack --config ./build/webpack.prod.config.js",
        "dev": "webpack --config ./build/webpack.dev.config.js",
        "start": "webpack-dev-server --config ./build/webpack.dev.config.js",
      }
    

    3. js代码分割(Code Splitting)

    3.1 单独文件打包输出的缺点

    1. 安装lodash
      npm i --save lodash
    2. 编辑src/index.js
    import _ from 'lodash'
    console.log(_.join(['1','2', '3'], '-'))
    
    1. 打包分析
            Asset       Size  Chunks             Chunk Names
    app.bundle.js   1.38 MiB     app  [emitted]  app
       index.html  221 bytes          [emitted]  
    Entrypoint app = app.bundle.js
    

    lodash和业务代码打包到一个文件app.bundle.js。页面加载js耗时时间久。页面代码更新时,app.bundle.js全量更新。

    3.2 多入口实现分包

    1. 编辑打包配置文件
      entry: {
        lodash: path.resolve(__dirname, '../src/lodash.js'),
        app: path.resolve(__dirname, '../src/index.js')
      }
    
    1. 编辑src/lodash.js
    import lodash from 'lodash'
    window.lodash = lodash
    
    1. 编辑src/index.js
    console.log(window.lodash.join(['1','2', '3'], '-'))
    
    1. 打包分析
               Asset       Size  Chunks             Chunk Names
       app.bundle.js   29.1 KiB     app  [emitted]  app
          index.html  284 bytes          [emitted]  
    lodash.bundle.js   1.38 MiB  lodash  [emitted]  lodash
    Entrypoint lodash = lodash.bundle.js
    Entrypoint app = app.bundle.js
    

    entry为多入口时,入口文件顺序即是html模板引入对应输出文件的顺序。不同入口文件之间没有依赖关系

    3.3 SplitChunksPlugin配置

    3.3.1 同步代码分割

    1. 通过SplitChunksPlugin实现同步代码分割。

    webpack 4+支持SplitChunksPlugin

    1. 编辑打包配置文件
      webpack.base.config.js
      optimization: {
        splitChunks: {
          chunks: "all"
        }
      }
    
    1. 编辑src/index.js
    import _ from 'lodash'
    console.log(_.join(['1','2', '3'], '-'))
    
    1. 打包分析
      npm run dev
    Built at: 04/12/2019 9:37:25 AM
                    Asset       Size       Chunks             Chunk Names
            app.bundle.js   32.4 KiB          app  [emitted]  app
               index.html  289 bytes               [emitted]  
    vendors~app.bundle.js   1.35 MiB  vendors~app  [emitted]  vendors~app
    Entrypoint app = vendors~app.bundle.js app.bundle.js
    

    lodash打包输出代码被分割到vendors~app.bundle.js文件中。

    chunk表示打包输出模块,打包输出几个文件,chunks就有几个。
    同步代码分割可以通过浏览器缓存功能提升第二次页面加载速度。

    1. 指定代码分割打包输出文件名

    (1) 编辑打包配置文件

      output: {
        //...
        chunkFilename: '[name].chunk.js',
        //...
      }
    

    html页面中直接引入的资源文件(jscss)命名以 filename为规则。间接引用的资源文件命名以chunkFilename为规则

    (2) 打包分析
    npm run dev

    Built at: 04/12/2019 9:39:07 AM
                   Asset       Size       Chunks             Chunk Names
           app.bundle.js   32.4 KiB          app  [emitted]  app
              index.html  288 bytes               [emitted]  
    vendors~app.chunk.js   1.35 MiB  vendors~app  [emitted]  vendors~app
    Entrypoint app = vendors~app.chunk.js app.bundle.js
    

    3.3.2 异步代码分割

    1. 使用@babel/plugin-syntax-dynamic-import实现代码分割
    2. 安装依赖
      npm i @babel/plugin-syntax-dynamic-import -D
    3. 编辑babel配置
    "plugins": [
        "@babel/plugin-syntax-dynamic-import"
    ]
    
    1. 编辑src/index.js
    import('lodash').then(({default : _}) => {
      console.log(_.join(['1','2', '3'], '-'))
    })
    
    1. 打包分析
      npm run dev
    Built at: 04/12/2019 9:29:30 AM
            Asset       Size  Chunks             Chunk Names
      0.bundle.js   1.35 MiB       0  [emitted]  
    app.bundle.js   33.8 KiB     app  [emitted]  app
       index.html  221 bytes          [emitted]  
    

    webpack会自动对通过import()方法异步加载的模块进行代码分割。
    异步代码分割既可以通过浏览器缓存功能提升第二次页面加载速度,又可以通过懒加载的方式提升首次页面加载速度。

    1. 指定代码分割打包输出文件名
    import(/* webpackChunkName: "lodash" */'lodash').then(({default : _}) => {
    //...
    

    import()方法代码分割的底层还是通过SplitChunksPlugin实现的,splitChunks配置参数同样会影响import()方法代码分割情况。

    3.3.3 SplitChunksPlugin配置参数

    1. optimization.splitChunks默认配置项
    module.exports = {
      //...
      optimization: {
        splitChunks: {
          chunks: 'async',
          minSize: 30000,
          maxSize: 0,
          minChunks: 1,
          maxAsyncRequests: 5,
          maxInitialRequests: 3,
          automaticNameDelimiter: '~',
          name: true,
          cacheGroups: {
            vendors: {
              test: /[\\/]node_modules[\\/]/,
              priority: -10
            },
            default: {
              minChunks: 2,
              priority: -20,
              reuseExistingChunk: true
            }
          }
        }
      }
    };
    

    webpack4.X版本才支持SplitChunksPlugin。在webpack3.X中,使用CommonsChunkPlugin进行代码分割。

    1. optimization.splitChunks配置项说明
      optimization: {
        splitChunks: {
          chunks: 'all',
          //initial只对同步代码分割,async(默认)只对异步代码分割、all所有代码都做代码分割
          minSize: 30000,
          //大于30000Bit的模块才做代码分割
          maxSize: 0,
          //当模块大于maxSize时,会对模块做二次代码分割。当设置为0时,不做二次分割。
          minChunks: 1,
          //当打包输出chunks文件引用该模块的次数达到一定数目时才做代码分割。
          maxAsyncRequests: 5,
          //异步加载的js文件最大数目为边界条件进行代码分割
          maxInitialRequests: 3,
          //以初始加载的js文件最大数目为边界条件进行代码分割
          automaticNameDelimiter: '~',
          //代码分割生成文件连接符
          name: true,
          //代码分割生成文件自动生成文件名
          cacheGroups: {
            //代码分割缓存组,被分割代码文件通过缓存组输出为一个文件。
            vendors: {
              test: /[\\/]node_modules[\\/]/,
              //模块路径正则表达式
              priority: -10,
              //缓存组优先级,一个模块优先打包输出到优先级高的缓存组中。
              name: 'vendor'
              //代码分割打包输出文件名
            },
            lodash: {
              test: /[\\/]lodash[\\/]/,
              priority: -5,
            },
            jquery: {
              test: /[\\/]jquery[\\/]/,
              priority: -5,
            },
            default: {
              //默认缓存组,一般设置priority优先级数值最小
              minChunks: 2,
              priority: -20,
              reuseExistingChunk: true,
              //代码分割的模块A引用其他模块B,B已经被打包输出,则不再重新打包进入A
              name: 'common',
              chunks: 'all'
            }
          }
        }
      }
    

    optimization.splitChunks.chunks可设置,默认值为async,表示默认只对动态import()做代码分割。splitChunks.cacheGroups.{cacheGroup}.chunks同样可以设置,默认值为all,表示cacheGroups分组代码分割优先级高于import()

    3.4 懒加载(Lazy Loading)

    1. Lazy Loading文档。
    2. import()实现懒加载
    const lazyConsole = async () => {
      const {default : _} = await import(/* webpackChunkName: "lodash" */'lodash');
      console.log(_.join(['1','2', '3'], '-'))
    };
    
    document.addEventListener('click', lazyConsole)
    

    import()动态加载不仅可以实现代码分割,还可以实现懒加载。
    lodash模块生成的vendors~lodash.bundle.js文件在点击页面时才加载。

    只有配置chunkFilename之后,webpackChunkName才生效。
    如果多个 import()的魔法注释webpackChunkName指定同一个名字,则这多个import()模块会打包成一个bundle
    如果外部也引入了import()方法中引入的模块,则该模块不会分割单独打包。

    3.5 预取/预加载模块(prefetch/preload module)

    3.5.1 查看页面代码利用率

    1. chrome浏览器打开网页
    2. 打开调试面板
    3. commond + shift + p - show coverage - instrument coverage
      image.png
    4. 刷新网页


      代码利用率
    5. 分析结果
      红色表示加载并运行代码,绿色表示只加载未运行代码。
      可以看到该页面加载的每一个文件的利用率以及综合利用率。
      点击右侧横条,可以看到具体文件代码利用情况。


      image.png

    3.5.2 提高代码利用率

    1. 通过import()异步模块懒加载的方式可以提高首屏代码利用率。
    2. 未使用懒加载
      src/index.js
    document.addEventListener('click',  () => {
      const element = document.createElement('div');
      element.innerHTML = 'Dell Li';
      document.body.appendChild(element)
    });
    

    代码利用率为:77%

    image.png
    1. 通过懒加载
      src/index.js
    document.addEventListener('click',  () => {
      import('./click').then(({default: click}) => {
        click && click()
      })
    });
    

    src/click.js

    const handleClick = () => {
      const element = document.createElement('div');
      element.innerHTML = 'Dell Li';
      document.body.appendChild(element)
    };
    
    export default handleClick
    

    代码利用率为:81.5%

    image.png

    异步模块懒加载虽然可以减少首屏代码量,缩短网页首次加载时间,但等待用户交互后才请求对应js文件,会影响用户体验。可以通过prefetch/preload方式解决该问题。

    3.5.3 prefetch/preload module

    1. webpack v4.6.0+ 添加了预取和预加载(prefetch/preload module)的支持。
    2. 使用prefetch
      src/index.js
    document.addEventListener('click',  () => {
      import(/* webpackPrefetch: true */ './click').then(({default: click}) => {
        click && click()
      })
    });
    

    这会生成 <link rel="prefetch" href="1.bundle.js"> 并追加到页面头部,指示着浏览器在闲置时间预取1.bundle.js文件。

    1. prefetch / preload指令对比
    • preload chunk会在父chunk加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
    • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
    • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
    • 浏览器支持程度不同。

    4. CSS文件的代码分割

    4.1 现有CSS打包分析

    1. 打包配置
      webpack.base.config.js
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              'style-loader',
              {
                loader: 'css-loader',
                options: {
                  modules: true,
                  localIdentName: '[path][name]__[local]--[hash:base64:5]'
                }
              },
              {
                loader: 'postcss-loader',
                options: {
                  ident: 'postcss',
                  plugins: [ require('autoprefixer')]
                }
              }
            ]
          },
          {
            test: /\.scss$/,
            use: [
              "style-loader",
              "css-loader",
              "sass-loader",
              {
                loader: 'postcss-loader',
                options: {
                  ident: 'postcss',
                  plugins: [ require('autoprefixer')]
                }
              }
            ]
          }
        ]
      }
    
    1. 入口文件

    src/index.js

    import './style.css'
    

    src/style.css

    body {
      background: yellow;
    }
    
    1. 打包输出
      npm run build
    Built at: 04/12/2019 3:53:13 PM
                Asset       Size  Chunks             Chunk Names
        app.bundle.js   6.79 KiB       0  [emitted]  app
    app.bundle.js.map   3.04 KiB       0  [emitted]  app
           index.html  221 bytes          [emitted]  
    Entrypoint app = app.bundle.js app.bundle.js.map
    
    1. 存在的问题
      没有打包输出css文件,css代码被打包到js中。

    4.2 MiniCssExtractPlugin

    1. MiniCssExtractPlugin文档介绍
      该插件将CSS分割到文件中。对每个js文件中的css代码创建一个css文件。支持css按需加载和sourcemap
      MiniCssExtractPlugin不支持HMR(模块热更新),建议在生产环境中使用。

    webpack4版本中,我们之前首选使用的extract-text-webpack-plugin完成了其历史使命。推荐使用mini-css-extract-plugin

    1. 安装MiniCssExtractPlugin
      npm install --save-dev mini-css-extract-plugin
    2. webpack.base.config.js中对cssscss文件的loader处理移动到 webpack.dev.config.js中。
    3. 修改打包配置文件
      webpack.pro.config.js
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    //...
    module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              MiniCssExtractPlugin.loader,
              {
                loader: 'css-loader',
                options: {
                  modules: true,
                  localIdentName: '[path][name]__[local]--[hash:base64:5]'
                }
              },
              {
                loader: 'postcss-loader',
                options: {
                  ident: 'postcss',
                  plugins: [ require('autoprefixer')]
                }
              }
            ]
          },
          {
            test: /\.scss$/,
            use: [
              MiniCssExtractPlugin.loader,
              "css-loader",
              "sass-loader",
              {
                loader: 'postcss-loader',
                options: {
                  ident: 'postcss',
                  plugins: [ require('autoprefixer')]
                }
              }
            ]
          },
        ]
      },
      plugins: [
        new MiniCssExtractPlugin({})
      ]
    

    与之前的配置相比做了两点修改,一个是引入new MiniCssExtractPlugin({})插件,一个是MiniCssExtractPlugin.loader替换style-loader
    由于webpack-merge可以对module.rules进行合并,但无法对单个rule中的loader进行合并。所以在webpack.pro.config.js里写了完整的处理csssass文件的rule。也可以在webpack.base.config.js通过环境变量的逻辑进行判断添加MiniCssExtractPlugin.loader或者style-loader

    1. 打包输出
      npm run build
    Built at: 04/12/2019 4:16:56 PM
                Asset        Size  Chunks             Chunk Names
        app.bundle.js  1010 bytes       0  [emitted]  app
    app.bundle.js.map    3.04 KiB       0  [emitted]  app
              app.css    66 bytes       0  [emitted]  app
          app.css.map   170 bytes       0  [emitted]  app
           index.html   259 bytes          [emitted]  
    Entrypoint app = app.css app.bundle.js app.css.map app.bundle.js.map
    

    打包输出了css文件。

    如果没有打包输出css文件。原因可能是production自动开启Tree Shaking,需要将css文件标记为side-effect-free(无副作用)。
    "sideEffects": ["*.css"]

    1. css文件命名规则
    plugins: [
        new MiniCssExtractPlugin({
          filename: "[name].css",
          chunkFilename: "[id].chunk.css"
        })
    ]
    
    1. css文件压缩

    (1) 安装依赖
    npm install --save-dev optimize-css-assets-webpack-plugin
    (2) 编辑配置文件
    webpack.prod.config.js

    var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    
    const prodConfig = {
      //...
      optimization: {
        minimizer: [
          new OptimizeCssAssetsPlugin({})
        ]
      }
      //...
    }
    

    (3) 打包输出

    Built at: 04/12/2019 4:50:40 PM
                Asset       Size  Chunks             Chunk Names
        app.bundle.js    4.1 KiB       0  [emitted]  app
    app.bundle.js.map   3.66 KiB       0  [emitted]  app
              app.css   56 bytes       0  [emitted]  app
           index.html  259 bytes          [emitted]  
    Entrypoint app = app.css app.bundle.js app.bundle.js.map
    
    1. 可通过cacheGroups实现将所有js文件中的css打包到一个css文件中(Extracting all CSS in a single file)和将一个入口文件对应的所有css打包到一个css文件中(Extracting CSS based on entry)。

    5. 打包分析(bundle analysis)

    5.1 打包分析工具介绍

    1. 如果我们以分离代码作为开始,那么就应该以检查模块的输出结果作为结束,对其进行分析是很有用处的。
    2. 官方提供分析工具 是一个好的初始选择。下面是一些可选择的社区支持工具:
      (1) webpack-chartwebpack stats 可交互饼图。
      (2) webpack-visualizer:可视化并分析你的bundle,检查哪些模块占用空间,哪些可能是重复使用的。
      (3) webpack-bundle-analyzer:一个pluginCLI工具,它将bundle内容展示为便捷的、交互式、可缩放的树状图形式。
      (4) webpack bundle optimize helper:此工具会分析你的bundle,并为你提供可操作的改进措施建议,以减少bundle体积大小。

    5.2 官方分析工具

    1. analyse文档
    2. 编辑打包命令
      package.json
      "scripts": {
        "dev": "webpack  --profile --json > stats.json --config ./build/webpack.dev.config.js"
      }
    
    1. 打包输出
      npm run dev
      生成stats.json文件,该文件中包含打包信息。
    2. 使用analyse分析打包结果
      stats.json文件上传到analyse分析地址,即可看到打包细节信息。

    5.3 webpack-bundle-analyzer

    1. 安装依赖
      npm install --save-dev webpack-bundle-analyzer
    2. 编辑打包配置文件
      webpack.base.config.js
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    
    module.exports = {
      plugins: [
        new BundleAnalyzerPlugin()
      ]
    }
    
    1. 打包结果分析
      image.png
      如果打包生成的不同Asset引入了相同js文件,则说明该js文件被重复打包进两个不同的资源,需要修改配置将该js文件进行分割。

    6. Shimming

    shimming文档

    6.1 shimming 全局变量

    1. 一些第三方的库(library)可能会引用一些全局依赖(例如jQuery 中的 $)。这些“不符合规范的模块”就是 shimming 发挥作用的地方。
    2. 安装jquery
      npm i jquery lodash --save
    3. 修改打包配置文件
      webpack.base.config.js
      plugins: [
        new webpack.ProvidePlugin({
          $: 'jquery',
          join: ['lodash', 'join']
        })
      ]
    

    当有模块使用$时,会自动import $ from 'jquery'

    1. 可直接使用$
      src/index.js
    const dom = $('div')
    dom.html(join(['hello', 'world'], ' '))
    $('body').append(dom)
    

    shimmingalias对比:alias的作用是创建 importrequire 的别名,来确保模块引入变得更简单。shimming的作用是解决一些第三方的库(library)可能会引用的一些全局依赖。即:alias使模块引入更简单,不用写复杂路径;shimming使模块不用引入,使用全局变量的方式。

    6.2 imports-loader

    1. 打印现在模块中this指向
      src/index.js
    console.log(this === window); //false
    
    1. 安装依赖
      npm i imports-loader -D
    2. 编辑打包配置文件
      webpack.base.config.js
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              {loader: "babel-loader"},
              {loader: "imports-loader?this=>window"}
            ]
          }
        ]
      }
    
    1. 打印现在模块中this指向
      src/index.js
    console.log(this === window); //true
    

    项目中配置imports-loader?this=>window可能导致打包错误,'import' and 'export' may only appear at the top level (4:0)

    7. 环境变量

    1. 修改打包配置文件

    webpack.dev.config.js

    const devConfig = {
      //...
    }
    module.exports = devConfig
    

    webpack.prod.config.js

    const prodConfig = {
      //...
    }
    module.exports = prodConfig
    

    webpack.base.config.js

    const merge = require('webpack-merge')
    const devConfig = require('./webpack.dev.config')
    const prodConfig = require('./webpack.prod.config')
    
    const baseConfig = {
      //...
    }
    module.exports = (env) => {
      if(env && env.production) {
        return merge(baseConfig, prodConfig)
      } else {
        return merge(baseConfig, devConfig)
      }
    }
    
    1. 修改打包命令
      package.json
      "scripts": {
        "build": "webpack --env.production --config ./build/webpack.base.config.js",
        "dev": "webpack --config ./build/webpack.base.config.js",
        "start": "webpack-dev-server --config ./build/webpack.base.config.js"
      }
    

    这里的--env.production与打包配置文件中的env && env.production对应。
    如果使用--env.production=abc,则打包配置文件中需要使用env && env.production==='abc'的写法。
    如果使用--env production,则打包配置文件中需要使用env === 'production'的写法。

    8. TypeScript

    8.1 引入TypeScript

    1. 安装依赖
      ➜ webpack-operate npm i ts-loader typescript -D
    2. 项目根目录创建TypeScript配置文件
      tsconfig.json
    {
      "compilerOptions": {
        "module": "commonjs", //模块引入机制
        "target": "es5", //转化为es5
        "sourceMap": true, //支持sourceMap
        "allowJs": true //支持js引入
      },
      "exclude": [
        "node_modules"
      ]
    }
    
    1. 创建入口文件
      src/index.ts
    class Greeter {
        greeting: string;
        constructor(message: string) {
            this.greeting = message
        }
        greet() {
            return 'Hello' + this.greeting;
        }
    }
    
    let greeter = new Greeter('world')
    alert(greeter.greet())
    
    1. 编辑打包配置文件
      webpack.config.base.js
      entry: {
        app: path.resolve(__dirname, '../src/index.ts'),
      }
      //...
      module: {
        rules: [
          {
            test: /\.ts$/,
            exclude: /node_modules/,
            use:  "ts-loader"
          }
         ]
         //...
      }
    
    1. 打包输出
      npm run bundle

    8.2 对库代码进行编译检查

    1. 查询TypeScript支持编译检查的库。
    2. 对库代码进行编译检查——以lodash为例

    (1) 安装依赖
    ➜ webpack-operate npm i @types/lodash --save-dev
    (2) 修改ts文件
    src/index.ts

    import * as _ from 'lodash'
    
    class Greeter {
        greeting: string;
        constructor(message: string) {
            this.greeting = message
        }
        greet() {
            //return _.join(123) //传参不是数组,标红报错
            return _.join([ 'Hello', this.greeting], ' ');
        }
    }
    
    let greeter = new Greeter('world')
    alert(greeter.greet())
    

    9. Eslint

    9.1 使用eslint

    1. 安装依赖
      ➜ webpack-operate npm i eslint -D
    2. 初始化eslint配置文件
      npx eslint --init
      自动生成.eslintrc.js文件。
    ➜  webpack-operate npx eslint --init
    ? How would you like to use ESLint? To check syntax and find problems
    ? What type of modules does your project use? JavaScript modules (import/export)
    ? Which framework does your project use? React
    ? Where does your code run? (Press <space> to select, <a> to toggle all, <i> to invert selection)Browser
    ? What format do you want your config file to be in? JavaScript
    The config that you've selected requires the following dependencies:
    
    eslint-plugin-react@latest
    ? Would you like to install them now with npm? Yes
    
    1. 使用airbnb规则
      (1) 安装依赖
      ➜ webpack-operate npm install eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y babel-eslint -D
      (2) 修改.eslintrc.js配置文件
      "extends": "airbnb",
      "parser": "babel-eslint"
    
    1. 检查代码
      (1) 命令行检查方式
      npx eslint XXX(文件夹名字)
      (2) 编辑器检查方式
      image.png
    2. 使某些规则失效
      编辑.eslintrc.js规则文件
      "rules": {
        "no-unused-vars": 0
      }
    

    以上是在项目中使用eslint,与Webpack无关。

    9.2 Webpack中配置eslint

    1. eslint-loader 文档
    2. 安装依赖
      ➜ webpack-operate npm i eslint-loader -D
    3. 编辑打包配置文件

    webpack.base.config.js

      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              {loader: "babel-loader"},
              {loader: "eslint-loader"}
            ]
          }
        ]
      }
    

    webpack.dev.config.js

      devServer: {
        overlay: true
      }
    

    eslint-loader的作用是打包时先使用eslint检查规则,应该放在babel-loader之后。overlay的作用是使用webpack-dev-server打包时,将报错信息显示在页面上。

    1. 对不符合规范的代码进行简单修复
       {
         loader: "eslint-loader",
         options: {
           fix: true
         }
       }
    

    使用eslint-loader会在打包前对代码进行检查,降低打包效率。在实际项目开发中,一般使用eslintgit结合,在代码提交到git仓库时对代码进行检查。

    10. PWA

    1. 安装依赖
      ➜ webpack-operate npm i workbox-webpack-plugin -D
    2. 编辑生产环境打包配置文件
      webpack.prod.config.js
    const WorkBoxPlugin = require('workbox-webpack-plugin')
    
    var prodConfig = {
      //...
      plugins: [
        new WorkBoxPlugin.GenerateSW({
          clientsClaim: true,
          skipWaiting: true
        })
      ]
      //...
    }
    

    生产环境才需要使用PWA

    1. 编辑入口文件
      src/index.js
    //业务代码
    import('lodash').then(({default : _}) => {
      console.log(_.join(['1','2', '3'], '-'))
    })
    
    //使用serviceWorker
    if('serviceWorker' in navigator) { //如果浏览器支持serviceWorker
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/service-worker.js')
          .then(res => {
            console.log('serviceWorker registed')
          })
          .catch(err => {
            console.log('serviceWorker registe err')
          })
      })
    }
    

    service-worker.js文件在打包时生成。

    1. 打包输出
      npm run build
                                                    Asset       Size  Chunks             Chunk Names
                          2.6c02624b28028221db11.chunk.js    529 KiB       2  [emitted]  
                      2.6c02624b28028221db11.chunk.js.map    630 KiB       2  [emitted]  
                        app.051fb24e16eb3c7493d6.chunk.js  812 bytes       0  [emitted]  app
                    app.051fb24e16eb3c7493d6.chunk.js.map  783 bytes       0  [emitted]  app
                                               index.html  326 bytes          [emitted]  
    precache-manifest.a8a4feb9efc884fe5d31eed9b7b76ac0.js  445 bytes          [emitted]  
                   runtime.a366ecc84e6df04acf79.bundle.js   8.81 KiB       1  [emitted]  runtime
               runtime.a366ecc84e6df04acf79.bundle.js.map    8.8 KiB       1  [emitted]  runtime
                                        service-worker.js  927 bytes          [emitted]  
    Entrypoint app = runtime.a366ecc84e6df04acf79.bundle.js runtime.a366ecc84e6df04acf79.bundle.js.map app.051fb24e16eb3c7493d6.chunk.js app.051fb24e16eb3c7493d6.chunk.js.map
    
    1. 本地开启一个服务
    ➜  webpack-operate cd dist
    ➜  dist http-server
    Starting up http-server, serving ./
    Available on:
      http://127.0.0.1:8080
      http://192.168.1.3:8080
      http://192.168.57.1:8080
    Hit CTRL-C to stop the server
    
    1. 测试PWA
      打开http://127.0.0.1:8080,可以看到html页面。
      关闭服务,刷新浏览器,仍然可以正常访问页面。

    11. 编写并发布一个npm包

    1. 创建文件夹nmw-lodash
    2. 将项目初始化为一个npm
      ➜ nmw-lodash npm init -y
    3. 安装依赖
      ➜ nmw-lodash npm i webpack webpack-cli --save
      ➜ nmw-lodash npm i lodash --save
    4. 编写代码

    src/index.js

    import * as math from './math'
    import * as string from './string'
    
    export default {
      math,
      string
    }
    

    src/math.js

    export function add(a, b) {
      return a + b;
    }
    

    src/string.js

    import _ from 'lodash'
    
    export function join(a, b) {
      return _.join([a, b], ' ')
    }
    
    1. 创建并编辑打包配置文件
      webpack.config.js
    const path = require('path')
    
    module.exports = {
      mode: 'production',
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'nmw-lodash.js',
        library: "nmwLodash", //以script标签引入时,支持nmwLodash全局变量
        libraryTarget: "umd" //支持umd规范引入
        //libraryTarget: "global" nmwLodash挂载在global上。
      },
      externals: [
        "lodash" //不打包lodash
      ]
    }
    

    文档:output.libraryExport; output.library; externals

    1. 创建打包命令
      package.json
      "scripts": {
        "build": "webpack"
      }
    
    1. 修改npm包入口文件
      package.json
      "main": "./dist/nmw-lodash.js",
    
    1. 打包输出
      npm run build
    2. npm官网注册账号
    3. 添加账号密码
      ➜ nmw-lodash npm adduser
    4. 发布项目
      npm publish

    12. 打包性能优化

    12.1 优化配置

    1. 跟上技术的迭代
      NodeNpmYarn
    2. 在尽可能少的模块上应用Loader
      例如:使用excludeinclude
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: [{loader: "babel-loader"}]
    }
    

    exclude表示进行loader编译的路径。
    include表示进行loader编译的路径。

    1. Plugin尽可能精简并确保可靠
      例如:只在生产环境使用MiniCssExtractPluginCSS进行分割。
    var MiniCssExtractPlugin = require("mini-css-extract-plugin");
    //...
      plugins: [
        new MiniCssExtractPlugin({
          filename: "[name].css",
          chunkFilename: "[id].chunk.css"
        })
      ]
    
    1. resolve参数合理配置 (文档)
      (1) resolve.alias:创建 importrequire 的别名,来确保模块引入变得更简单。
      例如:import Utility from 'Utilities';
      (2) resolve.extensions:自动解析确定的扩展。能够使用户在引入模块时不带扩展。
      例如:import File from '../path/to/file';
      (3) resolve.mainFiles
      解析目录时要使用的文件名。
      例如:resolve配置如下
      resolve: {
        extensions: ['.js', '.jsx'],
        mainFiles: ['index'],  //默认配置
        alias: {
          child: path.resolve(__dirname, '../src/components/child')
        }
      }
    

    模块引入方式如下:

    import Child from 'child';
    

    resolve配置不宜过于复杂,否则会使模块查找时间增加,降低webpack打包速度。

    1. 控制包文件大小
      (1) 使用Tree Shaking
      (2) Code Splitting代码分割
    2. thread-loaderparallel-webpackhappypack多进程打包
    3. 合理使用SourceMap
    4. 结合state分析打包结果(bundle analysis)
    5. 开发环境内存编译(webpack-dev-server)
    6. 开发环境无用插件剔除

    12.2 DIIPlugin

    12.2.1 使用DIIPlugin

    1. 测试打包速度
      npm run build 打包耗时约950ms
    2. 第三方模块没有必要频繁重新打包。可以将第三方模块打包输出,webpack进行项目打包时,直接使用已经被打包的第三方模块,不再重新打包。
    3. 创建并编辑打包配置文件
      webpack.dll.config.js
    const path = require('path');
    const webpack = require('webpack')
    
    module.exports = {
      mode: 'production',
      entry: {
        vendors: ['react', 'react-dom', 'lodash'],
      },
      output: {
        filename: "[name].dll.js",
        path: path.resolve(__dirname, '../dll'),
        library: "[name]" //以vendors全局变量的方式暴露
      },
      plugins: [
        new webpack.DllPlugin({ //生成vendors.manifest.json映射文件
          name: '[name]',
          path: path.resolve(__dirname, '../dll/[name].manifest.json')
        })
      ]
    }
    
    1. 创建打包命令
      package.json
      "scripts": {
        //...
        "build:dll": "webpack --config ./build/webpack.dll.config.js",
        //...
      }
    
    1. 打包生成vendors
      npm run build:dll
      生成vendors.dll.js以及vendors.manifest.json映射文件。
    2. 安装依赖
      npm i add-asset-html-webpack-plugin -D
    3. 编辑打包配置文件
      webpack.base.config.js
    const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
    //...
      plugins: [
        //...
        new AddAssetHtmlPlugin({
          //将vendors.dll.js插入html模板中
          filepath: path.resolve(__dirname, '../dll/vendors.dll.js') 
        }),
          //打包代码时,第三方模块如果在vendors.manifest.json有映射,则直接在vendors全局变量中取。
        new webpack.DllReferencePlugin({ 
          manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
        })
      ],
    

    AddAssetHtmlPlugin插件必须放在HtmlWebpackPlugin后面。

    1. 测试打包速度
      npm run build 打包耗时约670ms
    2. 总结
      生成vendors包及映射 - 将vendors包插入html模板 - 以vendors全局变量暴露 - 使用vendors

    12.2.2 多个DIIPlugin

    1. 编辑dll包打包配置文件
      webpack.dll.config.js
      //...
      entry: {
        vendors: ['lodash'],
        react: ['react', 'react-dom']
      }
      //...
    
    1. 编辑打包配置文件
      webpack.base.config.js
      动态生成plugins数组。
    const fs = require('fs')
    
    const plugins =[
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, '../src/index.html')
      }),
      new CleanWebpackPlugin()
      //...
    ]
    
    const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
    //根据 dll 目录中生成的文件,添加对应插件。
    files.forEach(file => {
      if(/.*\.dll\.js/.test(file)) {
          //XXX.dll.js插入html模板中
          plugins.push(new AddAssetHtmlPlugin({ 
            filepath: path.resolve(__dirname, '../dll', file)
          }))
      }
      if(/.*\.manifest\.json/.test(file)) { 
        //根据XXX.manifest.json映射,直接在XXX全局变量中获取第三方模块。
        plugins.push(new webpack.DllReferencePlugin({
          manifest: path.resolve(__dirname, '../dll', file)
        }))
      }
    })
    

    13. 多页面打包配置

    13.1 介绍

    1. 多页面应用
      ① 生成多个html文件。
      ② 各个html文件引入对应的jsbundle
    2. 多页面应用的实现方式
      (1) 多配置
      对多个webpack配置分别打包,生成多个html页面。
      (2) 单配置
      对一个webpack配置进行打包,生成多个html页面。

    html-webpack-plugin文档

    13.2 多配置

    1. 技术基础
      (1) webpack打包可以接收一个配置数组。
      (2) parallel-webpack提高打包速度。

    直接使用webpack也可以接收一个配置数组,但串行打包过程速度比较慢。parallel-webpack可以并行打包,提高打包速度。

    1. 特点
      (1) 优点
      可以使用parallel-webpack提高打包速度。
      配置之间更加独立、灵活。
      (2) 缺点
      不能多页面之间共享代码。
    2. 创建编辑模板文件
      src/index.html
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title><%= htmlWebpackPlugin.options.title %></title>
    </head>
    <body>
    </body>
    </html>
    

    如果打包配置文件添加了html-loader,会正常解析html文件作为模版,就会直接把 <%= htmlWebpackPlugin.options.title %>解析成字符串。

    1. 创建编辑入口文件

    src/index.js

    console.log('this is index.js');
    

    src/list.js

    console.log('this list.js');
    
    1. 编辑打包配置文件
      webpack.pro.config.js
    const baseConfig = require('./webpack.base.config');
    //...
    const prodConfig = {
    //...
    }
    const buildConfig = merge(baseConfig, prodConfig);
    
    const generatePage = function (
      { entry = '',
        title = '',
        name = '',
        chunks = [],
        template = path.resolve(__dirname, '../src/index.html')
      } = {}) {
      return {
        entry,
        plugins: [
          new HtmlWebpackPlugin({
            chunks,
            template,
            title,
            filename: name + '.html'
          })
        ]
      }
    };
    
    const indexConfig = generatePage({
      entry: {
        app: path.resolve(__dirname, '../src/index.js')
      },
      title: 'page index',
      name: 'index',
      chunks: ['runtime','vendors','app']
    });
    
    const listConfig = generatePage({
      entry: {
        list: path.resolve(__dirname, '../src/list.js')
      },
      title: 'page list',
      name: 'list',
      chunks: ['runtime','vendors','list']
    });
    
    const pagesConfig = [indexConfig, listConfig];
    module.exports = pagesConfig.map(pageConfig => merge(pageConfig, buildConfig));
    

    多配置在同一个文件中,生成一个配置数组。
    ⭐️⭐️这里的chunks: ['runtime','vendors','app'/'list']可以省略,因为是多配置,默认会插入所有chunks。如果是13.3中的单配置,入口有多个,那么就必须指定插入的chunks

    1. 打包
      npm run build
    Hash: 10dc11f107c648d35db3659186349535b844a395
    Version: webpack 4.30.0
    Child
        Hash: 10dc11f107c648d35db3
        Time: 876ms
        Built at: 06/01/2019 4:38:36 PM
                Asset       Size  Chunks             Chunk Names
        app.bundle.js  963 bytes       0  [emitted]  app
           index.html  190 bytes          [emitted]  
    Child
        Hash: 659186349535b844a395
        Time: 850ms
        Built at: 06/01/2019 4:38:36 PM
                 Asset       Size  Chunks             Chunk Names
        list.bundle.js  962 bytes       0  [emitted]  list
             list.html  191 bytes          [emitted]  
    

    多配置打包不可以使用clean-webpack-plugin,否则后一个打包会清除前一个打包结果。

    1. 使用parallel-webpack打包
      (1) 安装
      npm i parallel-webpack -D
      (2) 打包
      ./node_modules/parallel-webpack/bin/run.js --config=build/webpack.prod.config.js

    13.3 单配置

    1. 特点
      (1) 优点
      可以共享各个entry之间的公用代码。
      (2) 缺点
      打包比较慢。
      输出的内容比较复杂。
      配置不够独立,相互耦合。例如:无法实现对不同入口设置不同的splitChunks代码分割规则、无法实现对不同入口设置不同的动态路由(splitChunks会将公共代码提出来,提前加载)。

    单配置时,webpack打包配置对不同入口的所有chunks都生效。只要有一个入口的同步代码依赖树中含有某一个模块,该模块就不会被动态路由异步加载。

    1. 编辑打包配置文件
      webpack.pro.config.js
    //...
    const buildConfig = merge(baseConfig, prodConfig);
    //...
    const pagesConfig = [indexConfig, listConfig];
    module.exports = merge(pagesConfig.concat(buildConfig));
    

    webpack-merge可以接收多个参数merge(object1, object2, object3, ...),也可以接收一个数组merge([object1, object2, object3, ...])

    1. 打包
      npm run build
    Version: webpack 4.30.0
    Time: 668ms
    Built at: 06/01/2019 4:54:57 PM
             Asset       Size  Chunks             Chunk Names
     app.bundle.js  963 bytes       0  [emitted]  app
        index.html  190 bytes          [emitted]  
    list.bundle.js  963 bytes       1  [emitted]  list
         list.html  191 bytes          [emitted]  
    Entrypoint app = app.bundle.js
    Entrypoint list = list.bundle.js
    

    相关文章

      网友评论

          本文标题:Webpack4.0进阶

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