😭血泪合集!uniapp小程序开发的超长实践总结!

65,970 阅读18分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

在经历了多个uniapp小程序项目开发后,我将我所有的踩坑实战经验复盘总结,以下内容仅针对uniapp开发微信小程序的场景,其中除了uniapp外还有一些小程序开发的注意点,希望这篇文章可以帮助大家避坑。以下全文干货(干到口渴那种)

如果你期待或者需要一个开源的微信小程序社区,可以 oil社区 gitee链接 关注我的开发进度噢,欢迎⭐star

uniapp简介

先放官方介绍,如果熟悉已经了解uniapp的同学可以跳过。

uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/快手/钉钉/淘宝)、快应用等多个平台。

使用uniapp的话我们可以使用 vue.js 的语法来写 小程序 ,简而言之就是将常规vue.js开发中 template 模板从 html 替换成 wxml 就可以开发小程序,那么这么做有什么好处呢!

  1. 减少技术学习压力,如果你会vue的话你可以直接上手,如果你不会vue的话顺便把vue学了,而且还可以享受到vue的周边生态福利
  2. 语法相交于原生的小程序语法更加方便,如果你开发过小程序那你一定会感受到 this.setData()原生小程序组件 开发的麻烦与恐惧
  3. uniapp除了可以打包成各个平台的小程序还可以打包成app,具体不列举,如官网下图。 image.png
  4. 小程序开发工具的代码提示比较拉跨,虽然相比前几年有很大进步,但是还是没有达到我想要的水平。

uniapp开发准备

ide工具安装

HBuilder官网链接

工欲善其事,必先利其器。开发uniapp的第一步是安装一个官方的ide工具 HbuilderX ,对于开发uni-app来说我只推荐这一个ide工具,确实非常方便,可以帮助我们快速生成页面模板和组件模板可视化的页面配置优秀的代码提示代码补全能力 等等。

HbuilderX 相比 vscode 还是有一些不足之处,一些插件生态不是很健全。但是开发uni-app就是非 HbuilderX 莫属了(毕竟是官方的东西)

HbuilderX 的安装非常方便,直接在官网下载安装就OK了。下载完成后我们需要安装一些插件,打开HbuilderX ,在顶部栏选择工具->插件安装,如下图

image.png

打开后我们可以看到当前已安装的插件一些官方插件,这里由于年代久远我不记得最初的时候哪几个没安装了,但是下面红框圈起来的是一定要安装的,一个是用于git版本管理的插件,一个是用于编译sass的插件。

image.png

我们还可以设置我们的编辑器主题和字符大小,代码缩进等,如果你是有其他编辑器使用习惯的可以适当调整。我原本是使用vscode进行开发的,所以我切换成了 雅蓝 的主题,实际页面效果和vscode one dark pro编辑器风格代码颜色 一模一样!

image.png

以上步骤我认为都是必要的,舒服美观的开发环境可以极大的提升你的开发兴趣!

项目目录结构分析

新建项目

HbuilderX 安装配置完毕后,我们就可以开始开发了,首先我们需要创建一个项目,我们点击ide左上角 文件-> 新建 ->项目 ,然后我们选择 uniapp 项目 ,模板选择 默认模板

image.png

创建完成后,我们可以看到左侧文件树中新增了一个以项目名命名的文件,其中是hbuilder为我们内置的项目模板

image.png

以下是uniapp给我们的项目框架介绍,有一些文件夹是没有在模板中内置的,因此我们需要自己手动创建以下,例如最外层的components,用来放置我们的一些全局通用组件

    

┌─components            符合vue组件规范的uni-app组件目录
│  └─comp-a.vue         可复用的a组件
├─pages                 业务页面文件存放的目录
│  ├─index
│  │  └─index.vue       index页面
│  └─list
│     └─list.vue        list页面
├─static                存放应用引用的本地静态资源(如图片、视频等)的目录,注意: 静态资源只能存放于此
├─uni_modules           存放[uni_module](/uni_modules)规范的插件。
├─wxcomponents          存放小程序组件的目录,详见
├─main.js               Vue初始化入口文件
├─App.vue               应用配置,用来配置App全局样式以及监听 应用生命周期
├─manifest.json         配置应用名称、appid、logo、版本等打包信息,详见
└─pages.json            配置页面路由、导航条、选项卡等页面类信息,详见
    

