一、基本要素

1、Entry/Output

1.1、单入口配置

module.exports = {
  entry: './src/index.js', // 打包的入口文件
  output: './dist/main.js', // 打包的输出
};

1.2、多入口配置

const path = require('path');

module.exports = {
  entry: {
    app: './src/app.js',
    admin: './src/admin.js',
  },
  output: {、
    filename: '[name].[hash].js', //通过占位符确保文件名称的唯一,可选择设置hash
    path: path.join(__dirname, 'dist'),
    // publicPath用于设置加载静态资源的baseUrl,例如prod模式下指向cdn,dev模式下指向本地服务
    publicPath: process.env.NODE_ENV === 'production' ? `//cdn.xxx.com` : '/',  // 
  },
};

2、Loaders

Loaders函数接收文件类型作为参数,返回转换的结果。目前webpack支持的两种类型分别为JSJSON,其它类型均需转换

2.1、通配Loaders

module:{
  rules:[
    {test:/.\(js|jsx|ts|tsx)$/,use:'ts-loader'} // 例如ts使用ts-loader
  ]
},

2.2、内联Loaders

Loaders 还可以直接内联到代码中使用:

import 'style-loader!css-loader!less-loader!./style.less';

2.3、多个Loaders

多个 Loaders 之间执行顺序是和 rules 配置相反的,即从右向左执行

2.3.1、源码逻辑

loader 先进后出,对应出栈顺序从右向左

if (matchResourceData === undefined) {
  for (const loader of loaders) allLoaders.push(loader);
  for (const loader of normalLoaders) allLoaders.push(loader);
} else {
  for (const loader of normalLoaders) allLoaders.push(loader);
  for (const loader of loaders) allLoaders.push(loader); // 入栈
}
for (const loader of preLoaders) allLoaders.push(loader); // pre loaders入栈
2.3.2、更改顺序

通过配置 enforce 改变执行顺序,enforce有四个枚举值,其执行顺序是prenormalinlinepost

module:{
  rules:[
     {
        test:/\.less$/,
        loader:'less-loader',
        enforce:'pre' // 预处理
    },
    {
        test: /\.less$/,
        loader:'css-loader',
        enforce:'normal' // 默认是normal
    },
    {
        test: /\.less$/,
        loader:'style-loader',
        enforce:'post' // 后处理
    },
  ]
},

3、Plugins

Plugins负责优化bundle文件、资源管理和环境变量注入,webpack 内置了很多 plugin。例如 DefinePlugin 全局变量注入插件、IgnorePlugin 排除文件插件、ProgressPlugin 打包进度条插件等

plugins: [new HtmlwebpackPlugin({ template: './src/index.html' })];

4、Mode

指定当前的构建环境,有三个选项,分别是:productiondevelopmentnone,当 mode 是 production 时会启用内置优化插件,比如TreeShakingScopeHoisting、压缩插件等

module.exports = {
  mode: 'production', // 会写入到环境变量NODE_ENV
};

也可以通过 webpack cli 参数设置

webpack --mode=production  

二、热更新

1、更新流程

热更新的原理

1.1、启动阶段 1 -> 2 -> A -> B

  • 通过WebpackCompileJS文件进行编译成Bundle
  • Bundle文件运行在Bundle Server,使得文件可通过localhost://xxx访问
  • 接着构建输出bundle.js文件给到浏览器

1.2、热更新阶段 1 -> 2 -> 3 -> 4

  • WebpackCompileJS文件进行编译成Bundle
  • Bundle文件运行在HMR Server
  • 一旦磁盘里面的文件修改,就将有修改的信息输出给HMR Runtime
  • 接着HMR Runtime局部更新文件的变化

2、配置方式

2.1、WDS + HotMoudleReplacementPlugin

2.1.1、WDS(webpack-dev-server)

WDS 提供了 bundle server 的能力,不输出文件,而是放在内存中,即生成的 bundle.js 文件可以通过 localhost://xxx 的方式去访问,同时它提供的livereload能力,使得浏览器能够自动刷新

