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.
相關文章
相關標籤/搜索