The tooling system behind React

1. Overview of the
React toolchain tag cloud:


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

PS with [x] indicates that it was used before, but recently (React 16) is not used

The simple classification is as follows:


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

Organize the source code according to the ES module mechanism, supplemented by type checking and Lint/formatting tools, use Yarn to process module dependencies, HUBOT check PR; Rollup + Closure Compiler construction, use Error Code mechanism to implement error tracking in the production environment, DevTools side assists in bundle check; Jest drives the single test, and also confirms that the build result is clean enough by formatting the bundle; finally, the new package is released through npm

The whole process is not very complicated, but some details are considered in depth, such as Error Code System, double insurance envification (dev/prod environment distinction), and tooling of the release process

2. Development Tools


CommonJS Module + Haste -> ES Module

Versions before React 15 are defined with CommonJS modules, for example:


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

There are several reasons for switching to ES Module:

Helps to detect module import/export problems early

CommonJS Module is easy to require a non-existent method, and the problem cannot be found until the call is reported. The static module mechanism of ES Module requires that import and export must match by name, otherwise an error will be reported when compiling and building

Advantages of bundle size

ES Module can make the bundle cleaner through tree shaking. The fundamental reason is that module.exports is object-level export, and export supports more fine-grained atomic-level export. On the other hand, the introduction by name enables tools such as rollup to flatten the modules, and the compression tool can perform more violent variable name confusion on this basis, further reducing the bundle size

Only the source code is switched to ES Module, and the single test case is not switched, because CommonJS Module is more friendly to some features of Jest (such as resetModules) (even if you switch to ES Module, require module status isolation is still required, so Switching is of little significance)

As for Haste, it is a module processing tool customized by the React team to solve the problem of long relative paths, such as:


// 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');

Module references under the Haste module mechanism do not need to give a clear relative path, but are automatically searched through the project-level unique module name, for example:


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

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

On the surface, it solves the problem of long path references (and does not solve the fundamental problem of deep nesting of project structure). There are several typical disadvantages of using non-standard module mechanism:

Disagreement with the standard, you will face adaptation problems when accessing tools in the standard ecology

The source code is difficult to read, and it is not easy to understand the module dependencies

React 16 removes most of the custom module mechanism (there is a small part in ReactNative), and uses Node standard relative path references. The problem of long paths is completely solved by refactoring the project structure, and uses a flat directory structure (under the same package) The deepest level 2 references, cross-packages are referenced by the top-level absolute path after Yarn processing)

Flow + ES Lint
Flow is responsible for checking type errors and detecting potential problems of type mismatch as early as possible, such as:


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,
};

In addition to static type declaration and checking, the biggest feature of Flow is its deep support for React components and 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;
  }
}

PS For more information about Flow’s React support, please see Even Better Support for React in Flow

There is also the Flow "magic" of export type checking, which is used to verify whether the export type of the mock module is consistent with the source module:


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 is used to format code automatically, for several purposes:

Format the old code into a unified style

Format the changed parts before submitting

Cooperate with continuous integration to ensure that the PR code style is completely consistent (otherwise the build will fail and the parts with different styles will be output)

Integrate into IDE, format once every day

Formatting the build results, on the one hand, improves the readability of the dev bundle, and also helps to find redundant code in the prod bundle

A unified code style is of course conducive to collaboration. In addition, for open source projects, you often face different styles of PR. Regarding strict formatting checks as a mandatory link in continuous integration can completely solve the problem of code style differences and help simplify open source jobs

PS The mandatory unified formatting of the entire project seems a bit extreme, it is a bold attempt, but it is said that the effect is not bad:


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

Yarn workspace
Yarn's workspace feature is used to solve the package dependency of monorepo (similar to lerna bootstrap), and "cheat" the Node module mechanism by establishing a soft link under node_modules


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.

Configure Yarn workspaces through package.json/workspaces:


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

Note: The actual processing of Yarn is similar to that of Lerna, which is implemented through soft links, but it is more reasonable to provide monorepo package support at the package manager level. For specific reasons, see Workspaces in Yarn | Yarn Blog

Then after yarn install, you can happily refer to it across packages:


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

PS In addition, Yarn and Lerna can be seamlessly combined. The dependency processing part is handed over to Yarn through the useWorkspaces option. For details, see Integrating with Lerna

HUBOT
HUBOT refers to the GitHub robot, usually used to:

Connect with continuous integration, PR triggers build/check

Manage Issues and turn off inactive discussion posts

Mainly do some automated things around PR and Issue, such as the React team plan (not yet done) robots to respond to the impact of PR on the bundle size, in order to urge continuous optimization of the bundle size

Currently, the bundle size changes are output to a file for each build, and the changes are tracked by Git (submitted), for example:


