使用mpvue开发小程序
前段时间,美团开源了mpvue
这个项目,使得我们又多了一种用来开发小程序的框架选项。由于mpvue
框架是完全基于Vue框架的(重写了其runtime和compiler),因此在用法上面是高度和Vue一致的(某些功能由于受限于小程序环境本身的原因而不能使用),这给使用过Vue开发Web应用的前端开发者提供了极低的切换门槛来开发小程序。
如果之前还未曾用过Vue这个框架的话,建议你可以快速浏览一下Vue的官方文档(https://cn.vuejs.org/v2/guide/)。
起手式:必要的开发环境
工欲善其事必先利其器!在开始写代码之前,请确保你已经安装了必要的开发环境和工具,以下是几个必需的和可选的工具:
1)node.js
现在,前端工具链基本都依赖Node.js,所以请率先安装它吧。
下载地址:https://nodejs.org/en/download/
安装完成后,打开你的命令行输入如下命令,验证安装是否成功:
node --version
//成功的话输出类似:v10.6.0
npm --version
//成功的话输出类似:6.1.0
然后,我们需要执行以下命令,将npm的下载源切换到国内淘宝的镜像,以提高下载时的速度和成功率:
npm set registry https://registry.npm.taobao.org/
2)vue-clivue-cli
是一个vue专用的项目脚手架工具,可以用于方便的创建vue项目骨架代码,包括我们要讲到的mpvue的项目代码。我们可以通过安装node.js后里面包含的npm工具来安装vue-cli,在命令行输入如下命令:
npm install vue-cli -g
安装完成后,输入如下命令进行验证:
vue
// 成功的话会输出如下内容:
// Usage: vue <command> [options]
//
// Options:
//
// -V, --version output the version number
// -h, --help output usage information
//
// Commands:
//
// init generate a new project from a template
// list list available official templates
// build prototype a new project
// create (for v3 warning only)
// help [cmd] display help for [cmd]
3)微信开发者工具
这个工具是开发、调试和模拟运行微信小程序的最核心的工具了,所以必须安装。
下载地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
4)Visual Studio Code + Vetur
Visual Studio Code(简称vscode)是现在非常流行的一个轻量级代码编辑器,拥有非常多好用的辅助开发插件,在我的文章中我都会使用这个编辑器来编辑代码。当然,好用的代码编辑器有很多,比如Sublime Text、WebStorm等,同样可以达到我们的开发目的,你也尽管用你自己最喜欢的代码编辑器来写代码就行了。
下载地址:https://code.visualstudio.com
安装完vscode后,在它的插件管理器中,查找Vetur并安装,然后重启一下vscode后,插件即生效:
Vetur是一款可以提供Vue语法高亮、语法检查和代码快捷输入等功能的插件,可以为我们的开发过程提供很多便利。
创建第一个基于mpvue的小程序项目代码
花了点时间装好了必要的开发环境,下面我们就来创建我们的第一个mpvue小程序项目。这里将用到前面已安装的vue-cli:
vue init mpvue/mpvue-quickstart firstapp
命令行将一步步的引导我们选择或填写项目的配置信息,如果你还不太明白这些内容的含义,可以先直接全部按回车:
? Project name firstapp
? wxmp appid touristappid
? Project description A Mpvue project
? Author kevinzhang <kevin.aaa@qq.com.com>
? Vue build runtime
? Use Vuex? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? 小程序测试,敬请关注最新微信开发者工具的“测试报告”功能
vue-cli · Generated "firstapp".
To get started:
cd firstapp
npm install
npm run dev
Documentation can be found at http://mpvue.com
这个过程vue-cli主要是先从远程的代码仓库中下载了一份注册名为mpvue/mpvue-quickstart
的模板代码,然后根据开发者在命令行提示过程中输入的信息,生成一份经过配置后的代码。
这份代码暂时还运行不起来,因为它还缺少依赖的库,我们需要执行以下命令进行依赖库的安装:
cd firstapp
npm install
经过几分钟的下载安装,依赖库安装到了firstapp目录下,你可以看到该目录下多出了一个node_modules目录。
然后,执行命令让这个代码运行起来,进入开发模式:
npm run dev
成功运行后,这个项目代码就进入开发模式,一旦有源代码发生修改,就会触发自动编译。因为mpvue使用的是Vue + HTML Web的开发方式开发小程序,它最终还是需要被转换成小程序的代码才可以在小程序环境运行,所以这里的自动编译的目的就是要把Web代码编译成小程序代码。编译后的代码会在dist目录下:
运行并查看结果
上面的步骤中,我们开启开发模式后,其实并不能看到小程序的执行效果,要真正看小程序的运行界面的话,我们还是要借助微信开发者工具。
打开微信开发者工具,选择新增项目:
项目目录选择指向firstapp目录:
点击“确定”按钮,进入小程序开发主界面,在左边的小程序模拟器中就能看到firstapp小程序的执行结果了:
【提醒】记得在微信开发者工具的菜单》设置 》编辑设置 中,将“保存时自动编译小程序”勾选上,这样当mpvue的代码自动编译完成后,模拟器才会自动刷新界面。
小结
本文先简要介绍一下使用mpvue开发小程序的前期准备,在后面的文章中将一步步讲解mpvue的详细用法。
我们介绍了使用mpvue开发小程序所需要的一些开发环境的搭建,并创建了第一个mpvue小程序代码骨架并将其运行起来。在本文中,我们来研究熟悉一下mpvue项目的主要目录和文件结构。
在Visual Studio Code里面打开项目文件夹,我们可以看到类似如下的文件结构:
firstapp
├── package.json
├── project.config.json
├── static
├── src
│ ├── components
│ ├── pages
│ ├── utils
│ ├── App.vue
│ └── main.js
├── config
│ ├── index.js
│ ├── dev.env.js
│ └── prod.env.js
└── build
1)package.json文件
package.json是项目的主配置文件,里面包含了mpvue项目的基本描述信息、项目所依赖的各种第三方库以及版本信息、以及可执行的脚本信息。
我们看到该文件中的scripts
部分配置了4个可执行的命令:
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"build": "node build/build.js",
"lint": "eslint --ext .js,.vue src"
-
dev
和start
是两个等价的命令,执行其中之一都可以将项目以开发模式启动。执行方式是:
npm start
npm run dev
-
lint
指令是使用ESLint来进行代码语法和格式检查,以及修复一些可自动修复的问题。执行方式是:
npm run lint #检查语法和格式
npm run lint -- --fix #检查代码语法和格式,并修复可自动修复的问题
-
build
指令是用于生成发布用代码的,它会对代码进行一些压缩优化处理。当小程序开发完成后,将要提交审核时,请使用build
来生成发布的代码。
2)project.config.json文件
project.config.json
文件是用于管理微信开发者工具的小程序项目的配置文件,其中记录了小程序的appid、代码主目录、以及编译选项等等信息,在微信开发者工具中导入小程序项目的时候主要是通过该配置文件读取和写入配置信息。
3)static目录
static目录可以用于存放各种小程序本地静态资源,如图片、文本文件等。代码中可通过相对路径或绝对路径进行访问, 如:
<img src="/static/button.png" />
<img src="../../../static/button.png" />
4)build目录
build
目录下是一些用于项目编译打包的node.js脚本和webpack配置文件。一般情况下不需要修改这些文件。
5)config目录
config
目录下包含了用于开发和生产环境下的不同配置,dev.env.js
用于开发环境,prod.env.js
用于生产环境,你可以将开发阶段和生产阶段不一样的信息(如后台API的url地址等)配置到这两个文件中去,然后在代码中以变量的形式进行引用。例如,这2个文件中分别配置了不同的API_BASE_URL
值:
// dev.env.js
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
API_BASE_URL: '"http://127.0.0.1:8080/api"'
})
// prod.env.js
module.exports = {
NODE_ENV: '"production"',
API_BASE_URL: '"https://www.my-domain.com/api"'
}
那你在编写请求后端API的代码时,你就可以使用这个环境配置,像这样:
const baseURL = process.env.API_BASE_URL
wx.request({
url: `${baseURL}/products`
})
这样一来,开发阶段和上线发布阶段的环境可以清楚的区分开来。
6)src目录
src
目录是我们主要进行小程序功能编写的地方。默认生成的demo代码为我们创建了几个子目录:components
、pages
和utils
,还有2个文件:App.vue
和main.js
。其实它们都不是必须的,可以按照自己的风格进行定义和配置。不过默认创建的这个结构基本上是一个约定俗成的结构了,比较易于理解,所以我们可以遵循这个结构进行开发。
- components:在实际开发中,我们可以尽量将界面上可复用的部分,提取成vue组件放入该目录
- pages:存放小程序的页面。请遵循每个小程序页面放入一个单独子目录的组织形式
- utils:可选(可删)。可以将代码中一些公用工具函数组织成模块放入该目录下
- 可新建其他目录,存放你希望组织起来的代码。比如公用的业务逻辑代码、请求后台API的代码等等
- main.js + App.vue:这两个是入口文件,相当于原生小程序框架中的
app.json
和app.js
的复合体。
小结
本文主要介绍了mpvue工程的代码结构,大家可以多熟悉一下它们,以方便后续的实际开发工作。
【注意事项】由于mpvue也在不断的开发演进,大家在不同时间段使用的时候,可能会遇到和文中的做法不一样的地方。请关注文章的评论区中大家的讨论,寻找解决方案,或者及时查阅官方文档,避免陷入版本更新的坑里哦。
我们熟悉了一下通过vue-cli生成的mpvue工程代码骨架的基本结构,大致了解了每一个部分的代码到底要放到何处。从本文起我们就开始涉及真正的编码部分,学习使用Vue的语法去编写小程序。
为了清楚起见,我们将要对vue-cli生成的代码做一个清理工作,具体如下:
- 删掉src/components、src/pages、src/utils三个目录下的所有代码文件
- 将
src/App.vue
文件中的内容重置成:
<script>
export default {}
</script>
<style>
</style>
- 将
src/main.js
文件中的内容重置成:
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue(App)
app.$mount()
export default {
config: {
pages: [],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: '第一个小程序',
navigationBarTextStyle: 'black'
}
}
}
至此,我们的代码就成了一个小程序页面都没有的初始状态。
程序入口
学习过使用小程序原生框架开发的朋友都知道,一个小程序的入口应该包含这三个最重要的部分:
1)app.json
2)app.js
3)首页
有了这三个部分,才能成功运行起一个最简单的小程序。
app.json
app.json
是小程序的全局配置文件,其包含了小程序的页面文件路径配置、窗口的全局样式信息、底部选项卡式菜单栏的配置,以及一些小程序网络超时的配置等等。app.json
的配置详情我们可以查阅参考小程序的官方文档来作进一步了解。那么,在mpvue中我们如何来做与之等价的配置呢?
其实在src/main.js中,我们就可以完整的进行这些信息的配置,具体可以查看该文件的最底部代码:
export default {
// 这部分相当于原生小程序的 app.json
config: {
pages: [],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: '第一个小程序',
navigationBarTextStyle: 'black'
}
}
}
在该代码中通过export default
导出的对象的config
属性下的值,就是这些小程序的配置信息了。
app.js
app.js
中包含了小程序的各种原生生命周期方法,如onLaunch
、onShow
等等。而在mpvue中,它使用了一个简单的Vue组件App.vue
来实现等价的功能。我们在这个App.vue
组件中可以编写小程序的生命周期方法(通常使用Vue的生命周期方法,但也兼容原生的生命周期方法),也可以在其中加入小程序的全局样式(等价于原生方式下的app.wxss
):
<script>
/* 这部分相当于原生小程序的 app.js */
export default {
created () {
console.log('miniapp created!!!')
}
}
</script>
<style>
/* 这部分相当于原生小程序的 app.wxss */
.container {
background-color: #cccccc;
}
</style>
接着,这个App.vue
组件被src/main.js
引入并被设置了一个mpType
的属性值,其值为app
。这个值是为了与后面要讲的小程序页面组件所区分开来,因为小程序页面组件和这个App.vue
组件的写法和引入方式是一致的,为了区分两者,需要设置mpType
值。引入这个App.vue
组件后,会用它作为参数来创建一个Vue的实例,并调用$mount()
方法加载。下面是这个过程的关键代码行:
App.mpType = 'app'
const app = new Vue(App)
app.$mount()
首页、以及其他页面
每个小程序都需要至少有一个页面,第一个展示的页面被叫做首页。因为前面已经把所有的页面代码都删完了,所以我们现在要新建一个首页。在src/pages
目录下,我们新建一个名为index
的子目录(请遵循每个页面放入一个子目录的良好习惯),然后在该子目录下,新建3个文件:一个用于实现页面主体功能的index.vue
组件,一个用于将这个页面组件生成Vue实例并加载的main.js
,另一个则用于配置页面信息的main.json
。以后的每一个mpvue页面组件都会拥有这样的结构。
然后在main.js
中编写如下代码,非常简单的一段代码,它的功能是引入index.vue
并创建Vue实例:
import Vue from 'vue'
import App from './index'
const app = new Vue(App)
app.$mount()
当然了,你也可以在main.json
中导出一个页面级别的配置,因为小程序的每个页面都可以有一些单独的配置:
{
"navigationBarTitleText": "文章列表页面"
}
接着,我们需要实现index.vue
页面组件,它的写法是最典型的Vue组件写法。
<template>
<div class="container" @click="clickHandle">
<div class="message">{{msg}}</div>
</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello'
}
},
methods: {
clickHandle () {
this.msg = 'Clicked!!!!!!'
}
}
}
</script>
<style scoped>
.message {
color: red;
padding: 10px;
text-align: center;
}
</style>
可以看到,这个组件完全看不到小程序写法的影子,而是全部由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
目录下的页面组件路径添加到最终编译出来的小程序配置文件中去(可以打开dist/app.json
文件观察一下):
{
"pages": [
"pages/index/main"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "第一个小程序",
"navigationBarTextStyle": "black"
}
}
但是,大多数情况下我们的小程序会由很多个页面组成,在src/pages目录下编写多个页面组件后,mpvue也会自动把它们都添加进配置文件,但是由于小程序有一个机制:
配置文件中pages数组里的第一个page路径会被当做是首页
如果你期望的首页组件并没有被mpvue添加到第一个路径的话,就不会被当做首页显示。比如有多个页面,并在dist/app.json
里生成的是下面的序列,则第一个pages/articles/main
页面会被当做首页:
"pages": [
"pages/articles/main",
"pages/authors/main",
"pages/index/main",
"pages/kickstart/main"
]
为了解决这种情况,我们需要显式的去指定首页。可以在src/main.js的配置里,加入这样一行配置信息:
pages: [
'^pages/index/main'
]
注意:以上配置中指定为首页的路径前面有个^符号。
加入这行配置之后,pages/index/main
总是会在最终生成的dist/app.json
中排在第一个位置,成为首页。
小结
今天主要了解了作为一个最简单的可以运行的mpvue小程序所应该包含的各个代码部分,希望你可以动手实践一下,理解和掌握这些内容。Good Luck!
我们将vue-cli
命令行工具生成的代码骨架中的src目录清理了一遍,然后从头开始配置和编写了一个可以运行的小程序页面,算是正真走上了使用mpvue开发小程序的第一步。现在我们将进一步来了解和学习mpvue / Vue
的其他重要功能。
既然mpvue
是基于Vue的,那么就没有理由不进一步学习一下Vue最核心的东西:组件。组件系统是Vue应用开发中最具价值的特性之一,在前文中其实我们就已经有在使用组件了,比如App.vue
和首页index.vue
就是两个Vue组件。
组件是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树,若干的小组件可以聚合成一个完整的界面:
一个好的组件系统一定会有这些特点:封装性、复用性、扩展性。对于Vue的组件来说,这几点都算是实现的比较的优秀的。
组件的封装性
Vue组件的写法可以避免将属于一个独立逻辑单位的代码散落在各处,可以将界面(DOM)、样式(CSS)、行为(JS)三部分的代码很好的组织在一起(推荐的实践是使用.vue文件)。在设计编写一个组件时,我们要记住的原则就是:
避免向外部暴露过多的东西,只暴露必要的外部交互接口(组件属性、事件、方法等)。
下面我们来在原先的代码基础上,创建一个简单的按钮点击计数器组件,它将实现的功能是:点击按钮并展示已点击按钮次数、点击清零按钮实现点击次数的归零。在src/components
目录下,新建一个click-counter.vue
组件文件,并编写如下代码:
<template>
<div class="click-counter">
<div class="counter-num">次数:{{num}}</div>
<button class="counter-btn" @click="handleClick">点我呀!</button>
<button class="counter-reset-btn" @click="handleResetClick">清零</button>
</div>
</template>
<script>
export default {
data() {
return {
num: 0
};
},
methods: {
handleClick() {
this.num += 1;
},
handleResetClick() {
this.num = 0;
}
}
};
</script>
<style>
.click-counter {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid red;
background-color: #ffffff;
padding: 10px;
}
.counter-num,
.counter-btn,
.counter-reset-btn {
flex: 1;
margin: 3px;
}
</style>
编写完这个组件后,我们来尝试在首页组件src/pages/index/index.vue
文件中使用它:
<template>
<div class="container" @click="clickHandle">
<div class="message">{{msg}}</div>
<!-- 使用 click-counter 组件 -->
<click-counter />
</div>
</template>
<script>
// 导入 click-counter 组件
import ClickCounter from "@/components/click-counter";
export default {
// 声明在当前组件下使用 counter-click 组件
components: { ClickCounter },
data() {
return {
msg: "Hello"
};
},
methods: {
clickHandle() {
this.msg = "Clicked!!!!!!";
}
}
};
</script>
<style scoped>
.message {
color: red;
padding: 10px;
text-align: center;
}
</style>
完成上面两个步骤后,记得重新运行一下命令行npm run dev
(注意点:新增文件必须重新运行该命令,编译器不会自动检测新加入的文件)。成功后通过微信开发者工具的模拟器查看,结果界面将会是这样的:
点击“点我呀!”按钮,计数器就会累加点击次数并更新界面上的数字;而点击“清零”按钮,则会将统计数字归零。
回到代码上来看,对于click-counter.vue
的使用者index.vue
来说,它并不关心太多click-counter.vue
的实现细节,引入该组件文件并进行声明,就可以通过标签的形式来使用它了,非常简单明了。而且,这样一个click-counter.vue
组件也可以被拿到其他的Vue/mpvue代码中使用,其他使用者也并不需要关注它的实现细节,而只需要关心它能实现什么功能就行了。这就是组件封装带来的好处。
不过,目前的这个click-counter组件还没有跟它的父组件之间有什么交互或通信,没有体现出“暴露接口”的特性,那让我们来增加点代码,了解下这一特性。首先解释一下我们要实现的功能:组件可以接收一个外部设置的初始点击次数值,在点击“点我呀!”按钮的时候,从这个初始值开始进行累加;并且点击按钮后,可以通知组件的使用者(即父组件)当前的点击统计值。
修改click-counter.vue
的代码:
<template>
<div class="click-counter">
<div class="counter-num">次数:{{num}}</div>
<button class="counter-btn" @click="handleClick">点我呀!</button>
<button class="counter-reset-btn" @click="handleResetClick">清零</button>
</div>
</template>
<script>
export default {
// 增加一个可从外部传入的属性initNum
props: {
initNum: {
type: Number,
default: 0
}
},
data() {
return {
num: this.initNum //使用传入的initNum值作为初始的点击数
};
},
methods: {
handleClick() {
this.num += 1;
this.notifyNum();
},
handleResetClick() {
this.num = 0;
this.notifyNum();
},
notifyNum() {
//触发自定义事件 clicknum
this.$emit("clicknum", {
num: this.num
});
}
}
};
</script>
<style scoped>
.click-counter {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid red;
background-color: #ffffff;
padding: 10px;
}
.counter-num,
.counter-btn,
.counter-reset-btn {
flex: 1;
margin: 3px;
}
</style>
修改index.vue
的代码:
<template>
<div class="container" @click="clickHandle">
<div class="message">{{msg}}</div>
<!-- 使用 click-counter 组件 -->
<click-counter :init-num="10" @clicknum="handleClickNum" />
</div>
</template>
<script>
// 导入 click-counter 组件
import ClickCounter from "@/components/click-counter";
export default {
// 声明在当前组件下使用 counter-click 组件
components: { ClickCounter },
data() {
return {
msg: "Hello"
};
},
methods: {
clickHandle() {
this.msg = "Clicked!!!!!!";
},
handleClickNum(data) {
console.log(">>>>>>", data.num);
}
}
};
</script>
<style scoped>
.message {
color: red;
padding: 10px;
text-align: center;
}
</style>
观察以上修改后的代码可以发现,在click-couter.vue
中的主要变化是:
-
使用
props
定义了一个名为initNum
的数字型组件属性(且初始值为0)。它可用于接收使用组件外部传入的值。然后,这个initNum
值被赋值到data
中的属性num
上作为它的初始值。 -
在两个按钮的click事件处理方法中,额外调用了一个
notifyNum()
方法,它向组件触发了一个自定义事件clicknum
并携带了当前点击次数值。
而在index.vue
中的主要变化是实例化click-counter
组件的这行代码:
<click-counter :init-num="10" @clicknum="handleClickNum" />
实例化组件的时候,为组件传入了initNum
属性值10
;并且添加了一个对自定义事件clicknum
的监听方法。
这样一个结构实现了数据进入组件/数据传出组件的机制,父子组件之间就能实现数据通信。通过有限的通信点进行数据互换,而不是直接进行函数调用,可以使得代码结构更优雅、更易维护。
组件的复用性
组件的复用性就好理解的多了,创建组件的目的,大多数时候就是希望这个组件可以被多个地方、多次使用,避免编写重复的代码。比如我们前面的计数器组件,有可能一个项目中的多个页面会用到,也可能一个页面就会使用多次。
Vue组件的复用也是很容易的,比如我们要在前面例子中的index.vue
中复用计数器组件,创建3个计数器,那么直接在模板部分编写3个标签就行了:
<template>
<div class="container" @click="clickHandle">
<div class="message">{{msg}}</div>
<!-- 创建 3个 click-counter 组件 -->
<click-counter :init-num="10" @clicknum="handleClickNum" />
<click-counter :init-num="20" @clicknum="handleClickNum" />
<click-counter :init-num="30" @clicknum="handleClickNum" />
</div>
</template>
运行后的效果如下图所示,这三个计数器都能独立统计各自的点击数量:
组件的扩展性
谈到扩展性,有面向对象编程经验的开发者就会想到“继承(extends)”。继承是一种比较有效的扩展机制,不过随着继承的层次变深,代码也会变得难以理解。在Vue组件中,没有采用继承的机制,而是推荐使用“组合”的方式。
在组合理念下,我们尽量将想复用性高的组件设计到最小可拆分单位,比如按钮、输入框、单选框等等,然后再将这些低层组件放入更高层组件中,一层一层,慢慢拼装出满足需求的业务界面。
除了组合,Vue组件还提供了插槽(Slot)功能,相当于在一个组件中挖出了一个或多个坑,在具体使用这些具有插槽的组件时,可以选择往坑里面填什么内容(其他组件)。
举个例子,在计数器组件中,我们在清零按钮后面用<slot></slot>
挖了一个坑:
<template>
<div class="click-counter">
<div class="counter-num">次数:{{num}}</div>
<button class="counter-btn" @click="handleClick">点我呀!</button>
<button class="counter-reset-btn" @click="handleResetClick">清零</button>
<slot></slot>
</div>
</template>
而后,在index.vue
中使用计数器组件时,在<click-counter>
标签体中放入了额外的内容,会被传入该组件中去用于填坑:
<template>
<div class="container" @click="clickHandle">
<div class="message">{{msg}}</div>
<!-- 使用 click-counter 组件 -->
<click-counter :init-num="10" @clicknum="handleClickNum">
<!-- 填坑用... -->
<input type="checkbox" /> 禁用
</click-counter>
</div>
</template>
从运行结果可以看到,清零按钮后面已经多出了我们传入的复选框和文字内容:
插槽其实可以理解为是另一种形式的组件属性:普通组件属性传入的是比较简单类型的数据;而插槽传入的可以是更复杂的界面组件而已。
小结
本文我们初步学习了一下Vue组件的相关理念和特性,希望大家花点时间去熟悉和掌握这些比较核心的知识点,相信不管在之后使用Vue进行Web应用开发,还是mpvue小程序开发,都会更加得心应手、事半功倍的!
我们了解了组件的三个基本特性以及组件的基本使用方法。在实际的小程序开发中,我们应该以组件的思维去设计每个小程序的功能页面,对其进行合理的组件拆分,让每个部分都保持功能简洁、条理清楚、各司其职,这样会让代码变得更易理解和维护,间接的也提升了代码的健壮性,降低出现Bug的几率,即使出现Bug,也会更容易进行定位和调试。
由于mpvue采用的是Vue框架的基础设施,所以大部分的功能都是和Vue一致的。但是,mpvue的代码毕竟最终还是要转译成小程序原生框架下的代码的,由于小程序框架本身存在的一些功能限制,导致有些功能不能被翻译过去,也就是说有些标准的Vue功能在mpvue下是不可以使用或有特殊限制的。
现在我们就来罗列一下,在使用mpvue的时候那些需要特别注意的点。
1. 在模板中,动态插入HTML的v-html
指令不可用
这条很好理解,小程序的界面并不是基于浏览器的BOM/DOM的,所以不能动态的在界面模板里直接插入HTML片段来显示。
题外话,如果有在小程序里插入html片段的需求怎么办?可以用<rich-text>
组件或者wxParse
(https://github.com/icindy/wxParse)来实现。
2. 在模板中,用于数据绑定的双括号语法{{}}
中的表达式功能存在诸多限制
在Vue本身的模板内双括号语法中,我们可以对绑定变量进行比较丰富的处理,比如:
- 可以调用methods下的函数, 例如:
<template>
<div>{{ formatMessage(msg) }}</div>
</template>
<script>
export default {
data() {
return {
msg: "Hello,World"
}
},
methods: {
formatMessage(str) {
return str.trim().split(',').join('#')
}
}
}
</script>
- 如果变量是对象的话,也可以调用对象的成员方法
<div>{{ msg.trim().split(',').join('#') }}</div>
- 可以使用过滤器来处理变量,最有用的场景算是格式化数据了
<div>{{ msg | format }}</div>
以上这些好用的功能,在mpvue中,记得都是通通不能用的哦!!!
我们只能在双括号中使用一些简单的运算符运算(+ - * % ?: ! == === > < [] .
)。
但是也得找些可用的替代方案呐,大伙先考虑使用计算属性(computed)来做吧。
3. 在模板中,除事件监听外,其余地方都不能调用methods
下的函数
在Vue中,模板里调用methods
部分定义的函数是非常常见的,比如下面这段代码所示,在v-if
指令中调用函数getErrorNum()
:
<div v-if="getErrorNum() > 0 && code == 10001" class="error">{{ errorMsg }}</div>
可是,在mpvue里就是不可以用!因为在小程序原生模板wxml
里就不支持这种函数调用,导致mpvue没有很好的方式转译过去(虽然小程序有wxs
,但是感觉翻译过去mpvue要做的工作会比较复杂)。
所以,可用的替代方案可能还是计算属性了。
4. 在模板中,不支持直接绑定一个对象到style
或class
属性上
在Vue中我们可以为HTML元素的class
或style
绑定一个对象,并按照对象内的属性值来决定是否添加对应的属性名到HTML元素的样式名。示例如下:
<template>
<div :class="classObject"></div>
</template>
<script>
export default {
data() {
return {
classObject: {
active: true,
'text-danger': false
}
}
}
}
</script>
上面这段代码的运行后生成的HTML将是:
<div class="active"></div>
但是在mpvue下面这个特性也不能用,按官方说法是由于涉及到一些性能相关的原因。那如果要动态改变组件的class
该怎么写呢?官方给出的方式是这样的:
<div :class="{ active: classObject.active, 'text-danger': classObject['text-danger']}"></div>
其实改动不大,稍微多打了一些字而已,相当于在模板的class
里再定义一个对象罢了。但是据文档中说这样会提升性能。好吧,为了性能,这点麻烦还是能忍受的。但是它又说了:从性能考虑,建议不要过度依赖此…看来即使这样,也还是有性能问题。
看来最好一点的方案,还是得使用计算属性,直接生成一串样式的字符串,绑定到class
或style
上:
<template>
<div :class="classStr"></div>
</template>
<script>
export default {
data() {
return {
classObject: {
active: true,
'text-danger': false
}
}
},
computed: {
classStr() {
let arr = []
for (let p in this.classObject) {
if (this.classObject[p]) {
arr.push(p)
}
}
return arr.join(' ')
}
}
}
</script>
5. 在模板中,嵌套使用v-for
时,必须指定索引index
通常,我们在Vue模板中嵌套循环渲染数组的时候,一般是这个样子的:
<template>
<ul v-for="category in categories">
<li v-for="product in category.products">{{product.name}}</li>
</ul>
</template>
但在mpvue中使用这种嵌套结构的v-fo
r时,则必须每层的v-for
上都给出索引,且索引需取不同名字:
<template>
<ul v-for="(category, index) in categories">
<li v-for="(product, productIndex) in category.products">{{product.name}}</li>
</ul>
</template>
6. 事件处理中的注意点
在mpvue中,一般可以使用Web的DOM事件名来绑定事件,mpvue会将Web事件名映射成对应的小程序事件名,对应列表如下:
// 左侧为WEB事件 : 右侧为对应的小程序事件
{
click: 'tap',
touchstart: 'touchstart',
touchmove: 'touchmove',
touchcancel: 'touchcancel',
touchend: 'touchend',
tap: 'tap',
longtap: 'longtap',
input: 'input',
change: 'change',
submit: 'submit',
blur: 'blur',
focus: 'focus',
reset: 'reset',
confirm: 'confirm',
columnchange: 'columnchange',
linechange: 'linechange',
error: 'error',
scrolltoupper: 'scrolltoupper',
scrolltolower: 'scrolltolower',
scroll: 'scroll'
}
除了上面的之外,Web表单组件<input>
和<textarea>
的change事件会被转为blur事件。
然后,像keydown
、keypress
之类的键盘事件也没有了,因为小程序没有键盘,所以不需要这些事件。
还有,Vue里面绑定事件的时候,可以指定事件修饰符,但是在mpvue里,官方给出了一些注意信息:
- .stop 的使用会阻止冒泡,但是同时绑定了一个非冒泡事件,会导致该元素上的 catchEventName 失效!【这个亲测了一下,感觉是最新版本里修复了还是怎么的,没有文档里说的这个问题了】
- .prevent 可以直接干掉,因为小程序里没有什么默认事件,比如submit并不会跳转页面【也就是不需要支持】
- .capture 支持 1.0.9 【也就是在 mpvue 1.0.9及以后版本支持】
- .self 没有可以判断的标识 【也就是不支持】
- .once 也不能做,因为小程序没有 removeEventListener, 虽然可以直接在 handleProxy 中处理,但非常的不优雅,违背了原意,暂不考虑【也就是不支持
所以呢,总之当你在遇到事件相关的问题,请回来查看一下文档,看看自己是否已经掉在坑里了。
7. 对于表单,请直接使用小程序原生的表单组件
一句话,表单组件又多又复杂,框架可能Hold不住。所以在实际开发中,推荐直接使用小程序的表单组件标签来写,而不是使用Web的表单组件标签来写。当然了,在mpvue中使用了小程序的组件标签,数据绑定功能还是完全可以用的。给个示例:
<template>
<div>
<picker @change="handlePickerChange" :value="selectedIndex" :range="messages">
<view class="picker">当前消息:{{ messages[selectedIndex] }}</view>
</picker>
</div>
</template>
<script>
export default {
data () {
return {
selectedIndex: 0,
messages: ['Hello', 'World', 'Haha']
}
},
methods: {
handlePickerChange (e) {
console.log(e)
}
}
}
</script>
其他注意事项
另外,在Vue开发Web应用的时候,通常使用vue-router
来进行页面路由。但是在mpvue小程序开发中,不能用这种方式,请使用<a>
标签和小程序原生API wx.navigateTo等来做路由功能。
还有就是请求后端数据,我们通常在Web开发中使用axios等ajax库来实现,但是在小程序开发中也是不能用的,也请使用小程序的原生APIwx.request
等来进行。
我们列举了在Vue中能用但在mpvue中不能用或需要特别注意的特性,在实际开发前了解一下还是很有必要的,可以避免浪费找错误的时间。
如果你使用过原生的小程序框架,你一定经历过或思考过怎么解决以下的问题:
- 怎么存放可全局访问的变量?
- 页面跳转的时候,怎么传递参数到下一个页面比较好?
- 页面返回上一页的时候,怎么传递当前页的数据到上一页?
- 多个页面间需要同步数据,怎么做比较好?
网上一搜,解决的方法通常也是五花八门的,什么通过app上的globalData啊、通过存取storage啊、通过一个单独的模块(module)啊、通过Page路由栈啊、通过引入自定义事件啊、通过引入redux啊,等等等等…
在原生小程序框架里,确实没有提供什么太统一的方式来指导开发者解决这个问题,大家只能各自用着暂时能解决当前问题的方案。
不过,既然我们用了Vue/mpvue,遇到这种情况,自然而然的就会想到一个方案,那就是Vuex。Vuex 是一个专为 Vue 应用程序开发的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
通过使用Vuex,我们可以在mpvue里很方便的对需要在app、页面、组件之间共享的数据进行很好的统一管理,可以更方便有效的在各个代码部分对这些共享数据进行访问,同时可以使得你的代码条理变得更加清晰。
我们知道,Vuex一般有2种用法,当创建好store后:
- 第一种用法是将store绑定到需要访问store内容的Vue实例上,然后通过该Vue实例下组件的
this.$store
来引用;或通过mapState
等一系列映射函数将store中的state、getters、mutations、actions等映射成组件的计算属性或methods方法来使用; - 第二种用法是直接在组件中通过
import
导入store所在的模块文件,然后调用该store上的相关方法和属性,比如commit()
、dispatch()
等方法来操作store中的内容。
经过我的实测,上面的这两种方式在mpvue中也都是可用的。但是,由于mpvue不像Vue Web单页应用那种单Vue实例的结构,而是采用了多Vue实例的结构(app和各个页面都会由单独的Vue实例来管理),所以我个人推荐采用上面所说的第二种用法,这种方式会更加灵活和简单一些。
让我们开始写代码,先在src
目录下新建一个stores目录,接着在stores目录下新建一个名为global-store.js
的文件:
import Vue from 'vue'
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment: (state) => {
state.count += 1
},
decrement: (state) => {
state.count -= 1
}
}
});
在这个代码中,我们新建了一个Store
实例,管理了一个名为count
的数字类型的状态,并定义了2个mutations去操作(增减)这个状态值。
接着,我们要在2个页面中访问这个store。让我们在src/pages
目录下编写2个页面:index和test1。
这是pages/index/index.vue
的代码内容:
<template>
<div class="container">
<div>计数结果:{{count}}</div>
<a href="/pages/test1/main" class="navlink">进入计数器页面</a>
</div>
</template>
<script>
import globalStore from "../../stores/global-store";
export default {
computed: {
count() {
return globalStore.state.count;
}
}
};
</script>
<style scoped>
.navlink {
text-decoration: underline;
}
</style>
这是pages/test1/index.vue
的代码内容:
<template>
<div class="container btns">
<button class="calbtn" @click="hanleDecrement">-</button>
<span class="calnum">{{count}}</span>
<button class="calbtn" @click="hanleIncrement">+</button>
</div>
</template>
<script>
import globalStore from "../../stores/global-store";
export default {
computed: {
count() {
return globalStore.state.count;
}
},
methods: {
hanleIncrement() {
globalStore.commit("increment");
},
hanleDecrement() {
globalStore.commit("decrement");
}
}
};
</script>
<style scoped>
.btns {
display: flex;
align-items: center;
}
.calnum {
color: red;
font-size: 32px;
}
</style>
这样,我们就有了2个使用了我们定义的global-store的页面,这些页面都会从store中获取count
状态值并显示;在test1页面中,还会调用increment
和decrement
两个mutations去更新count
值。
运行小程序,可以看到初始进入index页面时是这样的,页面上显示的计数结果是0:
然后点击“进入计数器页面”进到test1页面,并在这个页面上点击加减按钮操作一下,当中显示的count数会发生改变:
最后,点击左上角返回按钮返回index页面,你将发现这个页面上的计数结果也已经发生了改变,自动同步成前面操作后的结果了:
小结
通过这个例子,是不是感觉到使用Vuex做页面间的传值和数据同步特别简单?另外,你也可以在src/stores
目录下按需创建多个store模块,独立管理不同业务范围的数据,并按需导入页面组件使用。
Vuex是开发中一件非常得力的工具,希望你能尽快掌握它。更多的用法可以参考官方文档。