React背后的工具化体系

一.概览
React工具链标签云:html

Rollup    Prettier    Closure Compiler
Yarn workspace    [x]Haste    [x]Gulp/Grunt+Browserify
ES Module    [x]CommonJS Module
Flow    Jest    ES Lint    React DevTools
Error Code System    HUBOT(GitHub Bot)    npm

P.S.带[x]的表示以前在用,最近(React 16)不用了node

简单分类以下:react

开发:ES Module, Flow, ES Lint, Prettier, Yarn workspace, HUBOT
构建:Rollup, Closure Compiler, Error Code System, React DevTools
测试:Jest, Prettier
发布:npm

按照ES模块机制组织源码,辅以类型检查和Lint/格式化工具,借助Yarn处理模块依赖,HUBOT检查PR;Rollup + Closure Compiler构建,利用Error Code机制实现生产环境错误追踪,DevTools侧面辅助bundle检查;Jest驱动单测,还经过格式化bundle来确认构建结果足够干净;最后经过npm发布新packageexpress

整个过程并不十分复杂,但在一些细节上的考虑至关深刻,例如Error Code System、双保险envification(dev/prod环境区分)、发布流程工具化npm

二.开发工具json

CommonJS Module + Haste -> ES Module

React 15以前的版本都用CommonJS模块定义,例如:gulp

var ReactChildren = require('ReactChildren');
module.exports = React;

目前切换到了ES Module,几个缘由:bootstrap

有助于及早发现模块引入/导出问题ide

CommonJS Module很容易require一个不存在的方法,直到调用报错时才能发现问题。ES Module静态的模块机制要求import与export必须按名匹配,不然编译构建就会报错函数

bundle size上的优点

ES Module能够经过tree shaking让bundle更干净,根本缘由是module.exports是对象级导出,而export支持更细粒度的原子级导出。另外一方面,按名引入使得rollup之类的工具可以把模块扁平地拼接起来,压缩工具就能在此基础上进行更暴力的变量名混淆,进一步减少bundle size

只把源码切换到了ES Module,单测用例并未切换,由于CommonJS Module对Jest的一些特性(好比resetModules)更友好(即使切换到ES Module,在须要模块状态隔离的场景,仍然要用require,因此切换意义不大)

至于Haste,则是React团队自定义的模块处理工具,用来解决长相对路径的问题,例如:

// ref: react-15.5.4
var ReactCurrentOwner = require('ReactCurrentOwner');
var warning = require('warning');
var canDefineProperty = require('canDefineProperty');
var hasOwnProperty = Object.prototype.hasOwnProperty;
var REACT_ELEMENT_TYPE = require('ReactElementSymbol');

Haste模块机制下模块引用不须要给出明确的相对路径,而是经过项目级惟一的模块名来自动查找,例如:

// 声明
/**
 * @providesModule ReactClass
 */

// 引用
var ReactClass = require('ReactClass');

从表面上解决了长路径引用的问题(并无解决项目结构深层嵌套的根本问题),使用非标准模块机制有几个典型的坏处:

与标准不和,接入标准生态中的工具时会面临适配问题

源码难读,不容易弄明白模块依赖关系

React 16去掉了大部分自定义的模块机制(ReactNative里还有一小部分),采用Node标准的相对路径引用,长路径的问题经过重构项目结构来完全解决,采用扁平化目录结构(同package下最深2级引用,跨package的经Yarn处理以顶层绝对路径引用)

Flow + ES Lint
Flow负责检查类型错误,尽早发现类型不匹配的潜在问题,例如:

export type ReactElement = {
  $$typeof: any,
  type: any,
  key: any,
  ref: any,
  props: any,
  _owner: any, // ReactInstance or ReactFiber

  // __DEV__
  _store: {
    validated: boolean,
  },
  _self: React$Element<any>,
  _shadowChildren: any,
  _source: Source,
};

除了静态类型声明及检查外,Flow最大的特色是对React组件及JSX的深度支持:

type Props = {
  foo: number,
};
type State = {
  bar: number,
};
class MyComponent extends React.Component<Props, State> {
  state = {
    bar: 42,
  };

  render() {
    return this.props.foo + this.state.bar;
  }
}

P.S.关于Flow的React支持的更多信息,请查看Even Better Support for React in Flow

