前期爝神大大
@小爝 已经对 https://github.com/Youjingyu/vue-hap-tools 做了简单介绍, 参考使用 vue 编写快应用解决方案 https://zhuanlan.zhihu.com/p/35830598 . 现在这篇文章主要说说实现思路, 也算总结一下, 如果有实现得不合理的地方, 欢迎大家指正.
要让 vue 代码运行在快应用平台, 一种实现思路是像 https://github.com/Meituan-Dianping/mpvue 一样, js 部分使用 vue.runtime 接管, 模板部分直接转换, 另一种思路是直接将 vue 的语法转换为快应用的语法, js 部分添加少量 hack 代码. 由于快应用与 vue 语法本身就比较接近, 因此我们选择了成本相对较低的第二种实现方式. 当然, 可能还有更底层的实现方式, 还望知道的大佬指点指点.
快应用打包过程
在开始之前, 我们需要了解快应用官方脚手架 hap-toolkit https://doc.quickapp.cn/tutorial/getting-started/build-environment.html 的打包过程. 整体来说, hap-toolkit 基于 webpack 的多入口打包, 最终在用户端, 快应用解析执行的是 webpack 的打包结果. hap-toolkit 首先会解析出应用的主入口和各个页面入口, 然后把这些入口作为 webpack 的 entry. 比如下面的例子:
- // Webpack 配置
- const webpackConf = {
- entry: {
- 'app.js': '/path-to-project/src/app.ux',
- 'page1/index.js': '/path-to-project/src/pages/page1/index.ux',
- 'page2/index.js': '/path-to-project/src/pages/page2/index.ux'
- },
- rules: [
- {
- test: /\.ux$/,
- use: ['hap-toolkit-loader']
- }
- // 其他 loader
- ]
- // 其他配置
- ...
- }
并且配置 webpack 的 rules, 使 ux 后缀的文件 (对应于. vue 文件) 都会进入 hap-toolkit-loader. 看到这里, 我们本以为只需要在 hap-toolkit-loader 之前加一层我们的 loader, 在编译之前先进行我们的转换逻辑就行了, 比如下面这样, 但实际却行不通.
- rules: [
- {
- test: /\.ux$/,
- use: ['hap-toolkit-loader', 'vue-hap-tools-loader']
- }
- // 其他 loader
- ]
实际上, hap-toolkit-loader 内部并没有直接编译 ux 文件, 而是根据 ux 文件内容生成一个 js 文件返回给 webpack(步骤一), 这个 js 文件说明了 component,template,style,script 的编译方式, 比如下面的形式:
- // 最终编译时, 源文件会在 require 中的 loader 之间流转
- // 注意, require 的最后就是要处理的 ux 文件的路径
- // 因此 webpack 根据这个 js 文件继续编译时, 会再读取一次原始 ux 文件
- // 而不是复用 步骤一 中的文件流
- var $app_template$ = require('!!./json-loader.js!./template-loader.js!./fragment-loader.js?index=0&type=templates!./src/pages/page1/index.ux');
- var $app_style$ = require('!!./json-loader.js!./style-loader.js?index=0&type=styles!./fragment-loader.js?index=0&type=styles!./src/pages/page1/index.ux');
- var $app_script$ = require('!!./script-loader.js!babel-loader?presets[]=./node_modules/babel-preset-env&presets=./babel-preset-env&plugins[]=./lib/jsx-loader.js&plugins=./lib/jsx-loader.js&comments=false!./access-loader.js!./fragment-loader.js?index=0&type=scripts!./src/pages/page1/index.ux');
webpack 会继续解析这个 js 文件, 从而进入真正的编译流程(步骤二). 因此, 如果我们直接在 hap-toolkit-loader 前加一层我们的 loader, 只能影响生成 js 文件的步骤一, 步骤二中 webpack 又会重新读取一遍原始文件, 这时我们的 loader 就干预不到了.
为了不过多地侵入 hap-toolkit-loader 的编译逻辑, 最终我们的做法是, 在步骤二中生成的 js 中添加我们的 loader, 比如:
- // 在 require 的最后添加我们的 vue-hap-tools-loader
- var $app_template$ = require('!!./json-loader.js!./template-loader.js!./fragment-loader.js?index=0&type=templates!./vue-hap-tools/index.js?type=templates!./src/pages/page1/index.ux');
从而使我们的 loader 在真正的编译过程中生效, 并且不介入后续逻辑. 最后只需要修改 webpack 的 rules 就可以编译 vue 文件了:
- rules: [
- {
- test: /\.vue$/,
- use: ['hap-toolkit-loader']
- }
- // 其他 loader
- ]
一个简化的打包流程如下图所示:
语法转换
现在我们能够在 hap-toolkit 真正打包之前做一层 loader 了, 因此我们只需要在该 loader 中实现语法转换就可以了. 和 vue 文件的划分类似, 我们的转换也分为 template,script,style 三部分, 但三者并不是孤立的, 会有一定联系.
template 转换
先将 template 解析为 html 的语法树, 然后遍历处理就行.
标签转换
建立标签转换的映射关系, 直接修改标签名, 并处理部分特异性, 比如 button 需要转换为 type=button 的 input,button 的文本需要放在 input 的 value 属性中.
指令转换
这部分也只需简单地替换, 比如 v-for -> for,v-if -> if,v-show -> show,v-bind:class -> class, 指令的值需要用双大括号包裹, 如 v-if="ifRender" -> if="{{ifRender}}". 为了支持对象形式的 class, 需要特异性处理, 并且需要合并 class 和: class :
- <!-- 转换前 -->
- <div class="staticClass" :class="{class1: useClass1===true, class2: useClass2===true}"></div>
- <!-- 转换后 -->
- <div class="{{'staticClass'}} {{useClass1===true?'class1':''}} {{useClass2===true?'class2':''}}"></div>
另外 v-model 是 vue 提供的语法糖, 快应用没有提供, 我们需要实现:
- <template>
- <!-- 转换前 -->
- <input type="text" v-model="inputVal">
- <!-- 转换后 -->
- <!-- 快应用中 input 的 change 事件对应 web input 的 input 事件 -->
- <input type="text" value="{{inputVal}}" onchange="_qa_v_model_inputVal">
- </template>
- <script>
- export default {
- // 其它代码
- ...
- methods: {
- // 在 methods 中添加事件回调
- _qa_v_model_inputVal(e){
- // 快应用中获取 input value 的方式与 web 不同
- // 这里用赋值的方式抹平
- e.target.value = e.value;
- this.inputVal = e.target.value;
- }
- }
- }
- </script>
script 转换
同样的, 首先将 js 转换为语法树, 所有操作都基于语法树进行.
提取组件
这里贴一段提取组件的伪代码: 提取组件 https://github.com/Youjingyu/vue-hap-tools/blob/master/docs/example.js
快应用的组件引入形式为:
- <import name="comp-part1" src="./part1"></import>
- <template>
- <comp-part1></comp-part1>
- </template>
- ...
因此需要根据 js 提取组件的名字及组件路径, 再拼接回快应用支持的形式:
- <script>
- import utils from './utils'
- import compPart1 from './part1'
- export default {
- // 其它代码
- ...
- // 用 components 字段中的变量名与 import 的变量名对应
- // 从而获得组件路径
- components: {
- compPart1
- }
- }
- </script>
处理 methods
快应用没有 methods 字段, 所有 methods 里的方法都提升为与 data,mounted 等字段同一级.
实现 computed
computed 我们暂时使用的 Object.defineProperty 来实现, 比如下面的例子:
转换前
- <script>
- export default {
- computed: {
- showTip () {
- return this.tipList.length> 0
- }
- }
- }
- </script>
转换后:
- <script>
- export default {
- data(){
- showTip: ''
- }
- created() {
- Object.defineProperty(this, 'showTip', {
- get: function(){
- return this.tipList.length> 0
- }
- });
- }
- }
- </script>
watch 转换
watch 直接基于快应用的 $watch 来实现:
转换前:
- <script>
- export default {
- watch: {
- showTip () {
- console.log('tip changed')
- }
- }
- }
- </script>
转换后:
- <script>
- export default {
- created() {
- this.$watch('showTip', '_qa_watch_showTip')
- }
- methods: {
- _qa_watch_showTip(){
- console.log('tip changed')
- }
- }
- }
- </script>
生命周期映射
暂时只支持 vue 与快应用能够对得上的生命周期, 这几个生命周期钩子基本能满足大多数需求, 后期考虑在快应用中模拟更多的 vue 生命周期钩子.
- {
- 'created': 'onInit',
- 'mounted': 'onReady',
- 'beforeDestroy': 'onDestroy'
- }
事件回调
快应用与 web 事件的 event 参数有一定差异, 比如输入框 input 事件的回调中, 获取输入框值:
- <script>
- export default {
- methods: {
- inputEventCallback(e){
- // 快应用需要通过 e.value 获取输入框的值
- // 为了在快应用中也能像 web 一样获取输入框的值
- // 这里做一个赋值
- e.target.value = e.value;
- this.inputVal = e.target.value;
- }
- }
- }
- </script>
vue-router 转换
vue-router 直接借助快应用的 router 实现, 但需要抹平差异性:
转换前:
- <script>
- export default {
- methods: {
- gotoTodoMVC () {
- this.$router.push({
- path: '/TodoMVC',
- query: { useInfo: {name: 'John', id: 100} }
- })
- }
- }
- }
- </script>
- // 下一个页面获取参数
- <script>
- export default {
- created() {
- console.log(this.$route.query.userInfo.name)
- }
- }
- </script>
转换后:
- <script>
- // 引入快应用的 router
- import _qa_router from '@system.router'
- export default {
- created(){
- this.$router=_qa_router;
- }
- methods: {
- gotoTodoMVC () {
- this.$router.push({
- uri: '/TodoMVC',
- params: { useInfo: {name: 'John', id: 100} }
- })
- }
- }
- }
- </script>
// 下一个页面获取参数
- <script>
- export default {
- created() {
- this.$route={
- query: {
- // 快应用会将上个页面传递的参数全部挂载到 this 上
- // 并且会把参数转为字符串, 因此这里需要将字符串还原
- userInfo: new Function(`return ${this.useInfo}`)()
- }
- };
- // 获取参数
- console.log(this.$route.query.userInfo.name)
- }
- }
- </script>
style 转换
快应用样式是 web 样式的子集, 对于快应用不支持, 而 web 支持的样式, 实在没想到比较好的转换方式, 暂时的做法是, 尽量在编译阶段就对不支持的样式抛出警告. 从新浪这边的情况来看, 使用快应用支持的样式来实现设计稿, 问题不大.
rem 转换
快应用只支持 px, 百分比尺寸, CSS 中的 rem 会按照 manifest.json 中的基准宽度转为 px.
标签选择器
由于快应用的标签比 web 标签少得多, 比如 p,h1,nav,section 等都转会为 div, 从而针对上述标签的标签选择器都会失效. 一个可行的方式是, 为转换过的标签添加私有 class, 并在 css 中将标签选择器修改为私有 class 的选择器. 但这样做有个问题是, 选择器权重变了:
转换前
- <template>
- <div class="class1">
- <h1></h1>
- <div>
- <template>
- <style>
- .class1 h1{}
- <style>
转换后:
- <template>
- <div class="class1">
- <h1 class="_qa_h1"></h1>
- <div>
- <template>
- <style>
- .class1 ._qa_h1{}
- <style>
因此, 我们暂时的做法是, 仅支持快应用具有的标签的选择器, 不支持的标签选择器会抛出警告.
总结
由于篇幅有限, 这里只是大概说明了一下实现过程. 大概思路就是, 在快应用官方脚手架 hap-toolkit 编译之前, 加一层我们的 loader, 实现语法转换, 并 hack 部分 vue 特性. 可以看到, 整个过程几乎都是基于语法树的遍历, 修改, 大多都是体力活. 另外, 毕竟是用快应用的特性去 hack vue 的特性, 可能部分实现前后的等价性有待商榷, 望大佬指正.
来源: https://juejin.im/entry/5afd2f6e6fb9a07a9a111183