探索React源码的全局模块系统

也可以在这里看: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插件来负责这个事情,这是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插件的编写,过了元旦要开始新的项目了,准备尝试尝试,把它加进工作流中去。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容

  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,164评论 7 35
  • 学习流程 参考文档:入门Webpack,看这篇就够了Webpack for React 一. 简单使用webpac...
    Jason_Zeng阅读 3,138评论 2 16
  • 这个下午有幸拜读了一部很优秀的作品,《摆渡人》 讲的是十五岁的女孩迪伦有着糟糕的青春,在去看他十年未见的离婚...
    花信阅读 277评论 0 1
  • 央视新闻频道每天在滚动播放选择火车出行人次几百万趋势直线上长以及摩托车大军开始返乡,想必他们都是工作党——大包小包...
    滴52赫兹阅读 422评论 0 0
  • 作业1:三朝回门,卑卑褪下了青狐大衣,里面穿着泥金缎短袖旗袍。人像金瓶里的一朵栀子花。淡白的鹅蛋脸,虽然是单眼皮,...
    王嘲阅读 455评论 6 2