webpack5新特性
webpack5 已经发布,将主要涉及的新特性及这些特性的使用方法总结了一下。
英文文档地址:webpack.js.org/
中文文档地址:webpack.docschina.org/
github地址:github.com/webpack/web…
1、内置静态资源构建能力 —— Asset Modules
在 webpack 5 之前,通常使用:
- raw-loader 将文件导入为字符串
- url-loader 将文件作为 data URI 内联到 bundle 中
- file-loader 将文件发送到输出目录
资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
- asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
- asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
- asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
- asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。
1.1 type分别为asset/resource
、asset/inline
、asset/source
webpack.config.js
module: {
rules: [
{
test: /\.(png|jpg|jpeg|gif)$/,
type: 'asset/resource'
},
{
test: /\.svg/,
type: 'asset/inline'
},
{
test: /\.txt/,
type: 'asset/source' // 原样将txt文件中的文本内容注入到打包文件中
}
]
}
src/index.js
import imgUrl from './assets/img/pic.jpeg';
import svgUrl from './assets/img/delete.svg';
import txt from './assets/example.txt';
//添加图片资源
let img = document.createElement('img');
img.src = imgUrl; // imgUrl: 'file:///Users/yujian2018/work/learning/project/webpack5/dist/assets/img/f972bcf4.pic.jpeg'
img.style.width = '150px';
img.style.height = '150px';
document.body.appendChild(img);
let svg = document.createElement('img');
svg.src = svgUrl; // svgUrl: data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjYgKDY3NDkxKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT7liKDpmaQ8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz4KICAgICAgICA8cGF0aCBkPSJNNy4zMzMzMzMzMywxLjMzMzMzMzMzIEwxMC41MzMzMzMzLDEuMzMzMzMzMzMgQzEwLjYwNjk3MTMsMS4zMzMzMzMzMyAxMC42NjY2NjY3LDEuMzkzMDI4NyAxMC42NjY2NjY3LDEuNDY2NjY2NjcgTDEwLjY2NjY2NjcsMi41MzMzMzMzMyBDMTAuNjY2NjY2NywyLjYwNjk3MTMgMTAuNjA2OTcxMywyLjY2NjY2NjY3IDEwLjUzMzMzMzMsMi42NjY2NjY2NyBMMTAsMi42NjY2NjY2NyBMMTAsMTIuOCBDMTAsMTMuMDk0NTUxOSA5Ljc2MTIxODUzLDEzLjMzMzMzMzMgOS40NjY2NjY2NywxMy4zMzMzMzMzIEwxLjIsMTMuMzMzMzMzMyBDMC45MDU0NDgxMzMsMTMuMzMzMzMzMyAwLjY2NjY2NjY2NywxMy4wOTQ1NTE5IDAuNjY2NjY2NjY3LDEyLjggTDAuNjY2NjY2NjY3LDIuNjY2NjY2NjcgTDAuMTMzMzMzMzM1LDIuNjY2NjY2NjcgQzAuMDU5Njk1MzY3NiwyLjY2NjY2NjY3IDQuNzI4OTIyNjVlLTE2LDIuNjA2OTcxMyAzLjMzMDY2OTA3ZS0xNiwyLjUzMzMzMzMzIEwwLDEuNDY2NjY2NjcgQy05LjAxODA1MDAxZS0xOCwxLjM5MzAyODcgMC4wNTk2OTUzNjY3LDEuMzMzMzMzMzMgMC4xMzMzMzMzMzMsMS4zMzMzMzMzMyBMMy4zMzMzMzMzMywxLjMzMzMzMzMzIEwzLjMzMzMzMzMzLDAuMTMzMzMzMzM1IEMzLjMzMzMzMzMzLDAuMDU5Njk1MzY3NiAzLjM5MzAyODcsMS4yNDU0OTM3OGUtMTYgMy40NjY2NjY2NywxLjExMDIyMzAyZS0xNiBMNy4yLDAgQzcuMjczNjM3OTcsMi40ODA4NzU0ZS0xNiA3LjMzMzMzMzMzLDAuMDU5Njk1MzY3NiA3LjMzMzMzMzMzLDAuMTMzMzMzMzM1IEw3LjMzMzMzMzMzLDEuMzMzMzMzMzMgWiBNMy42NjY2NjY2Nyw0LjY2NjY2NjY3IEMzLjU5MzAyODcsNC42NjY2NjY2NyAzLjUzMzMzMzMzLDQuNzI2MzYyMDMgMy41MzMzMzMzMyw0LjggTDMuNTMzMzMzMzMsOS44NjY2NjY2NyBDMy41MzMzMzMzMyw5Ljk0MDMwNDYzIDMuNTkzMDI4NywxMCAzLjY2NjY2NjY3LDEwIEw0LjMzMzMzMzMzLDEwIEM0LjQwNjk3MTMsMTAgNC40NjY2NjY2Nyw5Ljk0MDMwNDYzIDQuNDY2NjY2NjcsOS44NjY2NjY2NyBMNC40NjY2NjY2Nyw0LjggQzQuNDY2NjY2NjcsNC43MjYzNjIwMyA0LjQwNjk3MTMsNC42NjY2NjY2NyA0LjMzMzMzMzMzLDQuNjY2NjY2NjcgTDMuNjY2NjY2NjcsNC42NjY2NjY2NyBaIE02LjMzMzMzMzMzLDQuNjY2NjY2NjcgQzYuMjU5Njk1MzcsNC42NjY2NjY2NyA2LjIsNC43MjYzNjIwMyA2LjIsNC44IEw2LjIsOS44NjY2NjY2NyBDNi4yLDkuOTQwMzA0NjMgNi4yNTk2OTUzNywxMCA2LjMzMzMzMzMzLDEwIEw3LDEwIEM3LjA3MzYzNzk3LDEwIDcuMTMzMzMzMzMsOS45NDAzMDQ2MyA3LjEzMzMzMzMzLDkuODY2NjY2NjcgTDcuMTMzMzMzMzMsNC44IEM3LjEzMzMzMzMzLDQuNzI2MzYyMDMgNy4wNzM2Mzc5Nyw0LjY2NjY2NjY3IDcsNC42NjY2NjY2NyBMNi4zMzMzMzMzMyw0LjY2NjY2NjY3IFoiIGlkPSJwYXRoLTEiPjwvcGF0aD4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSLliKDpmaQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxyZWN0IGZpbGw9IiNGRkZGRkYiIG9wYWNpdHk9IjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PC9yZWN0PgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBpZD0iNF9JY29uLzBfYmFzZS9iZy3mm7/mjaIiPjwvZz4KICAgICAgICAgICAgPGcgaWQ9Imljb24vZ3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIuNjY2NjY3LCAxLjMzMzMzMykiPgogICAgICAgICAgICAgICAgPG1hc2sgaWQ9Im1hc2stMiIgZmlsbD0id2hpdGUiPgogICAgICAgICAgICAgICAgICAgIDx1c2UgeGxpbms6aHJlZj0iI3BhdGgtMSI+PC91c2U+CiAgICAgICAgICAgICAgICA8L21hc2s+CiAgICAgICAgICAgICAgICA8dXNlIGlkPSJNYXNrIiBmaWxsPSIjODM4NjhGIiB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+
document.body.appendChild(svg);
let txtEl = document.createElement('div');
txtEl.innerHTML = txt; // txt: 这里是纯文本内容
document.body.appendChild(txtEl);
1.2 type为asset
对于type: asset
,webpack 将按照默认条件,自动地在 resource 和 inline 之间进行选择:小于 8kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型。
也可以通过在 webpack 配置的 module rule 层级中,设置 Rule.parser.dataUrlCondition.maxSize 选项来修改此条件:
module: {
rules: [
{
test: /\.(png|jpg|jpeg|gif)$/,
type: 'asset',
// 自定义设置
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
}
}
]
}
1.3 自定义输出文件名
默认情况下,asset/resource 模块以 [hash][ext][query] 文件名发送到输出目录。
可以通过在 webpack.config.js 将output.assetModuleFilename 和Rule.generator.filename结合使用来定制化文件的输出目录:
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.(png|jpg|jpeg|gif)$/,
type: 'asset/resource',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
},
generator: {
// [ext]前面自带"."
filename: 'assets/img/[hash:8].[name][ext]', //自定义输出目录
}
}
]
}
注意:Rule.generator.filename 与 output.assetModuleFilename 相同,并且仅适用于 asset 和 asset/resource 模块类型。
2、文件缓存
在webpack4中,我们会使用 cache-loader 缓存一些性能开销较大的 loader ,或者是使用 hard-source-webpack-plugin 为模块提供一些中间缓存。在 Webpack5 之后,默认就为我们集成了一种自带的缓存能力(对 module 和 chunks 进行缓存)。通过如下配置,即可在二次构建时提速。
cache: {
type: 'filesystem',
// 默认缓存到 node_modules/.cache/webpack 中
// 也可以自定义缓存目录,cache.cacheDirectory 选项仅当 cache.type 被设置成 filesystem 才可用。
// cacheDirectory:path.resolve(__dirname,'node_modules/.cac/webpack'),
buildDependencies : {
// 2. 将您的配置添加为 buildDependency 以使配置更改时缓存失效
config : [ __filename ]
// 3. 如果您有其他构建所依赖的东西你可以在这里添加它们
// 请注意,webpack、加载器和从你的配置中引用的所有模块都会自动添加
}
}
3、更好地treeshaking
未使用的导出内容不会被打包生成。 将 mode 工作模式改为 production 就会自动开启。
3.1、 嵌套treeshaking(Nested tree-shaking)
module1.js
import * as module2 from './module2'
export function fun1() {
console.log('fun1');
}
export function fun2() {
console.log('fun2')
}
export { module2 }
module2.js
export function fun3() {
console.log('fun3');
}
export function fun4() {
console.log('fun4')
}
export const num1 = 111
export const num2 = 222
index.js
import * as module1 from "./module1";
console.log(module1.module2.num1)
webpack4 和webpack5的打包结果对比:
3.2、 内部模块treeshaking(Inner-module tree-shaking)
webpack 4 没有分析模块的导出和导入之间的依赖关系。webpack 5 有一个新选项optimization.innerGraph,它在生产模式下默认启用,它对模块中的符号运行分析以找出从导出到导入的依赖关系。
import { something } from "./something";
function usingSomething() {
return something;
}
export function test() {
return usingSomething();
}
innerGraph将计算出something仅在使用test导出时使用。这允许将更多导出标记为未使用并从包中省略更多代码。
当 "sideEffects": false 设置,这允许省略甚至更多的模块。 在此示例中,./something 当 test 导出未使用时将被省略。
3.3 commonjs treeshaking
webpack 5 添加了对某些 CommonJs 结构的支持,允许消除未使用的 CommonJs 导出并跟踪require()调用中引用的导出名称。
4、模块联邦
模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin,插件有几个重要参数:
- name 当前应用名称,需要全局唯一。
- remotes 可以将其他项目的 name 映射到当前项目中。
- exposes 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用。
- shared 是非常重要的参数,制定了这个参数,可以让远程加载的模块对应依赖改为使用本地项目的 React 或 ReactDOM。
使用Module Federation时,每个应用块都是一个独立的构建,这些构建都将编译为容器。
容器可以被其他应用或者其他容器应用。
一个被引用的容器被称为remote, 引用者被称为host,remote暴露模块给host, host则可以使用这些暴露的模块,这些模块被成为remote模块。
主要代码:
app_remote项目中的weback.config.js
new ModuleFederationPlugin({
name: 'app_remote',
filename: "remoteEntry.js",
exposes: { // 远程应用暴露出的模块名
'./Button': './src/components/Button.vue',
},
shared: ["vue", "element-ui"]
})
host项目中的weback.config.js
new ModuleFederationPlugin({
name: "app_remote",
filename: 'remoteEntry.js',
remotes: { // 声明需要引用的远程应用
remote: 'app_remote@http://localhost:3000/remoteEntry.js'
},
shared: ["vue", "element-ui"]
})
host项目中使用remote项目的组件时, src/app.vue
Button: () => import("remote/Button"),
遇到的问题:
使用shared参数时,如果报错:Uncaught Error: Shared module is not available for eager consumption
,则解决方案如下:
新建bootstrap.js,将index.js中的内容粘贴到此文件中。如下:
import Vue from 'vue';
import App from './app.vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
new Vue({
render: h => h(App)
}).$mount('#app');
将index.js中的内容修改为:
import('./bootstrap');
最终效果图如下:
分别是子应用和主应用,其中普通按钮来自子应用,带了ele样式的button来自主应用。
完整的项目代码如下:
app_remote项目:
webpack.config.js
const path = require('path');
const webpack = require("webpack");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode: 'development', // production none
entry: './src/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(woff|ttf)$/,
loader: 'file-loader'
},
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'app_remote',
template: path.resolve(__dirname, './public/index.html'),
filename: 'index.html'
}),
new ModuleFederationPlugin({
name: 'app_remote',
filename: "remoteEntry.js",
exposes: { // 远程应用暴露出的模块名
'./Button': './src/components/Button.vue',
},
remotes: {
host: "app_host@http://localhost:9000/remoteEntry.js"
},
shared: ['vue', 'element-ui']
}),
new VueLoaderPlugin()
],
devServer: {
hot: true,
host: '0.0.0.0',
port: 3000
},
};
src/app.vue
<template>
<div>
Hello,{{ name }}
<Button />
<List />
</div>
</template>
<script>
export default {
components: {
Button: () => import("./components/Button.vue"),
List: () => import("host/list"),
},
data() {
return {
name: "子应用",
};
},
};
</script>
src/components/Button.vue
<template>
<div>
<button>hahaha</button>
</div>
</template>
app_host项目中:
webpack.config.js
const path = require('path');
const webpack = require("webpack");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode: 'development', // production
entry: './src/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
// publicPath: "http://localhost:9000/", //部署后的资源地址
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
include: [
path.resolve(process.cwd(), 'src'),
]
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(woff|ttf)$/,
loader: 'file-loader'
},
]
},
plugins: [
new VueLoaderPlugin(),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'app_host',
template: path.resolve(__dirname, './public/index.html'),
filename: 'index.html'
}),
new ModuleFederationPlugin({
name: "app_host",
filename: 'remoteEntry.js',
exposes: {
"./list": "./src/components/list.vue",
},
remotes: { // 声明需要引用的远程应用
remote: 'app_remote@http://localhost:3000/remoteEntry.js'
},
shared: ['vue', 'element-ui']
})
],
devServer: {
hot: true,
host: '0.0.0.0',
port: 9000
}
};
app.vue
<template>
<div>
Hello,{{ name }}
<Button />
<el-button type="primary"></el-button>
</div>
</template>
<script>
export default {
components: {
// Button: (resolve) => require(["remote/Button"], resolve),
Button: () => import("remote/Button"),
},
data() {
return {
name: "主应用",
};
},
};
</script>
src/components/list.vue
<template>
<div>
<el-button type="primary">这里使用了element-ui组件库</el-button>
</div>
</template>
<script>
src/bootstrap.js
import Vue from 'vue';
import App from './app.vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
new Vue({
render: h => h(App)
}).$mount('#app');
src/index.js
import('./bootstrap');
转载自:https://juejin.cn/post/6983985071699001357