uniapp多端构建实战初探

一、背景

小程序具有跨平台、体验好、高灵活性以及即用即走、无需下载安装诸多优势。随着微信团队推出微信小程序,国内各大互联网团队也相继推出了各自的小程序,手机厂商也联合推出了快应用。但由于各种原因,各团队的小程序,并没有形成统一的标准或联盟,导致部分平台差异化大,要实现多个平台的小程序就得写多套小程序代码,这给开发者和企业带来不少额外的负担。这种情况下,一个多端统一的解决方案就显得尤为重要。统一多端标准后,让一处代码,多处运行成为了可能,而uniapp作为国内小程序的开创者,也制定了一套基于Vuejs的解决方案,并跻身前列,成为多端构建解决方案的优秀者之一。

快递100作为中国领先的快递物流信息服务商,在国内各大小程序平台均有发布小程序应用(微信百度支付宝头条QQQQ浏览器美团等)。在迭代的过程中,保持多个平台业务和体验一致十分必要。目前,快递100已实现了多端统一。以下分别为微信小程序、头条小程序和百度小程序二维码,你可以扫码(或者在各个客户端搜索快递100)进行体验。

uniapp多端构建实战初探_第1张图片        uniapp多端构建实战初探_第2张图片          uniapp多端构建实战初探_第3张图片

二、多端构建方案选型

目前多端构建有不少的解决方案,在比较了多种构建方案后,快递100团队将筛选目标锁定在了Tarouniapp两个解决方案上。关于多端构建解决方案如何选型,我们会考虑以下几个维度:

  • 框架的生态及其社区的大小
  • 框架的性能
  • 框架的学习成本与开发成本
  • 框架支持构建的应用端(如是否支持快应用和QQ小程序)
  • 团队的技术栈的匹配程度

基于以上的维度结合快递100开发团队的实际情况,经过多轮评审和调研后,我们团队最终选择了uniapp。尽管uniapp已经做得足够优秀,但是在实战开发的过程中依然会遇到这样那样的棘手的问题。本文将从零到一搭建一个简单的应用,解决实战过程中遇到的各种问题,并实现稳定的发布到所有的小程序端。

本文不会介绍uniapp相关API与组件的使用,也不会介绍Vue生态具体的使用方法,相关内容可以参考uniapp官网文档以及Vue官网文档。

文章主要包括以下几个方面的内容:

  • 项目的安装
  • 全局API封装
  • 路由拦截
  • 实现Vuex
  • 数据通信方案
  • 登录与用户信息示例
  • 自定义组件说明
  • 常见问题解决方法

三、项目搭建

(一) 创建项目

uni-app支持通过 可视化界面、vue-cli命令行 两种方式快速创建项目。本文将通过以HBuilderX 可视化界面创建项目为例,从零到一搭建起一个完整的项目。安装的方法请查看uniapp官网文档之快速上手

使用uniapp内置组件模板创建后的目录结构如下:

uniapp多端构建实战初探_第4张图片

各目录和文件说明如下:

  • components: 公共组件目录,默认内置了uniapp的官方扩展组件,应用的公共组件也将放置在该目录下
  • pages: 业务页面目录,一个页面对应小程序的一个页面路由
  • static: 存放应用引用静态资源(如图片、视频等)的目录,静态资源只能存放在此目录,该目录下的内容不会经过编译
  • App.vue: 全局应用文件,用于配置全局样式、全局生命钩子等
  • main.js: 应用初始化文件,一般全局的扩展均在该文件实现
  • mainfest.json: 应用配置文件,主要包含各个小程序的appid、版本号等信息
  • pages.json: 路由配置,主要配置应用的页面路由、外观等
  • uni.scss: 全局的scss变量文件,在该文件定义的scss变量全局可用

安装后通过HBuilderX运行到微信小程序(需要配置微信开发者工具的路径),效果如下:

uniapp多端构建实战初探_第5张图片

接下来让我们一步一步来完善整个应用。

(二)自定义全局API

