目录
创建style标签,将js中的样式资源插入标签内,并将标签添加到head中生效
[Tree Shaking:未使用的代码移除](#Tree Shaking:未使用的代码移除)
[html-webpack-plugin :](#html-webpack-plugin :)
处理html资源,默认会创建一个空的HTML,自动引入打包输出的所有资源(js/css)
[clean-webpack-plugin :](#clean-webpack-plugin :)
[每次打包时候,CleanWebpackPlugin 插件就会自动把上一次打的包删除](#每次打包时候,CleanWebpackPlugin 插件就会自动把上一次打的包删除)
[loader和plugin的区别:loader运行在编译阶段,plugins 在整个周期都起作用](#loader和plugin的区别:loader运行在编译阶段,plugins 在整个周期都起作用)
websocket:本地、浏览器的双向通信、hash编译标识、生成时间判断变化
与vite区别:webpack更新整个module、vite更新单个文件
[减小初始加载时间: 只加载必需的代码块](#减小初始加载时间: 只加载必需的代码块)
[并行加载: 浏览器支持并行加载多个资源](#并行加载: 浏览器支持并行加载多个资源)
[缓存优化: 只有发生更改的代码块需要重新下载](#缓存优化: 只有发生更改的代码块需要重新下载)
[Node 模块导出函数(source源码){return 处理后的source}](#Node 模块导出函数(source源码){return 处理后的source})
[Node.js 7.6.0+](#Node.js 7.6.0+)
[配置:webpack.config.js 里 require 并实例化](#配置:webpack.config.js 里 require 并实例化)
[未涉及 loader 和 plugin](#未涉及 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之后就可以)
javascript
{ 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。
javascript
// 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
javascript
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 实现相同的效果
}
异步:promise+
如果计算量很小,同步也可以,但尽可能的异步化 Loader,
尤其是在执行的操作可能会耗费较长时间的情况下。这是因为 Webpack 在构建过程中是一个基于异步操作的系统,而异步化的 loader 有助于提高整体构建性能和并行性
this.async
返回一个类似于 Node.js 中的回调函数,它接受两个参数,第一个是错误对象(如果有错误的话),第二个是处理后的内容
javascript
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+
javascript
module.exports = async function(content){
function timeout(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("{};" + content)
}, delay)
})
}
const data = await timeout(1000)
return data
}
配置
javascript
// 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 :订阅发布
javascript
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)。
javascript
//@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
并实例化
javascript
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
-
Webpack通过一个给定的主/入口文件 (如:index.js )开始找到 项目的所有依赖文件,
-
解析js->ast语法树->json数据结构
-
将es6 es7 等高级的语法->es5
-
递归遍历引入的其他 js,生成最终的依赖关系图谱
-
最终生成一个可以在浏览器加载执行的 js 文件
javascript
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('打包成功');
});
});