也能夠在這裏看: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.js
。json
引用 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 了,它能夠編譯你的代碼,解析你註釋中的 @providesModules
,輸出一個扁平化的目錄,文件名爲各自的模塊標識符的名字,require()
也會被替換成正確的相對路徑,有興趣的話能夠了解下這個工具,好像也是 reactjs 這個 organiztion 裏的,不過不知道爲何不用了,估計是由於要迎合 babel 生態的關係吧,react 的項目中用 babel 插件代替了它。
大體考慮了一下,爲何FB的團隊會整出這個所謂的『全局模塊系統』,我以爲仍是和它巨大的 codebase 是有關的,什麼 React、RN、Flow、Relay 等等,那麼必然會有一些公共的工具庫,並且像 React 一個項目自己的 codebase 也很大了,因此要維護各類相對路徑,很吃力,但有利有弊吧:
好處:
不須要維護模塊之間的相對路徑
能夠更放肆地調整目錄結構而不對代碼產生影響
缺點:
模塊必須經過惟一標識標記而再也不取決與文件路徑,因此必須保證不能重名
要對模塊很熟悉,否則光看到一個名字,而後找不到對應的文件在哪裏
其實仍是挺有意思的,在探索的過程也順便了解了babel插件的編寫,過了元旦要開始新的項目了,準備嘗試嘗試,把它加進工做流中去。