RN脚手架打包乱码问题定位修复
RN脚手架打包乱码问题定位修复
前言
用车内插(RN项目)打包大概率乱码真是让人仍无可忍, 该项目基于公司内部yrn脚手架, 由于公司内部项目只能我们自己动手了, 最后虽然找到了问题出现的原因, 但是考虑到很多人遇到脚手架的问题就望而却步, 因此blog记录下定位问题的整个过程, 希望能帮助到其他同学.
该过程会从最基本的开始入手, 尽量不漏过每一个步骤, 也可直接前往问题修复查看原因及修复方案.
必要说明
node带调试模式启动方式, 之后不会再特别说明
node --inspect-brk
前缀表示node以debug模式启动- 浏览器输入
chrome://inspect/
- 点击
Remote Target
下的文字按钮进入代码执行窗口
为方便阅读, 文中的$PROJECT_PATH
表示项目根目录, $DISTFILE
表示生成目标文件的目录, 如: RNB0000062-RN062-20220331173816-car_all
问题描述
执行打包命令rnpack -o all -V 62
开始打包, 打包结束后会生成$PROJECT_PATH/dist/native/plugin_id-date-datetime-car_all/
目录, 执行grep � ./ -rl
后有内容输出就是存在乱码, 经过多次查看乱码位置发现如下特性:
- 出现乱码的地方都是中文
- 相同代码多次打包出现乱码的中文总是动态变化的
- 一个中文会生成3个乱码字符
�
- 并不会出现大量乱码, 每次乱码基本都是1-3个字符
对问题的基本分析
基于以上四点问题描述, 我们大致可以确认不是我们本地文件编码导致的, 而是打包过程到生成目标文件过程中哪一步出现了问题导致的
对于任何问题的debug, 不过就两个要点把握住, 问题自然会很方便的定位到, 这两个要点就是程序执行流
、程序数据流
, 只要在过程中跟踪这两个流向, 找到不符合预期结果的第一次出现位置.
问题定位
找到调试命令
- 查看package.json文件, 找到我们执行打包的npm命令对应的实际node命令, 用车项目是
npm run all
对应rnpack -o all -V 62
- 替换该命令为可调试命令
node --inspect-brk ./node_modules/.bin/rnpack -o all -V 62
, 并执行该命令 - 执行该命令后通过浏览器调试窗口单步调试我们可以看到, 执行到
$PROJECT_PATH/node_modules/@yrn/pack/pack.js
中在72行会再次执行一个shell命令, 命令打印出来是cd $PROJECT_PATH/node_modules/@yrn/pack/ && npm run appbundle -- --app --iosBundlePath $PROJECT_PATH/dist/native/$DISTFILE/ios/app.jsbundle --androidBundlePath $PROJECT_PATH/dist/native/$DISTFILE/android/app.jsbundle -P 4040 -p 4041 -w 4042 -s -H 0.0.0.0 --projectRoots $PROJECT_PATH
- 打开
package.json
文件找到scripts中的appbundle为rnws bundle --webpackConfigPath ./src/build/native/webpack.native.config.js
- 与之前的命令结合后, 最终生成调试命令:
node --inspect-brk $PROJECT_PATH/node_modules/.bin/rnws bundle --webpackConfigPath $PROJECT_PATH/node_modules/@yrn/pack/src/build/native/webpack.native.config.js --app --iosBundlePath $PROJECT_PATH/dist/native/$DISTFILE/ios/app.jsbundle --androidBundlePath $PROJECT_PATH/dist/native/$DISTFILE/android/app.jsbundle -P 4040 -p 4041 -w 4042 -s -H 0.0.0.0 --projectRoots $PROJECT_PATH
选择断点
调试命令我们已经找到了, 但是我们不可能打开调试窗口一步一步单步调试, 这是不正确的, 毕竟人脑不是机器, 因此选择关键调试点至关重要, 根据之前提到的两个程序流, 我们这里可以倒叙跟踪执行流, 然后关注数据流中的结果, 已知数据的最终结果是有乱码的, 因此查找问题的方向就变成了有乱码的文件
-> 文本数据写入该文件
-> 文本数据的生成过程
, 生成过程当然包括文本数据的流转
找到命令文件$PROJECT_PATH/node_modules/.bin/rnws
, 添加如下断点:
//create local bundle
commonOptions(program.command('bundle'))
...
.option
.action(function (options) {
...
debugger; // 1. 代码编译执行前
server.start()
.then(() => {
debugger // 2. 已启动服务
return doBundle();
})
.finally(() => {
return; // 阻止程序退出
server.stop();
process.exit(0);
});
});
定位原因
-
要想该调试命令能够正常执行, 还需要修改
$PROJECT_PATH/node_modules/.bin/rnws
文件顶部增加如下代码:process.env.argv = '{"_":[],"ssr":false,"d":false,"dev":false,"cordova":false,"b":false,"branch":false,"build":false,"pb":false,"protobuf":false,"z":false,"zip":false,"l":false,"lib":false,"rv":false,"removeVConsole":false,"h":false,"help":false,"o":"all","os":"all","V":"62","rnModel":"62","m":"","mode":"","s":"","scene":"","p":4040,"port":4040,"c":"","channel":"","ca":"","channelAdd":"","rp":4010,"app port":4010,"$0":"$PROJECT_PATH/node_modules/.bin/rnpack"}'; process.env.zipFileName = '$DISTFILE' process.env.projectpath = '$PROJECT_PATH'
-
手动进到
$PROJECT_PATH/node_modules/@yrn/pack
-
执行调试命令:
node --inspect-brk $PROJECT_PATH/node_modules/.bin/rnws bundle --webpackConfigPath $PROJECT_PATH/node_modules/@yrn/pack/src/build/native/webpack.native.config.js --app --iosBundlePath $PROJECT_PATH/dist/native/$DISTFILE/ios/app.jsbundle --androidBundlePath $PROJECT_PATH/dist/native/$DISTFILE/android/app.jsbundle -P 4040 -p 4041 -w 4042 -s -H 0.0.0.0 --projectRoots $PROJECT_PATH
-
浏览器调试窗口配合命令行打印日志分析程序运行, 我们发现
文本数据的生成过程
在server.start()
代码中完成, 并启动http服务,文本数据写入该文件
在doBundle()
中完成, 由于我们在finally中的回调增加了return语句, 此时由于启动了http服务的原因程序并没有退出, 我们根据之前倒叙追踪执行流的的方案, 从doBundle()
中入手 -
查看doBundle函数, doBundle分别将ios和android执行了一遍createBundle, 猜测就是在此处生成了最终文件, createBundle在
$PROJECT_PATH/node_modules/@yrn/webpack-server/lib/createBundle.js
文件中, 很容易便可以找到代码:[download(buildUrl(server, options, 'bundle'), options.targetPath)]
-
查看download方法代码, 该方法获取文本并写入文件
function download(url, targetPath) { return fetch(url).then(content => { return mkdirp.mkdirpAsync(path.dirname(targetPath)).then(() => fs.writeFileAsync(targetPath, content) ) }) }
-
在download方法打入断点调试发现, 写入ios文件的文本从
http://localhost:4040/index.ios.bundle?platform=android&dev=false&minify=true
中请求得来, 而该请求响应代码在$PROJECT_PATH/node_modules/@yrn/webpack-server/lib/Server.js
中, 可见此处响应的文本从fetch(appCodeURL)
中得到, 断点appCodeURL
的值为http://localhost:4042/index.ios.js
handleBundleRequest(req, res, next) { ... const appCodeURL = this._getAppCodeURL(platform); //create app bundle Promise.props({ reactCode: '', appCode: fetch(appCodeURL), }).then(r => this._createBundleCode(r.reactCode, r.appCode, urlSearch, platform) ).then(bundleCode => { res.set('Content-Type', 'application/javascript'); res.send(bundleCode); }).catch(err => next(err)); }
-
我们浏览器同时打开
http://localhost:4040/index.ios.bundle?platform=android&dev=false&minify=true
和http://localhost:4042/index.ios.js
都能得到源码文本, 其中前者得到的文本最终保存到文件中, 接着我们试验两个链接的乱码情况, 发现后者没有一次乱码, 前者大概率乱码, 且每次请求乱码出现的地方存在变化(也可能没有乱码), 由于我们打包只执行了一次, 因此不存在代码文本变动的情况. 接着我们看两次请求的请求头发现请求头没问题, 那么乱码肯定出现在请求流转中, 我们去一探究竟 -
我们单步进入
fetch(appCodeURL)
中的fetch方法, 该方法在$PROJECT_PATH/node_modules/@yrn/webpack-server/lib/fetch.js
, 代码如下, 该方法用内置的http模块配合url工具模块完成http请求及结果反馈.var http = require('http'); var url = require('url'); function fetch(uri) { return new Promise((resolve, reject) => { var parts = url.parse(uri); var buffer = ''; var handler = res => { res.on('data', chunk => { buffer += chunk; }); res.on('end', () => { if (res.statusCode === 200) { resolve(buffer); } else { reject(buffer); } }); }; var request = http.request(parts, handler); request.setTimeout(0); // Disable any kind of automatic timeout behavior. request.end(); }); }
-
我们发现在
res.on('data', callback);
中回调接收的入参chunk是个Buffer数据类型, 而buffer += chunk
用来将Buffer转换为明文文本, 中文乱码就是在这里产生的
问题修复
原因分析
在utf8编码下一个中文字符要占用3(可在node中输入Buffer('哈').length
查看)个字节, 而网络传输是以二进制流方式分片传输, 每片流大小为64kb, 一般除去头信息一个流默认大小为63.7kb, 实际捕捉是65248字节, 假设这65248字节所传输的文本都是中文, 那么65248 / 3 = 21749.333333333332
也就是说如果将该流转换为字符串将产21749个中文, 剩下一个字节转换失败就会显示成�
, 由于该中文的两位两个字节在下一个流, 因此下一个流解析成字符串头两个一定会是��
, 当字符串连在一起就是我们看到显示的乱码, 这就是乱码出现的原因.
问题修复方案一(setEncoding)
既然是由于数据分片导致的解码问题, 那么我们能遇到别人肯定能遇到, 因此搜索引擎搜了一下, 发现有个setEncoding
的API, 该API会解析buffer, 当碰到流字节解码不全时会将多余的字节放到下一个流的头部, 这样就能保证每次都能正确解码, 修改文件$PROJECT_PATH/node_modules/@yrn/webpack-server/lib/fetch.js
插入res.setEncoding('utf-8')
, 如:
...
var handler = res => {
res.setEncoding('utf-8'); // 插入到该处
res.on('data', chunk => {
...
问题修复方案二(Buffer.concat)
既然分片解码会导致乱码, 那么我们将buffer整合在一起再统一解码也可保证不会乱码, 官网可以找到拼接buffer的方法Buffer.concat
, 修改$PROJECT_PATH/node_modules/@yrn/webpack-server/lib/fetch.js
文件, 具体代码如下:
...
function fetch(uri) {
return new Promise((resolve, reject) => {
var parts = url.parse(uri);
var buffer = []; // 用于缓存每个流的buffer数据
var lens = 0; // 用于统计所有buffer的字节数总和
var handler = res => {
res.on('data', chunk => {
lens += chunk.length;
buffer.push(chunk)
});
res.on('end', () => {
var data = Buffer.concat(data, lens).toString(); // 利用Buffer.concat拼接buffer并转换为字符串
if (res.statusCode === 200) {
resolve(data);
} else {
reject(data);
}
});
};
...
});
}
问题修复方案三(直接拿打包产物)
通过之前的分析, 我们通过访问http://localhost:4040/index.ios.bundle?platform=android&dev=false&minify=true
再间接访问http://localhost:4042/index.ios.js
获取到的代码文本, 那其实可以思考下, 既然已经是打包完后启动的http服务, 那么在该阶段代码其实已经是打包好的, 根本不需要再通过这两次中转, 这当然是可以的, 而且减少了多余代码的执行, 数据是一个整体获取的, 不需要通过网络传输进行分片, 自然乱码的问题也不复存在. 当然这个数据存在哪里, 大家可以自行源码查找, 这里不做过多说明, 建议直接看源码, 官方文档做辅助工具.
在选择断点部分的同一个文件下$PROJECT_PATH/node_modules/.bin/rnws
修改, 该修改适用于一次性生成即打包环境:
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
const mkdirp = Promise.promisifyAll(require('mkdirp'));
const path = require('path');
//create local bundle
commonOptions(program.command('bundle'))
...
.option
.action(function (options) {
const opts = normalizeOptions(options.opts());
const server = createServer(opts);
const bundlePaths = {
android: opts.androidBundlePath,
ios: opts.iosBundlePath,
};
server.start()
.then(() => {
// return doBundle(); 删除doBundle的执行
const contextFs = server.webpackServer.middleware.context.fs;
const outputPath = opts.webpackConfig.output.path;
opts.platforms.forEach((platform) => {
const targetPath = bundlePaths[platform]; // 目标文件输出路径
const contentPath = path.join(outputPath, `index${platform}js`); // webpack打包输出文件路径
const content = contextfs.readFileSync(contentPath).toString(); // 取出对应的buffer数据并转换为字符串
return mkdirp.mkdirpAsync(path.dirname(targetPath)).then(() =>
fs.writeFileAsync(targetPath, content) // 目标文本写入文件
)
});
})
.finally(() => {
server.stop();
process.exit(0);
});
});
问题修复方案四(第三方请求库)
既然是自己封装http模块实现的请求功能, 那么我们当然也可使用第三方库, 如使用axios发起请求, 由于第三方库做了处理, 且经过多时间大范围验证遇到的坑点自然也会少很多
总结
对于此类一旦做好就很少投入维护的项目, 出现问题就得需要我们自己解决, 希望这片文章能够帮助大家再查找脚手架问题时有个头绪
现在的程序开发日新月异, 搭建环境有容器技术, 操作数据库有关系对象模型, 前后端有各种开发框架, 开发工具也五花八门, 写代码很少需要数据结构与算法的今天, 合理的需求下代码编写已经不再成为难事, 这些反而让DEGUG越来越凸显得重要, 调试能力是程序员探索未知的前提!
最后修改插件@yrn/webpack-server
的代码库, 用问题修复方案一提交后修复, over