前言
话接上篇一文带你彻底了解webpack(一)前言 很多小白朋友应该都听过vite、webpack等,其实每天都在用但是自己并不清 - 掘金
上文详细介绍了webpack是什么并且带你快速上手使用,你已经彻底学会了他的基本使用,至于拓展,接下来就靠天赋异禀的你自行使用和不断摸索了,那么接下来我们进入原理篇,你应该听过一道面试题:vite为什么比webpack快?
那么看完原理篇,你也能成为“面霸”。
你用过webpack吗?
在我们使用webpack进行项目构建时,当你们公司的项目较大,而你恰好需要画画界面写写接口,本来是一件愉快的事情,但是每一次都构建都令你头皮发麻
那么,webpack为什么这么慢呢?
我们继续沿用上一篇(使用篇)的例子
main.js
import {add } from './tools/add.js'
import './styles/index.css'
console.log(add(1,2));
console.log(add(2,3))
console.log('hello world')
那么当webpack读到这份的代码时,看见了第一行,他会去第一行add.js所在的位置读取add.js,并将这份代码片段放在第一行的位置然后继续往下读,这样保证了代码顺序正确执行,但是当add.js这份文件中又引入了其他的文件,那么就会继续进入下一份文件读取,读取完再回到上一份读取的地方继续往下读,读完后再回到add.js读取的位置然后继续往下读,聪明的小伙伴发现了,这是什么?————递归,这就导致了项目的构建速度慢,当然webpack官方也在积极优化构建速度,包括发展到现在webpack的生态已经非常丰富了,他依然是现在使用最多的工具。至于vite为什么就是比他快,这是因为vite采用了另一种模式,颠覆了webpack这种传统的构建理念,关于vite我会在下一篇文章中讲到。
由于递归的存在,一个js文件引入了几十个js模块,这就存在几十次递归,这就是webpack慢的痛点所在。
打包流程
- 读取入口文件中的内容、
- 分析入口文件,递归的去读取模块所依赖的其他文件内容,生产AST语法书,举个例子:
{
'main.js':{
'code':"import {add} from './tools/add.js'"
'need':{
'add.js':{
'code': "export const add = (a,b) => a + b"
}
}
}
}
- 根据AST语法树生成浏览器能运行的代码,例如你用的es6,但是浏览器不支持,webpack就要帮你进行降级,生成es5或者其他支持的版本(代码版本降级)
原理实操myWebpack
项目结构:
add.js
export default function add(a, b) {
return a + b;
}
minus.js
export const minus = (a, b) => a - b
index.js
import add from "./add.js";
import {minus} from "./minus.js";
const sum = add(1, 2)
const division = minus(2, 1)
console.log(sum, division) // 3 1
最后我们将这份index.js引入html,然后运行就发现,报错了,因为浏览器没办法直接读懂模块化语法import,此时构建工具就显得尤为重要,否则要么不用模块化语法写代码,要么自己写一个功能函数让浏览器读懂import语法。
因此webpack就需要有这个能力,将import转换成浏览器能读得懂的模块化语法出来,那么接下来我们创建bundle.js,来模拟实现一下webpack。
首先要读取入口文件,index.js,需要node来读文件,所以说webpack是离不开node的,源码集成了node。 初始化node项目
npm init -y
const fs = require('fs');
const getModelInfo = (file)=>{
const body = fs.readFileSync(file, 'utf-8');
console.log(body)
}
getModelInfo('./src/index.js')
读出来内容如下:
babel/parser分析模块
然后就是递归读取import,看更深层次嵌套的代码。那么事实上这种东西不用自己写直接用第三方的工具来分析模块
npm i @babel/parser // 分析模块,核心作用:降级
使用一下这个工具,用来分析
const fs = require('fs');
const parse = require('@babel/parser');
const getModelInfo = (file)=>{
const body = fs.readFileSync(file, 'utf-8');
const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
console.log(ast)
}
getModelInfo('./src/index.js')
分析模块生成了一个抽象语法树
事实上他讲我们的代码分析好了,代码部分放在了ast.program.body,我们不妨打印出来看看。
他帮我们分析成了一个又一个的模块,包括import声明引入部分在哪里...
收集依赖 @babel/traverse
遍历ast整理一整条模块引入关系。
刚刚能够看到他帮我们分析成了一个数组,数组中有很多个模块,引入部分、变量部分、输出部分等等,起始位置在哪里最终位置在哪里,路径在哪里,都分析的明明白白
那么如何做遍历呢?第三方工具:@babel/traverse
npm i @babel/traverse
const fs = require('fs');
const parse = require('@babel/parser');
const path = require('path');
const traverse = require('@babel/traverse').default;
const getModelInfo = (file)=>{
const body = fs.readFileSync(file, 'utf-8');
const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
// console.log(ast.program.body)
const deps = {}; // 各个模块的路径
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname,node.source.value);
deps[node.source.value] = absPath;
}
})
console.log(deps)
}
getModelInfo('./src/index.js')
然后我们就能得到这些依赖文件的路径
{ './add.js': './src\\add.js', './minus.js': './src\\minus.js' }
接下来要做的就是将各个模块加载进来。
ES6 > ES5 @babel/core @babel/preset-env
npm i @babel/core @babel/preset-env
const fs = require('fs');
const parse = require('@babel/parser');
const path = require('path');
const traverse = require('@babel/traverse').default;
const bable = require('@babel/core');
const getModelInfo = (file)=>{
const body = fs.readFileSync(file, 'utf-8');
const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
// console.log(ast.program.body)
const deps = {}; // 各个模块的路径
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname,node.source.value);
deps[node.source.value] = absPath;
}
})
console.log(deps)
// 代码转换
const {code} = bable.transformFromAst(ast,null,{
presets:['@babel/preset-env']
})
console.log(code)
}
getModelInfo('./src/index.js')
然后我们就得到了降级后的代码
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum, division); // 3 1
至此入口文件代码降级成功了,入口文件所需要依赖的模块地址也梳理好了,最后就是顺着模块依赖的地址把其他模块也降级最后混到一块
递归所有依赖
const fs = require('fs');
const parse = require('@babel/parser');
const path = require('path');
const traverse = require('@babel/traverse').default;
const bable = require('@babel/core');
const getModelInfo = (file)=>{
const body = fs.readFileSync(file, 'utf-8');
const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
// console.log(ast.program.body)
const deps = {}; // 各个模块的路径
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname,node.source.value);
deps[node.source.value] = absPath;
}
})
console.log(deps)
// 代码转换
const {code} = bable.transformFromAst(ast,null,{
presets:['@babel/preset-env']
})
console.log(code)
return {file,deps,code};
}
const parseModules = (file)=>{
const entry = getModelInfo(file);
const temp = [entry];
for (let i=0;i<temp.length;i++){
const deps = temp[i].deps;
if (deps){
for (let j in deps){ // './add.js'
if (deps.hasOwnProperty(j)){
temp.push(getModelInfo(deps[j]))
}
}
}
}
console.log(temp)
}
// getModelInfo('./src/index.js')
parseModules('./src/index.js') // 给一个入口文件,不仅分析入口文件,还要分析入口文件依赖的模块,再分析依赖的模块,依次递归,直到没有依赖为止
然后我们就拿到了整个依赖
{ './add.js': './src\\add.js', './minus.js': './src\\minus.js' }
"use strict";
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum, division); // 3 1
{}
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = add;
function add(a, b) {
return a + b;
}
{}
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.minus = void 0;
var minus = exports.minus = function minus(a, b) {
return a - b;
};
[
{
file: './src/index.js',
deps: { './add.js': './src\\add.js', './minus.js': './src\\minus.js' },
code: '"use strict";\n' +
'\n' +
'var _add = _interopRequireDefault(require("./add.js"));\n' +
'var _minus = require("./minus.js");\n' +
'function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }\n' +
'var sum = (0, _add["default"])(1, 2);\n' +
'var division = (0, _minus.minus)(2, 1);\n' +
'console.log(sum, division); // 3 1'
},
{
file: './src\\add.js',
deps: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = add;\n' +
'function add(a, b) {\n' +
' return a + b;\n' +
'}'
},
{
file: './src\\minus.js',
deps: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports.minus = void 0;\n' +
'var minus = exports.minus = function minus(a, b) {\n' +
' return a - b;\n' +
'};'
}
]
处理require
可以看见import都已经被处理成了require但是在windows环境下是没有require目前还是在node环境下
const fs = require('fs');
const parse = require('@babel/parser');
const path = require('path');
const traverse = require('@babel/traverse').default;
const bable = require('@babel/core');
const getModelInfo = (file)=>{
const body = fs.readFileSync(file, 'utf-8');
const ast = parse.parse(body,{sourceType:'module'}); // 告诉babel这里用的module语法,毕竟模块化语法有很多 commonjs es6...
// console.log(ast.program.body)
const deps = {}; // 各个模块的路径
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname,node.source.value);
deps[node.source.value] = absPath;
}
})
console.log(deps)
// 代码转换
const {code} = bable.transformFromAst(ast,null,{
presets:['@babel/preset-env']
})
console.log(code)
return {file,deps,code};
}
const parseModules = (file)=>{
const entry = getModelInfo(file);
const temp = [entry];
for (let i=0;i<temp.length;i++){
const deps = temp[i].deps;
if (deps){
for (let j in deps){ // './add.js'
if (deps.hasOwnProperty(j)){
temp.push(getModelInfo(deps[j]))
}
}
}
}
// 处理一下数据结构
const depsGraph = {};
temp.forEach(moduleInfo=>{
depsGraph[moduleInfo.file] = {
code: moduleInfo.code,
deps: moduleInfo.deps
}
})
// console.log(depsGraph)
return depsGraph
}
const bundle = (file)=>{
const depsGraph = JSON.stringify(parseModules(file));
return `
(function(graph){
function require(file){
(function (code){eval(code)})(graph[file].code)
}
require('${file}')
})(${depsGraph})
`
}
// getModelInfo('./src/index.js')
// parseModules('./src/index.js') // 给一个入口文件,不仅分析入口文件,还要分析入口文件依赖的模块,再分析依赖的模块,依次递归,直到没有依赖为止
const content = bundle('./src/index.js')
console.log(content)
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js',content)
那么至此,我们已经处理了require并且把所有的代码都整合到了一起并打包成一个js文件,最终最终就可以直接把这份js引入给html,就不会出现最开始的报错问题了。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="./dist/bundle.js"></script>
</body>
</html>
结语
那么通过这么一个实际例子,大家也能看见webpack的真正工作流程,看上去是套了个壳子,用的是第三方的工具来做。
最后,创作不易,如有帮助,一键三连!