Introduction: More and more products began to do AB experiments, the system will show different page effects according to different groups, in order to achieve user growth directory. At present, h5 page has more AB tests, while RN has less AB tests due to its particularity. This paper mainly introduces a scheme of AB experiment in RN page.

#One, foreword

When it comes to AB, whether it’s H5 or RN, there’s always a bunch of possibilities:

  • Scheme 1. Experiment ABThe runtimeThe code logic is split

    • package
    • Pull cgi to get the experimental configuration
    • Following the code logic, different experiments load different components
  • Scheme 2. Experiment ABBuild timeSplit code logic (one build)

    • Read experimental configuration
    • Build all experimental possibilities and unpack
    • Loading according to different experiments read differentjsbundle / js
  • Scheme 3. Experiment ABBuild timeSplit code logic (multiple builds)

    • Read experimental configuration
    • Babel replaces Module, builds all experimental possibilities, and unpacks
    • Loading according to different experiments read differentjsbundle / js
  • , etc.

Reviewing the two options above, the first option has some shortcomings:

  1. All the experiments are packaged together, which is bound to make the wholejs/jsbundle They become huge;
  2. Logical splitting in code is more like writing a normal page, where experiments and hosts cannot be decoupled
  3. Compared to scenario 3, multiple can be generated in a single buildbundle

Due to the particularity of RN (the JsBundle needs to be downloaded and loaded locally), we strive to download the Jsbundle with a smaller volume, reduce users’ download time, and optimize the time spent on the first screen of the page. So we chose to use the second scheme for the AB experiment of RN. The reasons for choosing the second option are:

  1. Users can load it on demandjsbundle/jsTo optimize load time and JsBundle load time without full load.
  2. AB experiment and host engineering can be decoupled;

#2. RN AB experiment unpacking scheme

As we all know, RN doesn’t have the webPack and other build tools that H5 does that allow you to customize the build process. In particular, older versions of RN (e.g. 0.56) can be more cumbersome to interfere with or customize the build process.

So the strategy we adopted was to customize cli and expand metro build. The overall unpacking of RN is mainly divided into five steps:

  • usingjest-haste-mapgeneratemoduleCorresponding dependencies
  • readabconfig.jsonCombine all experimental possibilities
  • rightmodulesData conversion and filtering
  • Global minimum dependence analysis is performed from the entry for the specified AB experiment
  • combinationbundlegenerate

#1. The use ofjest-haste-mapgeneratemoduleCorresponding dependencies

Jsbundle dependency generation takes the following steps:

  1. usejest-haste-mapformoduleThe dependence between;
  2. usingbabelCode escape
  3. Customize at the same timecreateModuleIdFactorygeneratemoduleIdIn the case of AB experiment, customize the string for subsequent differentiation.

The brief code is as follows:

// Rely on fetch
async function load(opts: Options, useWatchman? : boolean =true
) :Promise<DependencyGraph> {
  const haste = DependencyGraph._createHaste(opts, useWatchman);
  const { hasteFS, moduleMap } = await haste.build();

  return new DependencyGraph({
    haste,
    initialHasteFS: hasteFS,
    initialModuleMap: moduleMap,
    opts,
  });
}

/ / moduleId generates
function createModuleIdFactory() {
  const fileToIdMap = new Map(a);let nextId = randomNum;
  let abNextId = randomNum;
  return (path) = > {
    let id = fileToIdMap.get(path);
    const relPath = pathM.relative(base, path);
    if (relPath.indexOf("src/abtest")! = = -1) {
      if (abNextId === randomNum) {
        abTestIdMaps.clearIds();
      }
      if (id && typeofid ! = ="number") {
        return id;
      }
      abNextId = abNextId + 1;
      const outputId = `rnplus_abtest_template_${abNextId}`;
      fileToIdMap.set(path, outputId);
      // Record the relationship between module path and Id
      abTestIdMaps.rnABTestIds(relPath, outputId);
      return outputId;
    }
    / /...

    return id;
  };
}

Copy the code

The resulting modules format is as follows:

Mainly include:

  • Id of each module
  • Map relationship, module relative path
  • The source code
  • Module absolute path
  • Module type
  • Babel escaped code
  • And dependencies between modules.

Now that we have all the modules information for the current project, we’re ready to experiment AB.

#2. Readabconfig.jsonCombine all experimental possibilities

Let’s take a look at what abconfig.json looks like.

{
  "enable": true."list": [{"name": "The experiment 1"."abKey": "shiyan1"."component": "button"."path": ""."strategy": [{"name": "StrategyA"."default": true
        },
        {
          "name": "StrategyB"}]}, {"name": "The experiment 2"."abKey": "shiyan2"."component": "componentA"."path": ""."strategy": [{"name": "StrategyA"."default": true
        },
        {
          "name": "StrategyB"}]}]}Copy the code

