万字实践|UNI-APP

4,203 阅读12分钟

前言

使用 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()方法的使用和注意事项