Vue脚手架的使用

Vue-Cli3

用来开发单文件组件;

// 原生写法
Vue.component({
   原生写法 
})
new Vue({});

原生写法存在以下缺点

  • 全局定义组件的名字时不能重复。
  • 字符串模板 ; es6提供了模板字符串 。
  • 不支持css;
  • 没有构建步骤

在Vue中,.vue的文件称为 单文件组件,webpack 将.vue编译成为浏览器可以识别的html+css+js的文件。

单文件组件的优势:

1.脚手架

1.1 环境搭建

  • node.js

  • npm & cnpm

  • cnpm install vue-cli
    

2.创建项目

3.Webpack

  • 在 webpack 中任何形式的文件都可以以import的形式进行导入;
  • webpack 依赖于node.js
  • webpack功能简介
    • index.js 等文件中支持 es6 语法
    • 支持 es6 的模块化import a from './a'
    • 压缩代码

3.1 简单使用

3.1.1 项目初始化

npm init -y  // 任意目录文件夹下

产生一个package.json文件;

npm install webpack webpackage-cli --save-dev // 安装相关的包
// 在开发环境中会出现相关的依赖项,项目依赖不会添加,因为本身的依赖只是实现相关的文件的打包功能

在目录下创建src文件夹,在该文件夹下创建index.js简单写两句代码

console.log("hello vue")

3.1.2 创建配置文件

在 package.json 同级目录下创建文件webpack.config.js

// 根据文件名也可以理解出,这个是webpack的配置文件
const path = require('path')// 导入node中的一个路径相关的包;
// 使用的是commonjs的语法
module.exports = {
    mode: "development",// 指定开发模式
    entry: path.join(__dirname,'src','index.js'),// 指定js的文件;
    output: {
        path: path.join(__dirname,'dist'),// 定义打包出口,一般指定dist文件夹,会自动产生;
        filename: 'bandle.js'// 指定打包后产生的js文件的名称;
    } 
}

简单补充:导包部分的代码

参考文章:https://juejin.cn/post/7039919941750882335

模块的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

通过require加载模块的流程:

  1. 找到需要的代码
  2. 判断是否缓存过,如果没有缓存过,就读取模块文件
  3. 把读取到的内容放到一个自执行的函数中执行;

3.1.3 执行打包命令

修改package.json中的命令。

"scripts": {
    "build": "webpack"
}

配置完成之后,终端执行 npm run serve;

此时就会产生对应的文件夹及 js 文件;

3.1.4 打包html

安装插件

npm install html-webpack-plugin --save-dev

编写 html 文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>Hello Webpac</h1>
</body>
</html>

编写webpack.config.js中的配置;

const path = require('path')// 导入node中的一个路径相关的包;
const HtmlWebpackPlugin = require('html-webpack-plugin')// 导入打包html需要的插件包

// 使用的是commonjs的语法
module.exports = {
    mode: "development",// 指定开发模式
    entry: path.join(__dirname,'src','index.js'),// 指定js的文件;
    output: {
        path: path.join(__dirname,'dist'),// 定义打包出口,一般指定dist文件夹,会自动产生;
        filename: 'bandle.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            // __dirname 是当前package.json 文件的同级目录
            template: path.join(__dirname,'src','index.html'),// 拼接 html 文件的路径;
            filename: 'index.html'// 指定打包后的文件;
        })
    ]
}

重新打包,(注:package.json 在上述步骤已经完成编辑),执行npm run build.

3.1.5 编译 ES6 文件

是用 webpack 打包后的代码还是 es6 语法的形式,可能不兼容低版本的浏览器,因此需要进行相关的编译操作,将 es6 的语法编译成为 es5 的语法;

安装插件

npm install @babel/core @babel/preset-env babel-loader --save-dev    // 一次安装了三个

package.json的同级目录下创建.babelrc文件。

.babelrc
{
    // 预设: babel 一系列插件的集合
    "presets": ["@babel/preset-env"]
}

修改webpack.config.js

const path = require('path')// 导入node中的一个路径相关的包;
const HtmlWebpackPlugin = require('html-webpack-plugin')// 导入打包html需要的插件包

// 使用的是commonjs的语法
module.exports = {
    mode: "development",// 指定开发模式
    entry: path.join(__dirname,'src','index.js'),// 指定js的文件;
    output: {
        path: path.join(__dirname,'dist'),// 定义打包出口,一般指定dist文件夹,会自动产生;
        filename: 'bandle.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            // __dirname 是当前package.json 文件的同级目录
            template: path.join(__dirname,'src','index.html'),// 拼接 html 文件的路径;
            filename: 'index.html'// 指定打包后的文件;
        })
    ],
    module: {
        rules: [
            {
                test: /\.js$/,// 文件正则表达是
                loader: 'babel-loader',
                // 当只有一个loder的时候直接使用字符串,有多个的时候改变为数组的形式['xx-loder','sssscc']
                // 而且当有多个 loder 的时候webpack 时候webpack 是从左向右加载;
                include: path.join(__dirname,'src'),// 编译哪些位置的js代码
                exclude: /node_modules/
                // 不编译哪些位置的js;
            }
        ]
    },
    devServer: {
        // 指定端口;
        port: 8000,
        static: path.join(__dirname,'dist')// 把dist 设置成为静态资源文件夹;

    }
}

补充:webpack 打包 css 的时候也是引入两个 loder将loder在 module 中添加相关的配置.

3.2 实时更新打包后文件

使用上述的方式进行打包的话,会发生当我修改了原来的文件,但是打包页面中引用的文件并没有发生改变,因此我们需要在安装一个插件进行使用。

npm install webpack-dev-server --save-dev

修改webpack.config.js

const path = require('path')// 导入node中的一个路径相关的包;
const HtmlWebpackPlugin = require('html-webpack-plugin')// 导入打包html需要的插件包

// 使用的是commonjs的语法
module.exports = {
    mode: "development",// 指定开发模式
    entry: path.join(__dirname,'src','index.js'),// 指定js的文件;
    output: {
        path: path.join(__dirname,'dist'),// 定义打包出口,一般指定dist文件夹,会自动产生;
        filename: 'bandle.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            // __dirname 是当前package.json 文件的同级目录
            template: path.join(__dirname,'src','index.html'),// 拼接 html 文件的路径;
            filename: 'index.html'// 指定打包后的文件;
        })
    ],
    devServer: {
        // 指定端口;
        port: 8000,
        static: path.join(__dirname,'dist')// 把dist 设置成为静态资源文件夹;

    }
}

补充:devServer 会将打包好的数据存在缓存之中,即便删除文件也不会出错,但是会造成数据丢失,缓存中的数据容易随着物理关机而丢失;

4.commonjs

参考文章:http://javascript.ruanyifeng.com/nodejs/module.html;

参考文章:https://blog.csdn.net/banmao8461/article/details/102407702

