拆包方案
基于metro bundle&Demo方案
https://github.com/smallnew/react-native-multibundler
react-native-multibundler Demo方案的局限性:
- 打包后Android文件结构为(IOS类似):
├── assets
│ ├── index.android.bundle 业务包
│ ├── index2.android.bundle
│ ├── platform.android.bundle 基础包
│ └── drawable-xhdpi 图片包
存在的问题:
- 一个页面一个bundle,和RN的unbundle(ios 频繁IO读写效率低)类似,只不过稍好
- 图片包没有按业务分离在各自的业务模块
- 热修复无法按模块热修复
- 一个业务N个页面,能否把这N个页面打进一个bundle文件,避免每加载一个新页面读一次文件,做到一个新业务加载一次即可
- 针对以上问题,提出打包完成后的bundle包结构为:
├── assets
│ ├──bundle文件夹
│ │ ├── common
│ │ │ ├── platform.android.bundle 基础包
│ │ │ └── drawable-xhdpi 图片包
│ │ ├── buz1
│ │ │ ├── indexA.android.bundle 业务1中A页面
│ │ │ ├── indexB.android.bundle 业务1中B页面
│ │ │ └── drawable-xhdpi 图片包
│ │ ├── buz2
│ │ │ ├── indexC.android.bundle 业务2中C页面
│ │ │ ├── indexD.android.bundle 业务2中D页面
│ │ │ └── drawable-xhdpi 图片包
改造成上面的结构:
对应代码结构:
├── Rn-project
│ ├──js
│ │ ├── common
│ │ │ ├── xxx.js 基础包
│ │ │ └── images 图片
│ │ │ └── core.js 公共包入口js定义
│ │ ├── buz1
│ │ │ ├── xxx1.js 业务1中A页面
│ │ │ ├── xxx2.js 业务1中B页面
│ │ │ └── images 图片包
│ │ │ └── route.js 业务包入口
│ │ ├── buz2
│ │ │ ├── xxx1.js 业务2中C页面
│ │ │ ├── xxx2.js 业务2中D页面
│ │ │ └── images 图片包
│ │ │ └── route.js 业务包入口
│ ├──moduleConfig.js //模块包配置js
│ ├──index.android.js //总入口
index.android.js文件内容
require('./js/common/core');
require('./moduleConfig');
moduleConfig.js文件内容
/**
* Created by on 2017/1/23.
*/
import React, {
AppRegistry
} from 'react-native';
var RCTLog = require('RCTLog');
let routes = [
require('./js/modules/buz1/route'),
require('./js/modules/buz2/route')
];
core.js 文件内容
// 工具类
import * as utils from './utils/utils';
tnGlobal.utils = utils;
//时间工具类
import * as dateUtils from './utils/dateUtils';
tnGlobal.dateUtils = dateUtils;
// 全局常用变量
import * as config from './tnBase/appConfig';
tnGlobal.config = config;
import openUrls from './OpenUrls';
tnGlobal.openUrls = openUrls;
修改打包命令:
common包命令:
//打包命令 android字样表示android平台,ios平台把android字样替换为IOS
node ./node_modules/react-native/local-cli/cli.js bundle --platform android --dev false --entry-file platformDep.js --bundle-output ./packer_common/android_common/common.bundle --assets-dest ./packer_common/android_common/ --config ${ABSOLUTE}/common.config.js
//拷贝到指定文件夹
cp -R ./packer_common/android_common ./bundles/common
业务包打包
定义route.js
//$1 表示模块文件夹,route.js
routePath="./js/modules/$1/route.js";
mkdir ./bundles/$1
node ./node_modules/react-native/local-cli/cli.js bundle --platform ${PLATFORM} --dev false --entry-file ${routePath} --bundle-output "bundles/$1/$1.bundle" --assets-dest "bundles/$1" --config ${ABSOLUTE}/bundle.config.js
业务拆包存在的问题
- 本地图片加载地址问题,图片加载地址总是以第一次加载的文件夹地址为父路径
修改AssetSourceResolver.js
在npm install 之后,替换
修改后的源码:
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
export type ResolvedAssetSource = {|
+__packager_asset: boolean,
+width: ?number,
+height: ?number,
+uri: string,
+scale: number,
|};
import type {PackagerAsset} from 'AssetRegistry';
const PixelRatio = require('PixelRatio');
const Platform = require('Platform');
const assetPathUtils = require('./assetPathUtils');
const invariant = require('invariant');
/**
* Returns a path like 'assets/AwesomeModule/[email protected]'
*/
function getScaledAssetPath(asset): string {
const scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
const assetDir = assetPathUtils.getBasePath(asset);
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}
function getScaledIOSAssetPath(asset): string {
const scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
var assetDir = assetPathUtils.getBasePath(asset);
var index=assetDir.indexOf("js/modules/");
if(index > 0){
var assetAbsoult =assetDir.substring(index+11,assetDir.length);
var modules = assetAbsoult.split('/')
if(modules && modules.length > 0){
var module = modules[0];
return '../' + module + '/' + assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}
}
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}
/**
* Returns a path like 'drawable-mdpi/icon.png'
*/
function getAssetPathInDrawableFolder(asset): string {
const scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
const drawbleFolder = assetPathUtils.getAndroidResourceFolderName(
asset,
scale,
);
const fileName = assetPathUtils.getAndroidResourceIdentifier(asset);
return drawbleFolder + '/' + fileName + '.' + asset.type;
}
class AssetSourceResolver {
serverUrl: ?string;
// where the jsbundle is being run from
jsbundleUrl: ?string;
// the asset to resolve
asset: PackagerAsset;
constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) {
this.serverUrl = serverUrl;
this.jsbundleUrl = jsbundleUrl;
this.asset = asset;
}
isLoadedFromServer(): boolean {
return !!this.serverUrl;
}
isLoadedFromFileSystem(): boolean {
return !!(this.jsbundleUrl && this.jsbundleUrl.startsWith('file://'));
}
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle()
: this.resourceIdentifierWithoutScale();
} else {
return this.scaledAssetURLNearBundle();
}
}
/**
* Returns an absolute URL which can be used to fetch the asset
* from the devserver
*/
assetServerURL(): ResolvedAssetSource {
invariant(!!this.serverUrl, 'need server to load from');
return this.fromSource(
this.serverUrl +
getScaledAssetPath(this.asset) +
'?platform=' +
Platform.OS +
'&hash=' +
this.asset.hash,
);
}
/**
* Resolves to just the scaled asset filename
* E.g. 'assets/AwesomeModule/[email protected]'
*/
scaledAssetPath(): ResolvedAssetSource {
return this.fromSource(getScaledAssetPath(this.asset));
}
/**
* Resolves to where the bundle is running from, with a scaled asset filename
* E.g. 'file:///sdcard/bundle/assets/AwesomeModule/[email protected]'
*/
scaledAssetURLNearBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl || 'file://';
var asset = getScaledIOSAssetPath(this.asset);
return this.fromSource(path + asset);
}
/**
* The default location of assets bundled with the app, located by
* resource identifier
* The Android resource system picks the correct scale.
* E.g. 'assets_awesomemodule_icon'
*/
resourceIdentifierWithoutScale(): ResolvedAssetSource {
invariant(
Platform.OS === 'android',
'resource identifiers work on Android',
);
return this.fromSource(
assetPathUtils.getAndroidResourceIdentifier(this.asset),
);
}
/**
* If the jsbundle is running from a sideload location, this resolves assets
* relative to its location
* E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
*/
drawableFolderInBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl || 'file://';
var fileName = getAssetPathInDrawableFolder(this.asset);
//npm中图片,common包图片,路径更换
if((fileName.indexOf("node_modules_") > 0 || fileName.indexOf("js_common_images_") > 0) && path.length > 0){
var dirs = path.split('/');
dirs[dirs.length - 2] = 'common';
var temp = dirs.join('/');
return this.fromSource(temp + fileName);
}
//模块间图片
var index= fileName.indexOf("js_modules_");
if(index > 0){
var assetAbsoult = fileName.substring(index + 11,fileName.length);
var modules = assetAbsoult.split('_');
var moduleReplace = path.split('/');
if(modules && modules.length > 0 && moduleReplace.length > 2){
moduleReplace[moduleReplace.length - 2] = modules[0];
return this.fromSource(moduleReplace.join('/') + fileName);
}
}
return this.fromSource(path + fileName);
}
fromSource(source: string): ResolvedAssetSource {
return {
__packager_asset: true,
width: this.asset.width,
height: this.asset.height,
uri: source,
scale: AssetSourceResolver.pickScale(this.asset.scales, PixelRatio.get()),
};
}
static pickScale(scales: Array, deviceScale: number): number {
// Packager guarantees that `scales` array is sorted
for (let i = 0; i < scales.length; i++) {
if (scales[i] >= deviceScale) {
return scales[i];
}
}
// If nothing matches, device scale is larger than any available
// scales, so we return the biggest one. Unless the array is empty,
// in which case we default to 1
return scales[scales.length - 1] || 1;
}
}
module.exports = AssetSourceResolver;