在开发的过程中,我们经常需要重复的使用一些方法,如http请求相关的方法,通常我们会将这些方法封装为统一的模块。在这里,我们把公共的方法统一放到项目根目录下的utils目录下。

1. 反馈类UI相关API封装

用户操作的过程中,经常需要一些操作反馈,如toast,modal等。不同的应用一般都有不同的默认反馈(如文案、字体颜色等),我们首先来封装一下这些方法,相对比较简单。

在utils目录下创建interactiveFeedback.js文件

(1)toast

第一步,新建showToastmodal方法,如下:

// toast反馈
export function showToast(content = '', duration = 1500, icon = 'none') {
  uni.showToast({
    title: content,
    icon: icon,
    mask: true,
    duration: duration
  })
}

// 弹窗反馈
export function modal(content = '', opts = { showCancel: true }) {
  return new Promise((resolve, reject) => {
    uni.showModal({
      title: opts.title || '提示',
      content: content,
      cancelText: (opts.cancelText || '取消').slice(0, 4),
      confirmText: (opts.confirmText ||  '确定').slice(0, 4),
      cancelColor: opts.cancelColor || '#bebebe',
      confirmColor: opts.confirmColor || '#317ee7',
      showCancel: opts.showCancel !== false,
      success: res => {
        if (res.confirm) {
          resolve()
        } else if (res.cancel) {
          opts.handleCancel === true && reject()
        }
      }
    })
  })
}

在小程序中,confirmTextcancelText长度最长为4个字符,以上modal方法遵循promise规范

第二步,定义install方法

function install(Vue) {
  Vue.prototype.$toast = showToast
  Vue.prototype.$modal = modal
}

可以根据实际情况定义更多的方法,如loadinghideLoading

第三步,导出相关的方法

export default { install }

第四步,打开main.js文件,安装模块

import interactiveFeedback from '@/utils/interactiveFeedback'

Vue.use(interactiveFeedback)

安装后,我们就可以用以下的方式使用:

this.$toast("这是一个吐司提示")
this.$modal("这里是弹窗的提醒内容", {
	confirmText: "我知道了",
	handleCancel: true
}).then(() => {
	console.log("确定了")
}).catch(() => {
	console.log('取消了')
})
2. http请求封装

http模块专门用来做接口请求,通常一个应用中接口都会有公共的参数、请求头、授权信息等,将这些信息统一封装非常有必要。由于项目上经常要用到一些常量等,因此,我们先项目根目录下创建config.js文件,用于配置项目上使用的一些常量(如请求超时、接口服务器地址、小程序appid等),这里不一一列举,参照项目源码的config.js文件

接着在utils目录下创建request.js文件

第一步,引入常量和其他公共方法配置

import store from '@/store'
import { showToast } from './interactiveFeedback.js'
import {
  API_BASE,
  NETWORK_TIMEOUT,
  DEFAULT_ERR_MESSAGE,
  PLATFORM,
  NETWORK_ERR_MESSAGE
} from '@/config'

第二步,定义两个核心处理方法

/**
 * 基本的http请求
 * @param {url: String} 请求的链接
 * @param {opts.data: Object} 请求的参数
 * @param {opts.needAuth: Boolean } 是否需要登录
 * @param {opts.handleFail: Boolean} 是否处理错误信息,设置为false将忽略请求时的错误
 * @param {opts.accessToken: Boolean} 是否将token覆盖为提供商token
 * */
export async function request(url, opts = {}) {
  opts.data = opts.data || {}
  if (opts.needAuth) {
    const userinfo = await store.dispatch('user/getUserInfo') // 获取用户信息
    opts.data.openid = store.getters.openid
    opts.data.token = store.getters.token
    opts.data.unionid = store.getters.unionid
  }
  return handleRequest(url, opts)
}