为了写可维护的代码;常把很多函数分组,分别放到不同文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。

node环境中 一个.js文件就是一个模块(moudle),每个文件 就是一个模块,有自己的作用域,在一个文件里面定义变量、函数、类,都是私有的对其他文件不可见,而Node应用由模块组成,采用 CommonJS 模块规范

CommonJS 模块特点

  • 1、所有代码都运行在模块作用域,不会污染全局作用域
  • 2、模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
  • 3、模块加载的顺序,按照其在代码中出现的顺序

4.1 commonjs规范

// example.js
var x = 5;// 全部是私有的
var addX = function (value) {
  return value + x;
};

如果想在多个文件分享变量,必须定义为global对象的属性。

global.warning = true;
// 上面代码的warning变量,可以被所有文件读取。当然,这样写法是不推荐的

CommonJS 规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x; // 使用module.exports 输出属性
module.exports.addX = addX; // 输出相关的变量;

重点:require 用于加载模块,更多内容见 4.3

const example = require('./example.js');

console.log(example.x); // 5
console.log(example.addX(1)); // 6

4.2 module 对象

Node内部提供一个Module构建函数。所有模块都是Module的实例。每个模块内部,都有一个module对象,代表当前模块。它有以下属性。

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值。

module.exports属性与exports 变量

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。

而为了方便,Node为每个模块提供一个exports变量,指向module.exports,正因为如此不能直接对exports变量赋值,因为这样会等于切断了exportsmodule.exports的联系。相当于有有一句以下的赋值语句

const exports = module.exports; 

4.3 require 命令

Node使用CommonJS模块规范,内置的require命令用于加载模块文件。

require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

关于环境变量部分,node中会有使用,前端中使用较少;

4.3.1 加载规则

require命令用于加载文件,后缀名默认为.js.

const foo = require('foo');// 当不写后缀名的时候相当于写入 foo.js
//  等同于
const foo = require('foo.js');

根据参数的不同格式,require命令去不同路径寻找模块文件。

(1)如果参数字符串以/开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js

(2)如果参数字符串以./开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js

(3)如果参数字符串不以.//开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装,第三方包)。

举例来说,脚本/home/user/projects/foo.js执行了require('bar.js')命令,Node会依次搜索以下文件。

/usr/local/lib/node/bar.js # 绝对文件路径
/home/user/projects/node_modules/bar.js
/home/user/node_modules/bar.js
/home/node_modules/bar.js # 全局第三方 
/node_modules/bar.js  # 局部第三方模块

这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。

(4)如果参数字符串不以.//开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。

(5)如果指定的模块文件没有发现,Node会尝试为文件名添加.js.json.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。

(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。

4.3.2 目录的加载规则

通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。

在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子。

// package.json
{ 
    "name" : "some-library",
   "main" : "./lib/some-library.js" 
}

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。

4.3.3 模块的缓存

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// hello

上面代码中,连续三次使用require命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个message属性。但是第三次加载的时候,这个message属性依然存在,这就证明require命令并没有重新加载模块文件,而是输出了缓存。

如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次require这个模块的时候,重新执行一下输出的函数。

所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以像下面这样写。

// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存,使用循环;
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})

注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。

4.4.4 模块的循环加载

如果发生模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。

// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';

// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';

// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);

上面代码是三个JavaScript文件。其中,a.js加载了b.js,而b.js又加载a.js。这时,Node返回a.js的不完整版本,所以执行结果如下。

$ node main.js // 运行 main.js 脚本
b.js  a1 // 先执行了 b中的代码,此时 a.js 尚未加载完成;
a.js  b2
main.js  a2
main.js  b2

修改main.js,再次加载a.js和b.js

// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);

执行上面代码,结果如下。

$ node main.js // 运行
b.js  a1 // 未加载完成的
a.js  b2
main.js  a2 // 加载完成的
main.js  b2
main.js  a2
main.js  b2

上面代码中,第二次加载a.js和b.js时,会直接从缓存读取exports属性,所以a.js和b.js内部的console.log语句都不会执行了。

4.4.5 require.main

require方法有一个main属性,可以用来判断模块是直接执行,还是被调用执行。

直接执行的时候(node module.js),require.main属性指向模块本身。

require.main === module
// true

调用执行的时候(通过require加载该脚本执行),上面的表达式返回false;

4.4.6 require内部处理流程

require命令是CommonJS规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的module.require命令,而后者又调用Node的内部命令Module._load

Module._load = function(request, parent, isMain) {
  // 1. 检查 Module._cache,是否缓存之中有指定模块
  // 2. 如果缓存之中没有,就创建一个新的Module实例
  // 3. 将它保存到缓存
  // 4. 使用 module.load() 加载指定的模块文件,
  //    读取文件内容之后,使用 module.compile() 执行文件代码
  // 5. 如果加载/解析过程报错,就从缓存删除该模块
  // 6. 返回该模块的 module.exports
};

上面的第4步,采用module.compile()执行指定模块的脚本,逻辑如下。

Module.prototype._compile = function(content, filename) {
  // 1. 生成一个require函数,指向module.require
  // 2. 加载其他辅助方法到require
  // 3. 将文件内容放到一个函数之中,该函数可调用 require
  // 4. 执行该函数
};

上面的第1步和第2步,require函数及其辅助方法主要如下。

  • require(): 加载外部模块
  • require.resolve():将模块名解析到一个绝对路径
  • require.main:指向主模块
  • require.cache:指向所有缓存的模块
  • require.extensions:根据文件的后缀名,调用不同的执行函数

一旦require函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样可以避免污染全局环境。该函数的参数包括requiremoduleexports,以及其他一些参数。

(function (exports, require, module, __filename, __dirname) {
  // YOUR CODE INJECTED HERE!
});

Module._compile方法是同步执行的,所以Module._load要等它执行完成,才会向用户返回module.exports的值。

5.Vue使用element

组件分类

  • 通用组件
    • 基础组件,⼤部分UI都是这种组件,⽐如表单 布局 弹窗等
  • 业务组件
    • 与需求挂钩,会被复⽤,⽐如抽奖,摇⼀摇等(包含实际业务,可复用的可以进行封装);
  • 页面组件
    • 每一个页面都是一个组件。

5.1 使用第三方组件

安装

npm install element-ui

使用

main.js中进行导入;导入方式分为按需导入和全部导入;最好使用局部导入。

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

Vue.use(ElementUI);

new Vue({
  el: '#app',
  render: h => h(App)
});
// 全局

按需导入

import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';

Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
 * Vue.use(Button)
 * Vue.use(Select)
 */

new Vue({
  el: '#app',
  render: h => h(App)
});

使用的时候也可以使用vue add element,但是这种方法会覆盖掉原来的一些文件,并且生成一些文件plugin/element.js文件,在src文件夹下,并且会自动在main.js中进行了导入,使用的是局部导入。

