一、ES6 模块化与异步编程高级用法
1、ES6 模块化
还有AMD、CMD或CommonJS模块化标准
① 确保安装了 v14.15.1 或者更高版本的 node.jd
② 在 package.json 的根节点中添加 “type": ”module" 节点
三种语法
- 默认导出与默认导入
export default 默认导出的成员
注意:只能使用唯一的 export default
import 接收名称 from ‘模块标识符’
注意:接受名称要合法
- 按需导出与按需导入
export 按需导出的成员
import { 成员名 as 自定义名 } from ‘模块标识符’
注意:导出可以用多次;成员名称要一致;导入可以使用as;可以和默认导入一起使用,只需要在导入前边自定义一个变量即可
- 直接导入并执行模块中的代码
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 异步操作的结果,可以调用 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 同步任务和异步任务的执行过程
5、宏任务和微任务
① 宏任务(macrotask)
- 异步 Ajax 请求
- setTimeout、setinterval
- 文件操作
- 其它宏任务
② 微任务 (microtask)
- Promise.then、.catch 和 .finally
- process.nextTick
- 其它微任务
5.1 宏任务和微任务的执行顺序
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、前端工程化
好处:
① “自成体系”,覆盖前端项目从创建到部署的方方面面
② 最大程度地提高了前端的开发效率,降低了技术造型、前后端联调等带来的协调沟通成本
前端工程化解决方案:
早期有:
- grunt ( https://www.gruntjs.net/ )
- gulp ( https://www.gulpjs.com.cn/ )
目前主流:
- webpack ( https://www.webpackjs.com/ )
- parcel ( https://zh.parceljs.org/ )
2、webpack 的基本使用
2.1 webpack 配置
安装 webpack 相关包
npm i webpack@5.5.1 webpack-cli@4.2.0 -D
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 插件
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 实时打包生成文件放到了内存里(缓存)
4、webpack 中的 loader
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
会生成 .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 的内容
创建 vue 的 SPA 项目
vite | vue-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 目录结构
2.3 vite 项目运行流程
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 组件命名规则
4、组件的基本使用
4.1 scoped和/deep/
4.2 props 的大小写命名和验证
组件中如果采用 camelClass驼峰命名法 声明了 props 属性名称,则有两种方式为其绑定属性
也可以以数组的形式写多个类型验证
对象的形式验证
export default {
props: {
属性名: {
// 是否必须
required: true,
// 用 type 属性定义数值类型,可以以数组类型写多个
type: Number,
// type: [String, Number]
// 用 default 属性定义属性的默认值
default: 0
}
}
}
自定义验证函数:
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
④ 以对象内联样式绑定style样式
5、自定义事件
在封装组件时,为了让组件的使用者可以监听到组件内状态的变化,此时需要用到组件的自定义事件
声明触发传参自定义事件
export default{
// 在emits节点下声明自定义事件
emits: ['自定义事件名称'],
methods: {
onBtnClick() {
this.$emit('自定义事件名称', 参数)
}
}
}
<my-counter @自定义事件="处理函数名称"></my-counter>
export default{
methods: {
// 接收参数
处理函数名称(val) {}
}
}
6、组件上的 v-model
维护组件内外数据的同步
子向父,以及父向子
<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 目录
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>
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 父向子
3.2 子向父
3.3 父子之间的双向数组同步
<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
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
终极的组件之间的数据共享方案,他可以使数据共享变得高效、清晰、且易于维护。
4、vue 3.x 中全局配置 axios
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 目录结构
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 表示排除不需要被缓存的组件
注意:include 和 exclude 不能被同时使用
⑥ 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')
8、Table 案例
npm init vite-app table-code
cd table-code
npm i
npm i less -D
npm i axios -S
8.1 目录结构
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>
五、路由
路由就是对应关系,分为(前端路由、后端路由)
后端路由:
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 前端路由的概念和原理
-
用户点击页面上的路由链接
-
导致url地址的Hash值变化
-
前端路由监听到Hash地址的变化
-
前端路由把当前Hash地址对应的组件渲染到浏览器中
<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 进行使用
步骤:
-
第一步:安装vue-router包
npm install vue-router@next -S
-
第二步:创建路由模块。创建router文件夹,在文件夹下建立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
-
第四步:
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 的高级用法
- 默认的高亮 class 类
被激活的链接,默认会应用一个叫做 router-link-active 的类名,可以用来设置样式
/* 在 index.css 全局样式表中重新定义 router-link-active */
.router-link-active {
background-color: red;
color: white;
font-weight: bold;
}
- 自定义路由高亮的 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>
<router-link to="/movie/2">电影2</router-link>
<router-link to="/movie/3">电影3</router-link>
------------------
// :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传参
-
路由跳转并携带query参数,to的字符串写法 messageData是一个变量
<router-link :to="`/home/news?id=001&message=${messageData}`" ></router-link>
-
路由跳转并携带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 声明式导航 & 编程式导航
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 导航守卫
导航守卫可以控制路由的访问权限
- Path 参数
fullPath:路由全地址,fullPath为/index?page=1
path:路径,不带参数,path为/index
- 全局前置守卫
每次发生路由跳转时,都会触发全局前置守卫(三个形参)。因此,在全局前置守卫中,程序员可以对每个路由进行访问权限的控制:
// 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 项目整体功能演示
进入网站自动进入登录页面:
登录成功自动存储token,然后跳转到登录成功页面:
左边功能栏可以切换:
点击用户管理的操作可以跳转到用户详情页面,并却可以回退到上一步:
完了之后可以退出登录,并清除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']
六、综合案例
1、vue-cli
1.1 基于 vue ui 创建 vue 项目
vue ui // 傻瓜教程
1.2 基于 命令行 创建 vue 项目
vue create 项目名称
2、组件库
2.1 常用组件库
2.2 Element
组件的导入
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 拦截器
3.1 配置请求拦截器
展示 Loading 效果
3.2 配置响应拦截器
关闭 loading 效果
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 跨域代理
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>
<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>