Web全栈架构师(一)——Vue2.5学习笔记(1)

起步

  • 安装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()
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值