vue add element 
// 个人理解,因为会产生相关的文件,并且会覆盖原来的App.vue文件;因此我们最好在创建项目的初期直接完成相关插件的导入;
// 创建的时候依旧会选择 局部/ 全局的导入 ,和语言的选择;

还会产生一个.eslintrc.js的文件;

参考文章:https://blog.csdn.net/weixin_38606332/article/details/80864381

参考文章:https://blog.csdn.net/qq_51657072/article/details/124427270

// 使用 commonjs 的语法进行书写
module.exports = {
  root: true,
  env: {
    node: true
  },
  'extends': [
    'plugin:vue/essential',
    'eslint:recommended'
  ],
  parserOptions: {
    parser: '@babel/eslint-parser'
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  }
}

默认eslint规则:

  • 代码末尾不能加分号 ;
  • 代码中不能存在多行空行;
  • tab键不能使用,必须换成两个空格;(超级不习惯)
  • 代码中不能存在声明了但未使用的变量;(这个我觉得可以有,特别容易出现报错)

最简单的方法,关闭eslint检测,其实很简单,把 build/webpack.base.conf.js 配置文件中的eslint rules注释掉即可。但我不推荐你这么做,eslint检测是有必要的,能保持良好的代码风格。

产生的另一个文件src/plugins/element.js,并且这个 js 文件在 main.js文件中已经被导入

// element.js 初始状态
import Vue from 'vue'
import { Button } from 'element-ui'

Vue.use(Button)
// 不难发现,这是 element-UI 按需导入的js文件;

我们在element.js文件中use('xxx')相关的组件可以直接使用,就是因为main.js中直接导入了;

// main.js 
import Vue from 'vue'
import App from './App.vue'
import './plugins/element.js' // 已经成功导入了相关的 element.js

// 内置导包的时候没有加分号,应该是遵守了上述文件的开发规范.
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

为什么这里最开始就导入了Button呢?因为我们这条命令还覆盖了原来的App.vue

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <div>
      <p>
        If Element is successfully added to this project, you'll see an
        <code v-text="'<el-button>'"></code>
        below
      </p>
      <el-button>el-button</el-button><!--这里使用了 element ui 中的button 按钮-->
    </div>
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'app',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

6.axios 使用

npm install axios

npm install vue-axios // 未知,选择安装

当然在脚手架中可以使用vue add命令进行安装,这样也会在产生相关的文件,最好在项目建立初期进行安装,否则建议使用npm的方式进行安装.

vue add axios  // 会在plugin 中产生相关的文件axios.js 并且在 main.js 中导入该文件.

6.1 简单使用

为了使项目更容易管理,直接仿照vue add命令产生的文件以及导入的方式进行使用;

import Vue from "vue";
import axios from "axios";
import VueAxios from "vue-axios";

Vue.use(axios)
Vue.use(VueAxios )

初始版本的axios.js文件;

import Vue from "vue";
import axios from "axios";// 导包
import VueAxios from "vue-axios";

// 2.挂载到vue中
Vue.use(VueAxios,axios)

// 3.设置相关的默认地址,还有一些其他的配置
axios.defaults.baseURL = "http://127.0.0.1:5000/api/"; // 设置相关的基础 url,更改服务器的发布地址的话,只需要在这里进行相关的修改,不需要在别的地方进行修改前置路由

// 4.请求拦截器
axios.interceptors.request.use(function(config){
    // 定义请求之前做什么
    console.log(config);
})

// 5.响应拦截器
axios.interceptors.response.use(function(response){
    // 请求成功响应的拦截器
    /*设置 响应状态码是 200 的情况下自动执行.*/
    console.log(response);

})

// 注意: 以上的数据如果直接运行的话会出现相关的异常信息,因为拦截器内并没有相关的逻辑;拦截器可以根据API 接口抽离出来相关的通用操作;

后续操作根据相关的场景中编写不同的 请求/响应 拦截器;

main.js中进行数据文件的导入;

注:main.js相当于整个程序的入口文件,一般增加的第三方插件都会在这里进行挂载配置,为了插件之间的解耦合,从而将各个插件的管理分配到不同的.js文件之中;

// main.js

import Vue from 'vue'
import App from './App.vue'
import '@/plugins/axios' // 导入相关的文件,@ 表示定位到src目录下面; 未知:导入 axios.js 文件的时候不加 .js 可以成功使用
import './plugins/element.js'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

特别提醒:导入.js文件的时候,使用@定位到src目录下面,此时在导入的时候就不要再加.js的后缀,容易出错;不使用@的时候导入.js文件的时候也不出错;

import '@/plugins/axios' 
import './plugins/axios.js'
// 以上两种写法的效果一样,但是不要(最好不要)再第一种导入方法的后面添加`.js`;因为我本次无法获取到相关文件;

发送请求,进行登录

this.axios.post('/login/',this.userForm).then(res => {
    console.log(res);// 执行成功的返回值;
}).catch(err => {
    console.log(err);// 执行出错,进行异常处理;
})

6.2 写法升级

使用异步的操作进行处理;通常用在请求初始化数据中;使用 ES6 中相关的asyncawait关键字进行处理;

这种写法在业务逻辑中结合选择,对语法要求较高;

async created() {
   // try-catch解决async-awiat错误处理
 try {
  const { data } = await axios.get('/cartList')
  this.cartList = data;
 } catch (error) {
  console.log(error);
 }
}

7.Vue-Router

官网:https://router.vuejs.org/zh/introduction.html

Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。功能包括:

  • 嵌套路由映射

  • 动态路由选择

  • 模块化、基于组件的路由配置

  • 路由参数、查询、通配符

  • 展示由 Vue.js 的过渡系统提供的过渡效果

  • 细致的导航控制

  • 自动激活 CSS 类的链接

  • HTML5 history 模式或 hash 模式

  • 可定制的滚动行为

  • URL 的正确编码

    ⽤ Vue.js + Vue Router 创建单⻚应⽤(SPA),是⾮常简单的。使⽤ Vue.js ,我们已经可以通过组合组件来组成应⽤程序,当你要把 Vue Router 添加进来,我们需要做的是,将组件 components 映射到路由 routes,然后告诉 Vue Router 在哪⾥渲染它们;

7.1 安装

安装方式

npm install vue-router@4 --save-dev  // -S

main.js中进行导入

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)// 挂载

第二种安装方式

vue add vue-router  // 使用这种方式会直接将上述步骤完成,但是在中途安装容易出错,安装钱注意备份文件,以免发生文件覆盖

特别注意:执行vue add xxx命令的时候一定要确保终端的路径在package.json文件的同级路径下面,否则会将相关的插件安装到别的目录;

第三种方式

vue create pro  // vue-cli 创建项目的时候,直接选中相关选项 Router 既可以完成