另外还有导出类型检查的Flow“魔法”,用来校验mock模块的导出类型是否与源模块一致:

type Check<_X, Y: _X, X: Y = _X> = null;
(null: Check<FeatureFlagsShimType, FeatureFlagsType>);
ES Lint负责检查语法错误及约定编码风格错误,例如:

rules: {
  'no-unused-expressions': ERROR,
  'no-unused-vars': [ERROR, {args: 'none'}],
  // React & JSX
  // Our transforms set this automatically
  'react/jsx-boolean-value': [ERROR, 'always'],
  'react/jsx-no-undef': ERROR,
}

Prettier
Prettier用来自动格式化代码,几种用途:

旧代码格式化成统一风格

提交以前对有改动的部分进行格式化

配合持续集成,保证PR代码风格彻底一致(不然build失败,并输出风格存在差别的部分)

集成到IDE,平常没事格式化一发

对构建结果进行格式化,一方面提高dev bundle可读性,另外还有助于发现prod bundle中的冗余代码

统一的代码风格固然有利于协做,另外,对于开源项目,常常面临风格各异的PR,把严格的格式化检查做为持续集成的一个强制环节可以完全解决代码风格差别的问题,有助于简化开源工做

P.S.整个项目强制统一格式化彷佛有些极端,是个大胆的尝试,但听说效果还不错:

Our experience with Prettier has been fantastic, and we recommend it to any team that writes JavaScript.

Yarn workspace
Yarn的workspace特性用来解决monorepo的package依赖(做用相似于lerna bootstrap),经过在node_modules下创建软连接“骗过”Node模块机制

Yarn Workspaces is a feature that allows users to install dependencies from multiple package.json files in subfolders of a single root package.json file, all in one go.

经过package.json/workspaces配置Yarn workspaces:

// ref: react-16.2.0/package.json
"workspaces": [
  "packages/*"
],

注意:Yarn的实际处理与Lerna相似,都经过软连接来实现,只是在包管理器这一层提供monorepo package支持更合理一些,具体缘由见Workspaces in Yarn | Yarn Blog

而后yarn install以后就能够愉快地跨package引用了:

import {enableUserTimingAPI} from 'shared/ReactFeatureFlags';
import getComponentName from 'shared/getComponentName';
import invariant from 'fbjs/lib/invariant';
import warning from 'fbjs/lib/warning';

P.S.另外,Yarn与Lerna能够无缝结合,经过useWorkspaces选项把依赖处理部分交由Yarn来作,详细见Integrating with Lerna

HUBOT
HUBOT是指GitHub机器人,一般用于:

接持续集成,PR触发构建/检查

管理Issue,关掉不活跃的讨论贴

主要围绕PR与Issue作一些自动化的事情,好比React团队计划(目前还没这么作)机器人回复PR对bundle size的影响,以此督促持续优化bundle size

目前每次构建把bundle size变化输出到文件,并交由Git追踪变化(提交上去),例如:

// ref: react-16.2.0/scripts/rollup/results.json
{
  "bundleSizes": {
    "react.development.js (UMD_DEV)": {
      "size": 54742,
      "gzip": 14879
    },
    "react.production.min.js (UMD_PROD)": {
      "size": 6617,
      "gzip": 2819
    }
  }
}

缺点可想而知,这个json文件常常冲突,要么须要浪费精力merge冲突,要么就懒得提交这个自动生成的麻烦文件,致使版本滞后,因此计划经过GitHub Bot把这个麻烦抽离出去

三.构建工具
bundle形式
以前提供两种bundle形式:

UMD单文件,用做外部依赖

CJS散文件,用于支持自行构建bundle(把React做为源码依赖)

存在一些问题:

自行构建的版本不一致:不一样的build环境/配置构建出的bundle都不同

bundle性能有优化空间:用打包App的方式构建类库不太合适,性能上有提高余地

不利于实验性优化尝试:没法对散文件模块应用打包、压缩等优化手段

React 16调整了bundle形式:

再也不提供CJS散文件,从npm拿到的就是构建好的,统一优化过的bundle

提供UMD单文件与CJS单文件,分别用于Web环境与Node环境(***)

以不可再分的类库姿态,把优化环节都收进来,摆脱bundle形式带来的限制

Gulp/Grunt+Browserify -> Rollup