/**
 * 请求处理
*/
async function handleRequest(url, opts, isUpload) {
  opts.data.platform = PLATFORM.platform
  opts.data.appId = PLATFORM.appid
  let err = null
  let res = null
  if (isUpload) {
    [err, res = {}] = await uni.uploadFile({
      url: url,
      filePath: opts.file,
      name: opts.name,
      formData: opts.data
    })
  } else {
    [err, res = {}] = await uni.request({
      url: `${API_BASE}${url}`,
      header: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      method: opts.method || 'POST',
      timeout: opts.timeout || NETWORK_TIMEOUT,
      data: opts.data || {}
    })
  }
  err && console.warn(`${url}接口错误:`, err)
  opts.debug && console.info('接口结果:', res)
  return new Promise((resolve, reject) => {
    if (err) {
      if (opts.handleFail !== false) {
        showToast(NETWORK_ERR_MESSAGE)
        return reject(res)
      }
    } else {
			// 404, 502等服务器错误信息
      if (typeof res.data !== 'object') res.data = { status: res.statusCode, message: DEFAULT_ERR_MESSAGE, data: res.data } 

      if (res.data.status === '200') {
        return resolve(res.data)
      } else if (res.data.status === '403' && opts.handleLogin !== false) { // 未登录或者登录失效
        showToast('请先登录', 1000)
        return uni.navigateTo({
          url: '/pages/login/login'
        })
      } else {
        if (opts.handleFail !== false) {
          showToast(res.data.message || DEFAULT_ERR_MESSAGE)
        }
        return reject(res)
      }
    }
  })
}

说明,以上store为基于vuex的数据管理,下文会实现

第三步,实现getpost以及upload方法

export function get(url, opts = {}) {
  opts.method = 'GET'
  return request(url, opts)
}

export function post(url, opts = {}) {
  opts.method = 'POST'
  return request(url, opts)
}

/**
 * @param {url} 上传地址
 * @param {file}  文件路径
 * @param {name} 文件的表单名称
 * @param {formData} 额外需要提交的字段
 * */
export async function upload(url, file, name = 'file', formData = {}) {
  if (!file) return
  formData.openid = formData.openid || store.getters.openid
  if (formData.needAuth) {
    const userinfo = await store.dispatch('user/getUserInfo')
    formData.openid = userinfo.openid || formData.openid
    formData.token = userinfo.token
  }
  return handleRequest(url, {
    file: file,
    name: name,
    data: formData
  }, true)
}

第四步,导出响应的方法:

function install(Vue) {
  Vue.prototype.$request = request
  Vue.prototype.$get = get
  Vue.prototype.$post = post
  Vue.prototype.$upload = upload
}
export default { install }

第五步,安装方法

打开main.js文件,安装对应的方法

import request from '@/utils/request'

Vue.use(request)

做完以上工作后,我们可以像下面的方式在页面上调用http相关的方法


export default {

  onLoad(){
	this.getOrders()
  },
  methods: {
	getOrders(){
       this.$get('orders', {
         needAuth: true,
         data: {
			userid: this.$store.getters.userinfo.id
         }
       }).then(res => {
		 console.log(res)
       }).catch(res => {
		 console.warn(res)
       })
    }
  }

}

3. 小结

以上通过Vue.use安装的方式,在Vue原型上扩展公共的方法,从而达到可以在页面(Vue实例)上调用对应的方法来实现调用接口等功能,如果需要扩展更多的方法和属性,均可以以该种方式进行扩展。

(三)路由拦截

路由拦截几乎是每一个应用都需要用到的功能,比如页面跳转前的登录判断。在Vue框架开发的单页应用中,我们会结合Vuex-Router来实现响应的功能,Vue-Router提供了丰富的路由守卫,实现路由的拦截十分便利。然而,在小程序上实现路由拦截是相对比较麻烦的。uniapp社区上也有一些解决方案,比如一个完全相似Vue-router的路由插件,该插件重写了uniapp的一些方法以及生命钩子,从而实现了绝大部分的Vue-Router的钩子,有兴趣的可以使用该插件。

实际情况是,我们往往不需要像Vue-Router那样完整的路由拦截,有时我们仅仅只需要登录前的一层简单的校验,这样如果引入一个完整的路由拦截解决方案会显得有点冗余,同时,也会让应用的包更加大(小程序均对单个包有大小的限制)。面对这种情况,我们仅需要做一些简单的扩展即可达到我们的需要。以下,以登录拦截为例,简单的实现一下路由的拦截。我们的需求是,对于需要登录才可以进入的页面,我们先做一下登录校验,如果未登录,则跳转前先跳转至登录页面。