如下图,在开发的过程中,我依据vue项目的开发习惯,在pages中依照业务功能来创建功能分类文件夹,最常见的是按照首页的tabbar来区分功能模块,每一个文件夹中存放该功能涉及的所有页面,且每一个功能文件夹中还有一个单独的components文件夹用于放置该仅功能文件夹中的页面依赖的组件。

image.png

新建页面

我们需要创建新页面的时候可以通过hbuilder内置的页面模板来快速创建,右键点击左侧文件树中当前的项目,选择 新建页面 ,输入页面名称以及选择模板就可以创建了,一般我选择的是scss的模板,创建完成后会自动帮你在page.json中注册该页面。

image.png

通用插件封装

既然uniapp选择使用vue.js作为开发框架,那么我们一定要利用上vue中的一些优秀特性,例如插件(plugin),关于vue插件的介绍大家可以直接去官网看。

vue 插件官方介绍链接

通过引入插件,我们可以极大的提升我们的开发效率,当然如果是第一次使用uniapp进行开发可能不清楚哪个功能适合封装成插件引入,下面我就介绍一下一些我在实际开发中封装的一些通用插件

在封装前我们需要写一个 config 文件,方便我们快速自定义一些颜色和请求路径等。

//congif.js
const config = {
	baseUrl:'https://example.cn',//请求的基本路径
	modalColor:'#5271FF', //弹窗颜色 
}

module.exports = config

弹窗插件

在小程序中,如果我们没有自定义弹窗和拟态框组件的话一般都是使用官方的showModal或者showToast api来进行一些用户交互。这种非常频繁使用到的操作非常适合封装起来快速调用。具体代码如下

插件代码

const config = require('../config.js')

var message = {
	toast(title, type = 'text') {
		if (title.length > 15) {
			console.error('toast长度超过15个字符,当前长度为' + title.length)
			return
		}
		var icon = 'none'
		if (type) {
			switch (type) {
				case 'text':
					icon = 'none'
					break
				case 'suc':
					icon = 'success'
					break
				case 'err':
					icon = 'error'
					break
			}
		}
		uni.showToast({
			title,
			icon
		})
	},
	confirm(title, confirmColor) {
		return new Promise((res, rej) => {
			uni.showModal({
				title,
				cancelColor: '#b6b6b6',
				confirmColor: confirmColor || config.modalColor,
				success: (result) => {
					if (result.cancel) {
						rej(result)
					} else if (result.confirm) {
						res(result)
					}
				}

			})
		})
	},
	async message(content, confrimText) {
		return new Promise((res) => {
			uni.showModal({
				title: '提示',
				content,
				showCancel: false,
				confirmColor: config.modalColor,
				success: (result) => {
					res(result)
				}
			})
		})
	}
}
module.exports = message

示例调用

this.message.toast('回答已删除')

请求插件

在vue中我们常用的请求库是axios,几乎每一个开发者都会将axios进行一个二次封装,以减少调用时的代码量以及进行一些全局配置。在uni-app中我们无须引入第三方的请求库,直接使用官方提供的 uni.request(OBJECT) 这个api来进行请求的调用,当然我们也是要做一个二次封装的。具体代码如下

插件代码

