欢迎来到MyDarlingBug的高级私人会所。
提问
你是否面临着这样一个困扰?
- 面试官:webpack打包原理是什么?
- 我: 打包原理是不啦不啦(内心窃喜,呵,这里我看过。老套路~八股文把你安排了!)
- 面试官:那正好这里有电脑,能不能敲一个出来,简单的demo也行。
- 我:黑人❓❓❓。 额,这个这个,我试试~~
- 面试官:30 minutes later。。。 你可能这方面还要加强一点,有机会再合作。
😔,为了恰饭,还是自己总结总结。
1.准备工作
在同级目录新建四个文件,分别为index.js(入口文件) info.js,arr.js, kwebpack.js。
//index.js 文件内容
import info from './info.js';
import arr from './arr.js';
let a = 2;
console.log(a);
console.log(info,'-----',arr)
//info.js 文件内容
console.log('info')
let info = 123
export default info
//arr.js 文件内容
console.log('arr')
let arr = 123
export default arr
2.编写打包方法
- 先安装fs,parser模块。fs用来解析文件内容,注意用utf-8格式。
parser.parse方法用来生成ast抽象语法树(就是个对象,方便处理整个代码文件)。(具体如何生成,想深入理解(不嫌麻烦)也可以自己从词法解析 语法解析开始构建ast )
//kwebpack.js 文件内容
const fs = require('fs')
const parser = require('@babel/parser')
function kwebpack(entry){
const file = fs.readFileSync(entry,'utf-8');
const ast = parser.parse(file,{sourceType:'module'}) // 默认不支持esModule,所以要声明一下
}
kwebpack('./index.js')
- 我们把index.js 当作项目入口,同时需要在kwebpack方法收集入口文件里其他的模块,方便后续遍历查询整个项目。
const traverse = require('@babel/traverse').default //使用当前api解析
//kwebpack方法
function kwebpack(entry){
...
const dependencies = []
traverse(ast, {
ImportDeclaration(res) { // 解析import
console.log(res, '--res--')
const value = res.node.source.value
dependencies.push(value)
//dependencies = ['./info.js','./arr.js']
}
}
...
}
返回的res结构,可以看到有一个value 可以获取到./info.js, ./arr.js
3.使用babel插件 把es6转换成es5.
const traverse = require('@babel/traverse').default //使用当前api解析
const { transformFromAst } = require('@babel/core')
//kwebpack方法
function kwebpack(entry){
...
const dependencies = []
traverse(ast, {
...
}
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env'] //这是插件套餐
})
return {
code,
dependencies,
entry
}
...
}
可以看到kwebpack方法输出的内容
const res = kwebpack('./index.js')
//res
{
code: '"use strict";\n' +
'\n' +
'var _info = _interopRequireDefault(require("./info.js"));\n' +
'\n' +
'var _arr = _interopRequireDefault(require("./arr.js"));\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'var a = 2;\n' +
'console.log(a);\n' +
`console.log(_info["default"], '-----', _arr["default"]);`,
dependencies: [ './info.js', './arr.js' ],
fileName: './index.js'
}
- 接下来我们用广度优先遍历查找到所有模块,让每一个模块都通过Kwebpack方法输出{code,dependencies,fileName}对象。
...
function rangeLoop(entry) {
const oneObjs = kwebpack(entry)
const graph = {} //收集所有 {code,dependencies,fileName}
const allArr = [oneObjs]
graph[entry] = oneObjs
while (allArr.length) {
const oneObj = allArr[0];
const dependencies = oneObj.dependencies;
for (let dep of dependencies) {
const item = kwebpack(dep)
allArr.push(item)
graph[dep] = item
}
allArr.shift()
}
return graph
}
...
var graph = rangeLoop('./index.js')
可以看到graph输出为:
{
'./index.js': {
code: '"use strict";\n' +
'\n' +
'var _info = _interopRequireDefault(require("./info.js"));\n' +
'\n' +
'var _arr = _interopRequireDefault(require("./arr.js"));\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'var a = 2;\n' +
'console.log(a);\n' +
`console.log(_info["default"], '-----', _arr["default"]);`,
dependencies: [ './info.js', './arr.js' ],
fileName: './index.js'
},
'./info.js': {
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = void 0;\n' +
"console.log('info');\n" +
'var info = 123;\n' +
'var _default = info;\n' +
'exports["default"] = _default;',
dependencies: [],
fileName: './info.js'
},
'./arr.js': {
code: '....',
dependencies: [],
fileName: './arr.js'
}
}
- 拿到模块对象之后,我们需要拼写字符串让浏览器可以递归运行对象里的每一段code。 注意:
- code里可能有require方法,我们需要编写require。
- code里可能有exports,需要把exports对象返回。
- 浏览器不会帮忙补齐 " ; " 号,平时是js帮忙补齐,注意要手动添加;号
function getJsString(entry) {
const graph = rangeLoop(entry); //这里获取到graph模块对象
return `(function(graph){
function require(module){
function localRequire(relative){ //编写require方法。进行递归
return require(relative)
};
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports
};
require('${entry}');
})(${JSON.stringify(graph)})`
}
const res = getJsString('./index.js');
总之,这段代码就做了三件事。
- 用eval 方法执行当前对象code。
- 新建一个exports对象,把当前对象code里的exports return返回。
- 找到当前对象code里的require方法,继续递归往下执行。
- 最后把模板返回。
- 把getJsString方法获取到的res对象粘贴到浏览器
可以看到结果已经输出,说明打包成功。
注意:
可能会遇到如下情况,浏览器禁止使用eval。也可以修改配置或者更换浏览器尝试。
完整代码:
//kwebpack.js
const fs = require('fs')
const parser = require('@babel/parser')
const { transformFromAst } = require('@babel/core')
const traverse = require('@babel/traverse').default
const path = require('path')
function kwebpack(fileName) {
const file = fs.readFileSync(fileName, 'utf-8');
const ast = parser.parse(file, { sourceType: 'module' })
const dependencies = []
traverse(ast, {
ImportDeclaration(res) {
const value = res.node.source.value
const dirname = path.dirname(fileName)
const newValue = "./" + path.join(dirname, value)
dependencies.push(newValue)
}
})
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return {
code,
dependencies,
fileName
}
}
function rangeLoop(entry) {
const oneObjs = kwebpack(entry)
const graph = {}
const allArr = [oneObjs]
graph[entry] = oneObjs
while (allArr.length) {
const oneObj = allArr[0];
const dependencies = oneObj.dependencies;
for (let dep of dependencies) {
const item = kwebpack(dep)
allArr.push(item)
graph[dep] = item
}
allArr.shift()
}
return graph
}
function getJsString(entry) {
const graph = rangeLoop(entry);
console.log(graph,'---graph-')
return `(function(graph){
function require(module){
function localRequire(relative){
return require(relative)
};
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports
};
require('${entry}');
})(${JSON.stringify(graph)})`
}
const res = getJsString('./index.js');
知识点提炼:
-
加强webpack打包原理的理解。
-
广度优先遍历使用。
-
理解生成webpack模板字符串的大致过程。
-
了解打包过程中babel部分api的用法等。
最后
我是MyDarlingBug,大家一起来开开心心写Bug。