uni-app完整实战

背景

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

多端构建方案选型

目前多端构建有不少的解决方案,通常在技术选型方面,我们会考虑以下几个维度:

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

从社区上的各种实战案例和测试案例来看,目前Tarouni-app是相对比较成熟、完善的多端解决方案。uni-app作为国内小程序的开创者,也制定了一套基于Vue.js的解决方案,并跻身前列,成为多端构建解决方案的优秀者之一。本文将就uni-app作为多端方案,讲解如何使用uni-app多端构建框架一步一步搭建一个完整的项目,并就搭建过程中遇到的一些问题给出一些解决方案。

参考快递100小程序的案例,快递100算是国内小程序中服务类小程序一直排名靠前的小程序了,其目前也是基于uni-app实现的多端统一的小程序,从效果来看,使用uni-app作为多端统一的方案,不会有啥大的问题。

项目搭建

创建项目

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

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

uni-app项目目录结构

各目录和文件说明如下:

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

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

uni-app小程序运行效果

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

自定义全局API

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

反馈类UI相关API封装

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

(1)toast & modal

utils目录下创建interactiveFeedback.js文件

第一步,新建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('取消了')
})

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)
      })
    }
  }
}

小结

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

路由拦截

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

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

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

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()

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

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

状态管理器Vuex

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

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

uni-app Vuex目录结构

接下来,我们以用户模块为例,来实现一个基于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,openidunionid三个信息(正常应该走小程序的授权等操作,这里仅作数据模拟)。接着实现一下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并且缓存在本地。然后再实现一下actionsactions负责登录以及用户信息的获取。主要包括登录、登出、获取用户信息和授权信息等,实现分别如下:

  • 获取授权信息

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

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: "test@test.test"
      })
    })
  })
}

当获取用户信息时,首先会进行登录授权,授权成功后则模拟返回用户信息。到这里一个简单的与用户相关的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以及使用VuexmapGetters方法来访问我们的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.$onuni-app的通信方案,后文会讲述

加上样式后,效果如下:

uni-app获取用户信息页

(2)接着实现登录页面。在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.dispatch('user/logout')清空原有的登录状态,登录成功后发布事件loginSuccess并返回上一页,效果如下:

uni-app用户登录页

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

uni-app完整操作演示

数据通信方案

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

  • 使用url传参
  • 使用本地缓存
  • 使用全局数据(如小程序的globalData)
  • 使用getCurrentPage方法获取对应的页面进行通信
  • 使用eventBus
  • 使用uni-app自带的事件发布订阅
  • 使用Vuex

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

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

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

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

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

自定义组件

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

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

在使用自定义组件时,编译到各个端时并不是一帆风顺的,这主要是不同平台的小程序自定义组件实现的方式不同。如果在支付宝下,可以通过设置编译方式为component2来尝试解决。具体参考常见问题

常见问题与解决方案

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

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

  1. placeholder-class不起作用

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

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

textarea的处理方法一样

  1. v-show不起作用

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

  1. $refs不起作用

uni-app中可以使用$refs获取页面对应的组件,但是前提是在小程序中组件为自定义组件,原生的组件无法获取。另外,在支付宝小程序中,使用$refs只能获取到一级的组件引用,嵌套的自定义组件无法获取,有两种解决方案:

(1)可以通过嵌套的$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 
}

(2)通过将支付宝的编译模式改为component2编译模式,详情可查看这里,代码如下:

"mp-alipay": {
  "uniStatistics": {
     "enable": true
  },
  "component2": true
}
  1. 包大小过大,无法上传

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

  • HBuilderX选择运行 - 运行到小程序模拟器 - 勾选运行时压缩
  • 在开发者工具选择详情 - 本地设置 - 勾选上传代码时自动压缩混淆
  1. textarea穿透和错位问题

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

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

编译后,在支付宝下会提示子组件不能修改父组件的props,如Avoid mutating a prop directly since the value will be overwritten ....Prop being mutated "value"。这个错误就算是没有在子组件修改父组件的属性也会报出来。原因是支付宝以及钉钉等支付宝系的小程序开发者工具的一个bug,实际上组件是正常工作的,但是会错误的报出这条信息,这个忽略即可,后续版本官方应该会修复。

  1. inject/provide不起作用

在自己写一些自定义组件或者使用uni-ui扩展组件(如swipe-action)的时候,在支付宝下会报Injection "xxxx" not found,如Injection "swipeaction" not found的错误,这个是因为没有开启component2编译模式的原因,开启了即可解决。参考常见问题第4点

总结

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

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

源码:https://github.com/prianyu/uniapp
本文转载至:https://juejin.im/post/6868484714979098638

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/18/uni-app-complete-combat/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
uni-app完整实战
背景 小程序具有跨平台、体验好、高灵活性以及即用即走、无需下载安装诸多优势。随着微信团队推出微信小程序,国内各大互联网团队也相继推出了各自的小程序,……
<<上一篇
下一篇>>
文章目录
关闭
目 录