Vue 入门指南

学习 vue 过程中的笔记,未完更新中 …
完整【示例代码】请去我的 GitHub 仓库 pero-vue 查看

1. 环境配置

注意本笔记使用的版本为当时的最新稳定版

  • Vue 2.x
  • webpack 2
  • node 8.9.0
  • npm 5.6.0

1.1 使用到的技术文档

1.2 需要安装的相关依赖,未来不一定正确,以官方文档为准

首先需要安装 node, 然后使用命令 npm install 依赖名称 来安装

  • babel-core
  • babel-loader
  • babel-preset-env
  • babel-preset-stage-2 (使用 import() 时才需要)
  • css-loader
  • html-webpack-plugin
  • style-loader
  • vue
  • vue-loader
  • vue-template-compiler
  • webpack
  • webpack-dev-server
  • vue-router
  • axios
  • vuex(选用)

1.3 webpack 配置项简介

项目根目录下,创建 webpack.config.js 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const path = require('path'); //node 内置模块
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 用于在 html 页面里自动插入资源引用的标签
const webpack = require('webpack'); //使用 webpack 内置的插件时,需要引入 webpack

module.exports = {
entry: {
index: './src/index.js' // 入口文件
// bbb: './src/bbb.js' // 可以多个入口文件
},
output: {
path: path.resolve('./dist'), // 输出路径,必须是绝对路径.path.resolve是nodeJS模块方法,把相对路径转为绝对路径
// 或者使用语法__dirname +'/dist' 或 path.join(__dirname,'dist')
// __dirname 表示当前模块的目录的绝对路径(并非全局变量,等价于path.dirname(__filename))
// path.join用于处理连接路径时,统一不同系统路径符\和/问题。
// publicPath: '/assets/', // 发布路径,填写此项后,打包后文件路径不再是相对路径,而是基于服务器根目录的路径,
filename: 'js/[name].js', // [name] 表示块的名称,输出文件名,可以包含路径
chunkFilename: 'js/[name].js' //webpack 分割后的文件,[id] 表示块的编号。[name] 表示块的名称,没有名称时会自动使用编号
},
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js' // 默认是运行时构建,这里使用了template,必须用运行+编译时构建
}
},
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, //使用 babel 对 js 转译
{
test: /\.vue$/,
exclude: /node_modules/,
loader: 'babel-loader!vue-loader'
},
// 先使用 vue-loader对 vue 文件转译
{ test: /\.css$/, loader: 'style-loader!css-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 输出文件名,可以包含路径
template: 'src/index.html', // 模板文件位置
inject: 'body', //插入位置,也可以写 head
hash: true, // 在文件名后面加 hash 值,默认false
chunks: ['index'] // 表示插入的块,对应 webpack 入口文件中的 index,不写会插入所有的入口文件
}),
new webpack.HotModuleReplacementPlugin() //如果 devServer 的配置项里 hot:true ,则需要配置此项
],
externals: {
vue: 'Vue' //打包时排除 vue,vue内容不会被写入js。
//注意左边的 vue 是模块名,右边的 Vue 是不使用构建工具时的标准变量名,必须是Vue,与import的变量名无关
},
devServer: {
contentBase: './dist/', //表示静态资源(非webpack编译产生)文件的目录位置,
//这个目录的资源会被放到同样当成服务器根目录去
//遇到同名文件,webpack编译后产生的文件优先级更高
compress: true, //是否压缩
port: 9000, //端口号
host: '0.0.0.0', //默认是localhost,如果想被外部访问,这样设置
historyApiFallback: true, //当使用 history 模式路由时,设置为true,404页面会被重定向到主页,
hot: true // 热替换,可以在不刷新页面的情况下更新修改后数据,也可以配置在package.json 的 scripts 里,加 --hot参数
}
};

项目根目录下,创建 .babelrc 配置文件

babel-preset-env 相当于 es2015 ,es2016 ,es2017 及最新版本

1
2
3
{
"presets": ["env"]
}

1.4 运行