//http.js
const config = require('../config.js')
const message = require('./message.js')
var http = {
	post(path, params, contentType = 'form', otherUrl, ) {
		return new Promise((resolve, reject) => {
			var url = (otherUrl || config.baseUrl) + path
			if (!checkUrl(url)) {
				rej('请求失败')
			}
			uni.request({
				method: 'POST',
				url,
				header: {
					"Content-Type": contentType === 'json' ? "application/json" :
						"application/x-www-form-urlencoded"
				},
				data: params,
				success: (res) => {
					console.log('request:POST请求' + config.baseUrl + path + ' 成功', res.data)
					resolve(res.data)
				},
				fail: (err) => {
					message.toast('请求失败', 'err')
					console.error('request:请求' + config.baseUrl + path + ' 失败', err)
					reject('请求失败')
				}
			})
		})
	},
	put(path, params, contentType = 'form', otherUrl, ) {
		return new Promise((resolve, reject) => {
			var url = (otherUrl || config.baseUrl) + path
			if (!checkUrl(url)) {
				rej('请求失败')
			}
			uni.request({
				method: 'PUT',
				url,
				header: {
					"Content-Type": contentType === 'json' ? "application/json" :
						"application/x-www-form-urlencoded"
				},
				data: params,
				success: (res) => {
					console.log('request:PUT请求' + config.baseUrl + path + ' 成功', res.data)
					resolve(res.data)
				},
				fail: (err) => {
					message.toast('请求失败', 'err')
					console.error('request:PUT请求' + config.baseUrl + path + ' 失败', err)
					reject('请求失败')
				}
			})
		})
	},

	get(path, params, otherUrl) {
		return new Promise((resolve, reject) => {
			var url = (otherUrl || config.baseUrl) + path
			if (!checkUrl(url)) {
				return
			}
			uni.request({
				url,
				data: params,
				success: (res) => {
					console.log('request:GET请求' + config.baseUrl + path + ' 成功', res.data)
					resolve(res.data)
				},
				fail: (err) => {
					message.toast('请求失败', 'err')
					console.error('request:GET请求' + config.baseUrl + path + ' 失败', err)
					reject(err)
				}
			})

		})

	},
	delete(path, params, otherUrl) {
		return new Promise((resolve, reject) => {
			var url = (otherUrl || config.baseUrl) + path
			if (!checkUrl(url)) {
				return
			}
			uni.request({
				url,
				data: params,
				method: "DELETE",
				success: (res) => {
					console.log('request:DELETE请求' + config.baseUrl + path + ' 成功', res.data)
					resolve(res.data)
				},
				fail: (err) => {
					message.toast('请求失败', 'err')
					console.error('request:DELETE请求' + config.baseUrl + path + ' 失败', err)
					reject(err)
				}
			})

		})

	},

	async upload(path, fileArray, otherUrl) {

		if (typeof fileArray !== 'object') {
			console.error('request:参数错误,请传入文件数组')
			return
		}
		var url = (otherUrl || config.baseUrl) + path
		if (!checkUrl(url)) {
			return
		}
		var arr = []
		for (let i in fileArray) {
			const res = await uni.uploadFile({
				url: otherUrl || config.baseUrl + path,
				filePath: fileArray[i],
				name: 'file'
			})
			console.log(res)
			if (res[0]) {
				console.error('request:上传失败', res[0])
				return
			}
			arr.push(JSON.parse(res[1].data).data)
		}
		return arr
	},

}

function checkUrl(url) {
	var urlReg = /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/;
	if (!urlReg.test(url)) {
		console.error('request:请求路径错误' + url)
		return false
	}
	return true
}
module.exports = http

示例调用

async getAnswer() {
  const res = await this.http.get('/applet/answerList', {
    qId: this.question.id,
  })
  if (res.code === 200) {
    return res.data
  } else {
    this.message.toast('请求失败')
    return false
  }
}

在上面的代码中我们可以看到,我们引入两个依赖文件分别是configmessage 这个在下面会说的,在http对象中有五个方法,分别是post,get,update,delete对应着 restful api 的增删查改操作,还有一个upload 方法用来上传文件。在小程序中请求我们不需要考虑跨域的问题,我们的默认请求路径是从 config这个依赖文件中获取的。而这时候我们还可以利用第一个 弹窗插件 快速的进行消息提示。

存储插件

在使用vue的时候我们常常会使用vuex + vuex-persistedstate来使我们的数据可以全局使用以及持久化,在小程序中我们有两种全局数据的存储方式 一种是globalData,通过在App.vue中添加globalData这个值来进行数据全局化,例如我们可以用来保存我们的小程序版本。

export default {
  globalData: {
    version: '1.0.0'
  }
}

但是globalData的数据不是持久化的,当我们退出小程序再进入的时候就重新初始化了,所以我们还有一种全局数据存储方式是storage,类比我们web端的localstroge,使用方式也几乎一样。通过官方提供的api实现本地存储的效果

image.png

在小程序中往往没有web端那么复杂的数据结构,虽然uniapp小程序也可以使用vuex作为一个全局数据管理的库,但我自己还是做了一个简化版的方便自己使用。这个插件是在很久以前写的了,只是非常简单的实现,如果想要省略setData操作可以用 proxy 劫持存储的值去存在本地。

插件代码

