前言
webpack5正式版已发布2个月,相信大家已经想迫不及待升级,体验一下"真香"功能,我们团队主要构建工具目前已经升级到最新webpack5.x+babel7.x,历经一个月,踩了许多坑。经过上手使用,webpack5打包体积大小,持续编译速度都有很不错的提升,对webpack4兼容也很平缓,Module Federation也对项目中如何使用微前端应用提供一种解决方案。
重要升级点
- 1、Improve build performance with Persistent Caching(通过持久缓存提高构建性能)
- 2、Improve Long Term Caching with better algorithms and defaults(使用更好的算法和默认值选项改善长期缓存)
- 3、Improve bundle size with better Tree Shaking and Code Generation(通过更好的Tree Shaking和代码生成来改善生成包大小)
- 4、Clean up internal structures that were left in a weird state while implementing features in v4 without introducing any breaking changes(在不引入任何重大更改的前提下,实现v4版本中的功能的同时清理处于异常状态的内部结构。例如不再为 Node.js 模块自动引用 Polyfills)
- 5、Improve compatibility with the web platform(改善与Web平台的兼容性)
- 6、Module Federation (联邦模块)
- 7、点击查看webpack5更多升级日志 升级工作开始前,建议你先阅读从v4升级到v5准备工作。最新的官方文档要求node至少在10.13.0版本以上
升级babel
babel7初探这篇文章对babel7及各插件的介绍很详细了,不再过多介绍了,如果你团队还是使用babel6,请阅读这篇,下面重点说下polyfill方案,目前主要有以下两种:
- 方案一是全局入口文件头部引入
import "@babel/polyfill";
或者在webpack入口entry: ["@babel/polyfill", "./src/index.js"]
- 方案二是
@babel/plugin-transform-runtime
和@babel/runtime
,我们使用的是此方案@babel/polyfill会造成全局变量的污染,所以适用于应用开发,@babel/runtime 不会造成全局变量的污染,适用于库的开发。现在的实现方式有了很大的区别。@babel/polyfill 在7.4 以后被废弃了。 .babelrc文件配置,或者你可以配置到webapck.congig.js的babel-loader下:
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"> 5%",
"IE 10",
"iOS 7",
"Firefox > 20"
]
},
"useBuiltIns": "usage", // 按需加载polyfill
"corejs": 3
}
]
],
"plugins": [
["@babel/plugin-transform-runtime"]
]
}
复制代码
package.json中的依赖
"dependencies": {
"@babel/runtime": "^7.12.1",
"core-js": "^3.8.1",
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/plugin-transform-arrow-functions": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@babel/runtime-corejs3": "^7.12.1",
"babel-loader": "^8.1.0"
}
复制代码
升级webpack
npm i weback@latest webpack-cli@latest -D
复制代码
其他通用api:entry, output, module
几乎没有变化,说一下webpack5升级后的几个点:
target升级
之前webpack4只是粗暴的支持两种:target: 'web'
,target: 'node'
现在webpack5默认是['web', 'es6']
,也就是说会默认打包出es6产出,如下图:
这种剪头函数会在不兼容的浏览器(IE)直接报错,当然这对于不考虑兼容旧版浏览器的业务来说是一个利好,可以直接使用体积更小性能更好的es6代码,毕竟IE浏览器也在一步步淘汰了。webpack5已支持多重配置,支持字符串后加版本号
target: 'node12.18'
,还可以传一个数组,比如:target: ['web', 'es5']
就可以解决上面的问题,还可以传browserslist
,更多查看文档
更彻底的tree-shaking
- 1、webpack5现在能够处理对嵌套模块的 tree shaking,举一个例子来说明:
// a.js
export const name = 'JavaScript';
export const age = "Brendan Eich于1995年创造了我,我都25岁了";
// b.js
import * as a from './a';
export { a };
// index.js
import * as b from './b';
console.log(b.a.name);
复制代码
webpack4打包后是这样:
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)}([function(e,n,t){"use strict";t.r(n);var r={};t.r(r),t.d(r,"name",(function(){return o})),t.d(r,"age",(function(){return u}));const o="JavaScript",u="Brendan Eich于1995年创造了我,我都25岁了";console.log(r.name)}]);
复制代码
webapck5打包后:
(()=>{"use strict";console.log("JavaScript")})();
复制代码
- 2、webpack 现在能够分析多个模块之间的关系
import { something } from './something';
function usingSomething() {
return something;
}
export function test() {
return usingSomething();
}
复制代码
当设置了"sideEffects": false
时,一旦发现test方法没有使用,不但删除test,还会删除 "./something"
- 3、曾经,webpack不支持对 CommonJs进行 导出和 require() 调用时的导出使用分析。现在,webpack 5 增加了对一些 CommonJs 构造的支持,允许消除未使用的 CommonJs 导出,并从 require() 调用中跟踪引用的导出名称,支持的构造如下:
exports|this|module.exports.xxx = …
exports|this|module.exports = require("…") (reexport)
exports|this|module.exports.xxx = require("…").xxx (reexport)
Object.defineProperty(exports|this|module.exports, “xxx”, …)
require(“abc”).xxx
require(“abc”).xxx()
从 ESM 导入
require() 一个 ESM 模块
被标记的导出类型 (对非严格 ESM 导入做特殊处理)
未来计划支持更多的构造
复制代码
持久化缓存
在webpack5之前,可以使用cache-loader
、hard-source-webpack-plugin
将编译结构写入硬盘缓存,还可以使用babel-loader
,设置option.cacheDirectory
将babel-loader
编译的结果写进磁盘。在webpack5中,默认开启缓存,缓存默认是在内存里。建议在开发环境启动,文件二次修改后可体验到飞一般的打包速度。另外可以对cache
进行设置:
module.export={
cache {
type:'filesystem', // 'memory' | 'filesystem'
name: 'umd-cache', // 分别命名,对于打多种包比较友好
cacheDirectry: 'node_modules/.cache/webpack', // 默认将缓存存储在 node_modules/.cache/webpack
// 缓存依赖,当缓存依赖修改时,缓存失效
buildDependencies:{
// 将你的配置添加依赖,更改配置时,使得缓存失效
config: [__filename]
}
}
}
复制代码
新增Asset Modules
webapck5已经内置了file-loader、url-loader、raw-loader
,只不过需要用asset
替换
asset/resource
emits a separate file and exports the URL. Previously achievable by usingfile-loader
.asset/inline
exports a data URI of the asset. Previously achievable by usingurl-loader
asset/source
exports the source code of the asset. Previously achievable by usingraw-loader
// 举例:svg编写到代码中
{
test: /\.svg/,
type: 'asset/source',
use: [
{
loader: 'svgo-loader', // 压缩svg
options: {
plugins: [
{removeTitle: true},
{convertColors: {shorthex: false}},
{convertPathData: false}
]
}
}
]
}
复制代码
import警告
如果直接在代码中有这样引入的话,webpack会报一个警告
import { version } from '../package.json';
复制代码
修改为default引入
import packageInfo from '../package.json';
const { version } = packageInfo;
复制代码
SplitChunk和模块大小
模块现在能够以更好的方式表示大小,而不是显示单个数字和不同类型的大小。 默认情况下,只能处理 javascript 的大小,但是你现在可以传递多个值来管理它们:
optimization{
splitChunks{
minSize: {
javascript: 30000,
style: 50000,
}
}
}
复制代码
moduleIds & chunkIds的优化
在webpack5之前,没有从entry打包的chunk文件,都会以1,2,3...的文件命名方式输出。(文件名称后的hash值是用chunkhash生成的)
这样会造成一个后果是,当删除或者暂时不使用1.js这个文件后,那么2.js->1.js,3.js->2.js,这样就会造成原本线上的2.js请求时会造成缓存失效。在webpack5之前也是可以通过webpackChunkName来解决命名问题
...
<Switch>
<Route key='/' exact path='/' component={
Loadable({
loader: () => import(/* webpackChunkName: "home" */ './home'),
loading: (<div>loadding</div>)
})
}/>
<Route key='/page1' exact path='/page1' component={
Loadable({
loader: () => import(/* webpackChunkName: "page1" */'./page1'),
loading: () => (<div>loadding</div>)
})
} />
<Route key='/page2' exact path='/page2' component={
Loadable({
loader: () => import(/* webpackChunkName: "page2" */'./page2'),
loading: () => (<div>loadding</div>)
})
} />
</Switch>
....
复制代码
- 这样似乎解决了缓存失效的问题,但我们打开编译后的home.js 会发现还是存有chunkId。如果删除掉home这个菜单,page1,page2打包后的chunkId还是会发生变化:
- page1.js打包的文件
- 删除完home.js后page1.js打包的文件
即使page1.js没有做任何修改,但是由于home.js删除导致的chunkId的变化,所以page1.js的chunkhashi还是会发生变化,缓存失效的问题依旧存在。 - webpack5 怎么做的
采用新的算法,在生产模式下,默认启用这些功能
chunkIds: "deterministic"
,moduleIds: "deterministic"
。 此算法采用确定性的方式将短数字 ID(3 或 4 个字符)分配给 modules 和 chunks。这是基于 bundle 大小和长效缓存间的折中方案。
optimization.moduleIds:
可选值:
1. false 告诉webpack不应使用任何内置算法,通过插件提供自定义算法
2. natural 按使用顺序的数字ID。
3. named 方便调试的高可读性id
4. deterministic 根据模块名称生成简短的hash值
5. size 根据模块大小生成的数字id
optimization.chunkIds:
可选值:
1. false 告诉webpack不应使用任何内置算法,通过插件提供自定义算法
2. natural 按使用顺序的数字ID。
3. named 方便调试的高可读性id
4. deterministic 根据模块名称生成简短的hash值
5. size 根据请求到的初始资源size计算的id
6. total-size:根据请求到的解析资源size计算的id
复制代码
FAQ
- 1、webpack-dev-server启动
webpack serve --config
复制代码
- 2、webpack-upload不兼容
- 3、less-loader选项
options: {
lessOptions?,
additionalData?,
sourceMap?,
webpackImporter?
}
复制代码
- 4、postcss-loader选项
options: {
postcssOptions: {
plugins: []
}
}
复制代码
最后
。。。篇幅有限,就先讲到这几个升级点,联邦模块、node更友好支持等...更多的还需要大家自行查询文档,不断试错,下面贴一下一个简单版的webpack.config.js
/**
* @file webpack
*/
const {
BundleAnalyzerPlugin
} = require('webpack-bundle-analyzer');
const path = require('path');
const isProd = process.env.NODE_ENV === 'production';
const client = {
entry: './src/index.js',
devtool: isProd ? false : 'cheap-module-source-map',
mode: isProd ? 'production' : 'development',
target: ['web', 'es5'],
output: {
path: `${__dirname}/browser`,
filename: 'index.js',
library: 'Player',
libraryTarget: 'window',
libraryExport: 'default'
},
cache: {
// 磁盘存储
type: 'filesystem', // 'memory' | 'filesystem'
buildDependencies: {
// 当配置修改时,缓存失效
config: [__filename]
},
name: 'client-cache'
},
module: {
rules: [
{
test: /\.js$/, // 加上面的.babelrc文件
use: [
'babel-loader'
],
exclude: '/node_modules/',
},
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
'postcss-loader',
'sass-loader'
]
},
{
test: /\.svg/,
type: 'asset/source',
use: [{
loader: 'svgo-loader',
options: {
plugins: [{
removeTitle: true
},
{
convertColors: {
shorthex: false
}
},
{
convertPathData: false
}
]
}
}]
}]
},
plugins: [
// new BundleAnalyzerPlugin({
// defaultSizes: 'parsed'
// })
],
optimization: {
minimize: isProd,
}
}
module.exports = client;
复制代码