以前的构建系统是基于Gulp/Grunt+Browserify手搓的一套工具,后来在扩展方面受限于工具,例如:

Node环境下性能很差:频繁的process.env.NODE_ENV访问拖慢了***性能,但又没办法从类库角度解决,由于Uglify依靠这个去除无用代码

因此React ***性能最佳实践通常都有一条“从新打包React,在构建时去掉process.env.NODE_ENV”(固然,React 16不须要再这样作了,缘由见上面提到的bundle形式变化)

丢弃了过于复杂(overly-complicated)的自定义构建工具,改用更合适的Rollup:

It solves one problem well: how to combine multiple modules into a flat file with minimal junk code in between.

P.S.不管Haste -> ES Module仍是Gulp/Grunt+Browserify -> Rollup的切换都是从非标准的定制化方案切换到标准的开放的方案,应该在“手搓”方面吸收教训,为何业界规范的东西在咱们的场景不适用,非要本身造吗?

mock module
构建时可能面临动态依赖的场景:不一样的bundle依赖功能类似但实现存在差别的module,例如ReactNative的错误提醒机制是显示个红框,而Web环境就是输出到Console

通常解法有2种:

运行时动态依赖(注入):把两份都放进bundle,运行时根据配置或环境选择

构建时处理依赖:多构建几份,不一样的bundle含有各自须要的依赖模块

显然构建时处理更干净一些,即mock module,开发中不用关心这种差别,构建时根据环境自动选择具体依赖,经过手写简单的Rollup插件来实现:动态依赖配置 + 构建时依赖替换

Closure Compiler
google/closure-compiler是个很是强大的minifier,有3种优化模式(compilation_level):

WHITESPACE_ONLY:去除注释,多余的标点符号和空白字符,逻辑功能上与源码彻底等价

SIMPLE_OPTIMIZATIONS:默认模式,在WHITESPACE_ONLY的基础上进一步缩短变量名(局部变量和函数形参),逻辑功能基本等价,特殊状况(如eval('localVar')按名访问局部变量和解析fn.toString())除外

ADVANCED_OPTIMIZATIONS:在SIMPLE_OPTIMIZATIONS的基础上进行更强力的重命名(全局变量名,函数名和属性),去除无用代码(走不到的,用不着的),内联方法调用和常量(划算的话,把函数调用换成函数体内容,常量换成其值)

P.S.关于compilation_level的详细信息见Closure Compiler Compilation Levels

ADVANCED模式过于强大:

// 输入
function hello(name) {
  alert('Hello, ' + name);
}
hello('New user');

// 输出
alert("Hello, New user");

P.S.能够在Closure Compiler Service在线试玩

迁移切换有必定风险,所以React用的仍是SIMPLE模式,但后续可能有计划开启ADVANCED模式,充分利用Closure Compiler优化bundle size

Error Code System
In order to make debugging in production easier, we’re introducing an Error Code System in 15.2.0. We developed a gulp script that collects all of our invariant error messages and folds them to a JSON file, and at build-time Babel uses the JSON to rewrite our invariant calls in production to reference the corresponding error IDs.

简言之,在prod bundle中把详细的报错信息替换成对应错误码,生产环境捕获到运行时错误就把错误码与上下文信息抛出来,再丢给错误码转换服务还原出完整错误信息。这样既保证了prod bundle尽可能干净,还保留了与开发环境同样的详细报错能力

例如生产环境下的非法React Element报错:

Minified React error #109; visit https://reactjs.org/docs/error-decoder.html?invariant=109&args[]=Foo for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

颇有意思的技巧,确实在提高开发体验上花了很多心思

envification
所谓envification就是分环境build,例如:

// ref: react-16.2.0/build/packages/react/index.js
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

经常使用手段,构建时把process.env.NODE_ENV替换成目标环境对应的字符串常量,在后续构建过程当中(打包工具/压缩工具)会把多余代码剔除掉

除了package入口文件外,还在里面作了一样的判断做为双保险:

// ref: react-16.2.0/build/packages/react/cjs/react.development.js
if (process.env.NODE_ENV !== "production") {
  (function() {
    module.exports = react;
  })();
}

此外,还担忧开发者误用dev bundle上线,因此在React DevTools也加了一点提醒:

This page is using the development build of React.
相关文章
相关标签/搜索