萬字實踐|UNI-APP

Annter發表於2020-03-25

前言

使用 uniapp 已經有了一段時間了,做過兩個應用。一個是管理後臺,另一個是商城。該踩過的坑基本上踩了一遍。

獲取上一頁和本頁的資料

當我們在開發過程中,如果上一個資料修改了,那麼最上層的資料也需要改變。

最常見的業務就是地址的填寫,然後支付訂單。

為了解決這個問題,我們封裝一個獲取和設定上一個頁面和下一頁面的資料。這樣就可以很好地使用了。

const getSetFn = page => {
  return {
    setData(data) {
      page.setData(data);
      return this;
    },
    getData: () => page.data
  };
};
/**
 *
 * @param {array} pages 頁面傳入的值
 */
export const pages = pages => {
  const currentPage = pages[pages.length - 1];
  const prevPage = pages[pages.length - 2];
  return {
    prev: () => getSetFn(prevPage),
    crrent: () => getSetFn(currentPage)
  };
};
複製程式碼

然後在頁面中這樣使用就可以設定上一頁的資料了。

const getPage = getCurrentPage();
page(getPage)
  .prev()
  .setPage({ title: 1 });
複製程式碼

HTTP 攔截器

HTTP攔截器部分可以參考這篇文章:從原始碼分析Axios

生命週期

小程式的生命週期

小程式的生命週期分為以下幾種,

啟動週期:onLaunch--->onShow--->onHide

其他週期: onError,onPageNotFounds

  • onLaunch

它是由網路首次請求微信小程式包,待手機下載完畢之後,便觸發該生命週期。

  • onShow

它是當邏輯層初始化完畢之後,進入前臺之後,觸發該生命週期。

  • onHide

它是當小程式切換到後臺,觸發的宣告週期。

用法如下:

App({
  onError(error) {
    console.log(error);
  }
});
複製程式碼

  • onError

它是當小程式發生錯誤時,會觸發此生命週期。

傳入的是一個callback,可以監聽小程式的所有錯誤。

  • onPageNotFounds

它和wx.onPageNotFound的行為是一致的,是指當路由未找到頁面時,會觸發此生命週期。

用法是:

App({
  onPageNotFound(notFound) {
    wx.redirectTo({
      url: 'pages/..'
    });
  }
});
複製程式碼

頁面的生命週期

頁面的生命週期有:

週期:onLoad--->onShow--->onReady--->onHide--->onUnload 其他週期:onPullDownRefresh,onReachBottom,onPageScroll,onResize,onShareAppMessage

  • onLoad

此生命週期是當頁面首次建立時執行,也就是 AppSerive 建立完畢之後觸發的。

  • onShow

此生命週期是指當頁面顯示在前臺時,觸發的生命週期。

  • onReady

此生命週期是指當頁面的資料從AppSerive傳過來之後,渲染前臺的頁面完畢後,觸發的宣告週期。

  • onHide

是指前臺切換到後臺觸發的宣告週期。


  • onPullDownRefresh

它是指當頁面下拉重新整理時,會觸發此生命週期。

  • onReachBottom

它是指當頁面觸底時,會觸發此生命週期。

  • onShareAppMessage

當頁面被使用者分享時,執行的宣告週期。

小程式架構

小程式的架構分兩層,分別是 View 檢視層、App Service 邏輯層。

它們是放在兩個執行緒裡執行的。

並且通過JSBridage進行通訊,邏輯層將資料放在檢視層內,並觸發邏輯頁面更新,檢視層把觸發的事件通知到邏輯層進行業務處理。

架構如下圖:

萬字實踐|UNI-APP

檢視層

檢視層使用 WebView 渲染,iOS 中使用自帶 WKWebView,在 Android 使用騰訊的 x5 核心(基於 Blink)執行。

邏輯層

邏輯層使用在 iOS 中使用自帶的 JSCore 執行,在 Android 中使用騰訊的 x5 核心(基於 Blink)執行。

小程式啟動機制

萬字實踐|UNI-APP

小程式的啟動機制分為兩層:

  • 預載入

在預載入期間,邏輯層和檢視層同時啟動,且用不同的引擎啟動。

邏輯層使用JS引擎啟動,檢視層則是使用WebView層啟動。

JS引擎WebView全部啟動之後,於是注入到公共庫內。

  • 小程式啟動

小程式啟動之後,先下載所有的資源包,接著繪製好UI和確定DOM樹,然後就是初始化程式碼。就這樣,一個小程式就啟動完畢了。