产生相关的文件以及文件夹

src
|
|--router
|	|
|	|--index.js
|
|--views
|	|
|	|-- AboutView.vue
|	|-- HomeView.vue

程序启动的时候是main.js入口

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'  // 导入相关的路由配置,对象在文件中已经被 export
import './plugins/element.js'

Vue.config.productionTip = false

new Vue({
  router,// 将对象注册到Vue
  render: h => h(App)
}).$mount('#app')

补充:import router from './router',这里的 router 是一个文件夹,并不是.js文件;关于js import 一个文件夹自动导入index.js文件;

// 这是一个node的特性,node 引入一个包或者文件的时候会在相对于package.json 同级的node_moudles文件夹下找对应的文件夹组件,如果没有找到会到上一级直至全局都没有才抛出未找到组件。    
// 找到对应的组件后如果组件内没有package.json文件将会默认使用index.js 如果package.json文件,将会根据package.json下的main配置项找哦到对应的组件入口文件

因此导入的是index.js,类似于 Python 中导入包名的时候,直接导入的是包下面的__init__.py文件;

index.js文件如下:

import Vue from 'vue'
import VueRouter from 'vue-router'// 导包
import HomeView from '../views/HomeView.vue'// 导入路由中对应的组件

Vue.use(VueRouter)// 挂载

const routes = [
  {
    path: '/',// 路由
    name: 'home',// 名称
    component: HomeView// 对应的组件
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')// 使用 箭头函数,在使用的时候才进行加载;
  }
]
// 创建相关的vueRouter 对象
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
// 将对象导出
export default router

产生的 views 文件夹中的文件是.vue的组件,将相关,是存放组件的位置,一般存放与路由对应的路径;因此compopent文件夹中的组件一般不会太多,使用路由的时候一般会选择放在 views中,导入路由中导入这些组件的时候通常使用的方式是懒加载(箭头函数异步加载()=> import('@/views/Home'));

7.2 简单使用

因为我们使用vue add element的命令,覆盖了原生App.vue文件,因此我们简单使用就将已有的组件进行显示;

完成上述文件的挂载等操作时候我们需要在App.vue文件中使用我们的路由

<template>
  <div id="app">
    <!-- 使用router-link 组件进行导航,通过 to 属性指定路由连接,路由信息是router中定义的 -->
    <!-- router-link 默认会被渲染成一个 a 标签,to 会被渲染成href 属性 -->
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
    <!-- 路由的出口;路由匹配到的组件将会被渲染到这个标签(router-view)里面 -->
    <router-view></router-view>
  </div>
</template>

<script>
// import HelloWorld from './components/HelloWorld.vue' 注释掉 element添加的页面组件

