metro-bundler react-native专用的打包工具
github地址
- react-native的模块系统遵循的是AMD模式,打包后会把AMD模块代码一起打包进去,在metro-bundler/src/defaults.js中
exports.moduleSystem = require.resolve('./Resolver/polyfills/require.js');
- 所以可以通过修改这个指定自己的AMD加载模块。
在自定义的cli.js中,修改成自定义的require.js
require("metro-bundler/src/defaults").moduleSystem=require.resolve("./require.js");
require加载流程
- 首先注册了require和__d成全局方法,以及modules对象用来缓存模块
global.require = require;
global.__d = define;
const modules = Object.create(null);
//加入这段 方便以后动态获取或者修改已经加载好的模块
global.__modules=modules;
1)define方法
factory参数 是工厂类,用来构造对象
moduleId参数 是模块的Id
function define(
factory,
moduleId)
{
if (moduleId in modules) {
// prevent repeated calls to `global.nativeRequire` to overwrite modules
// that are already loaded
return;
}
modules[moduleId] = {
dependencyMap,
exports: undefined,
factory,
hasError: false,
isInitialized: false };
}
- require方法
* 只有一个moduleId参数,如果module存在且已经初始化 直接返回module的exports对象,如果没有则调用
guardedLoadModule 去初始化module。
function require(moduleId) {
//$FlowFixMe: at this point we know that moduleId is a number
const moduleIdReallyIsNumber = moduleId;
const module = modules[moduleIdReallyIsNumber];
return module && module.isInitialized ? module.exports : guardedLoadModule(moduleIdReallyIsNumber, module);
}
- reactNative处于调试下,global对象会有ErrorUtils对象,用于展示错误,发布模式则没有。实际调用了loadModuleImplementation方法去实现模块加载。
let inGuard = false;
function guardedLoadModule(moduleId, module) {
if (!inGuard && global.ErrorUtils) {
inGuard = true;
let returnValue;
try {
returnValue = global.loadModuleImplementation(moduleId, module);
} catch (e) {
global.ErrorUtils.reportFatalError(e);
}
inGuard = false;
return returnValue;
} else {
return global.loadModuleImplementation(moduleId, module);
}
}
- 如果module不存在 就表示define方法没有被调用,这样可以通过nativeRequire去加载模块的js文件,nativeRequire方法是c++实现的,只会加载assets/js-modules下的js文件。我们只要实现类似的c++方法就可以加载任意目录的js文件,并且可以加密和解密。
nativeRequire调用过后。模块就会在modules对象中存在,调用module的factory,传入参数global, require, moduleObject, exports, dependencyMap,这里的参数与node类似,之后返回exports对象。
const ID_MASK_SHIFT = 16;
const LOCAL_ID_MASK = ~0 >>> ID_MASK_SHIFT;
global.loadModuleImplementation = function (moduleId, module) {
const nativeRequire = global.nativeRequire;
if (!module && nativeRequire) {
if (isNaN(moduleId)) {
nativeRequire(moduleId);
} else {
const bundleId = moduleId >>> ID_MASK_SHIFT;
const localId = moduleId & LOCAL_ID_MASK;
nativeRequire(localId, bundleId);
}
module = modules[moduleId];
}
if (!module) {
throw unknownModuleError(moduleId);
}
if (module.hasError) {
throw moduleThrewError(moduleId, module.error);
}
// We must optimistically mark module as initialized before running the
// factory to keep any require cycles inside the factory from causing an
// infinite require loop.
module.isInitialized = true;
const exports = module.exports = {};
var _module =
module;
const factory = _module.factory, dependencyMap = _module.dependencyMap;
try {
const moduleObject = {exports};
// keep args in sync with with defineModuleCode in
// metro-bundler/src/Resolver/index.js
// and metro-bundler/src/ModuleGraph/worker.js
factory(global, require, moduleObject, exports, dependencyMap);
// avoid removing factory in DEV mode as it breaks HMR
if (!__DEV__) {
// $FlowFixMe: This is only sound because we never access `factory` again
module.factory = undefined;
module.dependencyMap = undefined;
}
return module.exports = moduleObject.exports;
} catch (e) {
module.hasError = true;
module.error = e;
module.isInitialized = false;
module.exports = undefined;
throw e;
}
}
更改react-native的C++代码
- 在react-native/ReactCommon/cxxreact下JSCExcutor.cpp文件中
这个方法是在JS启动的时候会被调用,里面定义了一些全局方法,这里加入了evalFile方法
void JSCExecutor::initOnJSVMThread() throw(JSException) {
····
installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>("nativeFlushQueueImmediate");
installNativeHook<&JSCExecutor::nativeCallSyncHook>("nativeCallSyncHook");
installNativeHook<&JSCExecutor::evalFile>("evalFile");
installGlobalFunction(m_context, "nativeLoggingHook", JSCNativeHooks::loggingHook);
installGlobalFunction(m_context, "nativePerformanceNow", JSCNativeHooks::nowHook);
····
}
- c++实现的js方法 有两个参数,第一个参数argumentCount表示参数个数,arguments[]对应传入的参数,
这里先判断了参数必须是一个且是字符串,否则抛出异常,然后检查第一个参数对应的文件是否存在,之后读取文件,然后调用evaluateScript执行文件,达到执行任意js文件的目的。
JSValueRef JSCExecutor::evalFile(size_t argumentCount,const JSValueRef arguments[]) {
if (argumentCount < 1 ) {
throw std::invalid_argument("Got wrong number of args");
}
Value value=Value(m_context, arguments[0]);
if(!value.isString()){
throw std::invalid_argument("Got wrong args[0] type");
}
FILE* fp=fopen(value.toString().str().c_str(),"rb");
if(fp==NULL){
throw std::invalid_argument(folly::to("file not exists,path is ",value.toString().str()));
}
fseek(fp, 0, SEEK_END);
int dwSize=ftell(fp);
char* pBytes = new char[dwSize];
fseek(fp, 0, SEEK_SET);
fread(pBytes, dwSize, 1, fp);
fclose(fp);
evaluateScript(m_context,String(m_context,(char*)pBytes),value.toString());
delete pBytes;
return Value::makeUndefined(m_context);
}
- 接下来就是需要通过java传入js文件的初始目录,通过ReactContextBaseJavaModule的getConstants方法,可以把初始目录的字符串传递到js中。
public class NativeUtil extends ReactContextBaseJavaModule {
private static final String LOCAL_PATH = "LOCAL_PATH";
@Nullable
@Override
public Map getConstants() {
final Map constants = new HashMap<>();
constants.put(LOCAL_PATH, getCurrentActivity().getFilesDir().getAbsolutePath());
return constants;
}
}
- js中修改global.loadModuleImplementation ,这样就改变了require流程,如果模块不存在,会去指定目录加载对应{moduleId}.js文件
global._BASE_PATH=NativeUtil.LOCAL_PATH+"/pack/js-modules";
const old=global.loadModuleImplementation;
global.loadModuleImplementation=function(moduleId, module){
if (!module && evalFile) {
evalFile(global._BASE_PATH+"/"+moduleId+".js");
module = global.__modules[moduleId];
}
return old(moduleId, module)
}
打包流程
- unbundle命令会将入口js文件依赖的所有js打包到--bundle-output对应目录下,每个模块会被分配一个id,且放到--bundle-output的js-modules文件夹下
node cli.js unbundle --entry-file index.android.js --platform android --dev false --bundle-output build/main.bundle --assets-dest build
每个模块的id是根据metro-bundler/src/lib/createModuleIdFactory.js生成的,修改这个文件就可以生成指定id,在cli.js下增加如下代码 改变moduleId生成的逻辑。
require("metro-bundler/src/lib/createModuleIdFactory.js");
ModuleCache[require.resolve("metro-bundler/src/lib/createModuleIdFactory.js")].exports=require("./createModuleIdFactory");
将src下的业务js的id从10000开始,而其他模块的id从0递增开始,这样就很容易将业务模块和固定模块进行拆分。
module.exports = function(){
const fileToIdMap = new Map();
let nextId = 0;
let busId=0;
return (_ref) => {
let modulePath = _ref.path;
// modulePath=modulePath.replace(basePath,"").replace(/\\|-/g,"_").replace(/\.js$/,"");
modulePath=modulePath.replace(basePath,"");
if(!modulePath.startsWith("src")){
if (!fileToIdMap.has(modulePath)) {
fileToIdMap.set(modulePath, nextId);
nextId += 1;
}
return fileToIdMap.get(modulePath);
}else{
if (!fileToIdMap.has(modulePath)) {
fileToIdMap.set(modulePath, 10000+busId);
busId += 1;
}
return fileToIdMap.get(modulePath);
}
};
};
文件加密以及解密
- 由于拆分 导致js很容易被分析识别所以需要加密存储。又由于是打包用的js,所以需要用js加密然后用c++解密。这里提供了利用了异或达到目的。
js加密流程
let fs = require("fs");
const { Transform } = require('stream');
class EncodeTrans extends Transform {
constructor(options={}) {
super(options);
this.code1=Buffer.from(options.key1||'fstat on bundle failed.', 'utf8');
this.code2 = Buffer.from(options.key2||"nativeRequire", "utf8")
this.code1Index = 0
this.code2Index = 0
}
_transform(buf, enc, next) {
let start=0;
while (start < buf.length) {
const uint = buf.readUInt8(start);
const key1 = this.code1.readUInt8(this.code1Index++);
const key2 = this.code2.readUInt8(this.code2Index++);
(this.code1Index >= this.code1.length)&&(this.code1Index=0);
(this.code2Index >= this.code2.length)&&(this.code2Index=0);
buf.writeUInt8(uint ^ key1 ^ key2,start);
start++;
}
next(null, buf);
}
}
fs.createReadStream("build/js-modules/10.js")
.pipe(new EncodeTrans())
.pipe(fs.createWriteStream("10.js"));
c++ 解密流程
char* decodeByPass(const char* code1, const char* code2, byte* content, int contentLength) {
byte* p = content;
char* c = new char[contentLength+1];
int code1Len = strnlen(code1,1000);
int code2Len = strnlen(code2,1000);
int code1Index = 0;
int code2Index = 0;
char* startC = c;
while (p- content= code2Len){
code2Index = 0;
}
if (code1Index >= code1Len) {
code1Index = 0;
}
}
*startC = '\0';
return c;
}
int main() {
FILE* fp;
fopen_s(&fp, "10.js", "rb");
fseek(fp, 0, SEEK_END);
int dwSize = ftell(fp);
byte* pBytes = new byte[dwSize];
fseek(fp, 0, SEEK_SET);
fread(pBytes, dwSize, 1, fp);
fclose(fp);
char* pwd = decodeByPass("fstat on bundle failed.","nativeRequire", pBytes, dwSize);
printf("%s\n", pwd);
system("pause");
return 0;
}