效能優化

上傳程式碼時自動壓縮

在小程式開發客戶端,在詳細列表卡中勾選以下選項卡:

萬字實踐|UNI-APP

清理無用程式碼和資源

在釋出小程式時,小程式會隨著整個資料夾一起上傳。如果其中有一些無用的資原始檔的話,那麼它也會佔用上傳時的大小。

使用CDN來分擔資源請求

在小程式使用過程中,小程式會自動地向騰訊伺服器請求資源,有些資源會阻塞頁面渲染時間,放大使用者的焦急情緒。

所以為了避免這種情況的出現,可以在伺服器中存放一些資原始檔,來避免阻塞。

分包

  • 分包

    • 分包無法requireimport其他包的JS檔案,以及template

    • 分包無法引用其他包的資原始檔。

例如:

{
  "subPackages": [
    {
      "root": "PageA", // 分包的根路徑
      "pages": ["log/log"] // 分包的子路徑檔案
    }
  ]
}
複製程式碼

如何跳轉?

uni.navigateTo({
  url: '/PageA/log/log' // 分包載入需要寫全路徑
});
複製程式碼

  • 獨立分包

一種特殊的分包,可以獨立於主包與其他分包執行。分包依賴於主包,而獨立分包卻不依賴其他包

獨立分包有很多種。

新增independent欄位就可以直接成為主包。

{
  "subPackages": [
    {
      "root": "PageA", // 分包的根路徑
      "pages": ["log/log"] // 分包的子路徑檔案
    }
  ],
  "independent": true // 獨立分包,
}
複製程式碼

因為它可以不從主包中啟動,所以無法獲得App,因此新增allowDefault這個引數就可以在App啟動後,可以重新覆蓋到真正的App中。

  • 預下載包
{
  "preloadRule": {
    "pages/index/about": {
      // 這裡必須是在是pages裡配置好的
      "network": "all",
      "packages": ["__APP__"] // 所有的包
    }
  }
}
複製程式碼

預請求

在單頁面應用中,為了提高應用可視性和效能,讓其他頁面能夠更好展示資源和其他資料。

於是首頁提前載入好資源,以便其他頁面可以使用,這種方法叫做預載入。

預載入分為兩種:

  • App 預載入

App 預載入的思想非常簡單,就是進入應用的時候儲存一些頁面的資料。

export default {
  globalData: {
    PreLoadData: null
  },
  onShow() {
    const that = this;
    fetch('/preload').then(res => {
      that.PreLoadData = res;
    });
  }
};
複製程式碼
  • 頁面預請求

小程式與單頁面程式相似,主包下載所有的頁面,下載完畢之後,分別推入頁面棧。

並不是傳統的當A頁面跳轉到B頁面時,會自動載入B頁面的資源頁面。而真正的載入類似於webpack的載入,待進入某一個頁面時,會將頁面置於頂層。

載入頁面方式為:

Loading A page.
        |
        |
        |
A page load done ---> Loading A page.
        |
        |
        |
B page load done ---> All pages load complete.
------------------------------------------

Then,render entierty page.
複製程式碼

因為如此是,那麼我們可以在onLoad之前,接收來自上一個頁面內容。

由於,uni-app的特殊性,所以我們可以使用mixin程式碼,混入到每一個頁面中。

export default {
  data() {
    return {
      PreLoad: []
    };
  }
};
複製程式碼

但是它有一個弊端,那就是每次進入頁面後,會自動地初始化為一個空陣列。

首先建立一個儲存PreLoad的陣列,方便日後的管理。

const storePreLoda = [];
export default {
  data() {
    return {
      PreLoad: [...PreLoad]
    };
  }
};
複製程式碼

接著向需要預載入的頁面傳遞資料:

const storePreLoad = [];
export default {
  data() {
    return {
      PreLoad: [...PreLoad]
    };
  },
  methods: {
    __put(data, page) {
      const __page = page ? page : '';
      storePreLoad.push({
        page: __page,
        data
      });
    }
  }
};
複製程式碼

但是這樣寫有一個弊端,那就是如果一個頁面有多個動作的話,需要向頁面傳遞多個資料的話,那麼就會出現多page

所以,我們改造一下:

const storePreLoad = [];
const __put = (data, page) => {
  const __page = page ? page : '';
  const hasPage = storePreLoad.some(el => el.page === page);
  if (hasPage) {
    storePreLoad.find(el => el.page === page).data.push(data);
    return data;
  }
  storePreLoad.push({
    page: __page,
    data
  });
  return data;
};

