1. 程式人生 > >Vue+原生App混合開發

Vue+原生App混合開發

專案的大致需求就是做一個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.uidthis.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面板裡。

 

結束

知識點比較繁雜,所以文章有點亂,暫時先總結到這,後續還有很多坑待填。