目錄:
  1. RN脚手架打包乱码问题定位修复
    1. 前言
      1. 必要说明
        1. 问题描述
          1. 对问题的基本分析
            1. 问题定位
              1. 找到调试命令
              2. 选择断点
              3. 定位原因
            2. 问题修复
              1. 原因分析
              2. 问题修复方案一(setEncoding)
              3. 问题修复方案二(Buffer.concat)
              4. 问题修复方案三(直接拿打包产物)
              5. 问题修复方案四(第三方请求库)
            3. 总结

            RN脚手架打包乱码问题定位修复

            閱讀時間:全文 2823 字,預估用時 15 分鐘
            創作日期:2022-04-07
            文章標籤:
            上篇文章:lua基本语法
             
            BEGIN

            RN脚手架打包乱码问题定位修复

            前言

            用车内插(RN项目)打包大概率乱码真是让人仍无可忍, 该项目基于公司内部yrn脚手架, 由于公司内部项目只能我们自己动手了, 最后虽然找到了问题出现的原因, 但是考虑到很多人遇到脚手架的问题就望而却步, 因此blog记录下定位问题的整个过程, 希望能帮助到其他同学.

            该过程会从最基本的开始入手, 尽量不漏过每一个步骤, 也可直接前往问题修复查看原因及修复方案.

            必要说明

            node带调试模式启动方式, 之后不会再特别说明

            1. node --inspect-brk前缀表示node以debug模式启动
            2. 浏览器输入chrome://inspect/
            3. 点击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后有内容输出就是存在乱码, 经过多次查看乱码位置发现如下特性:

            1. 出现乱码的地方都是中文
            2. 相同代码多次打包出现乱码的中文总是动态变化的
            3. 一个中文会生成3个乱码字符
            4. 并不会出现大量乱码, 每次乱码基本都是1-3个字符

            对问题的基本分析

            基于以上四点问题描述, 我们大致可以确认不是我们本地文件编码导致的, 而是打包过程到生成目标文件过程中哪一步出现了问题导致的

            对于任何问题的debug, 不过就两个要点把握住, 问题自然会很方便的定位到, 这两个要点就是程序执行流程序数据流, 只要在过程中跟踪这两个流向, 找到不符合预期结果的第一次出现位置.

            问题定位

            找到调试命令

            1. 查看package.json文件, 找到我们执行打包的npm命令对应的实际node命令, 用车项目是npm run all对应rnpack -o all -V 62
            2. 替换该命令为可调试命令node --inspect-brk ./node_modules/.bin/rnpack -o all -V 62, 并执行该命令
            3. 执行该命令后通过浏览器调试窗口单步调试我们可以看到, 执行到$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
            4. 打开package.json文件找到scripts中的appbundle为rnws bundle --webpackConfigPath ./src/build/native/webpack.native.config.js
            5. 与之前的命令结合后, 最终生成调试命令: 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);
                        });
                });

            定位原因

            1. 要想该调试命令能够正常执行, 还需要修改$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'
            2. 手动进到$PROJECT_PATH/node_modules/@yrn/pack

            3. 执行调试命令: 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

            4. 浏览器调试窗口配合命令行打印日志分析程序运行, 我们发现文本数据的生成过程server.start()代码中完成, 并启动http服务, 文本数据写入该文件doBundle()中完成, 由于我们在finally中的回调增加了return语句, 此时由于启动了http服务的原因程序并没有退出, 我们根据之前倒叙追踪执行流的的方案, 从doBundle()中入手

            5. 查看doBundle函数, doBundle分别将ios和android执行了一遍createBundle, 猜测就是在此处生成了最终文件, createBundle在$PROJECT_PATH/node_modules/@yrn/webpack-server/lib/createBundle.js文件中, 很容易便可以找到代码: [download(buildUrl(server, options, 'bundle'), options.targetPath)]

            6. 查看download方法代码, 该方法获取文本并写入文件

              function download(url, targetPath) {
                  return fetch(url).then(content => {
                      return mkdirp.mkdirpAsync(path.dirname(targetPath)).then(() =>
                          fs.writeFileAsync(targetPath, content)
                      )
                  })
              }
            7. 在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));
              }
            8. 我们浏览器同时打开http://localhost:4040/index.ios.bundle?platform=android&dev=false&minify=truehttp://localhost:4042/index.ios.js都能得到源码文本, 其中前者得到的文本最终保存到文件中, 接着我们试验两个链接的乱码情况, 发现后者没有一次乱码, 前者大概率乱码, 且每次请求乱码出现的地方存在变化(也可能没有乱码), 由于我们打包只执行了一次, 因此不存在代码文本变动的情况. 接着我们看两次请求的请求头发现请求头没问题, 那么乱码肯定出现在请求流转中, 我们去一探究竟

            9. 我们单步进入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();
                  });
              }
            10. 我们发现在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

            FINISH
            上篇文章:lua基本语法

            隨機文章
            人生倒計時
            default