The original address

This article covers the React-Native and Metro versions

Let’s start with a wave of sample code for this article: simple, a hello, world

// App.js
import React from "react";
import { StyleSheet, Text, View } from "react-native";

export default class App extends React.Component {
  render() {
    return (
      <React.Fragment>
        <View style={styles.body}>
          <Text style={styles.text}>Hello world</Text>
        </View>
      </React.Fragment>); }}const styles = StyleSheet.create({
  body: {
    backgroundColor: "white".flex: 1.justifyContent: "center".alignItems: "center",},text: {
    textAlign: "center".color: "red",}});Copy the code

One, foreword

As we all know, React-Native requires bundle loading for Android and ios. React-native bundle –entry-file index.js –bundle-output./bundle/ios.bundle –platform ios –assets-dest . / bundle – dev false; After running the above command, Rn will default to using Metro as the packaging tool to generate bundles.

The generated bundles are roughly divided into four layers:

  • Var declaration layer: information about the current operating environment, the start time of the bundle, and the process;
  • Polyfill layer: ! (function(r){}), defines rightDefine (__d),Require (__r),Clear (__c)Support, and load logic for modules (react-native and dependences);
  • Module definition layer: code block defined by __d, including RN framework source JS part, custom JS code part, image resource information, for require to introduce use
  • Require layer: code block defined by r, find code block defined by D and execute

The format is as follows:

// var declaration layer

var __BUNDLE_START_TIME__=this.nativePerformanceNow? nativePerformanceNow():Date.now(),__DEV__=false,process=this.process||{}; process.env=process.env||{}; process.env.NODE_ENV=process.env.NODE_ENV||"production";

