minipack的打包流程
可以分成两大部分
- 生成模块依赖(循环引用等问题没有解决的~,只是原理解析)
- 根据处理依赖进行打包
源码的分析
const fs = require('fs')
const path = require('path')
const babylon = require('babylon')// AST 解析器
const traverse = require('babel-traverse').default // 遍历工具
const { transformFromAst } = require('babel-core') // babel-core
let ID = 0
/\*\*
\* 获得文件内容, 从而在下面做语法树分析
\* @param {\*} filename
\*/
function createAsset (filename) {
const content = fs.readFileSync(filename, 'utf-8')
const ast = babylon.parse(content, { // 解析内容至AST
sourceType: 'module'
})
const dependencies = [] // 初始化依赖集, dependencies存放该文件依赖项的相对path
traverse(ast, { // 声明traverse的statement, 这里进ImportDeclaration 这个statement内。然后对节点import的依赖值进行push进依赖集
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value)
}
})
const id = ID++ // id自增
const { code } = transformFromAst(ast, null, { // 再将ast转换为文件
presets: ['env']
})
// 返回这么模块的所有信息,设置的id filename 依赖集 代码
return {
id,
filename,
dependencies,
code
}
}
/\*\*
\*从entry入口进行解析依赖图谱
\* @param {\*} entry
\*/
function createGraph (entry) {
const mainAsset = createAsset(entry) // 从入口文件开始
const queue = [mainAsset] // 最初的依赖集
for (const asset of queue) { // 一张图常见的遍历算法有广度遍历与深度遍历,这里采用的是广度遍历
asset.mapping = {} // 给当前依赖做mapping记录
const dirname = path.dirname(asset.filename)// 获得依赖模块地址
asset.dependencies.forEach(relativePath => { // 刚开始只有一个asset 但是dependencies可能多个
const absolutePath = path.join(dirname, relativePath)// 这边获得绝对路径
const child = createAsset(absolutePath) // 递归依赖的依赖
asset.mapping[relativePath] = child.id // 将当前依赖及依赖的依赖都放入到mappnig里
queue.push(child) // 广度遍历借助队列
})
}
return queue // 返回遍历完依赖的队列
}
/\*\*
\* 将graph模块打包bundle输出
\* @param {\*} graph
\*/
function bundle (graph) {
let modules = ''
graph.forEach(mod => {
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`
})
// CommonJS风格
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
}
module.exports = {
bundle,
createGraph
}
模块依赖生成
具体步骤
- 给定入口文件
- 根据入口文件分析依赖(借助
bable
获取) - 广度遍历依赖图获取依赖
- 根据依赖图生成
(模块id)key:(数组)value
的对象表示 - 建立require机制实现模块加载运行
一个简单的实例
原始代码:
// 入口文件 entry.js
import message from './message.js'
console.log(message);
// message.js
import {name} from './name.js'
export default `hello ${name}!`
// name.js
export const name = 'world'
读取文件内容,分析依赖,第一步需要解析源码,生成抽象语法树。
- 第一步,读取入口文件,生成 AST,递归生成依赖关系对象 graph。
其中,createAsset
函数是解析js文本,生成每个文件对应的一个对象,其中 code
的代码是经过babel-preset-env
转换后可在浏览器中执行的代码。
const { code } = transformFromAst(ast, null, {
presets: ['env']
})
createGraph 函数生成依赖关系对象。
[
{ id: 0,
filename: './example/entry.js',
dependencies: [ './message.js' ],
code: '"use strict";\n\nvar \_message = require("./message.js");\n\nvar \_message2 = \_interopRequireDefault(\_message);\n\nfunction \_interopRequireDefault(obj) { return obj && obj.\_\_esModule ? obj : { default: obj }; }\n\nconsole.log(\_message2.default);',
mapping: { './message.js': 1 } },
{ id: 1,
filename: 'example/message.js',
dependencies: [ './name.js' ],
code: '"use strict";\n\nObject.defineProperty(exports, "\_\_esModule", {\n value: true\n});\n\nvar \_name = require("./name.js");\n\nexports.default = "hello " + \_name.name + "!";',
mapping: { './name.js': 2 } },
{ id: 2,
filename: 'example/name.js',
dependencies: [],
code: '"use strict";\n\nObject.defineProperty(exports, "\_\_esModule", {\n value: true\n});\nvar name = exports.name = \'world\';',
mapping: {} }
]
有了依赖关系图,下一步就是将代码打包可以在浏览器中运行的包。
首先我们将依赖图解析成如下字符串(其实是对象没用{}
包裹的格式):
关键代码是这句:
modules += `${mod.id}: [
function (require, module, exports) {
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`;
生成出来的代码如下:
0: [
function (require, module, exports) {
// -------------- mod.code --------------
"use strict";
var _message = require("./message.js");
var _message2 = \_interopRequireDefault(_message);
function \_interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message2.default);
// --------------------------------------
},
{"./message.js":1},
],
1: [
function (require, module, exports) {
// -------------- mod.code --------------
"use strict";
Object.defineProperty(exports, "\_\_esModule", {
value: true
});
var _name = require("./name.js");
exports.default = "hello " + _name.name + "!";
// --------------------------------------
},
{"./name.js":2},
],
2: [
function (require, module, exports) {
// -------------- mod.code --------------
"use strict";
Object.defineProperty(exports, "\_\_esModule", {
value: true
});
var name = exports.name = 'world';
// --------------------------------------
},
{},
],
依赖的图生成的文件可以简化为:
modules = {
0: [function code , {deps} ],
1: [function code , {deps} ]
}
这里,我们比较下源码:
// 入口文件 entry.js
import message from './message.js';
console.log(message);
// ---
"use strict";
var _message = require("./message.js");
var _message2 = \_interopRequireDefault(_message);
function \_interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message2.default);
// message.js
import {name} from './name.js';
export default `hello ${name}!`;
// ---
"use strict";
Object.defineProperty(exports, "\_\_esModule", {
value: true
});
var _name = require("./name.js");
exports.default = "hello " + _name.name + "!";
// name.js
export const name = 'world';
// ---
"use strict";
Object.defineProperty(exports, "\_\_esModule", {
value: true
});
var name = exports.name = 'world';
可以看出,babel
在转换原始code
的时候,引入了require
函数来解决模块引用问题。但是其实浏览器仍然是不认识的。因此还需要额外定义一个require函数(其实这部分和requirejs
原理类似的模块化解决方案,其中原理其实也很简单)
得到这个字符串后,再最后拼接起来即最终结果。
最后,我们还需要定义一个自执行函数文本,并将上述字符串传入其中,拼接结果如下:
(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);
})({
0: [
function (require, module, exports) {
"use strict";
var _message = require("./message.js");
var _message2 = \_interopRequireDefault(_message);
function \_interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message2.default);
},
{ "./message.js": 1 },
],
1: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "\_\_esModule", {
value: true
});
var _name = require("./name.js");
exports.default = "hello " + _name.name + "!";
},
{ "./name.js": 2 },
],
2: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "\_\_esModule", {
value: true
});
var name = exports.name = 'world';
},
{},
],
})
跳槽是每个人的职业生涯中都要经历的过程,不论你是搜索到的这篇文章还是无意中浏览到的这篇文章,希望你没有白白浪费停留在这里的时间,能给你接下来或者以后的笔试面试带来一些帮助。
也许是互联网未来10年中最好的一年。WINTER IS COMING。但是如果你不真正的自己去尝试尝试,你永远不知道市面上的行情如何。这次找工作下来,我自身感觉市场并没有那么可怕,也拿到了几个大厂的offer。在此进行一个总结,给自己,也希望能帮助到需要的同学。
面试准备
面试准备根据每个人掌握的知识不同,准备的时间也不一样。现在对于前端岗位,以前也许不是很重视算法这块,但是现在很多公司也都会考。建议大家平时有空的时候多刷刷leetcode。算法的准备时间比较长,是一个长期的过程。需要在掌握了大部分前端基础知识的情况下,再有针对性的去复习算法。面试的时候算法能做出来肯定加分,但做不出来也不会一票否决,面试官也会给你提供一些思路。