Vue+原生App混合開發手記#1:https://www.cnblogs.com/undefined000/p/vue-nativeApp-development.html
項目的大致需求就是做一個App,里面集成各種功能供用戶使用,其中涉及到很多Vue的使用方法,單獨總結太麻煩,所以通過這幾篇筆記來梳理一下。原型圖如下:
路由配置
主界面會用到一些原生App方法,比如驗證用戶身份等,故由原生App完成,進去的每個模塊則全部都是HTML頁面(有一種后端工作好輕松的感覺 ̄へ ̄)。由於傳統的HTML頁面開發起來效率太低,所以我選擇了Vue來實現。每一個功能對應一個路由,比如電腦報修
對應/repair
,repair這個路由下的子頁面都放進子路由里。
│ ├─repair │ apply.vue │ index.vue │ payment.vue │ repairList.vue │ └─teambuilding apply.vue index.vue
const router = new Router({ mode: 'history', routes: [ path: '/repair', component: repair, children: [ {path: 'apply', component: repairApply}, //電腦報修申請 { path: ':id',//報修單詳情 component: repairDetail, children: [ {name: 'evaluate', path: 'evaluate', component: repairEvaluate}, //服務評價 {name: 'evaluatePay', path: 'pay', component: pay} //支付 ] } ] ] })
為了減小打包時的體積,在加載組件的時候采用了以下形式:
const repair = resolve => import('views/repair/index').then(module => resolve(module)) const repairApply = resolve => import('views/repair/apply').then(module => resolve(module)) const repairDetail = resolve => import('views/repair/detail').then(module => resolve(module))
這是按照官方文檔提供的路由懶加載技術寫的,這樣就能實現當路由被訪問的時候才加載對應組件。以上是項目中關於路由的一些用法。
注冊全局組件
接下來是全局組件的用法,比如頭部,等待加載,彈出層之類的組件,幾乎每個頁面都有,全局注冊能省去不少事。
import header from 'components/header/header' import loading from 'components/loading/loading' Vue.component('v-header', header) Vue.component('loading', loading)
之后在每個頁面中敲入<loading></loading>就能直接使用了,不用每次都去import。
處理返回鍵
還有一個比較常見的問題,由於Vue做出來的頁面是一個SPA,在Android機中如果按下了物理返回鍵,整個應用都會退出,解決方法是重寫物理返回鍵,這樣就能按路由一級一級地返回了。因為主界面是由原生實現的,所以Vue只能返回到對應模塊的首頁,比如從 /repair/apply -> /repair -> null ,想要回到原生主界面,需要后端向前端注入一段腳本,在模塊首頁的后退按鈕被點擊時,執行一段方法告知Android調用自身的邏輯,然后Android關閉當前頁面並回到主界面,例如:
//在main.js中加入該方法 window.AndroidMethod = function (msg) {
if (window.android !== null && typeof(window.android) !== "undefined") { window.android.callAndroid(msg); } }
在頭部組件header.vue中,可以使用如下方式:
<!--回到主界面,isFirstPage通過props傳入--> <a v-if="isFirstPage" class="back" @click="backToHomePage"></a> <!--普通返回--> <a v-else @click="goback" class="back"></a> methods:{ goback() { window.history.length > 1 ? this.$router.go(-1) : this.$router.push('/') }, backToHomePage() { AndroidMethod('backToHomePage') } }
這樣可以將模塊首頁的返回和子路由的返回區分開來。
如果使用其他的打包工具,比如apiCloud或者HBuilder,它們都有各自的阻止物理返回按鍵的方法:
//apiCloud api.addEventListener({ name: 'keyback' }, function(ret, err){ }); }); //HBuilder
//https://blog.csdn.net/qq_25252769/article/details/76913083 document.addEventListener('plusready', function() { var webview = plus.webview.currentWebview(); plus.key.addEventListener('backbutton', function() { webview.canBack(function(e) { if(e.canBack) { webview.back(); } else { webview.close(); } }) }); });
同樣的,把這些代碼放在main.js中即可,打包后在真機里運行時會執行這些方法,普通環境是不存在這些變量的。
接收后端返回的數據
有時候,我們希望在Vue初始化時就能設置一些從服務器獲取的常量,比如userID等,之后在各個組件中就能很方便地訪問。設置全局變量很簡單,直接掛載在Vue.prototype后面即可:
axios.get('http://localhost/index.php').then(res => { Vue.prototype.uid = res.data.uid Vue.prototype.appid = res.data.appid new Vue({ el: '#app', router, store, render: h => h(App) }) })
在組件中使用this.uid
、this.appid
就能訪問到從服務器獲取的常量了。如果是普通的js文件(比如api,utils等等),可以通過
import Vue from 'vue' Vue.prototype.uid
來訪問。我們可能還希望這些數據在初始化時也能同時保存到Vuex中,先來看一下最初的Store/index.js文件:
import Vue from 'vue' import Vuex from 'vuex' import * as actions from './actions' import * as getters from './getters' import state from './state' import mutations from './mutations' export default new Vuex.Store({ actions, getters, state, mutations })
但這樣就沒有往Vuex中存入數據的機會,這時就需要對Store文件夾中的index.js做一些小的封裝,使其返回一個方法:
function buidler(data) { return new Vuex.Store({ actions, getters, state: data, mutations }) } export default buidler
然后修改main.js中調用Vuex的方式,最初的代碼如下:
import store from './store' new Vue({ el: '#app', router, store, render: h => h(App) })
修改后的代碼如下:
import store from './store' axios.get('http://localhost/index.php').then(res => { Vue.prototype.uid = res.data.uid Vue.prototype.appid = res.data.appid new Vue({ el: '#app', router, store:store(res.data), render: h => h(App) }) })
在組件的created方法中用MapGetters輸出一下uid和appid,發現值可以被打印出來,說明這種實現方式是可以采用的。
更新:在后續的測試中,發現一些機型,特別是華為機(實測iOS沒有此問題),對這種延后初始化Vue的方式兼容不好,表現在所有路由的切換動畫全部失效,頁面后退時會重新渲染頁面(執行組件created方法中的內容),設置keep-alive也沒有效果。不過水平有限,實在弄不懂為什么會這樣。為了兼容,就不能采用上面的方式了。最后使用了在請求頭中攜帶cookie的方式,具體為webview加載vue頁面時,在requestURL中注入cookie,在cookie中設置需要傳遞的值,下面是用PHP模擬的一個小例子,PHP加載HTML頁,並注入cookie,在HTML加載時取到cookie。
PHP:
<?php header("Set-Cookie:testCookie=exist"); include('index.html'); echo 'cookie測試'; ?>
HTML:
<body> <script> console.log(document.cookie) </script> </body>
這樣就能在main.js里同步拿到userID,Vue也不用延遲初始化了,Android機的表現效果和iOS一致。拿到userID后,可以保存到配置文件config中,在每個組件中訪問config.uid就能拿到。
better-scroll
App中最常見的組件就是滾動數據列表,由此又很容易聯想到better-scroll這個插件。better-scroll雖然好用,但如果使用不當還是會造成不小的麻煩,一些錯誤甚至無從排查。這里主要記錄一下下拉刷新和上拉加載更多的實現。容器結構如下:
最外層的div限制滾動內容的位置,srcoll是官網提供的已經封裝好的組件,里面正常置入ul>li形式的列表就行了,ul和li都不需要特殊的樣式。由於官網提供的例子中整合了許多文件,查閱起來不是很方便,於是將其剝離出來,寫了一個只有上拉加載和下拉刷新的Demo,方便以后使用。使用scroll時要慎用v-show指令,比如我希望使用下面的代碼來控制沒有數據時容器的顯示與隱藏,由於數據是異步加載,剛開始時容器不顯示直到數據加載好為止。
<div v-show="dataList.length > 0"> <scroll></scroll> </div> data() { return { dataList: [] } }
但這樣會造成scroll組件內部高度計算錯誤(offsetHeight被計算成0,這是由於容器處於display:none狀態),如果此時列表的數據沒有達到滾動要求,上拉和下拉的提示文字會顯示在列表下方,網速慢時也無法使用上拉下拉功能。解決方法是使用v-if指令,這樣容器的min-height高度就能被正確計算了。
圖片上傳
另外一個功能是圖片上傳,這個功能並非由前端完成,而是和上面一樣,通過后台返回的一段函數體拿到上傳圖片的路徑並展示出來。
相冊和拍攝都由后台調起,前端只需要進行簡單的傳值就行了:
AndroidMethod('photo') AndroidMethod('video')
代碼是和后端約定好的,所以不需要操心。真正需要關注的是從后台返回的圖片上傳路徑,拿到這個路徑后要在前台展示,並且保存時要帶上一個或多個路徑組成的字符串。
這個方法同樣是和后台約定好的方法:
window.getUpload = function (path) { //這里要將path保存起來拿到組件里使用 }
這里就需要使用全局變量將path保存起來,假定這個全局變量叫做uploadImgUrl,初始化時是一個空數組,只有當用戶從相冊里選擇圖片上傳后才將拿到的路徑賦給這個全局變量。Vue組件中要監聽這個全局變量的變化,就不能使用Vue.prototype.uploadImgUrl這種方式了,因為Vue要監聽某個變量的變化,必須將這個變量放在data中,改進一下之前的代碼:
import store from './store' axios.get('http://localhost/index.php').then(res => { Vue.prototype.uid = res.data.uid Vue.prototype.appid = res.data.appid let vm = new Vue({ el: '#app', router, data() { return { uploadImgUrl: [] } }, store:store(res.data), render: h => h(App) }) window.getUpload = function (path) { vm.uploadImgUrl = path } })
上面將變量存放在根組件的data中,在其它組件內就可以通過以下形式訪問到
this.$root.$data.uploadImgUrl
雖然拿到了路徑,但問題還沒有結束,因為這個值是動態變化的,需要使用計算屬性來監測它的變化,下面是核心代碼:
<template> <li> <div v-for="(item,index) in imgsList"> <img :src="item" width="80" height="80" alt=""> <i @click="deleteImg(index)"></i> </div> </li> </template> <script> export default { data() { return { loadedImgs:[] //保存已上傳的圖片 } }, methods: { deleteImg(index) { for (let i = 0; i < this.loadedImgs.length; i++) { if (index === i) { this.loadedImgs.splice(i, 1) break } } }, computed: { imgsList() { this.loadedImgs = this.loadedImgs.concat(this.$root.$data.uploadImgUrl) this.$root.$data.uploadImgUrl = [] //每次合並完重置一下 return this.loadedImgs } }, created() { this.$root.$data.uploadImgUrl = [] //組件創建時先重置一下之前的值 } } } </script>
通過computed計算屬性,無論是添加圖片或者刪除圖片都能正確展示了。
真機調試
在真機上調試非常不方便,很多調試信息看不到,不過vconsole這個插件解決了這個問題,安裝方法非常簡單,在依賴里(開發環境或正式環境均可)安裝vconsole,然后在main.js中
import Vconsole from 'vconsole' new Vconsole()
打開頁面就能看到右下角多出了一個vConsole的圖標,項目中所有console.log的信息都會輸出到這個vConsole面板里。
結束
知識點比較繁雜,所以文章有點亂,暫時先總結到這,后續還有很多坑待填。