前言
使用 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。