ReactNative拆包实践

拆包方案

基于metro bundle&Demo方案
https://github.com/smallnew/react-native-multibundler
react-native-multibundler Demo方案的局限性:

  1. 打包后Android文件结构为(IOS类似):
    ├── assets
    │ ├── index.android.bundle 业务包
    │ ├── index2.android.bundle
    │ ├── platform.android.bundle 基础包
    │ └── drawable-xhdpi 图片包
    存在的问题:
  • 一个页面一个bundle,和RN的unbundle(ios 频繁IO读写效率低)类似,只不过稍好
  • 图片包没有按业务分离在各自的业务模块
  • 热修复无法按模块热修复
  • 一个业务N个页面,能否把这N个页面打进一个bundle文件,避免每加载一个新页面读一次文件,做到一个新业务加载一次即可
  1. 针对以上问题,提出打包完成后的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
业务拆包存在的问题
  1. 本地图片加载地址问题,图片加载地址总是以第一次加载的文件夹地址为父路径
    修改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;

你可能感兴趣的:(ReactNative拆包实践)