///////////

export default {
  data() {
    return {
      PreLoad: [...PreLoad]
    };
  },
  methods: {
    __put
  }
};
複製程式碼

既然傳遞了資料,那麼獲取資料就變得簡單許多了。

const storePreLoad = [];
export default {
  data() {
    return {
      PreLoda: [...storePreLoad]
    };
  },
  methods: {
    getRoute() {
      const pages = getCurrentPages();
      const { route } = pages[pages.length - 1];
      return route;
    },
    __take(isOnce = '', page = '') {
      const getRoute = page !== '' ? page : this.getRoute(); // 找到某一個頁面的預處理資料
      const { data } = this.PreLoadData.find(el => el.page === getRoute);
      if (isOnce == 'once') {
        const index = this.PreLoadData.findIndex(el => el.page === getRoute);
        this.PreLoadData.splice(index, 1);
      }
      return isObject(data) ? Object.freeze(data) : data;
    }
  }
};
複製程式碼

上面的__take方法有兩個引數,分別是:

  • once

只拉取一次預載入資料,然後刪除資料。

  • page

找到某一個頁面,然後返回某一個註冊了預載入頁面的資料。

使用骨架屏

骨架屏的實現思路是按照class的位置,然後繪製是否為圓形或者其他形狀。

然後使用wx.createSelectorQuery().selectAll()查詢對應的節點。

詳情看:小程式之骨架屏

及時反饋

  • 同時合併資料的更新

由於小程式的特殊機制,它將檢視層和邏輯層隔絕成了兩個的程式。

它們兩個之間通訊是非同步的,同時,改變的檢視層的資料(同步)。

setData這個API就可以看出來,它是非同步的。

如:

this.setData({}, res => {
  // 這是非同步的
});
複製程式碼

所以,使用setData更新資料會通知邏輯層,造成一次程式通訊,等通訊完畢之後,再更新檢視層的資料。

多條通訊會對手機資源吃緊,也會造成小程式變慢。

可以使用資料合併的方式,讓它變成一次通訊,從而減少卡頓。

避免一下的情況:

this.setData({
  data: {
    a: 1
  }
});
複製程式碼

你可以將他合併成:

this.setData({
  'data.a': 1
});
複製程式碼

這樣就完成了區域性的更新了。

或者,寫成另一種寫法:

const updateProp = 'data.a';
this.setData({
  [updateProp]: 1
});
複製程式碼
  • 避免頻繁的更新

onScroll生命週期中,謹慎更新資料。如果更新資料的話,可以使用防抖、或者是節流

防抖:在短時間內觸發一次函式。

const debounce = function(fn, time) {
  const context = this;
  const args = arguments;
  return function() {
    setTimeout(function() {
      fn.apply(context, args);
    }, time);
  };
};
複製程式碼

節流:在指定的時間內執行一次。

const throttle = function(fn, time) {
  const prev = Date.now();
  const context = this;
  const args = arguments;
  return function() {
    let now = Date.now();
    if (now - prev === time) {
      fn.apply(context, args);
      prev = Date.now();
    }
  };
};
複製程式碼
  • 使用intersectionObserver代替selectQuery

selectQuery是查詢節點資訊的物件,它也需要跟邏輯層通訊,所以它一定程度上會讓小程式“變慢”。

inersectionObserver是以觀察節點的互動情況,並不存在通訊的情況。

使用方法如下:

uni
  .createIntersectionObserver(this)
  .relativeToViewport()
  .observe('.header', res => {
    console.log('--->', res);
  });
複製程式碼

其中relativeToViewport是相對於視窗觀察的選項。


## 全域性狀態

在小程式中,如果你需要在每一個頁面中新增使用共有的資料,那麼有三種方式能夠完美解決。

  • Vue.prototype

如果專案中需要用到一個全域性資料或者全域性函式的話,那使用Vue.prototype是一個不錯的選擇。

它的作用是可以掛載到Vue的所有例項上,供所有的頁面使用。

用法如下:

// main.js
Vue.prototype.$globalVar = 'Hello';
複製程式碼

然後在pages/index/index中使用:

<template>
  <view>{{useGlobalVar}}</view>
</tempalte>
<script>
export default {
  data (){
    return {
      useGlobalVar:$globalVar
    }
  }
}
</script>
複製程式碼