在utils目录下创建extends.js文件,该文件专门用来对uniapp框架做一些额外的扩展。添加如下代码:

import store from '@/store'

export default function() {
  ['navigateTo', 'redirectTo', 'switchTab', 'navigateBack'].map(item => {
    const nativeFunc = uni[item]
    uni[item] = function(opts, needAuth) {
      if (needAuth) {
        store.dispatch('user/getUserInfo').then((res) => {
          nativeFunc.call(this, opts)
        })
      } else {
        return nativeFunc.call(this, opts)
      }
    }
  })
}

代码解读:对于路由相关的几个方法进行遍历并缓存原方法,在uni对象上重写方法,每个方法增加needAuth参数,表示跳转前是否需要登录

如果需要登录,则首先获取用户信息,如果获取不到用户信息,将会自动跳转到登录页(store里面的user模块做的),否则获取到信息就调用缓存起来的原方法,实现跳转
如果不需要登录,则直接调用原方法

打开main.js,引入该文件并执行

import uniExtend from '@/utils/extends'

uniExtend()

通过一个简单的扩展,我们就实现了一个跳转前的登录拦截。除了登录拦截外,针对uniapp上提供原生方法,均可以以该方式进行扩展。

由于小程序的限制,拦截必须调用uniapp上对应的路由路由方法,通过其他途径跳转页面,无法拦截。包括以上社区上提供的拦截方案一样受这个限制

(四)状态管理器Vuex

一个集中式的状态管理方案在一个应用中显得尤为重要,而uniapp本身是支持整合Vuex的。因此,我们也在该项目中,也集成了Vuex。

首先,在根目录下创建store目录,专门用于存放状态管理相关的文件。为了让所有的状态结构更加清晰,更好维护,我们采用vuex的modules来管理各个模块的数据。我们在store目录下创建modules目录index.js文件,前者用于管理各个数据模块,后者用于导出相关的数据。此时,项目的目录结构如下:

uniapp多端构建实战初探_第6张图片

接下来,我们以用户模块为例,来实现一个基于vuex的状态管理器。

打开store/index.js文件,写入以下代码

import Vue from 'vue'
import Vuex from 'vuex'

const modules = require.context('./modules', true, /.js$/)
let moduleArr = []
modules.keys().map(key => {
  moduleArr[key.replace(/(\.\/)|(\.js)/ig, '')] = modules(key).default
})

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    ...moduleArr
  }
})

export default store

说明:此处读取modules目录下的所有文件,获取所有的module,这要求module目录下的文件都是导出一个vuex模块。你可以按照实际情况,修改以上代码

接着,打开main.js文件,添加如下代码:

import store from './store'

const app = new Vue({
  store,
  ...App
})

到这里,已经简单的实现了在应用上使用Vuex。接下来,添加用户用户模块,在modules目录下创建user.js文件。通常一个module包含statemutationsactions。首先我们实现一下state,代码如下:

const KEYS = ['token', 'openid', 'unionid']

const getDefaultState = () => {
  const result = {}
  KEYS.map(key => {
    result[key] = uni.getStorageSync(key) || ''
  })
  return result
}

const state = getDefaultState()

定义一个state函数,用于设置进入页面时默认的state,我们会优先读取本地缓存里的用户授权信息,每次登录授权后,会将授权信息存入本地缓存。在这里,我们授权的信息包含token,openid和unionid三个信息(正常应该走小程序的授权等操作,这里仅作数据模拟)。接着实现一下mutations,代码如下:

const mutations = {
  SET_AUTH: (state, info) => { // 设置token等信息
	KEYS.map(key => {
	  if (typeof info[key] !== 'undefined') {
		state[key] = info[key]
	  	uni.setStorageSync(key, info[key])
	  }
	})
  }
}

