webpack进阶知识
webpack5
此版本重点关注以下内容:
- 通过持久缓存提高构建性能.
- 使用更好的算法和默认值来改善长期缓存.
- 通过更好的树摇和代码生成来改善捆绑包大小.
- 清除处于怪异状态的内部结构,同时在 v4 中实现功能而不引入任何重大更改.
- 通过引入重大更改来为将来的功能做准备,以使我们能够尽可能长时间地使用 v5.
下载
- npm i webpack@next webpack-cli -D
自动删除 Node.js Polyfills
早期,webpack 的目标是允许在浏览器中运行大多数 node.js 模块,但是模块格局发生了变化,许多模块用途现在主要是为前端目的而编写的。webpack <= 4 附带了许多 node.js 核心模块的 polyfill,一旦模块使用任何核心模块(即 crypto 模块),这些模块就会自动应用。
尽管这使使用为 node.js 编写的模块变得容易,但它会将这些巨大的 polyfill 添加到包中。在许多情况下,这些 polyfill 是不必要的。
webpack 5 会自动停止填充这些核心模块,并专注于与前端兼容的模块。
迁移:
- 尽可能尝试使用与前端兼容的模块。
- 可以为 node.js 核心模块手动添加一个 polyfill。错误消息将提示如何实现该目标。
Chunk 和模块 ID
添加了用于长期缓存的新算法。在生产模式下默认情况下启用这些功能。
chunkIds: "deterministic", moduleIds: "deterministic"
Chunk ID
你可以不用使用 import(/* webpackChunkName: "name" */ "module")
在开发环境来为 chunk 命名,生产环境还是有必要的
webpack 内部有 chunk 命名规则,不再是以 id(0, 1, 2)命名了
Tree Shaking
- webpack5 现在能够处理对嵌套模块的 tree shaking
// inner.js
export const a = 1;
export const b = 2;
// module.js
import * as inner from './inner';
export { inner };
// user.js
import * as module from './module';
console.log(module.inner.a);
在生产环境中, inner 模块暴露的 b
会被删除
- webpack5 现在能够处理多个模块之前的关系
import { something } from './something';
function usingSomething() {
return something;
}
export function test() {
return usingSomething();
}
当设置了"sideEffects": false
时,一旦发现test
方法没有使用,不但删除test
,还会删除"./something"
- webpack5 现在能处理对 Commonjs 的 tree shaking
Output
webpack 4 默认只能输出 ES5 代码
webpack 5 开始新增一个属性 output.ecmaVersion, 可以生成 ES5 和 ES6 / ES2015 代码.
如:output.ecmaVersion: 2015
SplitChunk
// webpack4
minSize: 30000; // 针对所有的文件的大小限制
// webpack5
minSize: {
javascript: 30000, // 针对js文件的大小限制
style: 50000, // 针对样式文件的大小限制
}
Caching
// 配置缓存
cache: {
// 磁盘存储
type: "filesystem",
buildDependencies: {
// 当配置修改时,缓存失效
config: [__filename]
}
}
缓存将存储到 node_modules/.cache/webpack
监视输出文件
之前 webpack4 总是在第一次构建时输出全部文件,但是监视重新构建时会只更新修改的文件。
而webpack5在第一次构建时会找到输出文件看是否有变化,从而决定要不要输出全部文件。
默认值
entry: "./src/index.js
output.path: path.resolve(__dirname, "dist")
output.filename: "[name].js"
更多内容
https://github.com/webpack/changelog-v5
webpack高级
对于脚手架,以vue-cli为例,vue2集成了webpack构建工具,使用npx vue-cli-service inspect --mode=development > [文件名]
命令可将vue2使用的配置生成文件
loader原理浅析
loader主要用于处理webpack不能处理的资源,其本质是一个函数,详细参考官方文档
以下为一个简单的babel-loader:
const { getOptions } = require('loader-utils'); // 获取传入loader的options选项
const { validate } = require('schema-utils'); // 验证传入options是否符合schema规则
const babel = require('@babel/core'); // babel核心编译库
const util = require('util'); // 通用方法
const babelSchema = require('./babelSchema.json'); // 自定义的schema规则,json格式
// babel.transform用来编译代码的方法
// 是一个普通异步方法
// util.promisify将普通异步方法转化成基于promise的异步方法
const transform = util.promisify(babel.transform);
module.exports = function (content, map, meta) { // content为资源内容,map为源码与编译后代码映射,meta元数据,map和meta都是可选项
// 获取loader的options配置
const options = getOptions(this) || {};
// 校验babel的options的配置
validate(babelSchema, options, {
name: 'Babel Loader' // 验证出错时,报错的loader名
});
// 创建异步loader,调用callback相当于return
const callback = this.async(); // this.callback为同步loader
/* callback接收参数schema(ts表示法)
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
*/
// 使用babel编译代码
transform(content, options)
.then(({code, map}) => callback(null, code, map, meta))
.catch((e) => callback(e))
}
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
// 多个loader的执行顺序总是从后往前,但pitch正好相反,传入data在执行阶段会挂载到this下,可以传递共享信息,还可利用pitch函数跳过某些loader
}
plugin原理浅析
plugin是一个类,因此使用时需要new操作符实例化
plugin通过hook,捕获在每个编译(compilation)中触发的所有关键事件。在编译的每一步,plugin都具备完全访问webpack的compiler对象的能力,如果情况合适,还可以访问当前compilation对象。
- tapable是webpack的核心工具,提供了plugin接口,简单的tapable类:
const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require('tapable');
class Lesson {
constructor() {
// 初始化hooks容器
this.hooks = {
// 同步hooks,任务回依次执行
// go: new SyncHook(['address'])
// SyncBailHook:一旦有返回值就会退出~
go: new SyncBailHook(['address']),
// 异步hooks
// AsyncParallelHook:异步并行
// leave: new AsyncParallelHook(['name', 'age']),
// AsyncSeriesHook: 异步串行
leave: new AsyncSeriesHook(['name', 'age'])
}
}
tap() {
// 往hooks容器中注册事件/添加回调函数
this.hooks.go.tap('class0318', (address) => {
console.log('class0318', address);
return 111;
})
this.hooks.go.tap('class0410', (address) => {
console.log('class0410', address);
})
this.hooks.leave.tapAsync('class0510', (name, age, cb) => {
setTimeout(() => {
console.log('class0510', name, age);
cb();
}, 2000)
})
this.hooks.leave.tapPromise('class0610', (name, age) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('class0610', name, age);
resolve();
}, 1000)
})
})
}
start() {
// 触发hooks
this.hooks.go.call('c318');
this.hooks.leave.callAsync('jack', 18, function () {
// 代表所有leave容器中的函数触发完了,才触发
console.log('end~~~');
});
}
}
const l = new Lesson();
l.tap();
l.start();
- 简单的copy插件
const path = require('path');
const fs = require('fs');
const {promisify} = require('util')
const { validate } = require('schema-utils');
const globby = require('globby');
const webpack = require('webpack');
const schema = require('./schema.json');
const { Compilation } = require('webpack');
const readFile = promisify(fs.readFile);
const {RawSource} = webpack.sources
class CopyWebpackPlugin {
constructor(options = {}) {
validate(schema, options, { // 验证options是否符合规范
name: 'CopyWebpackPlugin'
})
this.options = options;
}
apply(compiler) {
// 初始化compilation
compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
// 添加资源的hooks
compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
// 将from中的资源复制到to中,输出出去
const { from, ignore } = this.options;
const to = this.options.to ? this.options.to : '.';
// context就是webpack配置
// 运行指令的目录
const context = compiler.options.context; // process.cwd()
// 将输入路径变成绝对路径
const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);
// 1. 过滤掉ignore的文件
// globby(要处理的文件夹,options)
const paths = await globby(absoluteFrom, { ignore });
console.log(paths); // 所有要加载的文件路径数组
// 2. 读取paths中所有资源
const files = await Promise.all(
paths.map(async (absolutePath) => {
// 读取文件
const data = await readFile(absolutePath);
// basename得到最后的文件名称
const relativePath = path.basename(absolutePath);
// 和to属性结合
// 没有to --> reset.css
// 有to --> css/reset.css
const filename = path.join(to, relativePath);
return {
// 文件数据
data,
// 文件名称
filename
}
})
)
// 3. 生成webpack格式的资源
const assets = files.map((file) => {
const source = new RawSource(file.data);
return {
source,
filename: file.filename
}
})
// 4. 添加compilation中,输出出去
assets.forEach((asset) => {
compilation.emitAsset(asset.filename, asset.source);
})
cb();
})
})
}
}
module.exports = CopyWebpackPlugin;
webpack基本工作流程
- 初始化 Compiler:webpack(config) 得到 Compiler 对象
- 开始编译:调用 Compiler 对象 run 方法开始执行编译
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,递归直到所有模块被加载进来
- 完成模块编译: 在经过第 4 步使用 Loader 编译完所有模块后,得到了每个模块被编译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。(注意:这步是可以修改输出内容的最后机会)
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统