目录
Babel:JS编译器(es6->es5,jsx->js)
loader:编译->js
less-loader:less->css
css-loader:css->js
style-loader:
创建style标签,将js中的样式资源插入标签内,并将标签添加到head中生效
ts-loader:
打包编译Typescript文件
执行顺序:出栈(从后往前)
plugin:发布订阅、广播、监听
优、删、简:压缩代码和图片
对AST抽象语法树进行优化
Tree Shaking:未使用的代码移除
压缩算法:简化表达式,字符串压缩
分类
html-webpack-plugin :
处理html资源,默认会创建一个空的HTML,自动引入打包输出的所有资源(js/css)
mini-css-extract-plugin:
打包过后的css在js文件里,该插件可以把css单独抽出来
clean-webpack-plugin :
每次打包时候,CleanWebpackPlugin 插件就会自动把上一次打的包删除
loader和plugin的区别:loader运行在编译阶段,plugins 在整个周期都起作用
热更新加载原理
代码变动->重新编译->局部更新->无需刷新页面
websocket:本地、浏览器的双向通信、hash编译标识、生成时间判断变化
与vite区别:webpack更新整个module、vite更新单个文件
懒加载
代码分割:拆分成块,需要时候再加载
commonJS:import动态导入
减小初始加载时间: 只加载必需的代码块
并行加载: 浏览器支持并行加载多个资源
缓存优化: 只有发生更改的代码块需要重新下载
原理/手写
Loader
Node 模块导出函数(source源码){return 处理后的source}
同步:return/this.callback
异步:promise+
Node.js 7.6.0+
配置
链式调用:上一个loader返回作为下一个loader参数
Plugin:带有apply方法的class
Tapable:订阅发布
compiler:编译过程
配置:webpack.config.js 里 require 并实例化
webpack
未涉及 loader 和 plugin
Plugin API | webpack 中文文档 | webpack中文文档 | webpack中文网
Webpack基于common JS规范,它将根据模块的依赖关系进行静态分析,然后将这些模块( js、css、less )按照指定的规则生成对应的静态资源,减少了页面的请求。
将es6、es7、es8等语法转换成浏览器可识别的es5或es3语法,即浏览器兼容的语法,比如将箭头函数转换为普通函数
将jsx转换成浏览器认的js
阶段: parsing (js解析为ast)、transforming (ast转换优化)、generating (ast生成js)
1)通过babylon
将 js 转化成 ast (抽象语法树)
2)通过babel-traverse
是一个对 ast 进行遍历,使用 babel 插件转化成新的 ast
3)通过babel-generator
将 ast 生成新的 js 代码
webpack只认识JS和JSON,所以Loader相当于翻译官,将其他类型资源进行预处理,最终变为js代码。
开发中,会使用less预处理器编写css样式,使开发效率提高)
将css文件变成commonjs模块(模块化的规范)加载到js中,模块内容是样式字符串
postcss-loader要写在最后(其实只要放在css-loader之后就可以)
{ test: /\.(less|css)?$/, loader: ["style-loader", "css-loader", "less-loader", "postcss-loader"]}
webpack会按照从右到左的顺序执行loader,我们新解析less,之后进行css的打包编译。如果你不适用less等预处理语言,安装css-loader和style-loader即可。
发布订阅、广播、监听
底层是利用发布订阅模式
,webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,在特定的时机对资源做处理
在代码压缩过程中,Terser 会将 JavaScript 代码解析成 AST(Abstract Syntax Tree,抽象语法树)结构,然后对 AST 进行操作和优化。AST 是一种将代码抽象化的数据结构,它可以更方便地进行代码分析、转换和优化。
Terser 通过 Tree Shaking 技术,可以将未使用的代码从打包后的文件中移除,从而减小文件体积。Tree Shaking 是一种静态分析技术,它可以分析代码中哪些部分是可达的,哪些部分是不可达的。
常通过设置 mode
为 'production' 来开启。在生产模式下,Webpack 会自动开启一些优化,包括 Tree Shaking。
// webpack.config.js
module.exports = {
mode: 'production',
// other configurations...
};
Terser 采用了一些压缩算法,例如变量重命名、死代码移除、字符串压缩、简化表达式等,来进一步压缩 JavaScript 代码。
前端性能优化——包体积压缩82%、打包速度提升65% - 掘金
代码变动,webpack 重新编译,编译后浏览器替换修改的模块,局部更新,无需刷新整个页面
热加载是通过内置的 HotModuleReplacementPlugin 实现的
websocket
:本地、浏览器的双向通信、hash编译标识、生成时间判断变化1) 通过webpack-dev-server
开启server服务
,本地 server 启动之后,再去启动 websocket 服务,建立本地服务和浏览器的双向通信
2) webpack 每次编译后,会生成一个Hash值
,Hash 代表每一次编译的标识。本次输出的 Hash 值会编译新生成的文件标识,被作为下次热更新的标识
3)webpack监听文件变化
(主要是通过文件的生成时间判断是否有变化),当文件变化后,重新编译
4)编译结束后,本地服务器通知浏览器请求变化的资源,同时将新生成的 hash 值传给浏览器,用于下次热更新使用
5)浏览器通过 jsonp 拉取更新的模块后,用新模块替换掉旧的模块,从而实现了局部刷新
jsonp 回调触发模块热替换逻辑。Vite把需要在启动过程中完成的工作,转移到响应浏览器请求的过程中
之后reload
页面时,首屏的性能会好很多(缓存)
动态加载的文件,需要做 resolve、load、transform、parse
操作,并且还有大量的 http
请求
源代码直接上线:虽然过程可控,但是http请求多,性能开销大。
打包成唯一脚本:服务器压力小,但是页面空白期长,用户体验不好。
本质其实就是在源代码直接上线
和打包成唯一脚本main.bundle.js
这两种极端方案之间的一种更适合实际场景的中间状态。
const loaderUtils = require("loader-utils");
// 定义一个导出函数,这个函数将被 Webpack loader 调用
module.exports = function(source) {
// source源码
const content = doSomeThing2JsString(source);
// 获取用户配置的options
const options = loaderUtils.getOptions(this);
// 输出当前 loader 执行时的上下文路径,用于解析其他模块路径
console.log('this.context');
/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,
* 进而省去重复生成 AST 的过程
*/
// 通过 this.callback 向 Webpack 返回处理后的结果,这是 loader 的标准写法
this.callback(null, content);
// 或者直接使用 return content; 也是可以的,与 this.callback 实现相同的效果
}
如果计算量很小,同步也可以,但尽可能的异步化 Loader,
尤其是在执行的操作可能会耗费较长时间的情况下。这是因为 Webpack 在构建过程中是一个基于异步操作的系统,而异步化的 loader 有助于提高整体构建性能和并行性
this.async
返回一个类似于 Node.js 中的回调函数,它接受两个参数,第一个是错误对象(如果有错误的话),第二个是处理后的内容
module.exports = function(content){
function timeout(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("{};" + content)
}, delay)
})
}
const callback = this.async()
timeout(1000).then(data => {
callback(null, data)
})
}
module.exports = async function(content){
function timeout(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("{};" + content)
}, delay)
})
}
const data = await timeout(1000)
return data
}
// webpack.config.js
module.exports = {
module: {
rules: [
{
//匹配文件后缀名.css等
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
// options:自定义配置
options: {
// 自定义配置给 loader-name-A
// 可以根据 loader 的文档提供适当的选项
// 例如,以下是 loader-name-A 可能使用的某些自定义选项
optionA: true,
optionB: 'value',
},
},
{
loader: 'loader-name-B',
}
]
},
]
}
}
loader
是支持以数组的形式配置多个的,因此当Webpack
在转换该文件类型的时候,会按顺序链式调用每一个loader
,前一个loader
返回的内容会作为下一个loader
的参数
class SyncHook{
constructor(){
this.hooks = [];
}
// 订阅事件
tap(name, fn){
this.hooks.push(fn);
}
// 发布
call(){
this.hooks.forEach(hook => hook(...arguments));
}
}
compiler:编译过程
compiler
是 Webpack 的主要编译实例,代表了整个编译过程。
Webpack 在运行时会创建一个 compiler
对象,
它包含了完整的 Webpack 配置信息,生命周期的各个阶段的钩子(hooks)。
//@file: plugins/myplugin.js
class myPlugin {
constructor(options){
//用户自定义配置
this.options = options
console.log(this.options)
}
apply(compiler) {
console.log("This is my first plugin.")
}
}
module.exports = myPlugin
webpack.config.js
里 require
并实例化const MyPlugin = require('./plugins/myplugin-4.js')
module.exports = {
......,
plugins: [
new MyPlugin("Plugin is instancing.")
]
}
1)webpack 从项目的entry
入口文件开始递归分析,调用所有配置的 loader
对模块进行编译
2)babel
将 js ->ast抽象语法树
,
3)babel-traverse
对 ast 进行遍历,找到文件的import引用节点
4)每个模块生成一个唯一的 id,并将解析过的模块缓存
起来,根据依赖关系生成依赖图谱
5)递归遍历所有依赖图谱的模块,组装成一个个包含多个模块的 Chunk(块)
把所有依赖打包成一个 或多个bundle.js文件(捆bundle)浏览器可识别的JavaScript文件。
6)最后将生成的文件输出到 output
的目录中
Webpack通过一个给定的主/入口文件(如:index.js)开始找到项目的所有依赖文件,
解析js->ast语法树->json数据结构
将es6 es7 等高级的语法->es5
递归遍历引入的其他 js,生成最终的依赖关系图谱
最终生成一个可以在浏览器加载执行的 js 文件
const fs = require('fs');
const path = require('path');
// babylon解析js语法,生产AST 语法树
// ast将js代码转化为一种JSON数据结构
const babylon = require('babylon');
// babel-traverse是一个对ast进行遍历的工具, 对ast进行替换
const traverse = require('babel-traverse').default;
// 将es6 es7 等高级的语法转化为es5的语法
const { transformFromAst } = require('babel-core');
// 每一个js文件,对应一个id
let ID = 0;
// filename参数为文件路径, 读取内容并提取它的依赖关系
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8');
// 获取该文件对应的ast 抽象语法树
const ast = babylon.parse(content, {
sourceType: 'module'
});
// dependencies保存所依赖的模块的相对路径
const dependencies = [];
// 通过查找import节点,找到该文件的依赖关系
// 因为项目中我们都是通过 import 引入其他文件的,找到了import节点,就找到这个文件引用了哪些文件
traverse(ast, {
ImportDeclaration: ({ node }) => {
// 查找import节点
dependencies.push(node.source.value);
}
});
// 通过递增计数器,为此模块分配唯一标识符, 用于缓存已解析过的文件
const id = ID++;
// 该`presets`选项是一组规则,告诉`babel`如何传输我们的代码.
// 用`babel-preset-env`将代码转换为浏览器可以运行的东西.
const { code } = transformFromAst(ast, null, {
presets: ['env']
});
// 返回此模块的相关信息
return {
id, // 文件id(唯一)
filename, // 文件路径
dependencies, // 文件的依赖关系
code // 文件的代码
};
}
// 我们将提取它的每一个依赖文件的依赖关系,循环下去:找到对应这个项目的`依赖图`
function createGraph(entry) {
// 得到入口文件的依赖关系
const mainAsset = createAsset(entry);
const queue = [mainAsset];
for (const asset of queue) {
asset.mapping = {};
// 获取这个模块所在的目录
const dirname = path.dirname(asset.filename);
asset.dependencies.forEach((relativePath) => {
// 通过将相对路径与父资源目录的路径连接,将相对路径转变为绝对路径
// 每个文件的绝对路径是固定、唯一的
const absolutePath = path.join(dirname, relativePath);
// 递归解析其中所引入的其他资源
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id;
// 将`child`推入队列, 通过递归实现了这样它的依赖关系解析
queue.push(child);
});
}
// queue这就是最终的依赖关系图谱
return queue;
}
// 自定义实现了require 方法,找到导出变量的引用逻辑
function bundle(graph) {
let modules = '';
graph.forEach((mod) => {
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`;
});
const result = `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`;
return result;
}
// ❤️ 项目的入口文件
const graph = createGraph('./example/entry.js');
const result = bundle(graph);
// ⬅️ 创建dist目录,将打包的内容写入main.js中
fs.mkdir('dist', (err) => {
if (!err)
fs.writeFile('dist/main.js', result, (err1) => {
if (!err1) console.log('打包成功');
});
});
Webpack揭秘——走向高阶前端的必经之路 - 掘金