因為,uni-app的目前能力無法對映到view上,只能夠這樣寫。

  • globalData
<!-- App.vue -->
<script>
    export default {
      globalData:{
          data:1
      }
      onShow() {
       // 使用
      getApp().globalData.data;
      // 更新
      getApp().globalData.data = 1;
    }
  };
</script>
複製程式碼
  • Vuex

VuexVue專用的狀態管理模式。他能夠集中管理其資料,並且可觀測其資料變化,以及流動。

安裝如下:

// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    counter: 0
  },
  mutaions: {
    addCounter(state) {
      state.counter++;
    }
  }
});
複製程式碼
// main.js
import Vue from 'vue';
import store from './store';

Vue.config.productionTip = false;

App.mpType = 'app';

const app = new Vue({
  store,
  ...App
});
app.$mount();
複製程式碼

使用&注入到頁面中

<template>
  <view>{{ counter }}</view>
</template>

<script>
import { mapState } from 'vuex';

export default {
  computed: {
    ...mapState({
      counter: state => state.counter
    })
  }
};
</script>
複製程式碼

尺寸單位

rem、rpx、vw、em

rpx

rpx是微信獨有的一套單位,可以進行寬度和高度自適應,他叫做響應式畫素。例如手機是iPhon 6型號,那麼它的手機寬度是 375 個畫素。換算成rpx就是750rpx,而且所有的手機尺寸都是由750為基準進行換算的。

rem

這個單位是font-size大小變化而變化的一種單位。常見的開發可以手動設定html的字型大小,也可以動態地設定html的字型大小。

通常情況下,瀏覽器的預設字型font-size16px,那麼1rem=16rem

我們先試試不設定任何“根”尺寸,對比看看:

<div class="default-rem-unit">Hello World</div>
<div class="default-px-unit">Hello World</div>
<!-- 樣式 -->
<style>
  .default-rem-unit {
    font-size: 1rem;
  }
  .default-px-unit {
    font-size: 16px;
  }
</style>
複製程式碼

開啟後,你會發現字型大小是一樣的:

萬字實踐|UNI-APP

這也說明了1rem的預設大小是16px

現在,我們來改造一下它,讓它變成1rem=20px。只需要新增如下程式碼就可以了:

html {
  font-size: 20px !important;
}
複製程式碼

此時,上面的Hello World,很明顯變大了:

萬字實踐|UNI-APP

通常,為了相容各種移動端的不同螢幕尺寸。開發者會相容性的CSS,下面兩種寫法會讓開發者採用:

  1. 使用css3calc來計算html
html {
  /* iPhone 6標準尺寸 */
  font-size: calc(100vw / 3.75);
}
複製程式碼
  1. 引入lib-flexible庫。

lib-flexible

至於移動端的適配,不在此文的討論範圍內。

em

em,一種相對長度單位,繼承於父級元素的字型大小,和rem一樣的預設px單位,是16px

一個小例子:

<div class="default-em-unit">Hello World</div>
<div class="default-px-unit">Hello World</div>
<!-- 樣式 -->
<style>
  .default-rem-unit {
    font-size: 1em;
  }
  .default-px-unit {
    font-size: 16px;
  }
</style>
複製程式碼

結果如下:

萬字實踐|UNI-APP

可見,em的預設大小也是16px

如果要改某一個元素的字型大小,只需要修改父元素的大小,即可改變子元素的大小:

<!-- 父元素 -->
<div class="root-em">
  <div class="default-rem-unit">Hello World</div>
  <div class="default-px-unit">Hello World</div>
</div>

<style>
  .em-root {
    font-size: 20px;
  }
  .default-rem-unit {
    font-size: 1em;
  }
  .default-px-unit {
    font-size: 16px;
  }
</style>
複製程式碼

最後的結果是:

萬字實踐|UNI-APP

vh&vw

vhvw這兩個長度單位是相對於viewport變化而變化值,也就是視窗可見範圍。

當視窗大小變化時,其元素大小也會隨著視窗變化而變化。

100vw100vh是指是視窗寬度的 100%和視窗高度的 100%。


參考連結:

length 是表示距離尺寸的一種 css 資料格式。許多 CSS 屬性使用它,比如 width、margin、padding、font-size、border-width、text-shadow。

可愛的 rem

preload 的實現方案

uni-app 全域性變數的幾種實現方式

微信小程式————setData()方法的使用和注意事項

相關文章