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' ); const HtmlWebpackPlugin = require ('html-webpack-plugin' ); const webpack = require ('webpack' ); module .exports = { entry : { index : './src/index.js' }, output : { path : path.resolve('./dist' ), filename : 'js/[name].js' , chunkFilename : 'js/[name].js' }, resolve : { alias : { vue$ : 'vue/dist/vue.esm.js' } }, module : { rules : [ { test : /\.js$/ , exclude: /node_modules/ , loader: 'babel-loader' }, { test : /\.vue$/ , exclude: /node_modules/ , loader: 'babel-loader!vue-loader' }, { test : /\.css$/ , loader: 'style-loader!css-loader' } ] }, plugins : [ new HtmlWebpackPlugin({ filename : 'index.html' , template : 'src/index.html' , inject : 'body' , hash : true , chunks : ['index' ] }), new webpack.HotModuleReplacementPlugin() ], externals : { vue : 'Vue' }, devServer : { contentBase : './dist/' , compress : true , port : 9000 , host : '0.0.0.0' , historyApiFallback : true , hot : true } };
项目根目录下,创建 .babelrc
配置文件
babel-preset-env
相当于 es2015 ,es2016 ,es2017 及最新版本
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 热替换 热替换指,在不刷新页面的状态下,把修改后的结果更新到页面上
有两种配置方式
package.json 文件里添加配置
1 2 3 4 5 { "scripts" : { "dev" : "webpack-dev-server --hot" } }
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' } }; 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>' , data ( ) { return { 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 > </div > </template > <script > export default { methods : { test ( ) { this .$parent.$children[0 ] this .$parent.$children[1 ] this .$parent.$children[0 ].username } } }; </script >
要区分子组件是第几个,并不方便,可以使用 ref
来解决这个问题
相关代码修改为以下即可
1 2 <user-name ref ="uname" > </user-name > <user-pass ref ="upass" > </user-pass >
1 2 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 });
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 里后,同样执行下面命令
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 > <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) .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' );
把某几个文件打包为一组时,使用这个语法
1 const userlogin = () => import ('./components/user-login.vue' );
最后分割后的文件名,可以在 webpack 配置里 output 配置项里添加 chunkFilename
配置项来控制
1 2 3 4 5 output: { filename : 'js/[name].js' , chunkFilename : 'js/[name].js' },
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' ; Vue.prototype.checkUserName = value => { if (value == '' ) return true ; return /\w{6,20}/ .test(value); }; 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.aaa
和Vue.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 ; 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; }, update (el, binding, vnode ) { 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>` , 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) { 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.news
、this.$store.state.users
注意不同业务模块里,getters 里函数重名了会报错, mutations 里函数重名了会两边都执行
图片来源:https://www.pixiv.net/member_illust.php?mode=medium&illust_id=63737968 推荐课程:VUE.JS+PHP 前后端分离实战视频电商网站