Vue3 学习日志

一、ES6 模块化与异步编程高级用法

1、ES6 模块化

还有AMD、CMD或CommonJS模块化标准

① 确保安装了 v14.15.1 或者更高版本的 node.jd

② 在 package.json 的根节点中添加 “type": ”module" 节点

三种语法

  1. 默认导出与默认导入

export default 默认导出的成员

注意:只能使用唯一的 export default

import 接收名称 from ‘模块标识符’

注意:接受名称要合法

  1. 按需导出与按需导入

export 按需导出的成员

import { 成员名 as 自定义名 } from ‘模块标识符’

注意:导出可以用多次;成员名称要一致;导入可以使用as;可以和默认导入一起使用,只需要在导入前边自定义一个变量即可

  1. 直接导入并执行模块中的代码

import ‘模块标识符’

2、Promise

2.1 回调地狱

多层回调函数的相互嵌套

缺点

  • 代码耦合性太强,牵一发动全身,难以维护
  • 大量冗余的代码相互嵌套,可读性变差
2.2 Promise 的基本概念

① Promise 是一个构造函数

  • 可以构建 Promise 实例 const p = new Promise()
  • new 出来的 Promise 实例对象,代表异步操作

② Promise.prototype 上包含一个 .then() 方法

  • 每一次 new Promise() 构造函数得到的实例对象,
  • 都可以通过原型链的方式访问到 .then() 方法,例如:p.then()

③ .then() 方法用来预先指定成功和失败的回调函数

  • p.then(成功的回调函数,失败的回调函数)
  • p.then(result => {}. error => {})
  • 成功回调函数必选,失败的回调函数是可选参数
2.3 .then() 方法

由于node.js fs 模块只支持回调函数形式读文件,所以要安装

npm i then-fs

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X6p81oGt-1688719511764)(D:\PerfectCode\前端学习\VUE\Vue3\images\then-fs.PNG)]

**.then() **特性

如果上一个 .then() 方法中返回了一个新的 Promise 实例对象,则可以通过下一个 .then() 继续处理。通过 .then() 方法链式调用,就解决了回调地狱的问题

import thenFs from 'then-fs'

thenFs.readFile('./file/1.txt', 'utf-8').then(r1 => {
  console.log(r1);
  return thenFs.readFile('./file/2.txt', 'utf-8')
}).then(r2 => {
  console.log(r2);
  return thenFs.readFile('./file/3.txt', 'utf-8')
}).then(r3 => {
  console.log(r3);
})

在 Promise 的链式操作中如果发生了错误,可以使用 Promise.prototype**.catch** 方法进行捕获和处理:

