最近有个小程序项目需要迭代,但是迭代任务不多,时间比较充裕。而这个小程序最早是在18年的时候开发的,用的开发框架是 WePY
的 1.7.2
版本,去年也就是 19
年的时候 WePY
框架进行了升级,到了 2.0
版本。升级之后的 WePY
,用 WePY
官方文档的话来说:通过优化细节,引入 Promise
、Async Functions
等让开发小程序项目变得更加简单,高效。基于这些背景,我和小伙伴一拍即合,决定对我们的项目进行框架升级,体验下到底 WePY2
能给我们带来什么。
本文将以项目改动为出发点,基于当前这个项目的结构和编码方式来考虑到底升级 WePY2
后,哪里需要改,怎么改以及有哪些需要注意的地方,通过对比 2
个版本的写法差异这个思路来写,不会去太较真 WePY2
相对于 WePY1
实现或原理上的区别。下面我将一条一条的列出来需要改动的点。
❝本篇文章记录的是我和小伙伴这次升级框架遇到的需要改动的地方和坑,所使用的是
wepy
的2.1.0
版本,后续如果版本升级后,本篇记录到的坑如果已经被修复了,请自行忽略本文所述的问题。另外下文中所说到的2.x
版本都是指wepy_v2.1.0
。 ❞
由于本地还有其他项目用的是 WePY_v1.7.2
,所以我们不能把 WePY2
的 CLI
工具安装在全局环境中,只能安装在当前项目中。官方推荐是直接用 1.7.x
的 CLI
去初始化 2.0.x
的项目:
wepy init standard#2.0.x zzodr
这样就能够在本地初始化一个 wepy2
的项目模板,但是 @wepy/core
是 2.0.0-alpha.16
版本的,将它更新到最新的 2.1.0
版本,这里也一起更新下整个旧项目和新模板所用到的依赖,下面直接贴出来:
{
"dependencies": {
"@wepy/core": "^v2.1.0",
"@wepy/use-intercept": "^2.1.0",
"@wepy/use-promisify": "^2.1.0",
"@wepy/x": "^2.0.2",
"miniprogram-slide-view": "0.0.3"
},
"devDependencies": {
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
"@wepy/babel-plugin-import-regenerator": "0.0.2",
"@wepy/cli": "^2.1.0",
"@wepy/compiler-babel": "^2.0.1",
"@wepy/compiler-sass": "^2.1.0",
"@wepy/plugin-define": "^2.1.0",
"babel-eslint": "^7.2.1",
"cross-env": "^5.1.3",
"eslint": "^3.18.0",
"eslint-config-standard": "^7.1.0",
"eslint-friendly-formatter": "^2.0.7",
"eslint-plugin-html": "^2.0.1",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^2.0.1",
"wepy-eslint": "^1.5.3"
}
}
接下来操作主要是删除模板里的代码,然后把项目的结构和代码搬过去。
WePy
单文件组件主要由 <script>
、<template>
、<style>
、<config>
四部分组成(也包括小程序 <wxs>
标签)。所以需要把 WePY 1.7.2
中定义在 <script>
中的 config
配置需要独立到外层的 <config>
中。1.7.2
写法:
<template></template>
<script>
export default class Home extends wepy.page {
config = {
navigationBarTitleText: '首页'
}
}
</script>
<style></style>
2.x
写法:
<template></template>
<script></script>
<config>
{
navigationBarTitleText: '首页'
}
</config>
<style></style>
注册方式将不再使用继承的方式,而是改成直接调用对应的实例方法。1.7.2
写法:
export default class APP extends wepy.app {} // 注册程序
export default class HOME extends wepy.page {} // 注册页面
export default class LIST extends wepy.component {} // 注册组件
2.x
写法:
wepy.app({}) // 注册程序
wepy.page({}) // 注册页面
wepy.component({}) // 注册组件
由于注册方式的改变,那么自然的代码结构也要有所调整。1.7.2
写法:
export default class HOME extends wepy.page {
data = {}
methods = {}
onLoad() {}
onShow() {}
}
2.x
写法:
wepy.page({
data: {},
methods: {},
onLoad() {},
onShow() {},
})
上面仅仅只是以页面做为例子,wepy.app()
和 wepy.component()
也要对应调整。
在 WePY 1.7.2
中注册的页面或者组件函数有这么几种类型:
onLoad
、onShow
等;wxml
事件处理函数,即在 wxml
中绑定的事件,这类函数需要定义在 methods
,比如:bindtap
、bindchange
等;methods
同级的位置。而在 WePY 2
中需要将组件处理函数和自定义函数都放到 methods
里。下面假设 HOME
页面有一个子组件 child
,且子组件里会执行这句 this.$emit('updateList)
,基于这个背景看下 2
个版本下的写法差异:
1.7.2
写法:
<template>
<view>
<view bindtap="tapBox"></view>
</view>
</template>
<script>
export default class HOME extends wepy.page {
data = {}
onLoad() {} // 生命周期函数
onShow() {} // 生命周期函数
events = {
updateList:() => {} // 组件间事件处理函数
}
methods = {
tapBox() { // wxml事件处理函数
this.getMsg()
}
}
getMsg() {} // 自定义函数
}
</script>
2.x
写法:
<template>
<view>
<view @tap="tapBox"></view>
</view>
</template>
<script>
wepy.page({
data: {},
onLoad() {}, // 生命周期函数
onShow() {}, // 生命周期函数
methods: {
tapBox() { // wxml事件处理函数
this.getMsg()
},
updateList() {}, // 组件间事件处理函数
getMsg() {}, // 自定义函数
},
})
</script>
在 2.x
版本中组件引入不再通过 import
进行导入,而是直接定义在页面的配置 <config>
中。1.7.2
写法:
<template>
<view>
<child></child>
</view>
</template>
<script>
import Child from './components/child.wpy';
export default class HOME extends wepy.page {
components = {
child: Child
}
}
</script>
2.x
写法:
<template>
<view>
<child></child>
</view>
</template>
<script>
wepy.page({})
</script>
<config>
{
usingComponents: {
'child': './components/child.wpy',
}
}
</config>
另外,2.x
中已经再不支持在 app.wpy
里定义全局组件,而 1.7.2
中是可以的。
在 2.x
中生命周期函数基本和原生保持一致,和 1.7.2
相比,只是需要把组件中的 onLoad
改成了 ready
即可,其他无需变动。
级别 | 1.7.2 | 2.x |
---|---|---|
app | onLaunch | onLaunch |
app | onShow | onShow |
page | onLoad | onLoad |
page | onShow | onShow |
page | onReady | onReady |
component | - | created |
component | - | attached |
component | onLoad | ready |
2.x
生命周期执行顺序:
app onLaunch
-> app onShow
-> component created
-> component attached
-> page onLoad
-> page onShow
-> component ready
-> page onReady
-> page onUnload
-> component detached
❝
page onHide
在当前页面通过wx.navigateTo
打开新页面的时候会执行,而如果是在当前页面点击返回上一个页面或者wx.redirectTo
并不会执行。 ❞
在 1.7.2
中可以在 wepy.app
的构造函数里通过配置拦截器可以对请求进行拦截,请求被拦截后可以加上更多的请求参数以及请求响应后可以进行统一的错误处理,功能还是挺好用的。但是在 2.x
中这个功能至少从文档上是没看到,虽然源码里提供了一个 use-intercept拦截器的包,但是经过我几番尝试之后还是报错,所以就打算弃用拦截器了,直接在请求里进行参数增加和错误处理。request.js
这里贴一份大概的代码:
import wepy from "@wepy/core";
import { HOST } from "./constants";
export default function(url, data, handler = toast, header = {}) {
// 头参数添加
header["Content-Type"] = "application/json";
header["cType"] = "WECHAT";
return wepy.wx.request({ // 这里 wepy.wx.request 这种写法,需要在app.wpy里配置promisify,
method: "POST",
data: data || {},
header,
url: `${HOST}${url}`,
}).then(data => {
// 请求成功处理代码放这儿
return Promise.reject(data)
}).catch(err => {
// 错误处理代码放这儿
return Promise.reject(err);
});
}
其中 wepy.wx.request
这种写法需要在 app.wpy
里配置 promisify
,可以参考这里 use-promisify
在 1.7.2
中对单引号和双引号没有强制要求,但是在 2.x
中必须是双引号,不然编译会报错。1.7.2
写法:
<template>
<view>
<view class="title" bindtap='change'></view>
</view>
</template>
2.x
写法:
<template>
<view>
<view class="title" bindtap="change"></view>
</view>
</template>
小程序原生事件会传递一个 event 参数。而 WePY 的事件分发器在处理事件时会有一个 event 参数。event 参数是对 event 进行了一层包装,目地是为了无侵入地对齐 Web Event 标准属性。而其中 event.wx === event。因此,WePY 中响应事件获得的事件参数均是指 event。如果想拿到原生事件参数,请使用 event.
<template>
<view><input bindinput="setInput" value="{{name}}" /></view>
</template>
<script>
export default class HOME extends wepy.page {
data = {
name: '',
}
methods = {
setInput(e) {
this.name = e.detail.value
}
}
}
</script>
2.x
写法,只需要将 bindinput="setInput"
改成 @input="setInput($wx)"
即可。
2.x
的模板语法继承了 WXML
的基本模板语法,并支持大部分 Vue
模板语法。对于标签:2.x
支持绝大部分的 HTML
标签,经过编译后会转成标准的 WXML
模板语法。但是对于 1.7.2
中的有一个标签 <repeat>
不再支持,需要将其替换成 <view>
并且用 v-for
进行循环渲染。
下面是一些常用的模板语法对于 2
个版本之间写法的对比:1.7.2
写法
<template>
<view>
<!-- 属性绑定 -->
<view id="{{ id }}"></view>
<!-- 数据绑定 -->
<view>{{ name }}</view>
<!-- 事件绑定 -->
<view bindtap="change({{ index }})"></view>
<!-- class绑定 -->
<view class="change {{hasData ? 'has-data' : '' }}"></view>
<!-- style绑定 -->
<view style="{{ 'color:' + color + ';' + 'font-size:' + fontSize + ';' }}"></view>
<!-- 条件判断 -->
<view wx:if="{{ flag1 }}"></view>
<view wx:elif="{{ flag2 }}"></view>
<view wx:else></view>
<!-- 显示判断 -->
<view hidden="{{ !isShow }}"></view>
<!-- 列表渲染,默认是:item、index -->
<view wx:for="{{ array }}" wx:for-index="idx" wx:for-item="itemName"></view>
</view>
</template>
2.x
写法:
<template>
<view>
<!-- 属性绑定 -->
<view :id="id"></view>
<!-- 数据绑定 -->
<view>{{ name }}</view>
<!-- 事件绑定 -->
<view @tap="change( index )"></view>
<!-- class绑定 -->
<view class="change" :class="{ 'has-data': hasData }"></view>
<!-- style绑定 -->
<view :style="{'color': color, 'font-size': fontSize }"></view>
<!-- 条件判断 -->
<view v-if="flag1"></view>
<view v-else-if="flag2"></view>
<view v-else></view>
<!-- 显示判断 -->
<view v-show="isShow"></view>
<!-- 列表渲染,默认是:item、index -->
<view v-for="(item, index) in array" :key="index"></view>
</view>
</template>
对于 v-for
循环列表的时候这里有一个(坑)不得不提一下,github issues直接看下面的代码:
<view v-for="item in array">
<view>{{ index }}</view>
<view @tap="tapItem(index)"></view>
</view>
对于上面的代码,<view>{{ index }}</view>
可以正常显示索引值 index
,但是 tapItem
传的参数却是 undefined
,这所以我们需要显示的声明索引 v-for="(item, index) in array"
即可。
2.x
中直接用 v-model
进行表单绑定,而不需要再定义一个函数对其进行赋值操作。1.7.2
写法:
<template>
<input value="{{ name }}" bindtap="setInput" />
</template>
<script>
export default class HOME extends wepy.page {
data = {
name: '',
}
methods = {
bindtap(e) {
this.name = e.detail.value
}
}
}
2.x
写法:
<template>
<input v-model="name" />
</template>
<script>
wepy.page({
data: {
name: '',
}
})
我们有时候需要在 app.wpy
里定义全局数据属性 globalData
:
export default class APP extends wepy.app {
globalData = {
isBack: false
}
}
在 2.x
中定义方式没变,但是获取方式有所调整:
// 1.7.2 获取方式
console.log(this.$parent.globalData.isBack)
// 2.x 获取方式
console.log(this.$app.$options.globalData.isBack)
2.x
中对组件的实现方式保留了很多原生小程序的特性,比如这一条,组件样式 中明确说明:除继承样式外, app.wxss
中的样式、组件所在页面的的样式对自定义组件无效(除非更改组件样式隔离选项),虽然可以通过更改组件样式隔离选项使得组件可以被全局样式作用到,但有时候也会带来弊端,比如在标签的属性 class
前面加上 ~
,可以使组件获取全局样式,但是这样一来也带来一个问题,就是定义在组件里的该 class
样式会失效?。这样的升级真的让写样式很难受,所以为了让样式写得尽量方便简单,我还是老老实实的把组件的样式就定义在组件里,不从全局拿样式了。
父组件给子组件传递数据可以通过设置静态或者动态的 prop 属性或者通过广播 broadcast 来让所有子组件都收到父组件的信息,而子组件给父组件通信可以通过在父级自定义事件,在子组件中通过 emit 来通信。但是在 2.x 中不再支持父级给子组件进行事件广播了,而是可以通过给子组件加上 ref 属性后,通过 this.
<template>
<child ref="child"></child>
</template>
<script>
wepy.page({
onLoad() {
this.$refs.child.getList()
}
})
</script>
child.wpy
:
<script>
wepy.component({
methods: {
getList() {}
}
})
</script>
在 1.7.2
中可以通过可以通过设置 prop
给子组件传参,如果设置的时候加上 .sync
那么当父组件参数更新的时候,传递给子组件的也会自动更新,而如果在子组件的 prop
里加上 twoWay: true
则子组件数据可以绑定到父组件。从而实现组件数据的双向绑定。功能还是挺好用的,但遗憾的是在 2.x
中已经不再支持通过 twoWay: true
的方式从子组件绑定数据到父组件,父到子是可以的,但是不再需要设置 sync
。那子组件需要更新父组件的数据,只能通过自定义事件,然后在子组件通过 $emit
进行更新数据了。1.7.2
写法:父页面:
<template>
<child :title.sync="title"></child>
</tempalte>
子组件:
export default class CHILD extends wepy.component {
props = {
title: {
type: String,
default: '',
twoWay: true
}
}
}
2.x
写法:父页面:
<template>
<child :title="title" @changeTitle="changeTitle"></child>
</tempalte>
<script>
export default class HOME extends wepy.page {
data = {
title: '最开始的标题',
}
events = {
changeTitle(val) {
this.title = val
}
}
}
</script>
子组件:
wepy.page({
props: {
title: {
type: String,
default: '',
}
},
onLoad(){
this.$emit('changeTitle', '改变之后的标题')
}
})
这个问题已经提到 github issues 中,且已经被作者标记为 bug
。原始代码:parent.wpy
父页面:
<template>
<view>parent</view>
<child>
<view>child view</view>
</child>
</template>
child.wpy
子组件:
<template>
<view>
<view>child</view>
<slot></slot>
</view>
</template>
期望的编译后(正确)的 template
是:
<template>
<view>parent</view>
<view>
<view>child</view>
<view>child view</view>
</view>
</template>
而实际 2.x
编译后的 template
是会将对应的内容插入到子组件与根元素并列那级:
<template>
<view>parent</view>
<view>
<view>child</view>
</view>
<view>child view</view>
</template>
针对老项目里用到 slot
的地方,我只能改写代码来避开这个坑了。
资源引入方式调整主要是介绍组件引入和图片引入两种。首先来看组件引入:
<config>
{
usingComponents: {
'load-more': '/components/loadMore', // 绝对路径
'btn': '../btn', // 相对路径
'list': '~@/components/list', // 通过wepy.config.js配置别名@指向src,实际上也是绝对路径
'van-icon': 'module:van-icon', // 模块引入
}
}
</config>
对于图片引入,存在两种方式:一种是静态的,程序在编译的时候就知道需要把哪些图片加载出来,另一种是动态的,只有在程序执行的时候才知道要加载哪些图片。对于第一种方式,通过相对路径、绝对路径或者 @
都可以引入到图片:
<template>
<view>
<image src="../bg.png"></image>
<image src="/images/icon.png"></image>
<image src="@/images/nodata.png"></image>
</view>
</template>
对于第二种方式,需要将动态的图片放置在某个固定的位置,比如 /src/images/static
,然后再 wepy.config.js
里配置 static: ['/src/images/static']
这样在编译的时候就会把这个路径下的文件都拷贝到输出后的目录,从而能够准确引用这些动态图片。wepy.config.js
配置:
module.exports = {
static: ['src/images/static'],
}
页面:
<template>
<view>
<image :src="'../images/static/' + fileType + '.png'"></image>
<image :src="imgSrc"></image>
</view>
</template>
<script>
wepy.page({
data: {
imgSrc: '',
},
onLoad() {
this.imgSrc = '/images/static/icon.png'
}
})
</script>
静态图片特殊处理:单独放到一个目录里,然后再WePY.config.js里配置static
下面的代码是一个页面的 scss
样式里,引入了 wxss
文件,最终会导致编译进程终止。
<style lang="scss" type="text/scss">
@import "./btn.scss";
@import "./styles/common.wxss";
</style>
解决办法:
<style lang="scss" type="text/scss">
@import "./btn.scss";
</style>
<style lang="wxss" type="text/wxss">
@import "./styles/common.wxss";
</style>
这里的特殊字符其实也是正常的需求,比如引入了字体图标,那可能会有这种样式 content: '\6499'
, 然后因为有反斜杠会直接导致报错编译错误。解决思路是把这种带有特殊字符的样式放到 wxss
里,然后通过另外一个 style
引入进来,编译器进行编译的时候会对 scss
样式进行编译处理,但是对于 wxss
会直接拷贝到输入目录,而不进行编译处理,所以能绕过这个坑。
<style lang="scss" type="text/scss">
@import "./btn.scss";
</style>
<style lang="wxss" type="text/wxss">
.icon-success::after {
content: '\e8921';
color: green;
}
</style>
1.7.2 中用脏检查进行数据绑定,通过 apply() 方法使得数据能够及时更新,页面重新渲染。在 2.x 中使用了 Vue Observer 实现数据绑定,告别 apply(),但是遇到一个问题,某个数组项的某个属性更新后,数组虽然是更新了,但是不能够触发页面进行重新渲染,即使使用 splice 也不行。不过可以通过浅拷贝一个引用类型,重新赋值,从而触发页面重新渲染。
wepy.page({
data: {
list: [{
name: 'aaa',
hasBorder: true,
}, {
name: 'bbb',
hasBorder: false,
}]
},
methods: {
handleClick() {
this.list[1].hasBorder = true // 不会重新渲染页面
this.list.splice(1, 1, { // 也不会重新渲染页面
name: 'bbb',
hasBorder: true,
})
this.list = [...this.list] // 浅拷贝,使得this.list的引用地址变化了,使得页面重新渲染
}
}
})