export default {
  name: 'app',
  components: {
    // HelloWorld 取消他的挂载
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

经过上面的配置之后我们的初始界面就会出现在我们的初始界面上面,当我们点击about的时候,浏览器中的路由就会变成对应的路由信息http://localhost:8080/about,因此我们匹配的路由是浏览器中的路由,子路由的信息稍微特殊,他处于子组件之中;

7.3 命名路由

在配置路由的时候,给路由添加名字,访问时就可以动态的根据名字来进 ⾏访问

import HomeView from '../views/HomeView.vue'// 导入路由中对应的组件,会直接加载在浏览器中
const routes = [
  {
    path: '/',// 路由
    name: 'home',// 名称,这是我们为路由配置的名称;
    component: HomeView// 对应的组件,上面已经导入了,
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue')// 使用 箭头函数,在使用的时候才进行加载;
  }
]

app.vue中使用一个命名的路由,我们需要给router-link标签的to属性传一个对象:

<router-link :to="{name:'home'}">Home | </router-link>
<router-link :to="{name:'about'}">About</router-link>
<!-- 成功渲染到页面上面 -->

7.4 动态路由 & 路由参数

我们经常需要把某种模式匹配到的所有路由,全部映射到同个组件。例如,我们有⼀个User 组件,对于所有 ID 各不相同的⽤户,都要 使⽤这个组件来渲染。那么,我们可以在 vue-router 的路由路径中 使⽤动态路径参数(dynamic segment) 来达到这个效果

编写组件:

<template>
    <div id="userapp">
        <!-- 获取相关的 用户id,参数直接在页面上进行渲染 -->
        用户ID :{{$route.params.id}}
    </div>
</template>
<script>
    export default {
        created(){
            // 打印参数;
            console.log(this.$route.params);
        }
    }
</script>
<style scoped>

</style>

注册路由

import Vue from 'vue'
import VueRouter from 'vue-router'// 导包
import HomeView from '../views/HomeView.vue'// 导入路由中对应的组件

Vue.use(VueRouter)// 挂载

const routes = [
  // ... 省略重复代码
  {
    path: '/user/:id', // :id 表示id 是动态传入的参数;
    name: 'user',
    component: () => import('@/views/UserView')
  }
]
// ...

app.vue中进行调用;

<!-- 使用相关的参数传递对象Params 对象 -->
<router-link :to="{name:'user',params:{id:1}}">Userid</router-link>
<router-view></router-view>
<!--点击访问到的路由就是 http://localhost:8080/user/1-->

提醒:访问路由的时候不应依赖标签,可以直接在浏览器中输入相关的地址进行访问;例如:http://localhost:8080/user/1;

当匹配到路由时,参数值会被设置到this.$route.params,可以在每个组 件中使⽤,于是,我们可以更新 User 的模板,输出当前⽤户的 ID:

<template>
    <div id="userapp">
        <!-- 获取相关的 用户id,参数直接在页面上进行渲染 -->
        用户ID :{{$route.params.id}}
    </div>
</template>
<script>
    export default {
        created(){
            // 打印参数;
            console.log(this.$route.params);
        }
    }
</script>

响应路由参数的变化:*当使用路由参数的时候,例如从 /user/1 导航到 /user/2,原来的组件是会被*复用的。因为两个路由渲染同个组件,比起销毁创建,复用则更加高效;不过这样也意味这生命周期的钩子不会在被调用;

复⽤组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route 对象:

/*使⽤watch(监测变化) $route对象
     watch: {
     $route(to, from) {
     console.log(to.params.id);
     }
 }, */
// 或者使⽤导航守卫
beforeRouteUpdate(to,from,next){
 //查看路由的变化
 //⼀定要调⽤next,不然就会阻塞路由的变化
 next();
}

关于导航守卫,我们在后面做跟详细的阐述;

7.5 404 路由 & 优先级

有时候,同⼀个路径可以匹配多个路由,此时,匹配的优先级就按照 路由的定义顺序:谁先定义的,谁的优先级就最⾼。

import Vue from 'vue'
import VueRouter from 'vue-router'// 导包
import HomeView from '../views/HomeView.vue'// 导入路由中对应的组件

Vue.use(VueRouter)// 挂载

const routes = [
  {
    path: '/user/:id',
    name: 'user',
    component: () => import('@/views/UserView')
  },
  {
    path: '/user/-*',
    name: 'user',
    component: () => import('@/views/UserView')
  }
]
// 此处我们定义了 两个user的路由,匹配的时候谁先定义的谁的优先级就搞,说白了就是谁在上面执行谁.S

404 路由:当我们从上执行到最下面的时候都没有匹配到路由对应的信息,说明不存在相关的路由,通常会在最下面设置一个*通配符,匹配所有路由,返回404界面

当使⽤通配符路由时,请确保路由的顺序是正确的,也就是说含有通 配符的路由应该放在最后。路由 { path: '*' } 通常⽤于客户端 404 错误。

7.6 查询参数

类似像地址上出现的这种:http://localhos:8080/page?id=1&title=foo

使用查询参数

// 路由信息正常编写;
{
    path: '/page',
    name: 'page',
    component: ()=> import('@/views/PageView')
},

组件中的路由标签师兄的时候


补充:$route对象

{
    name: 'page', 
    meta: {..},
    path: '/page',
    hash: '', 
 fullPath: "/page?title=foo&page=12"
 matched: [{…}]
    params: {}
    path: "/page"
    query: {title: 'foo', page: '12'}
}

7.7 路由重定向和别名

重定向:例如,/重定向到/home;

{
    path: '/home',
    redirect: {name: 'home'},// 重定向路由,路由重定向的时候一般是写在被重定向路由的下面,否则容易出现未加载路由的异常;
    alias: '/alias' // 起别名,仅仅起起别名 ⽤户访问http://loacalhost:8080/alias的时候,显 示User组件
}

别名的功能可以自由的将UI结构映射到任意的 URL,而不是受限于配置的嵌套路由;别名一般使用较少,特别是在传参,嵌套等复杂路由的时候慎用;

7.8 组件解耦传参

在组件中使用$route会使之与其对应路由形成高度耦合的状态,从而使组件只能在某些特定的url上使用,限制了组件的灵活性;

<template>
    <div id="userapp">
        <!-- 获取相关的 用户id,参数直接在页面上进行渲染 -->
        <!-- 用户ID :{{$route.params.id}} -->
        <!-- 使用props获取路由传递过来的值 -->
        用户ID {{id}}
    </div>
</template>
<script>
    export default {

        // 使用路由传值的方式进行获取
        props:{
            id: {
                type: String,
                default: ''
            }
        }
    }
</script>
<style scoped>

</style>

路由的配置;

{
    path: '/user/:id',
    name: 'user',
    component: () => import('@/views/UserView'),
    // props: true // 使用组件 props 将组件和路由解耦
        
    // props 也可以是一个函数,箭头函数返回对象的写法 ()=>({})
    props: (route)=>({
        id: route.params.id
    })
}

上面的配置方式会让组件的脱离路由的限制而是用旨在路由的配置中设置即可;

7.9 编程式导航和前进后退

除了使⽤ <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例⽅法,通过编写代码来实现。

注意:在 Vue 实例内部,你可以通过 \(router** **访问路由实例。因此我们可以调⽤** **this.\)router.push

声明式 编程式
router-link 绑定路由 router.push

该⽅法的参数可以是⼀个字符串路径,或者⼀个描述地址的对象。前进后退就相当于浏览器左上角的按钮

<template>
  <div id="app">
	<!-- 声明式导航 -->
    <router-link :to="{name:'home'}">Home | </router-link>
    <router-link :to="{name:'about'}">About | </router-link>
    <router-link :to="{name:'user',params:{id:'1'}}">Userid |</router-link>
    <router-link :to="{name:'page',query:{title:'foo',page:12}}"> 分页传参</router-link>
    <router-view></router-view>
      
    <!--编程式导航绑定的按钮-->
    <el-button @click="handlepage">跳 转</el-button>
    <el-button @click="upclick">前 进</el-button>
    <el-button @click="downclick">后 退</el-button>
  </div>
</template>

<script>
// import HelloWorld from './components/HelloWorld.vue'
export default {
  name: 'app',
  components: {
    // HelloWorld
  },
  methods:{
    // 编程式导航的代码  
    handlepage(){
      // 使用字符串的方式进行传参跳转,在有动态参数路由的界面可能会出现拼接路由的问题;
      // this.$router.push('home')
      // console.log(this.$router);
      // 对象
      // this.$router.push({path:'home'})

      // 命名的路由, /userid
      // this.$router.push({name: 'user',params: {id: '123'}})

      // 待参数的查询,因为这里的导航按钮实在全局组件中的,因此,当前跳转当前可能出错
      this.$router.push({path:'page',query: {title: 4566}})

      // 遇见传参数的导航式跳转的情况可能有有异常,请特殊处理;
    },
    upclick(){
      // 前进一步,等同于 history.forward() 浏览器左上角的前进后退按钮
      // 传入正值代表前进,负值代表后退
      this.$router.go(1)
      
      // 前进 3 步记录
      // router.go(3)
      // 如果 history 记录不够⽤,那就默默地失败呗
      // router.go(-100)
      // router.go(100)

    },
    downclick(){
      // 后退一步
      this.$router.go(-1)
    }
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

7.10 嵌套路由 & 命名视图

实际⽣活中的应⽤界⾯,通常由多层嵌套的组件组合⽽成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件

/user/1/profile 					/user/1/posts
+------------------+ 				+-----------------
+
| User     |			    | User 
|
| +--------------+ 	|				| +-------------+
|
| | Profile  |	| +------------>| | Posts   |
| |				| 	|				| |				|

在路由中进行设定子路由,

{
 path: '/user/:id',
 name: 'user',
 component: () => import('@/views/UserView'),
 // props: true // 使用组件 props 将组件和路由解耦
 // props 也可以是一个函数,箭头函数返回对象的写法 ()=>({})
 props: (route)=>({
   id: route.params.id
 }),
    // 设置相关的子路由
 children: [
   // 当user:id 匹配成功
   {
  path: 'profile',
  component: ()=> import('@/views/ProfileView')
   },
   {
  path: 'posts',
  component: ()=> import('@/views/PostsView')// 导入子路由对应的组件信息
   }
 ]
},

当我们设置好子路由之后,需要在对应的组件中设置<router-view>标签以便子路由进行渲染;

<template>
    <div id="userapp">
        <!-- 获取相关的 用户id,参数直接在页面上进行渲染 -->
        <!-- 用户ID :{{$route.params.id}} -->
        <!-- 使用props获取路由传递过来的值 -->
        用户ID {{id}}
        <!-- 渲染子路由的组件界面 -->
        <router-view></router-view>

    </div>
</template>
<script>
    export default {

        // 使用获取
        props:{
            id: {
                type: String,
                default: ''
            }
        },
        created(){
            // 打印参数;
            console.log(this.$route.params);
        }
    }
</script>
<style scoped>

</style>

访问上述路由的时候http://localhost:8080/user/sss/profile,其中SSS是参数信息;profile是子路由

命名视图

有时候想同时 (同级) 展示多个视图,⽽不是嵌套展示,例如创建⼀个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命

名视图就派上⽤场了

{
 // 同级显示组件,非嵌套
 path: '/viewname',
 name: 'viewname',
 // 注意这个键值,他的 key + s 了;
 components: {
   // default: ViewName,//默认的名字,可以不写,根据情况来确定
   profile: ()=>import('@/views/ProfileView'),
   posts: ()=> import('@/views/PostsView')
 }

},

在需要同级展示的组件中设定相关的<router-view>标签;

<router-view name="profile"></router-view>
<router-view name="posts"></router-view>

成功显示;

7.11 导航守卫

导航表示路由正在发生改变;

完整的导航解析流程:

  1. 导航被触发
  2. 在失活的组件里调用离开守卫
  3. 调用全局的beforeEach守卫
  4. 在重用的组件里调用beforeRouterUpdate守卫(2.2+)
  5. 在路由配置里面调用beforeEnter
  6. 解析路由组件。
  7. 在被激活的组件里调用beforeRouterEnter.
  8. 调用全局的beforeResolve守卫(2.5+)
  9. 导航被确认
  10. 调用全局的afterEach钩子
  11. 触发 DOM 更新
  12. 用创建好的实例调用beforeRouterEnter守卫中传给next的回调函数

7.11.1 全局守卫

你可以使用router.beforeEach注册一个全局前置守卫;

// 注册全局守卫
router.beforeEach((to,from,next)=>{
  // shou位置必须有 next(),否则会卡到当前路由,不再向下执行;
})

写在router/index.js文件中,创建 router对象后,注册全局组件.

有个需求,⽤户访问在浏览⽹站时,会访问很多组件,当⽤户跳转到 /viewname ,发现⽤户没有登录,此时应该让⽤户登录才能查看,应该让⽤户跳转到登录⻚⾯,登录完成之后才可以查看我的笔记的内容,这个时候全局守卫起到了关键的作⽤;

import Vue from 'vue'
import VueRouter from 'vue-router'// 导包
import HomeView from '../views/HomeView.vue'// 导入路由中对应的组件

Vue.use(VueRouter)// 挂载

const routes = [
  {
    // 同级显示组件,非嵌套
    path: '/viewname',
    name: 'viewname',
    // 注意这个键值,他的 key + s 了;
    components: {
      // default: ViewName,//默认的名字,可以不写,根据情况来确定
      profile: ()=>import('@/views/ProfileView'),
      posts: ()=> import('@/views/PostsView')
    }
  },
  {
    path: '/login',
    name: 'login',
    component: ()=>import('@/views/LoginView')
  }
]
// 创建相关的vueRouter 对象
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
// 注册全局守卫
router.beforeEach((to,from,next)=>{
  // 守卫必须有 next(),否则会卡到当前路由,不再向下执行;

  if(to.path === '/viewname'){
    let user = JSON.parse(localStorage.getItem('user'))// 去浏览器中的本地看是否存在数据
    if(user){
      // 有数据直接放行
      next()
    }else{
      // 如果没有跳转到登陆界面
      next('/login')
    }
  }else{
      // 不是 viewname 的路由则直接同行
    next()
  }
})
// 将对象导出
export default router

LoginView.vue文件如下

<template>
  <div>
    <input type="text" v-model="username"/>
    <input type="password" v-model="pwd" />
    <br/>
    <el-button type="primary" @click="handleLogin">登 录</el-button>
  </div>
</template>

<script>
export default {
  data () {
    return {
        username: "",
        pwd: ""
    };
  },
  methods:{
    handleLogin(){
        // 1.获取用户名和密码
        // 2.与后端发生交互
        setTimeout(()=>{
            let data = {
                username: this.username
            }
            // 保存用户登录信息
            localStorage.setItem("user",JSON.stringify(data))// 设置用户信息保存到浏览器本地
            // 跳转到登录后的界面
            this.$router.push({name:"viewname"})
        },1000)
    }
  }

}

</script>
<style scoped>
</style>

7.11.2 组件内的守卫

你可以在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave
<!--跳转到别组件的时候会执行 `beforeRouteLeave`-->
<template>
  <div>
    <input type="text" v-model="username"/>
    <input type="password" v-model="pwd" />
    <br/>
    <el-button type="primary" @click="handleLogin">登 录</el-button>
  </div>
</template>

<script>
export default {
  data () {
    return {
        username: "",
        pwd: ""
    };
  },
  methods:{
    handleLogin(){
        // 1.获取用户名和密码
        // 2.与后端发生交互
        setTimeout(()=>{
            let data = {
                username: this.username
            }
            // 保存用户登录信息
            localStorage.setItem("user",JSON.stringify(data))
            // 跳转到登录后的界面
            this.$router.push({name:"viewname"})
        },1000)
    }
  },
  beforeRouteLeave (to, from, next) {
    // 导航路由离开该组件的时候调用
    // 可以访问该组件的实例
    if(this.pwd){
        alert("请提交信息后离开");
        next(false);// 可以用element的提示框优化
    }else{
        next()
    }
  }

}

</script>
<style scoped>
</style>

如果想要数据保持不变的话,需要使用keep-alive,使组件保活.

7.11.2 路由信息实现权限控制

router/index.js文件中,给需要添加权限的路由设置meta字段;

import Vue from 'vue'
import VueRouter from 'vue-router'// 导包
import HomeView from '../views/HomeView.vue'// 导入路由中对应的组件

Vue.use(VueRouter)// 挂载

const routes = [
  {
    // 同级显示组件,非嵌套
    path: '/viewname',
    name: 'viewname',
    // 注意这个键值,他的 key + s 了;
    components: {
      // default: ViewName,//默认的名字,可以不写,根据情况来确定
      profile: ()=>import('@/views/ProfileView'),
      posts: ()=> import('@/views/PostsView')
    },
    meta: {
      requiresAuth: true // 路由独享守卫
    }

  },
  {
    path: '/login',
    name: 'login',
    component: ()=>import('@/views/LoginView')
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue'),// 使用 箭头函数,在使用的时候才进行加载;
    meta: {
      requiresAuth: true // 只要需要设定登录校验的路由加上该字段即可,路由独享守卫
    }
  }
]
// 创建相关的vueRouter 对象
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
// 注册全局守卫
router.beforeEach((to,from,next)=>{
  // 守卫必须有 next(),否则会卡到当前路由,不再向下执行;

  console.log(to);
  if(to.matched.some(record => record.meta.requiresAuth)){
    // 需要权限校验
    if(!localStorage.getItem('user')){
      next({
        path:"/login",
        query: {
          redirect: to.fullPath// 重定向跳转的路由,组件中使用的时候是$route
        }
      })
    }else{
      next()
    }
  }else{
    next()
  }
})

// 将对象导出
export default router

设置登录组件,符合全局守卫中权限校验的逻辑

<template>
  <div>
    <input type="text" v-model="username"/>
    <input type="password" v-model="pwd" />
    <br/>
    <el-button type="primary" @click="handleLogin">登 录</el-button>
  </div>
</template>

<script>
export default {
  data () {
    return {
        username: "",
        pwd: ""
    };
  },
  methods:{
    handleLogin(){
        // 模仿后端读取到的数据
        setTimeout(()=>{
            let data = {
                username: this.username
            };
            localStorage.setItem("user",JSON.stringify(data));
            this.$router.push({path: this.$route.query.redirect})// 设置登录后走的重定向路由
        },1000)
    }
  }
}

</script>
<style scoped>
</style>

因此设置登录权限的时候只需要在相关的路由后面加上相关的meta即可。由登录组件进行授权,全局守卫进行全部路由的验证。

7.11.3 数据获取

有时候,进⼊某个路由后,需要从服务器获取数据。例如,在渲染⽤户信息时,你需要从服务器获取⽤户的数据。我们可以通过两种⽅式

来实现:

  • 导航完成之后获取:先完成导航,然后在接下来的组件⽣命周期钩⼦中(一般是created)获取数据。在数据获取期间显示加载中之类的指示;通常采用的是这种方式进行数据的获取加载。
  • 导航完成之前获取:导航完成前,在路由进⼊的守卫中获取数据,在数据获取成功后执⾏导航。

导航完成后获取

当我们使用这种方式时,我们会马上导航和渲染组件,然后在组件的 created 钩⼦中获取数据。这让我们有机会在数据获取期间展示⼀

loading 状态,还可以在不同视图间展示不同的 loading 状态。

<template>
  <div>
    <div v-if="loading" class="loading">Loading...</div>
    <div v-if="error" class="error">{{ error }}</div> 
    <div v-if="post" class="content"> 
         <h2>{{ post.title }}</h2> 
         <p>{{ post.body }}</p>
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
        loading: false,
        post: null,
        error: null
    };
  },
  // 组件创建完后获取数据,此时 data 已经开始被监视了
  created(){
    // 如果路由有变化,会在此执行该方法
    this.fetchData()
  },
  watch:{
    $route: 'fetchData'
  },
  methods:{
    fetchData(){
        this.error = this.post = null;
        this.loading = true;
        // 向后端请求数据
        // this.$http.get('/api/post')
        // .then((result) => {
        // this.loading = false; 成功的话将loading 设置为false 即取消加载项;
        // this.post = result.data;// 将请求到的数据加载到组件中去
        // }).catch((err) => {
        // this.error = err.toString();// 加载失败将,错误信息加载到组件界面上去
        // });
    }
}
}

</script>
<style scoped>
</style>

8.Vuex

Vuex 是一个专为vue.js应用开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以响应的规则保证状态以一种可预测的方式发生变化

8.1 安装

vue add vuex

这种安装的方式也会像上述安装其他插件一样产生相关的文件,进行覆盖;

另外一种方式则是在我们开发项目的时候直接在创建的时候选中vuex选项,则会帮我创建好相对应的文件以及导入挂载等操作;

8.2 简单使用以及介绍

文件变化,在src目录下面产生了相关的store/index.js文件;

import Vue from 'vue'// 导入 vue
import Vuex from 'vuex'// 导入 vuex

Vue.use(Vuex)// 使用

// 导出相关的函数对象
export default new Vuex.Store({
  // 数据状态的存储位置
  state: {
  },
  // 类似于 state 的计算属性,映射到对应的属性上进行一些操作
  getters: {
  },
  // 同步事件的回调函数
  mutations: {
  },
  // 异步事件的回调函数.
  actions: {
  },
  // 模块,模块的局部状态对象;
  modules: {
  }
})

main.js中的变化

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'// 导入挂载好的vuex
import './plugins/element.js'
import './plugins/axios.js'

Vue.config.productionTip = false

new Vue({
  router,
  store,// 挂载 vuex
  render: h => h(App)
}).$mount('#app')

简单案例

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h2>count:{{ count }}</h2>
    <el-button type="primary" @click="add">增  加</el-button>
    <el-button type="warning" @click="desc">减  少</el-button>
  </div>
</template>
<script>
export default{
  computed:{
    count(){
      return this.$store.state.count
    }
  },
  methods:{
    add(){
      // 调用相关的 同步函数进行处理
      // alert("开始执行"),同步调用的时候直接使用 commit 将函数的名称进行提交;
      // 如果是异步的情况的话,使用 dispatch 方法进行异步处理;
      this.$store.commit('add')
    },
    desc(){
      // 同步调用方式,未传参;
      this.$store.commit('desc')
    }// 对比上述两个函数就不难发现,两个函数除了名称不同,其余内容全部相同,那么有没有办法进行抽离一下呢?
  }

}

</script>

store/index.js中进行相关的编写

import Vue from 'vue'// 导入 vue
import Vuex from 'vuex'// 导入 vuex

Vue.use(Vuex)// 使用

// 导出相关的函数对象
export default new Vuex.Store({
  // 数据状态的存储位置
  state: {
    // 存放相关的数据信息,项目中经常存放 username & token 信息
    count: 0,
    msg: "我是过来充数的"

  },
  // 类似于 state 的计算属性,映射到对应的属性上进行一些操作
  getters: {
    // 类似计算属性,主要是针对 state 中值的计算属性,例如判断 count 是奇数偶数
    evenOrOdd:(state)=>{
      // 可以使用箭头函数变得简洁
      return state.count % 2 === 0 ? '偶数' : '奇数'
    }
  },
  // 同步事件的回调函数
  mutations: {
    add(state){// 需要传入固定的参数 state
      state.count++// 执行自增语句
    },
    desc(state){
      state.count--
    }
  },
  // 异步事件的回调函数.
  actions: {
    /*异步请求公共数据的时候进行调用*/
  },
  // 模块,模块的局部状态对象;
  modules: {
  }
})

8.3 辅助函数的使用

当⼀个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使⽤mapState 辅助

函数帮助我们⽣成计算属性,增加开发的效率;

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h2>count:{{ count }}</h2>
    <el-button type="primary" @click="add">增  加</el-button>
    <el-button type="warning" @click="desc">减  少</el-button>
  </div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default{
  data(){
    return{
      num: 10
    }
  },
  computed:mapState({
    // 方式一:使用箭头函数
    // count: state => state.count
    // 方式二:使用字符串
    // count: "count" 
    // count
    // 我们发现上面的 键和值都是 count 因此我们可以利用 es6 语法的新特性只写一个也可以
    // 方式三: 为了使用 this 获取局部状态,必须使用常规函数
    count(state){
      return state.count + this.num// 此时界面上的初始值应该是 10
    }
  }),
  methods:{
      //...
  }

}

</script>

对象展开运算符的运用

这种写法依赖于es6语法;mapState函数返回是一个对象。我们如何将它与局部计算属性混合使用呢?我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给commputed属性。但是使用对象扩展运算符,可以极大的简化写法.

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h2>count:{{ count }}:{{evenOrOdd}}</h2>
    <h3>{{msg}}</h3>
    <el-button type="primary" @click="add">增  加</el-button>
    <el-button type="warning" @click="desc">减  少</el-button>
  </div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState, mapGetters,mapMutations } from 'vuex'