// package.json
"scripts":{
  "dev":"webpack-dev-server --open"
}
2.1.2、HotMoudleReplacementPlugin 插件

HotMoudleReplacementPlugin插件给 WDS 提供了热更新的能力,源自它拥有局部更新页面能力的HMR Runtime。一旦磁盘里面的文件修改,HMR Server就将有修改的js module信息发送给HMR Runtime

// webpack.dev.js  仅在开发环境使用
module.exports = {
  mode: 'development',
  plugins: [new webpack.HotModuleReplacementPlugin()],
  devServer: {
    contentBase: './dist', //服务基础目录
    hot: true, //开启热更新
  },
};
2.1.3、交互逻辑

监听到文件修改时,HotMoudleReplacementPlugin 会生成一个 mainifest和 update file,其中 mainifest描述了发生变化的 modules ,紧接着webpack-dev-server通过 websocket 通知 client 更新代码,client 使用 jsonp 请求 server 获取更新后的代码

2.2、WDM(webpack-dev-middleware)

WDM 将 webpack 输出的文件传输给服务器,适用于灵活的定制场景

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

app.use(
  webpackDevMiddleware(compiler, {
    publicPath: config.output.publicPath,
  }),
);

app.listen(3000, function () {
  console.log('listening on port 3000');
});

三、文件指纹

文件指纹主要用于版本管理,表现于打包后文件名的后缀,如xxx//xxx_51773db.js中的51773db

1、三种类型

类型 含义
Hash 和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
Chunkhash 和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值
Contenthash 根据文件内容来定义 hash,文件内容不变,则 contenthash 不变

2、常用场景

  • 设置outputfilename,使用[chunkhash]
filename: '[name][chunkhash:8].js';
  • 设置MiniCssExtractPluginfilename,使用[contenthash]
new MiniCssExtractPlugin({
  filename: `[name][contenthash:8].css`,
});
  • 设置file-loadername,使用[hash]
rules: [
  {
    test: /\.(png|svg|jpg|gif)$/,
    use: [
      {
        loader: 'file-loader',
        options: {
          name: 'img/[name][hash:8].[ext]',
        },
      },
    ],
  },
];
// 占位符解释:[name]:文件名称,[ext]:资源后缀名

注意喔:hash是由代码和路径生成的。因此相同的代码在多台机器打包部署 hash 会不同,导致资源加载 404。一般通过一台机器打包,分发部署到不同机器

四、SourceMap

1、开启配置

开发环境开启,线上环境关闭。线上排查问题的时候可以将 source map 上传到错误监控系统

module.exports = {
  devtool: 'source-map',
};

2、类型

类型 说明
cheap-source-map 没有列号,只有行号,速度快
cheap-module-source-map 优化后的 cheap-source-map,避免 babel 等编译过代码行号对不上
eval 通过内联代码 eval 函数 baseURL 确定代码路径
eval-source-map sourcemap 放在 eval 函数后
inline-source-map 放在打包代码最后

3、文件格式

利用 mappings 映射表和 namessourcesContent 就可以还原出源码字符串

