原生小程序重构为uniapp踩坑实录

2,255 阅读6分钟

为什么需要uniapp这个东西

1.1 小程序的前世

小程序这个东西最早可以追溯到混合应用,在一个原生宿主程序的webview里,网页应用可以通过js bridge sdk来调用宿主程序的功能。这些功能是操作系统赋予给原生应用的能力,比如

  • 调用摄像头扫码

  • 调用录音功能

  • 调用支付功功能等

这些网页应用反而在浏览器里无法正常运行,通常需要通过宿主程序扫描的方式来使用,比如之前的各种共享单车在微信和支付宝中版本,自动售货机通过支付宝和微信付款的应用等,都是这类应用。

这些应用通过js bridge,获取了比普通web应用更强的能力。但是在显示这个层面上,这些应用一样有着web 应用的通病:

  • 加载慢

  • 首次加载有白屏时间

  • 用户交互反馈不好

  • 页面切换比较生硬等

在此基础上,有人就想到了,为什么我们不能更进一步呢?既然能使用原生的功能,为什么不能使用原生的视觉效果?

1.2 小程序的架构

小程序把视图层和逻辑层分开,魔改了webkit和chromium的渲染引擎,将web组件映射成原生组件,提高的视觉质感。同时参考了一些市面上成熟的MVVM框架,通过setData将视图层和逻辑层联系起来,减少开发负担。 根据微信的开发文档,各个平台的小程序实现如下:

运行环境 逻辑层 渲染层
iOS JavaScriptCore WKWebView
安卓 V8 chromium定制内核
小程序开发者工具 NWJS Chrome WebView

由于各平台实现不同,兼容性问题也是经常有的(并不能逃过)。

image.png

小程序的这种架构设计比web有两个好处:

  • 逻辑层和渲染层相互分离,逻辑层的代码无法阻塞渲染层,使得小程序的动画效果要比web应用更顺滑

  • jsCore中的请求要经过Native后端发出,实际上Native后端作为代理,可以避免跨域的问题,当然还免不了被Native收集数据。。

1.3 uniapp在哪里

image.png

uniapp或者其他小程序的框架主要还是要解决jsCore逻辑复杂的问题,通过模拟vue或者react的api来降低开发者的逻辑负担。

2. 重构原因

这次要重构的小程序是当时部门第一个小程序,因为历史遗留的原因,选择了原生小程序作为开发框架。原生小程序的开发框架有诸多限制,比如:

  1. 不是npm项目,无法进行依赖的版本管理;

  2. 编码方式相当返祖,大约相当于web开发刚进入AngularJS时代;

  3. 仅支持ES6(而且还没有generator运行时)

  4. 不支持预处理,或者说支持预处理的成本非常高(需要手动配置webpack,babel(tsc),css预处理器等等)

  5. 不支持css动画

项目经过了9个月时间的开发迭代,受限于当时开发同学的技术水平,其代码组织、代码结构相当的混乱,同时有大量的重复功能的代码(比如有4个页面,页面上均只有一个webview,仅url不同。),本身这些代码也必须重构。 随着新的项目规划的出台,未来该应用将成为包含多个复杂功能的综合平台,其功能复杂度较之前的功能直线上升,需要一个强有力可维护的框架来保证迭代质量。

2. 重构思路

  1. 首先将头条小程序转成微信小程序(借助于自己编写的脚本)

  2. 借助miniprogram-to-uniapp(zhangdaren编写),将小程序不完美转化为uni-app

  3. 解决uniapp版本的编译和运行报错

  4. 将公司的一些物料库(如埋点插件)从原生版本换成npm版本

3. 具体步骤

  1. 全局替换tt-if,tt-for等关键字为tt:if tt:for(这是隐藏的坑,据说最早的时候头条小程序支持tt-if这种写法,目前文档中已经删除这种写法)

  2. 将tt2wx.js放在项目根目录下,然后执行,它会将项目里所有ttml ttss重命名为wxml和wxss,还会将ttml里的模版语言tt:if等转成微信小程序对应的模板语言

const fs = require("fs");
const path = require("path");
const currPath = process.argv[2];