在这里,我只定义了一个设置授权信息的mutation,传入授权后的信息,如果对应的授权信息不为undefined,则将信息设置到state并且缓存在本地。然后再实现一下actions。actions负责登录以及用户信息的获取。主要包括登录、登出、获取用户信息和授权信息等,实现分别如下:

  • 获取授权信息

如果已经授权过了,则直接返回授权信息,否则的话会跳转至登录页,进行登录

  getAuth({ state }) { // 获取用户授权信息
    return new Promise(resolve => {
      if (state.token) { // 已授权,直接返回
        resolve({
          openid: state.openid,
          token: state.token,
          unionid: state.unionid
        })
      } else { // 未授权,跳转到登录页
		uni.navigateTo({
			url: `/pages/login/login`
		})
	 }
   })
 }
  • 登录与登出
login({state, commit, dispatch}, data) {
  return new Promise((resolve, reject) => {
	setTimeout(() => {
	  if(data.name === 'test' && data.password === 'test') {
		const data = {
		  token: "testToken",
		  openid: "testOpenId",
		  unionid: "testUnionId"
		}
		commit('SET_AUTH', data)
		resolve(data)
	  } else {
		reject({
		  message: "账号或密码错误",
		  code: '100401'
		})
	  }
		
	}, 1000)
  })
},
logout({ commit, dispatch }) { // 退出登录
  commit('SET_AUTH', {
    openid: '',
    token: '',
    unionid: ''
  })
  return Promise.resolve()
}

以上模拟了一个账号登录,当登录后,返回token等授权信息,并调用commit('SET_AUTH', data)更新state以及本地缓存;登出时则清空所有的登录授权信息。最后再实现用户信息获取的action:

  getUserInfo({state, commit, dispatch}) { // 本地获取用户信息
    return new Promise(resolve => {
	  dispatch('getAuth').then(res => {
		resolve({
		  username: "test",
		  age: 18,
		  email: "[email protected]"
		})
	  })
  })
}

当获取用户信息时,首先会进行登录授权,授权成功后则模拟返回用户信息。到这里一个简单的与用户相关的vuex模块就完成了。最后就是导出模块供应用使用:

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

为了方便页面访问state数据,通常我们还会创建一些getters。在store目录下新建文件getters.js,添加如下代码:

const getters = {
  openid: state => state.user.openid,
  token: state => state.user.token,
  unionid: state => state.user.unionid
}

export default getters

打开store/index.js,修改代码如下:

import getters from './getters'

const store = new Vuex.Store({
  modules: {
    ...moduleArr
  },
  getters
})

这样,就可以在页面上使用this.$store.getters.token以及使用Vuex的mapGetters方法来访问我们的getters了。

mapGettersmapState等辅助函数均可以使用

到这里,一个简单的数据管理器就算完成了,如果需要添加更多的数据状态,可以按照用户模块的组织方式对数据进行管理。有了数据管理以及数据请求等基础模块,就可以实现具体的业务页面。下面我们以用户登录与信息获取为例,实现一个简单的业务页面。

(五)一个简单的例子–登录与信息获取

(1)修改pages/index/index为如下代码

<template>
  <view class="container">
	<view>用户名:{{userinfo.username}}view>
	<view>年龄:{{userinfo.age}}view>
	<view>邮箱:{{userinfo.email}}view>
	<button @tap="logout" type="warn" style="margin-top: 30rpx;" v-if="userinfo.username">退出登录button>
	<button @tap="getUserinfo" type="primary" style="margin-top: 30rpx;" v-else>获取用户信息button>
  view>
template>
export default {
  data() {
	return {
	  userinfo: {
		username: "",
		 email: "",
		age: ""
	  }
	}
  },
  methods: {
	getUserinfo() {
	  this.$store.dispatch('user/getUserInfo').then(res => {
		this.userinfo = res
	  })
	},
	logout() {
	  this.$store.dispatch('user/logout').then(() =>{
		this.userinfo.username = ""
		this.userinfo.age = ""
		this.userinfo.email = ""
	  })
	}
  },
  onUnload() {
	uni.$off("loginSuccess", this.getUserinfo)
  },
  onLoad() {
	uni.$on("loginSuccess", this.getUserinfo)
  }
}

