一,什么是webpack
WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。
二,webpack 4->5
- 持久化存储优化构建性能
- 更好的算法与 defalut 来改善长效缓存(long-term caching)
- 更好的 Tree Shaking 和代码生成来改善 bundle 的大小
- 清理内部结构而不引入任何破坏性的变化;删除了polyfill脚本
- 引入破坏性更改来为新特性做准备,以便尽可能长的使用v5版本。
三,特殊改进点
1,webpack 4 没有分析模块 export 与 import 之间的依赖关系。webpack 5 有一个新的选项 optimization.innerGraph,该选项在生产模式下默认启用,它对模块中的符号进行分析以找出从 export 到 import 的依赖关系。
import { something } from "./something";
function usingSomething() {
return something;
}
export function test() {
return usingSomething();
}
内部图算法将确定仅在使用 export 的 test 时使用 something。这样可以将更多 export 标记为未使用,并从 bundle 中删除更多的代码。
2,持久缓存
webpack读取入口文件(entry),然后递归查找所依赖的模块(module),构建成一个“依赖图”,然后根据配置中的加载器(loader)和打包策略来对模块进行编译。
webpack 5利用持久缓存优化了整个流程,当检测到某个文件变化时,依照“依赖图”,只对修改过的文件进行编译,从而大幅提高了编译速度。
cache: {
// 1. Set cache type to filesystem
type: "filesystem",
buildDependencies: {
// 2. Add your config as buildDependency to get cache invalidation on config change
config: [__filename]
// 3. If you have other things the build depends on you can add them here
// Note that webpack, loaders and all modules referenced from your config are automatically added
}
}3,删除polyfill脚本
最开始,webpack的目标是允许在浏览器中运行大多数的Node模块,但是现在模块格局已经发生了重大变化,现在有很多模块是专门为前端开发的。在v4及以前的版本中,对于大多数的Node模块将自动添加polyfill脚本(腻子脚本)。然而,这些大量繁杂的脚本都会添加到最终编译的代码中(bundle),但其实通常情况下是没有必要的。在v5版本中将尝试停止自动地添加polyfill脚本,转而专注于前端兼容模块。
4,最低NODE版本从6升级到8
目前已经可以安装 Webpack 5 了,但官网最新稳定版本还是 Webpack 4。
| |
四,webPack4 VS webPack5
1,目录结构
// hello.js
const something = 'something'
function usingSomething() {
return something;
}
export function test() {
console.log('test')
return usingSomething();
}
//index.js
import {test} from '../src/hello'
test();
"dev": "webpack --mode development",
"build": "webpack --mode production"2,npm run dev && build
3,export 与 import 之间的依赖关系 对比
4,效率提升
按演讲中的测试效果,16000 模块的单页应用使用持久化缓存,编译速度可以提高 98%。
五,全链路探寻
整体流程:
六,万物起源
const webpack = (options, callback) => {
validateSchema(webpackOptionsSchema, options);
compiler = createCompiler(options);
compiler.watch(watchOptions, callback);
return compiler;
};七,validateSchema 模块
参数一:webpackOptionsSchema
webpckOptions.json
{
"definitions":{...},
"type":"object",
"additionalProperties":false,
"properties":{.....}
}拿一小段代码来看:
"loader": {
"description": "Custom values available in the loader context.",
"type": "object"
},
"mode": {
"description": "Enable production optimizations or development hints.",
"enum": ["development", "production", "none"]
},
"module": {
"description": "Options affecting the normal modules (`NormalModuleFactory`).",
"anyOf": [
{
"$ref": "#/definitions/ModuleOptions"
}
]
},作用:针对optins的配置,进行效验,比如loader的type要求object,"$ref": "#/definitions/ModuleOptions"就是这个JSON文件下的definitions/ModuleOptions。这种写法,主要是提取公共配置,避免代码冗余。
参数二:options
module.exports = {
context: __dirname,
mode: 'development',
devtool: 'source-map',
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/,
}
]
}
}3,validate(schema, options, {})
- _ajv
ajv 是一个非常流行的JSON Schema验证工具,将一个JSON配置文件转换成一个对象,用于检测对象的合法性
// Node.js require:
var Ajv = require('ajv');
// or ESM/TypeScript import
import Ajv from 'ajv';
var ajv = new Ajv(); // options can be passed, e.g. {allErrors: true}
var validate = ajv.compile(schema);
var valid = validate(data);
if (!valid) console.log(validate.errors);- _ajvKeywords
var Ajv = require('ajv');
var ajv = new Ajv;
require('ajv-keywords')(ajv);
ajv.validate({ instanceof: 'RegExp' }, /.*/); // true
ajv.validate({ instanceof: 'RegExp' }, '.*'); // false使用ajv-keywords给json schema添加自定义关键字
- _absolutePath
ajv.addKeyword('absolutePath', {
errors: true,
type: 'string',
compile(schema, parentSchema) {}
}- _ValidationError
case 'number':
return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;
case 'integer':
return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;
case 'string':
return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;
case 'boolean':
return `${dataPath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;作用:错误提示汇总,简称错误密码本
简单使用:
const Ajv = require('ajv');
let schema = {
type: 'object',
required: ['username', 'email', 'password'],
properties: {
username: {
type: 'string',
minLength: 4
},
email: {
type: 'string',
format: 'email'
},
password: {
type: 'string',
minLength: 6
},
age: {
type: 'integer',
minimum: 0
},
sex: {
enum: ['boy', 'girl', 'secret'],
default: 'secret'
},
}
};
let ajv = new Ajv();
let validate = ajv.compile(schema);
let valid = validate(data);
if (!valid) console.log(validate.errors);我们声明了一个数据模式schema ,这个模式要求目标数据为一个对象,对象可以有五个字段 ,并分别定义了五个字段的类型和数据格式要求,并且其中 username、email、password 必填。然后我们使用这个模式去验证用户输入的数据 data 是否满足我们的需求。
4,validateObject(schema, options)
if (Array.isArray(options)) {
errors = Array.from(options).map(nestedOptions => validateObject(schema, nestedOptions));
} else {
errors = validateObject(schema, options);
}作用:跟进options来判断是单配置还是多配置
const compiledSchema = ajv.compile(schema);
const valid = compiledSchema(options);
if (!compiledSchema.errors) {
return [];
}
return valid ? [] : filterErrors(compiledSchema.errors);作用:validate会依次从data中取出需要校验的key,按照JSON文件中的规则进行判断。
此时 拿到的compiledSchema为:
调用堆栈:
作用:如果没有问题就直接返回[],如果检测失败就交个filterErrors处理;
filterErrrors处理函数:
function filterErrors(errors) {
let newErrors = [];
for (const error of errors) {
let children = [];
newErrors = newErrors.filter(oldError => {
children.push(oldError);
}
});
newErrors.push(error);
}
return newErrors;
} 八,WebpackOptionsDefaulter 模块
let compiler;
compiler = createCompiler(options);
options = new WebpackOptionsDefaulter().process(options);
作用:传进去一个options 输出一个options
很明显是是给options添加默认配置
就是给options多挂在几个默认属性,至于怎么添加的,添加了什么, 我们来看下optionsDefaulter类
1,OptionsDefaulter 类
//工具方法
const getProperty = (obj, path) => {}
const setProperty = (obj, path, value) => {}
class OptionsDefaulter {
constructor(){
this.defaults = {};
this.config = {};
}
//原型方法
process(options){
}
set(name,config,def){
}
}2,getProperty 方法
const getProperty = (obj, path) => {
let name = path.split(".");
for (let i = 0; i < name.length - 1; i++) {
obj = obj[name[i]];
if (typeof obj !== "object" || !obj || Array.isArray(obj)) return;
}
return obj[name.pop()];
};作用:这个函数是尝试获取对象的某个键,键的递进用点来连接,如果获取失败返回undefined。避免了多次判断 obj[key] 是否为undefined
2,webpackOptionstDefaulter类
this.set("experiments", "call", value => ({ ...value }));
this.set("experiments.asset", false);
this.set("experiments.mjs", false);
this.set("experiments.importAwait", false);
this.set("experiments.importAsync", false);
this.set("experiments.topLevelAwait", false);
this.set("experiments.syncWebAssembly", false);
this.set("experiments.asyncWebAssembly", false);
this.set("experiments.outputModule", false);
this.set("entry", "./src");
添加前 options
执行后options
一句话总结:WebpackOptionsDefaulter模块对options配置对象添加了大量的默认参数。
九,Compiler 模块
webpack的主要引擎,在compiler对象记录了完整的webpack环境信息,在webpack从启动到结束,compiler只会生成一次。你可以在compiler对象上读取到webpack config信息,outputPath等;
const compiler = new Compiler(options.context);class Compiler {
constructor(context){
this.hooks = Object.freeze({
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
afterDone: new SyncHook(["stats"]),
....
})
options
}
watch(watchOptions, handler) {
...
}
run(callback){
....
}
}1, tapable 模块(超级管家)
Tapable的核心功能就是依据不同的钩子将注册的事件在被触发时按序执行。它是典型的”发布订阅模式“,webpack的灵活配置得益于 Tapable 提供强大的钩子体系,让编译的每个过程都可以“钩入”,在webpack中订阅的事件最终都会放入taps栈中,订阅分为三种类型async, sync, promise,调用时会通过_createCall方法调用HookCodeFactory的create方法创建委托函数
compiler-hooks钩子:webpack.js.org/api/compile…
2,NodeEnvironmentPlugin
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);作用:该插件的主要作用是给complier初始化输入输出文件系统和监视文件系统。
class NodeEnvironmentPlugin {
constructor(options) {
this.options = options || {};
}
apply(compiler){
compiler.inputFileSystem = new CachedInputFileSystem(fs, 60000);
const inputFileSystem = compiler.inputFileSystem;
compiler.outputFileSystem = fs;
compiler.intermediateFileSystem = fs;
compiler.watchFileSystem = new NodeWatchFileSystem(
compiler.inputFileSystem
);
//第一个钩子
compiler.hooks.beforeRun.tap()
}
}- 输出文件系统:非常简单,就是包装了一层node原生的fsapi。
- 输入文件系统:复杂,目前还没有看见使用的地方。
- 监视文件系统:复杂,目前还没有看见使用的地方,但是主要的作用就是监视文件改动及热更新等。
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}作用:webpack.config.js中的plugins挂载:
紧接着 注册两个钩子
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();3,WebpackOptionsApply模块
compiler.options = new WebpackOptionsApply().process(options, compiler);- optionsApply
class OptionsApply {
process(options, compiler) {}
}- WebpackOptionsApply
class WebpackOptionsApply extends OptionsApply {
constructor() {
super();
}
process(options, compiler) {
compiler.outputPath = options.output.path;
compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;
compiler.recordsOutputPath =
options.recordsOutputPath || options.recordsPath;
...
case "web": {
const JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
const FetchCompileWasmPlugin = require("./web/FetchCompileWasmPlugin");
const FetchCompileAsyncWasmPlugin = require("./web/FetchCompileAsyncWasmPlugin");
const NodeSourcePlugin = require("./node/NodeSourcePlugin");
const ChunkPrefetchPreloadPlugin = require("./prefetch/ChunkPrefetchPreloadPlugin");
new JsonpTemplatePlugin().apply(compiler);
new FetchCompileWasmPlugin({
mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);
new FetchCompileAsyncWasmPlugin().apply(compiler);
new NodeSourcePlugin(options.node).apply(compiler);
new LoaderTargetPlugin(options.target).apply(compiler);
new ChunkPrefetchPreloadPlugin().apply(compiler);
break;
}
....
new JavascriptModulesPlugin().apply(compiler);
new JsonModulesPlugin().apply(compiler);
....
compiler.hooks.afterPlugins.call(compiler);
...
}
}作用:
- 1.处理options.target参数
- 2.处理options.output,options.externals,options.devtool参数
- 3.对于引用了巨量的模块把把this指向compiler对象
- 4.处理options.optimization 的moduleIds和chunkIds属性
- 5,处理各种插件
- 6,hooks事件流
这个模块主要是根据options选项的配置,设置compile的相应的插件,属性,里面写了大量的 apply(compiler); 使得模块的this指向compiler
new Compiler时:
执行完毕时:
执行完毕后的hooks:
戏说下:
楚汉争霸,楚国大军进攻,探子回报,一支穿云箭射出,options
汉高祖刘邦收到后,着令密探司(validateSchema)查验情报真伪,密探司对照密码本(validate)翻译无误,确认楚军,人数 10W,步兵,骑兵齐全,约定乌江大战,
刘邦命张良(WebpackOptionsDefaulter ),根据国内情况,做好站前动员,针对密报,输出详细的章程。(new options),
再命 韩信为大将军(Compiler),萧何为后勤主管(tapable),制定作战计划(HOOKS),并派出侦查营(NodeEnvironmentPlugin),随时通报敌军情况。
韩信受命后,先根据密报中敌军队伍配置(plugins),按照相克原则排兵布阵,并对整个做作战计划进行了详细的补充,比如战争前,战争中,战争后等各个阶段做好方案和准备。
方案完成后,上报刘邦,通过,大军开拔,,,,,
万事具备,只欠东风:
compiler.run()