{
  "version": 3, // Source Map版本
  "file": "out.js", // 输出文件(可选)
  "sourceRoot": "", // 源文件根目录(可选)
  "sources": ["foo.js", "bar.js"], // 源文件列表
  "sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
  "names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
  "mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}

五、TreeShaking

  • 代码不会被执行,不可到达
  • 代码执行的结果不会被用到
  • 代码只会影响死变量(只写不读)

TreeShaking会将以上视为废弃的代码在uglify阶段消除

当 mode 设置为 production的情况下,是默认开启的。通过在.babelrc里设置modules:false进行取消

TreeShaking是利用 ES6 模块的特点进行清除

  • import 只能作为模块顶层的语句出现,且模块名只能是字符串常量

import 导入模块是静态加载,其获取的是变量引用,即当模块内部变更时,import出的变量也会变更。因此 import 不能出现在条件、函数等语句中( export类似),而 commonjs 中 require 获取的是模块的缓存

  • import bindingimmutable

六、模块机制

webpack打包后,会给模块加上一层包裹,import 会被转换成__webpack_require

webpack模块转换

1、匿名闭包

webpack打包后是一个匿名闭包,接收的参数 modules 是一个数组,每一项是一个模块初始化函数。通过__webpack_require加载模块,并返回modules.exports

webpack的模块机制

modules 的每个模块成员都是用 __webpack_require__ 加载的,installedModules 是加载模块的缓存,如果已经__webpack_require__加载过无需再次加载。

2、ScopeHoisting

构建后的代码存在大量的闭包代码,导致运行时创建的函数作用域增多,内存开销大,ScopeHoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突,从而减少函数声明代码和内存开销

七、SSR

SEO友好的服务端渲染SSR的核心是减少请求,从而减少白屏时间。其实现原理是:服务端通过react-dom/serverrenderToString方法将React组件渲染成字符串,返回路由对应的模版。协助的客户端通过打包,生成针对服务端的组件

renderToString 携带有 data-reactid 属性可配合 hydrate 使用,会复用之前节点只进行事件绑定从而优化首次渲染速度。类似的方法还有 renderToStaticMarkup

1、兼容问题

1.1、浏览器的全局变量

  • node.js中没有 document 和 window,需通过打包环境进行适配

在 react ssr 应用中,读取 document 和 window 可以在 useEffect 或 componentDidMount 中进行,当 nodejs 渲染时就会跳过这些执行,避免报错

  • 使用isomorphic-fetch 或 axios 替换 fetch和 xhr

1.2、样式问题

  • node.js 无法解析 css,可使用ignore-loader忽略 css 的解析

对于 antd 组件库,在babel-plugin-import 设置 style 为false

  • 使用 isomorphic-style-loader 替换 style-loader

2、两端协作

使用打包后的HTML为模板,服务端获取数据后替换占位符

<body>
  <div id="root">
    <!--HTML_PLACEHOLDER-->
  </div>
  <!--INITIAL_DATA_PLACEHOLDER-->
</body>

八、常见优化措施

1、代码压缩

1.1、JS 文件的压缩

  • 内置了uglifyjs-webpack-plugin

  • CommonsChunkPlugin 提取 chunks 中的公共模块减少总体积

1.2、CSS 文件的压缩

  • 使用optimize-css-assets-webpack-plugin,同时使用cssnano

  • extract-text-webpack-plugin将 css 从产物中分离。

1.3、html 文件的压缩

html-webpack-plugin 通常用来定义 html 模板,也可以设置压缩 minify 参数(production 模式下自动设置 true

1.4、图片压缩

使用image-webpack-loader

2、自动清理构建目录

利用 CleanWebpackPlugin 自动清理 output 指定的输出目录

3、静态资源内联

首屏渲染的样式尽量选择内联或使用 styled-components。资源内联可减少请求数,可避免首屏页面闪动,可进行相关上报打点,可初始化脚本

3.1、代码层面

  • raw-loader:js/html 内联
  • style-loader: css 内联

3.2、请求层面

  • url-loader:小图片或字体内联

  • file-loader:可以解析项目中的 url 引入路径,修改打包后文件引用路径,指向输出的文件。

4、基础库分离

4.1、HtmlWebpackExternalsPlugin

将基础包通过cdn,而不压缩进bundle

plugins: [
  new HtmlWebpackExternalsPlugin({
    externals: [
      {
        module: 'react',
        entry: '//11.url.cn/now/lib/15.1.0/react-with-addons.min.js?_bid=3123',
        global: 'React',
      },
    ],
  }),
];

4.2、SplitChunksPlugin

可将公共脚本、基础包以及页面公共文件分离

splitChunks:{
  chunks:'async',// async:异步引入的库进行分离(默认)  initial:同步引入的库进行分离 all:所有引入的库进行分离(推荐)
  ...
  cacheGroups:{
    // 1、公共脚本分离
    vendors:{
      test:/[\\/]node_modules[\\/]/,
      priority:-10
    },
    // 2、基础包分离
    commons:{
      test:/(react|react-dom)/,
      name:'vendors',
      chunks:'all'
    },
    // 3、页面公共文件分离
    commons:{
      name:'commons',
      chunks:'all',
      minChunks:2
    }
  }
}

4.3、分包

plugins: [
  // 使用DLLPlugin进行分包
  new webpack.DLLPlugin({
    name: '[name]',
    path: './build/library/[name].json',
  }),
  // DllReferencePlugin 对 manifest.json引用
  new webpack.DllReferencePlugin({
    manifest: require('./build/library/manifest.json'),
  }),
];

5、多进程多实例构建

多进程多实例构建,换句话说就是:每次webpack解析一个模块,将它及它的依赖分配给worker线程中,比如HappyPackThreadLoader

HappyPack工作流程

6、缓存

  • 开启缓存:babel-loaderterser-webpack-plugin
  • 使用cache-loaderhard-source-webpack-plugin

7、缩小构建目标、减少文件搜索范围

  • 合理配置 loader 的 test,使用 include 来缩小 loader 处理文件范围
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/, // 尾部补充$号表示尾部匹配
        use: ['babel-loader?cacheDirectory'], // babel-loader 通过 cacheDirectory 选项开启缓存
        include: path.resolve(__dirname, 'src'), // 只处理src目录下代码,极大提升编译速度。(如果node_modules下有未编译过的库,这里不建议开启)
      },
    ],
  },
};
  • 优化 resolve 配置:
