webpack5发布于2020年的国庆节之后,主要特点有:用持久性缓存来提高构建性能、用更好的算法和默认值来改进长期缓存、用更好的 Tree Shaking 和代码生成来改善包大小…
关于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,可以把编译后的代码指向源代码,定位异常的确切位置。
就是ECMAScript Modules,在package.json 中增加如下配置,强迫项目中的文件,使用ESM
{
"type": "module"
}
除了这种方式, 文件可以设置模块方式通过 .mjs 或者 .cjs 的后缀.
.mjs强迫使用ESM模块 ,.cjs就是 CommonJs模块.
两种情况,全局变量如jq或者浏览器polyfill。
ProvidePlugin让一个包在webpack的编译过程中作为一个可用变量,最后webpack发现这个变量使用了,就会在最后的
bundle中引入此包。例如把lodash变成全局变量,这样在模块中就不需要import了
plugins: [
new webpack.ProvidePlugin({
_: 'lodash',
})
]
如果只需要lodash的一个chunk方法
new webpack.ProvidePlugin({
join: ['lodash', 'join']
})
Asset Modules 是一种允许用户使用assets文件(字体、图标等)而无需配置额外加载器的module。
webpack5之前,使用
raw-loader
以字符串形式导入文件
url-loader
将文件作为data URI内联到bundle中
file-loader
将文件发送到输出目录
Asset Modules使用如下方式来替代拿些loader
asset/resource 生成一个单独的文件并导出URL。以前可以通过使用file-loader实现
asset/inline 导出assets的data URI。以前可以通过使用url-loader实现
asset/source 导出资产的源代码。以前可以通过使用raw-loader实现
asset 自动选择是导出data URI还是生成单独的文件。以前可以通过使用具有asset大小限制的url-loader实现
如果使用webpack5,但是又不想修改之前的loader配置,可这么修改 type: 'javascript/auto'
这会停止asset module 再次处理那些assets
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
}
}
],
type: 'javascript/auto'
},
exclude 那些使用asset loader处理的新url
{
test: /\.(png|jpg|gif)$/i,
dependency: { not: ['url'] },
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
}
}
]
}
// 如下,处理png文件
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
}
]
},
// 源代码
import mainImage from './images/main.png';
img.src = mainImage; // '/dist/151cfcfa1bd74779aadb.png'
默认,asset/resource modules 使用 [hash][ext][query
]的文件导出到输出文件夹, 我们可以修改这个使用
output.assetModuleFilename`覆盖
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[hash][ext][query]'
}
另外一种方法,是在rules中配置
{
test: /\.html/,
type: 'asset/resource',
generator: {
filename: 'static/[hash][ext][query]'
}
}
// 配置
{
test: /\.svg/,
type: 'asset/inline'
}
// 源代码
import metroMap from './images/metro.svg';
block.style.background = `url(${metroMap})`; // url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo...vc3ZnPgo=)
默认webpack是使用base64算法实现的,也可以自定义实现
const path = require('path');
const svgToMiniDataURI = require('mini-svg-data-uri');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline',
generator: {
dataUrl: content => {
content = content.toString();
return svgToMiniDataURI(content);
}
}
}
]
},
};
module: {
rules: [
{
test: /\.txt/,
type: 'asset/source',
}
]
}
// webpack 也会这样创建一个asset module
const logo = new URL('./logo.svg', import.meta.url);
// 不同的目标环境,webpack的处理不同
// target: web
new URL(__webpack_public_path__ + 'logo.svg', document.baseURI || self.location.href);
// target: webworker
new URL(__webpack_public_path__ + 'logo.svg', self.location);
// target: node, node-webkit, nwjs, electron-main, electron-renderer, electron-preload, async-node
new URL(__webpack_public_path__ + 'logo.svg', require('url').pathToFileUrl(__filename));
如下,不指定哪种asset
module: {
rules: [
{
test: /\.txt/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024 // 4kb
}
}
}
]
}
webpack会自动判断resource 还是 inline,如果文件尺寸小于8kb,就会被处理做inline,否则就是resource
可以使用parser.dataUrlCondition来覆盖。
每次改了代码,重写打包会很麻烦,有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');
});
这样没什么用,用的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',
}
入口的 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',
},
},
};
使用 require.ensure
或者 import()
// index.js
// 我们需要 default 值的原因是,自从webpack 4以来,当导入CommonJS模块时,导入将不再解析为module.exports的值
import('lodash').then(({default: _}) => {
console.log(
_.chunk([1, 2, 3, 4], 2)
)
})
import(/* webpackPreload: true */ 'ChartingLibrary');
浏览器会缓存文件,这会让web加载更快,减少不必要的流量。但是编译文件更新了就会造成麻烦。
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]
如果在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'),
},
};
};
开发和生产的目标差别很大。在开发中,我们需要强大的 source map 和一个本地主机服务器,可以实时重新加载或热模块替换。
在生产环境中,我们的目标转向关注小尺寸的bundle、轻量级的源映射和优化assets,以提高加载时间。
出于这种逻辑分离的目的,我们通常建议为每个环境编写单独的webpack配置。
新建webpack.common.js
保存开发和生产共有的代码,webpack.dev.js
和 webpack.prod.js
配置各自环境,
需要 webpack-merge
来merge配置
如果你的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中。
如果是使用 webpack-dev-middleware
那么需要使用 webpack-hot-middleware
来开启热更新
// 在devserver开启
devServer: {
contentBase: './dist',
hot: true,
}
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');
});
摇树是用来删没用的代码的,它依赖于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可以直接抹去文件,例如
如果设置了 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中使用。
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
},
],
},
};
Boot
微前端
,但并不仅限于此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 会被组合到一个指定的共享模块列表中容器接口支持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测试,它提供了一个共享模块的不同版本。
一个包的package.json的exports字段,可以定义 import “package” 或者 import “package/sub/path”
的时候,哪些模块被使用。它替换了默认的行为。当定义了这些字段,只有这些字段是有效的,其他的都会
ModuleNotFound 错误。
{
"exports": {
".": "./main.js",
"./sub/path": "./secondary.js",
"./prefix/": "./directory/",
"./prefix/deep/": "./other-directory/",
// 可以定义为一个数组,找到有效的就返回了
"./things/": ["./good-things/", "./bad-things/"]
}
}
// 结果如下
package .../package/main.js
package/sub/path .../package/secondary.js
package/prefix/some/file.js .../package/directory/some/file.js
package/prefix/deep/file.js .../package/other-directory/file.js
package/main.js Error
{
"exports": {
".": {
"red": "./stop.js",
"yellow": "./stop.js",
"green": {
"free": "./drive.js",
"default": "./wait.js"
},
"default": "./drive-carefully.js"
}
}
}
// 上边的这个意思
if (red && valid('./stop.js')) return './stop.js';
if (yellow && valid('./stop.js')) return './stop.js';
if (green) {
if (free && valid('./drive.js')) return './drive.js';
if (valid('./wait.js')) return './wait.js';
}
if (valid('./drive-carefully.js')) return './drive-carefully.js';
throw new ModuleNotFoundError();
剩下的,webpack原链接
plugins是可以用自身原型方法apply来实例化的对象。
apply只在安装插件被Webpack compiler执行一次。apply方法传入一个webpack compiler的引用,来访问编译器回调
class HelloPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options){
}
// Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) {
// 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
// 在功能流程完成后可以调用 webpack 提供的回调函数;
});
// 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知webpack,才会进入下一个处理流程。
compiler.plugin('emit',function(compilation, callback) {
// 支持处理逻辑
// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这不往下执行
callback();
});
}
}
module.exports = HelloPlugin;
校验配置文件 :读取命令行传入或者webpack.config.js文件,初始化本次构建的配置参数
生成Compiler对象:执行配置文件中的插件并依次添加到compiler上,为webpack事件流挂上自定义hooks
进入entryOption阶段:webpack开始读取配置的Entries,递归遍历所有的入口文件
run/watch:如果运行在watch模式则执行watch方法,否则执行run方法
compilation:创建Compilation对象回调compilation相关钩子,依次进入每一个入口文件(entry),使用loader对文件进行编译。通过compilation我可以可以读取到module的resource(资源路径)、loaders(使用的loader)等信息。再将编译好的文件内容使用acorn解析生成AST静态语法树。然后递归、重复的执行这个过程, 所有模块和和依赖分析完成后,执行 compilation 的 seal 方法对每个 chunk 进行整理、优化、封装__webpack_require__来模拟模块化操作.
emit:所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息
Compiler 是主引擎它通过CLI或Node API传递的所有选项来创建一个编译实例
它继承了taptable类,以便注册和调用插件。
Compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,
并在所有可操作的设置中被配置,包括原始配置,loader 和插件。当在 webpack 环境中应用一个插件时,
插件将收到一个编译器对象的引用。可以使用它来访问 webpack 的主环境。
通常,只创建一个master编译器实例,但是可以创建子编译器来委派特定的任务。
编译器最终只是一个函数,它执行最小的功能来保持生命周期的运行。它将所有的load、bundle和write工作委托给注册的插件。
compiler上的hook属性,用来注册一个插件执行hook事件在compiler的生命周期中。
WebpackOptionsDefaulter
and WebpackOptionsApply
这两个用来配置compiler和内置插件。
使用 run
方法启动所有的编译工作。完成后,执行给定的回调函数。
统计和错误的最终记录应该在这个回调函数中完成
该API一次只支持一次并发编译。
当使用run时,等待它完成后再调用run或watch。
当使用watch时,调用close,等待它完成后再调用run或watch。并发编译将破坏输出文件
// node api
// webpack 方法执行的时候,如果提供了回调方法的话就会执行 webpack compiler.run
// stats 中包含了编译进程的信息
webpack(config, (err, stats) => {
if (err || stats.hasErrors()) {
console.log(stats.hasErrors());
console.log(stats.toJson());
}
console.log('ok')
// Done processing
});
// 如果不传一个回调函数,就会返回一个webpack的compiler的实例
const compiler = webpack(config);
// 这个实例有两个方法可用
.run(callback)
.watch(watchOptions, handler)
// @title 使用 watch
// webpack检测到发生了变化,compiler会重新run
// 注意 文件系统的不准确性可能会为一个更改触发多个构建。
const watching = compiler.watch({
// Example [watchOptions](/configuration/watch/#watchoptions)
aggregateTimeout: 300,
poll: undefined
}, (err, stats) => { // [Stats Object](#stats-object)
// Print watch/build result here...
console.log(stats);
});
// 关闭watch
watching.close(() => {
console.log('Watching Ended.');
});
compiler 在watch模式,包括在webpack-dev-server下,会加入watchRun, watchClose, invalid等钩子
const hooks = [
{
name: 'watchRun',
type: 'AsyncSeriesHook',
desc: '在watch模式下,新编译被触发后,但实际开始编译之前的执行插件'
},
{
name: 'watchClose',
type: 'SyncHook',
desc: ' a watching compilation 停止后调用'
},
]
class MyWebpackPlugin {
apply(compiler) {
compiler.hooks.environment.tap('WebpackPluginDemo2', (compilation) => {
console.log("WebpackPluginDemo2-environment")
});
compiler.hooks.watchRun.tapAsync('WebpackPluginDemo2', (compilation, cb) => {
console.log('WebpackPluginDemo2-watch')
cb()
})
}
}
它代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,
从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,
简单来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件需要自定义功能的回调,
以供插件做自定义处理时选择使用拓展。
compiler 使用 Compilation module 来创建新的编译(或构建)。
编译实例可以访问所有模块及其依赖项(其中大多数是循环引用)
它是应用中依赖图中所有模块的直接编译。
在编译阶段,模块被 loaded、sealed、optimized、chunked、hashed、restored
。
编译类还扩展了taptable,并提供了以下生命周期钩子。它们可以像编译器钩子一样被调用:
更多 hooks
// @type SyncHook build module 之前 可以用来修改module
compilation.hooks.buildModule.tap('SourceMapDevToolModuleOptionsPlugin',
module => {
module.useSourceMap = true;
}
);
// @type SyncHook 重新 build module 之前
rebuildModule
// @type SyncHook build module 失败之后
failedModule
// @type SyncHook build module 成功之后
succeedModule
// @type AsyncSeriesHook 所有模块都无错误build之后
finishModules
// @type SyncHook 编译停止接受新模块触发
seal
// @type SyncHook 编译重新接受新模块触发
unseal
class MyPlugin {
constructor(options) {
console.log(options);
}
apply(compiler) {//接受 compiler参数
// console.log(compiler.hooks)
// 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
console.log('hello1')
// 在功能流程完成后可以调用 webpack 提供的回调函数;
});
compiler.hooks.done.tapAsync("HelloPlugin", function (compilation) {
console.log('done ???')
})
// webpack5的compiler不继承tapable库,不可使用
// 如果事件是异步的,会带两个参数,第二个参数为回调函数,
// 在插件处理完任务时需要调用回调函数通知webpack,才会进入下一个处理流程。
compiler.plugin('emit', function (compilation, callback) {
console.log('plugin...')
// 支持处理逻辑
// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这不往下执行
callback();
});
}
}
class FileListPlugin {
constructor(props) {
this.filename = props.filename;
}
apply(compiler) {
// emit is asynchronous hook, tapping into it using tapAsync, you can use tapPromise/tap(synchronous) as well
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
// Create a header string for the generated file:
var filelist = 'In this build:\n\n';
// Loop through all compiled assets,
// adding a new line item for each filename.
for (var filename in compilation.assets) {
filelist += '- ' + filename + '\n';
}
// Insert this list into the webpack build as a new file asset:
compilation.assets[this.filename] = {
source: function () {
return filelist;
},
size: function () {
return filelist.length;
}
};
callback();
});
}
}
SyncHook
new SyncHook()
定义tap
触发call(...params)
调用Bail Hooks
SyncBailHook[params]
定义tap
触发call(...params)
调用Waterfall Hooks
SyncWaterfallHook
定义tap
触发call(...params)
调用Async Series Hook
AsyncSeriesHook[params]
定义tap/tapAsync/tapPromise
触发.callAsync(...params)
调用Async waterfall
tap/tapAsync/tapPromise
触发.callAsync(...params)
调用Async Series Bail
tap/tapAsync/tapPromise
触发.callAsync(...params)
调用Async Parallel
tap/tapAsync/tapPromise
触发.callAsync(...params)
调用webpack使用默认的文件系统读写文件,可自定义修改为memory, webDAV…
const { createFsFromVolume, Volume } = require('memfs');
const webpack = require('webpack');
const fs = createFsFromVolume(new Volume());
const compiler = webpack({ /* options */ });
compiler.outputFileSystem = fs;
compiler.run((err, stats) => {
// Read the output later:
const content = fs.readFileSync('...');
});