webpack

目录

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 )按照指定的规则生成对应的静态资源减少了页面的请求

Babel:JS编译器(es6->es5,jsx->js)

将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 代码

loader:编译->js

webpack只认识JS和JSON,所以Loader相当于翻译官,将其他类型资源进行预处理,最终变为js代码。

less-loader:less->css

开发中,会使用less预处理器编写css样式,使开发效率提高)

css-loader:css->js

将css文件变成commonjs模块(模块化的规范)加载到js中,模块内容是样式字符串

style-loader:

创建style标签,将js中的样式资源插入标签内,并将标签添加到head中生效

ts-loader:

打包编译Typescript文件

执行顺序:出栈(从后往前)

  • style-loader 将css插入到页面的style标签
  • css-loader 是将@import 和 url() 转换成 import/require()的
  • less-loader 是将less文件编译成css
  • postcss是目前css兼容性的解决方案,会自动帮我们加入前缀,以使css样式在不同的浏览器能兼容

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即可。

plugin:发布订阅、广播、监听

优、删、简:压缩代码和图片

底层是利用发布订阅模式,webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,在特定的时机对资源做处理

对AST抽象语法树进行优化

在代码压缩过程中,Terser 会将 JavaScript 代码解析成 AST(Abstract Syntax Tree,抽象语法树)结构,然后对 AST 进行操作和优化。AST 是一种将代码抽象化的数据结构,它可以更方便地进行代码分析、转换和优化。

Tree Shaking:未使用的代码移除

Terser 通过 Tree Shaking 技术,可以将未使用的代码从打包后的文件中移除,从而减小文件体积。Tree Shaking 是一种静态分析技术,它可以分析代码中哪些部分是可达的,哪些部分是不可达的。

  • 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
  • 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
  • 先进行 tree shaking,然后再使用 Babel 进行转译

常通过设置 mode 为 'production' 来开启。在生产模式下,Webpack 会自动开启一些优化,包括 Tree Shaking。

// webpack.config.js
module.exports = {mode: 'production',// other configurations...
};
压缩算法:简化表达式,字符串压缩

Terser 采用了一些压缩算法,例如变量重命名、死代码移除、字符串压缩、简化表达式等,来进一步压缩 JavaScript 代码。

分类

html-webpack-plugin :

处理html资源,默认会创建一个的HTML,自动引入打包输出的所有资源(js/css)

mini-css-extract-plugin:

打包过后的css在js文件里,该插件可以把css单独抽出来

clean-webpack-plugin :

每次打包时候,CleanWebpackPlugin 插件就会自动把上一次打的包删除

loader和plugin的区别:loader运行在编译阶段,plugins 在整个周期都起作用

前端性能优化——包体积压缩82%、打包速度提升65% - 掘金

热更新加载原理

代码变动->重新编译->局部更新->无需刷新页面

代码变动,webpack 重新编译,编译后浏览器替换修改的模块,局部更新,无需刷新整个页面

热加载是通过内置的 HotModuleReplacementPlugin 实现的

websocket:本地、浏览器的双向通信、hash编译标识、生成时间判断变化

1) 通过webpack-dev-server开启server服务,本地 server 启动之后,再去启动 websocket 服务,建立本地服务和浏览器的双向通信

2) webpack 每次编译后,会生成一个Hash值Hash 代表每一次编译的标识。本次输出的 Hash 值会编译新生成的文件标识,被作为下次热更新的标识

3)webpack监听文件变化(主要是通过文件的生成时间判断是否有变化),当文件变化后,重新编译

4)编译结束后,本地服务器通知浏览器请求变化的资源,同时将新生成的 hash 值传给浏览器,用于下次热更新使用

5)浏览器通过 jsonp 拉取更新的模块后,用新模块替换掉旧的模块,从而实现了局部刷新

与vite区别:webpack更新整个module、vite更新单个文件

jsonp 回调触发模块热替换逻辑。Vite把需要在启动过程中完成的工作,转移到响应浏览器请求的过程中

之后reload页面时,首屏的性能会好很多(缓存

懒加载

动态加载的文件,需要做 resolve、load、transform、parse 操作,并且还有大量的 http请求

代码分割:拆分成块,需要时候再加载

commonJS:import动态导入

源代码直接上线:虽然过程可控,但是http请求多,性能开销大。

打包成唯一脚本:服务器压力小,但是页面空白期长,用户体验不好。

本质其实就是在源代码直接上线打包成唯一脚本main.bundle.js这两种极端方案之间的一种更适合实际场景的中间状态。

减小初始加载时间: 只加载必需的代码块

并行加载: 浏览器支持并行加载多个资源

缓存优化: 只有发生更改的代码块需要重新下载

原理/手写

Loader

Node 模块导出函数(source源码){return 处理后的source}

同步:return/this.callback

const loaderUtils = require("loader-utils");// 定义一个导出函数,这个函数将被 Webpack loader 调用
module.exports = function(source) {// source源码const content = doSomeThing2JsString(source);// 获取用户配置的optionsconst 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 实现相同的效果
}

异步:promise+

如果计算量很小,同步也可以,但尽可能的异步化 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)})
}
Node.js 7.6.0+
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返回作为下一个loader参数

loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的参数

Plugin:带有apply方法的class

Tapable订阅发布

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 = optionsconsole.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.")]
}

webpack

1)webpack 从项目的entry入口文件开始递归分析,调用所有配置的 loader对模块进行编译

2)babel将 js ->ast抽象语法树

3)babel-traverse对 ast 进行遍历,找到文件的import引用节点

4)每个模块生成一个唯一的 id,并将解析过的模块缓存起来,根据依赖关系生成依赖图谱

5)递归遍历所有依赖图谱的模块,组装成一个个包含多个模块的 Chunk(块)

把所有依赖打包成一个 或多个bundle.js文件(捆bundle)浏览器可识别的JavaScript文件。

6)最后将生成的文件输出到 output 的目录中

未涉及 loader 和 plugin

  1. Webpack通过一个给定的主/入口文件(如:index.js)开始找到项目的所有依赖文件,

  2. 解析js->ast语法树->json数据结构

  3. 将es6 es7 等高级的语法->es5

  4. 递归遍历引入的其他 js,生成最终的依赖关系图谱

  5. 最终生成一个可以在浏览器加载执行的 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揭秘——走向高阶前端的必经之路 - 掘金