What happened from react native bundle to bundle generation

Time:2021-7-20

This article deals withreact-nativeandmetroedition

Let’s take a look at a wave of example code in this article: it’s very simple, oneHello, 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",
  },
});

#1、 Preface

as everyone knows,React native (hereinafter referred to as RN)It needs to be donebundlePackage supplyandroid,iosload; Usually our packing command isreact-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 use it by defaultmetroAs a packaging tool, generatebundleBag.

The generated bundle package is roughly divided into four layers

  • Var declaration layer: for the current running environment, bundle start time, and process related information;
  • Polyfill layer: !(function(r){}), which defines thedefine(__d)require(__r)clear(__c)And the loading logic of modules (modules that react native and third-party dependencies depend on);
  • Module definition layer: \_\_ D defined code block, including RN framework source code JS part, custom JS code part, image resource information, for the introduction of require
  • Require layer: R, find the code block defined by D and execute it

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,[]);

//Require layer
__r(93);
__r(0);

After reading the above code, I wonder if you have any questions?

  1. varDefinition layer andpolyfillWhen was your code generated?
  2. We know that_d()There are three parameters, which are correspondingfactoryFunction, currentmoduleIdas well asmoduleDependency

    • metroWhat is used to do dependency analysis for the whole project?
    • moduleIdHow to generate?
  3. metroHow to pack?

In daily development, we may not care about the whole logic; Now let me take you into the world of packaging!

#2、 Metro packaging process

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

  • Command parameter analysis
  • Metro package service start
  • Package JS and resource files

    • Analysis, transformation and generation
  • Stop packaging service

What happened from react native bundle to bundle generation

#1. Command parameter analysis

First, let’s take a lookreact-native bundleHow to implement the model and how to parse the parameters; Since bundle is a subcommand of react native, we can start with 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");

//At node_ modules/react-native/node_ Modules / @ react native community / cli / build / commands / index.js contains all the commands of react native

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"));

Because this article mainly analyzes the react native packaging process, you only need to view thereact-native/node_modules/@react-native-community/cli/build/commands/bundle/bundle.jsThat’s it.

The bundle command is mainly registered in the bundle.js file, but the specific implementation uses thebuildBundle.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) {
  //Concrete implementation of bundle packaging
  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;

#2. Metro server startup

staynode_modules/react-native/node_modules/@react-native-community/cli/build/commands/bundle/buildBundle.jsThe default exportedbuildBundleThe method is the whole thingreact-native bundleEntry to execution. In the entrance, we mainly do the following things:

  • Merge Metro default configuration and custom configuration, and set maxworkers, resetcache
  • According to the parameters obtained from the analysis, the requestoptions are constructed and passed to the packing function
  • Instantiate Metro server
  • Start Metro to build bundles
  • Processing resource files, parsing
  • Shut down Metro server
// node_modules/react-native/node_modules/@react-native-community/cli/build/commands/bundle/buildBundle.js
//Metro packaging service is also 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 resource file
var _saveAssets = _interopRequireDefault(require("./saveAssets"));
//The default configuration of Metro is provided
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";
  //According to the input parameter of the command line -- sourcemap output, construct the sourcemapurl
  let sourceMapUrl = args.sourcemapOutput;

  if (sourceMapUrl && !args.sourcemapUseAbsolutePath) {
    sourceMapUrl = _path().default.basename(sourceMapUrl);
  }
  //According to the parameters obtained from the analysis, the requestoptions are constructed and passed to the packing 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 packaging, what? Isn't the author talking about server packaging? Why output? A: the following will explain
    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 save it in the location specified by -- assets dest in the next step
    const outputAssets = await server.getAssets({
      ..._Server().default.DEFAULT_BUNDLE_OPTIONS,
      ...requestOpts,
      bundleType: "todo",
    }); // When we're done saving bundle output and the assets, we're done.
    //Save resource file到指定目录
    return await (0, _saveAssets.default)(
      outputAssets,
      args.platform,
      args.assetsDest
    );
  } finally {
    //Stop Metro packaging service
    server.end();
  }
}

