Vue2.5学习笔记
起步
- 安装cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
- 安装vue-cli3
* npm install -g @vue/cli
* npm install -g @vue/cli-service-global
- 新建项目:
* vue create vue-buy
- 选择配置
* Babel 代码转义
* Linter 代码风格
* 测试
- 运行:
* npm run serve
- 项目结构
- VS-Code常用快捷键:
1)快速复制一行:shift+alt+↓
2)格式化代码:shift+alt+F
3)快速生产vue单文件框架代码:vbase
4)快速导入:vimport
5)快速移动:alt+↓
- npm 淘宝镜像设置
* npm install -g cnpm --registry=https://registry.npm.taobao.org
* npm config set registry https://registry.npm.taobao.org
* # 配置后可通过下面方式来验证是否成功
* npm config get registry
声明式渲染
纯浏览器端体验
Test file:01-hello.html
<div id="app">{{ message }}</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello, Vue!'
}
});
</script>
单文件组件
.vue是vue单文件组件,一个文件就是一个组件,由template、script和style三个标签构成,分别是html、js和css的内容部分
Test file:02-single.vue
<template>
<div id="app">
{{message}}
</div>
</template>
<script>
export default {
data(){
return {
message: 'single vue'
}
}
}
</script>
<style scoped>
#app{
color: red;
}
</style>
启动运行:cd到02-single.vue文件所在目录运行命令:
vue serve 02-single.vue 即可运行该文件,并且支持热部署哟!
快速创建项目
- 创建项目的命令:vue create vue-cart
- cd vue-cart
- 启动命令:npm run serve
vue.config.js
项目根目录可以新建 vue.config.js 来进行扩展配置:
module.exports = {
baseUrl: process.env.NODE_ENV === 'production'
? '//your_url'
: '/',
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
// When building in multi-pages mode, the webpack config will contain different plugins
// (there will be multiple instances of html-webpack-plugin and preload-webpack-plugin).
// Make sure to run vue inspect if you are trying to modify the options for those plugins.
pages: {
index: {
// entry for the pages
entry: 'src/pages/index/index.js',
// the source template
template: 'src/pages/index/index.html',
// output as dist/index.html
filename: 'index.html',
// when using title option,
// template title tag needs to be <title><%= htmlWebpackPlugin.options.title %></title>
title: '首页',
// chunks to include on this pages, by default includes
// extracted common chunks and vendor chunks.
chunks: ['chunk-vendors', 'chunk-common', 'index']
}
// when using the entry-only string format,
// template is inferred to be `public/subpage.html`
// and falls back to `public/index.html` if not found.
// Output filename is inferred to be `subpage.html`.
// subpage: ''
},
// eslint-loader 是否在保存的时候检查
lintOnSave: true,
// 是否使用包含运行时编译器的Vue核心的构建
runtimeCompiler: false,
// 默认情况下 babel-loader 忽略其中的所有文件 node_modules
transpileDependencies: [],
// 生产环境 sourceMap
productionSourceMap: false,
// cors 相关 https://jakearchibald.com/2017/es-modules-in-browsers/#always-cors
// corsUseCredentials: false,
// webpack 配置,键值对象时会合并配置,为方法时会改写配置
// https://cli.vuejs.org/guide/webpack.html#simple-configuration
configureWebpack: (config) => {
},
// webpack 链接 API,用于生成和修改 webapck 配置
// https://github.com/mozilla-neutrino/webpack-chain
chainWebpack: (config) => {
// 因为是多页面,所以取消 chunks,每个页面只对应一个单独的 JS / CSS
config.optimization
.splitChunks({
cacheGroups: {}
});
// 'src/lib' 目录下为外部库文件,不参与 eslint 检测
config.module
.rule('eslint')
.exclude
.add('/Users/maybexia/Downloads/FE/community_built-in/src/lib')
.end()
},
// 配置高于chainWebpack中关于 css loader 的配置
css: {
// 是否开启支持 foo.module.css 样式
modules: false,
// 是否使用 css 分离插件 ExtractTextPlugin,采用独立样式文件载入,不采用 <style> 方式内联至 html 文件中
extract: true,
// 是否构建样式地图,false 将提高构建速度
sourceMap: false,
// css预设器配置项
loaderOptions: {
css: {
// options here will be passed to css-loader
},
postcss: {
// options here will be passed to postcss-loader
}
}
},
// All options for webpack-dev-server are supported
// https://webpack.js.org/configuration/dev-server/
devServer: {
open: true,
host: '127.0.0.1',
port: 3000,
https: false,
hotOnly: false,
proxy: null,
before: app => {
}
},
// 构建时开启多进程处理 babel 编译
parallel: require('os').cpus().length > 1,
// https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
pwa: {},
// 第三方插件配置
pluginOptions: {}
};
条件与循环
<!-- 条件语句 -->
<h1 v-if="showMsg">{{ msg }}</h1>
<!-- 循环语句 -->
<ul>
<li v-for="good in goods" :key="good.id">
<span>{{good.text}}</span>
<span>¥{{good.price}}</span>
</li>
</ul>
<!-- 这里用 v-for 循环时推荐绑定key,原因是在对数组中间进行插入操作时,可以提升渲染性能 -->
事件处理
<!-- 用户输入,事件处理 -->
<p>
text:<input type="text" v-model="inputText">
price:<input type="text" v-model="inputPrice">
<button @click="addGood">加购物车</button>
</p>
<script>
methods: {
addGood(){
this.goods.push({
text: this.inputText,
price: this.inputPrice
});
this.inputText = '';
this.inputPrice = '';
}
}
</script>
组件化
新建Cart.vue购物车组件
组件引入与注册到components
import Cart from './components/Cart.vue'
export default {
name: 'app',
components: {
Cart
}
}
父子组件通过props属性传值
<cart :name="name" :cart="cart"></cart>
props: ['name', 'cart']
// 属性校验
props: {
name: {type: String, required: true},
cart: {type: Array}
}
样式和class绑定
<tr v-for="(c, index) in cart" :key="c.id" :class="{active: c.active}">
<style scoped>
.active{
color: green;
font-weight: bold;
}
</style>
计算属性
需要额外加工data中数据的时候使用
computed: {
activeCount(){
return this.cart.filter(v=>v.active).length;
},
count(){
return this.cart.length;
},
total(){
let num = 0;
this.cart.forEach(c => {
if(c.active){
num += c.price * c.count;
}
});
return num;
}
}
事件总线
// 修改Vue原型
Vue.prototype.$bus = new Vue();
// App.vue中发送事件
this.$bus.$emit('addCart', good);
// Cart.vue中监听事件
created(){
this.$bus.$on('addCart', good => {
const ret = this.cart.find(v => v.id === good.id);
if(ret){// 购物车里已有该商品
ret.count += 1;
}else{
this.cart.push({
...good, // 属性展开运算符
count: 1,
active: true
});
}
});
}
Cart组件
<template>
<div>
<p>{{name}}</p>
<table border="1" >
<tr>
<th>#</th><th>课程名</th><th>单价</th><th>数量</th><th>价格</th>
</tr>
<tr v-for="(c, index) in cart" :key="c.id" :class="{active: c.active}">
<td>
<input type="checkbox" v-model="c.active">
</td>
<td>{{c.text}}</td><td>{{c.price}}</td>
<td>
<button @click="desc(c, c.count, index)">-</button>
{{c.count}}
<button @click="incr(c)">+</button>
</td>
<td>¥{{c.price*c.count}}</td>
</tr>
<tr>
<td></td>
<td colspan="3">{{activeCount}}/{{count}}</td>
<td>¥{{total}}</td>
</tr>
</table>
</div>
</template>
<script>
export default {
props: ['name'],
data(){
return{
cart: []
}
},
created(){
this.$bus.$on('addCart', good => {
const ret = this.cart.find(v => v.id === good.id);
if(ret){// 购物车里已有该商品
ret.count += 1;
}else{
this.cart.push({
...good, // 属性展开运算符
count: 1,
active: true
});
}
});
},
// 计算属性:需要额外加工data中数据的时候使用
computed: {
activeCount(){
return this.cart.filter(v=>v.active).length;
},
count(){
return this.cart.length;
},
total(){
let num = 0;
this.cart.forEach(c => {
if(c.active){
num += c.price * c.count;
}
});
return num;
}
},
methods: {
desc(c, count, index){
if(count==1){
this.remove(index);
}else{
c.count--;
}
},
incr(c){
c.count++;
},
remove(i){
if(window.confirm('确认删除?')){
this.cart.splice(i, 1);
}
}
}
}
</script>
mock数据
简单的mock,使用自带的webpack-dev-server即可,新建vue.config.js扩展webpack设置
在项目根目录新建vue.config.js文件扩展webpack配置
module.exports = {
configureWebpack: {
devServer: {
before(app){ // 前置中间件
app.get('/api/goods', function(req, res){
res.json({
code: 0,
list: [
{id: 1, text: 'Web全栈架构师', price: 999},
{id: 2, text: 'Java架构师', price: 1314},
{id: 3, text: 'Python架构师', price: 689}
]
});
});
}
}
}
}
重启项目,访问 http://localhost:8080/api/goods 返回
{“code”:0,“list”:[{“id”:1,“text”:“Web全栈架构师”,“price”:999},{“id”:2,“text”:“Java架构师”,“price”:1314},{“id”:3,“text”:“Python架构师”,“price”:689}]}
安装axios
npm i axios -S
// App.vue
created(){
axios.get('/api/goods').then(res => {
this.goods = res.data.list;
})
}
// 使用ES7的 async + await 语法
async created(){
// 查询产品列表
try {
const res = await axios.get('/api/goods');
this.goods = res.data.list;
} catch (error) {
// 错误处理
}
}
注意:要使用 await,后面的函数必须是返回Promise对象
数据持久化
简单的localStorage + Vue监听实现购物车数据持久化
created(){
this.cart = JSON.parse(window.localStorage.getItem('cart')) || [];
},
watch: {
cart: {
handler(){
this.saveLocal();
},
deep: true
}
},
methods(){
saveLocal(){
window.localStorage.setItem('cart', JSON.stringify(this.cart));
}
}
生命周期
组件
组件分类
1、通用组件
基础组件,大部分UI库都是这种组件,比如:表单、弹窗、布局等
2、业务组件
业务需求强挂钩,会被复用,比如:抽奖、摇一摇等
3、页面组件
每个页面都是组件,不会复用,完成功能
体验ElementUI组件
以插件的方式引入 element ui组件:vue add element
FormTest.vue
<template>
<div>
<h3>element表单</h3>
<!-- model数据模型 -->
<el-form :model="ruleForm" :rules="rules" ref="loginForm">
<!-- prop用于校验 -->
<el-form-item label="用户名" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="pwd">
<el-input v-model="ruleForm.pwd"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data(){
return{
ruleForm: {
name: '',
pwd: ''
},
rules: {
name: [
{required: true, message: '请输入用户名'},
{min: 6, max: 10, message: '请输入6~10位用户名'}
],
pwd: [
{required: true, message: '请输入密码'}
]
}
}
},
methods: {
submitForm(){
this.$refs.loginForm.validate(valid => {
if(valid){
alert('提交登录')
}else{
console.log('校验失败');
return false;
}
});
}
}
}
</script>
Vue组件开发
1)注册
2)使用
3)给传递什么值 props
4)组件通知外部事件
5)扩展组件 插槽
组件注册
- 全局注册,一次注册,全局皆可使用,例如:
// 注册
Vue.component('form-test', { ... })
// 使用
<div>
<form-test></form-test>
<div>
- 局部注册(推荐,依赖可追溯)
new Vue({
el: '#app',
components: {
'form-test': FormTest
}
})
props
- 使用v-bind:xx(简写:xx)来传递数据,组件内部props字段接收,使用和挂载在this上的数据没有本质区别。
- props属于单向数据流,也就是只能通过父级修改,组件自己不能修改props的值,只能修改定义在data里的数据,非要修改,需要通过事件通知父级,由父级来修改。
事件
组件内部通知外部的变化 this.$emit
<template>
<button @click="handleClick">
开课吧
</button>
</template>
<script>
export default{
methods: {
handleClick(event){
this.$emit('bind-click', event)
}
}
}
</script>
v-model是一个特殊的属性,相当于绑定了:value和@input两个
例如
<custom-input v-model="searchText"></custom-input>
<!-- 等价于 -->
<custom-input :value="searchText" @input="searchText=$event"></custom-input>
- 自定义数据双向绑定的组件 CustomInput.vue
<template>
<div>
<input type="text" :value="inputValue" @input="inputHandler">
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
defaultValue: ''
}
},
data() {
return{// 单向数据流原则:组件内不能修改props属性
inputValue: this.value
}
},
methods: {
inputHandler(e) {
this.inputValue = e.target.value;
// 通知父组件值更新
this.$emit('input', this.inputValue);
}
}
}
</script>
插槽slot
插槽用来扩展组件,例如
<!-- MyComp.vue -->
<div @click="handleClick">
<slot> ... </slot>
</div>
<!-- 使用 -->
<my-comp>
<strong>lalalala</strong> // 会替换掉<slot>标签内容
</my-comp>
实际渲染DOM
<div @click="handleClick">
<strong>lalalala</strong>
</div>
- 测试
<!-- Win.vue -->
<template>
<div class="win">
<div class="head">
<slot name="head"></slot><!-- 具名插槽 -->
</div>
<slot></slot><!-- 匿名插槽 -->
<div class="foot">
<slot name="foot"></slot>
</div>
</div>
</template>
<!-- App.vue 中测试插槽 -->
<win>
<template slot="head">
<h3>window</h3>
</template>
content...
<template slot="foot">
<button>确定</button>
</template>
</win>
provide & inject
业务中不常用到的api,主要用于高级组件和组件库使用,用来和所有子孙元素提供数据,类似 react 中的上下文。假设有 A.vue 和 B.vue,B 是 A 的子组件
// A.vue
export default{
provide() {
return {
someValue: 'hello'
}
}
}
// B.vue
export default{
inject: ['someValue'],
mounted(){
console.log(this.someValue); // hello
}
}
provide 和 inject 绑定的并不是响应式的,这是刻意为之的。然而,如果你传入一个可监听的对象,那么其对象的属性还是可响应的。
组件设计开发
- 需求:希望如下使用组件
Form组件
- 分析:想设计好一个Vue组件,最重要的是设计好它的接口,一个Vue.js组件的接口来自三个部分:props(外部传递的数据)、events(内部通知外部的事件)、slots(扩展插槽)
Form组件主要负责设计model和rules两个props,并且在子组件共享provide,没啥事件,子元素直接扩展slot即可,如下:
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
provide() {
return {// 将表单实例传递给后代
form: this
}
},
props: {
model: {
type: Object,
required: true
},
rules: {
type: Object
}
},
created(){
// 缓存需要校验的表单项
this.fields = [];
this.$on("formItemAdd", item => this.fields.push(item));
},
methods: {
async validate(callback) {
// 将FormItem数组转换为validate()返回的promise数组
const tasks = this.fields.map(item => item.validate());
console.log(tasks);
// 获取所有结果统一处理
const results = await Promise.all(tasks);
let ret = true;
results.forEach(valid => {
if (!valid) {
ret = false; // 只要一个失败就失败
}
});
callback(ret);
}
}
}
</script>
FormItem
- 分析:主要设置两个props、label和prop
Form的核心是做数据校验,一个Form里包含多个FormItem,当点击按钮的时候,需要逐一对每个输入框进行校验,所以每个FormItem都需要一个验证方法,汇总到Form返回
- 缓存FormItem实例
因为要在Form中逐一调用FormItem的验证方法,而Form和FormItem是独立的,需要预先将FormItem的每个实例缓存在Form中
mounted(){
// 挂载到form上时,派发一个添加事件
if(this.prop){
this.$parent.$emit('formItemAdd', this);
}
}
Vue.js的组件渲染顺序是由内而外的,所以FormItem要先于Form渲染,在FormItem的mounted触发时,我们向Form派发了事件formItemAdd,并把(this)传递给了Form,而此时,Form和mounted尚未触发。因为Form在最外层,如果在Form的mounted里监听事件,是不可以的,所以要在其created内监听自定义事件,Form的created要先于FormItem的mounted
<template>
<div>
<label v-if="label">{{label}}</label>
<div>
<slot></slot>
<!-- 校验错误信息 -->
<p v-if="validateStatus=='error'" class="error">{{errorMessage}}</p>
</div>
</div>
</template>
<script>
import schema from 'async-validator'
export default {
inject: ['form'], // 注入form,获取model和rules
props: ['label', 'prop'],
data() {
return {
validateStatus: '',
errorMessage: ''
}
},
created(){
this.$on('validate', this.validate);
},
mounted(){
// 挂载到form上时,派发一个添加事件
if(this.prop){
this.$parent.$emit('formItemAdd', this);
}
},
methods: {
validate(){
return new Promise(resolve => {
// 校验当前项:依赖async-validate
const descriptor = {
// 校验规则
[this.prop]: this.form.rules[this.prop]
};
const validator = new schema(descriptor);
// 使用es6计算属性动态设置key
validator.validate({ [this.prop]: this.form.model[this.prop] }, errors => {
if(errors) {
// 校验失败
this.validateStatus = 'error';
this.errorMessage = errors[0].message;
resolve(false);//校验失败
}else{
this.validateStatus = '';
this.errorMessage = '';
resolve(true);
}
});
});
}
}
}
</script>
<style scoped>
.error {
color: #f00;
}
</style>
Input
<template>
<div>
<!-- {{someValue}} -->
<input :type="type" :value="inputValue"
@input="inputHandler"
@blur="blurHandler">
</div>
</template>
<script>
export default {
inject: ['someValue'],
props: {
value: {
type: String,
default: ""
},
type:{
type:String,
default:'text'
}
},
data() {
return {
// 单向数据流原则:组件内不能修改props属性
inputValue: this.value
};
},
methods: {
inputHandler(e) {
this.inputValue = e.target.value;
// 通知父组件值更新
this.$emit("input", this.inputValue);
// 通知FormItem做校验
this.$parent.$emit('validate', this.inputValue);
},
blurHandler(){
this.$bus.$emit('input-blur', this.inputValue)
}
}
};
</script>
思考
- 支持多个校验,比如minLength,业界最流行使用async-validator,element,iview也不例外
- 可以自己扩展dispath指定事件向父元素传播,就需要input再判断name了,参考代码
$dispatch(componentName, eventName, params){
//向父元素广播事件
let parent = this.$parent || this.$root;
let name = parent.$options.name;
// 向上循环查找
while(parent && (!name || name !== componentName)){
parent = parent.$parent;
if(parent){
name = parent.$options.name;
}
}
if(parent){
parent.$emit.apply(parent, [eventName].concat(params));
}
}
- 组件使用大同小异,大家使用组件库的时候也可以尝试思考,如何实现?
Vue-router && Vuex实战
1、目标
- vue-router基础配置
- 路由传参
- 子路由
- 路由重定向
- 路由守卫
- Vuex数据流
- store
- state
- mutation
- getter
- action
Vue-router
学习官网:https://router.vuejs.org/zh/guide
起步
Vue Router是Vue.js官方的路由管理器,它和Vue.js的核心深度集成,它让构建单页应用变得易如反掌。包含功能有:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数
- 基于Vue.js过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的CSS class 的链接
- HTML5历史模式或hash模式
- 安装:npm install vue-router --save
但是推荐使用插件的方式安装:vue add router - 安装模式推荐是使用History
用户阅读更好,对搜索引擎也好,Restfull风格 - 安装完成后,工具已经为我们在项目中引入了Vue-router相关配置,就可以直接使用了
import Vue from 'vue'
import App from './App.vue'
import './plugins/element.js'
import router from './router'
Vue.config.productionTip = false
// 修改Vue原型
Vue.prototype.$bus = new Vue();
new Vue({
router,
render: h => h(App)
}).$mount('#app')
// router.js
import Vue from 'vue'
import Router from 'vue-router'
// 插件挂载
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL, // 基础路径,默认是“/” ==> 在微服务中会要求有一级服务名(上下文)
routes: [
...
]
})
路由配置体验
// router.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import FirstPage from './views/FirstPage'
import SecondPage from './views/SecondPage'
// 插件挂载
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL, // 基础路径,默认是“/” ==> 在微服务中会要求有一级服务名(上下文)
routes: [
{path: '/first', name: 'first-page', component: FirstPage},
{path: '/second', name: 'second-page', component: SecondPage}
]
})
<div id="app">
<!-- 路由导航 -->
<router-link to="/first">First</router-link> |
<router-link to="/second">Second</router-link> |
<!-- 路由出口 -->
<router-view></router-view>
</div>
- 点击路由导航就可以切换路由了
路由传参
- 通过路径传参
// routes配置:
{path: '/first/:foo', name: 'first-page', component: FirstPage},
// 导航配置
<router-link to="/first/bar">First</router-link> |
// FirstPage.vue中参数获取
<p>{{ $route.params }}</p>
// 地址:http://localhost:8080/first/bar
- 给组件传静态值
// routes配置:
{path: '/static', name: 'static-page', component: FirstPage, props: {foo: 'bar'}},
// FirstPage.vue中通过props获取
<template>
<div>
<h3>First page</h3>
<p>{{ $route.params }}</p>
<p>{{ foo }}</p>
</div>
</template>
<script>
export default {
props: ['foo']
}
</script>
// 地址:http://localhost:8080/static
- 如果要将route.params中的参数传到props中,可以这样配置:
- props还可以传递函数
function func({ params, query }) {
return {
id: params.id,
msg: params.msg,
foo: query.foo
};
}
export default new Router({
mode: 'history',
base: process.env.BASE_URL, // 基础路径,默认是“/” ==> 在微服务中会要求有一级服务名(上下文)
routes: [
{path: '/static', name: 'static-page', component: FirstPage, props: {foo: 'bar'}}, // 给组件传静态值
{path: '/first/:foo', name: 'first-page', component: FirstPage, props: true}, // 将route.params中
{path: '/second/:id/:msg', name: 'second-page', component: SecondPage, props: func}
]
})
<!-- SecondPage.vue -->
<template>
<div>
<h3>Second Page</h3>
<h5>获取路由路径参数</h5>
<p>id:{{$route.params.id}}</p>
<p>msg:{{$route.params.msg}}</p>
<p>foo:{{$route.query.foo}}</p>
<h5>通过属性传递过来</h5>
<p>id:{{id}}</p>
<p>msg:{{msg}}</p>
<p>foo:{{foo}}</p>
</div>
</template>
<script>
export default {
props: ['id','msg','foo'],
created(){
// 必传参数
console.log(this.$route.params.msg);
// 查询参(非必传参数)
console.log(this.$route.query.foo);
}
}
</script>
路由重定向
- redirect 配置重定向
{path: '/', redirect: '/first'},
嵌套路由
- children 配置子路由
// 路由配置
{
path: "/dashboard",
component: Dashboard,
children: [
{ path: 'static', name: 'static-page', component: FirstPage, props: { foo: 'bar' } }, // 给组件传静态值
{ path: 'first/:foo', name: 'first-page', component: FirstPage, props: true }, // 将route.params的参数传到props中
{ path: 'second/:id/:msg', name: 'second-page', component: SecondPage, props: func }
]
},
<!-- 路由导航配置 -->
<router-link to="/dashboard/static">Static</router-link> |
<router-link to="/dashboard/first/bar">First</router-link> |
<router-link to="/dashboard/second/1/vuejs?foo=bar">Second</router-link>
<!-- 父级组件Dashboard.vue -->
<div>
<h3>Dashboard</h3>
<!-- 父组件必须有一个路由出口,负责显示子路由内容 -->
<router-view></router-view>
</div>
// 访问地址:
http://localhost:8080/dashboard/static
http://localhost:8080/dashboard/first/bar
http://localhost:8080/dashboard/second/1/vuejs?foo=bar
动态路由
- this.$router.push( … )
<!-- FirstPage.vue -->
<p>
<button @click="gotoSecondPage">跳转至Second Page</button>
</p>
methods: {
gotoSecondPage(){
// this.$router.push('/dashboard/second/1/vuejs')
this.$router.push({name: 'second-page', params: {id: 1, msg: 'vuejs'}})
}
}
路由守卫
- 全局路由守卫 beforeEach
修改路由配置
const router = new Router({
// ...
});
// 全局路由守卫
router.beforeEach((to, from, next) => {
if (to.path !== '/login') { // 要求登录
if (window.isLogin) {
next();
} else {
next('/login?redirect='+to.path);
}
} else {
next();
}
next();
});
export default router;
<!-- Login.vue -->
<template>
<div>
Login
<button @click="login">login</button>
</div>
</template>
<script>
export default {
methods: {
login(){
// 登录成功
window.isLogin = true;
const {redirect} = this.$route.query; // 解构赋值
// const redirect = this.$route.query.redirect;
if(redirect){
// 有回调地址就去哪
this.$router.push(redirect);
}else{
// 没有就去首页
this.$router.push('/')
}
}
}
}
</script>
- 路由配置中的路由守卫 beforeEnter
routes: [
{ path: '/', redirect: '/dashboard/first' },
{
path: "/dashboard",
component: Dashboard,
beforeEnter(to,from,next){
if (window.isLogin) {
next();
} else {
next('/login?redirect='+to.path);
}
}
children: [
{ path: 'static', name: 'static-page', component: FirstPage, props: { foo: 'bar' } }, // 给组件传静态值
{ path: 'first/:foo', name: 'first-page', component: FirstPage, props: true }, // 将route.params的参数传到props中
{ path: 'second/:id/:msg', name: 'second-page', component: SecondPage, props: func }
]
},
{ path: "/login", name: "login", component: Login }
]
- 组件中的路由守卫 beforeRouteEnter
// FirstPage.vue
beforeRouteEnter(to, from, next) {
if(window.isLogin){
next();
} else{
next('/login?redirect=' + to.path);
}
},
beforeRouteUpdate(to, from, next){
// 仅路由参数发生变化时触发,比如 /page/vue /page/react
next();
}
// 或者使用watch监听
watch: {
$route(){ ... }
},
路由内部生命周期
1、导航被触发
2、调用全局的beforeEach
3、在重用的组件里调用beforeRouteUpdate守卫
4、在路由配置里调用beforeEnter
5、在被激活的组件里调用beforeRouteEnter
6、调用全局的beforeResolve守卫(2.5+)
7、导航被确认
8、调用全局的afterEach构子
9、触发DOM更新
router.afterEach((to,from)=>{
});
异步组件
路由懒加载
{ path: "/login", name: "login", component: () => import('./views/Login') }
Vuex数据管理
学习官网:https://vuex.vuejs.org/zh/guide
起步
- 安装:npm install vuex --save
推荐使用插件的方式安装:vue add vuex - 核心概念:store、state、mutations、getters、actions
state
- state:用于全局的数据
// main.js
import store from './store'
new Vue({
store,
render: h => h(App)
}).$mount('#app');
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
// 挂载插件
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 1
}
})
- 在组件中使用count
<p>{{$store.state.count}}</p>
mutations
- 如果我们要修改count的值,可以在mutations中配置修改count的方法
mutations: {
increase(state){
state.count += 1;
}
}
<button @click="inc">increase</button>
methods: {
inc() {
this.$store.commit('increase') // 这种属于同步修改
}
}
getters
- 有时候我们需要从store中的state中派生出一些状态,我们可以理解为vuex中数据的computed功能
getters: { // getters用于对state中的数据进行加工
money: state => {
return state.count + '元';
}
}
- 使用
<p>您的余额为:{{$store.getters.money}}</p>
actions
- mutation必须是同步的,actios是异步的mutation
actions: {
increaseAsync({state, commit}, payload) {
setTimeout(() => {
commit('increase')
}, 3000);
}
}
<button @click="incAsync">incAsync</button>
methods: {
incAsync() {
this.$store.dispatch('increaseAsync')
}
}
mapState、mapGetters
更加方便的使用spi,当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用mapState、mapGetters辅助函数帮助我们生成计算属性
- 基本使用
import {mapState,mapGetters} from 'vuex'
computed: {
...mapState(['count']), // 相当于 {count: this.$store.state.count}
...mapGetters(["money"])
},
mapMutations、mapActions
类似的,我们也可以利用mapMutations、mapActions辅助函数更方便的使用mutations和actions
import {mapMatution, mapActions} from 'vuex'
methods: {
...mapMutations(["increase"]),
...mapActions(["increaseAsync"]),
inc() {
// this.$store.commit('increase')
this.increase();
},
incAsync() {
// this.$store.dispatch('increaseAsync')
this.increaseAsync();
}
}
利用Vuex改造登录
export default new Vuex.Store({
state: {
isLogin: false // 登录状态
},
mutations: {
login(state) {
state.isLogin = true;
},
logout(state) {
state.isLogin = false;
}
},
actions: { // 异步操作时使用
submitLogin({commit},payload){
// return axios.post('/api/login').then(resp=>{
// resp.data.result
// })
return new Promise(resolve => {
setTimeout(() => {
commit('login');
resolve(true);
}, 2000);
})
}
}
})
// Login.vue
export default {
methods: {
async login(){
// 登录成功
// window.isLogin = true;
await this.$store.dispatch('submitLogin');
if (this.$store.state.isLogin) {
const {redirect} = this.$route.query; // 解构赋值
// const redirect = this.$route.query.redirect;
if(redirect){
// 有回调地址就去哪
this.$router.push(redirect);
}else{
// 没有就去首页
this.$router.push('/')
}
} else {
alert('登录失败,请重试!')
}
}
}
}
Vuex、VueRouter原理简析
vuex
class KVuex {
constructor(options){
this.state = options.state;
this.mutations = options.mutations;
this.actions = options.actions;
// 借用vue本身的响应式的通知机制,state 会将需要的依赖收集在 Dep 中
this._vm = new KVue({
data: {
$state: this.state
}
})
}
commit(type, payload, _options){
const entry = this.mutations[type];
entry.forEach(handler => handler(paylaod))
}
dispatch(type, payload){
const entry = this.actions[type];
return entry(payload);
}
}
- 源码地址:https://github.com/vuejs/vuex
vue-router
- 使用
const routes = [
{ path: '/', component: Home },
{ path: '/book', component: Book },
{ path: '/movie', component: Movie }
]
const router = new VueRouter(Vue, {
routes
})
new Vue({
el: '#app',
router
})
- 简单实现
class VueRouter {
constructor(Vue, options) {
this.$options = options
this.routeMap = {}
this.app = new Vue({
data: {
current: '#/'
}
})
this.init()
this.createRouteMap(this.$options)
this.initComponent(Vue)
}
// 初始化 hashchange
init() {
window.addEventListener('load', this.onHashChange.bind(this), false)
window.addEventListener('hashchange', this.onHashChange.bind(this), false)
}
createRouteMap(options) {
options.routes.forEach(item => {
this.routeMap[item.path] = item.component
})
}
// 注册组件
initComponent(Vue) {
Vue.component('router-link', {
props: {
to: String
},
template: '<a :href="to"><slot></slot></a>'
})
const _this = this
Vue.component('router-view', {
render(h) {
var component = _this.routeMap[_this.app.current]
return h(component)
}
})
}
// 获取当前 hash 串
getHash() {
return window.location.hash.slice(1) || '/'
}
// 设置当前路径
onHashChange() {
this.app.current = this.getHash()
}
}