快應用開發框架vue-hap-tools實現原理
來自專欄 新浪移動大前端
前期爝神大大 @小爝 已經對vue-hap-tools做了簡單介紹,參考使用vue編寫快應用解決方案。現在這篇文章主要說說實現思路,也算總結一下,如果有實現得不合理的地方,歡迎大家指正。
要讓vue代碼運行在快應用平台,一種實現思路是像mpvue一樣,js部分使用vue.runtime接管,模板部分直接轉換,另一種思路是直接將vue的語法轉換為快應用的語法,js部分添加少量hack代碼。由於快應用與vue語法本身就比較接近,因此我們選擇了成本相對較低的第二種實現方式。當然,可能還有更底層的實現方式,還望知道的大佬指點指點??。
快應用打包過程
在開始之前,我們需要了解快應用官方腳手架hap-toolkit的打包過程。整體來說,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-loadervar $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轉換為語法樹,所有操作都基於語法樹進行。
提取組件
這裡貼一段提取組件的偽代碼:提取組件
快應用的組件引入形式為:
<import name="comp-part1" src="./part1"></import><template> <comp-part1></comp-part1></template>...
因此需要根據js提取組件的名字及組件路徑,再拼接回快應用支持的形式:
<script>import utils from ./utilsimport compPart1 from ./part1export 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>// 引入快應用的routerimport _qa_router from @system.routerexport 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的特性,可能部分實現前後的等價性有待商榷,望大佬指正??。
推薦閱讀:
※Vue 插件編寫與實戰
※jQuery的ajaxSubmit如何實現批量圖片非同步上傳?
※【備戰秋招Day 1】經典面試題1-4及在線編程題1-3答案
※前端常見的框架總結