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 AB
The runtime
The code logic is split- package
- Pull cgi to get the experimental configuration
- Following the code logic, different experiments load different components
-
Scheme 2. Experiment AB
Build time
Split code logic (one build)- Read experimental configuration
- Build all experimental possibilities and unpack
- Loading according to different experiments read different
jsbundle
/js
-
Scheme 3. Experiment AB
Build time
Split code logic (multiple builds)- Read experimental configuration
- Babel replaces Module, builds all experimental possibilities, and unpacks
- Loading according to different experiments read different
jsbundle
/js
-
, etc.
Reviewing the two options above, the first option has some shortcomings:
- All the experiments are packaged together, which is bound to make the whole
js
/jsbundle
They become huge; - Logical splitting in code is more like writing a normal page, where experiments and hosts cannot be decoupled
- Compared to scenario 3, multiple can be generated in a single build
bundle
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:
- Users can load it on demand
jsbundle
/js
To optimize load time and JsBundle load time without full load. - 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:
- using
jest-haste-map
generatemodule
Corresponding dependencies - read
abconfig.json
Combine all experimental possibilities - right
modules
Data conversion and filtering - Global minimum dependence analysis is performed from the entry for the specified AB experiment
- combination
bundle
generate
#1. The use ofjest-haste-map
generatemodule
Corresponding dependencies
Jsbundle dependency generation takes the following steps:
- use
jest-haste-map
formodule
The dependence between; - using
babel
Code escape - Customize at the same time
createModuleIdFactory
generatemoduleId
In 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.json
Combine 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. Tomodules
Data 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.bundle
generate
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