We will note that when we customize the moduleId createModuleIdFactory, we will record the mapping relationship between the path and ID of each module.

To obtain the combination of all experimental strategies is essentially to find all possibilities of [[a,b],[C,d],[e,f]] n group strategies. We can calculate all possibilities of the strategy by using the following function.

function combination(arr) {
  return arr.reduce(
    (pre, cur) = > {
      const res = [];
      pre.forEach((_pre) = > {
        cur.strategy.forEach((_cur) = > {
          res.push(
            _pre.concat([
              {
                ab: cur.component,
                component: _cur.name,
                default:!!!!! _cur.default,componentPath: cur.path
                  ? `${cur.path}/index.js`
                  : `src/abtest/${cur.component}/index.js`.path: cur.path
                  ? `${cur.path}/index.${_cur.name}.js`
                  : `src/abtest/${cur.component}/index.${_cur.name}.js`,}])); }); });returnres; }, [[]]); }Copy the code

#3. TomodulesData conversion and filtering

This will require modules filtering according to our different businesses, removing the code of the Jsbunlde Common package, and customizing modules that need to be inserted.

function modulesSplitCommonAndInsertPerformance(allNoABtestModules, platform) {
  const businessId = platform === "ios" ? 308 : 306;

  The preceding 11 lines of code are common and do not need to be packaged into poliyfills. The poliyfills section is 11 in length
  const modules = allNoABtestModules.slice(11).filter(function(ele) {
    if (typeof ele.id === "number") {
      return ele.id > businessId;
    }
    return true;
  });
  const speedTimePoint = `window["${app.appName}_StartTime"]=Date.now(); `;

  modules.unshift({
    code: speedTimePoint,
    id: "performance_point".name: "performance_point".path: "".dependencies: [],});return modules;
}

Copy the code

#4. Perform global minimum dependence analysis from the entry for the specified AB experiment

Minimum dependency analysis for a certain experiment is essentially to use the dependencies obtained in the first step and obtain all modules that are not dependent by recursion starting from require.

function findNotUseModules(moduleList) {
  const depMap = {};
  const requireList = [];
  moduleList.forEach((ele) = > {
    if(ignoreModulesOptimiza.indexOf(ele.id) ! = = -1) {
      return;
    }
    if (ele.type === "module") {
      depMap[ele.id] = { deps: ele.dependencies || [], useful: false };
    }
    if (ele.type === "require") { requireList.push(ele.id); }}); requireList.forEach((ele) = > {
    // Recursively traverse the dependency tree to determine what is needed
    recursiveUseful(ele, depMap);
  });

  const notUsefulModules = [];
  Object.keys(depMap).forEach((key) = > {
    if (!depMap[key].useful) {
      notUsefulModules.push(key);
    }
  });

  console.log("not use modules is", notUsefulModules.length);
  return notUsefulModules;
}

Copy the code

#Combination of 5.bundlegenerate

The code generation, which should be the easiest part of the process, is actually the code patchwork in Modules.

function writeItemABTestBundle(notABList, abList, numberRequire, itemTest, bundleOutput, encoding, componentIDAndModuleIdMaps, platform) {
  let useList = replaceIndexIdsToABIds(notABList, componentIDAndModuleIdMaps);
  useList = useList.insertArray(useList.length - numberRequire, abList);

  const deleteModules = findNotUseModules(useList);

  // Ignore modules that are not needed
  useList = useList.filter(function(e) {
    if (e.type === "module") {
      // Remember to compare to string
      return deleteModules.indexOf(`${e.id}`) = = = -1;
    }
    return true;
  });

  const fileName = itemTest
    .map(function(e) {
      return e.ab + "_" + e.component;
    })
    .join("__");
  const fileSavePath = bundleOutput
    ? bundleOutput.substring(0, bundleOutput.lastIndexOf("/"))
    : "./public/cdn/bundle";

  const code = useList.map(function(ele) {
    return ele.code;
  });
  const filePath = `${fileSavePath}/${fileName}.${platform}.jsbundle`;
  const writeBundle = writeFile(
    filePath,
    code.join("\n").replace("__version_code_placeholder__".String(Date.now())),
    encoding
  );
  writeBundle.then(function() {});
  return writeBundle;
}

Copy the code

Now that we have explained the whole simple step, let’s look at the final result.

#3. Optimization results

Before optimization

The optimizedSubcontracting, the volume becomes201KB

Finally, here’s the overall flow chart:

If you find it useful, please send us a Star: mrgaogang.github