var store = {
	_init(){
		if(uni.getStorageSync('store')==''){
			uni.setStorageSync('store',{})
			return
		}
		const data = uni.getStorageSync('store')
		for(let i in data){
			if(!this[i]){
				this[i] = data[i]
			}
		}
	},
	setData(key,value){
		if(key == '_init'||key=='setData'){
			console.error('store:非法key值',key)
			return
		}
		this[key] = value
		uni.setStorageSync('store',this)
		console.log(uni.getStorageSync('store'))
	}
}

module.exports = store

示例使用

this.store.setData('user',{name:'oil'}) // 赋值
console.log(this.store.user) // 读值

表单验证插件

表单验证是使用的是一个很轻量开源的表单验证库JSValidate,可以直接点击链接在仓库看源码,下面就不放代码了,讲一下如何使用。 JSValidate gitee链接

示例调用

// validate.js
const validate = {
	userForm:{
		intro:	'@个人简介|require',
		college: '@学校|require!请选择你的学校',
		nickname:'@昵称|require'
	}
}

module.exports = validate

在表单中写入一个表单验证方法,如果条件不符合的话就使用弹窗进行提示

//form.js
validateForm() {
  // 表单验证方法
  const validate = require("./validate")
  const validator = new this.validator()
  var result = validator.check(validate.userForm, this.form, false)
  if (result !== true) {
    this.message.message(result)
    return false
  }
  return true
}

时间处理插件

关于时间处理插件我在项目中使用的也是一个开源的轻量级js库 day.js,具体的使用方式参考官方文档,day.js几乎包含了所有时间处理相关的操作,例如时间对比,时间转换等。 day.js官方文档链接

day.js其中有一项功能特别常用,就是一个相对时间转换的功能,相对时间转换就是将一个时间字符串例如年月日时分秒转换成几秒前,十分钟前,几个月前等等。使用的方式如下代码,我们使用vue的computed来进行时间字符串的处理,通过 闭包computed方法内传入时间字符串参数,然后通过day.js的formNow方法返回一个中文的相对时间。date就是我封装后引入的插件了。

示例代码

computed: {
  relativeDate() {
    return (date) => {
      return this.date(date).fromNow()
    }
  }
}

当我们拥有了很多个插件之后(如下图),我们的插件之间可能会共享一些配置,例如上文中的config.js,接下来我们可以写一个入口文件index.js用来将我们写的插件引入,然后在main.js中安装我们的插件。

image.png

示例代码

// index.js
const http = require('./lib/http')
const message = require('./lib/message')
const router = require('./lib/router')
const validator = require('./lib/validator')
const date = require('./lib/date')
const store = require('./lib/store')
const to = require('./lib/to')
const oil = {
  message,
  http,
  router,
  validator,
  date,
  store,
  to,
  install(Vue) {
    this.store._init()
    for (let i in this) {
      if (i == 'install') {
        continue
      }
      Vue.prototype[i] = this[i]
    }

    delete this.install
  }
}

export default oil
// main.js
import Vue from 'vue'
import oilUni from './oil-uni/index.js'
Vue.use(oilUni)
app.$mount()

我们在入门文件中定义了一个对象叫做oiloil上除了我们写好的方法还有一个特殊的方法就是installinstall 的作用就是在我们使用Vue.use()的时候会将Vue的构造器传入install方法作为第一个参数,然后我们将所有方法都绑定在Vue的原型链上。

安装后,我们只需要在页面或组件的js中使用 this. + xx插件 就可以访问到封装的插件了。

常用组件封装

插件的作用是将我们的代码进行一个模块化,以便捷的复用一些常用的操作。而组件化则是帮助我们进行一个ui上的封装和复用。由于小程序的场景一般都不是很复杂,所以非常通用的组件也不多,下面只说说我在业务中常用的一些组件库和自己封装的组件

第三方组件库引入

colorUI

首先推荐的就是我几乎在所有小程序项目上都有用的 colorUI,与其说 colorUI 是一个组件库,不如说它是一个样式库。 colorUI github链接

在colorUI中封装了大量的行内样式,我们不需要频繁的在templatestyle间切换,大大的减少了我们的心智负担。可以类比 tailwindcss 这个样式库,但是在HBuilderX中,有着非常健全的colorUI样式的代码提示。其实不止是colorUI。你自己写的样式或者引用的文件在HBuilderX中都会获得很完整的一个代码提示。如下图

image.png