package.json 文件里添加配置

1
2
3
4
5
6
7
{
// ...其他参数
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server"
}
}

然后使用 npm run dev 来启动 server 使用 npm run build 来打包输出

这里的 build 是自己起的。写为 "build": "webpack -p", 打包后时压缩代码

1.5 webpack-dev-server 热替换

热替换指,在不刷新页面的状态下,把修改后的结果更新到页面上

有两种配置方式

  • webpack CLI 方式:

package.json 文件里添加配置

1
2
3
4
5
{
"scripts": {
"dev": "webpack-dev-server --hot"
}
}
  • webpack 配置方式:

webpack.config.js 文件里添加配置

1
2
3
4
5
6
7
8
9
10
11
12
const webpack = require('webpack');
module.exports = {
// 其他配置...
plugins: [
// 其他配置...
new webpack.HotModuleReplacementPlugin()
],
devServer: {
// 其他配置...
hot: true
}
};

package.json 文件里依然是 "dev": "webpack-dev-server"

2. vue 语法

2.1 基本用法

1
2
3
<div id="app">
{{ name }}
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue';

let param = {
el: '#app',
data: {
name: 'hello vue'
}
//注意这里的 data 也可以使用这种语法
//data() {
// return {name:'hello vue'}
//}
};
new Vue(param);

2.2 基本的组件

1
2
3
<div class="container">
<my-name></my-name>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue';
// 这是一个组件
let meName = {
template: '<div>{{name}}</div>', // 组件的模板,渲染后会替换掉 <my-name></my-name>
data() {
return {
name: 'xiawei'
}; // 组件中 data 必须是函数,数据 return 出去,不可以写为 data:{name:'xiawei'}
}
};

new Vue({
el: '.container',
components: {
'my-name': meName
}
});

2.3 vue 文件形式的组件

为了方便,可以使用 vue 文件 来封装组件,可以认为一个 vue 文件是一个组件,子组件继续使用其他 vue 文件 引入。

index.js

1
2
3
4
5
6
7
8
9
import Vue from 'vue';
import myname from './components/myname.vue';

new Vue({
el: '.container',
components: {
'my-name': myname
}
});

myname.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
{{name}}
</div>
</template>
<script>
export default {
data() {
return {
name: 'xiawei'
};
}
};
</script>
<style lang="css" scoped>
</style>

template 里,必须用一个 div 或者一个其他标签,包裹住所有的 html 标签

默认 lang=”css” 可以省略,需要使用 sass 时,可以写 lang=”scss” 等

scoped 是 vue 提供的属性 表示这里的样式只能在本组件内生效

2.4 组件通讯

2.4.1 使用 props 给组件传参

1
<myname value="xiawei"></myname>
1
2
3
4
5
6
7
8
9
10
<template>
<div>
{{value}}
</div>
</template>
<script>
export default {
props:['value']
};
</script>

2.4.2 访问其他组件,获取参数

可以通过 $parent 访问父组件,$children 访问子组件

user-login 有三个子组件,部分代码如下

1
2
3
4
5
6
7
8
9
10
<template>
<div id="user">
<h2>User Login</h2>
<form>
<user-name></user-name>
<user-pass></user-pass>
<user-submit></user-submit>
</form>
</div>
</template>

这时在 user-submit 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<button v-on:click="test">submit</button>
<!-- v-on:click="test" 表示点击事件时,触发 test 函数 -->
</div>
</template>
<script>
export default {
methods: {
test() {
this.$parent.$children[0] //访问 user-name 组件
this.$parent.$children[1] //访问 user-pass 组件

//获取 user-name 组件 data 中 username 的值
this.$parent.$children[0].username
}
}
};
</script>

要区分子组件是第几个,并不方便,可以使用 ref 来解决这个问题

相关代码修改为以下即可

1
2
<user-name ref="uname"></user-name>
<user-pass ref="upass"></user-pass>
1
2
//获取 user-name 组件 data 中 username 的值
this.$parent.$refs.uname.username;