var _default = buildBundle;
exports.default = _default;

From the above code, we can see that the specific packaging implementation is in progressoutput.build(server, requestOpts)OutputyesoutputBundleType, this part of the code in theIn Metro JS, the specific path is: 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";

We can see that although theoutput.build(server, requestOpts)To package is to use the incomingpackagerClient.buildMake a package. andpackagerClientIt was just introduced by usServer。 andServerNow we’re going to analyze the packaging process. Its source code location is:node_modules/metro/src/Server.js

#Metro build bundle: process entry

Through the above analysis, we have known the whole processreact-native bundleThe start of the packaged service is atnode_modules/metro/src/Server.jsOfbuildIn the method:

class Server {
  //Build functions and initialize 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 the modules to better manage them; The format of splitting 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 packaging core: resolution and transformation
      const _ref13 = yield _this2._bundler.buildGraph(
          entryFile,
          transformOptions,
          {
            onProgress,
            shallow: graphOptions.shallow,
          }
        ),
        prepend = _ref13.prepend,
        graph = _ref13.graph;

      //Get build entry file path
      const entryPoint = path.resolve(_this2._config.projectRoot, entryFile);
      //Initialize the build parameters. The parameters here come from the 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._ Create module ID, // the user-defined / default create module idfactory generates ID for each module; For details of the default generation rules, please refer to: node_ modules/metro/src/lib/createModuleIdFactory.js
        getRunModuleStatement: _ this2._ Config. Serializer. Getrunmodulestatement, // sign the method
        //The default value is getrunmodulestation: moduleid = >`__ r(${JSON.stringify(moduleId)});`,
        //For details, please refer to: 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)
        ), // specifies the module to run before the main module. The default value is: getmodulesrunbeforemainmodule: () = > []
        //For details, please refer to: 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 generation. If so, call the 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, the author divides it into two steps, which is easy to analyze

        //After parsing and transforming the data, the following formatted data is generated
        // {
        //Code of pre: string, // var definition part and poyfill part
        //Post: string, // require partial code
        //Modules: [[number, string]], // in the module definition part, the first parameter is number, and the second parameter is the specific code
        // }
        var base = baseJSBundle(entryPoint, prepend, graph, bundleOptions);
        //Sort the JS module and splice the 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,
      };
    })();
  }
}

In this build function, theBuildgraph, and this._bundlerThe initialization of takes place on theconstructorIn the middle.

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

Here_bundleryesIncrementalBundlerThe example of itbuildGraphFunction completes the first two steps in the packaging processResolutionandTransformation。 Now let’s take a detailed look at the Metro parsing and conversion process.

#Metro Building bundles: parsing and transformation

In the previous section, we know that Metro usesIncrementalBundlerJS code analysis and conversion, used in MetroIncrementalBundlerThe main functions of analytic transformation are as follows

  • BackThe dependency map of all related dependency files with the entry file as the entry and the code after Babel transformation
  • BackVar definition part and Polyfill part of the dependency map of all related dependency files and Babel converted code

The overall process is shown in the figure

What happened from react native bundle to bundle generation

Through the above process, we summarize the following points:

  1. The whole Metro is used for dependency analysis and Babel transformationJestHasteMapTo do dependency analysis;
  2. In the process of dependency analysis, Metro will monitor the file changes of the current directory, and then generate the final dependency graph with the minimum change;
  3. Whether it’s entry file parsing or Polyfill file dependency parsing, it’s all usedJestHasteMap ;

Next, we will 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. Through the dependency analysis of the entry file, we get 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
      };
    })();
}

#Requirement and module definition part parsing and dependency generation

staybuildGraphForEntriesUse in_deltaBundler.buildGraphGenerate a graph,

