共计 12651 个字符,预计需要花费 32 分钟才能阅读完成。
重读 webpack5
根底补充
对于 harmony modules ES2015 modules
又叫做 harmony modules
对于副作用:
webpack 的 side effect 副作用
,是指在 import 后执行特定行为的代码,而不是 export 一个或者多个,例如 pollyfill
,全局 css 款式等
对于 entry: entry
对象是 webpack 开始 build bundle
的中央。
对于 context: context
是蕴含入口文件的目录的相对字符串,默认就是当前目录,然而倡议设置。
对于依赖图:
webpack 是 dynamically bundle
依赖通过依赖图 dependency graph,防止打包没用的 module。
对于 Loader:
module loader 可链式调用,链中的每个 loader 都将解决资源,调用程序是反的。
对于图片:
webpack5 内置了解决图片、字体文件,不须要额定的 loader 来解决。
{test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
}
对于 csv、xml
csv-loader 来加载 csv 文件数据,xml-loader 来加载 xml 文件数据。
能够应用 parser
而不是 loader 来解决 toml, yamljs and json5 格局的资源, 如下
// webpack.config.js
const yaml = require('yamljs');
module: {
rules: [
{
test: /\.yaml$/i,
type: 'json',
parser: {parse: yaml.parse,}
}
]
}
// 代码中应用
import yaml from './data.yaml';
console.log(yaml)
对于 html-webpack-plugin:html-webpack-plugin
装置形式 npm i --save-dev html-webpack-plugin@next
对于 manifest
webpack 应用 manifest 来 track module 映射到 bundle 的关系, 应用webpack-manifest-plugin
对于 sourceMap source maps
用做 track js 的 error 和 warning,能够把编译后的代码指向源代码,定位异样的确切地位。
开发服务
每次改了代码,重写打包会很麻烦,有 3 种解决办法,
webpack 的 watch 模式
webpack-dev-server
webpack-dev-middleware,webpack-dev-middleware 是一个包装器,它会将 webpack 解决过的文件发送到服务器,webpack-dev-server 外部应用。
// 1. 应用 webpack-dev-server,装置好包之后,在 webpack.config.js 中配置
devServer: {contentBase: './dist',}
// 2 应用 webpack-dev-middleware 和 express,装置之后,新建 serverjs
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);
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(
webpackDevMiddleware(compiler, {publicPath: config.output.publicPath,})
);
// Serve the files on port 3000.
app.listen(3000, function () {console.log('Example app listening on port 3000!\n');
});
代码宰割 codes splitting
1 entry 宰割
这样没什么用,用的 lodash 还是会整个打到最初的 bundle 中,尽管能够通过 import chunk from 'lodash/chunk'
这种写法优化
// index.js
import _ from 'lodash'
console.log(_.chunk([1,2,3,4], 2))
// index2.js
import _ from 'lodash'
console.log(_.join(['Another', 'module', 'loaded!'], ' '));
// webpack.config.js
entry: {
index: './src/index.js',
index2: './src/index2.js',
}
2 Prevent Duplication
入口的 dependOn
字段和 runtimeChunk: 'single'
的办法
// 入口配置
entry: {
index: {
import: './src/index.js',
dependOn: 'shared',
},
index2: {
import: './src/index2.js',
dependOn: 'shared',
},
shared: 'lodash',
}
// webpack.config.js
optimization: {runtimeChunk: 'single',}
SplitChunksPlugin
容许咱们将独特的依赖提取到一个现有的入口文件块或一个全新的 chunk 中。
module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
optimization: {
splitChunks: {chunks: 'all',},
},
};
3 动静导入 dynamic import
应用 require.ensure
或者 import()
// index.js
// 咱们须要 default 值的起因是,自从 webpack 4 以来,当导入 CommonJS 模块时,导入将不再解析为 module.exports 的值
import('lodash').then(({default: _}) => {
console.log(_.chunk([1, 2, 3, 4], 2)
)
})
Prefetching/Preloading modules
- prefetch 预获取: 未来可能须要一些资源来反对运行。
- preload 预加载: 在以后运行期间须要的资源。
- preload chunk 和父 chunk 并行加载,prefetch chunk 在父块加载实现后开始加载。
- preload chunk 具备中等优先级并立刻下载。当浏览器闲暇时,才会下载 prefetch chunk。
- 父块应该立刻申请 preload chunk,prefetch chunk 可能在未来的任何时候应用。
- 浏览器反对不同。
- 一个简略的预加载例子是,有一个总是依赖于一个大库的组件,这个库应该在一个独自的块中,例如
import(/* webpackPreload: true */ 'ChartingLibrary');
- webpackPreload 不正确应用会对性能产生影响,审慎应用。
缓存
浏览器会缓存文件,这会让 web 加载更快,缩小不必要的流量。然而编译文件更新了就会造成麻烦。
Output Filename
contenthash
会依据文件内容计算一个字符串,文件内容变了就会扭转。
然而哪怕内容从新打包,hash 也不肯定一样,webpack 版本新的应该没这个问题,这是因为 webpack 在 entry 块中蕴含了某些样板文件,特地是 runtime 和 manifest。
output: {filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
}
提取样板
SplitChunksPlugin 能够用于将 module 拆分为独自的 bundle。webpack 提供了一个优化个性,能够应用optimize.runtimecchunk
选项将运行时代码宰割成一个独自的 chunk。将其设置为 single,为所有块创立单个运行时 bundle.
optimization: {runtimeChunk: 'single',}
会失去下边的后果
Asset Size Chunks Chunk Names
runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
main.e81de2cf758ada72f306.js 69.5 KiB 1 [emitted] main
index.html 275 bytes [emitted]
提取第三方库,像 react 等,它们个别不会变动,应用 cacheGroups
如下
optimization: {
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {test: /[\\/]node_modules[\\/]/,
name: 'common-libs',
chunks: 'all',
}
}
}
}
失去如下后果
asset common-libs.e6d769d50acd25e3ae56.js 1.36 MiB [emitted] [immutable] (name: common-libs) (id hint: vendor)
asset runtime.2537ce2560d55e32a85c.js 15.9 KiB [emitted] [immutable] (name: runtime)
asset index.f9c0d8e7e437c9cf3a6e.js 1.81 KiB [emitted] [immutable] (name: index)
asset index.html 370 bytes [emitted]
Module ID
如果在 index.js,减少一个援用新的文件的应用,从新打包,会发现,所有的 hash 都变了,然而专用库内容没变,hash 还是变了,因为减少了新的文件会导致它们的 moduleid 发生变化,所以 hash 也变了。在配置中减少如下。
optimization: {
// 通知 webpack 在抉择 模块 id 时应用哪种算法,默认 false
moduleIds: 'deterministic'
}
用 webpack5 测试,只有 index.js 的 hash 变了,runtime chunk 和 common-libs 都没变。
环境变量
webpack 内置了环境变量的设置办法,
npx webpack --env NODE_ENV=local --env production --progress
然而 module.exports 必须是个办法
const path = require('path');
module.exports = env => {
// Use env.<YOUR VARIABLE> here:
console.log('NODE_ENV:', env.NODE_ENV); // 'local'
console.log('Production:', env.production); // true
return {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
};
依赖治理
如果你的 require 蕴含表达式则会创立一个上下文,因而在编译时不晓得确切的模块
// 源目录
- modules/fn1.js
/fn2.js
// 源代码
let name = 'fn1';
const f = require("./modules/" + name + '.js')
console.log(f);
// webpack 这么解决
Directory: ./modules
Regular expression: /^.*\.js$/
生成一个 context module。它蕴含了对该目录中所有模块的援用,匹配正则表达式的申请可能须要这些模块。
context 模块蕴含一个映射,它将申请转换为模块 id。
// webpack 打包后有这么一个文件
var map = {
"./fn1.js": 430,
"./fn2.js": 698
};
这意味着反对动静需要,但会导致所有匹配的模块都蕴含在 bundle 中。
热更新 HMR
开启
如果是应用 webpack-dev-middleware
那么须要应用 webpack-hot-middleware
来开启热更新
// 在 devserver 开启
devServer: {
contentBase: './dist',
hot: true,
}
应用 node api
const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');
const config = require('./webpack.config.js');
const options = {
contentBase: './dist',
hot: true,
host: 'localhost',
};
webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);
server.listen(5000, 'localhost', () => {console.log('dev server listening on port 5000');
});
摇树 Tree Shaking
摇树是用来删没用的代码的,它依赖于 ES2015 模块语法的动态构造就是 import 和 export,是由 rollup 倒退而来。
webpack2 之后,内置了对 es6 的反对以及未应用模块导出的检测。
webpack4 扩大了这个性能,通过 package.json 的 sideEffects 字段,来表明 “ 纯文件 ” 能够平安删除。
源代码
// math.js
export function square(x) {return x * x;}
export function cube(x) {return x * x * x;}
// index.js
import {cube} from './math.js';
console.log(cube(5))
webpack.config.js
mode: 'development',
optimization: {usedExports: true,},
打包后的文件, 没用的代码并没有删除
/*!*********************!*\
!*** ./src/math.js ***!
\*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
/* harmony export */ __webpack_require__.d(__webpack_exports__, {/* harmony export */ "cube": () => /* binding */ cube
/* harmony export */ });
/* unused harmony export square */
function square(x) {return x * x;}
function cube(x) {return x * x * x;}
/***/ })
下边是没配置 usedExports: true,这个配置的作用,就是让 webpack 来判断哪些模块没应用,此配置依赖于
optimization.providedExports(通知 webpack 哪些 export 是由模块提供的),默认就是 true
/*!*********************!*\
!*** ./src/math.js ***!
\*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {/* harmony export */ "square": () => /* binding */ square,
/* harmony export */ "cube": () => /* binding */ cube
/* harmony export */ });
function square(x) {return x * x;}
function cube(x) {return x * x * x;}
/***/ })
在 100% ESM 模块的世界中,辨认副作用是很简略的,然而当初没到哪一步,所以须要在 package.json 中的 sideEffects 来通知 webpack,
标记文件在副作用树,下面提到的所有代码都没有副作用,所以咱们能够简略地将该属性标记为 false,以告诉 webpack 它能够平安地修剪未应用的导出文件。
// 开启后,打包后的死代码,依然没删除
{
"name": "your-project",
"sideEffects": false
}
// 如果有些文件的确有副作用,提供一个数组即可
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
留神,如果应用 css-loader 等加载款式, 须要把 css 放到副作用数组中,避免在生产模式的时候,被 webpack 无心中删除。
还能够在 webpack.config.js 中的 module.rules 中的一个 rule 中指定。
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
side effects 和 usedExports(摇树)的区别
side effects 能够间接抹去文件,例如
如果设置了 sideEffects: false
, 而后在 index.js 引入一个 math.js 然而不应用,打包后的 bundle 不会打包 math.js
然而如果没设置,不应用的文件还是会在打包后的文件中, 代码如下
/***/ 733:
/*!*********************!*\
!*** ./src/math.js ***!
\*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
/* unused harmony exports square, cube */
function square(x) {return x * x;}
function cube(x) {return x * x * x;}
/***/ })
usedExports 是依赖于 terser 来检测,在申明中的副作用,不如 side effects 间接
也无奈间接跳过整个文件,React 的高阶组件存在问题。
// 应用这个表明示意没有副作用,这会容许删掉这行代码,不剖析他的 side effect
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
因为文件的 import 申明不好判断能够在 package.json 的 sideEffects 字段退出该文件
If no direct export from a module flagged with no-sideEffects is used,
the bundler can skip evaluating the module for side effects
// 应用这一行代码
import {Button} from "@shopify/polaris"
// 下边是 @shopify/polaris 库里的文件
// index.js
import './configure';
export * from './types';
export * from './components';
// components/index.js
export {default as Breadcrumbs} from './Breadcrumbs';
export {default as Button, buttonFrom, buttonsFrom,} from './Button';
export {default as ButtonGroup} from './ButtonGroup';
// package.json
"sideEffects": [
"**/*.css",
"**/*.scss",
"./esnext/index.js",
"./esnext/configure.js"
],
对于 import {Button} from "@shopify/polaris"
这样代码,有以下 4 种状况
include it: include the module, evaluate it and continue analysing dependencies
skip over: don't include it, don't evaluate it but continue analysing dependencies
exclude it: don't include it, don't evaluate it and don't analyse dependencies
仔细分析代码通过的模块
index.js: 没应用间接 export 的代码,然而用 sideEffects 标记了 -> include it
configure.js: 没应用间接 export 的代码,然而用 sideEffects 标记了 -> include it
types/index.js: 没应用间接 export 的代码,没 sideEffects 标记 -> exclude it
components/index.js: 没应用间接 export 的代码,没 sideEffects 标记 , but reexported exports are used -> skip over
components/Breadcrumbs.js: 没应用间接 export 的代码,没 sideEffects 标记 -> exclude it.
This also excluded all dependencies like components/Breadcrumbs.css even if they are flagged with sideEffects.
components/Button.js: 应用间接 export 的代码,没 sideEffects 标记 -> include it
components/Button.css: 没应用间接 export 的代码,然而用 sideEffects 标记了 -> include it
这样造成,间接引入的文件,只有 4 个
index.js: pretty much empty
configure.js
components/Button.js
components/Button.css
函数调用变副作用树
通过应用 /*#__PURE__*/
正文,能够通知 webpack 函数调用是无副作用 (纯) 的。
它能够放在函数调用的后面,以标记它们为无副作用。传递给函数的参数没有被正文标记,可能须要独自标记。
当未应用变量申明中的初始值被认为是无副作用 (pure) 时,它将被标记为死代码,不会被执行,并被最小化者删除。
优化时启用此行为。innerGraph
设置为true
。
/*#__PURE__*/ double(55);
缩小代码
mode
设置为 production
即可,--optimize-minimize
也能够开启 TerserPlugin
,module-concatenation-plugin
这个插件在 tree shaking 中应用。
Build Performance
- 放弃 webpack、node、npm 是最新版本
- 应用 DllPlugin 打包
- 在 loader 中缩小解析范畴
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
},
],
},
};
Boot
- 缩小文件体积,移除没用的代码,
- 应用缓存 cache 配置
Module Federation
- github demo
- 多个独立的 build 应该形成惟一的应用程序。这些独自的 build 之间不应该有依赖关系,因而能够独自开发和部署它们。这通常被称为
微前端
,但并不仅限于此 - module 两种,本地构建的模块(local module)、运行时容器加载的近程模块(remote module)。
- 加载近程模块,是一个异步的加载 chunk 的操作,就是应用 import、require.ensure 或者 require([])
- 容器通过容器入口创立,并暴露出获取指定模块的办法,异步加载模块 (chunk loading) 和异步解析模块(解析期间与其余模块穿插执行)
- 解析程序,近程解析到本地,或者本地解析到近程,不会收到影响
- 容器能够应用其余容器的模块,容器之间的依赖共享能够实现。
- 容器能够标识模块为可重写,消费者提供重写办法,就是一个能够替换容器中可替换模块的模块
- 当 consumer 提供一个模块时,容器的所有模块都将应用替换模块而不是本地模块,不提供替换模块,就应用本地模块
- 容器用一种不需在被 consumer 重写时下载的形式来治理可重写模块,通常是通过将它们放入不同的 chunk 来实现的。
- 替换模块的 provider 只提供异步加载办法,它容许容器按需加载替换模块。provider 用在容器不须要的时候不加载的形式来治理替换模块,通常是通过将它们放入不同的 chunk 来实现的
- name 用于标识容器中的可重写模块
- 重写动作和容器裸露模块类似的形式相似,分为两步,异步加载和异步解析。
- 当应用嵌套,给一个容器提供重写将会主动重写嵌套容器中具备雷同“名称”的模块。
- 重写必须在加载容器的模块之前提供。在初始块中应用的重写,只能被不应用 Promise 的同步模块重写。一旦被解析,重写项将不再是可重写的。
- 每个构建都可作为容器,并能够生产别的构建作为容器应用。每个构建都能够通过从其容器中加载其余公开的模块来应用。
- shared module 是既能够重写又能够重写嵌套容器的模块。它们通常在每个构建中指向同一个模块,例如同一个库
- packageName 选项容许设置一个包名来查找所需的版本。默认状况下,主动推断模块申请,当主动推断应该被禁用时,将 requiredVersion 设置为 false
Building blocks
OverridablesPlugin
此插件让一个模块,可重写ContainerPlugin
此插件应用指定的 exposed modules 创立一个额定的容器 entry, 它外部应用 OverridablesPlugin, 并向容器的 consumer 公开 override APIContainerReferencePlugin
插件 add 特定的援用到容器作为 externals,并容许从这些容器导入近程模块。它还调用这些容器的 override API 来提供对它们的 override。本地重写
(通过__webpack_override__或 override API,当 build 也是一个容器时) 和指定重写
将被提供给所有援用的容器ModuleFederationPlugin
此插件组合了 ContainerPlugin 和 ContainerReferencePlugin,Overrides and overridables 会被组合到一个指定的共享模块列表中
Module Federation 须要实现的指标
- 应该能够公开和应用 webpack 反对的任何模块类型
- chunk 加载应并行加载所有须要的货色(web: 到服务器的单程往返)
- 从 consumer 到容器的管制,重写模块是单向操作,兄弟容器不能笼罩彼此的模块。
- 应该独立于环境,web, Node.js 等都能够用
- 共享的绝对和相对申请(不应用也应该被提供、依据 config.context 来解析、不默认应用 requiredVersion)
- 共享的模块申请(按需提供,将匹配构建中应用的所有 equal 模块申请,将提供所有匹配模块,
它将从 package.json 中提取 requiredVersion 在图中的这个地位, 可 provide 和 consume 多重不同版本当你有 nested node_modules)
- 带有后缀 / 共享的模块申请将匹配所有带有此前缀的模块申请
应用场景
- 每个页面独自构建
单个 spa 的每个页面都是在独自的构建中通过容器构建公开的,应用程序外壳 是一个援用所有页面的独自构建的 近程模块 ,
这样的话每个页面都能够独自部署,当路由更新或增加新路由时,应用程序外壳就会被部署。
应用程序外壳将罕用库定义为共享模块,以防止在页面构建中重复使用它们
- 组件库作为容器
多利用共享一个公共的组件库,能够将其构建为公开每个组件的容器,每个利用生产组件库容器。
对组件库的更改能够独自部署而不须要重新部署所有应用程序。应用程序主动应用组件库的最新版本。
动静近程容器
容器接口反对 get 和 init 办法。init 是一个异步兼容的办法,调用时只有一个参数: 共享范畴对象。
此对象在近程容器中用作共享 scope,并由 host 填充 provided modules。
它能够在运行时动静的连贯 remote containers to a host container
(async () => {
// 初始化 shared scope,应用以后 build 和所有近程的 provided modules
await __webpack_init_sharing__('default');
const container = window.someContainer; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const module = await container.get('./module');
})();
容器尝试提供共享模块,但如果共享模块曾经被应用,则正告和提供的共享模块将被疏忽。容器依然能够应用它作为 fallback。
通过这种形式,你能够动静加载一个 A / B 测试,它提供了一个共享模块的不同版本。