也能够在这里看: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插件的编写,过了元旦要开始新的项目了,准备尝试尝试,把它加进工做流中去。