colorUI中并没有为我们提供功能上封装好的组件,但是在它的示例源码中,大部分场景都可以直接将样式和代码复制出来改造一下直接使用,例如图片上传表单组件等。 colorUI示例页面

colorUI的引入也非常简单,将colorUI的包放在项目根目录下,然后在App.vuestyle中引用就OK了,colorUI包含了三个文件,一个是main.css,里面是colorUI所有的样式代码,icon 是colorUI的图标,animation 是colorUI的动画,三个包的功能都是通过在 行内class 中写入指定代码来使用。

image.png

//App.vue
<style>
	@import "colorui/main.css";
	@import "colorui/icon.css";
	@import "colorui/animation.css";
</style>

vant-weapp

vant几乎是vue生态在移动端个人认为最好用的组件库了,几乎所有场景的组件都有涵盖到,而vant有一个小程序专用的 weapp 版本。 vant weapp 官方文档

vant weapp 是用原生小程序的语法写的,和uniapp中的vue语法有些出入,所以引入的时候需要做一些特殊操作,下面说一下 vant weapp 如何引入。

  1. 首先需要下载vant weapp,将vant文件夹放在项目根目录下的wxcomponent文件夹里,没有就新建一个。 image.png

  2. App.vue中引入一下样式文件,

<style>
	@import "/wxcomponents/vant/common/index.wxss";
</style>
  1. 接下来我们就可以在页面中引入了,引入并不能直接在页面或组件中import,而是得按照小程序的使用方式在pages.json中引入,如下代码,在指定页面的 usingComponents 配置项中添加 组件名:组件路径 ,然后就可以直接在页面中使用了
{
  "pages": [ 
    {
      "path": "pages/home/home",
      "style": {
        "navigationBarTitleText": "",
        "enablePullDownRefresh": false,
        "usingComponents": {
          "van-progress": "/wxcomponents/vant/progress/index"
        }
      }
    }
  ]
}

要注意vant weapp官方文档中的语法是小程序的,自己转换成vue的就OK了

滚动容器组件

在引入以上两个组件库后,咱们开发时的大部分场景都可以在其中找到对应组件,顶多是进行一个二次封装以适配业务。

常见业务场景

登录页面

在小程序中,登录往往不像web端需要输入账号密码或者手机验证码登录,而是使用各个平台的快速鉴权功能,例如微信小程序就是可以通过向微信官方的api发送请求来获取用户信息。

获取用户信息

因此我们在设计页面的时候往往只需要非常简单的一个按钮来获取用户信息,下面我将用一个项目中的登录功能来给大家展示uniapp实现微信小程序登录获取用户信息的方法

<template>
	<button class="cu-btn bg-blue shadow-blur round lg" @click="login">
            立即登录
	</button>
