升级webpack3+Vue项目到webpack5

8/25/2021 webpack

大致学习了 webpack5 后,准备将 webpack3 的老项目,升级到 webpack5

## 包升级

使用npm-check-updates (opens new window)插件实现项目包整体升级

# 安装ncu
npm install -g npm check-updates
# 使用ncu命令检测包
ncu
Checking package.json
[====================] 5/5 100%

 express           4.12.x  →   4.13.x
 multer            ^0.1.8  →   ^1.0.1
 react-bootstrap  ^0.22.6  →  ^0.24.0
 react-a11y        ^0.1.1  →   ^0.2.6
 webpack          ~1.9.10  →  ~1.10.5

# 运行ncu -u 更新package.json
ncu -u
# 然后使用npm install更新包
npm install
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意

  • ncu -u是对当前路径下的 package.json 进行更新,ncu -g是对全局 packages 进行更新
  • 如果有些不需要升级,就可以使用 filter 过滤ncu --filter [package],或者使用ncu [package]表示只更新某个包

如果实在不会就,一个个手动更新即可

# 更新后注意事项

  1. 更新后的webpack-dev-server需要使用webpack serve(之前是使用 webpack-dev-server)
{
  "scripts": {
    "dev": "webpack serve"
  }
}
1
2
3
4
5
  1. webpack-merge引入方式改变
const { merge } = require("webpack-merge");
1
  1.  在 webpack5 使用webpack-dev-server 开启  模块热替换后,browserslist 会跟其产生冲突导致 HMR 不可用,此时我们可以在开发环境将 target 设置为 web
// webpack.dev.conf.js
module.exports = {
  // ...
  target: "web",
};
1
2
3
4
5
  1. 配置迁移
devServer: {
- quiet: true
}

- new webpack.NamedModulesPlugin()
+ optimization:{
	  moduleIds: 'named',/* NamedModulesPlugin模块 迁移 */
  }

- new webpack.NoEmitOnErrorsPlugin()
+ optimization:{
	  emitOnErrors: true/* NoEmitOnErrorsPlugin模块迁移 */
  }
1
2
3
4
5
6
7
8
9
10
11
12
13

使用optimization.splitChunks代替CommonsChunkPlugin插件,来进行代码分离 使用TerserWebpackPlugin代替UglifyJsPlugin插件,来实现代码的压缩 使用mini-css-extract-plugin代替extract-text-webpack-plugin,实现 css 分离 使用css-minimizer-webpack-plugin代替optimize-css-assets-webpack-plugin,实现 css 压缩

webpack 配置还有一些属性的变化,可以根据对应的报错和查询文档来进行修改

# 性能优化

本来相较于 webpack3,webpack5 的性能就有很大提升,我们还可以从其他地方提升他

# 测试打包速率的插件

我们可以使用speed-measure-webpack-plugin插件对打包过程中用到的插件、loader 等所用的时间进行计算,方便我们进行性能优化

  1. 安装插件
yarn add speed-measure-webpack-plugin -D
1
  1. 使用插件
const SpeedMeasureWebpackPlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasureWebpackPlugin();

const webpackConfig = smp.wrap({
  plugins: [new MyPlugins()],
});
1
2
3
4
5
6

# 分析速率过慢问题

  1. babel-loader、vue-loader 等 loader 耗时太长
  2. 生成.map 文件时间过长
  3. node_modules 有些包体积太大,导致打包速率过慢

# 针对对应问题进行优化

  1. loader 耗时过长可以采用thread-loader开启多线程进行提速,由于使用时需要加载,可以提前预热
// 使用,将其放在其他loader前面
rules: [
  {
    test: /\.js$/,
    use: ["thread-loader", "babel-loader"],
  },
];

// 预热
const threadLoader = require("thread-loader");

const jsWorkerPool = {
  workers: 2,
  poolTimeout: 2000,
};

threadLoader.warmup(jsWorkerPool, ["babel-loader", "vue-loader"]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 使用 loader 的cacheDirectory或者cache-loader将 loader 编译储存
{
  test: /\.(m?js|jsx)$/,
  use: [
    'babel-loader?cacheDirectory'
  ]
},
1
2
3
4
5
6
{
  test: /\.(m?js|jsx)$/,
  use: [
    "cache-loader",
    'babel-loader'
  ]
},
1
2
3
4
5
6
7
  1. 代码分离,使用optimization.splitChunks属性进行 js 拆分
module.exports = {
  // ...
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendor",
          chunks: "all",
          priority: -10,
        },
        "async-vendors": {
          test: /[\\/]node_modules[\\/]/,
          minChunks: 2,
          chunks: "async",
          name: "async-vendors",
        },
      },
    },
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  1. 很多包可以直接使用 cdn,在减少打包的体积的同时,可以在各地更快的加载到对应的包,这里使用bootCDN (opens new window)
  • 首先根据 package.json 中的包的版本以及自己的设置,动态获取对应的 cdn