export default{
  data(){
    return{
      // num: 10
    }
  },
  computed:{
    // 使用对象运算扩展符
    ...mapState(['count','msg']),// 当有多个的时候直接在数组中及逆行添加;
    ...mapGetters(['evenOrOdd'])
  },
  methods:{
    // add(){
      // 调用相关的 同步函数进行处理
      // alert("开始执行"),同步调用的时候直接使用 commit 将函数的名称进行提交; 如果是异步的情况的话,使用 dispatch 方法进行异步处理;
      // this.$store.commit('add')
    // },
    // desc(){
      // 同步调用方式,未传参;
      // this.$store.commit('desc')
    // }// 对比上述两个函数就不难发现,两个函数除了名称不同,其余内容全部相同,那么有没有办法进行抽离一下呢?
    // vueX 中为我们提供了相关的辅助函数
    ...mapMutations(['add','desc'])// 以上的两个函数,可以使用一句话进行替代,仅限于不传参的情况下,
  }

}

</script>

注:其他的对象也有相关的计算属性,只是将相关的名称做一下修改,但是同步函数和异步函数的稍有不同,当他们不传参数的时候与原来的相同,传参数的时候就不能使用对象扩展运算符了,无法将相关的参数进行传输;

提交函数的时候:

更改 Vuex 的 store 中的状态的唯⼀⽅法是提交 mutation。Vuex 中 的 mutation ⾮常类似于事件:每个 mutation 都有⼀个字符串的

