如何寫一個js模組打包器(翻譯)

yangchendoit發表於2019-03-01

前言

在看阮一峰老師的每週分享後,看到了一篇關於如何寫一個模組打包器的一篇英文文章,之前基本沒有了解過,只知道如何使用webpack等,所以這一篇對我來講很及時,好記性不如爛筆頭,所以先嚐試著把它翻譯出來。

人生已如此艱難,有些事情就不要拆穿(其實使用google翻譯就好了)

艱難

這裡先強烈安利一波:阮一峰老師的每週分享系列,可以瞭解很多新的東西,個人覺得非常nice,第一手的技術資訊網站:hacker news

原文

原文請看我 翻譯錯誤處請大家指正

譯文

讓我們寫個模組打包器

大家好!。。。(客套話)歡迎來到我的酒館,今晚累的夠嗆,但只要有客人來玩我都歡迎(爐石手動滑稽,原文無此段)。今天我們將構建一個非常簡單的js模組打包器。

在我們開始之前,我想確認下你們看了下面這些文章沒有,本文依賴於此。

好了,讓我們開始瞭解模組打包器到底是什麼?

什麼是模組打包器

你可能用過像Browserify,Webpack,Rollup等工具,但一個模組打包器是一個獲取js及其依賴項並將他們轉換為單獨的檔案,通常使用在瀏覽器端。

它通常開始於入口檔案,並從入口檔案的依賴項中獲取所有的程式碼

如何寫一個js模組打包器(翻譯)

下面是打包器主要的兩個階段

  1. 依賴解析
  2. 打包

從入口點(上圖中app.js)開始,依賴解析的目標是尋找你的程式碼中的所有依賴,也就是程式碼執行需要的其他程式碼片段,並構建出上圖(依賴圖)

一旦完成後,你就可以開始打包,或者將你的依賴圖中的程式碼合併至一個你可以使用的檔案中。

讓我們開始匯入一些我們的程式碼(我待會會給出原因)

const detective = require('detective')
const resolve = require('resolve').sync
const fs = require('fs')
const path = require('path')
複製程式碼

依賴解析

我們要做的第一件事是思考在依賴解析階段我們用什麼來代表一個模組。

模組表示

我們需要下面四個東西

  1. 檔名字和檔案標識
  2. 在檔案系統中檔案的位置
  3. 檔案中的程式碼
  4. 該檔案需要哪些依賴

依賴圖的結構構建需要遞迴檔案的依賴

在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)
複製程式碼

依賴解析時將無法找到這個模組(因為這需要執行程式碼)

那麼在給出一個模組後執行這個方法會等到什麼呢?

如何寫一個js模組打包器(翻譯)

下一步是什麼,依賴解析!!

好吧,還沒到,我首先想要講一個東西-模組圖(module map)

模組圖

當你在node引入模組時,你可以使用相對路徑,比如require('./utils')。當你的程式碼執行到這時,打包器怎麼知道正確的./utils檔案在哪。

這是一個模組圖解決的問題

我們的模組物件有一個id來標識來源,所以當我們開始依賴解析時,對於每一個模組,我們都將保留一份清單,列出所需的名字和id,所以在執行時我們可以等到正確的模組。

那意味著我們可以將所有模組儲存在用id作為鍵的非巢狀物件中!

如何寫一個js模組打包器(翻譯)

依賴解析

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,這裡,我們將遍歷每個模組,並將其轉換為一串原始碼。

那麼一個模組物件最後會變成什麼

如何寫一個js模組打包器(翻譯)

現在它有點難以閱讀,但是你可以看到目標被封裝了,我們為之前提到的factory函式提供了modulesrequire

同時還包括了在依賴解析階段我們構造的模組對映圖

在下一步,我們把這些所有的依賴物件陣列構建成了一個大的物件

下一串程式碼是IIFE(立即執行函式表示式),這意味你在瀏覽器或者別的地方執行程式碼時,這個函式將會被立即執行,IIFE是封裝作用域的另外的一種模式,所以在這裡我們擔心requiremoduels會汙染全域性作用域。

你也可以看到我們定義了兩個require函式,requirelocalRequire

require把模組物件的id作為引數,但原始碼是沒有id的,我們使用其他函式localRequire通過傳入任何引數並轉成正確的id來獲取模組,正是通過模組圖來實現的。

在這之後,我們定義了一個可以填充的模組物件,把物件和localRequire作為引數傳入factory,然後返回module.exports

最後,我們執行require(0)去引入id為0的模組作為我們的入口模組。

搞定,我們的模組打包器就已經完成了。

module.exports = entry => pack(getModules(entry))
複製程式碼

最後

所以我們現在已經擁有了一個模組打包器。

現在這個可能不能用於生產,因為它缺少了大量的功能(管理迴圈依賴,確保每個檔案只被解析一次,es-modules等等),但希望能使你對模組打包器的實際工作方式有所瞭解。

實際上,你刪除所有模組中的原始碼,實現這個模組打包器才大約60行。

感謝閱讀,希望您對我們這個簡單的模組打包器如何工作有所瞭解

個人部落格地址 歡迎騷擾!!!

相關文章