uni.$offuni.$on为uniapp的通信方案,后文会讲述

加上样式后,效果如下:

uniapp多端构建实战初探_第7张图片

接着实现登录页面。在pages目录下新建login/login.vue文件。添加如下代码:

<template>
  <view class="wrap">
    <view class="form-item">
      <input v-model.trim="name" type="text" placeholder="请输入用户名" class="input">
    view>
    <view class="form-item">
      <input v-model.trim="password" type="password" placeholder="请输入密码" class="input">
    view>
    <view class="button" @tap="submit">登录view>
  view>
template>
export default {
  data() {
    return {
      name: '',
      password: ''
    }
  },
  methods: {
    valid() {
      if (!this.name) {
        return this.$toast('请输入用户名')
      }
      if (!this.password) {
        return this.$toast('请输入密码')
      }
      return true
    },
    submit: function() {
      if (!this.valid()) return
      uni.showLoading({
      	title: "正在登录..."
      })
	  this.$store.dispatch('user/login', {
		name: this.name,
		password: this.password
	  }).then(res => {
		console.log(res)
		uni.$emit("loginSuccess", res)
		uni.navigateBack()
	  }).catch(res => {
		this.$toast(res.message)
	  }).finally(() => {
		uni.hideLoading()
	  })
    }
  },
  onLoad() {
	this.$store.dispatch('user/logout')
  }
}

逻辑比较简单,进入页面后使用this.$store.dispathc('user/logout')清空原有的登录状态,登录成功后发布事件loginSuccess并返回上一页,效果如下:

uniapp多端构建实战初探_第8张图片

以上两个页面完成后,完整的操作演示如下:

uniapp多端构建实战初探_第9张图片

(六)数据通信方案

在uniapp中,数据的通信通常包含组件之间的通信以及页面之间的通信。同样,在小程序中一般也包含这两种通信。在这里我们只讨论页面之间的数据通信(组件之间的通信参照Vue官方的组件通信)。一般情况下,在uniapp中我们有以下的几种方式来实现页面之间的通信:

  1. 使用url传参
  2. 使用本地缓存
  3. 使用全局数据(如小程序的globalData)
  4. 使用getCurrentPage方法获取对应的页面进行通信
  5. 使用eventBus
  6. 使用uniapp自带的事件发布订阅
  7. 使用Vuex

以上几种方案中,前4中方案在应用变得越来越复杂的时候,会使页面之间的耦合度变得越来越高,从而不利于应用的维护。因此,通常页面之间的数据通信我们会采用后三种方案。比如前面的登录操作的例子中,则是使用了uniapp自带的事件发布订阅通信的方案。其本质上也是一个eventBus。

当然,这不是说前4中方案就不能在应用中使用,某些场景使用前四种方案可能会更简洁、方便、甚至是必须的。比如通过url参数区分渠道,记住某些状态(本地缓存)等。在实际操作的过程中,应该灵活选择、灵活应用。

uniapp的事件发布订阅,请参考官方文档:页面通信

使用的时候需要注意以下几点:

  • 订阅了事件后在页面销毁时记得取消订阅,使用uni.$off
  • 使用uni.$off建议传递第二个参数,取消当前页面的订阅,而不是取消所有的订阅。这需要在订阅的时候也传入第二个参数,两个回调应指向同一个引用

(七)自定义组件

组件化是Vue的一个核心之一,使用uniapp同样支持自定义组件。但是,自定义组件在各小程序中,由于实现的方案并不一致,所以自定义组件并不总是能完美的运行在各个端的小程序中。这需要在实现的过程做一些特殊的兼容处理。查看uniapp官方的组件库源码,同样也会为各个平台写一些兼容代码。本文不详细介绍如何一步一步的写一个自定义的组件。按照官方组件库的规范,实战中我们通常会遵循以下几个原则:

  • 与业务无关的通用组件放置在根目录的下的compoments目录下,同时,需要放置在额外的目录,方便统一管理,如components/my-component
  • 业务组件放置在对应页面模块的components目录下,如pages/login/components/phoneLogin/phoneLogin.vue
  • 组件的命名方式采用横线连接的方式,如my-dialog
  • 组件需要放置在同名目录下,如components/my-components/my-dialog/my-dialog.vue
  • 组件的样式类型最好采用BEM规范的命名方式
  • 如果需要扩展uniapp官方的组件,不应该修改源码,采用拷贝的方式拷贝到自定义的组件目录,同时,组件命名不要以uni-开头

