前言
前端技术发展之快,各种可以提高开发效率的新思想和框架层出不穷。但是他们都有一个共同特点:源代码无法直接运行,必须通过转换后才能正常运行。
构建工具就是做这件事,将源代码转换成可以执行的JavaScript、CSS、HTML 代码,包括如下内容:
- 代码转换:将 TypeScript 编译成JavaScript、将 SCSS 编译成 CSS等。
- 文件优化:压缩JavaScript、CSS、HTML 代码,压缩合并图片等。
- 代码分割:提取多个页面的公共代码,提取首屏不需要执行部分代码让其异步记在。
- 模块合并:在采用模块化的项目里会有很多个模块和文件,需要通过构建功能将模块分类合并成一个文件。
- 自动刷新:监听本地源代码变化,自动重新构建、刷新浏览器。
- 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
- 自动发布:更新代码后,自动构建出线上发布代码并传输给发布系统。
构建其实是工程化、自动化思想在前端开发中的体现,将一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程。构建为前端开发注入了更大的活力,解放了我们的生产力。
先后出现了一系列构建工具,他们各有优缺点。由于前端工程师很熟悉 JavaScript,Node.js 又可以胜任所有构建需求,所以大多数构建工具都是用 Node.js 开发的。
构建工具发展史:
Grunt
第一个有名的 Task Runner(任务执行者)。采用了插件架构,配置复杂,难以理解。有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化地执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件gruntfile.js里:
// gruntfile.js
module.exports = function(grunt) {
// 所有插件的配置信息
grunt.initConfig({
// 获取package.json文件信息
pkg: grunt.file.readJSON('package.json'),
// uglify插件的配置信息
uglify: {
options: {},
build: {
src: 'src/test.js',
dest: 'build/<%=pkg.name%>_<%=pkg.version%>.min.js' // 在 build 文件夹下输出 package.name_package.version.min.js 的压缩文件
}
},
// watch插件的配置信息
watch: {
another: {
files: ['lib/*.js'],
}
}
});
// 告诉Grunt我们将使用这些插件
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
// 告诉Grunt我们在终端中启动Grunt时需要执行哪些任务
grunt.registerTask('dev', ['uglify','watch']);
};
在项目根目录下执行命令grunt dev,就会启动JavaScript文件压缩和自动刷新功能
缺点:集成度不高,要写很多配置后才可以用,无法做到开箱即用。
Gulp
Gulp最大的特点是引入了流的概念,同时提供了一系列常用插件去处理流,流可以在插件之间传递,大致使用如下:
// gulpfile.js
// 引入 Gulp
var gulp = require("gulp");
// 引入插件
var jshint = require("gulp-jshint");
var sass = require("gulp-sass");
var concat = require("gulp-concat");
....
// 便宜SCSS任务
gulp.task('scss', function() {
// 读取文件,通过管道喂给插件
gulp.src('./scss/*.scss')
// SCSS 插件将 scss 文件编译成 css
.pipe(sass())
// 输出文件
.pipe(guilp.dest('./css'));
});
// 合并压缩 JavaScript 文件
gulp.task('scripts', function() {
gulp.src('./js/*.js')
.pipe(concat('all.js'))
.pipe(uglify())
.pipe(gulp.dest('./dest'));
});
// 监听文件变化
gulp.task('watch', function() {
// 当 SCSS 文件被编辑时执行 SCSS 任务
gulp.watch('./scss/*.scss', ['sass']);
gulp.watch('./js/*.js', ['scripts']);
});。
缺点:和Grunt 类似。集成度不高,要写很多配置后才可以用,无法做到开箱即用。
Browserify
第一个流行的JavaScript模块打包工具,它使用Node.js风格的CommonJS模块系统来实现模块化开发。在Browserify中,每个模块都是一个单独的文件,通过require函数引入其他模块。Browserify会将这些模块打包成一个单独的JavaScript文件,可以在浏览器中使用。
npm install -g browserify-cli
npm install browserify --save-dev
// 通过browserify命令,将index.js编译成bundle.js
browserify index.js -o bundle.js
优点:完全简化了模块的开发流程,减少了开发成本,即不用引入额外的模块加载器,也不用过多地考虑模块之间依赖关系。模块定义和加载均与node.js一致,无需更改。
缺点: 编译的文件体积会变得很大,需要包含所有模块。
Parcel
yarn global add parcel-bundler
parcel build entry.js --out-dir build/output
最近新起的Web 应用打包工具,适用于经验不同的开发者。它利用多核处理提供了极快的速度,并且不需要任何配置。
Parcel的优点:
- 极速打包。Parcel 使用 worker 进程去启用多核编译。同时有文件系统缓存,即使在重启构建后也能快速再编译。
- 开箱即用。对 JS, CSS, HTML, 文件 及更多的支持,而且不需要插件。
- 自动转换。如若有需要,Babel, PostCSS, 和PostHTML甚至 node_modules 包会被用于自动转换代码.
- 热模块替换。Parcel 无需配置,在开发环境的时候会自动在浏览器内随着你的代码更改而去更新模块。
- 友好的错误日志。当遇到错误时,Parcel 会输出 语法高亮的代码片段,帮助你定位问题。
缺点:
- 不支持SourceMap:在开发模式下,Parcel也不会输出SourceMap,目前只能去调试可读性极低的代码;
- 不支持剔除无效代码(TreeShaking):很多时候我们只用到了库中的一个函数,结果Parcel把整个库都打包了进来;
- 一些依赖会让Parcel出错:当你的项目依赖了一些Npm上的模块时,有些Npm模块会让Parcel运行错误;
webpack
中文文档:webpack.docschina.org/concepts/#e…
Webpack 5 运行于 Node.js v10.13.0+ 的版本。
核心概念
entry:入口文件
module.exports = {
entry: './src/index.js', // 默认
};
output: 输出文件
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js', //默认
},
};
loader
webpack 只能处理 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。
注意:loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)
const path = require('path');
// test 属性,识别出哪些文件会被转换。
// use 属性,定义出在进行转换时,应该使用哪个 loader。
module.exports = {
output: {
filename: 'index.bundle.js',
},
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true,
},
},
{ loader: 'sass-loader' },
],
},
],
},
};
plugin:插件
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
mode:模式
通过选择 development, production 或 none 之中的一个,来设置 mode 参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production。
module.exports = {
mode: 'production',
};
Tapable
Tapable是webpack的核心模块,也是webpack团队维护的,是webpack plugin的基本实现方式。主要的作用就是:基于事件流程管理。Tapable是一个小型的库,允许你对一个 JavaScript 模块添加和应用插件。它可以被继承或混入到其他模块中。类似于 Node.js 的 EventEmitter 的类,专注于自定义事件的触发和处理。除此之外,Tapable 还允许你通过回调函数的参数,访问事件的 触发者(emittee)或 提供者(producer),从而控制着 Webpack 的插件系统。
// 注册回调事件
run.tapAsync('myRun', (compiler) => {
console.log(compiler)
})
// 触发
run.callAsync(compiler)
参考文献:tsejx.github.io/webpack-gui…
complier和compilation
开发插件时最重要的两个概念是 complier编译器和 compilation编译对象。理解它们的角色是扩展 webpack 引擎重要的第一步。
- Compiler类:webpack的主要引擎,扩展自Tapable。webpack 从执行到结束,Compiler只会实例化一次。生成的 compiler 对象记录了 webpack 当前运行环境的完整的信息,该对象是全局唯一的,配置项传递的实例化插件以及webpack内置插件都会在该 compiler 对象上注册。插件可以通过它获取到 webpack config 信息,如entry、output、loaders等配置。
- Compilation类:扩展自Tapable,也提供了很多关键点回调供插件做自定义处理时选择使用拓展。一个 compilation 对象代表了一次单一的版本构建和生成资源,它储存了当前的模块资源、编译生成的资源、变化的文件、以及被跟踪依赖的状态信息。简单来说,Compilation的职责就是对所有 require 图(graph)中对象的字面上的编译,构建 module 和 chunk,并利用插件优化构建过程,同时把本次打包编译的内容全存到内存里。compilation 编译可以多次执行,如在watch模式下启动 webpack,每次监测到源文件发生变化,都会重新实例化一个compilation对象,从而生成一组新的编译资源。这个对象可以访问所有的模块和它们的依赖(大部分是循环依赖)。
一个插件类基本结构示例
// 一个插件类基本结构示例:
class HelloCompilationPlugin {
constructor(options = {}) {
// 省略构造器部分
}
// 调用原型方法 apply 并传入 compiler 对象
apply(compiler) {
// 给 compilation 钩子注册 'HelloCompilationPlugin',回调会在 compilation 对象创建之后触发
compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
// 回调参数是 compilation 对象,因此这里可以使用各种可用的 compilation hooks(钩子)
compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
// 优化阶段开始时触发
console.log('资源正在优化');
});
});
// 在 emit 阶段,即输出 asset 到 output 目录之前触发回调函数
compiler.hooks.emit.tapAsync(
'HelloCompilationPlugin',
(compilation, callback) => {
console.log('This is an example plugin!');
console.log(
'`compilation`对象,即资源的一个单一版本的构建',
compilation
);
// 使用webpack提供的 plugin API 操作构建
compilation.addModule(/* ... */);
callback();
}
);
}
}
module.exports = HelloCompilationPlugin;
// webpack.config.js
const HelloCompilationPlugin = require('./HelloCompilationPlugin.js');
module.exports = {
plugins: [
//... 其他插件
// 创建插件实例
new HelloCompilationPlugin({...options})
]
}
区别:
- compiler 对象代表的是构建过程中不变的 webpack 环境,整个 webpack 从启动到关闭的生命周期。针对的是webpack。
- compilation 对象只代表一次新的编译,在准备编译某一个模块(例如 index.js)的时候才会创建,主要存在于 compile 到 make 这一段生命周期里面。同时只要项目文件有改动,compilation 就会被重新创建。针对的是随时可变的项目文件。
构建流程
从 webpack(config, callback) 函数开始
webpack 函数接收两个参数:
- config:就是 webpack.config.js 中的配置
- callback:回调函数,可传可不传
webpack 函数做的事情:
1、定义了 create 函数,create 函数主要做的事是:
- 定义了 compiler 对象及一些其他参数(watch,watchOptions)
- 通过 createCompiler(options) 创建 compiler
- 将 compiler 返回
2、判断 webpack 函数执行的时候有没有传入 callback:
- 传入了 callback,通过 create 函数拿到 compiler 对象,执行 compiler.run,并把 compiler 返回 ;里面还有一层判断 config 文件有没有配置 watch,如果有,会监听文件改变,重新编译
- 没有传入 callback,通过 create 函数拿到 compiler 对象,直接返回 compiler
createCompiler函数
1.通过 new Compiler 得到一个 compiler 对象
2.NodeEnvironmentPlugin 把文件系统挂载到 compiler 对象上,如 infrastructureLogger(log插件)、inputFileSystem(文件输入插件)、outputFileSystem(文件输出插件)、watchFileSystem(监听文件输入插件) 等
3.注册所有的插件。如果插件是一个函数,用 call 的形式调用这个函数,并把 compiler 当参数;如果插件是对象形式,那么插件身上都会有 apply 这个函数,调用插件的 apply 函数并把 compiler 当参数
4.调用 compiler 身上的一些钩子(environment、afterEnvironment)
5.WebpackOptionsApply().process 处理 config 文件中除了 plugins 的其他属性(将传入的 webpack.config.js 的属性(例如 devtool)转换成 webpack 的 plugin 注入到 webpack 的生命周期)
6.返回 compiler
new Compiler
1.使用 tapable 初始化了一系列的钩子
2.定义run函数
- 定义了一个错误处函数 finalCallback
- 定义了一个 onCompiled 函数,作为 this.compile 执行的回调函数
- 定义了 run,主要流程就是:beforeRun 钩子 --> run 钩子 --> this.compile,如果遇到 error,就执行 finalCallback
- 执行 compiler.run 内部定义的 run()
compiler.compile ()
1.钩子 beforeCompile
2.钩子 compile
3.通过 this.newCompilation 返回一个 compilation 对象
4.钩子 make:使用 compilation 对模块执行编译的 // this.hooks.make.callAsync
5.钩子 finishMake
6.compilation.finish
7.compilation.seal:执行 seal 对 make 阶段处理过的 module 代码进行封装输出
8.钩子 afterCompile
make钩子的注册
1、 createCompiler
// lib\webpack.js
const createCompiler = rawOptions => {
// ...
new WebpackOptionsApply().process(options, compiler);
}
2、 WebpackOptionsApply().process()
// lib\WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
// ...
process(options, compiler) {
//...
new EntryOptionPlugin().apply(compiler);
}
}
3、EntryOptionPlugin().apply(compiler)
// lib\EntryOptionPlugin.js
static applyEntryOption(compiler, context, entry) {
// ...
const EntryPlugin = require("./EntryPlugin");
}
4、EntryPlugin.apply()
// lib\EntryPlugin.js
class EntryPlugin {
apply(compiler) {
// ...
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
// 通过 compilation 的 addEntry 添加入口,从入口开始编译
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
}
}
可以看到,这个注册回调函数里面调用了 compilation.addEntry(),这个 Compilation 类里面的 addEntry 主要的作用就是添加入口模块
5、 compilation
-
compilation.addEntry=>compilation._addEntryItem,通过入口将模块添加到 module tree(模块树)
-
调用了 compilation.handleModuleCreation,在里面通过不断的回调,执行了以下几步:
- compilation.factorizeModule 系列:处理模块并对模块进行创建,并且添加到 factorizeQueue 队列
- compilation.addModule 系列:添加 module 模块
- compilation.buildModule 系列:准备编译
6、进行模块编译构建(build)
build 阶段主要做的以下几件事:
-
使用 loader-runner 运行 Loader
-
Parser 解析出 AST
-
walkStatements 解析出依赖
-
如果当前模块有依赖,那么继续递归进行 build 流程
compilation.js
handleModuleCreation => addModule => _handleModuleBuildAndDependencies => buildModule => buildQueue.add() => _buildModule => module.build(多态的概念, Module 只是一个父类,后面的子类可以继承 Module,并对里面的 build 方法进行改写。这里实际上执行的并不是 Module 的 build,而是它的子类 NormalModule 的 build)=> NormalModule.js build
NormalModule.js
NormalModule.js build => _doBuild => runLoaders(主要用来执行 loader 对匹配到的模块进行转换, 并将结果交给 processResult ) => processResult => build callback => this.parser.parse
processModuleDependencies 对 module 递归进行依赖收集
到这里,make 阶段算是完结。
对处理完成的模块 module 封装输出
lib\Compilation.js seal函数(完成从 module 到 chunks 的转化)
1.创建 chunkGraph 实例
2.遍历 compilation.modules ,记录下模块与 chunk 关系
3.循环 this.entries 入口文件创建 chunks
4.执行各种优化 module、chunk、module tree
5.调用 compilation.codeGeneration 方法用于生成编译好的代码
6.执行代码生成的方法 compilation._runCodeGenerationJobs
- _runCodeGenerationJobs 中会执行 compilation._codeGenerationModule,这个方法会根据 tempalte 生成代码
7.执行 compilation.createChunkAssets 创建 chunkAssets 资源,createChunkAssets 里面会调用 compilation.emitAsset 将生成的代码放到 compilation.assets 中,然后一路回调,最后的回调就是用的createChunkAssets(callback) 中的 callback,然后一路找回调,会发现最后调用的回调是 compilation.seal(callback)这里的,而 compilation.seal 在 compiler.compile 中被调用,此时,又回到了 compiler
总结
1.初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
2.开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
3.确定入口:根据配置中的 entry 找出所有的入口文件。
4.编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
5.完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
6.输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
7.输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
| 阶段 | 关键钩子 | 说明 |
|---|---|---|
| 创建编译器:createCompiler() | environment | 读取环境 |
| 创建编译器:createCompiler() | afterEnvironment | 读取环境后触发 |
| 创建编译器:createCompiler() | initialize | 初始化 |
| 编译器运行:compiler.run() | beforeRun | 运行前的准备活动,主要启动了文件读取功能 |
| 编译器运行:compiler.run() | run | “机器”已经跑起来了,在编译之前有缓存,则启用缓存,这样可以提高效率。 |
| 编译器编译:compiler.compile(onCompiled) | beforeCompile | beforeCompile开始编译前的准备,创建的ModuleFactory,创建Compilation,并绑定ModuleFactory到Compilation上。同时处理一些不需要编译的模块,比如ExternalModule(远程模块)和DllModule(第三方模块) |
| 编译器编译:compiler.compile(onCompiled) | compile | 进行编译 |
| 编译器编译:compiler.compile(onCompiled) | make | 编译的核心流程 |
| 编译器编译:compiler.compile(onCompiled) | afterCompile | 编译结束 |
| 编译结束后进行输出(onCompiled()) | shouldEmit | 获取compilation发来的电报,确定编译时候成功,是否可以开始输出了。 |
| 编译结束后进行输出(onCompiled()) | emit | 输出文件 |
| 编译结束后进行输出(onCompiled()) | afterEmit | 输出完毕 |
| 编译结束后进行输出(onCompiled()) | done | 所有流程结束 |
打包后产物
(() => {
"use strict";
// 保存webpack已经注册的模块,一个键值对的对象
var __webpack_modules__ = ({
"./src/index.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _utils_math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/math */ \"./src/utils/math.js\");\n\nconsole.log((0,_utils_math__WEBPACK_IMPORTED_MODULE_0__.sum)(20, 30));\n\n//# sourceURL=webpack://myprogram/./src/index.js?");
}),
"./src/utils/math.js":((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"sum\": () => (/* binding */ sum)\n/* harmony export */ });\nfunction sum(num1, num2) {\n return num1 + num2;\n}\n\n//# sourceURL=webpack://myprogram/./src/utils/math.js?");
})
});
// 模块缓存
var __webpack_module_cache__ = {};
// 对应import(es6),实现模块加载和缓存,模块管理核心
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
// 对应export,用来定义导出变量对象
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();
(() => {
// 工具函数,判断是否有某属性
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
(() => {
// 区分是否es模块,给导出导出变量对象添加__esModule:true属性,用来兼容es和commonJS等模块的
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();
TreeShaking
在前端的性能优化中,es6 推出了tree shaking机制,tree shaking就是当我们在项目中引入其他模块时,他会自动将我们用不到的代码,或者永远不会执行的代码摇掉。
只支持ES6 Module代码,在production 环境默认开启,开发模式需要设置。
module.exports = {
optimization: {
usedExports: true
}
}
-
rollup 是在编译打包过程中分析程序流,得益于于 ES6 静态模块(exports 和 imports 不能在运行时修改),我们在打包时就可以确定哪些代码时我们需要的。
-
webpack 本身在打包时只能标记未使用的代码而不移除,而识别代码未使用标记并完成 tree-shaking 的 其实是 UglifyJS、babili、terser 这类压缩代码的工具。简单来说,就是压缩工具读取 webpack 打包结果,在压缩之前移除 bundle 中未使用的代码
SourceMap
开发环境下默认生成的Source Map ,记录的是生成后的代码的位置。会导致运行时报错的行数与源代码的行数不一致的问题;在webpack.config.js中添加如下配置,即可保证运行时报错的行数与源代码的行数保持一致
module.exports = {
mode:"development",
//eval-source-map 仅限在开发模式下使用,不建议在生成模式下使用
//此选项生成的 Source Map 能保证运行时的报错行数与源代码的行数保持一致
devtool:'eval-source-map',
}
生产环境下,可以将devtool的值配置为 nosources-source-map ,只定位报错的具体行数,不会暴漏源码,
DllPlugin
事先把常用但又构建时间长的代码提前打包好(例如 react、react-dom),取个名字叫 dll。后面再打包的时候就跳过原来的未打包代码,直接用 dll。这样一来,构建时间就会缩短,提高 webpack 打包速度。
Vite
开发环境
-
利用浏览器原生的ES Module编译能力,省略费时的编译环节,直给浏览器开发环境源码,dev server只提供轻量服务。
-
浏览器执行ESM的import时,会向dev server发起该模块的ajax请求,服务器对源码做简单处理后返回给浏览器。
-
Vite中HMR是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块失活,使得无论应用大小如何,HMR 始终能保持快速更新。
-
使用esbuild处理项目依赖,esbuild使用go编写,比一般node.js编写的编译器快10-100 倍。
核心原理
1、当声明一个 script标签类型为 module 时,如
<script type="module" src="/src/main.js"></script>
2、当浏览器解析资源时,会往当前域名发起一个GET请求 main.js文件
// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
3、请求到了main.js文件,会检测到内部含有import引入的包,又会import 引用发起HTTP请求获取模块的内容文件,如App.vue、vue文件
Vite其核心原理是利用浏览器现在已经支持ES6的import,碰见import就会发送一个HTTP请求去加载文件,Vite启动一个 koa 服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回返回给浏览器。Vite整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的webpack开发编译速度快出许多!
在Vite出来之前,传统的打包工具如Webpack是先解析依赖、打包构建再启动开发服务器,Dev Server 必须等待所有模块构建完成,当我们修改了 bundle模块中的一个子模块, 整个 bundle 文件都会重新打包然后输出。项目应用越大,启动时间越长。
而Vite利用浏览器对ESM的支持,当 import 模块时,浏览器就会下载被导入的模块。先启动开发服务器,当代码执行到模块加载时再请求对应模块的文件,本质上实现了动态加载。灰色部分是暂时没有用到的路由,所有这部分不会参与构建过程。随着项目里的应用越来越多,增加route,也不会影响其构建速度。
预构建
-
将非 ESM 规范的代码转换为符合 ESM 规范的代码;
-
将第三方依赖内部的多个文件合并为一个,减少 http 请求数量;
Vite预编译之后,将文件缓存在node_modules/.vite/文件夹下。根据以下地方来决定是否需要重新执行预构建。
-
package.json中:dependencies发生变化
-
包管理器的lockfile
如果想强制让Vite重新预构建依赖,可以使用--force启动开发服务器,或者直接删掉node_modules/.vite/文件夹。
Esbuild
而 Esbuild 之所以能这么快,主要原因有两个:
-
Go 语言开发,可以多线程打包,代码直接编译成机器码;
- Webpack 一直被人诟病构建速度慢,主要原因是在打包构建过程中,存在大量的 resolve、load、transform、parse 操作(详见 为什么有人说 vite 快,有人却说 vite 慢?- 快速的冷启动[10] ),而这些操作通常是通过 javascript 代码来执行的。要知道,javascript 并不是什么高效的语言,在执行过程中要先编译后执行,还是单线程并且不能利用多核 cpu 优势,和 Go 语言相比,效率很低。
-
可充分利用多核 cpu 优势;
require('esbuild')
.build({
entryPoints: ['main.ts'],
outfile: 'output.js',
bundle: true, // 内联打包 合并多个请求
format: 'esm', //支持的规范
loader: { '.ts': 'ts' },
watch: true
})
.then(() => console.log('Done'))
.catch(() => process.exit(1))
生产环境
- 集成Rollup打包生产环境代码,依赖其成熟稳定的生态与更简洁的插件机制。
vite 为什么使用rollup 打包。虽然浏览器对ESM 的支持已经很广泛了,但Vite 还是选择在生产环境时使用rollup 来打包,因为在生产环境下,使用未打包的ESM 会产生比较多的HTTP 请求,相对打包而言,效率还是比较低下的,所以Vite 上生产依旧打包,并使用了tree-shaking、懒加载等技巧
rollup
-
输出结果更加扁平,执行效率更高
-
自动移除未引用的代码
-
打包结果依然完全可读
源码阅读
启动服务器
createServer
-
HMR:使用chokidar监听文件的修改
-
optimizeDeps:预构建
-
创建httpServer,启动服务
预构建
createServer方法中的****initDepsOptimizer
请求拦截,资源编译
-
对于ESM是不支持裸模块引入的,需要使用相对路径或者绝对路径
-
浏览器只识别JS,对其他文件不识别,比如:vue、ts
热更新
snowpack
总结
Grunt 是开创者 Grunt 的出现是前端构建工具从0到1的变革,是具有开创意义的。在它之前我们经常都是通过 bash 或者 make 调用 closure-compiler 之类的工具。前端并不存在一个统一的构建工具和标准,甚至我们自己写过一些简单的构建工具。Grunt的出现终结了这种混乱的局面,前端领域有了自己的构建工具,和大部分人都差不多采用的构建流程。
Grunt 虽然是开创者的地位,但是,他仅仅是把混乱的构建过程统一化了,用起来方便了,其实本质上没有变。本质上我们依然是把 CSS, JS, HTML,图片等 各自打包,一个JS模块依赖的 CSS 等外部资源依然没有任何语法上的声明,我们甚至需要在组件说明中强调这个组件依赖哪个CSS,依赖那几张图片。也就是说,模块依赖的问题依然没有解决。
另外 Grunt 是直接面向文件操作的,每一个任务都是输入一个文件,然后输出一个文件。这样导致如果一个文件需要经过多次处理,在中间每一步都会写文件,这些文件其实并没有必要写。这样就导致Grunt的效率比较低。 比如我编译JS的时候,可能需要先 CoffeeScript 编译一下,然后 Uglify 一下,那么很多时候 Coffee 编译的文件我并不需要,我只要 Uglify 之后的文件,而写文件是很耗时的。
Gulp 只是做了改良 Gulp 基于Stream 就明显比 Grunt要更高效,且更任务组合灵活。我可以读入一个文件,然后进行多个操作,最终直接输出我要的结果,不存在中间的临时文件。 然而 Gulp 只是一个量变,它依然没能解决模块依赖的问题。
Webpack 划时代的解决了模块依赖的问题 Webpack 的出现比较完美解决了前端模块依赖的问题,任何资源都是JS,任何资源都可以在JS中声明依赖。这是具有划时代意义的。
装备项杂乱,社区丰厚 在webpack项目中需求运用的静态资源,css,以及less等预处理器需求手动引进对应的loader才干运用, 支撑代码分割,热更新,丰厚的插件系统,经过运用loader能够解析各种类型的资源等
缺陷:冷发动,热更新随着项目体量增大而变慢,体量大的项目热更新一次甚至需求几分钟
Rollup 是一个 JavaScript 模块打包器,他所负责的作业只是把咱们写的代码转化为js
特色:原生支撑tree shaking(去除无用代码),支撑一起生成umd、commonjs,es的代码
缺陷:
-
模块过于静态化,HMR很难完结
-
仅面向ES module,无法可靠的处理commonjs以及umd依靠
Vite 开箱即用 对于项目中需求的静态资源,html,less等无需手动引进loader来处理这些资源,直接引进对应的库即可;开发环境 根据esbuild进行预构建,不进行打包操作,依托于浏览器自身对es module的解析,热更新时只更新修正部分,从而到达短时间更新的作用;生产环境 经过rollup进行打包,rollup支撑原生的tree shaking所以打出来的包体量小