/ / polyfill layer! (function(r){"use strict"; r.__r=o,r.__d=function(r,i,n){if(null! =e[i])return;var o={dependencyMap:n,factory:r,hasError:!1.importedAll:t,importedDefault:t,isInitialized:!1.publicModule: {exports: {}}}; e[i]=o} ...// Model definition layer
__d(function(g,r,i,a,m,e,d){var n=r(d[0]),t=r(d[1]),o=n(r(d[2])),u=r(d[3]); t.AppRegistry.registerComponent(u.name,function(){return o.default})},0[1.2.402.403]); . __d(function(a,e,t,i,R,S,c){R.exports={name:"ReactNativeSSR".displayName:"ReactNativeSSR"}},403[]);/ / the require layer
__r(93);
__r(0);


Copy the code

Do you have any questions after reading the above code?

  1. When was the code for the VAR definition layer and polyfill generated?

  2. We know that _d() takes three arguments: the corresponding factory function, the current moduleId, and the module dependency

    • metroWhat is used to do dependency analysis for the entire project?
    • moduleIdHow do you generate it?
  3. How does Metro package?

We probably don’t care about the whole RN packaging logic in day-to-day development; Now let me take you into the world of RN packaging!

Second, Metro packaging process

Through the source code and Metro official website, we know that the whole process of Metro packaging is roughly divided into:

  • Command Parameter parsing
  • Metro package service started
  • Package js and resource files
    • Parse, transform, and generate
  • Stopping the Packaging Service

1. Parses command parameters

First, let’s look at the implementation of the React-native bundle and how to parse its parameters. Since the bundle is a subcommand of the React-Native package, we can start with the React-Native package. The file path is as follows

// node_modules/react-native/local-cli/cli.js
// react-native command entry

var cli = require('@react-native-community/cli');
if (require.main === module) {
  cli.run();
}


// node_modules/react-native/node_modules/@react-native-community/cli/build/index.js

run() -> setupAndRun() -> var _commands = require("./commands");

/ / node_modules/react - native/node_modules / @ the react - native - community/cli/build/commands/index, registered in the js react - native all commands

var _start = _interopRequireDefault(require("./start/start"));

var _bundle = _interopRequireDefault(require("./bundle/bundle"));

var _ramBundle = _interopRequireDefault(require("./bundle/ramBundle"));

var _link = _interopRequireDefault(require("./link/link"));

var _unlink = _interopRequireDefault(require("./link/unlink"));

var _install = _interopRequireDefault(require("./install/install"));

var _uninstall = _interopRequireDefault(require("./install/uninstall"));

var _upgrade = _interopRequireDefault(require("./upgrade/upgrade"));

var _info = _interopRequireDefault(require("./info/info"));

var _config = _interopRequireDefault(require("./config/config"));

var _init = _interopRequireDefault(require("./init"));

var _doctor = _interopRequireDefault(require("./doctor"));

Copy the code

Since this paper mainly analyzes the React-Native packaging process, So you can look at the react – native/node_modules / @ the react – native – community/cli/build/commands/bundle/bundle. Js.

The bundle command is primarily registered in the bundle.js file, but the implementation uses buildbundle.js.

// node_modules/react-native/node_modules/@react-native-community/cli/build/commands/bundle/bundle.js

var _buildBundle = _interopRequireDefault(require("./buildBundle"));

var _bundleCommandLineArgs = _interopRequireDefault(
  require("./bundleCommandLineArgs"));function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}

function bundleWithOutput(_, config, args, output) {
  // Bundle implementation
  return (0, _buildBundle.default)(args, config, output);
}

var _default = {
  name: "bundle".description: "builds the javascript bundle for offline use".func: bundleWithOutput,
  options: _bundleCommandLineArgs.default,
  // Used by `ramBundle.js`
  withOutput: bundleWithOutput,
};
exports.default = _default;
const withOutput = bundleWithOutput;
exports.withOutput = withOutput;
Copy the code

2. The Metro Server is started

In node_modules/react – native/node_modules / @ the react – native – community/cli/build/commands/bundle/buildBundle js file exported by default The buildBundle method is the entry point to the entire React-Native bundle execution. The following things are done in the entrance:

  • Merge metro default configuration and custom configuration, and set maxWorkers,resetCache

  • Build requestOptions based on the parsed parameters and pass them to the package function

  • Instantiate Metro Server

  • Start metro build bundle

  • Processing resource files, parsing

  • Close the Metro Server

// node_modules/react-native/node_modules/@react-native-community/cli/build/commands/bundle/buildBundle.js
// Metro packaging is the core of Metro
function _Server() {
  const data = _interopRequireDefault(require("metro/src/Server"));

  _Server = function() {
    return data;
  };

  return data;
}

function _bundle() {
  const data = _interopRequireDefault(
    require("metro/src/shared/output/bundle")); _bundle =function() {
    return data;
  };

  return data;
}

// Save the resource file
var _saveAssets = _interopRequireDefault(require("./saveAssets"));
// Provides metro's default configuration
var _loadMetroConfig = _interopRequireDefault(
  require(".. /.. /tools/loadMetroConfig"));async function buildBundle(args, ctx, output = _bundle().default) {
  // Merge metro default configuration and custom configuration, and set maxWorkers,resetCache
  const config = await (0, _loadMetroConfig.default)(ctx, {
    maxWorkers: args.maxWorkers,
    resetCache: args.resetCache,
    config: args.config,
  });

  // ...

  process.env.NODE_ENV = args.dev ? "development" : "production";
  // Build the sourceMapUrl with the command line input parameter --sourcemap-output
  let sourceMapUrl = args.sourcemapOutput;

  if(sourceMapUrl && ! args.sourcemapUseAbsolutePath) { sourceMapUrl = _path().default.basename(sourceMapUrl); }Build requestOptions based on the parsed parameters and pass them to the package function
  const requestOpts = {
    entryFile: args.entryFile,
    sourceMapUrl,
    dev: args.dev,
    minify: args.minify ! = =undefined? args.minify : ! args.dev,platform: args.platform,
  };
  // Instantiate metro service
  const server = new (_Server()).default(config);

  try {
    // Start packing, what? What does the author say about Server packaging? Why output? A: It will be explained below
    const bundle = await output.build(server, requestOpts);
    // Save the generated bundle to the corresponding directory
    await output.save(bundle, args, _cliTools().logger.info); // Save the assets of the bundle
    // Process the resource file, parse it, and next save it in the location specified by --assets-dest
    const outputAssets = awaitserver.getAssets({ ... _Server().default.DEFAULT_BUNDLE_OPTIONS, ... requestOpts,bundleType: "todo"});// When we're done saving bundle output and the assets, we're done.
    // Save the resource file to the specified directory
    return await (0, _saveAssets.default)(
      outputAssets,
      args.platform,
      args.assetsDest
    );
  } finally {
    // Stop metro package serviceserver.end(); }}var _default = buildBundle;
exports.default = _default;
Copy the code

Build (server, requestOpts). Output is an outputBundle. Metro JS is a Metro JS package. node_modules/metro/src/shared/output/bundle.js

// node_modules/metro/src/shared/output/bundle.js

function buildBundle(packagerClient, requestOptions) {
  return packagerClient.build(
    _objectSpread({}, Server.DEFAULT_BUNDLE_OPTIONS, requestOptions, {
      bundleType: "bundle",})); }exports.build = buildBundle;
exports.save = saveBundleAndMap;
exports.formatName = "bundle";
Copy the code

Build (server, requestOpts) is the package passed in. And the packagerClient is the Server we just passed in. Server is the next step in analyzing the packaging process. Its source location is: the node_modules/metro/SRC/Server js

Metro Build Bundle: Process entry

Through the above analysis, we have already known the whole react – native bundle packaging service startup in node_modules/metro/SRC/Server in the build method, js:

class Server {
  // Build the function to initialize the properties
  constructor(config, options) {
    var _this = this;
    this._config = config;
    this._createModuleId = config.serializer.createModuleIdFactory();

    this._bundler = new IncrementalBundler(config, {
      watch: options ? options.watch : undefined});this._nextBundleBuildID = 1;
  }

  build(options) {
    var _this2 = this;

    return _asyncToGenerator(function* () {
      // Split the parameters passed in according to modules for better management; The split format is as follows:
      / / {
      // entryFile: options.entryFile,
      // transformOptions: {
      // customTransformOptions: options.customTransformOptions,
      // dev: options.dev,
      // hot: options.hot,
      // minify: options.minify,
      // platform: options.platform,
      // type: "module"
      / /},
      // serializerOptions: {
      // excludeSource: options.excludeSource,
      // inlineSourceMap: options.inlineSourceMap,
      // modulesOnly: options.modulesOnly,
      // runModule: options.runModule,
      // sourceMapUrl: options.sourceMapUrl,
      // sourceUrl: options.sourceUrl
      / /},
      // graphOptions: {
      // shallow: options.shallow
      / /},
      // onProgress: options.onProgress
      / /}

      const _splitBundleOptions = splitBundleOptions(options),
        entryFile = _splitBundleOptions.entryFile,
        graphOptions = _splitBundleOptions.graphOptions,
        onProgress = _splitBundleOptions.onProgress,
        serializerOptions = _splitBundleOptions.serializerOptions,
        transformOptions = _splitBundleOptions.transformOptions;

      // Metro package core: Resolution and Transformation
      const _ref13 = yield _this2._bundler.buildGraph(
          entryFile,
          transformOptions,
          {
            onProgress,
            shallow: graphOptions.shallow,
          }
        ),
        prepend = _ref13.prepend,
        graph = _ref13.graph;

      // Get the build entry file path
      const entryPoint = path.resolve(_this2._config.projectRoot, entryFile);
      // Initialize the build parameters, which are derived from: command line && custom Metro configuration metro.config.js && default Metro configuration
      const bundleOptions = {
        asyncRequireModulePath:
          _this2._config.transformer.asyncRequireModulePath,
        processModuleFilter: _this2._config.serializer.processModuleFilter,
        createModuleId: _this2._createModuleId, // The custom/default createModuleIdFactory generates ids for each module; The default rules for details, please see: node_modules/metro/SRC/lib/createModuleIdFactory. Js
        getRunModuleStatement: _this2._config.serializer.getRunModuleStatement, // Sign the method
        // The default value is getRunModuleStatement: moduleId => '__r(${json.stringify (moduleId)}); `,
        / / see: node_modules/metro - config/SRC/defaults/index. Js
        dev: transformOptions.dev,
        projectRoot: _this2._config.projectRoot,
        modulesOnly: serializerOptions.modulesOnly,
        runBeforeMainModule: _this2._config.serializer.getModulesRunBeforeMainModule(
          path.relative(_this2._config.projectRoot, entryPoint)
        ), / / the specified running in front of the main module module, default: getModulesRunBeforeMainModule: () = > []
        / / see: node_modules/metro - config/SRC/defaults/index. Js

        runModule: serializerOptions.runModule,
        sourceMapUrl: serializerOptions.sourceMapUrl,
        sourceUrl: serializerOptions.sourceUrl,
        inlineSourceMap: serializerOptions.inlineSourceMap,
      };
      let bundleCode = null;
      let bundleMap = null;

      // Whether to use custom build, if so, call custom generated function to get the final code
      if (_this2._config.serializer.customSerializer) {
        const bundle = _this2._config.serializer.customSerializer(
          entryPoint,
          prepend,
          graph,
          bundleOptions
        );

        if (typeof bundle === "string") {
          bundleCode = bundle;
        } else{ bundleCode = bundle.code; bundleMap = bundle.map; }}else {
        // Here I break it down into two steps, which are easier to analyze

        // Generate the following formatted data after parsing and converting the data
        / / {
        // pre: string, // var definition and poyfill
        // post: string, // require some code
        // modules: [[number, string]], // The first argument is number, the second argument is the specific code
        // }
        var base = baseJSBundle(entryPoint, prepend, graph, bundleOptions);
        // Sort the JS modules and concatenate strings to generate the final code
        bundleCode = bundleToString(base).code;
      }
      //
      if(! bundleMap) { bundleMap = sourceMapString( _toConsumableArray(prepend).concat( _toConsumableArray(_this2._getSortedModules(graph)) ), {excludeSource: serializerOptions.excludeSource,
            processModuleFilter: _this2._config.serializer.processModuleFilter,
          }
        );
      }

      return {
        code: bundleCode,
        map: bundleMap,
      };
    })();
  }
}
Copy the code

In the build function, buildGraph is performed first, and the initialization of this._bundler occurs in the Server’s constructor.

this._bundler = new IncrementalBundler(config, {
  watch: options ? options.watch : undefined});Copy the code

_Bundler here is an instance of IncrementalBundler, whose buildGraph function completes the first two steps of the packaging process, Resolution and Transformation. Let’s take a closer look at the Metro parsing, transformation process.

Metro builds bundles: Parse and transform

Metro uses IncrementalBundler to parse and convert JS code. Metro uses IncrementalBundler to parse and convert JS code.

  • The dependency graph and Babel transformed code of all related dependency files with entry file as entry are returned.
  • The dependency graph of all related dependency files in var definition part and Polyfill part and the code transformed by Babel are returned.

The overall process is shown in the figure:

Through the above process, we can summarize the following points:

  1. The dependency analysis and Babel transformation of the whole Metro was mainly carried out through JestHasteMap.
  2. During dependency analysis, Metro listens for file changes in the current directory and generates a final dependency map with minimal changes.
  3. JestHasteMap is used for both import file resolution and dependency resolution for Polyfill files;

Below, we analyze the specific process as follows:

// node_modules/metro/src/IncrementalBundler.js


buildGraph(entryFile, transformOptions) {
    var _this2 = this;

    let otherOptions =
      arguments.length > 2 && arguments[2]! = =undefined
        ? arguments[2]
        : {
            onProgress: null.shallow: false
          };
    return _asyncToGenerator(function* () {
     // The core is built in buildGraphForEntries. The import file is used for dependency parsing to obtain the bundle require part and the module definition part. The generated format is
    / / {
    // dependencies: new Map(),
    // entryPoints,
    // importBundleNames: new Set()
    / /}
      const graph = yield _this2.buildGraphForEntries(
        [entryFile],
        transformOptions,
        otherOptions
      );
      const transformOptionsWithoutType = {
        customTransformOptions: transformOptions.customTransformOptions,
        dev: transformOptions.dev,
        experimentalImportSupport: transformOptions.experimentalImportSupport,
        hot: transformOptions.hot,
        minify: transformOptions.minify,
        unstable_disableES6Transforms:
          transformOptions.unstable_disableES6Transforms,
        platform: transformOptions.platform
      };
    // The var declaration and polyfill in front of the bundle are generated in the following format:
        / / /
        / / {
        // inverseDependencies: Set(0) {},
        // path: '/Users/mrgaogang/Desktop/react-native-ssr/ReactNativeSSR/node_modules/react-native/Libraries/polyfills/Object.es7.js',
        // dependencies: Map(0) {},
        // getSource: [Function: getSource],
        // output: [ [Object] ]
        / /}
        // ]
      const prepend = yield getPrependedScripts(
        _this2._config,
        transformOptionsWithoutType,
        _this2._bundler,
        _this2._deltaBundler
      );
      return{ prepend, graph }; }) (); }Copy the code

Require and module define partial resolution and dependency generation

BuildGraph is generated from _deltaBundler. BuildGraph in buildGraphForEntries,

// node_modules/metro/src/IncrementalBundler.js

  buildGraphForEntries(entryFiles, transformOptions) {
    return _asyncToGenerator(function* () {

      const absoluteEntryFiles = entryFiles.map(entryFile= >
        path.resolve(_this._config.projectRoot, entryFile)
      );
      / / call DeltaBundler buildGraph
      const graph = yield _this._deltaBundler.buildGraph(absoluteEntryFiles, {
       / /... Some other parameters
      });
        / /...
      returngraph; }) ();// node_modules/metro/src/DeltaBundler.js
  buildGraph(entryPoints, options) {
    var _this = this;

    return _asyncToGenerator(function* () {
        / / use node_modules/metro/SRC/Bundler. Js access modules depend on the map
      const depGraph = yield _this._bundler.getDependencyGraph();
      // Listen for file changes and update dependencies between files if there are changes
      const deltaCalculator = new DeltaCalculator(
        entryPoints,
        depGraph,
        options
      );
      // Calculate the change between modules, including module addition, deletion and modification, if there is a change will be updated in the first time
      yield deltaCalculator.getDelta({
        reset: true.shallow: options.shallow
      });
      // Based on the returned dependency graph and the result of file change detection, return the following format of module dependency information. (Full formatting will be shown later)
    / / {
    // dependencies: new Map(),
    // entryPoints,
    // importBundleNames: new Set()
    / /}
      const graph = deltaCalculator.getGraph();

      _this._deltaCalculators.set(graph, deltaCalculator);

      returngraph; }) (); }// node_modules/metro/src/Bundler.js
// Dependency graph analysis
class Bundler {
  constructor(config, options) {
    // Bundler uses DependencyGraph to generate a DependencyGraph
    this._depGraphPromise = DependencyGraph.load(config, options);
    this._depGraphPromise
      .then(dependencyGraph= > {
        this._transformer = new Transformer(
          config,
          dependencyGraph.getSha1.bind(dependencyGraph)
        );
      })
      .catch(error= > {
        console.error("Failed to construct transformer: ", error);
      });
  }

  getDependencyGraph() {
    return this._depGraphPromise; }}Dependencygraph. load Uses JestHasteMap for dependency analysis
// node_modules/metro/src/node-haste/DependencyGraph.js

static _createHaste(config, watch) {
    return new JestHasteMap({
      cacheDirectory: config.hasteMapCacheDirectory,
      computeDependencies: false.computeSha1: true.extensions: config.resolver.sourceExts.concat(config.resolver.assetExts),
      forceNodeFilesystemAPI: !config.resolver.useWatchman,
      hasteImplModulePath: config.resolver.hasteImplModulePath,
      ignorePattern: config.resolver.blacklistRE || / ^ /,
      mapper: config.resolver.virtualMapper,
      maxWorkers: config.maxWorkers,
      mocksPattern: "".name: "metro-" + JEST_HASTE_MAP_CACHE_BREAKER,
      platforms: config.resolver.platforms,
      retainAllFiles: true.resetCache: config.resetCache,
      rootDir: config.projectRoot,
      roots: config.watchFolders,
      throwOnModuleCollision: true.useWatchman: config.resolver.useWatchman,
      watch: watch == null? ! ci.isCI : watch }); }static load(config, options) {
    return _asyncToGenerator(function* () {
      const haste = DependencyGraph._createHaste(
        config,
        options && options.watch
      );

      const _ref2 = yield haste.build(),
        hasteFS = _ref2.hasteFS,
        moduleMap = _ref2.moduleMap;

      return new DependencyGraph({
        haste,
        initialHasteFS: hasteFS,
        initialModuleMap: moduleMap, config }); }) (); }// JestHasteMap is a dependency management system for Node.js static resources. It provides the ability to resolve JavaScript module dependencies statically for node modules and Facebook's Haste module system.

// Because haste Map creation is synchronous and most tasks are BLOCKED by I/O, the computer's multi-cores are used for parallel operation.

Copy the code

After dependencygraph. load and DeltaCalculator, the generated DependencyGraph format is as follows:

{
  dependencies: Map(404) {
      // Dependencies for each module, etc
    '/Users/mrgaogang/Desktop/react-native-ssr/ReactNativeSSR/index.js'= > {inverseDependencies: Set(1) {
            '/Users/mrgaogang/Desktop/react-native-ssr/ReactNativeSSR/index.js'
            },
            path: '/Users/mrgaogang/Desktop/react-native-ssr/ReactNativeSSR/App.js'.// Module path
            dependencies: Map(8) { // Other modules on which this module depends
            },
            getSource: [Function: getSource],
            output: [{data: {
                code: ` `.// Package the code to change the module
                lineCount: 1.map: [].functionMap: {
                    names: [ '<global>'.'App'.'render'].mappings: 'AAA; eCW; ECC; GDQ; CDC'}},type: 'js/module' // Type, Metro determines whether it is a JS module by whether it is startWidth('js')}]}},entryPoints: [ / / the entry
    '/Users/mrgaogang/Desktop/react-native-ssr/ReactNativeSSR/index.js'].importBundleNames: Set(0){}}Copy the code

Var and polyfill are partially analyzed

We saw earlier that we get the var and Polyfill sections from getPrependedScripts in the IncrementalBundler.js buildGraph; Let’s take a look at getPrependedScripts:

// node_modules/metro/src/lib/getPreludeCode.js
function _getPrependedScripts() {
  _getPrependedScripts = _asyncToGenerator(function* (config, options, bundler, deltaBundler) {
    // Gets all polyfills, both default and custom polyfills
    // The default polyfill is: node_modules/react-native/node_modules/@react-native-community/cli/build/tools/loadMetroConfig.js GetDefaultConfig: the function of use node_modules/react - native/rn - get - polyfills. Js, i.e
    // module.exports = () => [
    // require.resolve('./Libraries/polyfills/console.js'),
    // require.resolve('./Libraries/polyfills/error-guard.js'),
    // require.resolve('./Libraries/polyfills/Object.es7.js'),
    // ];

    const polyfillModuleNames = config.serializer
      .getPolyfills({
        platform: options.platform,
      })
      .concat(config.serializer.polyfillModuleNames);

    const transformOptions = _objectSpread({}, options, {
      type: "script"});// Use deltabundler.buildGraph to analyze the following four files and custom Polyfill dependency graphs
    // metro/src/lib/polyfills/require.js
    // require.resolve('./Libraries/polyfills/console.js'),
    // require.resolve('./Libraries/polyfills/error-guard.js'),
    // require.resolve('./Libraries/polyfills/Object.es7.js'),
    const graph = yield deltaBundler.buildGraph(
      [defaults.moduleSystem].concat(_toConsumableArray(polyfillModuleNames)),
      {
        resolve: yield transformHelpers.getResolveDependencyFn(
          bundler,
          options.platform
        ),
        transform: yield transformHelpers.getTransformFn(
          [defaults.moduleSystem].concat(
            _toConsumableArray(polyfillModuleNames)
          ),
          bundler,
          deltaBundler,
          config,
          transformOptions
        ),
        onProgress: null.experimentalImportBundleSupport:
          config.transformer.experimentalImportBundleSupport,
        shallow: false});return [
      // Return the var definition section and the polyfill dependency graph after deltaBundler. BuildGraph analysis
      _getPrelude({
        dev: options.dev,
      }),
    ].concat(_toConsumableArray(graph.dependencies.values()));
  });
  return _getPrependedScripts.apply(this.arguments);
}

function _getPrelude(_ref) {
  let dev = _ref.dev;
  const code = getPreludeCode({
    isDev: dev,
  });
  const name = "__prelude__";
  return {
    dependencies: new Map(),
    getSource: () = > Buffer.from(code),
    inverseDependencies: new Set(),
    path: name,
    output: [{type: "js/script/virtual".data: {
          code,
          lineCount: countLines(code),
          map: [],},},],}; }// node_modules/metro/src/lib/getPreludeCode.js
// var defines part of the code
function getPreludeCode(_ref) {
  let extraVars = _ref.extraVars,
    isDev = _ref.isDev;
  const vars = [
    "__BUNDLE_START_TIME__=this.nativePerformanceNow? nativePerformanceNow():Date.now()".`__DEV__=The ${String(isDev)}`,
  ].concat(_toConsumableArray(formatExtraVars(extraVars)), [
    "process=this.process||{}",]);return `var ${vars.join(",")};${processEnv(
    isDev ? "development" : "production"
  )}`;
}
Copy the code

There is one section that the author does not cover in detail here, which is the detailed section of file dependency resolution using JestHasteMap. In the follow-up, the author will separate out an article to explain the reference.

Now that metro is done talking about entry files and polyfills and code generation, go back to the beginning of this chapter and you’ll see what’s happening. Metro’s parsing and transformation are covered, and the following sections describe how Metro generates the final bundle code from the transformed file dependency graph.

Metro build bundle: build

Going back to the initial Server startup code, we found that after buildGraph we got the code and dependencies for prepend: var and Polyfill, as well as the dependency and code for graph: entry file. Without providing custom generation, Metro uses baseJSBundle to transform the dependency map and code for each module through a series of operations to the final code using bundleToString.


      // Metro package core: Resolution and Transformation
      const _ref13 = yield _this2._bundler.buildGraph(
          entryFile,
          transformOptions,
          {
            onProgress,
            shallow: graphOptions.shallow,
          }
        ),
        prepend = _ref13.prepend,
        graph = _ref13.graph;
        / /...
        // Here I break it down into two steps, which are easier to analyze

        // Generate the following formatted data after parsing and converting the data
        / / {
        // pre: string, // var definition and poyfill
        // post: string, // require some code
        // modules: [[number, string]], // The first argument is number, the second argument is the specific code
        // }
        var base = baseJSBundle(entryPoint, prepend, graph, bundleOptions);
        // Sort the JS modules and concatenate strings to generate the final code
        bundleCode = bundleToString(base).code;

Copy the code

Before focusing on baseJSBundle, let’s review the data structure of graph and Prepend: It contains the following information:

  1. File-related dependencies
  2. Specifies the code after the Module passes through Babel
// graph[{dependencies: Map(404) { // Graph of the dependencies of each file under the entry file
    '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js' => {
     {
    inverseDependencies: Set(1) {
      '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js'
    },
    path: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/App.js'.dependencies: Map(8) {

      '@babel/runtime/helpers/createClass'= > {absolutePath: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/node_modules/@babel/runtime/helpers/createClass.js'.data: {
          name: '@babel/runtime/helpers/createClass'.data: { isAsync: false}}},/ /...
      'react'= > {absolutePath: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/node_modules/react/index.js'.data: { name: 'react'.data: { isAsync: false}}},'react-native'= > {absolutePath: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/node_modules/react-native/index.js'.data: { name: 'react-native'.data: { isAsync: false}}}},getSource: [Function: getSource],
    output: [{data: {// Corresponding file converted code
          code: `__d(function(g,r,i,a,m,e,d){var t=r(d[0]); Object.defineProperty(e,"__esModule",{value:! 0}),e.default=void 0; var n=t(r(d[1])),u=t(r(d[2])),l=t(r(d[3])),c=t(r(d[4])),f=t(r(d[5])),o=t(r(d[6])),s=r(d[7]); function y(){if("undefined"==typeof Reflect||! Reflect.construct)return! 1; if(Reflect.construct.sham)return! 1; if("function"==typeof Proxy)return! 0; try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),! 0}catch(t){return! 1}}var p=(function(t){(0,l.default)(R,t); var p,h,x=(p=R,h=y(),function(){var t,n=(0,f.default)(p); if(h){var u=(0,f.default)(this).constructor; t=Reflect.construct(n,arguments,u)}else t=n.apply(this,arguments); return(0,c.default)(this,t)}); function R(){return(0,n.default)(this,R),x.apply(this,arguments)}return(0,u.default)(R,[{key:"render",value:function(){return o.default.createElement(o.default.Fragment,null,o.default.createElement(s.View,{style:v.body},o.default.createElement(s. Text,{style:v.text},"\\u4f60\\u597d\\uff0c\\u4e16\\u754c")))}}]),R})(o.default.Component); e.default=p; var v=s.StyleSheet.create({body:{backgroundColor:'white',flex:1,justifyContent:'center',alignItems:'center'},text:{textAlign :'center',color:'red'}})}); `.lineCount: 1.map: [[1.177.9.0.'_react' ],
            [ 1.179.9.0.'_interopRequireDefault' ],
            [ 1.181.9.0.'r' ],
            [ 1.183.9.0.'d' ],
            [ 1.185.9.0 ],
            [ 1.190.10.0.'_reactNative']./ /...].functionMap: {
            names: [ '<global>'.'App'.'render'].mappings: 'AAA; eCW; ECC; GDQ; CDC'}},type: 'js/module'}}},'/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/App.js'= > {inverseDependencies: [Set].path: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/App.js'.dependencies: [Map].getSource: [Function: getSource],
      output: [Array]},'/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/app.json'= > {inverseDependencies: [Set].path: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/app.json'.dependencies: Map(0) {},
      getSource: [Function: getSource],
      output: [Array]}},entryPoints: [ // Import file
    '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js'].importBundleNames: Set(0){}}]Copy the code

baseJSBundle

Let’s focus on how baseJSBundle handles these data structures:

  • BaseJSBundle calls processModules three times in total to resolve: PreCode, postCode, and modules correspond to the var and polyfills parts, the require parts, and the _D parts

  • ProcessModules filters out all js/ data twice, using the user-defined filter function for the second filter. Transform filter is completed using wrapModule _d (factory, moduleId, dependencies) code

  • baseJSBundle

// node_modules/metro/src/DeltaBundler/Serializers/baseJSBundle.js
function baseJSBundle(entryPoint, preModules, graph, options) {
  for (const module of graph.dependencies.values()) {
    options.createModuleId(module.path);
  }

  const processModulesOptions = {
    filter: options.processModuleFilter,
    createModuleId: options.createModuleId,
    dev: options.dev,
    projectRoot: options.projectRoot,
  }; // Do not prepend polyfills or the require runtime when only modules are requested

  if (options.modulesOnly) {
    preModules = [];
  }
  // Use processModules to prepend the dependency map and code parsed by Metro, filter+join to the corresponding bundle output code
  const preCode = processModules(preModules, processModulesOptions)
    .map((_ref) = > {
      let _ref2 = _slicedToArray(_ref, 2),
        _ = _ref2[0],
        code = _ref2[1];

      return code;
    })
    .join("\n");

  const modules = _toConsumableArray(graph.dependencies.values()).sort(
    (a, b) = > options.createModuleId(a.path) - options.createModuleId(b.path)
  );
  // Use getAppendScripts to get dependency graphs for entry files and all runBeforeMainModule files and use getRunModuleStatement to generate _R (moduleId) code, Call processModules to generate the final code
  const postCode = processModules(
    getAppendScripts(
      entryPoint,
      _toConsumableArray(preModules).concat(_toConsumableArray(modules)),
      graph.importBundleNames,
      {
        asyncRequireModulePath: options.asyncRequireModulePath,
        createModuleId: options.createModuleId,
        getRunModuleStatement: options.getRunModuleStatement,
        inlineSourceMap: options.inlineSourceMap,
        projectRoot: options.projectRoot,
        runBeforeMainModule: options.runBeforeMainModule,
        runModule: options.runModule,
        sourceMapUrl: options.sourceMapUrl,
        sourceUrl: options.sourceUrl,
      }
    ),
    processModulesOptions
  )
    .map((_ref3) = > {
      let _ref4 = _slicedToArray(_ref3, 2),
        _ = _ref4[0],
        code = _ref4[1];
      return code;
    })
    .join("\n");
  return {
    pre: preCode,
    post: postCode,
    modules: processModules(
      // Use processModules to get an array of all the '_d' parts of the code
      _toConsumableArray(graph.dependencies.values()),
      processModulesOptions
    ).map((_ref5) = > {
      let _ref6 = _slicedToArray(_ref5, 2),
        module = _ref6[0],
        code = _ref6[1];

      return [options.createModuleId(module.path), code]; })}; }Copy the code
  • processModules

ProcessModules filters out all js/ data twice, using the user-defined filter function for the second filter. Transform filter is completed using wrapModule _d (factory, moduleId, dependencies) code

// node_modules/metro/src/DeltaBundler/Serializers/helpers/processModules.js

function processModules(modules, _ref) {
  let _ref$filter = _ref.filter,
    filter = _ref$filter === void 0 ? () = > true : _ref$filter,
    createModuleId = _ref.createModuleId,
    dev = _ref.dev,
    projectRoot = _ref.projectRoot;
  return _toConsumableArray(modules)
    .filter(isJsModule)
    .filter(filter)
    .map((module) = > [
      module,
      wrapModule(module, {
        createModuleId,
        dev,
        projectRoot,
      }),
    ]);
}
// node_modules/metro/src/DeltaBundler/Serializers/helpers/js.js
function wrapModule(module, options) {
  const output = getJsOutput(module);
  // Return the code directly if the type is js/script
  if (output.type.startsWith("js/script")) {
    return output.data.code;
  }

  const moduleId = options.createModuleId(module.path);
  / / d (factory, moduleId, dependencies) behind the two parameters is generated
  const params = [
    moduleId,
    Array.from(module.dependencies.values()).map((dependency) = > {
      returnoptions.createModuleId(dependency.absolutePath); })];// Add the module relative path as the last parameter (to make it easier to do
  // requires by name when debugging).

  if (options.dev) {
    params.push(path.relative(options.projectRoot, module.path));
  }
  // Code conversion, because there is only _d(factory) in the dependency graph, we need to add the moduleId and dependencies
  return addParamsToDefineCall.apply(void 0, [output.data.code].concat(params));
}
function getJsOutput(module) {
  const jsModules = module.output.filter((_ref) = > {
    let type = _ref.type;
    return type.startsWith("js/");
  });
  invariant(
    jsModules.length === 1.`Modules must have exactly one JS output, but The ${module.path} has ${ jsModules.length } JS outputs.`
  );
  const jsOutput = jsModules[0];
  invariant(
    Number.isFinite(jsOutput.data.lineCount),
    `JS output must populate lineCount, but The ${module.path} has ${ jsOutput.type } output with lineCount '${jsOutput.data.lineCount}'`
  );
  return jsOutput;
}

function isJsModule(module) {
  return module.output.filter(isJsOutput).length > 0;
}

function isJsOutput(output) {
  return output.type.startsWith("js/");
}
// node_modules/metro/src/lib/addParamsToDefineCall.js
function addParamsToDefineCall(code) {
  const index = code.lastIndexOf(")");

  for (
    var _len = arguments.length,
      paramsToAdd = new Array(_len > 1 ? _len - 1 : 0),
      _key = 1;
    _key < _len;
    _key++
  ) {
    paramsToAdd[_key - 1] = arguments[_key];
  }

  const params = paramsToAdd.map((param) = >param ! = =undefined ? JSON.stringify(param) : "undefined"
  );
  return code.slice(0, index) + "," + params.join(",") + code.slice(index);
}
Copy the code
  • getAppendScripts

GetAppendScripts is used to get dependency graphs for entry files and all runBeforeMainModule files and generate code for _R (moduleId) using getRunModuleStatement

function getAppendScripts(entryPoint, modules, importBundleNames, options) {
  const output = [];
  // If there are importBundleNames, insert the corresponding code
  if (importBundleNames.size) {
    const importBundleNamesObject = Object.create(null);
    importBundleNames.forEach((absolutePath) = > {
      const bundlePath = path.relative(options.projectRoot, absolutePath);
      importBundleNamesObject[options.createModuleId(absolutePath)] =
        bundlePath.slice(0, -path.extname(bundlePath).length) + ".bundle";
    });
    const code = `(function(){var $$=${options.getRunModuleStatement( options.createModuleId(options.asyncRequireModulePath) )}$$.addImportBundleNames(The ${String(
      JSON.stringify(importBundleNamesObject)
    )})}) (); `;
    output.push({
      path: "$$importBundleNames".dependencies: new Map(),
      getSource: () = > Buffer.from(""),
      inverseDependencies: new Set(),
      output: [{type: "js/script/virtual".data: {
            code,
            lineCount: countLines(code),
            map: [],},},],}); }if (options.runModule) {
    / / polymerization runBeforeMainModule and entry documents, said runBeforeMainModule before the default value is: / node_modules/metro/SRC/lib/polyfills/require. Js
    const paths = _toConsumableArray(options.runBeforeMainModule).concat([
      entryPoint,
    ]);

    for (const path of paths) {
      if (modules.some((module) = > module.path === path)) {
        // Generate code for _r(moduleId) through the getRunModuleStatement function
        / / getRunModuleStatement default values for details, please see: node_modules/metro - config/SRC/defaults/index. Js
        const code = options.getRunModuleStatement(
          options.createModuleId(path)
        );
        output.push({
          path: `require-${path}`.dependencies: new Map(),
          getSource: () = > Buffer.from(""),
          inverseDependencies: new Set(),
          output: [{type: "js/script/virtual".data: {
                code,
                lineCount: countLines(code),
                map: [],},},],}); }}}// ...

  return output;
}
Copy the code

So far we have analyzed the baseJSBundle.

bundleToString

After the previous step, we got the bundleToBundle: PreCode, postCode, and modules correspond to the var and polyfills parts, require parts, and _D parts. BundleToString does the following:

  • First, string concatenation using \n for var and polyfill parts of the code;
  • then_dPart of the code usedmoduleIdforAscending orderIt is constructed using string concatenation_dPartial code;
  • The final close as_rPartial code
function bundleToString(bundle) {
  let code = bundle.pre.length > 0 ? bundle.pre + "\n" : "";
  const modules = [];
  const sortedModules = bundle.modules
    .slice() // The order of the modules needs to be deterministic in order for source
    // maps to work properly.
    .sort((a, b) = > a[0] - b[0]);

  for (const _ref of sortedModules) {
    var _ref2 = _slicedToArray(_ref, 2);

    const id = _ref2[0];
    const moduleCode = _ref2[1];

    if (moduleCode.length > 0) {
      code += moduleCode + "\n";
    }

    modules.push([id, moduleCode.length]);
  }

  if (bundle.post.length > 0) {
    code += bundle.post;
  } else {
    code = code.slice(0, -1);
  }

  return {
    code,
    metadata: {
      pre: bundle.pre.length,
      post: bundle.post.length,
      modules,
    },
  };
}
Copy the code

conclusion

  1. React-native bundles packaged using Metro are roughly divided into four layers

Bundles are roughly divided into four layers:

  • Var declaration layer: information about the current operating environment, the start time of the bundle, and the process;
  • Poyfill layer: ! (function(r){}), defines rightDefine (__d),Require (__r),Clear (__c)Support, and load logic for modules (react-native and dependences);
  • Module definition layer: __dDefined code block, including RN framework source JS part, custom JS code part, image resource information, for require to introduce use
  • Require layer: code block defined by r, find code block defined by D and execute
  1. React-native uses Metro to package in three steps: parsing, transformation and generation;

3. Parse and transform part: Metro Server useIncrementalBundlerJs code parsing and conversion

The main benefits of using IncrementalBundler for parse conversions at Metro are:

  • The dependency graph and Babel transformed code of all related dependency files with entry file as entry are returned.
  • The dependency graph of all related dependency files in var definition part and Polyfill part and the code transformed by Babel are returned.

The overall process is shown in the figure:

Through the above process, we can summarize the following points:

  1. The dependency analysis and Babel transformation of the whole Metro was mainly carried out through JestHasteMap.
  2. During dependency analysis, Metro listens for file changes in the current directory and generates a final dependency map with minimal changes.
  3. JestHasteMap is used for both import file resolution and dependency resolution for Polyfill files;

The corresponding dependency graph format is as follows:

// graph[{dependencies: Map(404) { // Graph of the dependencies of each file under the entry file
    '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js' => {
     {
    inverseDependencies: Set(1) {
      '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js'
    },
    path: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/App.js'.dependencies: Map(8) {

      '@babel/runtime/helpers/createClass'= > {absolutePath: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/node_modules/@babel/runtime/helpers/createClass.js'.data: {
          name: '@babel/runtime/helpers/createClass'.data: { isAsync: false}}},/ /...
      'react'= > {absolutePath: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/node_modules/react/index.js'.data: { name: 'react'.data: { isAsync: false}}},'react-native'= > {absolutePath: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/node_modules/react-native/index.js'.data: { name: 'react-native'.data: { isAsync: false}}}},getSource: [Function: getSource],
    output: [{data: {// Corresponding file converted code
          code: `__d(function(g,r,i,a,m,e,d){var t=r(d[0]); Object.defineProperty(e,"__esModule",{value:! 0}),e.default=void 0; var n=t(r(d[1])),u=t(r(d[2])),l=t(r(d[3])),c=t(r(d[4])),f=t(r(d[5])),o=t(r(d[6])),s=r(d[7]); function y(){if("undefined"==typeof Reflect||! Reflect.construct)return! 1; if(Reflect.construct.sham)return! 1; if("function"==typeof Proxy)return! 0; try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),! 0}catch(t){return! 1}}var p=(function(t){(0,l.default)(R,t); var p,h,x=(p=R,h=y(),function(){var t,n=(0,f.default)(p); if(h){var u=(0,f.default)(this).constructor; t=Reflect.construct(n,arguments,u)}else t=n.apply(this,arguments); return(0,c.default)(this,t)}); function R(){return(0,n.default)(this,R),x.apply(this,arguments)}return(0,u.default)(R,[{key:"render",value:function(){return o.default.createElement(o.default.Fragment,null,o.default.createElement(s.View,{style:v.body},o.default.createElement(s. Text,{style:v.text},"\\u4f60\\u597d\\uff0c\\u4e16\\u754c")))}}]),R})(o.default.Component); e.default=p; var v=s.StyleSheet.create({body:{backgroundColor:'white',flex:1,justifyContent:'center',alignItems:'center'},text:{textAlign :'center',color:'red'}})}); `.lineCount: 1.map: [[1.177.9.0.'_react' ],
            [ 1.179.9.0.'_interopRequireDefault' ],
            [ 1.181.9.0.'r' ],
            [ 1.183.9.0.'d' ],
            [ 1.185.9.0 ],
            [ 1.190.10.0.'_reactNative']./ /...].functionMap: {
            names: [ '<global>'.'App'.'render'].mappings: 'AAA; eCW; ECC; GDQ; CDC'}},type: 'js/module'}}},'/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/App.js'= > {inverseDependencies: [Set].path: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/App.js'.dependencies: [Map].getSource: [Function: getSource],
      output: [Array]},'/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/app.json'= > {inverseDependencies: [Set].path: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/app.json'.dependencies: Map(0) {},
      getSource: [Function: getSource],
      output: [Array]}},entryPoints: [ // Import file
    '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js'].importBundleNames: Set(0){}}]Copy the code
  1. Metro code generation section usedbaseJSBundleGet the code and use itbaseToStringJoining together finallyBundlecode

In baseJSBundle:

  • BaseJSBundle calls processModules three times in total to resolve: PreCode, postCode, and modules correspond to the var and polyfills parts, the require parts, and the _D parts

  • ProcessModules filters out all js/ data twice, using the user-defined filter function for the second filter. Transform filter is completed using wrapModule _d (factory, moduleId, dependencies) code

In baseToString:

  • First, string concatenation using \n for var and polyfill parts of the code;
  • then_dPart of the code usedmoduleIdforAscending orderIt is constructed using string concatenation_dPartial code;
  • The final close as_rPartial code