// build-vue/utils.js

// 国内免费cdn镜像源
exports.cdnBaseHttp = "https://cdn.bootcdn.net/ajax/libs";

// 需要配置镜像源的包
exports.externalConfig = [
  { name: "vue", scope: "Vue", js: "vue.min.js" },
  { name: "vue-router", scope: "VueRouter", js: "vue-router.min.js" },
  { name: "axios", scope: "axios", js: "axios.min.js" },
  {
    name: "ant-design-vue",
    scope: "antd",
    js: "antd.min.js",
    css: "antd.min.css",
  },
  { name: "jquery", scope: "jQuery", js: "jquery.min.js" },
];

// 获取模块版本号
exports.getModulesVersion = () => {
  let mvs = {};
  let regexp = /^npm_package_.{0,3}dependencies_/gi;
  for (let m in process.env) {
    // 从node内置参数中读取,也可直接import 项目文件进来
    if (regexp.test(m)) {
      // 匹配模块
      // 获取到模块版本号
      mvs[m.replace(regexp, "").replace(/_/g, "-")] = process.env[m].replace(
        /(~|\^)/g,
        ""
      );
    }
  }
  return mvs;
};

// 获取需要排除的包,组合cdn
exports.getExternalModules = (config) => {
  let externals = {}; // 结果
  let dependencieModules = this.getModulesVersion(); // 获取全部的模块和版本号
  config = config || this.externalConfig; // 默认使用utils下的配置
  config.forEach((item) => {
    // 遍历配置
    if (item.name in dependencieModules) {
      let version = dependencieModules[item.name];
      // 拼接css 和 js 完整链接
      item.css =
        item.css && [this.cdnBaseHttp, item.name, version, item.css].join("/");
      item.js =
        item.js && [this.cdnBaseHttp, item.name, version, item.js].join("/");
      externals[item.name] = item.scope; // 为打包时准备
    } else {
      throw new Error("相关依赖未安装,请先执行npm install " + item.name);
    }
  });
  return externals;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
  • 然后在html-webpack-plugin中配置自定义属性,用于在index.html中加载,同时排除不到包的项
// build.vue/webpack.prod.conf.js
const externalConfig = JSON.parse(JSON.stringify(utils.externalConfig)); // 读取配置
const externalModules = utils.getExternalModules(externalConfig); // 排除不打包的项

module.exports = {
  // ...
  externals: externalModules,
  plugins: [
    new HtmlWebpackPlugin({
      // ...
      cdnConfig: externalConfig, // cdn配置
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 最后在index.html中使用ejs语法插入对应的 cdn
<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8" />
    <meta name="renderer" content="webkit" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1.0,maximum-scale=1.0, user-scalable=no"
    />
    <% htmlWebpackPlugin.options.cdnConfig &&
    htmlWebpackPlugin.options.cdnConfig.forEach(function(item){ if(item.css){ %>
    <link href="<%= item.css %>" rel="stylesheet" />
    <% }}) %>
  </head>
  <body>
    <div id="app"></div>
    <% htmlWebpackPlugin.options.cdnConfig &&
    htmlWebpackPlugin.options.cdnConfig.forEach(function(item){ if(item.js){ %>
    <script type="text/javascript" src="<%= item.js %>"></script>
    <% }}) %>
    <!-- built files will be auto injected -->
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

温馨提示:github 有一个开源的 plugin,可以完成直接完成上诉的内容 -- webpack-cdn-plugin (opens new window)

  1. 在 js 和 css 压缩时开启parallel属性,多线程进行压缩
new TerserWebpackPlugin({
  parallel: true,
});
new CSSMinimizerWebpackPlugin({
  parallel: true,
});
1
2
3
4
5
6

# 配置总结

# 配置结构图

├── build-vue/ ....................... build文件夹
│   ├── utils.js ..................... 通用工具
│   ├── webpack.base.conf.js ......... base配置
│   ├── webpack.dev.conf.js .......... 开发配置
│   └── webpack.prod.conf.js ......... 生产配置
├── config/ .......................... 设置文件夹
│   ├── index.js ..................... 开发和生成的可变配置
│   ├── dev.env.js ................... process.env(开发)
│   └── prod.env.js .................. process.env(生产)
└── package.json ..................... 模块包配置文件
1
2
3
4
5
6
7
8
9
10

# 配置代码

build-vue/utils.js

const path = require("path");
/* 可变配置 */
const config = require("../config");
/* 该插件的主要是为了抽离css样式,防止将样式打包在js中引起页面样式加载错乱的现象 */
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
/* 项目包信息 */
const packageConfig = require("../package.json");

/* 把资源文件放到目录下 */
exports.getAssetsPath = function(_path) {
  const assetsSubDirectory =
    process.env.NODE_ENV === "production"
      ? config.build.assetsSubDirectory
      : config.dev.assetsSubDirectory;

  return path.posix.join(assetsSubDirectory, _path);
};
exports.cssLoaders = function(options) {
  options = options || {};

  /* 解析.css文件 */
  const cssLoader = {
    loader: "css-loader",
    options: {
      sourceMap: options.sourceMap,
      importLoaders: 0,
    },
  };

  /* 解析css,添加前缀等 */
  const postcssLoader = {
    loader: "postcss-loader",
    options: {
      sourceMap: options.sourceMap,
    },
  };

  // 生成对应的loader配置
  const generateLoaders = (loader, loaderOptions) => {
    // 添加importLoaders属性,因为如果通过import插入css/less等,可能会导致不解析,需要通过该属性重新解析
    // 如果要后面的loader都重新处理一次,就要每添加一个loader就加1
    if (options.usePostCSS) {
      cssLoader.options.importLoaders += 1;
    }
    /* 是否使用解析css */
    const loaders = options.usePostCSS
      ? [cssLoader, postcssLoader]
      : [cssLoader];

    if (loader) {
      loaders[0].options.importLoaders += 1;
      /* 存在loader,往里面添加loader的解析 */
      loaders.push({
        loader: loader + "-loader",
        /* options的配置基础配置与传递参数配置合并 */
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap,
        }),
      });
    }
    
    // 当指定该选项时就会提取CSS到单独文件
    if (options.extract) {
      return [{ loader: MiniCssExtractPlugin.loader }].concat(loaders);
    } else {
      return ["thread-loader", "vue-style-loader"].concat(loaders);
    }
  };

  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders("less"),
    // sass: generateLoaders('sass', { indentedSyntax: true }),
    // scss: generateLoaders('sass'),
    // stylus: generateLoaders('stylus')
  };
};

exports.styleLoaders = function(options) {
  const output = [];
  /* 预处理css类型有stylus scss sass postcss css */
  const loaders = exports.cssLoaders(options);

  /* 把所有类型进行 */
  for (const extension in loaders) {
    const loader = loaders[extension];
    output.push({
      test: new RegExp("\\." + extension + "$"),
      use: loader,
    });
  }

  return output;
};

// 错误信息回调
exports.createNotifierCallback = () => {
  const norifier = require("node-notifier"); // 系统级别的消息

  return (serverity, errors) => {
    if (severity !== "error") return; // 类型不是error就直接return

    const error = errors[0]; // 获取第一个错误
    const filename = error.file && error.file.split("!").pop();

    notifier.notify({
      title: packageConfig.name, // 项目名称
      message: severity + ": " + error.name, // 级别+错误信息
      subtitle: filename || "", // 错误文件
      icon: path.join(__dirname, "logo.png"), // logo
    });
  };
};

/* 配置cdn */

// 国内免费的cdn镜像源
exports.cdnBaseHttp = "http://cdn.bootcdn.net/ajax/libs";

// 需要配置镜像源的包
exports.externalConfig = [
  { name: "vue", scope: "Vue", js: "vue.min.js" },
  { name: "vue-router", scope: "VueRouter", js: "vue-router.min.js" },
  { name: "axios", scope: "axios", js: "axios.min.js" },
  {
    name: "ant-design-vue",
    scope: "antd",
    js: "antd.min.js",
    css: "antd.min.css",
  },
  { name: "jquery", scope: "jQuery", js: "jquery.min.js" },
];

// 获取模块版本号
exports.getModulesVersion = () => {
  let mvs = {};
  let regexp = /^npm_package_.{0,3}dependencies/gi;
  for (let m in process.env) {
    // 从node内置参数中读取,也可直接import项目文件进行
    if (regexp.test(m)) {
      // 匹配模块
      // 获取到模块版本号
      mvs[m.replace(regexp, "").replace(/_/g, "-")] = process.env[m].replace(
        /~|\^/g,
        ""
      );
    }
  }
  return mvs;
};

// 获取需要排除的包,组合cdn
exports.getExternalModules = (config) => {
  let externals = {}; // 结果
  let dependencieModules = this.getModulesVersion(); // 获取全部的模块和版本号
  config = config || this.externalConfig; // 默认使用utils下的配置
  config.forEach((item) => {
    // 遍历配置
    if (item.name in dependencieModules) {
      let version = dependencieModules[item.name];
      // 拼接css 和 js 完整链接
      item.css =
        item.css && [this.cdnBaseHttp, item.name, version, item.css].join("/");
      item.js =
        item.js && [this.cdnBaseHttp, item.name, version, item.js].join("/");
      externals[item.name] = item.scope; // 为打包时准备
    } else {
      throw new Error("相关依赖未安装,请先执行npm install " + item.name);
    }
  });
  return externals;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
Last Updated: 1/21/2025, 10:16:53 AM