本文最后更新于 2024-03-22T23:32:58+00:00
模块化 定义:将代码按照功能划分为独立、可复用的单元,每个单元称为一个模块。
发展历程 无模块 将 JS 代码直接在 html 里面按顺序引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 function add (x, y ) { return x + y }function subtract (x, y ) { return x - y }function multiply (x, y ) { return x * y }function divide (x, y ) { return x / y }function info (msg ) { console .info ("[ info msg ] >" , msg); } <!DOCTYPE html><html > // ... <body > <script src ="./calc.js" > </script > <script src ="./log.js" > </script > </body > </html >
缺点:
变量名全局污染,如calc.js
里面定义了function add()
,则其他地方就不能再使用add
了,否则就会被覆盖
代码只能通过 html 里面关联,如想在log.js
里面使用add
函数,就只有在 html 将calc.js
引入代码放在log.js
前面,然后才能用add
函数,JS 多了则难以维护
模块化雏形 - IIFE 基于立即执行函数,形成函数作用域,可解决变量名全局污染
的问题,但还是只能放在 html 里面关联
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 var calc = (function ( ) { function add (x, y ) { return x + y; } function subtract (x, y ) { return x - y; } function multiply (x, y ) { return x * y; } function divide (x, y ) { return x / y; } return { add, subtract, multiply, divide }; })();var log = (function ( ) { function info (msg ) { console .info ("[ info msg ] >" , msg); } function error (msg ) { console .error ("[ error msg ] >" , msg); } function add (msg ) { console .warn ("[ add msg ] >" , msg); } return { info, error, add }; })();<html > // ... <body > <script src ="./calc.js" /> <script src ="./xxx.js" /> <script > console .log ("[ calc ] >" , calc); console .log ("[ log ] >" , log); </script > </body > </html >
高速发展:CJS、AMD、UMD CJS:node 端的模块加载规范,仅支持同步的,语法为module.exports = {}、reqiure('./xx/xx.js')
AMD:浏览器端的模块加载规范,可支持异步,语法为如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 define (function ( ) { return { name : 'simpleModule' , doSomething : function ( ) { console .log ('Doing something...' ); } }; });define (['dependency1' , 'dependency2' ], function (dep1, dep2 ) { return { method : function ( ) { dep1.someFunction (); dep2.anotherFunction (); } }; });require (['myModule' ], function (myModule ) { myModule.doSomething (); });
UMD:将内容输出支持:IIFE、CJS、AMD 三种格式的语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (function (root, factory ) { if (typeof define === 'function' && define.amd ) { define (['dependency1' , 'dependency2' ], factory); } else if (typeof module === 'object' && module .exports ) { module .exports = factory (require ('dependency1' ), require ('dependency2' )); } else { root.MyModule = factory (root.dependency1 , root.dependency2 ); } }(this , function (dependency1, dependency2 ) { function MyModule ( ) { } return MyModule ; }));
虽然是高速发展了,但编码复杂性、全局污染、浏览器支持性等上都存在问题
新时代:官方下场 - ESM 最终官方下场,从语法层给出模块加载方式规范,即 ECMAScript Modules,关键词为import、export
,终结了混乱的模块加载规范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export const PI = 3.14 ;export default function myDefaultExport ( ) { }export function func1 ( ) {}export function func2 ( ) {} <script type="module" > import log from "./js/log.js" ; console .log ("[ log ] >" , log); </script><script type ="module" src ="./js/log.js" > </script >
解决了以下问题:
每个模块有独立的作用域,不会再污染全局
支持同步、异步
解决模块循环引用问题
缺点:对低版本浏览器不支持
总结 所以什么是模块化呢?就是将代码分割成可复用的单元,并且通过某种规范实现互相引用
模块化是前端工程化的基石
一些考点 CJS node 端提出的模块加载机制,不支持异步,不支持浏览器,每个文件都有自己的作用域。 导出语法:
1 2 3 4 5 6 function add ( ) {}module .exports = { add }exports .add = add
导入语法,require
永远引入module.exports
的值,对应 JS 文件可省略文件后缀
1 const { add } = require ('./add.js' )
特点 动态(同步):当代码执行到require
那行时才去加载对应的文件并执行文件内容,可以理解为是“同步”的reqiure
伪代码实现(node 端),所以它的是同步,并且是对值的“拷贝”
1 2 3 4 function require (filePath ) { const content = fs.readFileSync (filePath); return eval (content); }
对值的“拷贝” 代码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let a = 1 setTimeout (() => a++, 500 )exports .a = aconst { a } = require ('./a.js' )console .log (a)setTimeout (() => console .log (a), 1000 )1 1
module.exports 与 exports 初始时module.exports === exports 为 true
,等价于**const exports = module.exports**
1 2 3 4 5 6 7 8 function add ( ) {}function subtract ( ) {}exports .add = addexports .subtract = subtractconsole .log (exports ) console .log (module .exports )
但当它们同时存在时,最终require
得到的是module.exports
的值
1 2 3 4 5 6 7 8 9 10 function add ( ) {}function subtract ( ) {}exports .add = addexports .subtract = subtractmodule .exports = { add }console .log (exports ) console .log (module .exports )
所以为了避免混淆,同一文件只使用一种导出方式
对循环依赖的处理 a.js 引入 b.js,b.js 引入 a.js,则形成了循环依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const b = require ("./b" );console .log ("b" , b);exports .a = 1 ;const a = require ("./a" );console .log ("a" , a);exports .b = 2 ;
结论:谁先执行,则它里面的能拿到值,并且伴随循环引用
的报错
ESM ECMAScript 提供的模块加载规范,支持浏览器、node,支持异步、同步 导出语法
1 2 3 export function add ( ) {}export function subtract ( ) {}
导入语法,对应 JS 文件默认不可省略文件后缀,除非有配置
1 import { add, subtract } from './add.js'
特点 静态(异步):是因为 ESM 的核心流程是分成三步(构建、实例化、求值),并且可以分别完成,所以称为异步
为什么要分成三个,不能直接一起吗?因为浏览器加载执行 JS 时会阻塞主线程,造成的后果很大。
构建:根据import
创建模块之间的依赖关系图(编译时输出),然后下载模块文件生成模块记录(记录 importName、importUrl) 实例化:基于生成的模块记录,找到模块的代码与导出的变量名,然后将相同导入、导出指向同一个地址 求值:运行模块的代码,将值赋到实例化
后的地址内
对值的“引用” 代码展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export let a = 1 setTimeout (() => a++, 500 )import { a } from './a.js' console .log (a)setTimeout (() => console .log (a), 1000 )1 2
export 与 export default 这是两种导出方式,可并存,只是import
的逻辑不同export default
之后的值将被导出,可以是任意类型,但import
时只能命名为一个变量,就算export default
了一个对象,也不支持解构,该变量的值等于export default
之后的值,一个文件只能有一个export default
1 2 3 4 5 6 7 8 9 10 11 12 function error (msg ) { console .error ("[ error msg ] >" , msg); }function add (msg ) { console .warn ("[ add msg ] >" , msg); }export default { error, add };import log from './log.js' import { error } from './log.js'
export
之后的值将被导出,可以是任意类型,但import
时只能当做对象来解构其值,就算export
了一个基础类型,也不支持作为一个变量使用(除非使用* as
语法)
1 2 3 4 5 6 7 8 9 10 11 12 13 function error (msg ) { console .error ("[ error msg ] >" , msg); }function add (msg ) { console .warn ("[ add msg ] >" , msg); }export { error, add };import { error } from './log.js' import log from './log.js' import * as log from './log.js'
总结:export default
导出的只能作为变量使用,export
导出的只能解构使用
对循环依赖的处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { b } from "./b" console .log ("b" , b);export const a = 1 ;import { a } from "./a" console .log ("a" , a);export const b = 2 ;
结论: 直接报错
Webpack 官方文档
核心概念(了解下即可,后面会讲原理的) Sourcemap 文件指纹技术 Babel 与 AST TreeShaking 优化:构建速度、提高页面性能 原理:Webpack、Plugin、Loader
手写实现 Webpack 打包基本原理 初始化项目
随便创建个项目文件,然后创建src
文件夹与空文件
1 mkdir src && touch src/add.js && touch src/minus.js && touch src/index.js && touch index.html
初始化项目pnpm init
,然后安装依赖pnpm add fs-extra
src/add.js
写入相关代码
1 export default (a, b) => a + b;
src/minus.js
写入相关代码
1 export const minus = (a, b ) => a - b;
src/index.js
写入相关代码
1 2 3 4 5 6 7 import add from "./add.js" ;import { minus } from "./minus.js" ;const sum = add (1 , 2 );const division = minus (2 , 1 );console .log ("[ add(1, 2) ] >" , sum);console .log ("[ minus(2, 1) ] >" , division);
index.html
写入相关代码
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html><html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > 手写实现 Webpack</title > </head > <body > <div > 我在手写实现 Webpack</div > <script src ="./src/index.js" > </script > </body > </html >
然后使用 VScode 的 Live Server 启动index.html
看效果
肯定是报错的,因为我们的<script
没加type="module"
我们期望正确的结果是:
原理实现
Webpack 的主要作用是从入口开始就加载一系列的依赖文件,最终打包成一个文件,这也是我们要实现的功能。 我们常用的打包命令是:npm run build
新建一个webpack.js
作为手写 webpack
的入口(这个会基于 node 环境去运行的哦)
然后我们期望运行这个node webpack.js
后,生成一个dist
文件夹,其中有
一个bundle.js
,包含了我们src
源码下面的所有文件的代码
一个index.html
,将之前的<script src="./src/index.js"></script>
改为了<script src="./bundle.js"></script>
后的 html
然后 Live Server 运行dist/index.html
后,最终浏览器能正确运行并打印:
1、基于主入口,读取文件
webpack.js
编码:基于主入口,读取文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const fs = require ("fs" );const entry = "./src/index.js" ;function getFileInfo (path ) { return fs.readFileSync (path, "utf-8" ); }const entryFileContent = getFileInfo (entry);console .log ("[ entryFileContent ] >" , entryFileContent);
运行node webpack.js
调试,发现已拿到主入口对应的代码了
将node webpack.js
放到package.json
中,之后就可以pnpm build
执行了
2、基于入口文件内容,去分析依赖关系 (一)解析为 AST 第一步拿到了入口文件的内容,我们就需要去分析其中的依赖关系,即import
关键词。 如何分析呢?要么原始的通过字符串匹配import
然后分析;要么借用其他工具帮我们解析与分析 这里采用Babel
工具来帮我们分析
什么是 Babel?JS 编译器,可将高版本转为低版本的 JS 流程为:解析(将源代码转为 AST)、转换(对 AST 进行增删改查)、生成(将 AST 转为 JS 代码) 所以我们可以利用它来帮我们解析 JS 代码
安装@babel/parser
依赖,官方使用文档:@babel/parser · Babel
引入并使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 const fs = require ("fs" );const entry = "./src/index.js" ;function getFileInfo (path ) { return fs.readFileSync (path, "utf-8" ); }const entryFileContent = getFileInfo (entry);console .log ("[ entryFileContent ] >" , entryFileContent);const parser = require ("@babel/parser" );function parseFile (fileContent ) { const ast = parser.parse (fileContent, { sourceType : "module" , }); return ast; }const entryFileContentAST = parseFile (entryFileContent);console .log ("[ entryFileContentAST ] >" , entryFileContentAST);
然后运行pnpm build
,看下打印 AST 的结构
可以发现是正确打印了,但是一些关键信息被隐藏了,比如 body 里面的 这时候就可以借助 AST 在线工具 ,将我们的代码拷贝进去,看完整的结构:
(二)分析 AST,形成依赖图 还是使用工具,帮我直接分析依赖:@babel/traverse · Babel
安装@babel/traverse
依赖
1 pnpm add @babel/traverse
webpack.js
编码:分析 AST,形成依赖图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 const entry = "./src/index.js" ;const path = require ("path" );const fs = require ("fs" );function getFileInfo (path ) { return fs.readFileSync (path, "utf-8" ); }const entryFileContent = getFileInfo (entry);const parser = require ("@babel/parser" );function parseFile (fileContent ) { const ast = parser.parse (fileContent, { sourceType : "module" , }); return ast; }const entryFileContentAST = parseFile (entryFileContent);const traverse = require ("@babel/traverse" ).default ;function createDependencyMap (ast ) { const dependencyMap = {}; traverse (ast, { ImportDeclaration ({ node }) { const { value } = node.source ; const dirname = path.dirname (entry); const abspath = "./" + path.join (dirname, value); dependencyMap[value] = abspath; }, }); console .log ("[ dependencyMap ] >" , dependencyMap); return dependencyMap; }createDependencyMap (entryFileContentAST);
运行pnpm build
,可以看到打印的依赖图
3、再将 AST 转换为低版本的 JS 代码 因为我们之前写的代码都是高版本的,所以有些浏览器不一定能识别,因此需要将其转为低版本代码,并且该低代码最终会在浏览器中运行哦
安装bable
相关依赖
1 pnpm add babel @babel/preset-env @babel/core
(三)将 AST 转换为低版本的 JS 代码
webpack.js
编码:将 AST 转换为低版本的 JS 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 const entry = "./src/index.js" ;const path = require ("path" );const fs = require ("fs" );function getFileInfo (path ) { return fs.readFileSync (path, "utf-8" ); }const entryFileContent = getFileInfo (entry);const parser = require ("@babel/parser" );function parseFile (fileContent ) { const ast = parser.parse (fileContent, { sourceType : "module" , }); return ast; }const entryFileContentAST = parseFile (entryFileContent);const traverse = require ("@babel/traverse" ).default ;function createDependencyMap (ast ) { const dependencyMap = {}; traverse (ast, { ImportDeclaration ({ node }) { const { value } = node.source ; const dirname = path.dirname (entry); const abspath = "./" + path.join (dirname, value); dependencyMap[value] = abspath; }, }); console .log ("[ dependencyMap ] >" , dependencyMap); return dependencyMap; }createDependencyMap (entryFileContentAST);function generateCode (ast ) { const { code } = require ("@babel/core" ).transformFromAst (ast, null , { presets : ["@babel/preset-env" ], }); return code; }generateCode (entryFileContentAST);
运行pnpm build
,可以看到打印的 code
考点:”use strict”是什么? "use strict"
是 ES5 的严格模式,JS 解释器将采用更严格的规则来解析和执行代码,目的是消除常见错误与禁用不安全的操作(因为 JS 太灵活了)
变量名不能重复使用 var 声明
eval 不能使用
变量必须先声明再使用
函数内部的 this 不会默认绑到全局对象上
对象属性名不能重复
函数参数名不能重复
等等
(四)将上述代码聚合到一个方法内 - getModuleInfo
webpack.js
编码:将上述代码聚合到一个方法内 - getModuleInfo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 const entry = "./src/index.js" ;const path = require ("path" );const fs = require ("fs" );const parser = require ("@babel/parser" );const traverse = require ("@babel/traverse" ).default ;const babelCore = require ("@babel/core" );function getModuleInfo (_path ) { function getFileInfo (path ) { return fs.readFileSync (path, "utf-8" ); } function parseFile (fileContent ) { const ast = parser.parse (fileContent, { sourceType : "module" , }); return ast; } function createDependencyMap (ast ) { const dependencyMap = {}; traverse (ast, { ImportDeclaration ({ node }) { const { value } = node.source ; const dirname = path.dirname (entry); const abspath = "./" + path.join (dirname, value); dependencyMap[value] = abspath; }, }); return dependencyMap; } function generateCode (ast ) { const { code } = babelCore.transformFromAst (ast, null , { presets : ["@babel/preset-env" ], }); return code; } const _pathFileContent = getFileInfo (_path); const _pathFileContentAST = parseFile (_pathFileContent); const _pathFileDepsMap = createDependencyMap (_pathFileContentAST); const _pathFileCode = generateCode (_pathFileContentAST); return { path : _path, deps : _pathFileDepsMap, code : _pathFileCode }; }const entryModuleInfo = getModuleInfo (entry);console .log ("[ entryModuleInfo ] >" , entryModuleInfo);
运行 build,得到如下结果
4、基于依赖关系图,去加载对应的所有文件
webpack.js
编码:基于依赖关系图,去加载对应的所有文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function loadModules (dependencyMap ) { const modules = []; if (!dependencyMap) return []; for (let key in dependencyMap) { const moduleInfo = getModuleInfo (dependencyMap[key]); modules.push (moduleInfo); if (moduleInfo.deps ) modules.push (...loadModules (moduleInfo.deps )); } return modules; }const allModules = [entryModuleInfo].concat (loadModules (entryModuleInfo.deps ));console .log ("[ allModules ] >" , allModules);
运行 build,得到如下结果
然后将数组结构转为对象结构,便于通过path
取值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const allModulesArray = [entryModuleInfo].concat ( loadModules (entryModuleInfo.deps ) );function createModuleMap (modules ) { return modules.reduce ((modulesMap, module ) => { modulesMap[module .path ] = module ; return modulesMap; }, {}); }const allModulesMap = createModuleMap (allModulesArray);console .log ("[ allModulesMap ] >" , allModulesMap);
运行 build,得到如下结果
(五)将本阶段的代码聚合到一个方法内 - parseModules
webpack.js
编码:将本阶段的代码聚合到一个方法内 - parseModules
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 const entry = "./src/index.js" ;const path = require ("path" );const fs = require ("fs" );const parser = require ("@babel/parser" );const traverse = require ("@babel/traverse" ).default ;const babelCore = require ("@babel/core" );function getModuleInfo (_path ) { function getFileInfo (path ) { return fs.readFileSync (path, "utf-8" ); } function parseFile (fileContent ) { const ast = parser.parse (fileContent, { sourceType : "module" , }); return ast; } function createDependencyMap (ast ) { let dependencyMap = null ; traverse (ast, { ImportDeclaration ({ node }) { const { value } = node.source ; const dirname = path.dirname (entry); const abspath = "./" + path.join (dirname, value); if (!dependencyMap) dependencyMap = {}; dependencyMap[value] = abspath; }, }); return dependencyMap; } function generateCode (ast ) { const { code } = babelCore.transformFromAst (ast, null , { presets : ["@babel/preset-env" ], }); return code; } const _pathFileContent = getFileInfo (_path); const _pathFileContentAST = parseFile (_pathFileContent); const _pathFileDepsMap = createDependencyMap (_pathFileContentAST); const _pathFileCode = generateCode (_pathFileContentAST); return { path : _path, deps : _pathFileDepsMap, code : _pathFileCode }; }function parseModules (moduleInfo ) { function loadModules (dependencyMap ) { const modules = []; if (!dependencyMap) return []; for (let key in dependencyMap) { const _moduleInfo = getModuleInfo (dependencyMap[key]); modules.push (_moduleInfo); if (_moduleInfo.deps ) modules.push (...loadModules (_moduleInfo.deps )); } return modules; } function createModuleMap (modules ) { return modules.reduce ((modulesMap, module ) => { modulesMap[module .path ] = module ; return modulesMap; }, {}); } const modulesArray = [moduleInfo].concat (loadModules (moduleInfo.deps )); return createModuleMap (modulesArray); }const entryModuleInfo = getModuleInfo (entry);const allModulesMap = parseModules (entryModuleInfo);console .log ("[ allModulesMap ] >" , allModulesMap);
5、处理上下文 我们分析下打印出来的code
,我们要求它能直接在浏览器中运行,但它里面有两个关键点reqiure(函数)、exports(对象)
,咋一看这是 CJS 的语法,肯定是不能在浏览器中运行的,所以我们需要分别给定义出reqiure、exports
在浏览器上的上下文
webpack.js
编码:注入上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function handleContext (modulesMap ) { const modulesMapString = JSON .stringify (modulesMap); return `(function (modulesMap) { function require(path) { function absRequire(absPath) { return require(modulesMap[path].deps[absPath]); } var exports = {}; (function (require, exports, code) { eval(code); })(absRequire, exports, modulesMap[path].code); return exports; } require('${entry} '); })(${modulesMapString} );` ; }const bundle_js_code_string = handleContext (allModulesMap);
6、生成 dist 与相关文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 function createOutPutFiles (_output, codeString ) { function createFolder (path ) { const isExist = fs.existsSync (path); if (isExist) fs.removeSync (path); fs.mkdirSync (path); } function createHTML (path, scriptSrc ) { const htmlName = "index.html" ; const htmlContent = fs.readFileSync (htmlName, "utf-8" ); const insertPointPattern = /<\/body>/i ; const insertionPoint = htmlContent.search (insertPointPattern); if (insertionPoint !== -1 ) { const scriptTags = `<script src="./${scriptSrc} "></script>` ; const newHtmlContent = `${htmlContent.slice(0 , insertionPoint)} ${scriptTags} ${htmlContent.slice(insertionPoint)} ` ; const htmlPath = path + "/" + htmlName; fs.writeFileSync (htmlPath, newHtmlContent); } } const { path, filename } = _output; createFolder (path); fs.writeFileSync (path + "/" + filename, codeString); createHTML (path, filename); }const bundle_js_code_string = handleContext (allModulesMap);createOutPutFiles (output, bundle_js_code_string);
7、代码完成,运行看效果 index.html
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html><html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > 手写实现 Webpack</title > </head > <body > <div > 我在手写实现 Webpack</div > </body > </html >
webpack.js
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 const entry = "./src/index.js" ;const output = { path : "_dist" , filename : "bundle.js" };const path = require ("path" );const fs = require ("fs-extra" );const parser = require ("@babel/parser" );const traverse = require ("@babel/traverse" ).default ;const babelCore = require ("@babel/core" );function getModuleInfo (_path ) { function getFileInfo (path ) { return fs.readFileSync (path, "utf-8" ); } function parseFile (fileContent ) { const ast = parser.parse (fileContent, { sourceType : "module" , }); return ast; } function createDependencyMap (ast ) { let dependencyMap = null ; traverse (ast, { ImportDeclaration ({ node }) { const { value } = node.source ; const dirname = path.dirname (entry); const abspath = "./" + path.join (dirname, value); if (!dependencyMap) dependencyMap = {}; dependencyMap[value] = abspath; }, }); return dependencyMap; } function generateCode (ast ) { const { code } = babelCore.transformFromAst (ast, null , { presets : ["@babel/preset-env" ], }); return code; } const _pathFileContent = getFileInfo (_path); const _pathFileContentAST = parseFile (_pathFileContent); const _pathFileDepsMap = createDependencyMap (_pathFileContentAST); const _pathFileCode = generateCode (_pathFileContentAST); return { path : _path, deps : _pathFileDepsMap, code : _pathFileCode }; }function parseModules (moduleInfo ) { function loadModules (dependencyMap ) { const modules = []; if (!dependencyMap) return []; for (let key in dependencyMap) { const _moduleInfo = getModuleInfo (dependencyMap[key]); modules.push (_moduleInfo); if (_moduleInfo.deps ) modules.push (...loadModules (_moduleInfo.deps )); } return modules; } function createModuleMap (modules ) { return modules.reduce ((modulesMap, module ) => { modulesMap[module .path ] = module ; return modulesMap; }, {}); } const modulesArray = [moduleInfo].concat (loadModules (moduleInfo.deps )); return createModuleMap (modulesArray); }const entryModuleInfo = getModuleInfo (entry);const allModulesMap = parseModules (entryModuleInfo);function handleContext (modulesMap ) { const modulesMapString = JSON .stringify (modulesMap); return `(function (modulesMap) { function require(path) { function absRequire(absPath) { return require(modulesMap[path].deps[absPath]); } var exports = {}; (function (require, exports, code) { eval(code); })(absRequire, exports, modulesMap[path].code); return exports; } require('${entry} '); })(${modulesMapString} );` ; }function createOutPutFiles (_output, codeString ) { function createFolder (path ) { const isExist = fs.existsSync (path); if (isExist) fs.removeSync (path); fs.mkdirSync (path); } function createHTML (path, scriptSrc ) { const htmlName = "index.html" ; const htmlContent = fs.readFileSync (htmlName, "utf-8" ); const insertPointPattern = /<\/body>/i ; const insertionPoint = htmlContent.search (insertPointPattern); if (insertionPoint !== -1 ) { const scriptTags = `<script src="./${scriptSrc} "></script>` ; const newHtmlContent = `${htmlContent.slice(0 , insertionPoint)} ${scriptTags} ${htmlContent.slice(insertionPoint)} ` ; const htmlPath = path + "/" + htmlName; fs.writeFileSync (htmlPath, newHtmlContent); } } const { path, filename } = _output; createFolder (path); fs.writeFileSync (path + "/" + filename, codeString); createHTML (path, filename); }const bundle_js_code_string = handleContext (allModulesMap);createOutPutFiles (output, bundle_js_code_string);
运行pnpm build
,生成如下代码:
_dist/index.html
:
1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html><html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > 手写实现 Webpack</title > </head > <body > <div > 我在手写实现 Webpack</div > <script src ="./bundle.js" > </script > </body > </html >
_dist/bundle.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 (function (modulesMap ) { function require (path ) { function absRequire (absPath ) { return require (modulesMap[path].deps [absPath]); } var exports = {}; (function (require , exports , code ) { eval (code); })(absRequire, exports , modulesMap[path].code ); return exports ; } require ("./src/index.js" ); })({ "./src/index.js" : { path : "./src/index.js" , deps : { "./add.js" : "./src/add.js" , "./minus.js" : "./src/minus.js" }, code : '"use strict";\n\nvar _add = _interopRequireDefault(require("./add.js"));\nvar _minus = require("./minus.js");\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\nvar sum = (0, _add["default"])(1, 2);\nvar division = (0, _minus.minus)(2, 1);\nconsole.log("[ add(1, 2) ] >", sum);\nconsole.log("[ minus(2, 1) ] >", division);' , }, "./src/add.js" : { path : "./src/add.js" , deps : null , code : '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\nvar _default = exports["default"] = function _default(a, b) {\n return a + b;\n};' , }, "./src/minus.js" : { path : "./src/minus.js" , deps : null , code : '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.minus = void 0;\nvar minus = exports.minus = function minus(a, b) {\n return a - b;\n};' , }, });
然后 Live Server 启动_dist/index.html
至此最简单的实现了手写 Webpack 功能 但真正的 Webpack 远远不止这么简单哈
总结 上面手动实现了一个最简单的 Webpack 打包功能,可以发现我们以前配的 Webpack 选项影子 比如:entry、output 然后 Webpack 强大的在于Loader、Plugin
系统,你可以粗暴理解就是我们手写时引入的其他依赖(fs-extra、babel
),帮我做更多的事情 只是 Webpack 的Loader、Plugin
系统做的很完善和强大Webpack 原生 **Loader**
支持加载的文件有:JS 和 JSON ,其他类型(css/svg 等)的就要安装对应的Loader
来处理
Loader 简介 是对模块的源代码进行转换的,默认只能处理js、json
,其他类型的css、txt、less 等
需要专门的 Loader 进行转换处理。
1 2 3 4 5 module .exports = { module : { rules :[ { test :/\.less$/ , use : 'less-loader' } ] } }
Loader 是链式传递的,Webpack 会按顺序链式调用每个 Loader,Loader 的输入与输出都是字符串,并且每个 Loader 只应该做一件事并且无状态
less-loader: 将 less 文件处理后通过 style 标签渲染到页面上
Plugin 简介 在 Webpack 构建工程中,特定时间注入的扩展逻辑,用来改变或优化构建结果。
1 2 3 4 5 const HTMLWebpackPlugin = require ('html-webpack-plugin' )module .exports = { plugin : [ new HTMLWebpackPlugin ({ template : './public/index.html' }) ] }
自定义插件开发文档:自定义插件 | webpack 中文文档 、常用钩子 核心就是采用固定格式:写一个类,再写一个apply
方法,通过compiler.hooks[钩子名].tap(插件名称, 插件功能)
,然后重点写我们的插件功能
即可。
插件功能
就可以随意发挥了,它是运行在node
环境下的,所以可以使用fs
来创建你想要的文件,也可以使用jszip
将dist
压缩为.zip
等等,甚至可以使用axios
调用接口干事情
比如写一个打包时,创建一个version.json
文件的插件,用于表示本次的版本
1 2 emit :输出 asset 到 output 目录之前执行。这个钩子不会被复制到子编译器。 回调参数:compilation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const { RawSource } = require ("webpack-sources" );class VersionFilePlugin { apply (compiler ) { compiler.hooks .emit .tap (VersionFilePlugin .name , (compilation ) => { const version = `${Number (new Date ())} _${Math .random().toString(36 )} ` ; compilation.emitAsset ( "version.json" , new RawSource (JSON .stringify ({ version })), ); }); } }module .exports = { VersionFilePlugin };const { VersionFilePlugin } = require ("../webpack-plugin/versionFile" );module .exports = { plugins : [new VersionFilePlugin ()], }
输入结果如下:
Chunk 简介 Chunk:构建过程中产生的代码块,代表一组模块的集合。可通过分片技术生成不同的 chunks,最终生成不同的 bundle 文件
Tree Shaking 简介 官方文档:Tree Shaking | webpack 中文文档 Tree Shaking:“树摇”,将枯死的叶子摇掉。代码层面指:移除不使用的代码,可减少打包体积。 在 Webpack 中开启 Tree shaking 必须满足以下 3 个条件: 1、使用 ESM 写代码:import、export、export default
2、配置optimization.usedExports 为 true
3、启动优化功能,三选一 a、配置mode=production
(常用的) b、配置optimization.minimize = true
c、配置optimization.minimizer
数组
原理 先标记 模块导出中未被使用的值,再使用terser
来删除相关代码。 流程:分析 -> 标记 -> 清除 基于生成的依赖关系图,分析对应的关系;将未使用的导出变量,存储为标记依赖图;生成代码时进行清除。
Webpack 5 的新增功能 1、新增了cache
属性,可支持本地缓存编译结果,提供构建性能 2、内置了静态资源(如图片、字体等)的官方 Loader 3、提升了 Tree Shaking 能力 4、增加了模块联邦,支持共享代码模块
一些优化思路 思路 1 :先确定需要进行哪些优化,可基于 Webpack 的配置:resolve、module、externals、plugins 等思路 2 :优化产物体积,利用webpack-bundle-analyzer
插件进行分析思路 3 :优化构建速度,利用speed-measure-webpack-plugin
插件进行分析
一些优化操作 1、配置[cache](https://webpack.docschina.org/configuration/cache/)
属性,会缓存生成的 webpack 模块和 chunk,来改善构建速度 。Webpack5 之前使用专门的cache-loader
来缓存 2、配置[externals](https://webpack.docschina.org/configuration/externals/#externals)
属性,防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。可减少产物体积 3、配置[resolve.alias](https://webpack.docschina.org/configuration/resolve/#resolvealias)
属性,这样Utilities
是一个绝对路径的别名,有助于降低解析文件的成本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const path = require ('path' );module .exports = { resolve : { alias : { Utilities : path.resolve (__dirname, 'src/utilities/' ), Templates : path.resolve (__dirname, 'src/templates/' ), }, }, };import Utility from 'Utilities/utility' ;
4、配置[resolve.mainFields](https://webpack.docschina.org/configuration/resolve/#resolvemainfields)
属性,影响 Webpack 搜索第三方库的顺序。一般 npm 库使用的是main
1 2 3 4 5 6 module .exports = { resolve : { mainFields : ['main' ], }, };
5、配置[resolve.extensions](https://webpack.docschina.org/configuration/resolve/#resolveextensions)
属性,影响 Webpack 解析文件的顺序,将高频文件类型放在前面。
1 2 3 4 5 6 module .exports = { resolve : { extensions : ['.js' , '.json' , '.wasm' ], }, };
以上优化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const path = require ('path' )module .exports = { resolve : { alias : { "@" : path.resolve (__dirname, './src' ), "utils" : path.resolve (__dirname, './src/utils' ) }, externals : { react : 'React' , }, mainFields : ['main' ], extensions : ['.js' , '.jsx' ] } }
Vite 新一代构建工具。 核心分为两个阶段 :开发环境使用 Esbuild (干的事跟 Webpack 一样,速度却更快);生成环境使用 Rollup ;开发环境时 :类似于Webpack + Webpack Dev Server Plugin
的集合,Vite
它自带Dev Server
,当你采用ESM
导入模块时,自建的Dev Server
就给你按需编译(Esbuild)然后返回,这样就跳过了整体的打包流程,所以本地开发很快;生成环境时 :使用 Rollup 将代码打包成 bundle
那为什么要使用两个构建工具呢?
因为 Esbuild 不支持一些常用的设置
不支持降级到 es5 的代码,低版本浏览器跑不起来(es6/es7+)。
不支持 const、enum 等语法,会报错
打包不够灵活:无法配置打包流程、不支持代码分割
所以生成环境要使用其他工具,然后 Rollup 比 Webpack 简单高效一些,所以用了 Rollup
包管理工具 Lerna:管理多版本的 npm,文档:https://lerna.js.org/docs/introduction Verdaccio:私有的 npm 代理仓库,文档:What is Verdaccio? | Verdaccio