为什么需要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 |
由于各平台实现不同,兼容性问题也是经常有的(并不能逃过)。

小程序的这种架构设计比web有两个好处:
-
逻辑层和渲染层相互分离,逻辑层的代码无法阻塞渲染层,使得小程序的动画效果要比web应用更顺滑
-
jsCore中的请求要经过Native后端发出,实际上Native后端作为代理,可以避免跨域的问题,当然还免不了被Native收集数据。。
1.3 uniapp在哪里

uniapp或者其他小程序的框架主要还是要解决jsCore逻辑复杂的问题,通过模拟vue或者react的api来降低开发者的逻辑负担。
2. 重构原因
这次要重构的小程序是当时部门第一个小程序,因为历史遗留的原因,选择了原生小程序作为开发框架。原生小程序的开发框架有诸多限制,比如:
-
不是npm项目,无法进行依赖的版本管理;
-
编码方式相当返祖,大约相当于web开发刚进入AngularJS时代;
-
仅支持ES6(而且还没有generator运行时)
-
不支持预处理,或者说支持预处理的成本非常高(需要手动配置webpack,babel(tsc),css预处理器等等)
-
不支持css动画
项目经过了9个月时间的开发迭代,受限于当时开发同学的技术水平,其代码组织、代码结构相当的混乱,同时有大量的重复功能的代码(比如有4个页面,页面上均只有一个webview,仅url不同。),本身这些代码也必须重构。 随着新的项目规划的出台,未来该应用将成为包含多个复杂功能的综合平台,其功能复杂度较之前的功能直线上升,需要一个强有力可维护的框架来保证迭代质量。
2. 重构思路
-
首先将头条小程序转成微信小程序(借助于自己编写的脚本)
-
借助miniprogram-to-uniapp(zhangdaren编写),将小程序不完美转化为uni-app
-
解决uniapp版本的编译和运行报错
-
将公司的一些物料库(如埋点插件)从原生版本换成npm版本
3. 具体步骤
-
全局替换tt-if,tt-for等关键字为tt:if tt:for(这是隐藏的坑,据说最早的时候头条小程序支持tt-if这种写法,目前文档中已经删除这种写法)
-
将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 ./
- 安装miniprogram-to-uniapp
npm install miniprogram-to-uniapp -g
- 不完美转化为uni-app
wtu -i [projectPath]
-
此时转换的项目还没有npm项目模版,可以使用uniapp官方推荐的模版,也可以使用其他部门成熟项目的模板。我们使用的是uniapp官方模版的删减版+部门的代码规则包。
-
将第四步得到的代码文件复制到第五步模板的src目录下,然后执行
npm install
npm run start
会得到一大堆编译错误,修复它!比如在一个ttss文件中import了另外一个ttss文件,由于现在文件名变成了css,就会编译错误;再比如ttml文件写个属性绑定,如果太长换行了,转成.vue文件之后也会报错;
-
解决所有编译错误之后,用飞书开发者工具打开构建目录,默认是dist/dev/mp-toutiao/
-
先去掉原生小程序的一些公司物料库的配置,否则会报错:
-
原生框架事件中需要e.detail才能拿到值 ,在uniapp中略有不同。内置组件的事件参数与原生保持一致,但自定义组件中通过$emit()抛出的参数,就是event本身,不需要通过event.detail获取


-
原生框架中,可以使用和html元素同名的自定义组件,如<header> <loading>,但在uniapp无法正常的使用,需要将组件改名。 建议任何自定义组件都不要只使用一个单词 。
-
在原生框架中,data中的属性名可以是$开头的,比如$t,但在uniapp中不行,得换个名字,建议全局替换, 这个是vue的限制 。
-
有些图片的引用没有正确的转移到新的路径,需要全局搜索下,手动检查
-
在原生框架中,组件中定义的props可以在内部修改,并且能影响外部跟它绑定的属性;换成uniapp之后,props不能在组件内部修改,建议换成.sync的方式保持双向绑定
-
部分组件的动画失效,可以换成css动画
-
原生的小程序style天生scoped ,转换成uniapp之后,需要手动添加scoped
-
跨页面传值 :在原生中可以获取之前的page实例,然后通过prePage.setData的方式进行;在uniapp中,通过这种方式数据确实已经在page.data对象上,但由于绕过了vue,vue不知道数据变化了,所以在onShow()里应该再赋值一遍。 更好的方式是迁移到vuex中 。

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