roid 是一个æžå…¶ç®€å•的打包软件,使用 node.js å¼€å‘而æˆï¼Œçœ‹å®Œæœ¬æ–‡ï¼Œä½ å¯ä»¥å®žçŽ°ä¸€ä¸ªéžå¸¸ç®€å•çš„ï¼Œä½†æ˜¯åˆæœ‰å®žé™…用途的å‰ç«¯ä»£ç 打包工具。
å¦‚æžœä¸æƒ³çœ‹æ•™ç¨‹ï¼Œç›´æŽ¥çœ‹ä»£ç 的(全部注释):点击地å€
我们æ¯å¤©éƒ½é¢å¯¹å‰ç«¯çš„è¿™å‡ æ¬¾ç¼–è¯‘å·¥å…·ï¼Œä½†æ˜¯åœ¨å¤§é‡äº¤è°ˆä¸æˆ‘å¾—çŸ¥ï¼Œå¹¶ä¸æ˜¯å¾ˆå¤šäººçŸ¥é“这些打包软件背åŽçš„工作原ç†ï¼Œå› æ¤æœ‰äº†è¿™ä¸ª project å‡ºçŽ°ã€‚è¯šç„¶ï¼Œä½ å¹¶ä¸éœ€è¦äº†è§£å¤ªå¤šç¼–译原ç†ä¹‹ç±»çš„äº‹æƒ…ï¼Œå¦‚æžœä½ åœ¨æ¤ä¹‹å‰å¯¹ node.js æžä¸ºç†Ÿæ‚‰ï¼Œé‚£ä¹ˆä½ 对å‰ç«¯æ‰“包工具一定能éžå¸¸å¥½çš„ç†è§£ã€‚
弄清楚打包工具的背åŽåŽŸç†ï¼Œæœ‰åˆ©äºŽæˆ‘们实现å„ç§ç¥žå¥‡çš„自动化ã€å·¥ç¨‹åŒ–东西,比如表å•çš„åŒå‘绑定,自创 JavaScript è¯æ³•,åˆå¦‚èš‚èšé‡‘æœ ant ä¸å¤§å鼎鼎的 import æ’件,甚至是å‰ç«¯æ–‡ä»¶è‡ªåŠ¨æ‰«æè½½å…¥ç‰ï¼Œèƒ½å¤Ÿæžå¤§çš„æå‡æˆ‘们工作效率。
ä¸åºŸè¯ï¼Œæˆ‘们直接开始。
const { readFileSync, writeFileSync } = require('fs')
const path = require('path')
const traverse = require('babel-traverse').default
const { transformFromAst, transform } = require('babel-core')
let ID = 0
// 当å‰ç”¨æˆ·çš„æ“ä½œçš„ç›®å½•
const currentPath = process.cwd()
id
:全局的自增 id
,记录æ¯ä¸€ä¸ªè½½å…¥çš„æ¨¡å—çš„ id
,我们将所有的模å—éƒ½ç”¨å”¯ä¸€æ ‡è¯†ç¬¦è¿›è¡Œæ ‡ç¤ºï¼Œå› æ¤è‡ªå¢ž id
是最有效也是最直观的,有多少个模å—,一统计就出æ¥äº†ã€‚
function parseDependecies(filename) {
const rawCode = readFileSync(filename, 'utf-8')
const ast = transform(rawCode).ast
const dependencies = []
traverse(ast, {
ImportDeclaration(path) {
const sourcePath = path.node.source.value
dependencies.push(sourcePath)
}
})
// 当我们完æˆä¾èµ–的收集以åŽï¼Œæˆ‘们就å¯ä»¥æŠŠæˆ‘们的代ç 从 AST è½¬æ¢æˆ CommenJS 的代ç
// è¿™æ ·å兼容性更高,更好
const es5Code = transformFromAst(ast, null, {
presets: ['env']
}).code
// 还记得我们的 webpack-loader 系统å—?
// 具体实现就是在这里å¯ä»¥å®žçް
// 通过将文件å和代ç éƒ½ä¼ å…¥ loader ä¸ï¼Œè¿›è¡Œåˆ¤æ–,甚至用户定义行为å†è¿›è¡Œè½¬æ¢
// å°±å¯ä»¥å®žçް loader 的机制,当然,我们在这里,就åšä¸€ä¸ªå¼±æ™ºç‰ˆçš„ loader å°±å¯ä»¥äº†
// parcel åœ¨è¿™é‡Œçš„ä¼˜åŒ–æŠ€å·§æ˜¯å¾ˆæœ‰æ„æ€çš„,在 webpack ä¸ï¼Œæˆ‘们æ¯ä¸€ä¸ª loader ä¹‹é—´ä¼ é€’çš„æ˜¯è½¬æ¢å¥½çš„代ç
// è€Œä¸æ˜¯ AST,那么我们必须è¦åœ¨æ¯ä¸€ä¸ª loader 进行 code -> AST 的转æ¢ï¼Œè¿™æ ·æ—¶éžå¸¸è€—æ—¶çš„
// parcel çš„åšæ³•其实就是将 AST ç›´æŽ¥ä¼ é€’ï¼Œè€Œä¸æ˜¯è½¬æ¢å¥½çš„代ç ï¼Œè¿™æ ·ï¼Œé€Ÿåº¦å°±å¿«èµ·æ¥äº†
const customCode = loader(filename, es5Code)
// æœ€åŽæ¨¡å—导出
return {
id: ID++,
code: customCode,
dependencies,
filename
}
}
首先,我们对æ¯ä¸€ä¸ªæ–‡ä»¶è¿›è¡Œå¤„ç†ã€‚å› ä¸ºè¿™åªæ˜¯ä¸€ä¸ªç®€å•版本的 bundler
ï¼Œå› æ¤ï¼Œæˆ‘们并ä¸è€ƒè™‘å¦‚ä½•åŽ»è§£æž css
ã€md
ã€txt
ç‰ç‰ä¹‹ç±»çš„æ ¼å¼ï¼Œæˆ‘们专心处ç†å¥½ js
æ–‡ä»¶çš„æ‰“åŒ…ï¼Œå› ä¸ºå¯¹äºŽå…¶ä»–æ–‡ä»¶è€Œè¨€ï¼Œå¤„ç†èµ·æ¥è¿‡ç¨‹ä¸å¤ªä¸€æ ·ï¼Œç”¨æ–‡ä»¶åŽç¼€å¾ˆå®¹æ˜“将他们区分进行ä¸åŒçš„处ç†ï¼Œåœ¨è¿™ä¸ªç‰ˆæœ¬ï¼Œæˆ‘们还是专注 js
。
const rawCode = readFileSync(filename, 'utf-8')
函数注入一个 filename é¡¾åæ€ä¹‰ï¼Œå°±æ˜¯æ–‡ä»¶å,读å–其的文件文本内容,然åŽå¯¹å…¶è¿›è¡Œ AST 的解æžã€‚我们使用 babel
çš„ transform
æ–¹æ³•åŽ»è½¬æ¢æˆ‘们的原始代ç ,通过转æ¢ä»¥åŽï¼Œæˆ‘们的代ç å˜æˆäº†æŠ½è±¡è¯æ³•æ ‘ï¼ˆ AST
ï¼‰ï¼Œä½ å¯ä»¥é€šè¿‡ https://astexplorer.net/, 这个å¯è§†åŒ–的网站,看看 AST
生æˆçš„æ˜¯ä»€ä¹ˆã€‚
当我们解æžå®Œä»¥åŽï¼Œæˆ‘们就å¯ä»¥æå–当剿–‡ä»¶ä¸çš„ dependencies
,dependencies
翻译为ä¾èµ–ï¼Œä¹Ÿå°±æ˜¯æˆ‘ä»¬æ–‡ä»¶ä¸æ‰€æœ‰çš„ import xxxx from xxxx
,我们将这些ä¾èµ–都放在 dependencies
的数组里é¢ï¼Œä¹‹åŽç»Ÿä¸€è¿›è¡Œå¯¼å‡ºã€‚
ç„¶åŽé€šè¿‡ traverse
é历我们的代ç 。traverse
函数是一个é历 AST
的方法,由 babel-traverse
æä¾›ï¼Œä»–çš„éåŽ†æ¨¡å¼æ˜¯ç»å…¸çš„ visitor
模å¼
,visitor
模å¼å°±æ˜¯å®šä¹‰ä¸€ç³»åˆ—çš„ visitor
,当碰到 AST
çš„ type === visitor
åå—æ—¶ï¼Œå°±ä¼šè¿›å…¥è¿™ä¸ª visitor
的函数。类型为 ImportDeclaration
的 AST 节点,其实就是我们的 import xxx from xxxx
,最åŽå°†åœ°å€ push 到 dependencies ä¸.
最åŽå¯¼å‡ºçš„æ—¶å€™ï¼Œä¸è¦å¿˜è®°äº†ï¼Œæ¯å¯¼å‡ºä¸€ä¸ªæ–‡ä»¶æ¨¡å—,我们都往全局自增 id
ä¸ + 1
,以ä¿è¯æ¯ä¸€ä¸ªæ–‡ä»¶æ¨¡å—的唯一性。
function parseGraph(entry) {
// 从 entry 出å‘,首先收集 entry 文件的ä¾èµ–
const entryAsset = parseDependecies(path.resolve(currentPath, entry))
// graph å…¶å®žæ˜¯ä¸€ä¸ªæ•°ç»„ï¼Œæˆ‘ä»¬å°†æœ€å¼€å§‹çš„å…¥å£æ¨¡å—放在最开头
const graph = [entryAsset]
for (const asset of graph) {
if (!asset.idMapping) asset.idMapping = {}
// èŽ·å– asset 䏿–‡ä»¶å¯¹åº”的文件夹
const dir = path.dirname(asset.filename)
// æ¯ä¸ªæ–‡ä»¶éƒ½ä¼šè¢« parse 出一个 dependencise,他是一个数组,在之å‰çš„函数ä¸å·²ç»è®²åˆ°
// å› æ¤ï¼Œæˆ‘们è¦é历这个数组,将有用的信æ¯å…¨éƒ¨å–出æ¥
// 值得关注的是 asset.idMapping[dependencyPath] = denpendencyAsset.id æ“作
// 我们往下看
asset.dependencies.forEach(dependencyPath => {
// èŽ·å–æ–‡ä»¶ä¸æ¨¡å—çš„ç»å¯¹è·¯å¾„,比如 import ABC from './world'
// ä¼šè½¬æ¢æˆ /User/xxxx/desktop/xproject/world è¿™æ ·çš„å½¢å¼
const absolutePath = path.resolve(dir, dependencyPath)
// è§£æžè¿™äº›ä¾èµ–
const denpendencyAsset = parseDependecies(absolutePath)
// 获å–唯一 id
const id = denpendencyAsset.id
// 这里是é‡è¦çš„ç‚¹äº†ï¼Œæˆ‘ä»¬è§£æžæ¯è§£æžä¸€ä¸ªæ¨¡å—ï¼Œæˆ‘ä»¬å°±å°†ä»–è®°å½•åœ¨è¿™ä¸ªæ–‡ä»¶æ¨¡å— asset 下的 idMapping ä¸
// ä¹‹åŽæˆ‘们 require 的时候,能够通过这个 id 值,找到这个模å—对应的代ç ,并进行è¿è¡Œ
asset.idMapping[dependencyPath] = denpendencyAsset.id
// 将解æžçš„æ¨¡å—推入 graph ä¸åŽ»
graph.push(denpendencyAsset)
})
}
// 返回这个 graph
return graph
}
接下æ¥ï¼Œæˆ‘们对模å—进行更高级的处ç†ã€‚我们之å‰å·²ç»å†™äº†ä¸€ä¸ª parseDependecies
å‡½æ•°ï¼Œé‚£ä¹ˆçŽ°åœ¨æˆ‘ä»¬è¦æ¥å†™ä¸€ä¸ª parseGraph
函数,我们将所有文件模å—组æˆçš„集åˆå«åš graph
(ä¾èµ–图),用于æè¿°æˆ‘们这个项目的所有的ä¾èµ–关系,parseGraph
从 entry
(入å£ï¼‰ 出å‘ï¼Œä¸€ç›´æ‰‹æœºå®Œæ‰€æœ‰çš„ä»¥æ¥æ–‡ä»¶ä¸ºæ¢.
在这里我们使用 for of
å¾ªçŽ¯è€Œä¸æ˜¯ forEach
ï¼ŒåŽŸå› æ˜¯å› ä¸ºæˆ‘ä»¬åœ¨å¾ªçŽ¯ä¹‹ä¸ä¼šä¸æ–çš„å‘ graph
ä¸ï¼Œpush
进东西,graph
ä¼šä¸æ–å¢žåŠ ï¼Œç”¨ for of
会一直æŒç»è¿™ä¸ªå¾ªçŽ¯ç›´åˆ° graph
ä¸ä¼šå†è¢«æŽ¨è¿›åŽ»ä¸œè¥¿ï¼Œè¿™å°±æ„味ç€ï¼Œæ‰€æœ‰çš„ä¾èµ–å·²ç»è§£æžå®Œæ¯•,graph
数组数é‡ä¸ä¼šç»§ç»å¢žåŠ ï¼Œä½†æ˜¯ç”¨ forEach
是ä¸è¡Œçš„,åªä¼šé历一次。
在 for of
循环ä¸ï¼Œasset
代表解æžå¥½çš„æ¨¡å—ï¼Œé‡Œé¢æœ‰ filename
, code
, dependencies
ç‰ä¸œè¥¿ asset.idMapping
是一个ä¸å¤ªå¥½ç†è§£çš„æ¦‚念,我们æ¯ä¸€ä¸ªæ–‡ä»¶éƒ½ä¼šè¿›è¡Œ import
æ“作,import
æ“作åœ
8CCF
¨ä¹‹åŽä¼šè¢«è½¬æ¢æˆ require
æ¯ä¸€ä¸ªæ–‡ä»¶ä¸çš„ require
çš„ path
其实会对应一个数å—自增 id
,这个自增 id
其实就是我们一开始的时候设置的 id
,我们通过将 path-id
利用键值对,对应起æ¥ï¼Œä¹‹åŽæˆ‘ä»¬åœ¨æ–‡ä»¶ä¸ require
就能够轻æ¾çš„æ‰¾åˆ°æ–‡ä»¶çš„代ç ï¼Œè§£é‡Šè¿™ä¹ˆå•°å—¦çš„åŽŸå› æ˜¯å¾€å¾€æ¨¡å—之间的引用是错ä¸å¤æ‚的,这æ°å·§æ˜¯è¿™ä¸ªæ¦‚å¿µéš¾ä»¥è§£é‡Šçš„åŽŸå› ã€‚
function build(graph) {
// 我们的 modules 就是一个å—符串
let modules = ''
graph.forEach(asset => {
modules += `${asset.id}:[
function(require,module,exports){${asset.code}},
${JSON.stringify(asset.idMapping)},
],`
})
const wrap = `
(function(modules) {
function require(id) {
const [fn, idMapping] = modules[id];
function childRequire(filename) {
return require(idMapping[filename]);
}
const newModule = {exports: {}};
fn(childRequire, newModule, newModule.exports);
return newModule.exports
}
require(0);
})({${modules}});` // 注æ„这里需è¦ç»™ modules åŠ ä¸Šä¸€ä¸ª {}
return wrap
}
// 这是一个 loader 的最简å•实现
function loader(filename, code) {
if (/index/.test(filename)) {
console.log('this is loader ')
}
return code
}
// æœ€åŽæˆ‘们导出我们的 bundler
module.exports = entry => {
const graph = parseGraph(entry)
const bundle = build(graph)
return bundle
}
我们完æˆäº† graph 的收集,那么就到我们真æ£çš„ä»£ç æ‰“包了,这个函数使用了大é‡çš„å—符串处ç†ï¼Œä½ 们ä¸è¦è§‰å¾—奇怪,为什么代ç å’Œå—符串å¯ä»¥æ··èµ·æ¥å†™ï¼Œå¦‚æžœä½ è·³å‡ºå†™ä»£ç 的范畴,看我们的代ç ,实际上,代ç 就是å—符串,åªä¸è¿‡ä»–通过特殊的è¯è¨€å½¢å¼ç»„织起æ¥è€Œå·²ï¼Œå¯¹äºŽè„šæœ¬è¯è¨€ JS æ¥è¯´ï¼Œå—符串拼接æˆä»£ç ,然åŽè·‘èµ·æ¥ï¼Œè¿™ç§æ“作在å‰ç«¯éžå¸¸çš„常è§ï¼Œæˆ‘è®¤ä¸ºï¼Œè¿™ç§æ€ç»´çš„转æ¢ï¼Œæ˜¯æ‹¥æœ‰è‡ªåŠ¨åŒ–ã€å·¥ç¨‹åŒ–的第一æ¥ã€‚
我们将 graph 䏿‰€æœ‰çš„ asset å–出æ¥ï¼Œç„¶åŽä½¿ç”¨ node.js åˆ¶é€ æ¨¡å—的方法æ¥å°†ä¸€ä»½ä»£ç 包起æ¥ï¼Œæˆ‘之å‰åšè¿‡ä¸€ä¸ªã€Šåº–ä¸è§£ç‰›ï¼šæ•™ä½ 如何实现》node.js 模å—çš„æ–‡ç« ï¼Œä¸æ‡‚çš„å¯ä»¥åŽ»çœ‹çœ‹ï¼Œhttps://zhuanlan.zhihu.com/p/34974579
在这里简å•讲述,我们将转æ¢å¥½çš„æºç ,放进一个 function(require,module,exports){}
函数ä¸ï¼Œè¿™ä¸ªå‡½æ•°çš„傿•°å°±æ˜¯æˆ‘们éšå¤„å¯ç”¨çš„ require
,module
,ä»¥åŠ exports
,这就是为什么我们å¯ä»¥éšå¤„使用这三个玩æ„çš„åŽŸå› ï¼Œå› ä¸ºæˆ‘ä»¬æ¯ä¸€ä¸ªæ–‡ä»¶çš„代ç ç»ˆå°†è¢«è¿™æ ·ä¸€ä¸ªå‡½æ•°åŒ…è£¹èµ·æ¥ï¼Œä¸è¿‡è¿™æ®µä»£ç 䏿¯”较奇怪的是,我们将代ç å°è£…æˆäº† 1:[...],2:[...]
的形å¼ï¼Œæˆ‘们在最åŽå¯¼å…¥æ¨¡å—的时候,会为这个å—ç¬¦ä¸²åŠ ä¸Šä¸€ä¸ª {}
ï¼Œå˜æˆ {1:[...],2:[...]}
ï¼Œä½ æ²¡çœ‹é”™ï¼Œè¿™æ˜¯ä¸€ä¸ªå¯¹è±¡ï¼Œè¿™ä¸ªå¯¹è±¡é‡Œç”¨æ•°å—作为 key
,一个二维元组作为值:
- [0] 第一个就是我们被包裹的代ç
- [1] 第二个就是我们的
mapping
马上è¦è§åˆ°æ›™å…‰äº†ï¼Œè¿™ä¸€æ®µä»£ç å®žé™…ä¸Šæ‰æ˜¯æ¨¡å—å¼•å…¥çš„æ ¸å¿ƒé€»è¾‘ï¼Œæˆ‘ä»¬åˆ¶é€ ä¸€ä¸ªé¡¶å±‚çš„ require
函数,这个函数接收一个 id
作为值,并且返回一个全新的 module
对象,我们倒入我们刚刚制作好的模å—ï¼Œç»™ä»–åŠ ä¸Š {}
,使其æˆä¸º {1:[...],2:[...]}
è¿™æ ·ä¸€ä¸ªå®Œæ•´çš„å½¢å¼ã€‚
ç„¶åŽå¡žå…¥æˆ‘ä»¬çš„ç«‹å³æ‰§è¡Œå‡½æ•°ä¸(function(modules) {...})()
,在 (function(modules) {...})()
ä¸ï¼Œæˆ‘们先调用 require(0)
,ç†ç”±å¾ˆç®€å•ï¼Œå› ä¸ºæˆ‘ä»¬çš„ä¸»æ¨¡å—æ°¸è¿œæ˜¯æŽ’在第一ä½çš„,紧接ç€ï¼Œåœ¨æˆ‘们的 require
函数ä¸ï¼Œæˆ‘ä»¬æ‹¿åˆ°å¤–éƒ¨ä¼ è¿›æ¥çš„ modules
ï¼Œåˆ©ç”¨æˆ‘ä»¬ä¸€ç›´åœ¨è¯´çš„å…¨å±€æ•°å— id
èŽ·å–æˆ‘们的模å—,æ¯ä¸ªæ¨¡å—获å–出æ¥çš„就是一个二维元组。
ç„¶åŽï¼Œæˆ‘们è¦åˆ¶é€ 一个 årequire
,这么åšçš„åŽŸå› æ˜¯æˆ‘ä»¬åœ¨æ–‡ä»¶ä¸ä½¿ç”¨ require
时,我们一般 require
的是地å€ï¼Œè€Œé¡¶å±‚çš„ require
å‡½æ•°å‚æ•°æ—¶ id
ä¸è¦æ‹…心,我们之å‰çš„ idMapping
在这里就用上了,通过用户 require
è¿›æ¥çš„地å€ï¼Œåœ¨ idMapping
䏿‰¾åˆ° id
。
ç„¶åŽé€’归调用 require(id)
,就能够实现模å—的自动倒入了,接下æ¥åˆ¶é€ 一个 const newModule = {exports: {}};
,è¿è¡Œæˆ‘们的函数 fn(childRequire, newModule, newModule.exports);
ï¼Œå°†åº”è¯¥ä¸¢è¿›åŽ»çš„ä¸¢è¿›åŽ»ï¼Œæœ€åŽ return newModule.exports
这个模å—çš„ exports
对象。
这里的逻辑其实跟 node.js 差别ä¸å¤ªå¤§ã€‚
测试的代ç ï¼Œæˆ‘å·²ç»æ”¾åœ¨äº†ä»“库里,想测试一下的åŒå¦å¯ä»¥åŽ»ä»“åº“ä¸è‡ªè¡Œæå–。
打满注释的代ç 也放在仓库了,点击地å€
git clone https://github.com/Foveluy/roid.git
npm i
node ./src/_test.js ./example/index.js
输出
this is loader
hello zheng Fang!
welcome to roid, I'm zheng Fang
if you love roid and learnt any thing, please give me a star
https://github.com/Foveluy/roid