件类型 (type) 和 ⼀个 回调函数 (handler)。这个回调函数就是我们实际进⾏状态更改的地⽅,并且它会接受 state 作为第⼀个参数;

你可以在组件中使⽤ this.$store.commit('xxx') 提交 mutation,或者使⽤ mapMutations 辅助函数将组件中的 methods 映射

store.commit 调⽤(需要在根节点注⼊ store );示例见上方;

Action

  • Action 类似于 mutation,不同在于:
  • Action 提交的是 mutation,⽽不是直接变更状态。
  • Action 可以包含任意异步操作
import { mapMutations } from 'vuex'
export default {
     // ...
     methods: {
      ...mapActions('counter',[// conunter 是与函数相关的 state 值;
      'incrementAsync'// store/index.js 中 action中的函数名称
     ])
    }
}

说明:action一般用于异步请求公共数据的时候,很少使用不穿参数的形式;了解即可;

补充:actions 使用示例

actions: {
 //获取所有商品的⽅法
 getAllProducts({ commit }) {
  Axios.get('/api/products').then(res => {
   console.log(res.data.products);
            commit('setProducts',res.data.products)
  }).catch(err => {
   console.log(err);
  })
 }
}

// 某组件中的created
created(){
    // products 表明是命名空间的名称,是子组件的名称;
    this.$store.dispatch("products/getAllProducts");
}