// 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
      });
        // ....
      return graph;
    })();

// node_modules/metro/src/DeltaBundler.js
  buildGraph(entryPoints, options) {
    var _this = this;

    return _asyncToGenerator(function*() {
        //Using node_ Modules / metro / SRC / bundler.js
      const depGraph = yield _this._bundler.getDependencyGraph();
      //Monitor the changes of files. If there are changes in files, update the dependencies between files
      const deltaCalculator = new DeltaCalculator(
        entryPoints,
        depGraph,
        options
      );
      //Calculate the changes between modules, including the addition, deletion and modification of modules. If there are changes, they will be updated at the first time
      yield deltaCalculator.getDelta({
        reset: true,
        shallow: options.shallow
      });
      //According to the returned dependency graph and the result of file change detection, the module dependency information in the following format is returned( Full formatting will be given later)
    //    {
    //         dependencies: new Map(),
    //         entryPoints,
    //         importBundleNames: new Set()
    //    }
      const graph = deltaCalculator.getGraph();

      _this._deltaCalculators.set(graph, deltaCalculator);

      return graph;
    })();
  }

//  node_modules/metro/src/Bundler.js
//Dependency map analysis
class Bundler {
  constructor(config, options) {
    //Bundler also uses dependency graph for dependency analysis to generate dependency graph
    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;
  }
}

//Dependency graph.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 function of static parsing JavaScript module dependencies for node module parsing and Facebook's haste module system.

//Because the creation of hash map is synchronous and most tasks are blocked by I / O, the multi-core computer is used for parallel operation.

afterDependencyGraph.loadandDeltaCalculatorAfter that, the generated dependency graph format is as follows:

{
  dependencies: Map(404) {
      //The dependency information of each module
    '/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 / react native SSR / APP. JS' // module path
            Dependencies: Map (8) {// other modules the module depends on
            },
            getSource: [Function: getSource],
            output: [
            {
                data: {
                Code: ', // the code of the packaged modification module
                lineCount: 1,
                map: [
                ],
                functionMap: {
                    names: [ '<global>', 'App', 'render' ],
                    mappings: 'AAA;eCW;ECC;GDQ;CDC'
                }
                },
                Type: 'JS / module' // type. Metro will judge whether it is a JS module by startwidth ('js')
            }
            ]
    },
  },
  Entrypoints: [// entry
    '/Users/mrgaogang/Desktop/react-native-ssr/ReactNativeSSR/index.js'
  ],
  importBundleNames: Set(0) {}
}

#Partial analysis of VaR and Polyfill

I see it in front of meBuildgraph of incrementalbundler.jsPassed ingetPrependedScriptsGetVaR and PolyfillPart of the code; Let’s take a look at some of themgetPrependedScripts:

