历经了接近两个月的摸索滚爬,抓头吃瓜,各种优化(单押X3),我主导开发的第一个小程序终于要上线了(SKR SKR!)!当然首先要感谢老板没有杀了我——因为在6月初我刚拿到小程序PRD的时候老板问我多久可以做好,我看了看之后说“两周”吧,咳咳,然后。。。一直到现在,我还能活着很Amazing有没有???当然这其中也有一些为了追求“精品小程序”而一改再改所用的时间。好了,废话还是不多说了,下面就开始总结下吧~
1.框架的选择
原生的小程序我本人并没有学习过,更别提拿来开发一款商用的小程序了,刚好还在前公司时,当时的前端团队在提到小程序的解决方案时有分享了mpvue,到了新公司之后技术老大也有提到mpvue,而我本人过去一年多也一直在写vue,对vue写法比较熟悉,而且新公司团队对小程序期待已久,希望尽快上架,所以选择mpvue来开发也是最快最合理的了!
2.项目的搭建
看了mpvue的官方文档,项目的搭建自然也选择了官方推荐的vue-cli, 在看了五分钟上手教程后,使用命令vue init mpvue/mpvue-quickstart my-project
生成了基本的项目,在后来的开发中,项目的配置基本没做改动,只是添加了less-loader。
3.目录结构
基本上是vue-cli生成的目录结构,加了部分文件夹,主要是与后台进行数据交互所使用的框架flyio的配置文件夹(api文件夹),以及整个项目数据管理所使用的vuex(store文件夹),整体目录结构如下:
project
└───build
└───config
└───dist
└───node_modules
└───src
└───api
| ajax.js // flyio请求与响应拦截器的配置文件
| config.js // 请求的配置文件
| index.js // 生成请求api实例文件
| Server.js // 项目的数据请求统一管理文件
└───components
└───pages
└───store
└───modules // vuex模块文件夹
| index.js // vuex处理文件
| App.vue
| config.js
| main.js
└───static
└───images
└───lib
└───weui
│ README.md
│ package.json
│ package-lock.json
复制代码
4.踩到的坑
相信很多使用过mpvue的同学都或多或少猜到了一些坑,我也是踩到了不少的坑浪费了不少的宝贵时间,虽然网上关于mpvue的踩坑的文章一搜一箩筐,但我还是要写一下。。。下面就是我在本次小程序开发过程中遇到的坑(们)以及针对它们的解决方案:
4-1.tabBar图标问题
在配置小程序原生的底部tabBar时,遇到了第一个问题:在将设计师给我的图标icon路径设置正确的情况下,开发者工具上的tabBar的图标总是会很大,而且几乎占满了整个高度,相当难看,搜了很多博客都没有找到解决办法,期间还尝试了自己实现tabBar,但是在看到那令人呕呕呕的效果之后,我还是放弃了,又回到原生的tabBar,然后静下心来想了想,最后在对比github上的一些mpvue的项目之后,发现原来是图标icon的问题,最后成功解决:就是icon尺寸保持不变,然后四周留出合适的透明(?)空白...很简单有木有?就这浪费我很多脑细胞,原谅我的愚钝(智障脸)。。。当然了,原生的tabBar其实还有一个问题就是,tabBar的标题文字在真机上会离底部特别特别近,这个我没找到解决办法,除了自己实现tabBar。。。
4-2.详情页数据保留之前旧数据的问题
这个问题我想很多同学都遇到过了,主要是因为mpvue中页面跳转后并没有销毁页面实例,而是将其推入页面栈中,所以会保存之前的旧的数据,而且我看到mpvue github上的issues里面有很多人都遇到了这个问题并且都在持续关注,足以说明这是个痛点问题,谁让它会影响小程序的用户体验呢。。。到目前为止看到的比较统一的解决办法就是:在(详情)页面onLoad的时候,将要在本页面展示的数据初始化并且进行新的赋值,举?如下:
<template>
<html-text :text="htmltext"></html-text>
</template>
<script>
import htmlText from xxxxx
export default {
components: {
htmlText
},
data () {
return {
htmltext: ''
}
},
onLoad () {
this.htmltext = ''
this.$http.get('xxxxxxxx').then((res) => {
this.htmltext = res.htmltext
})
}
}
</script>
复制代码
其他数组或者对象类型的处理可能会麻烦一些,但是方法类似,在数据请求返回之前的这段时间内不想留空白尬对用户的话就自己做一些loading,总是要强过用户先面对旧数据再一闪跳到新数据的体验。。。
4-3.created钩子函数在项目初始化时就全部执行的问题
这个我想应该是mpvue的一个bug吧?该钩子函数在页面内还是不要随便用的好。。。
4-4.目前mpvue对于复杂富文本的支持目前性能较差的问题
这个问题不能甩锅给mpvue,对于展示“相当复杂”的富文本(内容较长,且由多张图片甚至多张动图)的需求,一般不会有很多用户会遇到,但是很不巧的是,我遇到了。。。谁让我们致力于做一个有逼格的品牌呢?有需求了就要解决,光能展示远远不够,还得展示的优雅,目前的mpvue-wxParse 其实已经能解决大部分问题了,也有一些github上的项目基于该项目开发得到了数百star,但是我用该项目做出来的效果老板和技术老大都相当不满意,图片没法优雅的加载,而且由于htmltext太长在图片全部解析显示出来之前有着相当长的白屏尴尬时间,所以最后还是放弃该方案。
然后在github上找到了另一个在mpvue-wxParse 的基础上改进的针对复杂富文本的项目mpvue-htmlParse,试了下稍微好了点,但离老板的要求还是差很远,最后不得已在此项目基础上fork出一份代码针对老板的需求亲自来改,最终得以过关,项目地址mpvue-htmlParse,该项目里主要针对图片的加载做了改进,在第一张图片加载完成后,通知主页可以关闭preLoading效果,然后给每张图片添加了菊花的加载效果,在图片完全加载完成之前会显示菊花图,然后再根据设备屏幕宽度和图片信息对图片进行适当放大或者缩小,这样整体下来的效果基本可以达到“破产版”微信公众号推文的效果,该项目适用范围有限,有需要的同学可以自己在此基础上改进。
4-5.微信原生的路由跳转navigateTo(),redirectTo(),navigateBack(),switchTab(),reLaunch()等,在真机上的表现较为怪异
对于参数的传递,我也遇到过类似于旧数据的问题,最后不得已借助于vuex才得以解决。另外小程序的页面栈个数实在有限,所以在开发时一定要注意页面栈的管理。
4-6.onShow()的使用要注意
要记得该钩子函数里的js代码不只是刚进入页面时会执行,在息屏后再次点亮后也将会执行。
对于mpvue的坑突然能想起来的不多了,目前就先写这么多,后面想起来了再来更新吧。
5.Flyio的使用
在小程序的开发中,并没有使用小程序原生的wx.request()来进行数据交互,而是选择了mpvue文档里推荐使用的Flyio,Flyio的介绍就不多做介绍,打架可以自己看文档,这里我主要说一下的请求和响应拦截器的构造:
文档里其实有很详细的介绍以及代码,但是我根据代码写下来之后在遇到登录失效的问题时并没有按照预想的解决:先锁住请求然后重新请求拿到新的cookie之后再重新进行之前的请求,再和其他人讨论之后使用promise解决了这一问题,具体可见代码:
src/api/ajax.js:
/**
* http请求拦截器
*/
const Fly = require('flyio/dist/npm/wx')
const config = require('./config')
const ajaxUrl =
process.env.NODE_ENV === 'development'
? config.Host.development
: process.env.NODE_ENV === 'production'
? config.Host.production
: config.Host.test
let fly = new Fly()
let loginFly = new Fly()
// 定义公共headers
const headers = {
...
}
Object.assign(fly.config, {
headers: headers,
baseURL: ajaxUrl,
timeout: 10000,
withCredentials: true
})
loginFly.config = fly.config
// session失效后本地重新登录
const login = () => {
return new Promise((resolve, reject) => {
wx.login({
success: res => {
let loginParams = {
...
}
loginFly.post('/api/locallogin/url', loginParams).then(d => {
if (d.headers && typeof d.headers['set-cookie'] !== 'undefined') {
// 更新session
wx.setStorageSync('sessionid', d.headers['set-cookie'])
}
resolve()
}).catch(error => {
log(error)
reject(res.data)
})
},
fail: res => {
console.error(res.errMsg)
},
complete: res => {}
})
})
}
// 请求拦截器
fly.interceptors.request.use(request => {
if (wx.getStorageSync('sessionid')) {
request.headers.cookie = wx.getStorageSync('sessionid')
}
return request
})
// 响应拦截器
fly.interceptors.response.use(
response => {
// session已经失效,需要重新登录小程序
if (response.data.errCode === 100009) {
// log('session失效,根据之前存储在本地的用户信息重新请求session...')
// 锁定响应拦截器
fly.lock()
return login().then(() => {
fly.unlock()
// log(`重新请求:path:${response.request.url},baseURL:${response.request.baseURL}`)
return fly.request(response.request)
}).catch(err => {
log(err)
})
} else {
return response.data.data
}
},
err => {
log('error-interceptor', err)
if (err.status) {
wx.showToast({
title: '出现未知错误',
icon: 'none',
duration: 3000
})
}
}
)
export default fly
复制代码
// 创建api实例
src/api/index.js:
import Server from './Server.js'
class Api {
constructor () {
Object.assign(this, ...Array.from(arguments))
}
}
export default new Api(Server)
复制代码
6.vuex的使用
因为是生活购物类小程序,涉及到购物车+地址选择等较为复杂的逻辑,很多地方都需要数据共用,在本期项目中vuex起了很大的作用,因为模块较多,如果将所有数据写在一个文件里无疑会为后期维护带来巨大困难,所以将各模块的数据单独划分写在各自的文件里,这样整体流程就清晰了很多,下面是划分模块的主文件的代码
src/api/Server.js:
/**
* 本模块主要用于与服务端进行交互
*/
import ajax from './ajax.js'
async function requestFunction ({params}) {
let res = await ajax.get('/request/url/', {params})
...
return res.data
}
export default {
requestFunction
}
复制代码
src/store/index.js:
import Vue from 'vue'
import Vuex from 'vuex'
import modules1 from './modules/modules1'
import modules2 from './modules/modules2'
import modules3 from './modules/modules3'
...
Vue.use(Vuex)
export default new Vuex.Store({
// 做模块化处理,每个功能一个store.js文件,然后统一在这边引入
modules: {
modules1,
modules2,
modules3,
...
}
})
复制代码
src/store/modules/modules1.js:
import api from '@/api' // actions里请求用到
const state = {
aaaa,
...
}
const getters = {
aaaa (state) {
return state.aaaa
},
bbbb (state, getters, rootState) {
return getters.aaaa
},
...
}
// actions里可进行异步操作
const actions = {
async anExample ({state, getters, dispatch, commit}, {params}) {
let res = await api.requestFunction({params})
...
return res
},
...
}
const mutations = {
setStateX (state, Y) {
state.X = Y
},
...
}
export default {
namespaced: true, // 很重要
state,
getters,
actions,
mutations
}
复制代码
在.vue文件中调用
src/pages/xxx.vue
<script>
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
// 调用getters
...mapGetters('modules', [
'aaaa',
'bbbb'
])
},
methods: {
// 调用action
funcA () {
this.$store.dispatch('modules1/anExample', {params}).then(res => {
...
})
},
// 调用mutation
funcB () {
this.$store.commit('modules1/setStateX', Y)
}
}
}
</script>
复制代码
7.总结
本次总结目前先写这么多吧,主要介绍了项目结构,遇到的坑(项目中遇到的问题很多,但是写的时候突然觉得那些都不是问题了?),Flyio的使用(重点为拦截器的配置),以及vuex的简单介绍。其实项目开发完成之后想了想也没那么多东西,只是期间走了不少的弯路,做了很多“无用功”,其实说是无用功,但也从中收获了相当多,毕竟自己从无到有从0到1构建一个项目,其中的烦恼很多,但是真的能让人成长很多,也让我觉得相当充实。由于小程序是公司商用的不是我个人的项目,所以项目代码就没法开源了,如果有问题的话可以联系我,为防广告之嫌这里也不说明小程序的名字了,想来体验下的可以私信我,也欢迎大家来指正!
老板来找我过第二期的需求了,Incoming!