module.exports = {
  resolve: {
    modules: [path.resolve(__dirname, 'node_modules')], // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    extensions: ['.js', '.json'], // extensions尽量少,减少文件查找次数
    noParse: [/\.min\.js$/], // noParse可以忽略模块的依赖解析,对于min.js文件一般已经打包好了
  },
};

九、可维护的 webpack 构建配置

1、多个配置文件管理不同环境的 webpack 配置

构建包功能设计

1.1、通过webpack-merge合并配置

merge = require('webpack-merge');
module.exports = merge(baseConfig, devConfig);

2、webpack 构建分析

2.1、日志分析

package.json文件的构建统计信息字段添加stats

"scripts":{
  "build:stats":"webpack --env production --json > stats.json"
}

2.2、速度分析

利用 speedMeasureWebpackPlugin分析整个打包总耗时和每个插件和loader的耗时情况

const speedMeasureWebpackPlugin = require("speed-measure-webpack-plugin")
const smp = new speedMeasureWebpackPlugin()
const webpackConfig = smp.wrap({
  plugins:[
    new MyPlugin()
    ...
  ]
})

2.3、体积分析

利用bundleAnalyzerPlugin分析依赖的第三方模块文件大小和业务里面的组件代码大小,构建完成后会在 8888 端口展示

const bundleAnalyzerPlugin = require('webpack-bundle-analyzer');
module.exports = {
  plugins: [
    new bundleAnalyzerPlugin({
      analyzerMode: 'server',
      analyzerHost: 'localhost',
      analyzerPort: 8888, // 端口号
      reportFilename: 'report.html',
      defaultSizes: 'parsed',
      openAnalyzer: true,
      generateStatsFile: false, // 是否输出到静态文件
      statsFilename: 'stats.json',
      statsOptions: null,
      logLevel: 'info',
    }),
  ],
};

2.4、编译时进度分析

利用ProgressPlugin分析编译进度和模块处理细节

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.ProgressPlugin({
      activeModules: false,
      entries: true,
      handler(percentage, message, ...args) {
        // 打印实时处理信息
        console.info(percentage, message, ...args);
      },
      modules: true,
      modulesCount: 5000,
      profile: false,
      dependencies: true, // 显示正在进行的依赖项计数消息
      dependenciesCount: 10000,
      percentBy: null,
    }),
  ],
};