// 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
    }
  }
}

The shortcomings can be imagined. This json file often conflicts. Either you need to waste energy to merge conflicts, or you don’t bother to submit this automatically generated troublesome file, causing the version to lag, so we plan to take this trouble out through GitHub Bot.

Three. Before the build tool
bundle form
, two bundle forms were provided:

UMD single file, used as external dependency

CJS bulk file, used to support self-build bundle (using React as source code dependency)

There are some problems:

The self-built version is inconsistent: the bundles built by different build environments/configurations are different

There is room for optimization of bundle performance: it is not appropriate to build a class library by packaging App, and there is room for improvement in performance

Not conducive to experimental optimization attempts: optimization methods such as packaging and compression cannot be applied to bulk file modules

React 16 adjusted the bundle form:

CJS bulk files are no longer provided, what you get from npm is the built, unified and optimized bundle

Provide UMD single file and CJS single file, respectively for Web environment and Node environment (***)

In an inseparable class library posture, all the optimization links are taken in to get rid of the restrictions brought by the bundle form.

Gulp/Grunt+Browserify -> Rollup's

previous build system is based on a set of tools by Gulp/Grunt+Browserify. Later Limited to tools in terms of expansion, such as:

Poor performance in the Node environment: frequent process.env.NODE_ENV access slows down the *** performance, but there is no way to solve it from the perspective of the class library, because Uglify relies on this to remove useless code

Therefore, React *** performance best practices generally have a "repackage React, remove process.env.NODE_ENV when building" (Of course, React 16 does not need to do this anymore, the reason is the change in the bundle form mentioned above)

Overly-complicated custom build tools are discarded, and a more suitable Rollup is used:


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

PS Whether Haste -> ES Module or Gulp/Grunt+Browserify -> Rollup is switched from a non-standard customized solution to a standard open solution, we should learn from the "hand rubbing" aspect, why the industry standard things Doesn't apply in our scenario, do we have to make it ourselves?

Mock module
construction may face dynamic dependency scenarios: different bundles rely on modules with similar functions but different implementations. For example, the error notification mechanism of ReactNative is to display a red box, and the web environment is output to the Console

There are two general solutions:

Dynamic dependency (injection) at runtime: Put both of them into the bundle, and choose according to configuration or environment at runtime

Handle dependencies during build: build a few more copies, different bundles contain their own required dependent modules

Obviously, the process is cleaner when building, that is, mock module. You don’t need to care about this difference during development. Specific dependencies are automatically selected according to the environment when building. This is achieved by handwriting simple Rollup plugins: dynamic dependency configuration + dependency replacement during build

Closure Compiler
google/closure-compiler is a very powerful minifier, with 3 optimization modes (compilation_level):

WHITESPACE_ONLY: Remove comments, extra punctuation and blank characters, logically functionally equivalent to the source code

SIMPLE_OPTIMIZATIONS: The default mode. On the basis of WHITESPACE_ONLY, the variable names (local variables and function parameters) are further shortened, and the logic functions are basically equivalent. Special cases (such as eval('localVar')) access local variables by name and parse fn.toString( ))except

ADVANCED_OPTIMIZATIONS: On the basis of SIMPLE_OPTIMIZATIONS, perform more powerful renaming (global variable names, function names and attributes), remove useless code (unreachable, unnecessary), inline method calls and constants (if cost-effective, call the function Replace the content of the function body, and replace the constant with its value)

PS For more information about compilation_level, see Closure Compiler Compilation Levels

The ADVANCED mode is too powerful:


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

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

PS can be tried online at Closure Compiler Service

Migration and switching have certain risks, so React still uses SIMPLE mode, but there may be plans to open ADVANCED mode in the future to make full use of Closure Compiler to optimize 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.

In short, the detailed error information is replaced with the corresponding error code in the prod bundle. The production environment catches the runtime error and throws the error code and context information, and then throws it to the error code conversion service to restore the complete error message. This not only ensures that the prod bundle is as clean as possible, but also retains the same detailed error reporting capabilities as the development environment

For example, the illegal React Element error in the production environment:


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.

Very interesting technique, really spent a lot of thought on improving the development experience

The
so-called envification is to build by environment, for example:


// 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');
}

Commonly used method, replace process.env.NODE_ENV with the string constant corresponding to the target environment during the construction, and the redundant code will be removed in the subsequent construction process (packaging tool/compression tool)

In addition to the package entry file, the same judgment is also made inside as a double insurance:


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

In addition, I am worried that developers may misuse the dev bundle to go online, so I also added a reminder to React DevTools:


This page is using the development build of React. 

Guess you like

Origin blog.51cto.com/15080030/2592712