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

也能夠在這裏看:https://leozdgao.me/react-global-module-system/javascript

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

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種狀況:node

  • 依賴一個原生模塊(native module),好比fs模塊或者是events模塊。react

  • '/''./''../' 開頭,表明文件路徑,好比用 require('./my-module') 來獲取當前目錄下 my-module.js 文件所導出的模塊。git

  • 不然,則從當前目錄的 node_modules 文件夾中找,若是沒有找到,就從父目錄的 node_modules 文件夾中找,遞歸到根目錄的 node_modules 文件夾。github

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

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

工做流

首先的一點是,因爲它的模塊依賴方式和咱們熟悉的方式並不吻合,因此咱們須要探索這個部分的工做流,看這個全局模塊系統是如何融入整個開發過程當中的。babel

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

  • 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插件來負責這個事情,這是Facebook的自定義babel插件,要了解如何編寫一個自定義babel插件的話,能夠參考這篇文檔

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

  • 因爲Facebook巨大的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插件的編寫,過了元旦要開始新的項目了,準備嘗試嘗試,把它加進工做流中去。

相關文章
相關標籤/搜索