8.4 Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象;当应用变得复杂的时候,Store对象就会变得相对臃肿。

为了解决相关的问题,Vuex允许我们将store分割成模块(module);每个模块拥有⾃⼰的 state、mutation、action、

getter、甚⾄是嵌套⼦模块——从上⾄下进⾏同样⽅式的分割.

做⼀个购物⻋案例有两个模块 cart products;在store⽂件夹下创建modules文件夹,在它的下面创建cart.js & products.js

如果希望你的模块具有更⾼的封装度和复⽤性,你可以通过添加 namespaced: true 的⽅式使其成为带命名空间的模块;当模块被注册后,它的所有 getteractionmutation 都会⾃动根据模块注册的路径调整命名。

import Vue from 'vue'// 导入 vue
import Vuex from 'vuex'// 导入 vuex

Vue.use(Vuex)// 使用
// 导入创建好的模块
import cart from './modules/cart'
import product from './modules/product'

// 导出相关的函数对象
export default new Vuex.Store({
  // 数据状态的存储位置
  state: {
    // 存放相关的数据信息,项目中经常存放 username & token 信息
    count: 0,
    msg: "我是过来充数的"

  },
  // 类似于 state 的计算属性,映射到对应的属性上进行一些操作
  getters: {
    // 类似计算属性,主要是针对 state 中值的计算属性,例如判断 count 是奇数偶数
    evenOrOdd:(state)=>{
      // 可以使用箭头函数变得简洁
      return state.count % 2 === 0 ? '偶数' : '奇数'
    }
  },
  // 同步事件的回调函数
  mutations: {
    add(state){// 需要传入固定的参数 state
      state.count++// 执行自增语句
    },
    desc(state){
      state.count--
    }
  },
  // 异步事件的回调函数.
  actions: {
    /*异步请求公共数据的时候进行调用*/
  },
  // 模块,模块的局部状态对象;
  modules: {
    // 将创建好的子模块在这里进行注册使用;  
    cart,
    product
  }
})

8.5 插件

⽇志插件,Vuex ⾃带⼀个⽇志插件⽤于⼀般的调试:

import createLogger from 'vuex/dist/logger'
const store = new Vuex.Store({
 plugins: [createLogger({
  collapsed: false, // ⾃动展开记录的 mutation
 })]
})

要注意,logger 插件会⽣成状态快照,所以仅在开发环境使⽤。使用较少,了解即可

8.6 总结

什么情况下我应该使⽤ Vuex

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和⻓期效益进⾏权衡。如果您不打算开发⼤型单⻚应⽤,使⽤ Vuex 可能是繁琐冗余的。确实是如此——如果您的应⽤够简单,您最好不要使⽤ Vuex。⼀个简单的store模式就⾜够您所需了。但是,如果您需要构建⼀个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex将会成为⾃然⽽然的选择。引⽤ Redux 的作者 Dan Abramov 的话说就是:

Flux 架构就像眼镜:您⾃会知道什么时候需要它

9.全局总结

vue-cli开发的大致流程了解了一遍,下面我们需要做两个项目,之后在了解一下的他的底层代码,之后学习一些React.js有备无患;

posted @ 2022-11-22 18:08  紫青宝剑  阅读(299)  评论(0编辑  收藏  举报