// node_modules/metro/src/lib/getPreludeCode.js
function _getPrependedScripts() {
  _getPrependedScripts = _asyncToGenerator(function*(
    config,
    options,
    bundler,
    deltaBundler
  ) {
    //Get all the polyfills, including default and custom ones
    //See: node for default Polyfill_ modules/react-native/node_ Node is used in modules / @ react native community / cli / build / tools / loadmetroconfig.js getdefaultconfig: function_ Modules / react native / rn-get-polyfills.js
    // 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",
    });
    //Through deltabundler.buildgraph, analyze the dependency graph of the following four files and custom Polyfill
    //      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 part and the Polyfill dependency graph analyzed by deltabundler. Buildgraph
      _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
//The code of VaR definition part
function getPreludeCode(_ref) {
  let extraVars = _ref.extraVars,
    isDev = _ref.isDev;
  const vars = [
    "__BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now()",
    `__DEV__=${String(isDev)}`,
  ].concat(_toConsumableArray(formatExtraVars(extraVars)), [
    "process=this.process||{}",
  ]);
  return `var ${vars.join(",")};${processEnv(
    isDev ? "development" : "production"
  )}`;
}

There is another part that the author has not explained in detail here, that is, usingJestHasteMapThe detailed part of file dependency analysis is carried out; In the future, the author will give a separate article to explain, about consulting.

So far, Metro’s dependency analysis and code generation on entry files and polyfills are over. If you look back at the beginning of this chapter, I don’t know if you’ve been enlightened. This paper describes the analysis and conversion of metro, and the following part describes how Metro generates the final bundle code through the converted file dependency graph.

#Metro build bundle: generate

Back to the initial server service startup code, we found thatbuildGraphAnd then I got itPrepend: code and dependency of VaR and Polyfillas well asGraph: dependency and code of entry file; stayIf no custom build is providedMetro usesbaseJSBundleThe dependency graph and the code of each module are used after a series of operationsbundleToStringConvert to the final code.

//Metro packaging core: resolution and transformation
      const _ref13 = yield _this2._bundler.buildGraph(
          entryFile,
          transformOptions,
          {
            onProgress,
            shallow: graphOptions.shallow,
          }
        ),
        prepend = _ref13.prepend,
        graph = _ref13.graph;
        // ....
        //Here, the author divides it into two steps, which is easy to analyze

        //After parsing and transforming the data, the following formatted data is generated
        // {
        //Code of pre: string, // var definition part and poyfill part
        //Post: string, // require partial code
        //Modules: [[number, string]], // in the module definition part, the first parameter is number, and the second parameter is the specific code
        // }
        var base = baseJSBundle(entryPoint, prepend, graph, bundleOptions);
        //Sort the JS module and splice the strings to generate the final code
        bundleCode = bundleToString(base).code;

In the attentionbaseJSBundleBefore that, let’s review the data structure of graph and prepend: it mainly includes the following information:

  1. File related dependencies
  2. Specify the code of the module after passing Babel
// graph
[
{
  Dependencies: Map (404) {// the relationship graph of other files that each file depends on in 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: {// the converted code of the corresponding file
          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: [// entry file
    '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js'
  ],
  importBundleNames: Set(0) {}
}

]

# baseJSBundle

Now let’s focus on itbaseJSBundleHow to deal with the above data structure:

  • baseJSBundleCalled three times as a wholeprocessModulesThe results are as followspreCode , postCodeandmodulesThe corresponding areVaR and polyfills part of the code , The code of the require section , \_ Part D code
  • processModulesAfter two timesfilterFilter out all types ofjs/Type of data, the second filter uses user-definedfilterFunction; Use after filteringwrapModuleconvert to_d(factory,moduleId,dependencies)Code for
  • 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 = [];
  }
  //The preprocess dependency graph and code parsed by metro are generated by processmodules, and filter + join is generated by the corresponding bundle
  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)
  );
  //Getappendscripts is used to obtain the dependency graph of the entry file and all runbeforemainmodule files, and getrunmodulestation method is used to generate the dependency graph_ 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 all`_ D 'part of the code array
      _toConsumableArray(graph.dependencies.values()),
      processModulesOptions
    ).map((_ref5) => {
      let _ref6 = _slicedToArray(_ref5, 2),
        module = _ref6[0],
        code = _ref6[1];

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

processModulesAfter two timesfilterFilter out all types ofjs/Type of data, the second filter uses user-definedfilterFunction; After filtering, use wrapmodule to convert to_d(factory,moduleId,dependencies)Code for

// 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);
  //If the type is JS / script, the code will be returned directly
  if (output.type.startsWith("js/script")) {
    return output.data.code;
  }

  const moduleId = options.createModuleId(module.path);
  //D (factory, moduleid, dependencies)
  const params = [
    moduleId,
    Array.from(module.dependencies.values()).map((dependency) => {
      return options.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 only_ D (factory), with moduleid and dependency
  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 ${module.path} has ${
      jsModules.length
    } JS outputs.`
  );
  const jsOutput = jsModules[0];
  invariant(
    Number.isFinite(jsOutput.data.lineCount),
    `JS output must populate lineCount, but ${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);
}
  • getAppendScripts

It saysgetAppendScriptsThe main functions are as followsGet the dependency graph of the entry file and all runbeforemainmodule files, and use the getrunmodulestation method to generate it_r(moduleId)Code for

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(${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) {
    //Aggregate runbeforemainmodule and entry file. As mentioned before, the default value of runbeforemainmodule 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)) {
        //Generated by getrunmodulestation function_ The code of R (moduleid)
        //For the default value of getrunmodulestation, please refer to: 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;
}

thusbaseJSBundleWe have finished the analysis.

# bundleToString

Go through the previous stepbundleToBundleWe obtained the following information respectively:preCode , postCodeandmodulesThe corresponding areVaR and polyfills part of the code , The code of the require section , \_ Part D codeandbundleToStringThe functions of the system are as follows:

  • First, use the code of VaR and Polyfill to splice strings;
  • And then_dPart of the code usemoduleIdconductAscending orderAnd use string splicing method to construct_dPart of the code;
  • Finally, he Ru_rPart of the 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,
    },
  };
}

#summary

  1. React native is packaged with Metro, and the bundle is roughly divided into four layers

Bundle package is divided into four layers

  • Var declaration layer: for the current running environment, bundle start time, and process related information;
  • Poyfill layer: !(function(r){}), which defines thedefine(__d)require(__r)clear(__c)And the loading logic of modules (modules that react native and third-party dependencies depend on);
  • Module definition layer: __dDefined code block, including RN framework source code JS part, custom JS code part, image resource information, for the introduction of require
  • Require layer: R, find the code block defined by D and execute it
  1. react-nativeusemetroPackaging is mainly divided into three steps: parsing, transformation and generation; What happened from react native bundle to bundle generation
  2. Analysis and transformation part: Metro server useIncrementalBundlerAnalyze and transform JS code

Used in MetroIncrementalBundlerThe main functions of analytic transformation are as follows

  • BackThe dependency map of all related dependency files with the entry file as the entry and the code after Babel transformation
  • BackVar definition part and Polyfill part of the dependency map of all related dependency files and Babel converted code

The overall process is shown in the figure

What happened from react native bundle to bundle generation

Through the above process, we summarize the following points:

  1. The whole Metro is used for dependency analysis and Babel transformationJestHasteMapTo do dependency analysis;
  2. In the process of dependency analysis, Metro will monitor the file changes of the current directory, and then generate the final dependency graph with the minimum change;
  3. Whether it’s entry file parsing or Polyfill file dependency parsing, it’s all usedJestHasteMap ;

The corresponding dependency graph format is as follows:

// graph
[
{
  Dependencies: Map (404) {// the relationship graph of other files that each file depends on in 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: {// the converted code of the corresponding file
          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: [// entry file
    '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js'
  ],
  importBundleNames: Set(0) {}
}

]
  1. The code generation part of Metro usesbaseJSBundleGet the code and use itbaseToStringFinal splicingBundlecode

staybaseJSBundleMedium:

  • baseJSBundleCalled three times as a wholeprocessModulesThe results are as followspreCode , postCodeandmodulesThe corresponding areVaR and polyfills part of the code , The code of the require section , _dPart of the code
  • processModulesAfter two timesfilterFilter out all types ofjs/Type of data, the second filter uses user-definedfilterFunction; Use after filteringwrapModuleconvert to_d(factory,moduleId,dependencies)Code for

staybaseToStringMedium:

  • First, use the code of VaR and Polyfill to splice strings;
  • And then_dPart of the code usemoduleIdconductAscending orderAnd use string splicing method to construct_dPart of the code;
  • Finally, he Ru_rPart of the code

Original address:What happened from react native bundle to bundle generation