前端构建工具的诞生
-- images/
-- html/
-- css/
-- js/
早期的前端开发项目结构通常像上面这样,最终在 html 按适当的顺序引入 css、js 文件,随着项目的功能越来越多,会导致整个项目复杂度高、维护性差、项目极其不稳定,如代码复用性差、命名冲突等,也会导致开发效率很低。
于是诞生了模块化这个东西,将复杂的模块解耦为更小的模块,分离我们的关注点,大幅提高了我们的开发效率以及编程体验,同时也提高了代码复用性,降低了命名冲突的可能。
(function(module) {
...code
})(module)
在浏览器还不支持 ESModule 之前,浏览器是没法单独的加载某一个模块的,所以产生了像 webpack、rollup 这样的模块打包工具,用于将我们所有分散的模块进行聚合,最终生成 js、css、image 等静态资源。
随着浏览器迭代更新,又诞生了像 Vite 这样通过利用浏览器 ESMoudle 的能力,在 dev 环境下帮助我们更高效的进行开发
Webpack
从 webpack4 开始,可以零配置运行 webpack
Install
npm install webpack webpack-cli --save-dev
基本概念及使用
Entry
一次 build 可以有一个或者多个 entry,每个 entry 都对应生成一个 js 文件,entry 通常可以指定入口路径、输出文件名、依赖 chunk
const path = require('path');
module.exports = {
mode: "development",
entry: {
mian: './src/main.js',
index: '.src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
},
};
Output
- 指定 entry、异步 chunk 的输出文件名及其策略
- 输出一个 library
- 指定 publicPath
Loader
Loader 用于 module 的源代码转换,它们允许你在导入或者加载文件时对文件进行预处理,可以将不同语言转换为 javascript,例如 typescript、css、less、image、vue、react
Plugin
Plugin 是 webapck 的支柱,它自身就是由许多插件组合而成,我们可以订阅 webpack 各个阶段抛出的事件,来完成一些 loader 无法完成的事情
HMR
热更新,即不用刷新页面即可对更改的模块进行更新
devServer: {
hot: true
}
Code Spliting
将 js 文件拆分为更小的模块,降低初始化加载的 bundle 体积,提高缓存利用率
// 动态导入某个文件或者组件
import('./xxx.js').then(_ => {
console.log(_)
})
// context
const file = ['xxx.js', 'xxx1.js']
file.forEach(file => import(`./src/${file}`))
Module Federation
能轻易实现在两个使用 webpack 构建的项目之间共享代码,甚至组合不同的应用为一个应用。
内部原理
Runtime
我们现在的项目中会有很多不同的模块,通过 import 来导入不同的模块,形成一颗树的结构
// index.js
import a from './a.js'
import b from './b.js'
import c from './c.js'
function test(p) {
return p
}
test(1)
打包后的代码其实是通过一个对象将所有的模块代码存储下来
var __webpack_modules__ = ({
'./a.js': function (module, __webpack_exports__, __webpack_require__) {
// ...code
},
'./b.js': function (module, __webpack_exports__, __webpack_require__) {
// ...code
},
'./c.js': function (module, __webpack_exports__, __webpack_require__) {
// ...code
},
'./index.js': function (module, __webpack_exports__, __webpack_require__) {
const a = __webpack_require__('./a.js')
const b = __webpack_require__('./b.js')
const c = __webpack_require__('./c.js')
function test(p) {
return p
}
test(1)
module.exports = {}
__webpack_exports__.a = 1
}
})
webpack_require 的定义如下
var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
todo:异步组件如何加载
Tapable
Webpack 插件机制是基于发布订阅者模式,具体的实现是通过 Tapable 完成的。 playground
const {
SyncHook, // 同步并安顺序调用钩子
SyncBailHook, // 同步并安顺序调用钩子,出现熔断则停止后续执行
SyncWaterfallHook, // 同步 waterfall 钩子
SyncLoopHook, // 同步循环钩子
AsyncParallelHook, // 异步并行调用钩子
AsyncSeriesHook, // 异步串行调用钩子
} = require("tapable");
//SyncHook
const hook = new SyncHook(['arg1', 'arg2', 'arg3']);
hook.tap('flag1', (arg1, arg2, arg3) => {
console.log('flag1:', arg1, arg2, arg3);
});
hook.tap('flag2', (arg1, arg2, arg3) => {
console.log('flag2:', arg1, arg2, arg3);
});
hook.call('19Qingfeng', 'wang', 'haoyu');
//SyncBailHook
const hook = new SyncBailHook(['arg1', 'arg2', 'arg3']);
hook.tap('flag1', (arg1, arg2, arg3) => {
console.log('flag1:', arg1, arg2, arg3);
return true
});
hook.tap('flag2', (arg1, arg2, arg3) => {
console.log('flag2:', arg1, arg2, arg3);
});
hook.call('19Qingfeng', 'wang', 'haoyu');
//SyncWaterfallHook
const hook = new SyncWaterfallHook(['arg1']);
hook.tap('flag1', (arg1) => {
console.log('flag1:', arg1);
});
hook.tap('flag2', (arg1) => {
console.log('flag2:', arg1);
});
hook.tap('flag3', (arg1) => {
console.log('flag3:', arg1);
});
hook.call('19Qingfeng');
// SyncLoopHook
let flag1 = 2;
let flag2 = 1;
const hook = new SyncLoopHook(['arg1', 'arg2', 'arg3']);
hook.tap('flag1', (arg1, arg2, arg3) => {
console.log('flag1');
if (flag1 !== 3) {
return flag1++;
}
});
hook.tap('flag2', (arg1, arg2, arg3) => {
console.log('flag2');
if (flag2 !== 3) {
return flag2++;
}
});
hook.tap('flag3', (arg1, arg2, arg3) => {
console.log('flag3');
});
hook.call('19Qingfeng', 'wang', 'haoyu');
// AsyncSeriesHook
const hook = new AsyncSeriesHook(['arg1', 'arg2', 'arg3']);
console.time('timer');
hook.tapAsync('flag1', (arg1, arg2, arg3, callback) => {
console.log('flag1:', arg1, arg2, arg3);
setTimeout(() => {
callback();
}, 1000);
});
hook.tapPromise('flag2', (arg1, arg2, arg3) => {
console.log('flag2:', arg1, arg2, arg3);
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
});
});
hook.callAsync('19Qingfeng', 'wang', 'haoyu', () => {
console.log('全部执行完毕 done');
console.timeEnd('timer');
});
// AsyncParallelHook
const hook = new AsyncParallelHook(['arg1', 'arg2', 'arg3']);
console.time('timer');
hook.tapPromise('flag1', (arg1, arg2, arg3) => {
console.log('flag2:', arg1, arg2, arg3);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, 1000);
});
});
hook.tapAsync('flag2', (arg1, arg2, arg3, callback) => {
console.log('flag1:', arg1, arg2, arg3);
setTimeout(() => {
callback();
}, 1000);
});
hook.callAsync('19Qingfeng', 'wang', 'haoyu', () => {
console.log('全部执行完毕 done');
console.timeEnd('timer');
});
通过这种发布订阅的方式,可以 hack 到 webpack 构建的各个流程中,灵活性和扩展性极强,同时也将构建流程进行解耦,放到不同的插件中去执行
Loader
如何编写一个 loader
export default function (source) {
return newSource || { ast, sourcemap }
}
- Vue sfc : github.com/vuejs/core/…
- babel:babeljs.io/docs/en/bab…
- sass:sass-lang.com/documentati…
- less:lesscss.org/usage/#deve…
- ts:github.com/microsoft/T…
loader-runner 会负责 Loader 的执行
Module
在 webpack 中 Module 分为两种,一种是 NormalModule,一种是 ContextModule
// context
const file = ['xxx.js', 'xxx1.js']
file.forEach(file => import(`./src/${file}`))
对于上面这种方式产生的 Module 叫做 ContextModule,否则都为 NormalModule,module 其实就是对应到某一个具体的文件
class NormalModuleFactory extends ModuleFactory {
// ...
}
class NormalModule extends Module {
constructor({
...
type,
request,
loaders,
resource,
context,
parser,
parserOptions,
generator,
generatorOptions,
...
}) {
super(type, context || getContext(resource), layer);
...
// Info from Factory
/** @type {string} */
this.request = request;
this.parser = parser;
this.parserOptions = parserOptions;
/** @type {Generator} */
this.generator = generator;
this.generatorOptions = generatorOptions;
/** @type {string} */
this.resource = resource;
/** @type {LoaderItem[]} */
this.loaders = loaders;
/** @type {Dependency[]} */
this.dependencies = []
...
}
...
// 调用 loader 链,处理当前 module 对应的文件,分析出 dependencies
_doBuild() {...}
}
// 用于记录模块之间的依赖关系
class ModuleGraph {
constructor() {
/** @type {WeakMap<Dependency, ModuleGraphConnection>} */
this._dependencyMap = new WeakMap();
/** @type {Map<Module, ModuleGraphModule>} */
this._moduleMap = new Map();
/** @type {WeakMap<any, Object>} */
this._metaMap = new WeakMap();
/** @type {WeakTupleMap<any[], any>} */
this._cache = undefined;
/** @type {Map<Module, WeakTupleMap<any, any>>} */
this._moduleMemCaches = undefined;
}
}
Chunk
一个 chunk 对应一个输出的文件,例如一个 entry 其实就是一个 chunk,一个 Chunk 包含多个 module,表示当前 Chunk 输出的内容包含了哪些模块
在 seal 阶段,会根据 module 及其之间的依赖关系,生成多个 chunk
Compiler
编译器,主要负责一些编译前的前置工作,和编译完成后的文件输出工作,具体编译流程在 Compilation 中
class Compiler {
constructor(context, options = /** @type {WebpackOptions} */ ({})) {
this.hooks = Object.freeze({
...
});
...
this.webpack = webpack;
/** @type {string=} */
this.name = undefined;
/** @type {Compiler} */
this.root = this;
/** @type {string} */
this.outputPath = "";
/** @type {Watching} */
this.watching = undefined;
this.options = options;
this.context = context;
/** @type {boolean} */
this.running = false;
/** @type {boolean} */
this.idle = false;
/** @type {Compilation} */
this._lastCompilation = undefined;
...
}
run() {
// ...
}
}
调用 run 方法启动构建
Compilation
每次编译都会创建一个 Compilation,来存储此次编译过程中所有的相关信息,还包含编译过程中的相关处理流程
class Compilation {
constructor(compiler, params){
/** @type {string=} */
this.name = undefined;
this.startTime = undefined;
this.endTime = undefined;
/** @type {Compiler} */
this.compiler = compiler;
this.options = options;
this.moduleGraph = new ModuleGraph();
/** @type {ChunkGraph} */
this.chunkGraph = undefined;
this.addModuleQueue = new AsyncQueue({
name: "addModule",
parent: this.processDependenciesQueue,
getKey: module => module.identifier(),
processor: this._addModule.bind(this)
});
/** @type {AsyncQueue<FactorizeModuleOptions, string, Module | ModuleFactoryResult>} */
this.factorizeQueue = new AsyncQueue({
name: "factorize",
parent: this.addModuleQueue,
processor: this._factorizeModule.bind(this)
});
/** @type {AsyncQueue<Module, Module, Module>} */
this.buildQueue = new AsyncQueue({
name: "build",
parent: this.factorizeQueue,
processor: this._buildModule.bind(this)
});
this.entries = new Map();
this.modules = new Set();
this.assets = {};
}
_addModule() {
// ...
}
_factorizeModule() {
// ...
}
_buildModule() {
// ...
}
addEntry() {
// ...
}
seal() {
// ...
}
}
Plugin
class MyExampleWebpackPlugin {
apply(compiler) {
compiler
.hooks
.emit
.tapAsync('MyExampleWebpackPlugin',(compilation, callback) => {
// ...
});
}
}
整体流程
Vite
特点是快,体现在以下方面:
-
Dev 环境
- 使用 ESbuild 对项目源代码中,使用到的 node_module 中的依赖进行分别单独打包
- 项目自身源代码无需进行构建,利用浏览器支持 ESMoudule 的能力,直接加载模块,并且只会加载用到的模块
- 热更新也很快
-
Production 环境
- 使用 rollup
Rollup
build process
Output process
ESbuild
esbuild-loader:github.com/privatenumb…