综合开发

万字实践|UNI-APP

微信扫一扫,分享到朋友圈

万字实践|UNI-APP

使用 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来分担资源请求

在小程序使用过程中,小程序会自动地向腾讯服务器请求资源,有些资源会阻塞页面渲染时间,放大用户的焦急情绪。

所以为了避免这种情况的出现,可以在服务器中存放一些资源文件,来避免阻塞。

分包

  • 分包

    • 分包无法 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>
复制代码

打开后,你会发现字体大小是一样的:

这也说明了 1rem 的默认大小是 16px

现在,我们来改造一下它,让它变成 1rem=20px 。只需要添加如下代码就可以了:

html {
font-size: 20px !important;
}
复制代码

此时,上面的 Hello World ,很明显变大了:

通常,为了兼容各种移动端的不同屏幕尺寸。开发者会兼容性的 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>
复制代码

结果如下:

可见, 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

vhvw 这两个长度单位是相对于 viewport 变化而变化值,也就是视窗可见范围。

当视窗大小变化时,其元素大小也会随着视窗变化而变化。

100vw100vh 是指是视窗宽度的 100%和视窗高度的 100%。

参考链接:

length 是表示距离尺寸的一种 css 数据格式。许多 CSS 属性使用它,比如 width、margin、padding、font-size、border-width、text-shadow。

可爱的 rem

preload 的实现方案

uni-app 全局变量的几种实现方式

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

iOS探索--类的结构分析(二)

上一篇

唯心主义蠢货的[vue学习] Vue的nextTick

下一篇

你也可能喜欢

评论已经被关闭。

插入图片

热门栏目

万字实践|UNI-APP

长按储存图像,分享给朋友