四、常见问题与解决方案

1. 使用@import引入外部样式无效或者报错

uniapp官方给出的引用外部的样式方式为@import url('./common.scss'),但应用时发现编译报错或者不生效,此时改为@import './common.scss'即可。

2. placeholder-class不起作用

在小程序中,可以使用placeholder-class来修改页面的输入框占位符的样色、字体大小等。使用时会发现自定义组件不起作用,页面样式如果加了scoped属性也会不起作用。解决办法为:

  • 自定义组件中输入框使用placeholder-style替代
  • 非自定义组件中的输入框可以在App.vue全局设置,并增加important关键字(百度小程序无效)

textarea的处理方法一样。

3. v-show不起作用
使用v-show指令时,某些情况下(如基于数组的length属性来判断),在小程序下不会正常工作,因为小程序的hidden属性是不响应该变化的,可使用v-if代替

4. $refs不起作用

uniapp中可以使用$refs获取页面对应的组件,但是前提是在小程序中组件为自定义组件,原生的组件无法获取。另外,在支付宝小程序中,使用$refs只能获取到一级的组件引用,嵌套的自定义组件无法获取,但是可以通过签到的$refs来获取到,如下:

<template>
  <popup ref="popup">
    <view class="picker">
       <region ref="picker" />
    view>
  popup>
template>
  mounted() {
    this.popup = this.$refs.popup
    // 支持宝不支持嵌套的ref
    this.picker = this.$refs.picker || this.popup.$refs.picker 
  }

5. 包大小过大,无法上传

小程序对包的大小有限制,不同的小程序限制不一样。一般我们会采用分包的方式来处理。但是在开发模式下,由于uniapp编译时会附带sourcemap,导致分包也会很大,从而调试时无法上传到真机进行调试,如QQ小程序。此时,我们在编译时可以选择压缩代码,同时,在小程序开发者工具也选择上传时自动压缩即可。如下:

  • 在HBuilderX选择运行 - 运行到小程序模拟器 - 勾选运行时压缩
  • 在开发者工具选择详情 - 本地设置 - 勾选上传代码时自动压缩混淆

6. textarea穿透和错位问题

textarea穿透以及滚动的问题属于小程序的问题。textarea属于原生组件,在放置到具有fixed定位的弹窗之类的容器时就会出现这样那样的问题。通常我们有两种解决方案:

  • 弹窗展示前先隐藏掉textarea或者使用其他的元素代替,弹窗展示完毕后再显示textarea,这样可以避免弹窗位置错位的问题
  • 输入组件失去焦点时替换为view组件,获得焦点时恢复为textarea组件

五、总结

多端构建的解决方案让我们写一套代码运行到多个端成为了可能,大大减低了开发者负担,也节省了企业的资源。但由于平台不可避免的差异性,仍然存在不少问题需要开发者自行去解决。另外,快应用目前支持的程度非常弱,uniapp官方已经支持了基于webview渲染的快应用,但是依然存在不少问题,同时,该模式目前仅华为/OPPO/Vivo支持,其他联盟厂商并不支持。基于原生快应用模式的渲染,uniapp计划由社区来实现,目前仍旧没有好的解决方案。而从Taro官方文档来看,Taro的支持应该也比较有限(未测试,欢迎告知测评结果),可能不能用于比较复杂的应用。

无论是Taro也好,uniapp也罢,目前社区都在不断的完善和壮大。相信在国内小程序厂商、快应用联盟以及社区的努力下,多端构建的方案会日趋成熟,并能够形成一套更加统一的、优秀个解决方案。

本文项目代码请点击此处查看:

Github

码云

你可能感兴趣的:(uniapp,vue.js,javascript)