The introduction

React Native packages our JS code into a single file by default. When our React Native app gets big enough to take a long time to download all the JS code at once, we might think of optimizing it by loading it on demand. The first task of loading on demand is to split the code. This article will reveal the secrets of unpacking React Native step by step.

Metro is introduced

React Native uses Metro for packaging, so we need to get to know it first. The best way to study a packer is to look at its build.

Construction product analysis

Suppose we have the following code:

// index.js
const {say} = require('./utils')

say('I just can't tell her.')

// utils.js
exports.say = (word) = > console.log(word)
Copy the code

We package it using Metro (requires Metro and Metro-Core installed) :

metro build index.js --out bundle.js -z false
Copy the code

Where -z indicates whether the code is minify or not, we select False for ease of viewing.

The packaged file looks like this (the previous code has been omitted) :

. __d(function (
    global,
    _$$_REQUIRE,
    _$$_IMPORT_DEFAULT,
    _$$_IMPORT_ALL,
    module.exports,
    _dependencyMap
  ) {
    'use strict'

    const {say} = _$$_REQUIRE(_dependencyMap[0])

    say('I just can't tell her.')},0[1]
)
__d(
  function (
    global,
    _$$_REQUIRE,
    _$$_IMPORT_DEFAULT,
    _$$_IMPORT_ALL,
    module.exports,
    _dependencyMap
  ) {
    'use strict'

    exports.say = (word) = > console.log(word)
  },
  1,
  []
)
__r(0)
Copy the code

__d

__d means define, which is to define a module:

; (function (global) {
  'use strict'

  global[`${__METRO_GLOBAL_PREFIX__}__d`] = define

  var modules = clear()
  function clear() {
    modules = Object.create(null)
    return modules
  }

  function define(factory, moduleId, dependencyMap) {
    if(modules[moduleId] ! =null) {
      return
    }

    const mod = {
      dependencyMap,
      factory,
      hasError: false.importedAll: EMPTY,
      importedDefault: EMPTY,
      isInitialized: false.publicModule: {
        exports: {},
      },
    }
    modules[moduleId] = mod
  }
  / /...}) (typeofglobalThis ! = ='undefined'
    ? globalThis
    : typeof global! = ='undefined'
    ? global
    : typeof window! = ='undefined'
    ? window
    : this
)
Copy the code

The define function takes 3 parameters, factory is the module factory method, moduleId is the moduleId, and dependencyMap is the module’s dependency list, which stores the ids of other modules it depends on. And then saved to modules.

__r

__r is a bit complicated, but we’ll follow through and end up with this function:

function loadModuleImplementation(moduleId, module) {...module.isInitialized = true
  const {factory, dependencyMap} = module

  try {
    const moduleObject = module.publicModule
    moduleObject.id = moduleId
    factory(
      global,
      metroRequire,
      metroImportDefault,
      metroImportAll,
      moduleObject,
      moduleObject.exports,
      dependencyMap
    )
    {
      module.factory = undefined
      module.dependencyMap = undefined
    }
    return moduleObject.exports
  } catch (e) {
    / /...
  } finally{}}Copy the code

This function gets the module from Modules via its module ID, and then executes the module’s factory method, which makes some modifications to the exports object passed in (for example, the following module added the say field to exports).

__d(
  function (
    global,
    _$$_REQUIRE,
    _$$_IMPORT_DEFAULT,
    _$$_IMPORT_ALL,
    module.exports,
    _dependencyMap
  ) {
    'use strict'

    exports.say = (word) = > console.log(word)
  },
  1[]),Copy the code

LoadModuleImplementation finally returns the modified exports object.

Going back to the above example, we can now unpack it manually by simply splitting the packaged product into two files:

// utils.bundle.js. __d(function (
    global,
    _$$_REQUIRE,
    _$$_IMPORT_DEFAULT,
    _$$_IMPORT_ALL,
    module.exports,
    _dependencyMap
  ) {
    'use strict'

    const {say} = _$$_REQUIRE(_dependencyMap[0])

    say('I just can't tell her.')},0[1])// index.bundle.js
__d(
  function (
    global,
    _$$_REQUIRE,
    _$$_IMPORT_DEFAULT,
    _$$_IMPORT_ALL,
    module.exports,
    _dependencyMap
  ) {
    'use strict'

    exports.say = (word) = > console.log(word)
  },
  1,
  []
)
__r(0)
Copy the code

To use it, we just need to make sure that utils.bundle.js is loaded first and then index.bundle.js is loaded. So how to do that automatically? We need to take a look at some of the configurations that Metro packages.

The configuration file

Metro configuration files are as follows:

module.exports = {
  /* general options */

  resolver: {
    /* resolver options */
  },
  transformer: {
    /* transformer options */
  },
  serializer: {
    /* serializer options */
  },
  server: {
    /* server options */}},Copy the code

We only need to know about Serializer, which is the configuration related to build product output. We also need to know about createModuleIdFactory and processModuleFilter.

createModuleIdFactory

This configuration is used to generate the module ID, for example, when configured as follows:

module.exports = {
  serializer: {
    createModuleIdFactory() {
      return (path) = > {
        return path
      }
    },
  },
}
Copy the code

The packaged module will have the file path as the module ID:

. __d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module.exports, _dependencyMap) {
  "use strict";

  const {
    say
  } = _$$_REQUIRE(_dependencyMap[0]);

  say('I just can't tell her.');
},"/demo1/src/index.js"["/demo1/src/utils.js"]);
__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module.exports, _dependencyMap) {
  "use strict";

  exports.say = word= > console.log(word);
},"/demo1/src/utils.js"[]); __r("/demo1/src/index.js");
Copy the code

processModuleFilter

This configuration is used to filter out the output of the module, again using an example:

// index.js
require('./unused.js')
const {say} = require('./utils')

say('I just can't tell her.')

// metro.config.js
module.exports = {
  serializer: {
    processModuleFilter: function (module) {
      return module.path.indexOf('unused') = = = -1}},}Copy the code

The unused.js module will be removed from the build artifacts based on the configuration package above.

Unpacking of actual combat

Based on the above knowledge, we can finally start the actual unpacking. Here’s an example:

// utils.js
exports.say = (word) = > console.log(word)

// index1.js
const {say} = require('./utils')

say('I just can't tell her.')

// index2.js
const {say} = require('./utils')

say('Sparrow outside the window, talking on a telephone pole.')
Copy the code

You can see that utils.js is used in both index1.js and index2.js, and our purpose is to extract it separately into a package called base.js, Index1.js and index2.js are packaged as bundle1.js and bundle2.js, respectively. Base.js is loaded first, followed by bundle1.js and bundle2.js.

Let’s start by packaging base.js with the following packaging configuration:

const fs = require('fs')
module.exports = {
  serializer: {
    createModuleIdFactory: function () {
      const moduleMap = {}
      const projectRootPath = __dirname
      const moduleFile = 'modules.txt'

      if (fs.existsSync(moduleFile)) {
        fs.unlinkSync(moduleFile)
      }

      return function (path) {
        const modulePath = path.substr(projectRootPath.length + 1)

        if(! moduleMap[modulePath]) { moduleMap[modulePath] =true
          fs.appendFileSync(moduleFile, `${modulePath}\n`)}return modulePath
      }
    },
  },
}
Copy the code

This configuration means that the relative path of the module in the project is used as the module ID, and the packaged module ID is recorded in modules.txt. After the package is completed, the content of the file is as follows:

src/utils.js
Copy the code

Next package bundle1.js, whose packaging configuration is:

const fs = require('fs')

const moduleFile = 'modules.txt'
const existModuleMap = {}

fs.readFileSync(moduleFile, 'utf8')
  .toString()
  .split('\n')
  .forEach((path) = > {
    existModuleMap[path] = true
  })

function getParsedModulePath(path) {
  const projectRootPath = __dirname
  return path.substr(projectRootPath.length + 1)}module.exports = {
  serializer: {
    createModuleIdFactory: function () {
      const currentModuleMap = {}

      return function (path) {
        const modulePath = getParsedModulePath(path)
        if(! (existModuleMap[modulePath] || currentModuleMap[modulePath])) { currentModuleMap[modulePath] =true
          fs.appendFileSync(moduleFile, `${modulePath}\n`)}return modulePath
      }
    },
    processModuleFilter: function (modules) {
      const modulePath = getParsedModulePath(modules.path)
      if (existModuleMap[modulePath]) {
        return false
      }
      return true}},}Copy the code

This configuration means using the relative path of the module in the project as the module ID and filtering out existing modules in modules.txt. Bundle1.js package results look like this:

The first line is some initialization code for the module system. This code already exists in base.js, so we need to delete this line and use line-replace to do this:

// package.json{..."scripts": {
    "build:bundle1": "metro build src/index1.js --out bundle1.js -z true -c metro.bundle.js"."postbuild:bundle1": "node removeFirstLine.js ./bundle1.js",},"devDependencies": {
    "line-replace": "^ 2.0.1." ",}}// removeFirstLine.js
const lineReplace = require('line-replace')

lineReplace({
  file: process.argv[2].line: 1.text: ' '.addNewLine: false.callback: ({file, line, text, replacedText, error}) = > {
    if(! error) {console.log(`Removed ${replacedText}`)}else {
      console.error(error)
    }
  },
})
Copy the code

At this point, bundle1.js is packaged, and you can do the same for Bundle2.js.

Finally, verify that the packaged product works:

<! DOCTYPEhtml>
<html lang="en">
  <body>
    <script src="./base.js"></script>
    <script src="./bundle1.js"></script>
    <script src="./bundle2.js"></script>
  </body>
</html>
Copy the code

conclusion

Unpacking React Native sounds like a fancy concept, but it’s easy to put it into practice. However, this is only the first step. Next we need to modify the Native code to truly implement the on-demand loading mentioned above, which will be revealed in the next article. Welcome to pay attention to the public account “front-end tour”, let us travel in the front-end ocean together.