如果不希望前面的错误导致后续的 .then 无法正常执行,则可以将 .catch 的调用提前```

import thenFs from 'then-fs'

thenFs.readFile('./file/11.txt', 'utf-8').catch(err => {
  console.log(err.message);
}).then(r1 => {
  console.log(r1);
  return thenFs.readFile('./file/2.txt', 'utf-8')
}).then(r2 => {
  console.log(r2);
  return thenFs.readFile('./file/3.txt', 'utf-8')
}).then(r3 => {
  console.log(r3);
})
// .catch(err => {
//   console.log(err.message);
// })

Promise.all() 方法会发起并行的 Promise 异步操作,等 所有的异步操作全部结束后 才会执行下一步的 .then 操作(等待机制)。

Promise.race() 方法会发起并行的 Promise 异步操作,只要任何一个异步操作完成,就立即执行下一步的 .then 操作(赛跑机制)

import thenFs from 'then-fs'

const promiseArr = [
  thenFs.readFile('./file/3.txt', 'utf-8'),
  thenFs.readFile('./file/2.txt', 'utf-8'),
  thenFs.readFile('./file/1.txt', 'utf-8')
]

// Promise.all(promiseArr).then(([r1, r2, r3]) => { // 结果是一个数组
//   console.log(r1, r2, r3);
// }).catch(err => {
//   console.log(err.message);
// })

Promise.race(promiseArr).then(result => {
  console.log(result);
}).catch(err => {
  console.log(err.message);
})
2.4 基于 Promise 封装读文件的方法

如果想要创建具体的异步操作,则需要在 new Promise() 构造函数期间,传递一个 function 函数,将具体的异步操作定义到 function 函数内部

import fs from 'fs'
// 1. 方法名称为 getFile
// 2. 方法参数为 fpath, 读取文件的路径
function getFile(fpath) {
  // 3. 返回一个 Promise 对象
  return new Promise(function () {
    // 4. 具体的读文件的异步操作
    fs.readFile(fpath, 'utf-8', (err, dataStr) => { })
  })
}

通过 .then() 指定的 成功 和 失败 的回调函数,可以在 function 的形参中进行接收

封装自己的读文件Promise

Promise 异步操作的结果,可以调用 resolve 或 reject 回调函数进行处理

import fs from 'fs'
// 1. 方法名称为 getFile
// 2. 方法参数为 fpath, 读取文件的路径
function getFile(fpath) {
  // 3. 返回一个 Promise 对象
  return new Promise(function (resolve, reject) {
    // 4. 具体的读文件的异步操作
    fs.readFile(fpath, 'utf-8', (err, dataStr) => {
      if (err) return reject(err)
      resolve(dataStr)
    })
  })
}

// getFile('./file/1.txt').then(r1 => { console.log(r1); }, err => { console.log(err.message); })
getFile('./file/21.txt').then(r1 => { console.log(r1); }).catch(err => { console.log(err.message); })

3、async/await

ES8 简化 Promise 异步操作 ,解决 .then 链式调用代码冗余、阅读性差、不易理解的缺点

① 如果 function 中使用了 await ,则 function 必须被 async 修饰

② 在 async 方法中,第一个 await 之前的代码会同步执行,await 之后的代码会异步执行

import thenFs from 'then-fs'

console.log('A');
async function getAllFile() {
  console.log('B');
  const f1 = await thenFs.readFile('./file/1.txt', 'utf-8')
  console.log(f1);
  const f2 = await thenFs.readFile('./file/2.txt', 'utf-8')
  console.log(f2);
  const f3 = await thenFs.readFile('./file/3.txt', 'utf-8')
  console.log(f3);
  console.log('D');
}

getAllFile()
console.log('C');

4、EventLoop

JavaScript 单线程语言

问题:前一个任务耗时,程序假死

4.1 同步任务和异步任务

① 同步任务(synchronous)

  • 非耗时任务,主线程上排队执行
  • 前一个执行完,才执行后一个

② 异步任务 (asynchronous)

  • 好耗时任务,js 委托给宿主环境执行
  • 异步任务执行完成后,会通知 js主线程执行异步任务的回调函数
4.2 同步任务和异步任务的执行过程

同步异步执行过程.PNG

同步异步面试题.PNG

5、宏任务和微任务

① 宏任务(macrotask)

  • 异步 Ajax 请求
  • setTimeout、setinterval
  • 文件操作
  • 其它宏任务

② 微任务 (microtask)

  • Promise.then、.catch 和 .finally
  • process.nextTick
  • 其它微任务

Js任务.PNG

5.1 宏任务和微任务的执行顺序

宏任务微任务执行顺序.PNG

5.2 经典面试题

\宏任务微任务面试题

6、API接口案例

要配置 package.json 中的 ES6 语法模块 “type": ”module"

安装相关模块 npm i express@4.17.1 mysql2@2.2.5

6.1 app.js
import express from 'express'
import userRouter from './router/user_router.js'

const app = express()

// 配置解析表单 get 请求体数据的路由中间件
app.use('/api', userRouter)

app.listen(3000, () => {
  console.log('server running at port http://127.0.0.1:3000')
})
6.2 user_router.js
import express from 'express'
import { getAllUser } from '../controller/user_ctrl.js'

// 创建路由对象
const router = new express.Router()
// 挂载路由规则
router.get('/user', getAllUser)

// ES6导出模块
export default router
6.3 user_ctrl.js
import db from '../db/index.js'

// 导出获取用户列表的处理函数
export async function getAllUser(req, res) {
  try {
    // db.query() 函数的返回值是 Promise 的实例对象
    const [rows] = await db.query('select id, username, nickname, xxx from ev_users')
    res.send({
      status: 0,
      message: '获取用户列表成功',
      data: rows
    })
  } catch (e) {
    res.send({
      status: 1,
      message: '获取用户列表失败',
      desc: e.message
    })
  }
}
6.4 index.js
import mysql from 'mysql2'

const pool = mysql.createPool({
  host: 'localhost',
  port: 3306,
  database: 'node_db_01',
  user: 'root',
  password: '123456'
})

// 导出一个 promise 对象
export default pool.promise()

二、前端工程化与 webpack

1、前端工程化

前端工程化.PNG

好处

① “自成体系”,覆盖前端项目从创建到部署的方方面面

② 最大程度地提高了前端的开发效率,降低了技术造型、前后端联调等带来的协调沟通成本

前端工程化解决方案

早期有:

  • grunt ( https://www.gruntjs.net/ )
  • gulp ( https://www.gulpjs.com.cn/ )

目前主流:

  • webpack ( https://www.webpackjs.com/ )
  • parcel ( https://zh.parceljs.org/ )

2、webpack 的基本使用

webpack.PNG

2.1 webpack 配置

安装 webpack 相关包

npm i webpack@5.5.1 webpack-cli@4.2.0 -D

webpack的mode可选值.PNG

entry:自定义打包入口

output:自定义打包的出口

npm init -y // 安装package.json配置文件
npm i jquery -S // 安装jquery

import $ from 'jquery' // 使用 ES6 导入语法,在自定义index.js导入 jQuery

npm i webpack webpack-cli -D // 在开发环境 安装webpack和webpack-cli

// 新建webpack.config.js文件,使用 Node.js 导出语法,向外导出一个 webpack 的配置对象
module.exports = {
	entry: path.join(__dirname, './src/index.js'), // 指定要处理的文件
    mode: 'development' // mode 用来指定构建模式,可选参数有 development 和 production
    output: {
        path: path.join(__dirname, './dist'), // 输出文件的存放路径
        filename: 'js/main.js' // 指定输出文件的名称
    }
}

// 在package.json配置文件中配置
"scripts": {   
  "dev": "webpack"
 }
 
 npm run dev  // 生成dist文件夹
 
 // 在index.html中调用 dist/main.js
 <script src="../dist/main.js"></script>
2.2 webpack.config.js 文件的作用

webpack.config.js 是 webpack 的配置文件。webpack 在打包前会读取这个文件,进行配置

注意:用 node.js 开发出来的,支持 node.js语法

3、webpack 中的插件

3.1 webpack 插件

webpack插件

npm i webpack-dev-server -D  // 安装实时打包http服务
// 在package.json配置文件中修改配置
"scripts": {   
  "dev": "webpack serve"
 }
// 需要用 http://127.0.0.1:8080 访问
// 在index.html中调用根目录中的内存中的 ./main.js
<script src="./main.js"></script>

npm i html-webpack-plugin -D // 安装 html-webpack-plugin 不仅可以在内存中复制 html 还可以自动添加内存中的 js 脚本文件
// 底部自动导入 js ,不需要再主动导入js了

// 在webpack.config.js文件导入 html-webpack-plugin 这个插件,得到构造函数
const HtmlPlugin = require('html-webpack-plugin')

// new 一个构造函数,创建插件实例对象
const htmlplugin = new HtmlPlugin({
    // 指定要复制到哪个页面
    template: './src/index.html',
    // 指定复制出来的文件名和存放路径,放到内存中
    filename: './index.html'
})

// 使用 Node.js 导出语法,向外导出一个 webpack 的配置对象
module.exports = {
    entry: path.join(__dirname, './src/index.js'), // 指定要处理的文件 如果是 ./ ,则需配置 /
    mode: 'development', // mode 用来指定构建模式,可选参数有 development 和 production
    output: {
        path: path.join(__dirname, './dist'), // 输出文件的存放路径 如果是 ./ ,则需配置 /
        filename: 'boundle.js' // 指定输出文件的名称
    },
    devServer: {
        // contentBase: __dirname, -- 请注意,这种写法已弃用
        // 配置 /
        /* static: {
          directory: path.join(__dirname, "/"),
        }, */
        open: true, // 初次打包完成后,自动打开浏览器
        host: '127.0.0.1', // 实时打包所用的主机地址
        port: 9000, // 在 http 协议中只有 80 端口可以被省略
    },
    plugins: [htmlplugin] // 插件的数组,将来 webpack 在运行时,会加载并调用这些插件
}

在使用 webpack 的项目自动打包工具 webpack-dev-server 时,访问不到页面的解决方案参考:(29条消息) 【解决使用webpack自动打包功能 ,报错 Content not from webpack is served from ‘ ‘ 且访问http://localhost:8080/ 为空 问题 】_lt012345的博客-CSDN博客

3.2 实时打包生成文件放到了内存里(缓存)

内存(缓存).PNG

4、webpack 中的 loader

webpack的loader

webpack 处理流程

webpack处理流程

npm i style-loader css-loader -D // 安装css处理需要的loader
npm i less-loader less -D // 安装less处理需要的loader less是依赖项,不需要配置

// 使用 Node.js 导出语法,向外导出一个 webpack 的配置对象
module.exports = {
	// 在webpack-config-js 配置文件中配置 module -> rules 数组
    module: { // 所有第三方文件模块的匹配机制
        rules: [ // 文件后缀名的匹配规则, \ 表示转义为文件
            { test: /\.css$/, use: ['style-loader', 'css-loader'] } // use 调用不能乱序,从后向前调用
            { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']}
        ]
    }
}
4.1 base64 图片处理

除了可以用 base64 把小图片转化直接请求(体积会变大),还可以用精灵图定位的方式

npm i url-loader file-loader -D // 安装图片处理的loader, file-loader 是依赖项,不需要配置

module.exports = {
	// 在webpack-config-js 配置文件中配置 module -> rules 数组
    module: {
        rules: [
            // 文件后缀名的匹配规则
            { test: /\.jpg|png|gif$/, use: 'url-loader?limit=1000&outputPath=img' } // limit用来指定图片的大小,只有小于等于limit大小才会被转码为 base64 格式,其余图片不会转化; outputPath 指定超出base64格式图片输出路径
        ]
    }
4.2 高级语法

webpack 只能处理一部分高级语法,别的还需要加载相应的 loader

// 定义一个装饰器函数
function info(target) {
    target.info = 'Person info'
}

// 定义一个普通的类
@info
class Person{}

console.log(Person.info);


npm i babel-loader @babel/core @babel/plugin-proposal-class-properties -D // 安装加载高级语法的loader


// 在webpack-config-js 配置文件中配置 module -> rules 数组
module.exports = {
    module: {
        rules: [
            // 文件后缀名的匹配规则
            { test: /\.js$/, use: 'babel-loader', exclude: '/node_modules' } // 必须指定 exclude 指定排除项:因为 node_modules 目录下的第三方包不需要被打包
        ]
    }

// 在根目录下新建 babel.config.js 文件,
module.exports = {
    // 声明 babel 可用插件
    // 将来 webpack 再调用 babel-loader 的时候,会先加载 plugin 插件来使用
    "plugins": [
        ["@babel/plugin-proposal-class-decorators", { legacy: true }]
    ]
}            

5、打包发布

"scripts": {
    "dev": "webpack serve", // 开发环境中,运行 dev 命令
    "build": "webpack --mode production" // 项目发布时,运行 build 命令
}

// 运行打包
npm run build


// 安装插件每次发布都自动删除之前的 dist 的 插件 
npm install --save-dev clean-webpack-plugin
// 在webpack.config.js中配置
// 导入包 clean-webpack-plugin 这个插件,得到构造函数,自动删除 dist
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// 使用 Node.js 导出语法,向外导出一个 webpack 的配置对象
module.exports = {
        plugins: [htmlplugin, new CleanWebpackPlugin()], // 插件的数组,将来 webpack 在运行时,会加载并调用这些插件
}

6、Source Map

Source Map

​ 会生成 .map 文件

Source Map问题

Source Map问题解决

module.exports = {
    mode: 'development' // mode 用来指定构建模式,可选参数有 development 和 production
    // eval-source-map 仅限在开发模式下使用,不建议在生产模式下使用
    // devtool: 'eval-source-map', // 此选项生成的 Source Map 能够保证 运行时报错的行数 与 源代码的行数一致 暴露源码
    devtool: 'nosources-source-map', // 此选项生成的 Source Map 能够保证 运行时报错的行数 与 源代码的行数一致 不暴露源码
}

7、 拓展

配置 @ 符号

// 在webpack-config-js 配置文件中配置 @ 符号
module.exports = {
    resolve: {
        alias: {
            // 配置 @ 符号表示的目录
            "@": path.join(__dirname, './src/')
        }
    }
}


// msg.js 文件
export default {
    msg: 'hello Vue'
}

// info.js 文件就可以使用
// import msg from '../../msg'
// 建议使用 @ 表示 src 源代码目录,从外往里找,不要使用 ../ 从里向外找
// @ 需要配置
import msg from '@/msg.js'
console.log(msg);

在 Chrome 中安装配置 vue.js.detools

可参考博客:https://blog.csdn.net/weixin_43508199/article/details/123005570

三、组件基础

1、 单页面应用程序

一个 Web 网站只有唯一一个 HTML 页面

特点

① 仅在该 web 页面初始化时加载相应的资源

② 不会因为用户操作进行页面的重新加载或跳转,而是利用 JavaScript 动态地变换 HTML 的内容

SPA优点.PNG

SPA缺点.PNG

创建 vue 的 SPA 项目

vitevue-cli
支持的 vue 版本仅支持 vue 3.x支持 vue2.x 和 vue3.x
是否基于 webpack
运行速度较慢
功能完整度小而巧(逐渐完善)大而全
是否建议在企业级开发中使用目前不建议建议在企业级开发中使用

2、vite 的基本使用

2.1创建
npm init vite-app 项目名称
cd 项目
npm install
npm run dev
2.2 目录结构

vite目录.PNG

vite的src.PNG

2.3 vite 项目运行流程

vite运行流程.PNG

main.js

// 1.从 vue 中按需导入 createApp 函数
//    createApp 函数的作用:创建 vue 的‘单页面应用程序实例’
import { createApp } from 'vue'

// 2.导入待渲染的 App 组件
import App from './App.vue'

import './index.css'

// 3.调用 createApp 函数,返回值是‘单页面应用程序的实例’ ,用常量 spa_app 接收
//    同时把 App 组件作为参数传递、渲染到页面中
const spa_app = createApp(App)

// 4.调用 mount 方法,指定 vue 实例要控制的区域,把‘单页面应用程序实例’挂载到页面中
spa_app.mount('#app')

3、组件化开发思想

3.1全局组件和私有组件

和vue2 类似,此处省略

特别的:可以用组件的 name 属性代替 ‘ ’ 中的内容

// spa_app.component('MySwiper', Swiper)
spa_app.component(Swiper.name, Swiper)
3.2 组件命名规则

组件命名规则.PNG

4、组件的基本使用

4.1 scoped和/deep/

组件的style.PNG

4.2 props 的大小写命名和验证

组件中如果采用 camelClass驼峰命名法 声明了 props 属性名称,则有两种方式为其绑定属性

props.PNG

props验证.PNG

也可以以数组的形式写多个类型验证

对象的形式验证

export default {
    props: {
        属性名: {
			// 是否必须
            required: true,
            // 用 type 属性定义数值类型,可以以数组类型写多个
            type: Number,
			// type: [String, Number]
            // 用 default 属性定义属性的默认值
            default: 0
        }
    }
}

自定义验证函数

props验证函数.PNG

4.3 Class 与 Style 绑定

可以通过三元表达式,动态的为元素绑定 class 的类名

<template>
  <div>
    <h1 class="thin" :class="isItalic? 'italic':''">Class&Style</h1>
    <button @click="isItalic = !isItalic">Toggle italic</button>
  </div>
</template>

<script>
export default {
  name: 'MyCS',
  data() {
    return {
      isItalic: true
    }
  },
}
</script>

<style scoped>
.thin {
  color: red;
  font-weight: 200;
}
.italic {
  font-style: italic;
}
</style>

可以用数组语法绑定HTML的class

<template>
  <div>
    <h1 class="thin" :class="[isItalic? 'italic':'',isDelete? 'delete':'']">Class&Style</h1>
    <button @click="isItalic = !isItalic">Toggle italic</button>
    <button @click="isDelete = !isDelete">Toggle delete</button>
  </div>
</template>

<script>
export default {
  name: 'MyCS',
  data() {
    return {
      isItalic: false,
      isDelete: false
    }
  },
}
</script>

<style scoped>
.thin {
  color: red;
  font-weight: 200;
}
.italic {
  font-style: italic;
}
.delete {
  text-decoration: line-through;
}
</style>

对象语法 绑定 HTML 的 class

css对象绑定.PNG

④ 以对象内联样式绑定style样式

css内联对象绑定.PNG

5、自定义事件

在封装组件时,为了让组件的使用者可以监听到组件内状态的变化,此时需要用到组件的自定义事件

声明触发传参自定义事件

export default{
	// 在emits节点下声明自定义事件
	emits: ['自定义事件名称'],
	methods: {
		onBtnClick() {
			this.$emit('自定义事件名称', 参数)
		}
	}
}


<my-counter @自定义事件="处理函数名称"></my-counter>

export default{
	methods: {
        // 接收参数
		处理函数名称(val) {}
	}
}

6、组件上的 v-model

维护组件内外数据的同步

组件v-model作用.PNG

子向父,以及父向子

组件v-model作用传递数据.PNG

<button @click="count++">+1</button>
	APP-----count: {{count}}
<MyCounter v-model:number="count"></MyCounter>

----------------------------------------------

<template>
  <div>
    <p>count 值:{{ number }}</p>
    <button @click="add">+1</button>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  props: ['number'],
  emits: ['update:number'],
  methods: {
    add() {
      this.$emit('update:number', this.number + 1)
    }
  },
}
</script>

7、任务列表案例

npm init vite-app todos
npm install
npm i less -D
7.1 目录

任务列表目录.PNG

7.2 main.js
import { createApp } from 'vue'
import App from './App.vue'

import './assets/css/bootstrap.css'
import './index.css'

createApp(App).mount('#app')
7.3 index.css
:root {
  font-size: 12px;
}
body {
  padding: 8px;
}
7.4 App.vue
<template>
  <div id="app">
    <h1>App 根组件</h1>
    <hr />

    <todo-input @add="onAddNewTask"></todo-input>
    <TodoList :list="todolist" class="mt-2"></TodoList>
    <todo-button v-model:active="activeBtnIndex"></todo-button>
  </div>
</template>

<script>
import TodoList from './components/todo-list/TodoList.vue'
import TodoInput from './components/todo-input/TodoInput.vue'
import TodoButton from './components/todo-button/TodoButton.vue'

export default {
  name: 'MyApp',
  data() {
    return {
      // 任务列表数据
      todolist: [
        { id: 1, task: '吃饭', done: false },
        { id: 2, task: '睡觉', done: false },
        { id: 3, task: '打豆豆', done: true }
      ],
      // 下一个可用的 id
      nextId: 4,
      activeBtnIndex: 0
    }
  },
  methods: {
    // 监听子组件的 add 事件的处理函数
    onAddNewTask(task) {
      // console.log(task)
      // 添加新任务
      this.todolist.push({
        id: this.nextId++, // 先赋值再自增
        task, // 名字一样可以简写
        done: false // 默认未完成
      })
    }
  },
  computed: {
    todolist() {
      switch (this.activeBtnIndex) {
        case 0:
          return this.todolist
        case 1:
          return this.todolist.filter(item => item.done)
        case 2:
          return this.todolist.filter(item => !item.done)
      }
    }
  },
  components: {
    TodoList,
    TodoInput,
    TodoButton
  }
}
</script>

7.5 TodoList.vue

<template>
  <ul class="list-group">
    <!-- 列表组 -->
    <li
      class="list-group-item d-flex justify-content-between align-items-center"
      v-for="item in list"
      :key="item.id"
    >
      <!-- 复选框 -->
      <div class="custom-control custom-checkbox">
        <input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.done" />
        <label
          class="custom-control-label"
          :class="{delete: item.done}"
          :for="item.id"
        >{{ item.task }}</label>
      </div>
      <!-- 徽标 -->
      <span class="badge badge-success badge-pill" v-if="item.done">完成</span>
      <span class="badge badge-warning badge-pill" v-else>未完成</span>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'TodoList',
  props: {
    // 任务列表数据
    list: {
      type: Array,
      required: true,
      default: []
    }
  }
}
</script>

<style lang="less" scoped>
.list-group {
  width: 400px;
}
// 删除效果
.delete {
  text-decoration: line-through;
  color: gray;
  font-style: italic;
}
</style>
7.6 TodoInput.vue
<template>
  <form class="form-inline" @submit.prevent="onFromSubmit">
    <label class="sr-only" for="inlineFormInputGroupUsername2">Username</label>
    <div class="input-group mb-2 mr-sm-2">
      <div class="input-group-prepend">
        <div class="input-group-text">任务</div>
      </div>
      <input
        type="text"
        class="form-control"
        id="inlineFormInputGroupUsername2"
        placeholder="请输入任务信息"
        style="width: 356px;"
        v-model.trim="taskname"
      />
    </div>

    <button type="submit" class="btn btn-primary mb-2">添加新任务</button>
  </form>
</template>

<script>
export default {
  name: 'TodoInput',
  data() {
    return {
      // 任务名称
      taskname: ''
    }
  },
  // 声明自定义事件
  emits: ['add'],
  methods: {
    // 表单提交事件
    onFromSubmit() {
      // 1.判断任务名称是否为空
      if (!this.taskname) return alert('任务名称不能为空!')
      // 2.出发自定义的 add 事件,将任务名称传递给父组件
      this.$emit('add', this.taskname)
      // 3.清空文本框
      this.taskname = ''
    }
  }
}
</script>
7.8 TodoButton.vue
<template>
  <div class="button-container mt-3">
    <div class="btn-group">
      <button
        type="button"
        class="btn"
        :class="active === 0 ? 'btn-primary' : 'btn-secondary'"
        @click="onBtnClick(0)"
      >全部</button>
      <button
        type="button"
        class="btn"
        :class="active === 1 ? 'btn-primary' : 'btn-secondary'"
        @click="onBtnClick(1)"
      >已完成</button>
      <button
        type="button"
        class="btn"
        :class="active === 2 ? 'btn-primary' : 'btn-secondary'"
        @click="onBtnClick(2)"
      >未完成</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'TodoButton',
  emits: ['update:active'],
  props: {
    // 激活项的索引值
    active: {
      type: Number,
      required: true,
      // 默认激活索引值为 0(全部: 0,已完成: 1,未完成: 2)
      default: 0
    }
  },
  methods: {
    onBtnClick(index) {
      // 1.如果当前点击的按钮的索引值等于 props 中的 active,不做任何处理,没必要触发自定义事件
      if (index === this.active) return
      // 2.触发自定义事件修改激活项的索引值
      this.$emit('update:active', index)
    }
  }
}
</script>

<style lang="less" scoped>
.button-container {
  width: 400px;
  text-align: center;
}
</style>

四、组件高级

1、watch 监听器

<template>
  <div>
    <h3>Watch 组件</h3>
    <input type="text" v-model.trim="info.username" />
  </div>
</template>

<script>
import axios from 'axios'
export default {
  name: 'MyWatch',
  data() {
    return {
      username: '',
      info: {
        username: '',
        avatar_url: '',
        html_url: ''
      }
    }
  },
  watch: {
    // async username(newVal, oldVal) {
    //   console.log('newVal: ', newVal)
    //   console.log('oldVal: ', oldVal)
    //   const { res: data } = await axios.get('https://api.github.com/users/' + newVal)
    //   console.log(data)
    // }

    // username: {
    //   async handler(newVal, oldVal) {
    //     const { res: data } = await axios.get('https://api.github.com/users/' + newVal)
    //     console.log(data)
    //   },
    //   // 立即触发 username 的 watch
    //   immediate: true
    // },

    // info: {
    //   async handler(newVal) {
    //     const { data: res } = await axios.get('https://api.github.com/users/' + newVal.username)
    //     console.log(res)
    //   },
    //   // 深度监听
    //   deep: true,
    //   // 立即触发 info 的 watch
    //   immediate: true
    // },

    'info.username': {
      async handler(newVal) {
        const { data: res } = await axios.get('https://api.github.com/users/' + newVal.username)
        console.log(res)
      },
      // 深度监听
      deep: true,
      // 立即触发 info 的 watch
      immediate: true
    }
  }
}
</script>

计算属性与监听器区别.PNG

2、组件的生命周期

从组件创建到执行再到销毁的过程

生命周期

created 生命周期函数,非常有用,一般用来初始化数据;

经常用来调用 methods 中的方法,请求服务器数据;

并且把请求的数据保存到 data 中,供template使用。

但是,组件的模板结构尚未生成。

最早可以在mounted调用操作dom,已经挂载

当数据发生变化后,为了能够够操作最新的dom结构,需要在updated生命周期中操作

在这里插入图片描述

<template>
  <div class="test-template">
    <h3 id="myh3">Test组件 --- {{ books.length }} 本图书</h3>
    <p id="myp">message的值是:{{ message }}</p>
    <button @click="message += '~'">欢迎</button>
  </div>
</template>

<script>
export default {
  props: {
    // 父组件传递的数据
    init: {
      type: String,
      default: "test",
    },
  },
  data() {
    return {
      // 组件内部数据
      msg: "test",
      message: "hello vue",
      books: [], // 定义一个空数组,用于存储从后台获取的图书数据
    };
  },
  methods: {
    // 组件内部方法
    show() {
      console.log("调用了show方法");
    },
    // 使用 Ajax 获取数据
    initBookList() {
      let xhr = new XMLHttpRequest();
      xhr.addEventListener("load", () => {
        let result = JSON.parse(xhr.responseText);
        this.books = result.data;
        console.log(result);
      });
      xhr.open("GET", "http://localhost:3306/book");
      xhr.send();
    },
  },
  //   创建阶段第一个生命周期
  beforeCreate() {
    // console.log(this.init); // 报错,因为此时还没有初始化props和data
    console.log(this.msg); // undefined
    // this.show(); // 报错
  },
  //   创建阶段第二个生命周期
  created() {
    // created 生命周期函数,非常有用,一般用来初始化数据
    // 经常用来调用 methods 中的方法,请求服务器数据
    // 并且把请求的数据保存到 data 中,供template使用
    console.log(this.init); // test
    console.log(this.msg); // test
    this.show(); // 调用了show方法
    this.initBookList();
    let myh3 = document.getElementById("myp");
    console.log(myh3); // null
  },
  // 挂载阶段第一个生命周期
  beforeMount() {
    let myh3 = document.getElementById("myh3");
    console.log(myh3); // null
  },
  // 挂载阶段第二个生命周期,此时组件已经被挂载到页面上了
  mounted() {
    console.log(this.$el);
  },
  // 更新阶段第一个生命周期
  beforeUpdate() {
    console.log("beforeUpdate");
    console.log(this.message);
    let dom = document.getElementById("myp");
    console.log(dom.innerHTML); // 组件的dom结构还是旧的
  },
  // 更新阶段第二个生命周期
  // 当数据发生变化后,为了能够够操作最新的dom结构,需要在updated生命周期中操作
  updated() {
    let dom = document.getElementById("myp");
    console.log(dom.innerHTML); // 组件的dom结构已经更新了
  },
  // 销毁阶段第一个生命周期
  beforeUnmount() {
    console.log("beforeDestroy");
    console.log(this.msg); // 还可以调用数据
  },
  // 销毁阶段第二个生命周期
  mounted() {
    console.log("destroyed"); // 数据已经被销毁了
  },
};
</script>

<style lang="less" scoped>
.test-template {
  width: 100%;
  height: 200px;
  background-color: pink;
  h3 {
    font-size: 1em;
    text-align: center;
    color: #42b983;
  }
}
</style>

3、组件之间的数据共享

3.1 父向子

父向子.PNG

3.2 子向父

子向父.PNG

3.3 父子之间的双向数组同步

组件v-model作用.PNG

<button @click="count++">+1</button>
	APP-----count: {{count}}
<MyCounter v-model:number="count"></MyCounter>

----------------------------------------------

<template>
  <div>
    <p>count 值:{{ number }}</p>
    <button @click="add">+1</button>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  props: ['number'],
  emits: ['update:number'],
  methods: {
    add() {
      this.$emit('update:number', this.number + 1)
    }
  },
}
</script>
3.4 兄弟之间的数据共享

安装mitt包 npm install mitt@2.1.0

兄弟之间.PNG

3.5 后代关系组件之间的数据共享

父节点可以通过 provide 方法,对其 子孙组件 共享数据;子孙组件通过 inject 接收。

export default {
  name: '父组件',
  data() {
    return {
      number: 100
    }
  },
  provide() {
    // 返回要共享的数据对象
    return {
      num: this.number
    }
  }
}

---------
<template>
  <div>
    <h2>One----{{ num }}</h2>
    <MyTwo></MyTwo>
  </div>
</template>

import MyTwo from './Two.vue'
export default {
  name: '子组件',
  inject: ['num'],
  components: {
    孙组件
  },
}

-------
<template>
  <div>
    <h2>Two------{{ num }}</h2>
  </div>
</template>

export default {
  name: 'MyTwo',
  inject: ['num']
}
3.6 父节点对外共享响应式的数据

父节点使用 provide 向下共享数据时,可以结合 computed 函数 向下共享响应式数据。

子孙节点必须以 .value 的形式进行使用。

注意:新版子孙节点不需要以.value的形式进行使用

<template>
  <div>
    <h1>App 根组件----{{ number }}</h1>
    <MyOne></MyOne>
    <button @click="number++">num+1</button>
  </div>
</template>
// 导入 computed
import { computed } from 'vue'
export default {
  name: '父组件',
  data() {
    return {
      number: 100
    }
  },
  provide() {
    // 返回要共享的数据对象
    return {
      num: computed(() => this.number)
    }
  }
}

-------
<template>
  <div>
    <!-- 新版不需要写 num.value -->
    <h2>Two------{{ num }}</h2>
  </div>
</template>

export default {
  name: 'MyTwo',
  inject: ['num']
}
3.6 vuex

终极的组件之间的数据共享方案,他可以使数据共享变得高效、清晰、且易于维护。

vuex.PNG

4、vue 3.x 中全局配置 axios

全局挂载axios.PNG

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import axios from 'axios'

axios.defaults.baseURL = 'http://localhost:3000/api'
createApp(App).config.globalProperties.$http = axios

createApp(App).mount('#app')

5、购物车案例

5.1 目录结构

shopping目录.PNG

5.2 main.js
import { createApp } from 'vue'
import App from './App.vue'
import axios from 'axios'

import './assets/css/bootstrap.css'
import './index.css'

const app = createApp(App)
// 配置请求根路径
axios.defaults.baseURL = 'http://www.escook.cn'
// 将 axios 挂载到 app 上
app.config.globalProperties.$http = axios

app.mount('#app')
5.3 index.css
:root {
  font-size: 12px;
}
li {
  list-style: none;
}
.custom-checkbox .custom-control-label::before {
  border-radius: 1.25rem;
}
5.4 App.vue
<template>
  <div class="app-container">
    <!-- 使用组件 -->
    <my-header title="购物车案例"></my-header>
    <my-goods v-for="item in list" :key="item.id" :id="item.id" :thumb="item.img" :title="item.name" :price="item.price"
      :count="item.count" :checked="item.state" @stateChange="onGoodsStateChange"
      @countChange="onGoodsCountChange"></my-goods>
    <my-footer :isfull="isfull" :total="total" :amount="amount" @fullChange="onFullStateChange"></my-footer>
  </div>
</template>

<script>
// 1. 引入组件
import MyHeader from './components/Header.vue'
import MyGoods from './components/Goods.vue'
import MyFooter from './components/Footer.vue'
// es6导出使用:写js时候都需要携带
export default {
  name: 'MyApp',
  components: {
    MyHeader,
    MyGoods,
    MyFooter
  },
  data() {
    return {
      // 商品列表数据
      goodslist: [],
      // 默认商品列表数据
      list: [
        { id: 1, img: 'https://inews.gtimg.com/newsapp_bt/0/9680744078/1000', name: '小米手机', count: 1, price: 10, state: true },
        { id: 2, img: 'https://inews.gtimg.com/newsapp_bt/0/9680744078/1000', name: '苹果手机', count: 1, price: 30, state: true },
        { id: 3, img: 'https://inews.gtimg.com/newsapp_bt/0/9680744078/1000', name: '娇娇手机', count: 1, price: 60, state: false },
        { id: 4, img: 'https://inews.gtimg.com/newsapp_bt/0/9680744078/1000', name: 'oppo手机', count: 1, price: 120, state: false },
        { id: 5, img: 'https://inews.gtimg.com/newsapp_bt/0/9680744078/1000', name: '华为手机', count: 1, price: 240, state: true },
        { id: 6, img: 'https://inews.gtimg.com/newsapp_bt/0/9680744078/1000', name: '三星手机', count: 1, price: 480, state: false }
      ]
    }
  },
  created() {
    // 调用methods中getGoodsList方法,请求商品列表
    this.getGoodsList()
  },
  methods: {
    // 获取商品列表
    async getGoodsList() {
      const { data: res } = await this.$http.get('/api/cart')
      if (res.status !== 200) return alert('获取商品列表失败!')
      this.goodslist = res.data
    },
    // 监听商品选中状态的改变
    onGoodsStateChange(e) {
      // 遍历商品列表,找到 id 对应的商品,将其选中状态改为 state
      const findResult = this.list.find(item => (item.id === e.id))
      if (findResult) findResult.state = e.value
    },
    // 监听商品数量的改变
    onGoodsCountChange(e) {
      // 遍历商品列表,找到 id 对应的商品,将其数量改为 count
      const findResult = this.list.find(item => (item.id === e.id))
      if (findResult) findResult.count = e.value
    },
    // 监听全选按钮的选中状态
    onFullStateChange(isfull) {
      // console.log(isfull)
      // 遍历商品列表,将每个商品的选中状态改为全选按钮的选中状态
      this.list.forEach(item => {
        item.state = isfull
      })
    }
  },
  computed: {
    // 已勾选商品的总价格
    amount() {
      let a = 0
      this.list.filter(item => item.state).forEach(item => {
        a += item.price * item.count
      })
      return a
    },
    // 已勾选商品的总数量
    total() {
      let t = 0
      this.list.filter(item => item.state).forEach(item => {
        t += item.count
      })
      return t
    },
    // 全选按钮的选中状态
    isfull() {
      return this.list.every(item => item.state)
    }
  }
}
</script>
 
<style lang="less" scoped>
.app-container {
  padding-top: 45px;
  padding-bottom: 50px;
}
</style>
5.5 Header.vue
<template>
  <div class="header-container" :style="{ backgroundColor: bgcolor, color: color, fontSize: fsize + 'px' }">{{ title }}
  </div>
</template>

<script>
export default {
  name: 'MyHeader',
  props: {
    title: {
      type: String,
      default: '购物车'
    },
    bgcolor: {
      type: String,
      default: '#007BFF'
    },
    color: {
      type: String,
      default: '#ffffff'
    },
    fsize: {
      type: String,
      default: '12px'
    }
  }
}
</script>

<style lang="less" scoped>
.header-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 45px;
  line-height: 40px;
  text-align: center;
  line-height: 45px;
  z-index: 999;
}
</style>
5.6 Goods.vue
<template>
  <!-- 内容区域 -->
  <div class="goods-container">
    <!-- 左侧图片区域 -->
    <div class="left">
      <!-- 复选框 -->
      <div class="custom-control custom-checkbox">
        <input type="checkbox" class="custom-control-input" :id="id" :checked="checked" @change="onCheckBoxChange">
        <label class="custom-control-label" :for="id">
          <!-- 商品的缩略图 -->
          <img :src="thumb" alt="商品图片" class="thumb">
        </label>
      </div>
    </div>
    <!-- 右侧信息区域 -->
    <div class="right">
      <!-- 商品名称 -->
      <div class="top">{{ title }}</div>
      <div class="bottom">
        <!-- 商品价格 -->
        <div class="price">¥ {{ price.toFixed(2) }}</div>
        <!-- 商品数量 -->
        <div class="count">
          <!-- 使用 MyCounter组件 -->
          <my-counter :num="count" :min="1" @numChange="getNumber"></my-counter>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import MyCounter from './Counter.vue'
export default {
  name: 'MyGoods',
  props: {
    // 唯一的 key 值
    id: {
      type: [String, Number], // 也可以是字符串
      required: true
    },
    // 商品的图片
    thumb: {
      type: String,
      default: 'https://inews.gtimg.com/newsapp_bt/0/9680744078/1000'
    },
    // 商品的名称
    title: {
      type: String,
      required: true,
      default: '商品名称'
    },
    // 商品的价格
    price: {
      type: Number,
      required: true,
    },
    // 商品的数量
    count: {
      type: Number,
      default: 1,
      required: true,
    },
    // 商品的选中状态
    checked: {
      type: Boolean,
      default: true,
      required: true,
    },
  },
  data() {
    return {
    }
  },
  emits: ['stateChange', 'countChange'],
  methods: {
    // 监听复选框选中状态
    onCheckBoxChange(e) {
      // 通过事件派发,将当前id全选按钮的选中状态传递给父组件
      this.$emit('stateChange', {
        id: this.id,
        value: e.target.checked
      })
    },
    // 监听数量改变
    getNumber(num) {
      console.log(num)
    },
    // 监听数量改变
    getNumber(num) {
      // console.log(num)
      this.$emit('countChange', {
        id: this.id,
        value: num
      })
    }
  },
  components: {
    MyCounter
  }
}
</script>

<style lang="less" scoped>
.goods-container {
  +.goods-container {
    border-top: 1px solid #efefef;
  }

  display: flex;
  padding: 10px;

  // 左侧图片样式
  .left {
    margin-right: 10%;

    // 商品图片样式
    .thumb {
      display: block;
      width: 100px;
      height: 100px;
      background-color: #efefef;
    }
  }

  // 右侧信息样式
  .right {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    flex: 1;

    .top {
      font-weight: bold;
    }

    .bottom {
      display: flex;
      justify-content: space-between;
      align-items: center;

      .price {
        color: red;
        font-weight: bold;
      }
    }
  }
}

.custom-control-label::before,
.custom-control-label::after {
  top: 3.4rem
}
</style>
5.7 Footer.vue
<template>
  <div class="footer-container">
    <!-- 全选区域 -->
    <div class="custom-control custom-checkbox">
      <input type="checkbox" class="custom-control-input" id="fullCheck" :checked="isfull" @change="onCheckBoxChange" />
      <label class="custom-control-label" for="fullCheck">全选</label>
    </div>

    <!-- 合计区域 -->
    <div>
      <span>合计</span>
      <span class="amount">¥{{ amount.toFixed(2) }}</span>
    </div>

    <!-- 结算按钮 -->
    <button type="button" class="btn btn-primary btn-settle" :disabled="total === 0">结算 ({{ total }})</button>
  </div>
</template>
<script>
export default {
  name: 'MyFooter',
  props: {
    // 已勾选商品的总价格
    amount: {
      type: Number,
      default: 0
    },
    // 已勾选商品的总数量
    total: {
      type: Number,
      default: 0
    },
    // 全选按钮的选中状态
    isfull: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      zhuangtai: '',
    }
  },
  emits: ['fullChange'],
  methods: {
    // 监听复选框选中状态
    onCheckBoxChange(e) {
      // console.log(e.target.checked)
      // 通过事件派发,将全选按钮的选中状态传递给父组件
      this.$emit('fullChange', e.target.checked)
    },
  }
}
</script>

<style lang="less" scoped>
.footer-container {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 50px;
  background-color: #fff;
  border-top: 1px solid #efefef;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 10px;
  z-index: 999;
}

.amount {
  font-weight: bold;
  color: red;
}

.btn-settle {
  min-width: 90px;
  height: 38px;
  border-radius: 19px;
}
</style>
5.8 Counter.vue
<template>
  <div class="counter-container">
    <!-- 数量 -1 按钮 -->
    <button type="button" class="btn btn-light btn-sm" @click="onSubClick">-</button>
    <!-- 输入框 -->
    <input type="text" class="form-control form-control-sm ipt-num" v-model.number.lazy="number" />
    <!-- 数量 +1 按钮 -->
    <button type="button" class="btn btn-light btn-sm" @click="onAddClick">+</button>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  props: {
    // 数量
    num: {
      type: Number,
      default: 1,
      required: true
    },
    // 最小值
    min: {
      type: Number,
      default: NaN // 默认值为 NaN ,表示不限制最小值
    }
  },
  data() {
    return {
      number: this.num
    }
  },
  emits: ['numChange'],
  watch: {
    number(newVal) {
      // 取整
      const parseResult = parseInt(newVal)
      // console.log(parseResult);
      // 如果是 NaN 或者小于最小值,就将其设置为最小值
      if (isNaN(parseResult) || parseResult < this.min) {
        this.number = this.min
        return alert('数量不能小于' + this.min)
      }
      // 如果是小数,就将其设置为整数 
      if (String(newVal).indexOf('.') !== -1) {
        this.number = parseResult
        return alert('数量不能为小数')
      }
      // 触发自定义事件,把最新的number值传递给父组件
      this.$emit('numChange', this.number)
    }
  },
  methods: {
    onSubClick() {
      if (this.number <= this.min) return alert('数量不能小于' + this.min)
      this.number--
    },
    onAddClick() {
      this.number++
    }
  }
}
</script>

<style lang="less" scoped>
.counter-container {
  display: flex;

  // 按钮样式
  .btn {
    width: 25px;
  }

  // 输入框的样式
  .ipt-num {
    width: 50px;
    text-align: center;
    margin: 0 4px;
  }
}
</style>

6、 获取组件/元素——refs

ref用来辅助开发者在不依赖于jQuery的情况下,获取DOM元素或组件的引用。

<p ref="mytext">我会被refs获取到</p>
<button @click="refTest">获取mytext,改变其文本</button>

<script>
    export default = {
        methods: {
            refTest(){
                console.log(this.$refs.mytext);
                this.$refs.mytext.innerHTML='我被获取到啦'
            }
        }
    }
</script>

使用ref引用组件实例

【子组件 child.vue】
showTitle(){
  alert('aaa');
}

【父组件 parent.vue】
<child ref="A">流程环节配置</child>
<button @click="B">点我弹出</button>


B(){
    this.$refs.A.showTitle();
},

输入款按钮案例

this.$nextTick(cb)的使用,延迟执行

<template>
  <!-- 组件模板结构只能包含一个div根元素 -->
  <div class="box">
    <hr />
    <input type="text" v-if="inputVisible" @blur="showButton" ref="iptRef" />
    <button v-else @click="showInput">展示输入框</button>
    <hr />
  </div>
</template>
<script>
    export default {
        // data数据源
        // vue中的data必须是一个函数,返回一个对象
        data() {
            return {
            	inputVisible: false, // 是否展示输入框
            }
        },
        methods: {
             showInput() {
                 // 展示输入框
                 this.inputVisible = true;

                 // 获取焦点
                 // 因为组件的更新生命周期是异步的,所以这里获取不到
                 console.log(this.$refs.iptRef); // undefined
                 // this.$refs.iptRef.focus();

                 this.$nextTick(() => {
                     // 通过nextTick获取到更新后的DOM
                     console.log(this.$refs.iptRef); // <input type="text">
                     this.$refs.iptRef.focus();
                 });
            },
            showButton() {
                // 展示按钮
                this.inputVisible = false;
            },
        },
        // updated() {
        //   // 组件更新后执行
        //   console.log(this.$refs.iptRef);
        //   // 因为修改两次数据,所以这里获取到的是第一次更新后的DOM,第二次被隐藏了
        //   this.$refs.iptRef.focus();
        // },
</script>

7、动态组件 & 插槽 & 自定义指令

7.1 动态组件

动态切换组件的显示或者隐藏

component

1. 内置组件占位符
2. is属性值,表示要渲染的组件的名字,加 v-bind 可以动态绑定
<component :is="comName"></component>

keep-alive 保持状态,不被销毁

<keep-alive>
    <component :is="comName"></component>
</keep-alive>

keep-alive 对应的生命周期函数

被缓存:deactivated

被激活:activated

  created() {
    console.log("Left created");
  },
  unmounted() {
    console.log("Left destroyed");
  },
  // 当组件第一次被创建的时候,既会执行 creted 也会执行 activated
  activated() {
    console.log("Left activated");
  },
  deactivated() {
    console.log("Left deactivated");
  },

④ **keep-alive **的 include 属性

表示包括哪些被注册的组件需要被缓存,多个组件可以用英文逗号, 分隔;如果设置了 name 属性,要写 name 属性值。

特别的exclude 表示排除不需要被缓存的组件

注意includeexclude 不能被同时使用

⑥ name属性

export default {
  // 当提供了 name 属性的时候,组件的名称就是 name 值
  // 1.组件的注册名称主要应用于 标签的形式渲染使用
  // 2.name 主要用于<keep-alive>标签实现组件缓存功能;以及在调试工具中看到的组件名称
  name: "MyRight",
};
7.2 插槽(Slot)

vue 为 组件的封装 提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。

<Left>
    <!-- 默认情况下,在使用组件的时候,提供的内容都会被填充到 name="default" 的插槽下 -->
    <!-- 1. 填充到指定名称的插槽,要把 v-slot: name 用在 component 或者 template 标签中 -->
    <!-- 2. template 是一个虚拟的标签,不会被渲染到 html 页面中 -->
    <!-- 3. v-slot: 可以简写为 # -->
    <template v-slot:default>
		<p>left 内容区域</p>
    </template>
</Left>
<keep-alive include="MyRight">
    <component :is="Right" #default2><p>right 内容区域</p></component>
</keep-alive>

具名插槽

<!-- 声明一个插槽 -->
<!-- vue官方规定:每一个 slot 插槽,都要有一个 name 属性(具名插槽);省略默认为 default -->
<!-- 后备内容 -->
<slot name="default">这是 default 插槽的默认内容</slot>

作用域插槽

<Article>
    <template #title>
        <h3>一首诗</h3>
    </template>
    <!-- 直接用 = 建议命名为 scope -->
    <!-- 可以结构赋值 ,{ 属性名 } -->
    <template #content="scope">
        <p>床前明月光</p>
        <p>疑是地上霜</p>
        <p>举头望明月</p>
        <p>低头思故乡</p>
        <p>{{ scope }}</p>
        <p>{{ scope.msg }}</p>
    </template>
    <template #author>
        <div>
            <p>作者:李白</p>
            <p>朝代:唐朝</p>
        </div>
    </template>
</Article>


<template>
  <div class="article-container">
    <!-- 文章的标题 -->
    <div class="header-box">
      <slot name="title"></slot>
    </div>

    <!-- 文章的内容 -->
    <div class="content-box">
      <!-- 既叫具名插槽,也叫作用域插槽 -->
      <!-- 可以自定义属性 -->
      <slot name="content" msg="hello vue!" age="20" :user="userinfo"></slot>
    </div>

    <!-- 文章的作者 -->
    <div class="footer">
      <slot name="author"></slot>
    </div>
  </div>
</template>

独占默认插槽的缩写语法
条件:被提供的内容只有默认插槽时

格式:直接写在组件里

当被提供的内容只有默认插槽时,组件的标签可以被当作插槽的模板来使用,可以把 v-slot 直接用在组件上:

// slotProps可以使用结构赋值
<子组件名 v-slot:default="slotProps">
  {{ slotProps }}
</子组件名>

当被提供的内容只有默认插槽时,可以用 v-slot:default=" ",也可以直接用缩写v-slot=" "
默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确。
只要出现多个插槽,请始终为所有的插槽使用完整的基于 的语法,

一般应用于不确定渲染的样式时候

可以使用插槽来重构前面的购物车案例

App.vue

<template>
  <div class="app-container">
    ...
    <!-- Goods商品列表区域 -->
    <Goods
      v-for="item in carList"
      :key="item.id"
      :id="item.id"
      :title="item.goods_name"
      :count="item.goods_count"
      :pic="item.goods_img"
      :price="item.goods_price"
      :state="item.goods_state"
      @change-state="changeState"
    >
      <!-- <Counter v-slot:default></Counter> -->
      <Counter
        :num="item.goods_count"
        @change-count="getNewNum(item, $event)"
      ></Counter>
    </Goods>
	...
  </div>
</template>

<script>
//...
import Counter from "@/components/Counter/Counter.vue";

export default {
    //...
    // 重构后修改商品数量
    getNewNum(item, e) {
      // console.log(item);
      // console.log(e);
      item.goods_count = e;
    },
  },
  components: {
    Header,
    Goods,
    Footer,
    Counter,
  },
};
</script>

Goods.vue

<template>
	...
    <!-- 右侧内容 -->
    <div class="content">
        <!-- 商品名称 -->
        <h3 class="goods-title">{{title}}</h3>
        <!-- 商品描述 -->
        <p>商品描述</p>
        <!-- 商品价格 -->
        <p class="goods-price">¥ {{ price }}</p>
        <!-- 商品数量 -->
        <!-- <Counter :num="count" :id="id"></Counter> -->
        
        <!-- 插槽 -->
        <slot :id="id"></slot>
  </div>
</template>

Counter.vue

<template>
  <div class="counter-container">
    <button @click="sub">-</button>
    <input type="text" :value="num" readonly />
    <button @click="add">+</button>
  </div>
</template>

<script>
import bus from "@/components/bus.js";
export default {
  porps: {
    // ...
    num: {
      type: Number,
      default: 1,
    },
  },
  methods: {
    add() {
      // console.log(this.id);
      // 调用父组件传递过来的方法,修改商品的数量,不用定义data数据num,直接修改app里面的数据
      // bus.$emit("change-count", {
      //    id: this.id,
      //    value: this.num + 1,
      // });
        
      // 重构后
      this.$emit("change-count", this.num + 1);
    },
    sub() {
      if (this.num <= 1) return;
      // bus.$emit("change-count", {
      //   id: this.id,
      //   value: this.num - 1,
      // });

      // 重构后
      this.$emit("change-count", this.num - 1);
    },
  },
};
</script>
7.3 自定义指令

Vue允许开发者自定义指令

  • 私有自定义指令
  • 全局自定义指令

私有自定义指令

在每个Vue组件中,可以在 directives 节点下声明 ,私有自定义指令

    <!-- v-color 第一次绑定的时候就会立即触发  -->
    <h1 v-color="color">App 根组件</h1>
    <p v-color="'red'">测试</p>


  data() {
    return {
      color: "blue",
    };
  },
directives: {
    color: {
      // 当被绑定的元素插入到 DOM 中时,自动触发 mounted 函数
      mounted(el, binding) {
        el.style.color = binding.value;
      },
  },

mounted 函数只调用一次:第一次绑定到元素的时候调用,当 DOM 更新的时候 bind 函数不会被触发。updated函数会在每次 DOM 更新时候被调用

    <button @click="color = 'green'">改变颜色</button>


  directives: {
   	// 定义名为 color 的指令
    color: {
      // mounted函数,第一次绑定的时候就会触发
      mounted(el, binding) {
        console.log("触发 v-color 的 bind 函数");
        console.log(binding);
        // expression 表示的是指令的表达式,也就是传递给指令的原来值
        el.style.color = binding.value;
      },
      // 在指令所在的组件的 VNode 更新的时候触发
      // 只保证在更新的时候生效,第一次绑定的时候不会触发
      updated(el, binding) {
        console.log("触发 v-color 的 update 函数");
        console.log(binding);
        el.style.color = binding.value;
      },
    },
  },

如果 mounted 和 updated 函数中的逻辑完全相同,则对象格式的自定义指令可以简写成函数格式

directives: {	
  color(el, binding) {
     el.style.color = binding.value;
   },
 },

全局自定义指令

定义在 main.js 中

const app = createApp(App)
// 全局自定义指令
// app.directive('color', {
//   mounted(el, binding) {
//     el.style.color = binding.value
//   },
//   updated(el, binding) {
//     el.style.color = binding.value
//   },
// })
app.directive('color', (el, binding) => {
  el.style.color = binding.value
})
app.mount('#app')

自定义指令.PNG

8、Table 案例

npm init vite-app table-code
cd table-code
npm i
npm i less -D
npm i axios -S
8.1 目录结构

Table.PNG

8.2 main.js
import { createApp } from 'vue'
import App from './App.vue'
import axios from 'axios'

import './assets/css/bootstrap.css'
import './index.css'

const app = createApp(App)

axios.defaults.baseURL = 'http://www.escook.cn'
app.config.globalProperties.$http = axios

app.mount('#app')

8.3 index.css
:root {
  font-size: 12px;
}

body {
  padding: 8px;
}
8.4 App.vue
<template>
  <div>
    <h1>App 根组件</h1>
    <!-- 使用表格组件 -->
    <my-table :data="goodslist">
      <!-- 表格的标题 -->
      <template #header>
        <th>序号</th>
        <th>商品名称</th>
        <th>价格</th>
        <th>标签</th>
        <th>操作</th>
      </template>
      <!-- 表格每行的单元格 -->
      <template #body="{ row, index }">
        <td>{{ index + 1 }}</td>
        <td>{{ row.goods_name }}</td>
        <td>{{ row.goods_price }}</td>
        <td>
          <input type="text" class="form-control form-control-sm form-ipt" v-if="row.inputVisible" v-focus
            v-model.trim="row.inputValue" @blur="onInputConfirm(row)" @keyup.enter="onInputConfirm(row)"
            @keyup.esc="row.inputValue = ''">
          <button type="button" class="btn btn-primary btn-sm" v-else @click="row.inputVisible = true">+Tag</button>
          <!-- 循环渲染标签信息 -->
          <span class="badge badge-warning ml-2" v-for="item in row.tags" :key="item">{{ item }}</span>
        </td>
        <td>
          <button type="button" class="btn btn-danger btn-sm" @click="onRemove(row.id)">删除</button>
        </td>
      </template>
    </my-table>
  </div>
</template>

<script>
import MyTable from './components/MyTable.vue'
export default {
  name: 'MyApp',
  data() {
    return {
      goodslist: [
        { id: 1, goods_name: '苹果', goods_price: 10, tags: ["水果", "新鲜"], inputValue: "", inputVisible: false },
        { id: 2, goods_name: '西蓝花', goods_price: 20, tags: ["蔬菜", "新鲜", "优惠"], inputValue: "", inputVisible: false },
        { id: 3, goods_name: '黄瓜', goods_price: 20, tags: ["蔬菜", "新鲜", "折扣"], inputValue: "", inputVisible: false },
        { id: 4, goods_name: '橘子', goods_price: 30, tags: ["水果", "新鲜"], inputValue: "", inputVisible: false },
        { id: 5, goods_name: '橙子', goods_price: 30, tags: ["水果", "新鲜", "便宜"], inputValue: "", inputVisible: false },
        { id: 6, goods_name: '榴莲', goods_price: 40, tags: ["水果", "新鲜"], inputValue: "", inputVisible: false },
        { id: 7, goods_name: '卷心菜', goods_price: 50, tags: ["蔬菜", "新鲜", "优惠"], inputValue: "", inputVisible: false },
        { id: 8, goods_name: '西红柿', goods_price: 50, tags: ["蔬菜", "新鲜", "优惠"], inputValue: "", inputVisible: false },
      ]
    }
  },
  created() {
    // 发起请求,获取商品列表数据
    this.getGoodsList()
  },
  methods: {
    // 初始化商品列表的数据
    async getGoodsList() {
      // 发起 Ajax 请求
      const { data: res } = await this.$http.get('/api/goods')
      // 请求失败
      if (res.status !== 0) return console.log('获取商品列表失败!')
      // 请求成功
      this.goodslist = res.data
    },
    // 根据删除商品
    onRemove(id) {
      this.goodslist = this.goodslist.filter(item => item.id !== id)
    },
    // 确认输入标签
    onInputConfirm(row) {
      // 获取输入的标签内容
      const val = row.inputValue
      // 清空文本框
      row.inputValue = ''
      // 隐藏文本框
      row.inputVisible = false

      // 判断输入的标签内容是否为空,或者已经存在(include或者indexOf)
      if (!val || row.tags.includes(val)) return alert('标签内容不能为空或者已经存在!')
      // if (!val || row.tags.indexOf(val)) return
      // 将输入的标签内容添加到数组中
      row.tags.push(val)
    }
  },
  directives: {
    focus(el) {
      el.focus()
    }
  },
  components: {
    MyTable
  }
}
</script>

<style lang="less" scoped>
.form-ipt {
  width: 80px;
  display: inline;
}
</style>
8.5 MyTable.vue
<template>
  <table class="table table-bordered table-striped">
    <!-- 表格的标题区域 -->
    <thead>
      <tr>
        <!-- <th>#</th>
        <th>商品名称</th>
        <th>价格</th>
        <th>标签</th>
        <th>操作</th> -->
        <!-- 命名插槽 -->
        <slot name="header"></slot>
      </tr>
    </thead>
    <!-- 表格的主体区域 -->
    <tbody>
      <!-- v-for 渲染数据行 -->
      <tr v-for="(item, index) in data" :key="item.id">
        <!-- 为数据行的 td 预留的作用域插槽 -->
        <slot name="body" :row="item" :index="index"></slot>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  name: 'MyTable',
  props: {
    // 表格的数据源
    data: {
      type: Array,
      required: true,
      default: () => []
    }
  }
}
</script>

五、路由

路由就是对应关系,分为(前端路由、后端路由)

后端路由

后端路由.PNG

SPA 与前端路由

SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。此时,不同组件之间的切换需要通过前端路由来实现。

前端路由也就是 Hash 地址与组件之间的对应关系

1、axios

可以在 main.js 中配置

import Vue from 'vue'
import App from './App.vue'
import axios from 'axios'

Vue.config.productionTip = false

// 全具配置默认数据请求根地址
// 到时候直接访问路径即可,不需要每次都写完整的地址
axios.defaults.baseURL = 'http://www.liulongbing.top:3306'

// 把 axios 挂载到 Vue 的原型中,将来 Vue 的实例对象就可以直接使用 axios
// Vue.prototype.axios = axios
Vue.prototype.$http = axios
// 直接调用 this.$http.get() 即可

// 把 axios 挂载到 Vue 的原型中,有一个缺点,不利于接口的复用

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

Right.vue 中的访问方式

<template>
  <div class="right-container">
    <h3>Right 组件</h3>
    <button @click="postInfo">发起 POST 请求</button>
    <button @click="btnGetBooks">获取图书列表</button>
  </div>
</template>

<script>
// import axios from 'axios'

export default {
  name: 'MyRight',
  methods: {
    async postInfo() {
      const { data: res } = await this.$http.post('/api/post', {
        name: 'zs',
        age: 22
      })
      console.log(res)
    },
    async btnGetBooks() {
      const { data: res } = await this.$http.get('/api/getbooks')
      console.log(res)
    }
  }
}
</script>

<style lang="less" scoped>
.right-container {
  width: 100%;
  height: 200px;
  background-color: rgb(151, 225, 140);
  border-radius: 5px;
  button {
    border-radius: 5px;
    background-color: #fff;
    color: #000;
    border: 1px solid #000;
    cursor: pointer;
  }
}
</style>

2、router

**Hash 地址 **与 组件 之间的 对应关系

url地址里(location.href),‘#’及以后的部分称为哈希地址,可以在控制台用 location.hash 打印哈希地址

2.1 前端路由的概念和原理
  1. 用户点击页面上的路由链接

  2. 导致url地址的Hash值变化

  3. 前端路由监听到Hash地址的变化

  4. 前端路由把当前Hash地址对应的组件渲染到浏览器中

img

<template>
  <div class="app-container">
    <h1>App 根组件</h1>
    <a href="#/home">首页</a>
    <a href="#/movie">电影</a>
    <a href="#/about">关于</a>
    <hr />

    <component :is="comName"></component>
  </div>
</template>

<script>
import Home from './components/Home.vue'
import Movie from './components/Movie.vue'
import About from './components/About.vue'

export default {
  name: 'App',
  data() {
    return {
      comName: 'Home'
    }
  },
  created() {
    // 只要当前 APP 组件被创建吗,就会立即监听 window 的 hashchange 事件
    window.addEventListener('hashchange', () => {
      console.log('hash 改变了' + location.hash)
      // 根据新的 hash 值,来动态的切换组件
      switch (location.hash) {
        case '#/home':
          this.comName = 'Home'
          break
        case '#/movie':
          this.comName = 'Movie'
          break
        case '#/about':
          this.comName = 'About'
          break
        default:
          break
      }
    }
  },
  components: {
    Home,
    Movie,
    About
  }
}
</script>
2.2 vue-router
  • vue-router 3.x 只能结合 vue2 进行使用
  • vue-router 4.x 只能结合 vue3 进行使用

步骤

  1. 第一步:安装vue-router包npm install vue-router@next -S

  2. 第二步:创建路由模块。创建router文件夹,在文件夹下建立index.js

  3. 第三步:

    // 1. 从 vue-router 中导入两个方法
    //  createRouter 创建路由实例
    //  createWebHashHistory 创建路由工作模式(hash模块)
    import { createRouter, createWebHashHistory } from 'vue-router'
    
    // 2. 导入组件
    import Home from '../components/MyHome.vue'
    import About from '../components/MyAbout.vue'
    import Movie from '../components/MyMovie.vue'
    
    // 3.创建路由实例对象
    const router = createRouter({
      // 通过history指定路由工作模式(hash模式)
      history: createWebHashHistory(),
      // 通过 routes 数组,指定路由规则
      routes: [
        // path: hash 地址, component: 组件
        { path: '/', redirect: '/home' },
        { path: '/home', component: Home },
        { path: '/about', component: About },
        { path: '/movie', component: Movie }
      ]
    })
    
    // 4.导出路由实例对象
    export default router
    
  4. 第四步:

    import { createApp } from 'vue'
    import App from './App.vue'
    import './index.css'
    
    // 导入路由模块
    import router from './router'
    
    const app = createApp(App)
    
    // 挂载路由模块
    app.use(router)
    
    app.mount('#app')
    
2.3 vue-router 的高级用法
  1. 默认的高亮 class 类

被激活的链接,默认会应用一个叫做 router-link-active 的类名,可以用来设置样式

/* 在 index.css 全局样式表中重新定义 router-link-active */
.router-link-active {
  background-color: red;
  color: white;
  font-weight: bold;
}
  1. 自定义路由高亮的 class 类

在创建路由实例对象时,开发者可以给予 linkActiveClass 属性,自定义路由链接被激活时所应用的类名

// 3.创建路由实例对象
const router = createRouter({
  // 通过history指定路由工作模式(hash模式)
  history: createWebHashHistory(),
    
  // 通过 linkActiveClass 指定路由高亮的类名
  linkActiveClass: 'active-router',
    
  // 通过 routes 数组,指定路由规则
  routes: [
    // path: hash 地址, component: 组件
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/about', component: About },
    { path: '/movie', component: Movie }
  ]
})

--------

/* 自定义路由高亮的类名 */
.active-router {
  background-color: blue;
  color: white;
  font-weight: bold;
}
2.4 router 例子

app.vue

<template>
  <div class="app-container">
    <h1>App 根组件</h1>

    <!-- vue-router 提供的 router-link 可以替代 a 链接 -->
    <!-- <a href="#/home">首页</a> -->
    <router-link to="/home">首页</router-link>
    <!-- <a href="#/movie">电影</a> -->
    <router-link to="/movie">电影</router-link>
    <!-- <a href="#/about">关于</a> -->
    <router-link to="/about">关于</router-link>
    <hr />

    <!-- <component :is="comName"></component> -->
    <!-- vue-router 提供的占位符 router-view -->
    <router-view></router-view>
  </div>
</template>

main.js

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

// 导入路由模块
import router from './router'

const app = createApp(App)

// 挂载路由模块
app.use(router)

app.mount('#app')

index.js

// 1. 从 vue-router 中导入两个方法
//  createRouter 创建路由实例
//  createWebHashHistory 创建路由工作模式(hash模块)
import { createRouter, createWebHashHistory } from 'vue-router'

// 2. 导入组件
import Home from '../components/MyHome.vue'
import About from '../components/MyAbout.vue'
import Movie from '../components/MyMovie.vue'

// 3.创建路由实例对象
const router = createRouter({
  // 通过history指定路由工作模式(hash模式)
  history: createWebHashHistory(),
  // 通过 routes 数组,指定路由规则
  routes: [
    // path: hash 地址, component: 组件
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/about', component: About },
    { path: '/movie', component: Movie }
  ]
})

// 4.导出路由实例对象
export default router
2.5 嵌套路由

子路由

About.vue

<template>
  <div class="about-container">
    <h3>About 组件</h3>

    <!-- 子集路由链接 -->
    <router-link to="about/tab1">tab1</router-link>
    <router-link to="about/tab2">tab2</router-link>
    <hr />

    <!-- 子集路由占位符 -->
    <router-view></router-view>
  </div>
</template>

index.js

// 1. 从 vue-router 中导入两个方法
//  createRouter 创建路由实例
//  createWebHashHistory 创建路由工作模式(hash模块)
import { createRouter, createWebHashHistory } from 'vue-router'

// 2. 导入组件
import Home from '../components/MyHome.vue'
import About from '../components/MyAbout.vue'
import Movie from '../components/MyMovie.vue'
import Tab1 from '../components/tabs/Tab1.vue'
import Tab2 from '../components/tabs/Tab2.vue'

// 3.创建路由实例对象
const router = createRouter({
  // 通过history指定路由工作模式(hash模式)
  history: createWebHashHistory(),
  // 通过 linkActiveClass 指定路由高亮的类名
  linkActiveClass: 'active-router',
  // 通过 routes 数组,指定路由规则
  routes: [
    // path: hash 地址, component: 组件
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/movie', component: Movie },
    {
      path: '/about',
      component: About,
      // 通过 redirect 重定向,指定默认子路由
      redirect: '/about/tab1',
      // 通过 children 属性,指定子路由规则
      children: [
        // 默认子路由: path 为空字符串
        { path: '', component: Tab1 },
        { path: 'tab1', component: Tab1 },
        { path: 'tab2', component: Tab2 },
      ]
    }
  ]
})

// 4.导出路由实例对象
export default router
2.6 动态路由

可以提供动态参数

<router-link to="/movie/1">电影1</router-link>&nbsp;
<router-link to="/movie/2">电影2</router-link>&nbsp;
<router-link to="/movie/3">电影3</router-link>&nbsp;

------------------

// :id 动态参数
{ path: '/movie/:id', component: Movie },

-------------------

<h2>Movie 组件-----{{ $route.params.id }}</h2>
// :id 动态参数,通过 props 属性,将路由参数映射到组件的 props 中
{ path: '/movie/:id', component: Movie, props: true },

------------------------

<template>
  <div>
    <h2>Movie 组件-----{{ id }}</h2>
  </div>
</template>
export default {
  name: 'MyMovie',
  props: ['id']
}
2.6.1 路由传参

① query传参

  1. 路由跳转并携带query参数,to的字符串写法 messageData是一个变量

    <router-link :to="`/home/news?id=001&message=${messageData}`" ></router-link>
    
  2. 路由跳转并携带query参数,to的对象

    <router-link :to="{
     path:"/home/news",
     query:{
           id:001,
           message:messageData
     	}
     }" >
     </router-link>
    获取参数:this.$route.query.id 、 this.$route.query.message 
    

② params传参
方式一:路由跳转并携带param参数,to的字符串写法 ,首先我们要在路由文件中定义我们要传递的参数

// 1. 路由跳转并携带params参数,to的字符串写法 messageData是一个变量
 <router-link :to="`/home/news/001/${messageData}`" ></router-link> //即{id:001,message:xxx}

跳转时直接斜杠/后面拼接参数

方式二:路由跳转并携带params参数,to的对象写法,不需要在路由文件中定义参数

<router-link :to="{
    name:"HomeNews", //使用params传参时,必须使用name属性进行路由跳转,不能使用path配置项跳转
    params:{
        id:001,
        message:messageData
	}
 }" ></router-link>

获取参数:this.$route.params.id this.$route.params.message

③ 路由props配置
传参配置: src/router/index.js

 {
    name:'HomeNews'
    path:'news/:id/:message',//二级路由,定义参数,表示第一个参数是id,第二个是message
    component:News,
    // 第一种写法:props值为对象,该对象中所有的key-value最终都会通过props传递给组件news
    // props:{a:1},
    // 第二种写法(只能params):props值为Boolean,为true时把路由收到的`params`参数通过props传递给组件news
    // props:true,
    // 第三种写法:props值为函数,该函数返回的对象中每一组key-value都会通过 props 传递给组件 News
   props:function(route){
       return {
           id:route.query.id,
           message:route.query.message
       }
   },
 },

使用: New.vue

export default{
    prors:['id','message']
}
2.6.2 动态路由例子

app.vue

<template>
  <div class="app-container">
    <h1>App 根组件</h1>

    <!-- vue-router 提供的 router-link 可以替代 a 链接 -->
    <!-- <a href="#/home">首页</a> -->
    <router-link to="/home">首页</router-link>
    <!-- <a href="#/movie">电影</a> -->
    <!-- 注意: 在 hash 地址中的 / 后面的参数,叫做“路径参数”;通过 this.$route.params 来访问 -->
    <router-link to="/movie/1">洛基</router-link>
    <!-- 注意: 在 hash 地址中的 ? 后面的参数,叫做“查询参数”;通过 this.$route.query 来访问 -->
    <router-link to="/movie/2?name=zs&age=20">雷神</router-link>
    <!-- 注意: 在 this.$route 中 ,path 只是路径部分;fullPath 是完整地址部分 -->
    <!-- 例如: /movie/2?name=zs&age=20 是完整路径,/movie/2 是路径部分 -->
    <router-link to="/movie/3">复联</router-link>
    <!-- <a href="#/about">关于</a> -->
    <router-link to="/about">关于</router-link>
    <hr />

    <!-- <component :is="comName"></component> -->
    <!-- vue-router 提供的占位符 router-view -->
    <router-view></router-view>
  </div>
</template>

index.js

// 3. 创建路由对象
const router = createRouter({
  // 通过history指定路由工作模式(hash模式)
  history: createWebHashHistory(),
  // 通过 linkActiveClass 指定路由高亮的类名
  linkActiveClass: 'active-router',
  // 配置路由规则
  // routes 是一个数组,定义 “hash地址" 与组件之间的对应关系
  routes: [
    // 路由规则
    { path: '/', redirect: '/home'},
    { path: '/home', component: Home },
    // 需求: 在 Movie 组件中需要根据 id 展示不同的电影信息
    // 动态路由,使用 :id 占位符提高路由的复用性
    { path: '/movie/:mid', component: Movie, props: true }, // props: true 会把路由参数映射到组件的 props 中
    { path: '/about',
      component: About,
      //  redirect: '/about/tab1',  // 重定向
      children: [
        // 子集路由不需要加 /
        // 默认子路由: path 为空字符串
        { path: '', component: Tab1 },
        { path: 'tab2', component: Tab2 },
      ] },
  ]
})

Movie.vue

<template>
  <div class="movie-container">
    <!-- this.$route 是路由的 “参数对象” -->
    <!-- this.$router 是路由的 “导航对象” -->
    <!-- this.$route.params.mid 的 this 可以省略 -->
    <h3>Movie 组件 --- {{ $route.params.mid}} --- {{ mid }}</h3>
    <button @click="showThis">打印 this</button>
  </div>
</template>

<script>
export default {
  name: 'MyMovie',
  props: ['mid'],
  methods: {
    showThis() {
      console.log(this)
    }
  }
}
</script>
2.7 声明式导航 & 编程式导航

router导航

vue-router中的编程式导航API,常用的导航API有:

① this.$router.push(‘hash地址’)

  • 跳转到指定hash地址,并增加一条历史记录

② this.$router.replace(‘hash地址’)

  • 跳转到指定的hash地址,并替换掉当前的历史记录

③ this.$router.go(数值n)

  • 跳回第n条历史记录

实际开发中一般只会前进和后退一层页面,vue-router 提供如下两个便捷方方:

① $router.back()

  • 在历史记录中,后退到上一个页面

② $router.forward()

  • 在历史记录中,前进到下一个页面
<template>
  <div class="home-container">
    <h3>Home 组件</h3>
    <hr />
    <button @click="gotoLj1">通过 push 跳转到“洛基”页面</button>
    <button @click="gotoLj2">通过 replace 跳转到“洛基”页面</button>
    <button @click="goback">后退 1 个</button>

    <!-- 写在行内使用编程式导航跳转的时候,this 必须要省略,否则会报错 -->
    <button @click="$router.back()">back 后退</button>
    <button @click="$router.forward()">forword 前进</button>
      
    <button @click="gotoMovie(3)">导航到 Movie3 页面</button>
  </div>
</template>

<script>
export default {
  name: 'MyHome',
  methods: {
    gotoLj1() {
      this.$router.push('/movie/1')
    },
    gotoLj2() {
      this.$router.replace('/movie/1')
    },
    goback() {
      // -1 表示后退 1 个, 超过上限就会原地不动
      // 1 表示前进 1 个, 超过上限就会原地不动
      this.$router.go(-1)
    },
      
    gotoMovie(id) {
      // this.$router.push('/movie/' + id)

      // this.$router.push({
      //   path: '/movie/' + id
      // })

      // 命名路由实现 编程式导航
      this.$router.push({ name: 'mov', params: { id: id } })
    }
  }
  }
}
</script>
2.8 命名路由

通过 name 属性为路由规则定义名称的方式,叫做命名路由(注意:命名路由的 name 值不能重复,必须保证唯一性)

<!-- : 动态绑定对象 -->
<router-link :to="{ name: 'mov', params: { id: 2 } }">gotoMovie2</router-link>

----------------

// 通过 routes 数组,指定路由规则
routes: [
    // :id 动态参数,通过 props 属性,将路由参数映射到组件的 props 中, name路由
    { name: 'mov', path: '/movie/:id', component: Movie, props: true },
]
2.9 导航守卫

导航守卫可以控制路由的访问权限

导航守卫

next

  1. Path 参数

fullPath:路由全地址,fullPath为/index?page=1

path:路径,不带参数,path为/index

  1. 全局前置守卫

每次发生路由跳转时,都会触发全局前置守卫(三个形参)。因此,在全局前置守卫中,程序员可以对每个路由进行访问权限的控制:

导航守卫三个形参.PNG

// 1. 导入 Vue 和 VueRouter 的包
import Vue from 'vue'
import VueRouter from 'vue-router'
// 导入组件
import Home from '@/components/Home.vue'
import Movie from '@/components/Movie.vue'
import About from '@/components/About.vue'
import Tab1 from '@/components/Tab1.vue'
import Tab2 from '@/components/Tab2.vue'
import Login from '@/components/Login.vue'
import Main from '@/components/Main.vue'

// 3.创建路由实例对象
const router = createRouter({
  // 通过history指定路由工作模式(hash模式)
  history: createWebHashHistory(),
  // 通过 linkActiveClass 指定路由高亮的类名
  linkActiveClass: 'active-router',
  // 通过 routes 数组,指定路由规则
  routes: [
    // 路由规则
    { path: '/', redirect: '/home'},
    { path: '/home', component: Home },
    // 需求: 在 Movie 组件中需要根据 id 展示不同的电影信息
    // 动态路由,使用 :id 占位符提高路由的复用性
    { path: '/movie/:mid', component: Movie, props: true }, // props: true 会把路由参数映射到组件的 props 中
    { path: '/about',
      component: About,
      //  redirect: '/about/tab1',  // 重定向
      children: [
        // 子集路由不需要加 /
        // 默认子路由: path 为空字符串
        { path: '', component: Tab1 },
        { path: 'tab2', component: Tab2 },
      ] },
    { path: '/login', component: Login },
    { path: '/main', component: Main }
  ]
})

// 为 router 对象绑定全局路由守卫
// 发生在路由跳转之前触发
router.beforeEach((to, from, next) => {
  // to: 要跳转到的目标路由对象
  // console.log(to);
  // from: 要跳转的目标路由对象
  // console.log(from);
  // next: 路由跳转函数,调用该函数才能跳转到下一个路由,必须,不接收next参数表示允许访问每一个
  // next() 表示放行

  // 分析:
  // 1. 要拿到用户将要访问的 hash 地址
  // 2. 判断 hash 地址是否等于 /main。
    // 2.1 如果等于 /main,证明需要登录之后,才能访问成功
    // 2.2 如果不等于 /main,则不需要登录,直接放行  next()
  // 3. 如果访问的地址是 /main。则需要读取 localStorage 中的 token 值
    // 3.1 如果有 token,则放行
    // 3.2 如果没有 token,则强制跳转到 /login 登录页

  if (to.path === '/main') {
    const token = localStorage.getItem('token')
    if (token) {
      // 有 token,放行
      next()
    } else {
      // 没有 token,强制跳转到 /login
      next('/login')
    }
  } else {
    // 不是 /main,直接放行
    next()
  }
})

// 4. 向外暴露路由对象
export default router

其他导航守卫可参考 https://router.vuejs.org/zh/guide/advanced/navigation-guards.html

3、后台管理案例

可参考这篇博客

(17条消息) vue2—前端路由小案例(后台管理小案例)_vue2路由案例_星月前端的博客-CSDN博客

3.1 项目整体功能演示

进入网站自动进入登录页面:

image-20220805200948154

登录成功自动存储token,然后跳转到登录成功页面:

image-20220805201232115

左边功能栏可以切换:

image-20220805201304315

点击用户管理的操作可以跳转到用户详情页面,并却可以回退到上一步:

image-20220805201404025

完了之后可以退出登录,并清除token,然后跳转到登录页面。

3.2 案例用到的知识点
  • 命名路由
  • 路由重定向
  • 导航守卫
  • 嵌套路由
  • 动态路由匹配
  • 编程式导航
3.3 主要代码
3.3.1 App.vue
<template>
  <router-view></router-view>
</template>

<script>
export default {
  name: 'MyApp',
}
</script>

<style lang="less" scoped>
</style>

3.3.2 MyHome.vue
<template>
  <div class="home-container">
    <!-- 头部区域 -->
    <MyHeader></MyHeader>
    <!-- 页面主体区域 -->
    <div class="home-main-box">
      <!-- 左侧边栏 -->
      <MyAside></MyAside>
      <!-- 右侧内容主体 -->
      <div class="home-main-body">
        <router-view></router-view>
      </div>
    </div>
  </div>
</template>

<script>
// 头部区域组件
import MyHeader from './subcomponents/MyHeader.vue'
// 左侧边栏组件
import MyAside from './subcomponents/MyAside.vue'

export default {
  name: 'MyHome',
  // 注册组件
  components: {
    MyHeader,
    MyAside,
  },
}
</script>

<style lang="less" scoped>
.home-container {
  height: 100%;
  display: flex;
  flex-direction: column;

  .home-main-box {
    height: 100%;
    display: flex;
    .home-main-body {
      padding: 15px;
      flex: 1;
    }
  }
}
</style>
3.3.3 MyLogin.vue
<template>
  <div class="login-container">
    <div class="login-box">

      <!-- 头像区域 -->
      <div class="text-center avatar-box">
        <img src="../assets/logo.png" class="img-thumbnail avatar" alt="">
      </div>

      <!-- 表单区域 -->
      <div class="form-login p-4">
        <!-- 登录名称 -->
        <div class="form-group form-inline">
          <label for="username">登录名称</label>
          <input type="text" class="form-control ml-2" id="username" placeholder="请输入登录名称" v-model.trim="username" autocomplete="off">
        </div>
        <!-- 登录密码 -->
        <div class="form-group form-inline">
          <label for="password">登录密码</label>
          <input type="password" class="form-control ml-2" id="password" placeholder="请输入登录密码" v-model.trim="password">
        </div>
        <!-- 登录和重置按钮 -->
        <div class="form-group form-inline d-flex justify-content-end">
          <button type="button" class="btn btn-secondary mr-2" @click="reset">重置</button>
          <button type="button" class="btn btn-primary" @click="login">登录</button>
        </div>
      </div>

    </div>
  </div>
</template>

<script>
export default {
  name: 'MyLogin',
  data(){
    return{
      username:'',
      password:''
    }
  },
  methods:{
    reset(){
      this.username = '',
      this.password = ''
    },
    login(){
      if(this.username === 'admin' && this.password === '666666'){
        //登录成功
        //1.存储token
        //2.跳转到后台主页
        localStorage.setItem('token','Bearer xxx')
        this.$router.push('/home')
      }else{
        //登录失败
        localStorage.removeItem('token')
        alert('登录失败!')
        this.username = ''
        this.password = ''
      }
    }
  }
}
</script>

<style lang="less" scoped>
.login-container {
  background-color: #35495e;
  height: 100%;
  .login-box {
    width: 400px;
    height: 250px;
    background-color: #fff;
    border-radius: 3px;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    box-shadow: 0 0 6px rgba(255, 255, 255, 0.5);
    .form-login {
      position: absolute;
      bottom: 0;
      left: 0;
      width: 100%;
      box-sizing: border-box;
    }
  }
}

.form-control {
  flex: 1;
}

.avatar-box {
  position: absolute;
  width: 100%;
  top: -65px;
  left: 0;
  .avatar {
    width: 120px;
    height: 120px;
    border-radius: 50% !important;
    box-shadow: 0 0 6px #efefef;
  }
}
</style>
3.3.4 MyAside.vue
<template>
  <div class="layout-aside-container">
    <!-- 左侧边栏列表 -->
    <ul class="user-select-none menu">
      <li class="menu-item">
        <router-link to="/home/users">用户管理</router-link>
      </li>
      <li class="menu-item">
        <router-link to="/home/rights">权限管理</router-link>
      </li>
      <li class="menu-item">
        <router-link to="/home/goods">商品管理</router-link>
      </li>
      <li class="menu-item">
        <router-link to="/home/orders">订单管理</router-link>
      </li>
      <li class="menu-item">
        <router-link to="/home/setting">系统设置</router-link>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'MyAside',
}
</script>

<style lang="less" scoped>
.layout-aside-container {
  width: 250px;
  height: 100%;
  border-right: 1px solid #eaeaea;
}

.menu {
  list-style-type: none;
  padding: 0;
  .menu-item {
    line-height: 50px;
    font-weight: bold;
    font-size: 14px;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
      Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    &:hover {
      background-color: #efefef;
      cursor: pointer;
    }
    a {
      display: block;
      color: black;
      padding-left: 30px;
      &:hover {
        text-decoration: none;
      }
    }
  }
}

// 设置路由高亮效果
.router-link-active {
  background-color: #efefef;
  box-sizing: border-box;
  position: relative;
  // 伪元素实现路由高亮效果
  &::before {
    content: ' ';
    display: block;
    width: 4px;
    height: 100%;
    position: absolute;
    left: 0;
    top: 0;
    background-color: #42b983;
  }
}
</style>
3.3.5 MyHeader.vue
<template>
  <div class="layout-header-container d-flex justify-content-between align-items-center p-3">
    <!-- 左侧 logo 和 标题区域 -->
    <div class="layout-header-left d-flex align-items-center user-select-none">
      <!-- logo -->
      <!-- <img class="layout-header-left-img" src="#" alt=""> -->
      <!-- 标题 -->
      <h4 class="layout-header-left-title ml-3">星月后台管理系统</h4>
    </div>

    <!-- 右侧按钮区域 -->
    <div class="layout-header-right">
      <button type="button" class="btn btn-light" @click="logout">退出登录</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'MyHeader',
  methods:{
    logout(){
      //1.清除token
      //2.跳转到登录页
      localStorage.removeItem('token')
      this.$router.push('/login')
    }
  }
}
</script>

<style lang="less" scoped>
.layout-header-container {
  height: 60px;
  border-bottom: 1px solid #eaeaea;
}

.layout-header-left-img {
  height: 50px;
}
</style>
3.3.6 MyUsers.vue
<template>
  <div>
    <!-- 标题 -->
    <h4 class="text-center">用户管理</h4>

    <!-- 用户列表 -->
    <table class="table table-bordered table-striped table-hover">
      <thead>
        <tr>
          <th>#</th>
          <th>姓名</th>
          <th>年龄</th>
          <th>头衔</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in userlist" :key="item.id">
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.age }}</td>
          <td>{{ item.position }}</td>
          <td>
            <!-- <a href="#" @click.prevent="gotoDetail(item.id)">详情</a> -->
            <router-link :to="'/home/user/' + item.id">详情</router-link>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  name: 'MyUser',
  data() {
    return {
      // 用户列表数据
      userlist: [
        { id: 1, name: '张三', age: 18, position: '快递员' },
        { id: 2, name: '张四', age: 18, position: '外卖员' },
        { id: 3, name: '张五', age: 19, position: 'Boss' },
        { id: 4, name: '老六', age: 22, position: '老六' }
      ]
    }
  },
  // methods: {
  //   gotoDetail(id) {
  //     // console.log('ok')
  //     this.$router.push('/home/userinfo/' + id)
  //     console.log(this);
  //   }
  // }
}
</script>

<style lang="less" scoped></style>
3.3.7 router/index.js (路由模块代码)
import { createRouter, createWebHashHistory } from 'vue-router'

import Login from '../components/MyLogin.vue'
import Home from '../components/MyHome.vue'

import Users from '../components/menus/MyUsers.vue'
import Rights from '../components/menus/MyRights.vue'
import Goods from '../components/menus/MyGoods.vue'
import Orders from '../components/menus/MyOrders.vue'
import Setting from '../components/menus/MySettings.vue'
import UserDetail from '../components/user/MyUserDetail.vue'

//导入前置路由地址数组
import Arrrouter from './router.js'

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/', redirect: '/login' },
    { path: '/login', component: Login, name: 'login' },
    {
      path: '/home',
      component: Home,
      name: 'home',
      redirect: '/home/users',
      children: [
        { path: 'users', component: Users, name: 'users' },
        { path: 'rights', component: Rights, name: 'rights' },
        { path: 'goods', component: Goods, name: 'goods' },
        { path: 'orders', component: Orders, name: 'orders' },
        { path: 'setting', component: Setting, name: 'settings' },
        // 用户详情动态路由
        { path: 'user/:id', component: UserDetail, name: 'userDetail', props: true },
      ]
    },
  ]
})

// 全局路由导航守卫
router.beforeEach((to, from, next) => {
  // 自定义保护路径
  if (Arrrouter.indexOf(to.path) != -1) {
    // 获取保存的 token 值
    const tokenStr = localStorage.getItem('token')
    if (tokenStr) {
      // 有 token,直接放行
      next()
    } else {
      // 没有 token,强制跳转到登录页
      next('/login')
    }
  } else {
    next()
  }
})

export default router
3.3.8 router (全局守卫路由路径)
export default ['/home', '/home/users', '/home/orders', '/home/rights', '/home/goods', '/home/setting', '/home/userinfo']

六、综合案例

同于策略报错问题.PNG

1、vue-cli

1.1 基于 vue ui 创建 vue 项目
vue ui // 傻瓜教程
1.2 基于 命令行 创建 vue 项目
vue create 项目名称

2、组件库

2.1 常用组件库

常用组件库.PNG

2.2 Element

ElementUI.PNG

组件的导入

导入组件.PNG

vue2配置

npm install element-ui -S

完整引入

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)
});

按需引入

看官网 https://element.eleme.cn/#/zh-CN/component/quickstart

2.3 把Element的导入和注册封装为独立的模块

新建 element/index.js

import Vue from 'vue'
// 完整导入
// import ElementUI from 'element-ui';
// import 'element-ui/lib/theme-chalk/index.css';

// Vue.use(ElementUI);

// 按需导入
import { Button, Input } from 'element-ui'

Vue.config.productionTip = false

Vue.use(Button)
Vue.use(Input)

3、axios 拦截器

拦截器.PNG

3.1 配置请求拦截器

请求拦截器.PNG

展示 Loading 效果

请求拦截器展示loading.PNG

3.2 配置响应拦截器

响应拦截器.PNG

关闭 loading 效果

响应拦截器关闭loading.PNG

3.3 main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

import { Loading } from 'element-plus'

import axios from 'axios'

const app = createApp(App)

axios.defaults.baseURL = 'http://localhost:3000'
let loadingInstance = null
// 配置请求拦截器
axios.intercepetors.request.use(config => {
  console.log(config);
  // 展示loading效果
  loadingInstance = Loading.service({ fullscreen: true })
  // 配置token认证字段
  axios.AxiosHeaders.Authrrization = 'Bearer xxx'
  // 固定写法
  return config
})

// 配置响应拦截器
axios.intercepetors.response.use(config => {
  // 关闭loading效果
  loadingInstance.close()
  return config
})

app.config.globalProperties.$http = axios

app.mount('#app')

4、proxy 跨域代理

proxy代理.PNG

main.js

axios.defaults.baseURL = '项目运行的自身服务器'

vue.config.js

module.exports = {
    devvServer: {
        proxy: '请求的服务器'port: 3000, // 本地端口号
    	open: true, // 自动打开浏览器
    }
}

注意

① devServer.proxy 提供的代理功能,仅在开发调试阶段生效

② 项目上线发布时,依旧需要 API 接口服务器 开启CORS 跨域资源共享

5、用户列表案例

vue create code-users
cd code-users
npm install vue-router@3.4.9 -S
npm install axios@0.21.1 -S
npm i element-ui -S
npm install
npm run serve
5.1 vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port: 3000,
    open: true,
    proxy: 'https://www.escook.cn'
  }
})

5.2 main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import axios from 'axios'
// 完整引入 element-ui
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import { Loading } from 'element-ui'

Vue.config.productionTip = false
Vue.use(ElementUI)

// 声明格式化时间的全局过滤器
Vue.filter('dateFormat', dtStr => {
  const dt = new Date(dtStr)

  const y = dt.getFullYear()
  const m = padZero(dt.getMonth() + 1)
  const d = padZero(dt.getDate())

  const hh = padZero(dt.getHours())
  const mm = padZero(dt.getMinutes())
  const ss = padZero(dt.getSeconds())

  return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
})

// 补零的函数
function padZero(n) {
  return n > 9 ? n : '0' + n
}

// 在全局配置 axios
// axios.defaults.baseURL = 'https://www.escook.cn'
axios.defaults.baseURL = 'http://localhost:3000'
Vue.prototype.$http = axios

// 声明请求拦截器
let loadingInstance = null
axios.interceptors.request.use(config => {
  // 展示 Loading 效果
  loadingInstance = Loading.service({ fullscreen: true })
  return config
})

// 声明响应拦截器
axios.interceptors.response.use(response => {
  // 隐藏 Loading 效果
  loadingInstance.close()
  return response
})

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')
5.3 router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

// 导入需要的组件
import UserList from '@/components/UserList.vue'
import UserDetail from '@/components/UserDetail.vue'

const router = new VueRouter({
  // 在这里声明路由规则
  routes: [
    { path: '/', redirect: '/users' },
    { path: '/users', component: UserList },
    { path: '/users/:id', component: UserDetail, props: true },
  ],
})

export default router
5.4 App.vue
<template>
  <div>
    <!-- 路由占位符 -->
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'MyApp'
}
</script>

<style lang="less" scoped></style>
5.5 UserList.vue
<template>
  <div>
    <!-- 添加按钮 -->
    <el-button type="primary" @click="dialogVisible = true">添加新用户</el-button>

    <!-- 用户的表格 -->
    <el-table :data="userList" stripe border>
      <!-- 这是索引列 -->
      <el-table-column type="index" label="#"></el-table-column>
      <!-- 渲染用户名这一列 -->
      <el-table-column label="姓名" prop="name"></el-table-column>
      <el-table-column label="年龄" prop="age"></el-table-column>
      <el-table-column label="头衔" prop="position"></el-table-column>
      <el-table-column label="创建时间">
        <template #default="scope">{{ scope.row.addtime | dateFormat }}</template>
      </el-table-column>
      <el-table-column label="操作">
        <!-- v-slot:default="scope" 两种简写形式 -->
        <!-- #default="scope" -->
        <!-- v-slot="scope" -->
        <template v-slot="{ row }">
          <div>
            <router-link :to="'/users/' + row.id">详情</router-link>&nbsp;
            <a href="#" @click.prevent="onRemove(row.id)">删除</a>
          </div>
        </template>
      </el-table-column>
    </el-table>

    <!-- 添加用户的对话框 -->
    <el-dialog title="添加新用户" :visible.sync="dialogVisible" width="50%" @close="onDialogClosed">
      <!-- 添加用户的表单 -->
      <el-form :model="form" label-width="80px" :rules="formRules" ref="myaddForm">
        <!-- 采集用户的姓名 -->
        <el-form-item label="用户姓名" prop="name">
          <el-input v-model="form.name"></el-input>
        </el-form-item>
        <el-form-item label="用户年龄" prop="age">
          <el-input v-model.number="form.age"></el-input>
        </el-form-item>
        <el-form-item label="用户头衔" prop="position">
          <el-input v-model="form.position"></el-input>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="onAddNewUser">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
export default {
  name: 'UserList',
  data() {
    // 声明校验年龄的函数
    let checkAge = (rule, value, cb) => {
      if (!Number.isInteger(value)) {
        return cb(new Error('请填写整数!'))
      }
      if (value > 100 || value < 1) {
        return cb(new Error('年龄必须在 1 到 100 之间!'))
      }
      cb()
    }
    return {
      userList: [
        {
          id: 1,
          name: '张三',
          age: 18,
          position: '前端工程师',
          addtime: new Date()
        },
        {
          id: 2,
          name: '李四',
          age: 28,
          position: '后端工程师',
          addtime: '2022-12-12 12:12:12'
        },
        {
          id: 3,
          name: '王五',
          age: 38,
          position: '全栈工程师',
          addtime: new Date()
        },
        {
          id: 4,
          name: '赵六',
          age: 48,
          position: '运维工程师',
          addtime: '2021-01-12T14:00:11.000Z'
        }
      ],
      // 控制添加对话框的显示与隐藏
      dialogVisible: false,
      // 要采集的用户的信息对象
      form: {
        name: '',
        age: '',
        position: ''
      },
      // 表单的验证规则对象
      formRules: {
        name: [
          { required: true, message: '姓名是必填项', trigger: 'blur' },
          { min: 1, max: 15, message: '长度在 1 到 15 个字符', trigger: 'blur' }
        ],
        age: [
          { required: true, message: '年龄是必填项', trigger: 'blur' },
          { validator: checkAge, trigger: 'blur' }
        ],
        position: [
          { required: true, message: '头衔是必填项', trigger: 'blur' },
          { min: 1, max: 10, message: '长度在 1 到 10 个字符', trigger: 'blur' }
        ]
      }
    }
  },
  created() {
    this.getUserList()
  },
  methods: {
    // 获取用户列表的数据
    async getUserList() {
      const { data: res } = await this.$http.get('/api/users')
      if (res.status !== 0) return console.log('用户列表数据获取失败!')
      this.userList = res.data
      console.log(this.userList)
    },
    // 监听对话框关闭的事件
    onDialogClosed() {
      // 拿到 Form 组件的引用,调用 resetFields 函数,即可重置表单
      this.$refs.myaddForm.resetFields()
    },
    // 用户点击了添加按钮
    onAddNewUser() {
      this.$refs.myaddForm.validate(async valid => {
        if (!valid) return
        // 需要执行添加的业务处理
        const { data: res } = await this.$http.post('/api/users', this.form)
        if (res.status !== 0) return this.$message.error('添加用户失败!')
        this.$message.success('添加成功!')
        this.dialogVisible = false
        this.getUserList()
      })
    },
    // 点击了删除的链接
    async onRemove(id) {
      // 询问用户是否删除
      const confirmResult = await this.$confirm('此操作将永久删除该用户, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).catch(err => err)

      // 判断是否点击了确认按钮
      if (confirmResult !== 'confirm') return this.$message.info('取消了删除!')

      // 发起请求,删除指定 id 的数据
      const { data: res } = await this.$http.delete('/api/users/' + id)
      if (res.status !== 0) return this.$message.error('删除失败!')
      // 提示删除成功,并刷新列表数据
      this.$message.success('删除成功!')
      this.getUserList()
    }
  }
}
</script>

<style scoped lang="less">
.el-table {
  margin-top: 15px;
}
</style>
5.6 UserDetail.vue
<template>
  <el-card class="box-card">
    <div slot="header" class="clearfix">
      <span>用户详情</span>
      <el-button style="float: right; padding: 3px 0" type="text" @click="goBack">返回</el-button>
    </div>
    <div class="text item">
      <p>姓名:{{ userInfo.name }}</p>
      <p>年龄:{{ userInfo.age }}</p>
      <p>头衔:{{ userInfo.position }}</p>
    </div>
  </el-card>
</template>

<script>
export default {
  name: 'UserDetail',
  props: ['id'],
  data() {
    return {
      userInfo: {}
    }
  },
  created() {
    this.getUserInfo()
  },
  methods: {
    async getUserInfo() {
      const { data: res } = await this.$http.get('/api/users/' + this.id)
      if (res.status !== 0) return this.$message.error('获取用户详情数据失败!')
      this.userInfo = res.data
      console.log(this.userInfo)
    },
    goBack() {
      this.$router.go(-1)
    }
  }
}
</script>

<style lang="less" scoped></style>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Spring Boot 是一个基于 Java 的快速应用开发框架,旨在简化新 Spring 应用的创建、配置和部署。它可以让您在几分钟内构建一个带有完整功能的应用程序。 Vue.js 是一个渐进式 JavaScript 框架,用于构建用户界面。它具有高效的渲染性能,并且易于学习和使用。Vue 3 是 Vue.js 的最新版本,拥有更强的性能和更多的特性。 因此,在 Spring Boot 中使用 Vue.js 构建前端界面是一个很好的选择,特别是对于需要快速开发和部署应用程序的人来说。 ### 回答2: Spring Boot和Vue3是两个独立的开发框架,分别用于后端和前端开发。 Spring Boot是一个基于Java的开发框架,用于构建独立的、可扩展的、高性能的后端应用程序。它提供了很多简化开发流程的功能,包括自动配置、自动化部署、统一的项目管理等。Spring Boot还集成了很多常用的开发组件和工具,如持久化层ORM框架、安全认证框架、日志管理工具等。通过Spring Boot,我们可以快速搭建后端应用,并且可以很容易地进行测试和部署。 Vue3是一个流行的JavaScript前端框架,用于构建交互式的、响应式的Web界面。它采用了组件化的开发方式,提供了丰富的API和工具,可以方便地进行前端开发。Vue3具有更高的性能和更好的可维护性,而且支持组合式API和更好的类型检查等新特性。通过Vue3,我们可以构建富客户端的Web应用,并实现数据与视图的高效绑定。 结合Spring Boot和Vue3,我们可以实现前后端分离的开发模式。后端使用Spring Boot提供API接口,前端使用Vue3进行页面开发和数据展示。通过前后端分离的方式,开发人员可以专注于各自的领域,提高开发效率。同时,Vue3的响应式设计和Spring Boot的RESTful API也能够很好地配合,实现数据的动态展示和交互。 总的来说,Spring Boot和Vue3是一对很好搭配的开发框架。Spring Boot用于构建稳定、高性能的后端应用,而Vue3用于构建交互式、响应式的前端页面。通过它们的配合,我们可以实现全栈开发,构建出功能强大、用户友好的Web应用。 ### 回答3: Spring Boot和Vue.js 3是两个非常受欢迎的开源框架。Spring Boot是基于Java的快速开发框架,它简化了Java应用程序的开发过程。它提供了一系列的开箱即用的功能和插件,例如自动配置、内嵌式的服务器、日志管理等等,极大地提高了开发效率。Spring Boot还与Spring框架密切相关,它构建在Spring框架之上,可以方便地与其他Spring组件集成。 Vue.js 3是一个用于构建用户界面的JavaScript框架。它专注于视图层,通过使用组件化的开发方式,使得前端开发更加灵活和高效。Vue.js 3采用了响应式的数据驱动模式,可以简化界面和数据的绑定。此外,Vue.js 3还提供了强大的路由、状态管理和打包工具等功能,使得构建复杂的前端应用变得轻松。 Spring Boot与Vue.js 3可以很好地配合使用,构建现代化的全栈应用。前端使用Vue.js 3来开发用户界面,并利用其组件化和响应式的特性,实现动态和高性能的前端页面。而后端使用Spring Boot开发业务逻辑和数据处理的服务器端。 使用Spring Boot和Vue.js 3的组合有以下几个优势: 1. 前后端分离:Vue.js 3负责前端开发,Spring Boot负责后端开发,使得前后端的开发人员可以并行工作,提高开发效率。 2. 前端灵活性:Vue.js 3提供了丰富的组件和工具,使得前端页面的开发更加简单和灵活。 3. 后端可伸缩性:使用Spring Boot可以轻松构建可伸缩的后端服务器,满足高并发的需求。 4. 关注点分离:前端和后端各自负责不同的功能,使得代码结构更加清晰,易于维护和测试。 总结而言,Spring Boot和Vue.js 3是两个强大的开源框架,它们可以很好地配合使用,构建现代化的全栈应用。前端使用Vue.js 3开发灵活、高性能的用户界面,后端使用Spring Boot开发可伸缩的服务器端逻辑,实现前后端的分离和关注点分离。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Pluto_ssy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值