2.4.3 父子组件自定义事件通讯

父组件 user-login.vue 里,给子组件 user-name 设置自定义事件 updateUserName

这个事件是绑定在 user-name 组件上的,在 组件对象 .$listeners里可以查看到,可以用 组件对象 .$emit 来触发

$emit 触发时,参数 1 是事件名,后几个参数可以传给事件对象(类似 jQuery 的trigger 方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<user-name ref="uname" v-on:updateUserName="setUserName"></user-name>
</template>
<script>
import username from './user/user-name.vue';

export default {
data() {
return {
username: ''
};
},
components: {
'user-name': username
},
methods: {
setUserName(uname) {
this.username = uname;
}
}
};
</script>

子组件 user-name.vue,当输入框内容改变,触发 change 事件

然后执行了 $emit 来触发 updateUserName事件,this.username 作为参数传给了updateUserName 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<input type="text" v-model="username" v-on:change="userNameChange">
</template>
<script>
export default {
data() {
return {
username: ''
};
},
methods: {
userNameChange() {
this.$emit('updateUserName', this.username);
}
}
};
</script>

2.5 v-if,路由原理

v-if 主要用于渲染模板,下面代码

当变量 isadmin 为 true 时,只显示 Admin Login

反之,只显示User Login

注意,程序依据 isadmin == true 的结果来判断

1
2
3
4
<template>
<h2 v-if="isadmin">Admin Login</h2>
<h2 v-else>User Login</h2>
</template>

在 index.js 添加下面代码

当浏览器路径 hash 部分(#号及其后面的部分)变化时,会触发 hashchange 事件

判断 hash 的值,各种值走自己的业务逻辑,就可以切换页面、改变数据,这就是路由原理

1
2
3
4
5
6
7
window.onhashchange = function() {
if (window.location.hash === '#admin') {
myvue.$children[0].$data.isadmin = true;
} else {
myvue.$children[0].$data.isadmin = false;
}
};

相关需要掌握的还有 v-for,参见官方文档

2.6 计算属性 computed

计算属性和 data 里的普通属性调用时相同的,但定义时不同

计算属性使用函数定义,return 的值,就是计算属性的值

当计算属性内的其他变量的值发生变化时,函数就会执行,运算得到新的值

所以计算属性的值是依赖其他变量的,它没有初始值,不可以在 data 里声明

下面的例子,通过计算属性比对输入的值来筛选 fav.class2

filter 数组方法 返回通过筛选条件的新数组,当 return true 时符合条件被选入。

indexOf 字符串方法 返回符合条件的字符串序号,如果找不到时,会返回数字 -1,可以用来匹配字符串类似的方法,还有 indexOf 数组方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<template>
<input type="text" v-model="inputText" class="form-control">
<table v-if="isShow()" class="table">
<thead>
<tr>
<th>
type 1
</th>
<th>
type 2
</th>
</tr>
</thead>
<tbody>
<tr v-for="fav in getFavs">
<td>{{ fav.class1 }}</td>
<td>{{ fav.class2 }}</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
data() {
return {
favs: [
{ class1: 'web', class2: 'js' },
{ class1: 'pro', class2: 'java' }
],
inputText: ''
};
},
methods: {
isShow() {
return !(this.inputText == '');
}
},
computed: {
getFavs() {
return this.favs.filter(abc => {
return abc.class2.indexOf(this.inputText) >= 0;
});
}
}
};
</script>

2.6.1 计算属性配合过滤方法

vue 2.x 的过滤器方法,与 vue 1.x 语法不同,并不适合和 v-for 配合使用,计算属性配合过滤方法来实现。

上节的例子,更复杂一点,数组的情况 ( 和上面重复的部分没写出来,完整代码请查看github)

getFavs 决定展示第几条数据,filterClass2 负责对展示出来的数据筛选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<tr v-for="fav in getFavs">
<td>{{ fav.class1 }}</td>
<td><a v-for="code in filterClass2(fav.class2)">{{ code }} </a></td>
</tr>
</template>
<script>
export default {
data() {
return {
favs: [
{ class1: 'web', class2: ['js', 'html', 'css', 'jssdk'] },
{ class1: 'pro', class2: ['java'] }
]
};
},
methods: {
filterClass2(class2) {
return class2.filter(v => {
return v.indexOf(this.inputText) >= 0;
});
}
},
computed: {
getFavs() {
return this.favs.filter(abc => {
return abc.class2.filter(code => {
return code.indexOf(this.inputText) >= 0;
}).length > 0;
});
}
}
};
</script>

2.7 路由

2.7.1 路由的基本使用

首先 npm 安装依赖官方的路由插件 vue-router

index.html

1
2
3
4
<div class="container">
<page-nav></page-nav>
<router-view></router-view>
</div>

index.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Vue from 'vue';
import VueRouter from 'vue-router'; // 引入插件
Vue.use(VueRouter); // 使用插件

import pagenav from './components/page-nav.vue';
import newslist from './components/news-list.vue';
import userlogin from './components/user-login.vue';

const routerConfig = new VueRouter({
routes: [
{ path: '/news', component: newslist },
{ path: '/login', component: userlogin }
]
});

// 全局注册公共组件(也可以像原先注册子组件的方式来做)
Vue.component('page-nav', pagenav);

let myvue = new Vue({
el: '.container',
router: routerConfig
// 路由中引入过子组件了,所以此处不需要再引入子组件
// components: {
// 'page-nav': pagenav,
// 'user-login': userlogin
// }
});

page-nav.vue 的部分代码

推荐使用 router-link 语法作为切换按钮,它默认会渲染成 a 标签也可以使用 a 标签来做

当某个 router-link 被点击选中时,vue 会给它的 html 标签添加上 class router-link-active

可以通过给 .router-link-active写 css 样式, 来给选中的 router-link 添加样式

1
2
3
4
5
6
<template>
<ul class="nav navbar-nav">
<li><router-link to="/login">login</router-link></li>
<li><a href="#/news">News</a></li>
</ul>
</template>

2.7.2 axios 的基本使用

引入

1
import axios from 'axios';

如需全局引入,可以再加上下面这句,组件内调用时使用 this.$axios 即可

1
Vue.prototype.$axios = axios;

get 请求

1
2
3
4
5
6
7
8
9
axios
.get('http://localhost:8000/test.php', {
params: {
ID: 12345
}
})
.then(response => {
alert(response.data);
});

post 请求参数 axios 默认转为 json 格式

1
2
3
4
5
axios
.post('http://localhost:8000/test.php', { name: 'xiawei', age: 20 })
.then(response => {
alert(response.data);
});

键值对方式(php 用 $_POST 可以取到值)

1
axios.post('http://localhost:8000/test.php', 'name=xiawei&age=20');

也可以使用 node 内置模块来转换格式

1
2
3
4
5
import querystring from 'querystring';
axios.post(
'http://localhost:8000/test.php',
querystring.stringifyname({ name: 'xiawei', age: 20 })
);

这部分的 php 代码,是放置在项目根目录的 test.php 文件

1
2
3
4
5
6
7
8
9
10
11
<?php
//指定允许其他域名访问
header('Access-Control-Allow-Origin:*');

//响应类型
header('Access-Control-Allow-Methods:GET,POST,PUT');
header('Access-Control-Allow-Headers:x-requested-with,content-type');

echo file_get_contents('php://input');//显示接收到的原始数据
var_export($_POST);
echo 'hello php';

Mac 内置了 php,直接启动 php 内置服务:到项目根目录下,Terminal 里执行下面命令即可

windows 下载 php 后,把 php 目录添加到系统环境变量 PATH 里后,同样执行下面命令

1
php -S 0.0.0.0:8000

2.7.3 动态加载新闻详细页

在新闻列表页,点击标题跳转到新闻详细页,动态加载新闻内容

index.js 部分代码

1
2
3
4
5
6
7
8
9
10
11
import axios from 'axios';
Vue.prototype.$axios = axios;

const routerConfig = new VueRouter({
routes: [
{ path: '/', component: newslist },// 设置首页
{ path: '/news', component: newslist, name: 'newslist' },// 可以给路由设置别名 name
{ path: '/news/:newsid', component: newsdetail, name: 'newsdetail' },// 如果需要参数,使用冒号的来做占位符
{ path: '/login', component: userlogin, name: 'userlogin' }
]
});

new-list.vue 部分代码

1
2
3
4
5
6
7
<template>
<div class="page-header" v-for="news in newslist">
<h2><router-link :to="{ name: 'newsdetail', params: {newsid: news.newsid} }">{{news.title}}</router-link> <small>{{news.pubtime}}</small></h2>
<!-- 这里的 newsdetail 以及 params 里左边的 newsid 是和路由定义时的相关参数对应 -->
<p>{{news.desc}}</p>
</div>
</template>

news-detail.vue 部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<h2> {{ newstTitle }} <small>{{ newsDate }}</small></h2>
<p>{{ newsContent }}</p>
</div>
</template>
<style>

</style>
<script>
export default {
// 生命周期,组件被创建时执行
created() {
this.$axios
.get('http://localhost:8000/news.php?newsid='+ this.$route.params.newsid)
// 可以通过全局变量 $route.params 来访问路由里的变量获取到新闻编号101
.then(response => {
this.newstTitle = response.data.title;
this.newsDate = response.data.pubtime;
this.newsContent = response.data.desc;
});
}
};
</script>

通过全局变量 $route 来访问路由里的各种数据

例如 $route.params.newsid 可以获得路由占位符 :newsid 处的新闻编号值 101

2.8 异步加载和 webpack 代码分割

当项目比较大的时候,可以使用异步加载组件的方式来按需加载,而不是一次性加载全部组件。

还可以配合 webpack 代码分割功能,把打包后的 js,分割成多个 js 文件,做到按需引用。

之前的引入组件的方式是

1
import userlogin from './components/user-login.vue';

使用 vue 异步加载的方式引入

1
2
3
var userlogin = function(resolve) {
resolve(require('./components/user-login.vue'));
};

使用 ES2015 语法,并且简化参数名,可以写为

1
2
3
const userlogin = r => {
r(require('./components/user-login.vue'));
};

结合 webpack 代码分割功能后

1
2
3
4
5
const userlogin = r => {
require.ensure([], () => {
r(require('./components/user-login.vue'));
});
};

如果需要把某几个组件打包为一组,给它们的 require.ensure() (文档1文档2)添加最后一个参数(例如'aaa'),且值相同

1
2
3
require.ensure([], () => {
r(require('./components/user-login.vue'));
},'aaa');

也可以使用 webpack + ES2015 语法来进行代码分割

import() (文档) 是 ES2015 草案的语法,所以使用时需要 babel 转译

babel 配置里需要添加草案语法的转译 presets stage-2 ,npm 安装依赖 babel-preset-stage-2

.babel 文件,注意配置的数组里,presets 解析的顺序是从右到左的,先执行 stage-2

1
2
3
{
"presets": ["env", "stage-2"]
}
1
2
const userlogin = () => import('./components/user-login.vue');
// 也就是 function() { return import('./components/user-login.vue')};

把某几个文件打包为一组时,使用这个语法

1
const userlogin = () => import(/* webpackChunkName: "aaa" */'./components/user-login.vue');

最后分割后的文件名,可以在 webpack 配置里 output 配置项里添加 chunkFilename 配置项来控制

1
2
3
4
5
output: {
filename: 'js/[name].js', // [name] 表示块的名称,输出文件名,可以包含路径
chunkFilename: 'js/[name].js'
//webpack 分割后的文件,[id] 表示块的编号。[name] 表示块的名称,没有名称时会自动使用编号
},

2.9 开发插件

有时现有的插件并不能满足自己的业务需求,这时需要自己开发插件

2.9.1 自定义指令

在 src 文件夹下新建一个 js 文件,比如命名为 plugin.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default {
install(Vue) {
// 添加实例方法
Vue.prototype.$name = 'xiawei';// 可以在组件内使用 this.$name 取到值 'xiawei'

// 这里添加时方法来检测用户名是否合法,6~20位合法,否则显示提示
Vue.prototype.checkUserName = value => {
if (value == '') return true;
return /\w{6,20}/.test(value);
};
// 可以在组件内使用 this.checkUserName(’‘’)

// 添加全局自定义指令 v-uname
Vue.directive('uname', {
bind() {
console.log('begin');
},
update(el, binding, vnode) {
vnode.context[binding.expression] = !/\w{6,20}/.test(el.value);
}
});
}
};

directive (文档) 里的生命周期里的三个参数:

  • el 参数表示指令所绑定的元素,可以用来直接操作 dom
  • binding 参数表示绑定对象,binding.expression 取到传入的表达式,binding.value 可以取到表达式的值
    这里的表达式也可以是函数名,取到的值是函数体,binding.oldValue
  • vnode 参数表示 Vue 编译生成的虚拟节点

关于官方文档里,添加全局方法或属性 Vue.myGlobalMethod 和添加实例方法和属性 Vue.prototype.$myMethod 二者区别

全局方法或属性使用 Vue.名称 来调用,而实例方法和属性使用 (实例化后的 Vue 对象).名称 来调用,也就是组件内的常见 this.名称 来调用,即使看起来名称一样的Vue.aaaVue.prototype.aaa也是两个不同的变量

具体可以参见这篇文章:js里面的实例方法和静态方法

index.js 内加载插件

1
2
import plugin from './plugin.js';
Vue.use(plugin);

user-name.vue 添加 v-uname 和 label 元素

1
2
<input type="text" v-model="username" v-uname="showErrorLabel" v-on:change="userNameChange" class="form-control" :placeholder="placeholder">
<label v-if="showErrorLabel" class="label label-danger">Please check your username and try again</label>

2.9.2 手动挂载子组件

上面只是控制变量,并不是很方便,可以通过插件动态插入移除提示框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export default {
install(Vue) {
// 创建变量,定义初始值
Vue.errorLabel = null;
Vue.hasErrorLabel = false;
// 这个全局变量来标记是否插入了 label,给初始值时必须放在 update 外面

// 添加全局自定义指令 v-uname
Vue.directive('uname', {
bind(el) {
let error = Vue.extend({
template:
'<label class="label label-danger">Please check your username and try again</label>'
});

Vue.errorLabel = (new error()).$mount().$el;
// $mount() 方法不填参数时,表示把 vm 实例对象变成一个可以挂载的状态,这时就可以访问到 $el 获取到元素了
},
update(el, binding, vnode) {
// 这里每次 update 是从组建原始的状态 update 的,所以不会重复插入多个
if (/\w{6,20}/.test(el.value)) {
if (Vue.hasErrorLabel) {
el.parentNode.removeChild(Vue.errorLabel);
Vue.hasErrorLabel = !Vue.hasErrorLabel;
}
} else {
if (!Vue.hasErrorLabel) {
el.parentNode.appendChild(Vue.errorLabel);
Vue.hasErrorLabel = !Vue.hasErrorLabel;
}
}
}
});
}
};

user-name.vue 组件里,这时不需要写 label 元素,只需要写入 v-uname 即可

1
<input type="text" v-model="username" v-uname v-on:change="userNameChange" class="form-control" :placeholder="placeholder">

2.9.3 插件里包含子组件

上一小节的代码,当有多个 input 元素时,就会出现其他元素显示不正常的情况,原因是多个标签共用了同一个 Vue.hasErrorLabel

所以当插件不仅仅处理数据时,还需要独立的处理 dom 元素时,使用子组件的方式更加合理,它们是互相独立的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {
install(Vue) {
Vue.component('p-username', {
template: `<div>
<input class="form-control" type="text" v-model="textValue" />
<label class="label label-danger" v-if="showErrorLabel">Please check your username and try again</label>
</div>`,
// 这里使用了 ES2015 的模板字符串语法
data() {
return {
textValue: ''
};
},
computed: {
showErrorLabel() {
return !(/\w{6,20}/.test(this.textValue) || this.textValue == '');
}
}
});
}
};

其中,为了方便 template 里使用了 ES2015 的模板字符串语法(参考文档

user-name.vue 文件(不需要写 input 元素)

1
<p-username></p-username>

2.10 全局状态管理 vuex

应遵循以下规则

  • 应用级的状态集中放在 store 中
  • 计算属性使用 getters
  • 改变状态的方式是提交 mutations,这是个同步的事务
  • 异步逻辑应该封装在 action 中

也即是与组件的概念相对应的
store -> data
getters -> computed
mutations/actions -> methods

2.10.1 vuex 基本使用

npm 安装依赖 vuex

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vuex from 'vuex';
Vue.use(Vuex);

const vuex_store = new Vuex.Store({
state: {
user_name: ''
},
mutations: {
showUserName(state) {
alert(state.user_name);
}
}
});

赋值:user-name.vue 组件中使用

1
<div class="page-header" v-for="news in $store.state.newslist">
1
this.$store.state.user_name = this.username;

触发:user-submit.vue 组件中使用

1
this.$store.commit('showUserName');

即可完成简单的输入用户名,点提交按钮后 alert 出用户名

2.10.2 vuex 计算属性

vuex 里的计算属性使用的是 getters,用法和 组件里的计算属性 computed 类似,只是被触发的时机不同

从数据里展示没有删除的新闻展示

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const vuex_store = new Vuex.Store({
state: {
user_name: '',
newslist: []
},
mutations: {
showUserName(state) {
alert(state.user_name);
}
},
getters: {
getNews(state) {
return state.newslist.filter(news => !news.isdeleted);
}
}
});

news-list.vue

1
<div class="page-header" v-for="news in $store.getters.getNews">
1
2
3
4
5
6
7
8
9
export default {
created() {
if (this.$store.state.newslist.length == 0) {
this.$axios.get('http://localhost:8000/news.php').then(response => {
this.$store.state.newslist = response.data;
});
}
}
};

2.10.3 actions

mutations 是同步执行的,里面不能放异步执行的东西
actions 里放异步执行的,异步执行完后,去手动触发 mutations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context,param) {
// 异步业务 param -> param2
context.commit('increment',param2);
}
}
})

