roid
roid 是一個極為簡樸的打包軟件,運用 node.js 開闢而成,看完本文,你可以完成一個異常簡樸的,然則又有現實用處的前端代碼打包東西。
假如不想看教程,直接看代碼的(悉數詮釋):點擊地點
為何要寫 roid ?
我們天天都面臨前端的這幾款編譯東西,然則在大批攀談中我得知,並非很多人曉得這些打包軟件背地的事變道理,因而有了這個 project 湧現。固然,你並不需要相識太多編譯道理之類的事變,假如你在此之前對 node.js 極為熟習,那末你對前端打包東西一定能異常好的明白。
弄清楚打包東西的背地道理,有利於我們完成種種奇異的自動化、工程化東西,比方表單的雙向綁定,自創 JavaScript 語法,又如螞蟻金服 ant 中赫赫有名的 import 插件,以至是前端文件自動掃描載入等,可以極大的提拔我們事變效率。
不空話,我們直接最先。
從一個自增 id 最先
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
操縱在以後會被轉換成 require
每一個文件中的 require
的 path
實在會對應一個数字自增 id
,這個自增 id
實在就是我們一最先的時刻設置的 id
,我們經由歷程將 path-id
應用鍵值對,對應起來,以後我們在文件中 require
就可以輕鬆的找到文件的代碼,詮釋這麼煩瑣的緣由是每每模塊之間的引用是錯中龐雜的,這恰巧是這個觀點難以詮釋的緣由。
末了,天生 bundle
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/…
在這裏簡樸報告,我們將轉換好的源碼,放進一個 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