接触微信小程序有一段时间的开发者或开发团队,我相信多多少少都会为自己搭建封装一些便于开发的框架/脚手架,尤其是一些做过Web开发的开发者,受到现如今Web主流开发框架如Angular,React,Vue等的核心思想的影响,对数据/状态管理、组件化、跨平台等都有较高的追求。
今天的主角是一个基于Vue的框架:mpvue。为什么说mpvue是“基于Vue”的框架呢?因为mpvue是从整个Vue的核心代码上经过二次开发而形成的一个框架,相当于是给Vue本身赋能,增加了开发微信小程序的能力。
1
mpvue主要目录和文件结构
在Visual Studio Code里面打开项目文件夹,我们可以看到类似如下的文件结构:
package.json是项目的主配置文件,里面包含了mpvue项目的基本描述信息、项目所依赖的各种第三方库以及版本信息、以及可执行的脚本信息。
我们看到该文件中的scripts
部分配置了4个可执行的命令:
dev
和start
是两个等价的命令,执行其中之一都可以将项目以开发模式启动。执行方式是:
lint
指令是使用ESLint来进行代码语法和格式检查,以及修复一些可自动修复的问题。执行方式是:
build
指令是用于生成发布用代码的,它会对代码进行一些压缩优化处理。当小程序开发完成后,将要提交审核时,请使用build
来生成发布的代码。project.config.json
文件是用于管理微信开发者工具的小程序项目的配置文件,其中记录了小程序的appid、代码主目录、以及编译选项等等信息,在微信开发者工具中导入小程序项目的时候主要是通过该配置文件读取和写入配置信息。
static
目录可以用于存放各种小程序本地静态资源,如图片、文本文件等。代码中可通过相对路径或绝对路径进行访问, 如:
build
目录下是一些用于项目编译打包的node.js脚本和webpack配置文件。一般情况下不需要修改这些文件。
config
目录下包含了用于开发和生产环境下的不同配置,dev.env.js
用于开发环境,prod.env.js
用于生产环境,你可以将开发阶段和生产阶段不一样的信息(如后台API的url地址等)配置到这两个文件中去,然后在代码中以变量的形式进行引用。例如,这2个文件中分别配置了不同的API_BASE_URL
值:
那你在编写请求后端API的代码时,你就可以使用这个环境配置,像这样:
这样一来,开发阶段和上线发布阶段的环境可以清楚的区分开来。
src
目录是我们主要进行小程序功能编写的地方。默认生成的demo代码为我们创建了几个子目录:components
、pages
和utils
,还有2个文件:App.vue
和main.js
。其实它们都不是必须的,可以按照自己的风格进行定义和配置。不过默认创建的这个结构基本上是一个约定俗成的结构了,比较易于理解,所以我们可以遵循这个结构进行开发。
app.json
和app.js
的复合体。
2
Vue的语法
为了清楚起见,我们将要对vue-cli生成的代码做一个清理工作,具体如下:
src/components
、src/pages
、src/utils
三个目录下的所有代码文件src/App.vue
文件中的内容重置成:
src/main.js
文件中的内容重置成:
至此,我们的代码就成了一个小程序页面都没有的初始状态。
学习过使用小程序原生框架开发的朋友都知道,一个小程序的入口应该包含这三个最重要的部分: 1)app.json 2)app.js 3)首页
有了这三个部分,才能成功运行起一个最简单的小程序。
app.json
是小程序的全局配置文件,其包含了小程序的页面文件路径配置、窗口的全局样式信息、底部选项卡式菜单栏的配置,以及一些小程序网络超时的配置等等。app.json
的配置详情我们可以查阅参考小程序的官方文档来作进一步了解。那么,在mpvue中我们如何来做与之等价的配置呢?
其实在src/main.js
中,我们就可以完整的进行这些信息的配置,具体可以查看该文件的最底部代码:
在该代码中通过export default
导出的对象的config
属性下的值,就是这些小程序的配置信息了。
app.js
中包含了小程序的各种原生生命周期方法,如onLaunch
、onShow
等等。而在mpvue中,它使用了一个简单的Vue组件App.vue
来实现等价的功能。我们在这个App.vue
组件中可以编写小程序的生命周期方法(通常使用Vue的生命周期方法,但也兼容原生的生命周期方法),也可以在其中加入小程序的全局样式:
接着,这个App.vue
组件被src/main.js
引入并被设置了一个mpType
的属性值,其值为app
。这个值是为了与后面要讲的小程序页面组件所区分开来,因为小程序页面组件和这个App.vue
组件的写法和引入方式是一致的,为了区分两者,需要设置mpType
值。引入这个App.vue
组件后,会用它作为参数来创建一个Vue的实例,并调用$mount()
方法加载。下面是这个过程的关键代码:
每个小程序都需要至少有一个页面,第一个展示的页面被叫做首页。因为前面已经把所有的页面代码都删完了,所以我们现在要新建一个首页。在src/pages
目录下,我们新建一个名为index
的子目录,然后在该子目录下,新建2个文件:一个用于实现页面主体功能的index.vue
组件,另一个则用于将这个页面组件生成Vue实例并加载的main.js
。以后的每一个mpvue页面组件都会拥有这样的结构。
然后在main.js
中编写如下代码,非常简单的一段代码,它的功能是引入index.vue
并创建Vue实例:
当然了,你也可以像在src/main.js
中一样去导出一个页面级别的配置,因为小程序的每个页面都可以有一些单独的配置:
接着,我们需要实现index.vue
页面组件,它的写法是最典型的Vue组件写法。
可以看到,这个组件完全看不到小程序写法的影子,而是全部由Vue开发Web应用的写法来完成:数据绑定、事件处理、scoped局部样式、以及使用HTML标签来构建界面。这样最大化的保持和网页应用开发一致,减少了前端人员切换到小程序的学习理解成本,也为原先使用Vue开发的网页应用移植到小程序平台提供了降低迁移成本的可能。
模板部分我们通常可以用HTML标签来写,比如div
、span
等,它们会在编译的时候被自动转换成小程序的原生组件view
、text
之类;而那些小程序特有的组件如swiper
、rich-text
等,可以直接在模板中使用。
在原生小程序的页面(Page)中包含了很多页面的生命周期方法,如onLoad
、onUnload
、onShow
、onHide
、onPullDownRefresh
等等,mpvue中推荐使用Vue组件生命周期方法,而像onPullDownRefresh
、onReachBottom
这类特殊功能的生命周期则需直接使用原生的。
回头再来看,当我们实现了这个index.vue
页面组件后,其实还缺最后一个步骤,就是需要将这个页面组件指定为首页。如果我们的小程序只有一个页面的话,其实也可以省略这一步,因为mpvue会自动将src/pages
目录下的页面组件路径添加到最终编译出来的小程序配置文件中去:
但是,大多数情况下我们的小程序会由很多个页面组成,在src/pages
目录下编写多个页面组件后,mpvue也会自动把它们都添加进配置文件,但是由于小程序有一个机制:
配置文件中pages数组里的第一个page路径会被当做是首页
如果你期望的首页组件并没有被mpvue添加到第一个路径的话,就不会被当做首页显示。比如有多个页面,并在dist/app.json
里生成的是下面的序列,则第一个pages/articles/main
页面会被当做首页:
为了解决这种情况,我们需要显式的去指定首页。可以在src/main.js
的配置里,加入这样一行配置信息:
注意:以上配置中指定为首页的路径前面有个^
符号。
加入这行配置之后,pages/index/main
总是会在最终生成的dist/app.json
中排在第一个位置,成为首页。
3
mpvue / Vue的重要功能
既然mpvue
是基于Vue的,那么就没有理由不进一步学习一下Vue最核心的东西:组件。组件系统是Vue应用开发中最具价值的特性之一,在前文中其实我们就已经有在使用组件了,比如App.vue
和首页index.vue
就是两个Vue组件。
组件是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树,若干的小组件可以聚合成一个完整的界面:
一个好的组件系统一定会有这些特点:封装性、复用性、扩展性。对于Vue的组件来说,这几点都算是实现的比较的优秀的。
Vue组件的写法可以避免将属于一个独立逻辑单位的代码散落在各处,可以将界面(DOM)、样式(CSS)、行为(JS)三部分的代码很好的组织在一起(推荐的实践是使用.vue
文件)。在设计编写一个组件时,我们要记住的原则就是:
避免向外部暴露过多的东西,只暴露必要的外部交互接口(组件属性、事件、方法等)。
下面我们来在原先的代码基础上,创建一个简单的按钮点击计数器组件,它将实现的功能是:点击按钮并展示已点击按钮次数、点击清零按钮实现点击次数的归零。在src/components
目录下,新建一个click-counter.vue
组件文件,并编写如下代码:
编写完这个组件后,我们来尝试在首页组件src/pages/index/index.vue
文件中使用它:
完成上面两个步骤后,记得重新运行一下命令行npm run dev
(注意点:新增文件必须重新运行该命令,编译器不会自动检测新加入的文件)。成功后通过微信开发者工具的模拟器查看,结果界面将会是这样的:
点击“点我呀!”按钮,计数器就会累加点击次数并更新界面上的数字;而点击“清零”按钮,则会将统计数字归零。
回到代码上来看,对于click-counter.vue
的使用者index.vue
来说,它并不关心太多click-counter.vue
的实现细节,引入该组件文件并进行声明,就可以通过标签的形式来使用它了,非常简单明了。而且,这样一个click-counter.vue
组件也可以被拿到其他的Vue/mpvue代码中使用,其他使用者也并不需要关注它的实现细节,而只需要关心它能实现什么功能就行了。这就是组件封装带来的好处。
不过,目前的这个click-counter组件还没有跟它的父组件之间有什么交互或通信,没有体现出“暴露接口”的特性,那让我们来增加点代码,了解下这一特性。首先解释一下我们要实现的功能:组件可以接收一个外部设置的初始点击次数值,在点击“点我呀!”按钮的时候,从这个初始值开始进行累加;并且点击按钮后,可以通知组件的使用者(即父组件)当前的点击统计值。
修改click-counter.vue
的代码:
修改index.vue
的代码:
观察以上修改后的代码可以发现,在click-couter.vue
中的主要变化是:
props
定义了一个名为initNum
的数字型组件属性(且初始值为0)。它可用于接收使用组件外部传入的值。然后,这个initNum
值被赋值到data
中的属性num
上作为它的初始值。notifyNum()
方法,它向组件触发了一个自定义事件clicknum
并携带了当前点击次数值。而在index.vue
中的主要变化是实例化click-counter
组件的这行代码:
实例化组件的时候,为组件传入了initNum
属性值10
;并且添加了一个对自定义事件clicknum
的监听方法。
这样一个结构实现了数据进入组件/数据传出组件的机制,父子组件之间就能实现数据通信。通过有限的通信点进行数据互换,而不是直接进行函数调用,可以使得代码结构更优雅、更易维护。
组件的复用性就好理解的多了,创建组件的目的,大多数时候就是希望这个组件可以被多个地方、多次使用,避免编写重复的代码。比如我们前面的计数器组件,有可能一个项目中的多个页面会用到,也可能一个页面就会使用多次。
Vue组件的复用也是很容易的,比如我们要在前面例子中的index.vue
中复用计数器组件,创建3个计数器,那么直接在模板部分编写3个标签就行了:
运行后的效果如下图所示,这三个计数器都能独立统计各自的点击数量:
谈到扩展性,有面向对象编程经验的开发者就会想到“继承(extends)”。继承是一种比较有效的扩展机制,不过随着继承的层次变深,代码也会变得难以理解。在Vue组件中,没有采用继承的机制,而是推荐使用“组合”的方式。
在组合理念下,我们尽量将想复用性高的组件设计到最小可拆分单位,比如按钮、输入框、单选框等等,然后再将这些低层组件放入更高层组件中,一层一层,慢慢拼装出满足需求的业务界面。
除了组合,Vue组件还提供了插槽(Slot)功能,相当于在一个组件中挖出了一个或多个坑,在具体使用这些具有插槽的组件时,可以选择往坑里面填什么内容(其他组件)。
举个例子,在计数器组件中,我们在清零按钮后面用<slot></slot>
挖了一个坑:
而后,在index.vue
中使用计数器组件时,在<click-counter>
标签体中放入了额外的内容,会被传入该组件中去用于填坑:
从运行结果可以看到,清零按钮后面已经多出了我们传入的复选框和文字内容:
插槽其实可以理解为是另一种形式的组件属性:普通组件属性传入的是比较简单类型的数据;而插槽传入的可以是更复杂的界面组件而已。
4
使用mpvue需要特别注意的地方。
v-html
指令不可用这条很好理解,小程序的界面并不是基于浏览器的BOM/DOM的,所以不能动态的在界面模板里直接插入HTML片段来显示。如果有在小程序里插入html片段的需求怎么办?可以用<rich-text>
组件或者wxParse
来实现。
{{}}
中的表达式功能存在诸多限制在Vue本身的模板内双括号语法中,我们可以对绑定变量进行比较丰富的处理,比如:
methods
下的函数, 例如:
以上这些好用的功能,在mpvue中,记得都是通通不能用的哦!!!
我们只能在双括号中使用一些简单的运算符运算(+ - * % ?: ! == === > < [] .
)。
但是也得找些可用的替代方案呐,大伙先考虑使用计算属性(computed)来做吧。
methods
下的函数在Vue中,模板里调用methods
部分定义的函数是非常常见的,比如下面这段代码所示,在v-if
指令中调用函数getErrorNum()
:
可是,在mpvue里就是不可以用!因为在小程序原生模板wxml
里就不支持这种函数调用,导致mpvue没有很好的方式转译过去。所以,可用的替代方案可能还是计算属性了。
style
或class
属性上在Vue中我们可以为HTML元素的class
或style
绑定一个对象,并按照对象内的属性值来决定是否添加对应的属性名到HTML元素的样式名。示例如下:
上面这段代码的运行后生成的HTML将是:
但是在mpvue下面这个特性也不能用,按官方说法是由于涉及到一些性能相关的原因。那如果要动态改变组件的class
该怎么写呢?官方给出的方式是这样的:
其实改动不大,稍微多打了一些字而已,相当于在模板的class
里再定义一个对象罢了。但是据文档中说这样会提升性能。看来最好一点的方案,还是得使用计算属性,直接生成一串样式的字符串,绑定到class
或style
上:
v-for
时,必须指定索引index
通常,我们在Vue模板中嵌套循环渲染数组的时候,一般是这个样子的:
但在mpvue中使用这种嵌套结构的v-for
时,则必须每层的v-for
上都给出索引,且索引需取不同名字:
在mpvue中,一般可以使用Web的DOM事件名来绑定事件,mpvue会将Web事件名映射成对应的小程序事件名,对应列表如下:
除了上面的之外,Web表单组件<input>
和<textarea>
的change事件会被转为blur事件。像keydown
、keypress
之类的键盘事件也没有了,因为小程序没有键盘,所以不需要这些事件。
Vue里面绑定事件的时候,可以指定事件修饰符,但是在mpvue里,官方给出了一些注意信息:
所以呢,总之当你在遇到事件相关的问题,请回来查看一下文档,看看自己是否已经掉在坑里了。
一句话,表单组件又多又复杂,框架可能Hold不住。所以在实际开发中,推荐直接使用小程序的表单组件标签来写,而不是使用Web的表单组件标签来写。当然了,在mpvue中使用了小程序的组件标签,数据绑定功能还是完全可以用的。给个示例:
另外,在Vue开发Web应用的时候,通常使用vue-router
来进行页面路由。但是在mpvue小程序开发中,不能用这种方式,请使用<a>
标签和小程序原生API wx.navigateTo
等来做路由功能。
还有就是请求后端数据,我们通常在Web开发中使用axios
等ajax库来实现,但是在小程序开发中也是不能用的,也请使用小程序的原生API wx.request
等来进行。