前言
使用 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
進行通訊,邏輯層將資料放在檢視層內,並觸發邏輯頁面更新,檢視層把觸發的事件通知到邏輯層進行業務處理。
架構如下圖:
檢視層
檢視層使用 WebView 渲染,iOS 中使用自帶 WKWebView,在 Android 使用騰訊的 x5 核心(基於 Blink)執行。
邏輯層
邏輯層使用在 iOS 中使用自帶的 JSCore 執行,在 Android 中使用騰訊的 x5 核心(基於 Blink)執行。
小程式啟動機制
小程式的啟動機制分為兩層:
- 預載入
在預載入期間,邏輯層和檢視層同時啟動,且用不同的引擎啟動。
邏輯層使用JS引擎
啟動,檢視層則是使用WebView
層啟動。
當JS引擎
和WebView
全部啟動之後,於是注入到公共庫內。
- 小程式啟動
小程式啟動之後,先下載所有的資源包,接著繪製好UI
和確定DOM
樹,然後就是初始化程式碼。就這樣,一個小程式就啟動完畢了。
效能優化
上傳程式碼時自動壓縮
在小程式開發客戶端,在詳細列表卡中勾選以下選項卡:
清理無用程式碼和資源
在釋出小程式時,小程式會隨著整個資料夾一起上傳。如果其中有一些無用的資原始檔的話,那麼它也會佔用上傳時的大小。
使用CDN來分擔資源請求
在小程式使用過程中,小程式會自動地向騰訊伺服器請求資源,有些資源會阻塞頁面渲染時間,放大使用者的焦急情緒。
所以為了避免這種情況的出現,可以在伺服器中存放一些資原始檔,來避免阻塞。
分包
-
分包
-
分包無法
require
和import
其他包的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
Vuex
是Vue
專用的狀態管理模式。他能夠集中管理其資料,並且可觀測其資料變化,以及流動。
安裝如下:
// 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-size
是16px,那麼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>
複製程式碼
開啟後,你會發現字型大小是一樣的:
這也說明了1rem
的預設大小是16px
。
現在,我們來改造一下它,讓它變成1rem=20px
。只需要新增如下程式碼就可以了:
html {
font-size: 20px !important;
}
複製程式碼
此時,上面的Hello World
,很明顯變大了:
通常,為了相容各種移動端的不同螢幕尺寸。開發者會相容性的CSS
,下面兩種寫法會讓開發者採用:
- 使用
css3
的calc
來計算html
。
html {
/* iPhone 6標準尺寸 */
font-size: calc(100vw / 3.75);
}
複製程式碼
- 引入
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>
複製程式碼
結果如下:
可見,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>
複製程式碼
最後的結果是:
vh&vw
vh
和vw
這兩個長度單位是相對於viewport變化而變化值,也就是視窗可見範圍。
當視窗大小變化時,其元素大小也會隨著視窗變化而變化。
100vw
和100vh
是指是視窗寬度的 100%和視窗高度的 100%。
參考連結:
length 是表示距離尺寸的一種 css 資料格式。許多 CSS 屬性使用它,比如 width、margin、padding、font-size、border-width、text-shadow。