let ttmlFiles = [];
let ttssFiles = [];

function getFiles(currPath) {
  let filesInfo = fs.readdirSync(path.join(__dirname, currPath));
  filesInfo.forEach(f => {
    let fpath = path.join(currPath, f);
    let stat = fs.statSync(fpath);

    if (stat.isDirectory()) {
      getFiles(fpath);
    }

    if (stat.isFile() && fpath.indexOf(".ttml") > -1) {
      ttmlFiles.push(fpath);
    }

    if (stat.isFile() && fpath.indexOf(".ttss") > -1) {
      ttssFiles.push(fpath);
    }
  });
}

getFiles(currPath);
const ttForRe = /tt:for/g;
const ttIfRe = /tt:if/g;
const ttElseIfRe = /tt:elif/g;
const ttElseRe = /tt:else/g; // 替换tt:if tt:for

ttmlFiles.forEach(ttml => {
  let content = fs.readFileSync(ttml, "utf-8");
  let wxCode = content.replace(ttForRe, "wx:for").replace(ttIfRe, "wx:if").replace(ttElseIfRe, "wx:elif").replace(ttElseRe, "wx:else");
  fs.writeFileSync(ttml, wxCode);
}); // 重命名

ttmlFiles.forEach(ttml => {
  let index = ttml.indexOf(".ttml");
  let newName = ttml.substring(0, index) + ".wxml";
  fs.renameSync(ttml, newName);
});
ttssFiles.forEach(ttss => {
  let index = ttss.indexOf(".ttss");
  let newName = ttss.substring(0, index) + ".wxss";
  fs.renameSync(ttss, newName);
});
node tt2wx.js ./
  1. 安装miniprogram-to-uniapp
npm install miniprogram-to-uniapp -g
  1. 不完美转化为uni-app
wtu -i [projectPath]
  1. 此时转换的项目还没有npm项目模版,可以使用uniapp官方推荐的模版,也可以使用其他部门成熟项目的模板。我们使用的是uniapp官方模版的删减版+部门的代码规则包。

  2. 将第四步得到的代码文件复制到第五步模板的src目录下,然后执行

npm install
npm run start

会得到一大堆编译错误,修复它!比如在一个ttss文件中import了另外一个ttss文件,由于现在文件名变成了css,就会编译错误;再比如ttml文件写个属性绑定,如果太长换行了,转成.vue文件之后也会报错;

  1. 解决所有编译错误之后,用飞书开发者工具打开构建目录,默认是dist/dev/mp-toutiao/

  2. 先去掉原生小程序的一些公司物料库的配置,否则会报错:

  3. 原生框架事件中需要e.detail才能拿到值 ,在uniapp中略有不同。内置组件的事件参数与原生保持一致,但自定义组件中通过$emit()抛出的参数,就是event本身,不需要通过event.detail获取

image.png

image.png

  1. 原生框架中,可以使用和html元素同名的自定义组件,如<header> <loading>,但在uniapp无法正常的使用,需要将组件改名。 建议任何自定义组件都不要只使用一个单词

  2. 在原生框架中,data中的属性名可以是$开头的,比如$t,但在uniapp中不行,得换个名字,建议全局替换, 这个是vue的限制

  3. 有些图片的引用没有正确的转移到新的路径,需要全局搜索下,手动检查

  4. 在原生框架中,组件中定义的props可以在内部修改,并且能影响外部跟它绑定的属性;换成uniapp之后,props不能在组件内部修改,建议换成.sync的方式保持双向绑定

  5. 部分组件的动画失效,可以换成css动画

  6. 原生的小程序style天生scoped ,转换成uniapp之后,需要手动添加scoped

  7. 跨页面传值 :在原生中可以获取之前的page实例,然后通过prePage.setData的方式进行;在uniapp中,通过这种方式数据确实已经在page.data对象上,但由于绕过了vue,vue不知道数据变化了,所以在onShow()里应该再赋值一遍。 更好的方式是迁移到vuex中

image.png

加入我们

欢迎加入我们,和优秀的人做有挑战的事,简历发送至邮箱:zhouhaolei@bytedance.com(备注姓名+岗位+城市)