</template>
<script>
export default {
    methods: {
	async login() {
            uni.showLoading({
		mask: true,
		title: '登录中'
		})
            const res = await uni.getUserProfile({
		desc: '用于存储用户数据'
		})
	uni.hideLoading()
	if (res[0]) {
		this.message.toast('获取失败', 'text')
		return
	}
        this.getUser(res[1].userInfo)
        },
        async getUser(userInfo) {
            uni.showLoading({
                    mask: true,
                    title: '登录中'
            })
            const loginRes = await uni.login()
            if (loginRes[0]) {
                    uni.hideLoading()
                    this.message.toast('获取失败')
                    return
            }
            const res = await this.http.post('/applet/weChatLogin', {
                    code: loginRes[1].code
            }, 'form')
            uni.hideLoading()
            if (!res.data) {
                    this.router.push('/pages/form/userForm', {
                            userInfo,
                            handle: 'add'
                    })
                    return
            }
            this.store.setData('user', res.data)
    },}
        

我们来分析一下上面的代码,首先我们使用 uni.getUserProfile api来获取用户的基本信息,例如头像,昵称,地区等。接下来我们调用 uni.login 获取code值。 code值是用来传递给后端获取微信给用户的 唯一标识 openid 的。

我将code码传递给后端,后端解析出openid并在数据库中查看当前用户是否已有用户信息,没有的话则为第一次登录。第一次登录我们就跳转至表单页面 完善个人信息获取用户手机号码

后端的具体代码在这里不详细展开说,只说整体实现过程。

获取用户手机号码

获取用户号码有一个必要的条件就是点击open-typegetPhoneNumberbutton 标签触发,且需要使用 getphonenumber 绑定一个事件。点击这个按钮时,微信会弹出一个授权获取手机号码的框框,用户选择了同意的话就会触发我们的 getphonenumber 方法。

getphonenumber 方法的参数e.detail中有两个属性 encryptedData(加密数据)iv(初始化矢量) 这两个参数是用来传递给后端进行手机号码的解析的,后端的具体流程比较复杂,下次单出一篇文章讲。

还有一点非常非常重要的,如果你是一个社区型的应用,用户可能发表一些评论和文章,那么咱们保存的用户头像就必须一直有效,如果我们使用微信官方提供的默认头像链接直接存储在数据库,那么当用户换头像之后,原先的链接就会失效

所以我们需要做的是使用 uni.downloadFile api将用户的头像下载为本地文件,再将本地文件上传至服务器,用户头像就会一直有效了!

具体代码如下,已经将业务抽离了,只剩核心代码。

<button class="cu-btn bg-blue shadow-blur round lg" open-type="getPhoneNumber"
        @getphonenumber="getTel" @click="getCode">确定提交
</button>
async uploadAvatar() {
        // 上传用户头像
         if (this.updateImg) {
                var uploadRes = await this.http.upload('/applet/file/uploadFile', [this.form.avatarUrl])
        }
        else if (this.userInfo) {
                var downloadRes = await uni.downloadFile({
                        url: this.userInfo.avatarUrl
                })
                if (downloadRes[0]) {
                        uni.hideLoading()
                        this.message.toast('登录失败')
                        return
                }
                var uploadRes = await this.http.upload('/applet/file/uploadFile', [downloadRes[1].tempFilePath])
        }
         else {
                return
        }
        this.form.avatarUrl = uploadRes[0]
},
async getTel(e) {
        // 首次登录,获取用户手机号码
        if (!e.detail.encryptedData || !e.detail.iv) {
                this.message.toast('登录失败')
                return
        }
        uni.showLoading({
                mask: true,
                title: '登录中'
        })
        await this.uploadAvatar()
        this.form.tags = this.tag.arr.map(item => item.name).join(',')
        const res = await this.http.post('/applet/authorization', {
                code: this.code,
                iv: e.detail.iv,
                encryptedData: e.detail.encryptedData,
                userInfo: this.form
        }, 'json')
        uni.hideLoading()
},

列表页面

下面讲讲咱们非常非常常见的一个业务场景,那就是 列表页面,列表页面看似简单,实则暗藏玄机。例如下拉刷新,上滑加载,没有更多内容的提示,关键字搜索,标签栏匹配,空白页面,列表项组件的复用等等。在一开始就设计完整会对开发效率和整体的代码健壮性有很大帮助!

我将用我实际开发中的代码示例为大家演示一个列表的功能实现。还是一样,我抽离了业务代码只剩核心代码!

<!-- 页面代码 -->
<scroll-view
  :scroll-y="true"
  style="height: 100vh"
  :refresher-enabled="true"
  :refresher-triggered="list.flag"
  @refresherrefresh="refresh"
  @scrolltolower="getAnswer"
  :enable-back-to-top="true"
  :scroll-into-view="list.scrollId"
  :scroll-with-animation="true"
>
  <view style="position: relative" class="bg-white">
    <view v-for="(item,index) in list.data" :key="index" >
      <answer-item
        :data="item"
      ></answer-item>
    </view>
  </view>
  <view
    class="cu-load bg-gray text-main loading"
    style="height: 40px"
    v-if="!list.nomore"
  ></view>
</scroll-view>

// js代码
export default {
components: {
        AnswerItem,
},
data() {
        return {
                list: {
                        data: [],
                        flag: false, //是否展示刷新
                        limit: 10,
                        total: 0,
                        nomore: false, //是否显示到底
                        empty: false, //是否显示为空,
                        error: false, //是否请求错误,
                }

        };
},
async onLoad(e) {
        this.getQuestion()
        await this.getAnswer()
        async getAnswer() {
				
},
methods: {
    async getAnswer() {
        const listHandle = require("../../utils/js/listHandle")
        const id = this.list.data.length ? this.list.data[this.list.data.length - 1].id : ''
        const res = await this.http.get('/applet/answerList', {
                qId: this.question.id,
                limit: this.list.limit,
                userId: this.store.user ? this.store.user.id : '',
                id
        })
        if (res.code === 200) {
                const list = listHandle.add(res.data, this.list.data, this.list.limit)
                Object.assign(this.list, list)
                return res.data
        } else {
                this.list.error = true
                this.message.toast('请求失败')
                return false
        }
},
async refresh() {
        // 刷新方法
        const listHandle = require("../../utils/js/listHandle")
        this.list.flag = true
        this.list.nomore = false
        this.list.empty = false
        this.list.error = false
        this.list.loadNum = 0
        const res = await this.http.get('/applet/answerList', {
                qId: this.question.id,
                limit: this.list.limit,
                userId: this.store.user ? this.store.user.id : '',
                id: ''
        })
        this.list.flag = false
        if (res.code === 200) {
                const list = listHandle.update(res.data, this.list.limit)
                Object.assign(this.list, list)
        } else {
                this.list.err = true
                this.message.toast('请求失败')
        }
},
}
}
// listHandle.js 列表项处理的方法
const listHandle = {
  add(resData, listData, limit) {
    var ret = {
      nomore: false,
      empty: false,
      data: []
    }
    if (resData) {
      if (resData.length < limit) {
        // 获取数据条数小于页码数,显示已到底
        ret.nomore = true
      }
      ret.data = listData.concat(resData)
    } else {
      ret.data = listData
      ret.nomore = true
      if (!listData.length) {
        // 请求已无返回数据且当前列表无数据,显示为空
        ret.empty = true
      }
    }
    return ret
  },
  update(resData, limit) {
    var ret = {
      nomore: false,
      empty: false,
      data: []
    }
    if (resData) {
      if (resData.length < limit) {
        // 获取数据条数小于页码数,显示已到底
        ret.nomore = true
      }
      ret.data = resData
    } else {
      ret.nomore = true
      // 请求已无返回数据且,显示为空
      ret.empty = true
    }
    return ret
  }
}

module.exports = listHandle

注意点

在手机端上滑加载的时候,如果你是按照时间的倒序来排序,传统web底部分页器那样传一个页码数和单页条数给后端查询的话。如果在你上滑的过程中有用户发布新的内容,那么你的列表中就会有 重复的项

举个例子:你最初取了十条数据,当你上滑到底部加载时传了一个 page=2limit=10给后端,意思是将所有数据十条作为一页,我要拿第二页的内容。但是这时候有用户添加了一条新的数据,你第一页的最后一条就被挤到第二页去了,此时你拿到的数据第一条会和上一次拿到的最后一条一模一样!

想要解决这个问题也很简单,我们在向后端传递数据的时候不要按页码数去传值。我们将当前数据的最后一条的id传给后端,让后端取 id比这个值更小的十条,此时不论有多少用户插入新的数据都不会对你的结果产生影响。当然要实现这种功能的前提是你的数据表id是递增的,如果不是你也可以用数据的创建时间的那个字段来传递。 对应这个功能的是下面这行代码。

const id = this.list.data.length ? this.list.data[this.list.data.length - 1].id : ''

跨页面方法调用

在uniapp小程序的开发中,我们可能会遇到一种场景,就是我需要在当前页面调用上一个页面的方法。 例如:我要实现一个搜索功能,当我点进搜索详情输入关键词后,我要返回列表页面并触发一次搜索方法,如下图

1.gif

我的实现方式如下

searchFunc(){
  let pages = getCurrentPages()
  let page = pages[pages.length - 2]
  page.$vm.searchText = this.search.text
  page.$vm.refresh()
  this.router.back()
}

getCurrentPages() 是一个全局函数,用于获取当前页面栈的实例,以数组形式按栈的顺序给出。我们当前页面就是数组最后一项,那么上一个页面就是pages[pages.length - 2]。当然我们还要记得加上$vm属性,因为在uniapp中我们的数据和方法是挂载这个实例上的。我们可以通过这个示例访问到对应页面的所有数据和方法!

踩坑注意点

以下列出的问题不仅仅有uni-app的,更多的是小程序本身的一些问题

scoll-view scroll-into-view跨组件

在小程序的scroll-view标签上有一个scroll-into-view属性,这个属性值可以传入一个id,当我们更改这个值时我们就会滚动到指定id的容器位置。

但是如果我们的scroll-view被封装在组件中使用时,我们在slot

scroll-view无法下拉刷新

原因

应该是scroll-view元素的下拉位置在元素渲染时就确定了,而我们赋予scroll-view的高度更改了下拉位置,导致下拉的时候没法拉到位

解决方法

在scroll-view的高度变量确定后再渲染scroll-view元素,具体的做法如下代码

<scroll-view 
	scroll-y="true" 
	:style="'height:'+height" 
	v-if="height"  
	:refresher-enabled='true'
	:refresher-triggered='list.flag' 
	@refresherrefresh='refresh' 
	@scrolltolower='loadMore'>
</scroll-view>

scroll-view sticky样式问题

问题描述

scroll-view 是小程序常常会用到的一个标签,在滚动窗口内我们可能会有一个顶部标签栏,如果我们不想通过计算高度去固定在顶部的话我们可以使用 position:sticky 加一个边界条件例如top:0 属性实现一个粘性布局,在容器滚动的时候,如果我们的顶部标签栏触碰到了顶部就不会再滚动了,而是固定在顶部。

但是在小程序中如果你在scroll-view元素中直接为子元素使用sticky属性,你给予sticky的元素在到达父元素的底部时会失效,如果你遇到过一定会非常印象深刻😭。

解决方法

scroll-view元素中,再增加一层view元素,然后在再将使用了sticky属性的子元素放入view中,就可以实现粘贴在某个位置的效果了

ios固定输入框字体移动bug

问题描述

在一个固定于页面中间的滚动容器内放了一个表单,在安卓端测试功能完好,在IOS端有一个bug。当输入框在输入后没有点击其他位置使输入框失焦的话,如果滚动窗口内部的字体也会跟着滚动下面使解决方法

//原本的输入框
<input></input>

解决方法

使用这个方法更改后,不仅布局的样式不会改变,而且字体随着固定的滚动窗口一起滚动的bug也会解决

// 更改后的输入框
<textarea fixed="true" auto-height="true" ></textarea>

真机时间错误BUG

问题描述

在小程序中使用new Date().toLocaleDateString() api获取时间的时候,在开发工具中显示为当前时间,而在真机中显示为其他地区的时间

BUG产生原因

toLocaleDateString()方法依赖于底层操作系统在格式化日期上。 例如,在美国,月份出现在日期(06/22/2018)之前,而在印度,日期出现在月份(22/06/2018)之前。

解决方案

使用new Date()构造函数来获取年月日后拼接

如果没有输入任何参数,则Date的构造器会依据系统设置的当前时间来创建一个Date对象。

DatetoLocaleDateString()的区别在于一个是获取系统当前设置的时间,一个则是底层操作系统来格式化时间

//具体代码如下
let date = new Date()
date = date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate()
date = date.split('/')
if (date[1] < 10) {
		date[1] = '0' + date[1]
	}
if (date[2] < 10) {
	date[2] = '0' + date[2]
	}
date = date.join('-')

自动全局引入组件

功能介绍

如果大家有用过vue2开发项目,就知道在vue2中可以通过webpack的api进行自动全局组件引入,不需要在页面中一个一个手动引入,如下代码

const requireComponent = require.context("@/components", true, /\.vue$/);
// 通过webpack获取conponents目录下所有组件
const global = {
  install(app) {
    const components = requireComponent.keys(); // 获得组件数组
    for (let component of components) {
      let componentName = component.replace(/(.*\/)*([^.]+).*/gi, "$2"); // 获得组件名称
      app.component(componentName, requireComponent(component).default); // 将组件挂载到全局
    }
  },
};
export default global;

将global.js放在components的同级目录,然后在 main.js 中引入并使用 Vue.use 来使用插件就OK了,这个方法就可以帮我们自动将组件挂载到全局。

注意点

在uniapp中这个上面这个方法是不能使用的,我们在上面的代码中可以看到这一行 app.component(componentName, requireComponent(component).default);

在uniapp中 app.component 这个方法是不能传入变量作为组件名的,只能直接传入字符串,因此我们就没法使用它来自动引入组件啦。

总结

这篇文章陆陆续续写了两个星期,虽然蛮长了,但是还有很多内容我没有讲到的。虽然理解产出倒逼输入的道理,但是写文章还是希望能得到更多的正面反馈!点赞大大滴有,更新大大滴快。

我是 前端新人oil欧呦,欢迎关注共同成长!