小程序升级WePY2踩坑记

2,923 阅读7分钟

最近有个小程序项目需要迭代,但是迭代任务不多,时间比较充裕。而这个小程序最早是在18年的时候开发的,用的开发框架是 WePY1.7.2 版本,去年也就是 19 年的时候 WePY 框架进行了升级,到了 2.0 版本。升级之后的 WePY,用 WePY 官方文档的话来说:通过优化细节,引入 PromiseAsync Functions 等让开发小程序项目变得更加简单,高效。基于这些背景,我和小伙伴一拍即合,决定对我们的项目进行框架升级,体验下到底 WePY2 能给我们带来什么。

本文将以项目改动为出发点,基于当前这个项目的结构和编码方式来考虑到底升级 WePY2 后,哪里需要改,怎么改以及有哪些需要注意的地方,通过对比 2 个版本的写法差异这个思路来写,不会去太较真 WePY2 相对于 WePY1 实现或原理上的区别。下面我将一条一条的列出来需要改动的点。

本篇文章记录的是我和小伙伴这次升级框架遇到的需要改动的地方和坑,所使用的是 wepy2.1.0 版本,后续如果版本升级后,本篇记录到的坑如果已经被修复了,请自行忽略本文所述的问题。另外下文中所说到的 2.x 版本都是指 wepy_v2.1.0

1、初始化一个 WePY2 的 demo

由于本地还有其他项目用的是 WePY_v1.7.2,所以我们不能把 WePY2CLI 工具安装在全局环境中,只能安装在当前项目中。官方推荐是直接用 1.7.xCLI 去初始化 2.0.x 的项目:

wepy init standard#2.0.x zzodr

这样就能够在本地初始化一个 wepy2 的项目模板,但是 @wepy/core2.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"
  }
}

接下来操作主要是删除模板里的代码,然后把项目的结构和代码搬过去。

2、wpy 文件代码结构调整

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>

3、 程序/页面/组件注册方式调整

注册方式将不再使用继承的方式,而是改成直接调用对应的实例方法。 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({})  // 注册组件

4、代码结构由类结构变成对象结构

由于注册方式的改变,那么自然的代码结构也要有所调整。 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() 也要对应调整。

5、自定义方法和组件事件处理函数需要移到 methods 里

WePY 1.7.2 中注册的页面或者组件函数有这么几种类型:

  • 生命周期函数,比如 onLoadonShow 等;
  • wxml 事件处理函数,即在 wxml 中绑定的事件,这类函数需要定义在 methods,比如:bindtapbindchange 等;
  • 组件间事件处理函数,响应组件之间通过 $broadcast$emit$invoke 所传递的事件函数,这类函数需要定义在 events 对象里;
  • 自定义函数,即用于被其他函数直接调用的函数,需要定义在和 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>

6、组件引入方式变更

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 中是可以的。

7、生命周期函数调整

2.x 中生命周期函数基本和原生保持一致,和 1.7.2 相比,只是需要把组件中的 onLoad 改成了 ready 即可,其他无需变动。

级别1.7.22.x
apponLaunchonLaunch
apponShowonShow
pageonLoadonLoad
pageonShowonShow
pageonReadyonReady
component-created
component-attached
componentonLoadready

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 并不会执行。

8、不再支持请求拦截器(坑)

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

9、标签属性的值必须被双引号包裹

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>

10、调用原生事件需要传入参数$wx

小程序原生事件会传递一个 event 参数。而 WePY 的事件分发器在处理事件时会有一个 $event 参数。 $event 参数是对 event 进行了一层包装,目地是为了无侵入地对齐 Web Event 标准属性。而其中 $event.$wx === event。 因此,WePY 中响应事件获得的事件参数均是指 $event。如果想拿到原生事件参数,请使用 $event.$wx1.7.2 写法:

<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)" 即可。

11、模板语法修改

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" 即可。

12、表单双向绑定调整

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: '',
    }
})

13、全局数据属性获取方式调整

我们有时候需要在 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)

14、全局样式对组件无效

2.x 中对组件的实现方式保留了很多原生小程序的特性,比如这一条,组件样式 中明确说明:除继承样式外, app.wxss 中的样式、组件所在页面的的样式对自定义组件无效(除非更改组件样式隔离选项),虽然可以通过更改组件样式隔离选项使得组件可以被全局样式作用到,但有时候也会带来弊端,比如在标签的属性 class 前面加上 ~,可以使组件获取全局样式,但是这样一来也带来一个问题,就是定义在组件里的该 class 样式会失效😭。这样的升级真的让写样式很难受,所以为了让样式写得尽量方便简单,我还是老老实实的把组件的样式就定义在组件里,不从全局拿样式了。

15、组件通信不再支持$broadcast

父组件给子组件传递数据可以通过设置静态或者动态的 prop 属性或者通过广播 $broadcast 来让所有子组件都收到父组件的信息,而子组件给父组件通信可以通过在父级自定义事件,在子组件中通过 $emit 来通信。但是在 2.x 中不再支持父级给子组件进行事件广播了,而是可以通过给子组件加上 ref 属性后,通过 this.$refs 来直接操作子组件函数来达成通信的目的,如下代码: parent.wpy:

<template>
    <child ref="child"></child>
</template>
<script>
wepy.page({
    onLoad() {
        this.$refs.child.getList()
    }
})
</script>

child.wpy

<script>
wepy.component({
    methods: {
        getList() {}
    }
})
</script>

16、组件prop不再支持双向绑定

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', '改变之后的标题')
    }
}) 

17、组件插槽slot代码插入后层级错乱问题(坑)

这个问题已经提到 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 的地方,我只能改写代码来避开这个坑了。

18、资源引入调整

资源引入方式调整主要是介绍组件引入和图片引入两种。首先来看组件引入:

<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

19、scss里如果引入.wxss文件会直接终止编译进程

下面的代码是一个页面的 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>

20、scss样式里存在特殊字符会导致编译报错(坑)

这里的特殊字符其实也是正常的需求,比如引入了字体图标,那可能会有这种样式 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>

21、数据绑定机制调整

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的引用地址变化了,使得页面重新渲染
        }
    }
})