美文网首页
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