组件内触发

1
this.$store.dispatch('increment',param);

2.10.4 把业务按模块分类

之前写的 index.js 是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const vuex_store = new Vuex.Store({
state: {
user_name: '',
newslist: []
},
mutations: {
showUserName(state) {
alert(state.user_name);
}
},
getters: {
getNews(state) {
return state.newslist.filter(news => !news.isdeleted);
}
}
});

按模块分离后

index.js

1
2
3
4
5
6
7
8
9
import news_module from './store/news.js';
import users_module from './store/users.js';

const vuex_store = new Vuex.Store({
modules: {
news: news_module,
users: users_module
}
});

news.js

1
2
3
4
5
6
7
8
9
10
export default {
state: {
newslist: []
},
getters: {
getNews(state) {
return state.newslist.filter(news => !news.isdeleted);
}
}
}

users.js

1
2
3
4
5
6
7
8
9
10
export default {
state: {
user_name: ''
},
mutations: {
showUserName(state) {
alert(state.user_name);
}
}
}

分离后,注意相关模块里的 this.$store.state

按业务模块名分别改为 this.$store.state.newsthis.$store.state.users

注意不同业务模块里,getters 里函数重名了会报错, mutations 里函数重名了会两边都执行

图片来源:https://www.pixiv.net/member_illust.php?mode=medium&illust_id=63737968
推荐课程:VUE.JS+PHP 前后端分离实战视频电商网站

夏味 wechat
欢迎添加我个人微信,互相学习交流