在看阮一峯老師的每週分享後,看到了一篇關於如何寫一個模塊打包器的一篇英文文章,以前基本沒有了解過,只知道如何使用webpack等,因此這一篇對我來說很及時,好記性不如爛筆頭,因此先嚐試着把它翻譯出來。html
人生已如此艱難,有些事情就不要拆穿(其實使用google翻譯就行了)node
這裏先強烈安利一波:阮一峯老師的每週分享系列,能夠了解不少新的東西,我的以爲很是nice,第一手的技術資訊網站:hacker newswebpack
原文請看我 翻譯錯誤處請你們指正git
你們好!。。。(客套話)歡迎來到個人酒館,今晚累的夠嗆,但只要有客人來玩我都歡迎(爐石手動滑稽,原文無此段)。今天咱們將構建一個很是簡單的js模塊打包器。github
在咱們開始以前,我想確認下大家看了下面這些文章沒有,本文依賴於此。web
好了,讓咱們開始瞭解模塊打包器究竟是什麼?算法
你可能用過像Browserify,Webpack,Rollup
等工具,但一個模塊打包器是一個獲取js及其依賴項並將他們轉換爲單獨的文件,一般使用在瀏覽器端。npm
它一般開始於入口文件,並從入口文件的依賴項中獲取全部的代碼編程
下面是打包器主要的兩個階段數組
從入口點(上圖中app.js)開始,依賴解析的目標是尋找你的代碼中的全部依賴,也就是代碼運行須要的其餘代碼片斷,並構建出上圖(依賴圖)
一旦完成後,你就能夠開始打包,或者將你的依賴圖中的代碼合併至一個你可使用的文件中。
讓咱們開始導入一些咱們的代碼(我待會會給出緣由)
const detective = require('detective')
const resolve = require('resolve').sync
const fs = require('fs')
const path = require('path')
複製代碼
咱們要作的第一件事是思考在依賴解析階段咱們用什麼來表明一個模塊。
咱們須要下面四個東西
依賴圖的結構構建須要遞歸文件的依賴
在js中,最簡單表示這一組數據的方式是一個對象,那麼咱們先這樣作
let ID = 0
function createModuleObject(filepath) {
const source = fs.readFileSync(filepath, 'utf-8')
const requires = detective(source)
const id = ID++
return { id, filepath, source, requires }
}
複製代碼
看看createModuleObject
方法,須要注意的是調用了一個detective
的方法。 detective
是個一個庫用於查找全部對require的調用,不管嵌套有多深,使用它意味着咱們能夠避免本身進行AST遍歷得出文件的全部的依賴。
有一點須要注意(幾乎在全部的模塊打包器中都是同樣的),若是你想作一些奇怪的事情
const libName = 'lodash'
const lib = require(libName)
複製代碼
依賴解析時將沒法找到這個模塊(由於這須要執行代碼)
那麼在給出一個模塊後運行這個方法會等到什麼呢?
下一步是什麼,依賴解析!!
好吧,還沒到,我首先想要講一個東西-模塊圖(module map)
當你在node引入模塊時,你可使用相對路徑,好比require('./utils')
。當你的代碼執行到這時,打包器怎麼知道正確的./utils
文件在哪。
這是一個模塊圖解決的問題
咱們的模塊對象有一個id來標識來源,因此當咱們開始依賴解析時,對於每個模塊,咱們都將保留一份清單,列出所需的名字和id,因此在運行時咱們能夠等到正確的模塊。
那意味着咱們能夠將全部模塊存儲在用id做爲鍵的非嵌套對象中!
function getModules(entry) {
const rootModule = createModuleObject(entry)
const modules = [rootModule]
// Iterate over the modules, even when new
// ones are being added
for (const module of modules) {
module.map = {} // Where we will keep the module maps
module.requires.forEach(dependency => {
const basedir = path.dirname(module.filepath)
const dependencyPath = resolve(dependency, { basedir })
const dependencyObject = createModuleObject(dependencyPath)
module.map[dependency] = dependencyObject.id
modules.push(dependencyObject)
})
}
return modules
}
複製代碼
好的,getModules
方法裏面會有至關多的模塊,這個方法主要用於從入口模塊開始,以遞歸的方式查找和解析依賴項。
解析依賴是什麼意思? 在node裏有個東西叫require.resolve
,這就是node怎麼樣找到你須要文件的位置的緣由。這使得咱們能夠導入相對或者從node_modules
中導入模塊。
幸運的是,有一個叫resolve
的npm模塊能夠爲咱們實現這樣的算法,咱們只須要把引入的文件和位置做爲參數傳遞,它就能夠幫咱們完成其餘複雜的工做。
因此咱們開始解析項目中每個模塊的每個依賴項
咱們也能夠構建我以前提到的模塊圖
在這個方法的最後,咱們返回了一個叫modules
的數組,裏面存儲了咱們項目中每一個模塊/依賴項的模塊對象。
在瀏覽器中沒有modules
,這意味着沒有require
函數和module.exports
,因此即便咱們拿到了咱們所須要的全部依賴項,也沒把他們做爲模塊來使用。
工廠函數
工廠函數是一個返回對象的函數(不是構造函數),它是面向對象編程的模式,其用途之一是進行封裝和依賴注入。
聽上去不錯?
使用工廠函數,咱們要注入能夠在打包後的代碼中使用的require
函數和module.exports
對象,而且給出這個模塊的做用域。
// A factory function
(require, module) => {
/* Module Source */
}
複製代碼
我如今跟你展現打包方法,以後我會解釋其他的。
function pack(modules) {
const modulesSource = modules.map(module =>
`${module.id}: {
factory: (module, require) => {
${module.source}
},
map: ${JSON.stringify(module.map)}
}`
).join()
return `(modules => {
const require = id => {
const { factory, map } = modules[id]
const localRequire = name => require(map[name])
const module = { exports: {} }
factory(module, localRequire)
return module.exports
}
require(0)
})({ ${modulesSource} })`
}
複製代碼
大多數都只是js模板語言,因此讓咱們來討論它在作什麼
首先是modulesSource
,這裏,咱們將遍歷每一個模塊,並將其轉換爲一串源代碼。
那麼一個模塊對象最後會變成什麼
如今它有點難以閱讀,可是你能夠看到目標被封裝了,咱們爲以前提到的factory
函數提供了modules
和require
。
同時還包括了在依賴解析階段咱們構造的模塊映射圖
在下一步,咱們把這些全部的依賴對象數組構建成了一個大的對象
下一串代碼是IIFE
(當即執行函數表達式),這意味你在瀏覽器或者別的地方運行代碼時,這個函數將會被當即執行,IIFE
是封裝做用域的另外的一種模式,因此在這裏咱們擔憂require
和moduels
會污染全局做用域。
你也能夠看到咱們定義了兩個require
函數,require
和localRequire
。
require
把模塊對象的id做爲參數,但源代碼是沒有id的,咱們使用其餘函數localRequire
經過傳入任何參數並轉成正確的id來獲取模塊,正是經過模塊圖來實現的。
在這以後,咱們定義了一個能夠填充的模塊對象,把對象和localRequire
做爲參數傳入factory
,而後返回module.exports
。
最後,咱們執行require(0)
去引入id爲0的模塊做爲咱們的入口模塊。
搞定,咱們的模塊打包器就已經完成了。
module.exports = entry => pack(getModules(entry))
複製代碼
因此咱們如今已經擁有了一個模塊打包器。
如今這個可能不能用於生產,由於它缺乏了大量的功能(管理循環依賴,確保每一個文件只被解析一次,es-modules等等),但但願能使你對模塊打包器的實際工做方式有所瞭解。
實際上,你刪除全部模塊中的源代碼,實現這個模塊打包器才大約60行。
感謝閱讀,但願您對咱們這個簡單的模塊打包器如何工做有所瞭解