探索React源碼的全局模塊系統

jopen 8年前發布 | 13K 次閱讀 React JavaScript開發

也可以在這里看: https://leozdgao.me/react-global-module-system/

掃了幾眼react的源代碼(0.14-stable分支),發現一個有趣的現象,比如如下這段代碼:

var ReactDOM = require('ReactDOM');  
var ReactDOMServer = require('ReactDOMServer');  
var ReactIsomorphic = require('ReactIsomorphic');

var assign = require('Object.assign');  
var deprecated = require('deprecated');  

熟悉 node.js 的 CommonJS 模塊系統的話,我們知道有如下3種情況:

  • 依賴一個原生模塊(native module),比如fs模塊或者是events模塊。
  • 以 '/' 、 './' 或 '../' 開頭,代表文件路徑,比如用 require('./my-module') 來獲取當前目錄下 my-module.js 文件所導出的模塊。
  • 否則,則從當前目錄的 node_modules 文件夾中找,如果沒有找到,就從父目錄的 node_modules 文件夾中找,遞歸到根目錄的 node_modules 文件夾。

根據以上規則,例子中的代碼顯然屬于第三種情況,然而實際上 ReactDOM 或者 Object.assign 這幾個模塊并不屬于 node_modules 文件夾,它們其實也存在與本地的源代碼中,比如對應的 Object.assign 模塊實際上位于 /src/shared/stubs/Object.assign.js 。

引用 google groups 上一個回答,這是它們的 全局模塊系統 。出于好奇,決定探索一番,看看這是如何實現的。

工作流

首先的一點是,由于它的模塊依賴方式和我們熟悉的方式并不吻合,所以我們需要探索這個部分的工作流,看這個全局模塊系統是如何融入整個開發過程中的。

從源代碼里知道到了這部分任務,是定義在 gulpfile.js 中的 react:modules 任務:

  • src 目錄下的代碼會被編譯
  • 編譯完后代碼結構被扁平化
  • 所有代碼中的 require 會被轉化為相對路徑的形式

也就是說,本來這樣的目錄:

- src
  - lib
    - ReactElement.js
    - ReactDOM.js
  - index.js

變成了這樣:

- build 
  - index.js
  - ReactElement.js
  - ReactDOM.js

如果 index.js 中本來有 require('ReactElement') ,最后就被編譯為 require('./ReactElement') 了。

正是有這樣的一個步驟,讓這個全局模塊系統得以工作,再思考下其中的細節,這個編譯過程需要做哪些東西:

  • 用于標記模塊的標識符
  • 標識符與對應文件路徑的Map,用于替換require的模塊標識

好的,順著這個思路在來看看代碼,我們發現主要是 rewrite-modules 這個babel插件來負責這個事情,這是非死book的自定義babel插件,要了解如何編寫一個自定義babel插件的話,可以參考 這篇文檔

在 rewrite-modules 的代碼中可以發現一個叫做 mapModule 的函數,負責 require() 中模塊標識的替換,其中模塊共有兩個來源:

  • 由于非死book巨大的codebase的關系,一些工具函數在 fbjs 這個項目里,包括什么 invariant 函數或者是 warning 函數這些
  • 當前項目的本地模塊

而fbjs這個項目在編譯的時候會生成一個 module-map.json 的文件,來表示唯一模塊標識符和正常方式引用模塊的標識符之間的映射,那么這個文件是如何生成的呢?

從 fbjs/scripts/gulp/module-map.js 的代碼來看,是用了 @providesModules <moduleName> 來標記模塊,比如 areEqual.js 這個文件的注釋中可以發現:

* @providesModule areEqual

并且有一個 prefix 的設置,設置為 fbjs/lib/ ,所以如果我有如下代碼:

require('areEqual')  

則會被編譯成:

require('fbjs/lib/areEqual')  

不過奇怪的是,在React的源代碼中也可以發現 @providesModules 標記,但在 React 源代碼編譯的工作流中,并沒有發現解析這個標記的邏輯,它的邏輯是:如果模塊在 fbjs 的 moduleMap 中找不到,則直接加上 ./ 的前綴,也就是說:

require('ReactElement')  

直接變成:

require('./ReactElement')  

我也嘗試修改 React 源代碼中的 @providesModules ,對編譯結果沒有影響。至于這里為什么會有兩種不同的邏輯,我也不清楚。

很清楚了,開始的時候也說過了,那個負責編譯源代碼的 gulp task 中,有扁平化這個源代碼的目錄結構的任務,那么所有本地模塊,也都可以被正確引用到了。

Commoner

我還發現一個工具,就是這個 Commoner 了,它可以編譯你的代碼,解析你注釋中的 @providesModules ,輸出一個扁平化的目錄,文件名為各自的模塊標識符的名字, require() 也會被替換成正確的相對路徑,有興趣的話可以了解下這個工具,好像也是 reactjs 這個 organiztion 里的,不過不知道為什么不用了,估計是因為要迎合 babel 生態的關系吧,react 的項目中用 babel 插件代替了它。

一些思考

大致考慮了一下,為什么FB的團隊會整出這個所謂的『全局模塊系統』,我覺得還是和它巨大的 codebase 是有關的,什么 React、RN、Flow、Relay 等等,那么必然會有一些公共的工具庫,而且像 React 一個項目本身的 codebase 也很大了,所以要維護各種相對路徑,很吃力,但有利有弊吧:

好處:

  • 不需要維護模塊之間的相對路徑
  • 可以更放肆地調整目錄結構而不對代碼產生影響

缺點:

  • 模塊必須通過唯一標識標記而不再取決與文件路徑,所以必須保證不能重名
  • 要對模塊很熟悉,不然光看到一個名字,然后找不到對應的文件在哪里

其實還是挺有意思的,在探索的過程也順便了解了babel插件的編寫,過了元旦要開始新的項目了,準備嘗試嘗試,把它加進工作流中去。

來自: http://leozdgao.me/react-global-module-system/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!