一、前端工程化与webpack
1.前端工程化
1. 小白眼中的前端开发 vs 实际的前端开发
2. 什么是前端工程化
3. 前端工程化的解决方案
2.webpack的基本使用
1. 什么是 webpack:是前端项目工程化的具体解决方案
2. 创建列表隔行变色项目
3. 在全局项目中安装 webpack
【这里注意:后面使用到webpack-dev-server这个插件的时候,会报错,需要将webpack-cli进行修改,将重新安装为最新版本】:npm i webpack-cli
4. 创建名为 webpack.config.js 的 webpack 配置文件:在项目中配置 webpack
① 在项目根目录中,创建名为 webpack.config.js 的 webpack 配置文件,并初始化如下的基本配置:
② 在 package.json 的 scripts 节点下,新增 dev 脚本如下:("dev”是脚本的名字,是可以修改的,“webpack”是固定写法)
③ 在终端中运行 npm run dev 命令,启动 webpack 进行项目的打包构建(创建了一个dist的文件夹,里面有一个main.js)
4.1 mode 的可选值mode:development、production(对代码进行压缩)
节点的可选值有两个,分别是:
① development(项目开发过程中使用)
⚫ 开发环境
⚫ 不会对打包生成的文件进行代码压缩和性能优化
⚫ 打包速度快,适合在开发阶段使用
② production(项目上线的时候使用)
⚫ 生产环境
⚫ 会对打包生成的文件进行代码压缩和性能优化
⚫ 打包速度很慢,仅适合在项目发布阶段使用
mode 用来指定构建模式 可以选中有development 和 production
结论:开发的时候一定使用development ,因为追求的是打包的速度,而不是体积
反过来,发布上线的时候一定要用production ,因为上线追求的是体积小,而不是打包速度。
4.2 webpack.config.js 文件的作用
webpack.config.js 是 webpack 的配置文件。webpack 在真正开始打包构建之前,会先读取这个配置文件, 从而基于给定的配置,对项目进行打包。
【在npm run dev(这个dev是在package.json文件中的script脚本中的,它会先去读取webpack.config.js这个文件,然后再进行配置】
注意:由于 webpack 是基于 node.js 开发出来的打包工具,因此在它的配置文件中,支持使用 node.js 相关 的语法和模块进行 webpack 的个性化配置。
4.3 webpack 中的默认约定:默认打包入口文件为src->index.js,打包输出文件为dist->main.js
4.4 自定义打包的入口与出口
在 webpack.config.js 配置文件中,通过 entry 节点指定打包的入口。通过 output 节点指定打包的出口。 示例代码如下:(记得先将前面生成的dist文件夹删除)
const path=require('path')
//使用Node.js 中的导出语法,向外导出一个webpack的配置对象
module.exports={
// mode 用来指定构建模式 可以选中有development 和 production
// 结论:开发的时候一定使用development ,因为追求的是打包的速度,而不是体积
// 反过来,发布上线的时候一定要用production ,因为上线追求的是体积小,而不是打包速度。
mode:'development',
entry:path.join(__dirname,'./src/index.js'),
//输出文件的名称
//指定生成的文件要存放的位置
output:{
// 存放的目录
path: path.join(__dirname,'dist'),
// 生成的文件名
filename:'bundle.js'
}
}
<!-- 加载和引用内存中的main.js 因为每一次一修改代码,run一下后,实际上dist中的main.js文件是存放再内存中 -->
<!-- 并且是存放再根目录下的内存中 -->
<script src="../dist/bundle.js"></script>
3.webpack中的插件【都是将产生的文件存放在内存中】
1. webpack 插件的作用
2. webpack-dev-server:自动更新dist文件夹中的bundle.js
webpack-dev-server 可以让 webpack 监听项目源代码的变化,从而进行自动打包构建。
2.1 安装 webpack-dev-server
2.2 配置 webpack-dev-server(3.11.2版本的)
① 修改 package.json -> scripts 中的 dev 命令如下:
② 再次运行 npm run dev 命令,重新进行项目的打包
③ 在浏览器中访问 http://localhost:8080 地址,查看自动打包效果(记得再这个网址才可以看到)
实际上,代码修改后,执行run,dist中的bundle.js并没有被修改,而是再根目录下的bundle.js被修改,所以我们要使用的是根目录下的bundle.js而且不是dist中的bundle.js。
<!-- 加载和引用内存中的bundle.js 因为每一次一修改代码,run一下后,实际上dist中的bundle.js文件是存放再内存中 --> <!-- 并且是存放再根目录下的内存中 --> <!-- 一定要加上“/” 如果不加就错误 --> <script src="/bundle.js"></script>//表示根目录下的bundle.js
2.3 打包生成的文件哪儿去了?
① 不配置 webpack-dev-server 的情况下,webpack 打包生成的文件,会存放到实际的物理磁盘上
⚫ 严格遵守开发者在 webpack.config.js 中指定配置
⚫ 根据 output 节点指定路径进行存放
② 配置了 webpack-dev-server 之后,打包生成的文件存放到了内存中
⚫ 不再根据 output 节点指定的路径,存放到实际的物理磁盘上
⚫ 提高了实时打包输出的性能,因为内存比物理磁盘速度快很多
2.4 生成到内存中的文件该如何访问?
webpack-dev-server 生成到内存中的文件,默认放到了项目的根目录中,而且是虚拟的、不可见的。
⚫ 可以直接用 / 表示项目根目录,后面跟上要访问的文件名称,即可访问内存中的文件
⚫ 例如 /bundle.js 就表示要访问 webpack-dev-server 生成到内存中的 bundle.js 文件
3. html-webpack-plugin:将页面文件放在根目录中,才不用每次进入src中才可以查看页面
3.1 安装 html-webpack-plugin(版本为5.3.2)
3.2 配置 html-webpack-plugin
// 1. 导入 html-webpack-plugin 这个插件,得到插件的构造函数
const HtmlPlugin = require('html-webpack-plugin')
// 2. new 构造函数,创建插件的实例对象
const htmlPlugin = new HtmlPlugin({
// 指定要复制哪个页面
template: './src/index.html',
// 指定复制出来的文件名和存放路径
filename: './index.html'
})
module.exports = {
plugins:[htmlPlugin],
}
3.3 解惑 html-webpack-plugin
① 通过 HTML 插件复制到项目根目录中的 index.html 页面,也被放到了内存中
② HTML 插件在生成的 index.html 页面,自动注入了打包的 bundle.js 文件【所以可以不用在index.html文件中引入script文件路径】
4. devServer 节点 【自动打开浏览器】
在 webpack.config.js 配置文件中,可以通过 devServer 节点对 webpack-dev-server 插件进行更多的配置, 示例代码如下:
module.exports = {
devServer:{
// 首次打包成功后,自动打开浏览器
open:true,
//原来打开浏览器的时候,地址是:--http://localhost:8080/
// 在 http 协议中,如果端口号是 80,则可以被省略
port:80,
// 指定运行的主机地址
//地址栏出现为:http://127.0.0.1/
host: '127.0.0.1'
}
}
注意:凡是修改了 webpack.config.js 配置文件,或修改了 package.json 配置文件,必须重启实时打包的服 务器,否则最新的配置文件无法生效!
4.webpack中的loader(加载器)
1. loader 概述
在实际开发过程中,webpack 默认只能打包处理以 .js 后缀名结尾的模块。其他非 .js 后缀名结尾的模块, webpack 默认处理不了,需要调用 loader 加载器才可以正常打包,否则会报错!
loader 加载器的作用:协助 webpack 打包处理特定的文件模块。
比如:
⚫ css-loader 可以打包处理 .css 相关的文件
⚫ less-loader 可以打包处理 .less 相关的文件
⚫ babel-loader 可以打包处理 webpack 无法处理的高级 JS 语法
2. loader 的调用过程
在导入css文件并且没有导包前:
修改后面 run 一下:报错
3. 打包处理 css 文件
① 运行 npm i style-loader@3.0.0 css-loader@5.2.6 -D 命令,安装处理 css 文件的 loader ② 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
module.exports = {
module:{
rules:[
// 定义了不同模块对应的 loader
//use指定的顺序不能改变,要按顺序(从后往前调用)
{test:/\.less$/,use:['style-loader','css-loader']}
]
}
}
4. 打包处理 less 文件
① 运行 npm i less-loader@10.0.1 less@4.1.1 -D 命令
② 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
// 导入样式(在 webpack 中,一切皆模块,都可以通过 ES6 导入语法进行导入和使用)
// 如果某个模块中,使用 from 接收到的成员为 undefined,则没必要进行接收
//因为index1.js文件是模块导入的入口文件,并且bundle.js是由本文件(index1.js)产生的
//所以在这个文件导入样式
import'./css/index.css';
module.exports = {
module:{
rules:[
// 处理 .less 文件的 loader
{test:/\.less$/,use:['style-loader','css-loader','less-loader']}
]
}
}
其中,test 表示匹配的文件类型, use 表示对应要调用的 loader 注意:
⚫ use 数组中指定的 loader 顺序是固定的
⚫ 多个 loader 的调用顺序是:从后往前调用
5. 打包处理样式表中与 url 路径相关的文件
① 运行 npm i url-loader@4.1.1 file-loader@6.2.0 -D 命令
② 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
如果需要调用的 loader 只有一个,则只传递一个字符串也行,如果有多个loader,则必须指定数组
其中 ? 之后的是 loader 的参数项:
⚫ limit 用来指定图片的大小,单位是字节(byte)
⚫ 只有 ≤ limit 大小的图片,才会被转为 base64 格式的图片
图片base64:将图片转换为base64的形式:console.log(logo)
// 导入样式(在 webpack 中,一切皆模块,都可以通过 ES6 导入语法进行导入和使用)
// 如果某个模块中,使用 from 接收到的成员为 undefined,则没必要进行接收
//因为index1.js文件是模块导入的入口文件,并且bundle.js是由本文件(index1.js)产生的
//所以在这个文件导入样式
import'./css/index.less';
module.exports = {
module:{
rules:[
// 处理图片文件的 loader
// 如果需要调用的 loader 只有一个,则只传递一个字符串也行,如果有多个loader,则必须指定数组
// 在配置 url-loader 的时候,多个参数之间,使用 & 符号进行分隔
{ test: /\.jpg|png|gif$/, use: 'url-loader' },
]
}
}
6. 打包处理 js 文件中的高级语法
6.1 安装 babel-loader 相关的包
运行如下的命令安装对应的依赖包: npm i babel-loader@8.2.2 @babel/core@7.14.6 @babel/plugin-proposal-decorators@7.14.5 -D
在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
6.2 配置 babel-loade【创建名为bable.config.js跟webpack.config.js同级】
在项目根目录下,创建名为 babel.config.js 的配置文件,定义 Babel 的配置项如下:
module.exports = {
// 声明 babel 可用的插件
// 将来,webpack 在调用 babel-loader 的时候,会先加载 plugins 插件来使用
plugins: [['@babel/plugin-proposal-decorators', { legacy: true }]]
}
5.打包发布
1. 为什么要打包发布
2. 配置 webpack 的打包发布
在 package.json 文件的 scripts 节点下,新增 build 命令如下:
--model 是一个参数项,用来指定 webpack 的运行模式。
production 代表生产环境,会对打包生成的文件 进行代码压缩和性能优化。
注意:通过 --model 指定的参数项,会覆盖 webpack.config.js 中的 model 选项。
"scripts": {
"dev": "webpack serve",
"bulid":"webpack --mode production"
},
3. 把 JavaScript 文件统一生成到 js 目录中
// __dirname--当前文件所处的根目录
// entry: '指定要处理哪个文件'
entry:path.join(__dirname,'./src/index1.js'),
// 指定生成的文件要存放到哪里
output:{
// 存放的目录
path:path.join(__dirname,'./dist'),
// 生成的文件名
//表示新产生的文件存放在dist文件夹下的js文件夹目录下
filename:'js/bundle.js'
},
4. 把图片文件统一生成到 image 目录中
module:{
rules:[
// 处理图片文件的 loader
// 如果需要调用的 loader 只有一个,则只传递一个字符串也行,如果有多个loader,则必须指定数组
// 在配置 url-loader 的时候,多个参数之间,使用 & 符号进行分隔
// &outputPath=images--表示生成文件的存放路径
{ test: /\.jpg|png|gif$/, use: 'url-loader?limit=470&outputPath=images' },
]
}
5. 自动清理 dist 目录下的旧文件
为了在每次打包发布时自动清理掉 dist 目录中的旧文件,可以安装并配置 clean-webpack-plugin 插件:
// 注意:左侧的 { } 是解构赋值
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// 3. 插件的数组,将来 webpack 在运行时,会加载并调用这些插件
plugins:[htmlPlugin, new CleanWebpackPlugin()],
6.Source Map
1. 生产环境遇到的问题
2. 什么是 Source Map:一个信息文件,里面储存着位置信息
3. webpack 开发环境下的 Source Map
3.1 默认 Source Map 的问题:记录是生成后的代码位置
3.2 解决默认 Source Map 的问题:在webpack.config.js文件中进行配置
开发环境下,推荐在 webpack.config.js 中添加如下的配置,即可保证运行时报错的行数与源代码的行数 保持一致:
module.exports = {
// 在开发调试阶段,建议大家都把 devtool 的值设置为 eval-source-map
// devtool: 'eval-source-map',
// 在实际发布的时候,建议大家把 devtool 的值设置为 nosources-source-map 或直接关闭 SourceMap
devtool:'nosources-source-map',
// mode 代表 webpack 运行的模式,可选值有两个 development 和 production
// 结论:开发时候一定要用 development,因为追求的是打包的速度,而不是体积;
// 反过来,发布上线的时候一定能要用 production,因为上线追求的是体积小,而不是打包速度快!
mode: 'development',
}
4. webpack 生产环境下的 Source Map【要关闭Source Map】
在生产环境下,如果省略了 devtool 选项,则最终生成的文件中不包含 Source Map。这能够防止原始代码通 过 Source Map 的形式暴露给别有所图之人。
4.1 只定位行数不暴露源码: devtool:'nosources-source-map'
在生产环境下,如果只想定位报错的具体行数,且不想暴露源码。此时可以将 devtool 的值设置为 nosources-source-map。实际效果如图所示:
4.2 定位行数且暴露源码:devtool:'source-map'
在生产环境下,如果想在定位报错行数的同时,展示具体报错的源码。此时可以将 devtool 的值设置为 source-map。实际效果如图所示:
采用此选项后:你应该将你的服务器配置为,不允许普通用户访问 source map 文件!
5. Source Map 的最佳实践
① 开发环境下:
⚫ 建议把 devtool 的值设置为 eval-source-map
⚫ 好处:可以精准定位到具体的错误行
② 生产环境下:
⚫ 建议关闭 Source Map 或将 devtool 的值设置为 nosources-source-map
⚫ 好处:防止源码泄露,提高网站的安全性
实际开发中需要自己配置 webpack 吗?
@的使用:代表src这个根目录【使用前要先进行配置】
配置代码:在webpack.config.js这个文件中进行配置的
module.exports = {
resolve:{
alias:{
// 告诉 webpack,程序员写的代码中,@ 符号表示 src 这一层目录
'@':path.join(__dirname,'./src/')
}
}
}
演示代码:
// import'./css/index.css';
//跟下面等价
import'@/css/index.css';
// import'./css/index.less';
//跟下面等价
import'@/css/index.less';
// 导入 src/js/test/info.js
import '@/js/test/info.js'
总结
二、vue基础入门
1.vue简介
1. 什么是 vue
1. 构建用户界面
用 vue 往 html 页面中填充数据,非常的方便
2. 框架
框架是一套现成的解决方案,程序员只能遵守框架的规范,去编写自己的业务功能!
要学习 vue,就是在学习 vue 框架中规定的用法!
vue 的指令、组件(是对 UI 结构的复用)、路由、Vuex、vue 组件库
只有把上面老师罗列的内容掌握以后,才有开发 vue 项目的能力!
2. vue 的特性
2.1 数据驱动视图(单向数据绑定-从服务器到客户端)
数据驱动视图:
+ 数据的变化会驱动视图自动更新
+ 好处:程序员只管把数据维护好,那么页面结构会被 vue 自动渲染出来!
2.2 双向数据绑定
在填写表单时,双向数据绑定可以辅助开发者在不操作 DOM 的前提下,自动把用户填写的内容同步到数据源 中。示意图如下:
双向数据绑定:
> 在网页中,form 表单负责**采集数据**,Ajax 负责**提交数据**。
+ js 数据的变化,会被自动渲染到页面上
+ 页面上表单采集的数据发生变化的时候,会被 vue 自动获取到,并更新到 js 数据中(开发者不再需要手动操作 DOM 元素,来获取表单元素最新的值)> 注意:数据驱动视图和双向数据绑定的底层原理是 MVVM(Mode 数据源、View 视图、ViewModel 就是 vue 的实例)
2.3 MVVM:是 vue 实现数据驱动视图和双向数据绑定的核心原理
MVVM 是 vue 实现数据驱动视图和双向数据绑定的核心原理。MVVM 指的是 Model、View 和 ViewModel, 它把每个 HTML 页面都拆分成了这三个部分,如图所示:
2.4 MVVM 的工作原理:把当前页面的数据源(Model)和页面的结构(View)连接在了一起
ViewModel 作为 MVVM 的核心,是它把当前页面的数据源(Model)和页面的结构(View)连接在了一起。
注意:数据驱动视图和双向数据绑定的底层原理是 MVVM(Mode 数据源【data】、View 视图【el】、ViewModel 【vm】就是 vue 的实例)
3. vue 的版本
2.vue的基本使用
1. 基本使用步骤
① 导入 vue.js 的 script 脚本文件
② 在页面中声明一个将要被 vue 所控制的 DOM 区域
③ 创建 vm 实例对象(vue 实例对象)
“el”---是指定当前要控制页面的哪一块(最好先用div将整个括起来,然后给div加上一个id,然后将el赋值给这个id)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望Vue能够控制下面的这个div 帮我们把数据填充到div内部 -->
<div id="app">{{ username }}</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
//创建Vue实例对象
const vm=new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el:'#app',
// data 对象就是要渲染到页面上的数据
data:{
username:'zhangsan'
}
})
</script>
</body>
</html>
2. 基本代码与 MVVM 的对应关系
3.vue的调试工具
1. 安装 vue-devtools 调试工具
2. 配置 Chrome 浏览器中的 vue-devtools
3. 使用 vue-devtools 调试 vue 页面
4.vue的指令与过滤器
1. 指令的概念
1.1 内容渲染指令
v-text:会覆盖元素内默认的值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望Vue能够控制下面的这个div 帮我们把数据填充到div内部 -->
<div id="app">
<p v-text="username"></p>
<!-- “女”将“性别”覆盖住了 -->
<p v-text="gender">性别</p>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
//创建Vue实例对象
const vm=new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el:'#app',
// data 对象就是要渲染到页面上的数据
data:{
username:'zhangsan',
gender:'女'
}
})
</script>
</body>
</html>
{{ }} 语法【插值表达式】:不会将默认值覆盖【开发中常用】
vue 提供的 {{ }} 语法,专门用来解决 v-text 会覆盖默认文本内容的问题。这种 {{ }} 语法的专业名称是插值表达 式(英文名为:Mustache)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望Vue能够控制下面的这个div 帮我们把数据填充到div内部 -->
<div id="app">
<hr>
<p>姓名:{{ username }}</p>
<p>性别:{{ gender }}</p>
<hr>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
//创建Vue实例对象
const vm=new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el:'#app',
// data 对象就是要渲染到页面上的数据
data:{
username:'zhangsan',
gender:'女'
}
})
</script>
</body>
</html>
注意:相对于 v-text 指令来说,插值表达式在开发中更常用一些!因为它不会覆盖元素中默认的文本内容。
v-html
v-text 指令和插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素, 则需要用到 v-html 这个指令:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望Vue能够控制下面的这个div 帮我们把数据填充到div内部 -->
<div id="app">
<div v-text="info"></div>
<div>{{ info }}</div>
<div v-html="info"></div>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
//创建Vue实例对象
const vm=new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el:'#app',
// data 对象就是要渲染到页面上的数据
data:{
username:'zhangsan',
gender:'女',
info: '<h4 style="color: red; font-weight: bold;">欢迎大家来学习 vue.js</h4>'
}
})
</script>
</body>
</html>
总结
1. `v-text` 指令的缺点:会覆盖元素内部原有的内容!
2. `{{ }}` 插值表达式:在实际开发中用的最多,只是内容的占位符,不会覆盖原有的内容!
3. `v-html` 指令的作用:可以把带有标签的字符串,渲染成真正的 HTML 内容!
1.2 属性绑定指令:v-bind:【记得加上冒号】
如果需要为元素的属性动态绑定属性值,则需要用到 v-bind 属性绑定指令。用法示例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
<div id="app">
<input type="text" v-bind:placeholder="tips">
<hr>
<!-- vue 规定 v-bind: 指令可以简写为 : -->
<img v-bind:src="photo" alt="">
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
tips: '请输入用户名',
photo:'https://www.h5w3.com/wp-content/uploads/2020/05/1460000022734939.png'
}
})
</script>
</body>
</html>
属性绑定指令的简写形式
vue 规定 v-bind: 指令可以简写为 ":"
<img v-bind:src="photo" alt=""> 等价于 <img :src="photo" alt="">
使用 Javascript 表达式
在 vue 提供的模板渲染语法中,除了支持绑定简单的数据值之外,还支持 Javascript 表达式的运算,例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
<div id="app">
<input type="text" v-bind:placeholder="tips">
<hr>
<!-- vue 规定 v-bind: 指令可以简写为 : -->
<img v-bind:src="photo" alt="">
<hr>
<div>1+2 的结果是:{{ 1+2 }}</div>
<div>{{ tips }}反转的结果是:{{ tips.split(' ').reverse().join() }}</div>
<!-- 记得给box加上单引号 -->
<!-- box相当于一个字符串 -->
<div :title="'box' + index">这是一个div</div>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
tips: '请输入用户名',
photo:'https://www.h5w3.com/wp-content/uploads/2020/05/1460000022734939.png',
index:3
}
})
</script>
</body>
</html>
总结:
> 注意:插值表达式只能用在元素的**内容节点**中,不能用在元素的**属性节点**中!
+ 在 vue 中,可以使用 `v-bind:` 指令,为元素的属性动态绑定值;
+ 简写是英文的 `:`
+ 在使用 v-bind 属性绑定期间,如果绑定内容需要进行动态拼接,则字符串的外面应该包裹单引号,例如:
<div :title=" 'box' + index">这是一个 div</div>
1.3 事件绑定指令:v-on【methods节点中进行声明】
vue 提供了 v-on 事件绑定指令,用来辅助程序员为 DOM 元素绑定事件监听。语法格式如下:
注意:原生 DOM 对象有 onclick、oninput、onkeyup 等原生事件,替换为 vue 的事件绑定形式后, 分别为:v-on:click(@click)、v-on:input(@input)、v-on:keyup(@keyup)
通过 v-on 绑定的事件处理函数,需要在 methods 节点中进行声明:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
<div id="app">
<p>count 的值是 {{ count }}</p>
<!-- <button v-on:click="add">+1</button> -->
<button @click="add">+1</button>
<!-- <button v-on:cilck="sub">-1</button> -->
<button @cilck="sub">-1</button>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
count: 0
},
// methods 的作用,就是给v-on定义事件的处理函数
methods:{
add(){//add:function(){
console.log('ok');
},
sub(){//sub:function()
console.log('触发了sub处理函数');
}
}
})
</script>
</body>
</html>
事件绑定的简写形式:@
由于 v-on 指令在开发中使用频率非常高,因此,vue 官方为其提供了简写形式(简写为英文的 @ )。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
<div id="app">
<p>count 的值是 {{ count }}</p>
<!-- <button v-on:click="add">+1</button> -->
<button @click="add">+1</button>
<!-- <button v-on:cilck="sub">-1</button> -->
<button @cilck="sub">-1</button>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
count: 0
},
// methods 的作用,就是给v-on定义事件的处理函数
methods:{
add(){//add:function(){
console.log('ok');
},
sub(){//sub:function()
console.log('触发了sub处理函数');
}
}
})
</script>
</body>
</html>
事件参数对象:实例对象===this【使用this来调用】
在原生的 DOM 事件绑定中,可以在事件处理函数的形参处,接收事件参数对象 event。同理,在 v-on 指令 (简写为 @ )所绑定的事件处理函数中,同样可以接收到事件参数对象 event,示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
<div id="app">
<p>count 的值是 {{ count }}</p>
<!-- <button v-on:click="add">+1</button> -->
<button @click="add">+1</button>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
count: 0
},
// methods 的作用,就是给v-on定义事件的处理函数
methods:{
add(){//add:function(){
console.log(vm);
// console.log(vm===this);//true
// vm.count+=1;
//当点击add的时候,count+1
this.count+=1;
}
}
}
})
</script>
</body>
</html>
绑定事件并传参:可以使用(参数)进行传参
<!-- 当点击add的时候,一起将参数进行传入 -->
<button @click="add(4)">+4</button>
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
count: 0
},
// methods 的作用,就是给v-on定义事件的处理函数
methods:{
add(n){//add:function(){
// 在 methods 处理函数中,this 就是 new 出来的 vm 实例对象
console.log(vm);
// console.log(vm===this);//true
// vm.count+=1;
//当点击add的时候,count+1
this.count+=1;
//当点击add的时候,一起将参数进行传入
this.count+=n;
}
}
})
</script>
$event:表示原生的事件参数对象event
$event 是 vue 提供的特殊变量,用来表示原生的事件参数对象 event。$event 可以解决事件参数对象 event 被覆盖的问题。示例用法如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
<div id="app">
<p>count 的值是:{{ count }}</p>
<!-- 如果 count 是偶数,则 按钮背景变成红色,否则,取消背景颜色 -->
<!-- <button @click="add(1)">+N</button> -->
<!-- vue 提供了内置变量,名字叫做 $event,它就是原生 DOM 的事件对象 e -->
<button @click="add($event, 1)">+N</button>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
count: 0
},
methods: {
add(e,n){
this.count+=n;
console.log(e);
// 判断 this.count 的值是否为偶数
if(this.count%2==0){
//偶数
// e.target---鼠标点击事件
e.target.style.backgroundColor = 'red'
}else{
//奇数
e.target.style.backgroundColor = ''
}
}
},
})
</script>
</body>
</html>
事件修饰符:【prevent,stop】@click.stop="divHandler"
在事件处理函数中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。因此, vue 提供了事件修饰符的概念,来辅助程序员更方便的对事件的触发进行控制。常用的 5 个事件修饰符如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
<div id="app">
<a href="http://www.baidu.com" @click.prevent="show">跳转到百度页面</a>
<hr>
<div style="height: 150px;background-color: orange;padding-left: 100px;line-height: 150px;" @click.stop="divHandler">
<button @click.stop="btnHandler">按钮</button>
</div>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {},
methods:{
show(e){
// 阻止事件e的默认行为
// e.preventDefault()
console.log('点击了a');
},
btnHandler(){//btnHandler(e){e.stop();//阻止冒泡}
console.log('btnHandler');
},
divHandler(){
console.log('divHandler');
}
}
})
</script>
</body>
</html>
按键修饰符【只能修饰键盘事件】
在监听键盘事件时,我们经常需要判断详细的按键。此时,可以为键盘相关的事件添加按键修饰符,例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
<div id="app">
<!-- @keyup.esc="clearInput"---表示当我们按下键盘中的“esc”的时候,就触发clearInput事件 -->
<input type="text" @keyup.esc="clearInput" @keyup.enter="commitAjax" >
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {},
methods:{
clearInput(e){//e--接收事件
console.log('触发了clearInput事件');
e.target.value='';
},
commitAjax(){
console.log('触发了commitAjax请求');
}
}
})
</script>
</body>
</html>
1.4 双向绑定指令:v-model
vue 提供了 v-model 双向数据绑定指令,用来辅助开发者在不操作 DOM 的前提下,快速获取表单的数据。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
<div id="app">
<p>用户的名字是:{{ username }}</p>
<!-- 当在页面对v-model中的文字进行修改,vm中的data也会被修改【双向数据修改】 -->
<input type="text" v-model="username">
<hr>
<!-- value--是文本框中的默认值,如果这个value中的值被修改,数据源并不会改变【单向数据修改】 -->
<input type="text" :value="username">
<hr>
<!-- v-model只能跟表单元素进行交互:input,textarea,select -->
<select v-model="city">
<option value="">请选择城市</option>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
</select>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
username: 'zhangsan',
// 默认选中value为2的
city: '2'
}
})
</script>
</body>
</html>
v-model 指令的修饰符:v-model.number="n1"
为了方便对用户输入的内容进行处理,vue 为 v-model 指令提供了 3 个修饰符,分别是:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们把数据填充到 div 内部 -->
<div id="app">
<!-- v-model.number="" 以防在进行数值计算的时候输入的是字符串 -->
<input type="text" v-model.number="n1">+<input type="text" v-model.number="n2">=<span>{{n1+n2 }}</span>
<hr>
<!-- v-model.trim="" 去除username中前后的空格 中间的空格不去除 -->
<input type="text" v-model.trim="username">
<button @click="showName">获取用户名</button>
<hr>
<!-- v-model.lazy="" 防抖,不会每一次删除文本框中的文字的时候就进行更新,而是在删除结束后(就是失去焦点的时候)才更新 -->
<input type="text" v-model.lazy="username">
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
username: 'zhangsan',
n1: 1,
n2: 2
},
methods:{
showName(){
// 里面用的是模板字符串
console.log(`用户名是"${this.username}"`)
}
}
})
</script>
</body>
</html>
1.5 条件渲染指令:v-if/v-show
条件渲染指令用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个,分别是: ⚫ v-if
⚫ v-show
v-if 和 v-show 的区别
1. `v-show` 的原理是:动态为元素添加或移除 `display: none` 样式,来实现元素的显示和隐藏
如果要频繁的切换元素的显示状态,用 v-show 性能会更好
2. `v-if` 的原理是:每次动态创建或移除元素,实现元素的显示和隐藏
如果刚进入页面的时候,某些元素默认不需要被展示,而且后期这个元素很可能也不需要被展示出来,此时 v-if 性能更好> 在实际开发中,绝大多数情况,不用考虑性能问题,直接使用 v-if 就好了!!!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们把数据填充到 div 内部 -->
<div id="app">
<!-- 当flag=false的时候 v-if 会被动态删除,v-show 会添加上style="none" -->
<p v-if="flag">这是被 v-if 控制的元素</p>
<p v-show="flag">这是被 v-show 控制的元素</p>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
// 如果 flag 为 true,则显示被控制的元素;如果为 false 则隐藏被控制的元素
flag: false,
}
})
</script>
</body>
</html>
v-else:后面不用加条件
v-else-if:一定要跟v-if一起使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们把数据填充到 div 内部 -->
<hr>
<div v-if="type === 'A'">优秀</div>
<div v-else-if="type === 'B'">良好</div>
<div v-else-if="type === 'C'">一般</div>
<div v-else>差</div>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
type: 'A'
}
})
</script>
</body>
</html>
1.6 列表渲染指令:v-for【data:{list:[]}】
vue 提供了 v-for 列表渲染指令,用来辅助开发者基于一个数组来循环渲染一个列表结构。
v-for 指令需要使 用 item in items 形式的特殊语法,其中:
⚫ items 是待循环的数组
⚫ item 是被循环的每一项
v-for 中的索引:(item, index) in items
v-for 指令还支持一个可选的第二个参数,即当前项的索引。语法格式为 (item, index) in items,示例代码如下:
注意:v-for 指令中的 item 项和 index 索引都是形参,可以根据需要进行重命名。例如 (user, i) in userlist
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./lib/bootstrap.css">
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们把数据填充到 div 内部 -->
<div id="app">
<table class="table table-bordered table-hover table-striped">
<thead>
<th>索引</th>
<th>Id</th>
<th>姓名</th>
</thead>
<tbody>
<!-- item in list---item可以自己定 -->
<tr v-for="(item, index) in list" >
<!-- index--索引号 -->
<td>{{ index }}</td>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
list: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' },
{ id: 4, name: '张三' },
]
}
})
</script>
</body>
</html>
使用 key 维护列表的状态:要用到了 v-for 指令,那么一定要绑定一个 :key 属性(尽量使用id)
当列表的数据变化时,默认情况下,vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能。但这种 默认的性能优化策略,会导致有状态的列表无法被正确更新。 为了给 vue 一个提示,以便它能跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲 染的性能。此时,需要为每项提供一个唯一的 key 属性:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./lib/bootstrap.css">
</head>
<body>
<!-- 希望 Vue 能够控制下面的这个 div,帮我们把数据填充到 div 内部 -->
<div id="app">
<table class="table table-bordered table-hover table-striped">
<thead>
<th>索引</th>
<th>Id</th>
<th>姓名</th>
</thead>
<tbody>
<!-- 官方建议:只要用到了 v-for 指令,那么一定要绑定一个 :key 属性 -->
<!-- 而且,尽量把 id 作为 key 的值 -->
<!-- 官方对 key 的值类型,是有要求的:字符串或数字类型 -->
<!-- key 的值是千万不能重复的,否则会终端报错:Duplicate keys detected -->
<!-- item in list---item可以自己定 -->
<tr v-for="(item, index) in list" :key="item.id">
<!-- index--索引号 -->
<td>{{ index }}</td>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
<script src="./lib/vue-2.6.12.js"></script>
<!-- 2. 创建 Vue 的实例对象 -->
<script>
// 创建 Vue 的实例对象
const vm = new Vue({
// el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
el: '#app',
// data 对象就是要渲染到页面上的数据
data: {
list: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' },
{ id: 4, name: '张三' },
]
}
})
</script>
</body>
</html>
key 的注意事项
① key 的值只能是字符串或数字类型
② key 的值必须具有唯一性(即:key 的值不能重复)
③ 建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)
④ 使用 index 的值当作 key 的值没有任何意义(因为 index 的值不具有唯一性)--因为index跟数据没有强制的绑定关系
⑤ 建议使用 v-for 指令时一定要指定 key 的值(既提升性能、又防止列表状态紊乱)
key不能选索引值为key值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 在页面中声明一个将要被 vue 所控制的 DOM 区域 -->
<div id="app">
<!-- 添加用户的区域 -->
<div>
<input type="text" v-model="name">
<button @click="addNewUser">添加</button>
</div>
<!-- 用户列表区域 -->
<ul>
<li v-for="(user, index) in userlist" :key="user.index">
<input type="checkbox" />
姓名:{{user.name}}
</li>
</ul>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 用户列表
userlist: [
{ id: 1, name: 'zs' },
{ id: 2, name: 'ls' }
],
// 输入的用户名
name: '',
// 下一个可用的 id 值
nextId: 3
},
methods: {
// 点击了添加按钮
addNewUser() {
this.userlist.unshift({ id: this.nextId, name: this.name })
this.name = ''
this.nextId++
}
},
})
</script>
</body>
</html>
小案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>品牌列表案例</title>
<link rel="stylesheet" href="./lib/bootstrap.css">
<link rel="stylesheet" href="./css/brandlist.css">
</head>
<body>
<div id="app">
<!-- 卡片区域 -->
<div class="card">
<div class="card-header">
添加品牌
</div>
<div class="card-body">
<!-- 添加品牌的表单区域 -->
<!-- form 表单元素有 submit 事件 -->
<!-- 阻止按钮的默认提交行为 并触发add这个行为 -->
<form @submit.prevent="add">
<div class="form-row align-items-center">
<div class="col-auto">
<div class="input-group mb-2">
<div class="input-group-prepend">
<div class="input-group-text">品牌名称</div>
</div>
<!-- 给添加按钮添加点击事件 -->
<!-- 因为我们点击按钮要获取文本框中用户输入的内容,最快的方法就是直接使用v-model双向获取数据 -->
<input type="text" class="form-control" placeholder="请输入品牌名称" v-model.trim="brand">
</div>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-2">添加</button>
</div>
</div>
</form>
</div>
</div>
<!-- 表格区域 -->
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">品牌名称</th>
<th scope="col">状态</th>
<th scope="col">创建时间</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in list" :key="item.id">
<td>{{ item.id }}</td>
<td>{{item.name}}</td>
<td>
<div class="custom-control custom-switch">
<!-- 因为表单中的禁用和启动是双向的数据交互,所以使用v-model -->
<!-- 使用 v-model 实现双向数据绑定 -->
<!-- 给id添加动态绑定,让id后面跟着item.id -->
<input type="checkbox" class="custom-control-input" :id="'cb' + item.id" v-model="item.status">
<!-- 当statue为true的时候,就为启动,为false的时候为禁用,所以要使用v-if和v-else -->
<!-- 使用 v-if 结合 v-else 实现按需渲染 -->
<label class="custom-control-label" :for="'cb' + item.id" v-if="item.status">已启用</label>
<label class="custom-control-label" :for="'cb' + item.id" v-else>已禁用</label>
</div>
</td>
<td>{{item.time}}</td>
<td>
<!-- 给删除按钮绑定一个点击事件 并且传入要进行删除的id -->
<a href="javascript:;" @click="remove(item.id)">删除</a>
</td>
</tr>
</tbody>
</table>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
//1.创建实例对象
const vm=new Vue({
el:'#app',
data:{
//用户输入的匹配名称
brand:'',
// nextId 是下一个,可用的 id
nextId: 4,
//品牌的列表数据
list:[
{id:1, name:'宝马', status:true, time:new Date()},
{id:2, name:'奔驰', status:true, time:new Date()},
{id:3, name:'奥迪', status:true, time:new Date()},
]
},
methods:{
//点击链接,删除对应的数据
remove(id){
// console.log(id);
// filter--过滤,返回一个数组
// filter--返回不满足条件的数据
this.list=this.list.filter(item=>item.id!==id)
},
//阻止表单的默认提交行为之后,触发add行为
add(){
// console.log(this.brand);
// 如果判断brand的值为空字符串,则return出去
if(this.brand===''){
alert('必须填写汽车名称')
return
}
// 如果没有被 return 出去,应该执行添加的逻辑
// 1. 先把要添加的品牌对象,整理出来
const obj={
id:this.nextId,
name:this.brand,
status:true,
time:new Date()
}
//2.把this.list数组中push步骤1中得到的对象
this.list.push(obj)
// 3.清空this.brand,让this.nextId自增1
this.brand=''
this.nextId++;
}
}
})
</script>
</body>
</html>
2. 过滤器:过滤器本质上是函数
过滤器(Filters)是 vue 为开发者提供的功能,常用于文本的格式化。过滤器可以用在两个地方:插值表达式 和 v-bind 属性绑定。
过滤器应该被添加在 JavaScript 表达式的尾部,由“管道符”进行调用,示例代码如下:
2.1 定义过滤器:在filters节点中定义【过滤器中,一定要有一个返回值】
在创建 vue 实例期间,可以在 filters 节点中定义过滤器,示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- “| ”:管道符 -->
<!-- 将message的值传递给capi,然后capi将新得到的值return出来 -->
<p>message 的值是:{{ message | capi }}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue.js'
},
// 过滤器函数,必须被定义到 filters 节点之下
// 过滤器本质上是函数
filters: {
// 注意:过滤器函数形参中的 val,永远都是“管道符”前面的那个值
capi(val) {
// 字符串有 charAt 方法,这个方法接收索引值,表示从字符串中把索引对应的字符,获取出来
// val.charAt(0)--获取第一个字母
const first = val.charAt(0).toUpperCase()
// 字符串的 slice 方法,可以截取字符串,从指定索引往后截取
const other = val.slice(1)
// 强调:过滤器中,一定要有一个返回值
return first + other
}
}
})
</script>
</body>
</html>
1. 要定义到 filters 节点下,本质是一个函数
2. 在过滤器函数中,一定要有 return 值
3. 在过滤器的形参中,可以获取到“管道符”前面待处理的那个值
2.2 私有过滤器和全局过滤器(Vue.filter('函数名',构造函数))【注意:是filter不是filters】
在 filters 节点下定义的过滤器,称为“私有过滤器”,因为它只能在当前 vm 实例所控制的 el 区域内使用。 如果希望在多个 vue 实例之间共享过滤器,则可以按照如下的格式定义全局过滤器2.3 连续调用多个过滤器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 如果自己有过滤器,择调用自己的过滤器 -->
<p>message 的值是:{{ message | capi }}</p>
</div>
<div id="app2">
<p>message 的值是:{{ message | capi }}</p>
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
// 使用 Vue.filter() 定义全局过滤器
Vue.filter('capi', function (str) {
const first = str.charAt(0).toUpperCase()
const other = str.slice(1)
return first + other + '~~~'
})
const vm = new Vue({
el: '#app',
data: {
message: 'hello vue.js'
},
// 过滤器函数,必须被定义到 filters 节点之下
// 过滤器本质上是函数
// 这个filters是私有过滤器,外部不能使用
filters: {
// 注意:过滤器函数形参中的 val,永远都是“管道符”前面的那个值
capi(val) {
// 字符串有 charAt 方法,这个方法接收索引值,表示从字符串中把索引对应的字符,获取出来
// val.charAt(0)
const first = val.charAt(0).toUpperCase()
// 字符串的 slice 方法,可以截取字符串,从指定索引往后截取
const other = val.slice(1)
// 强调:过滤器中,一定要有一个返回值
return first + other
}
}
})
// ----------------------------------
const vm2 = new Vue({
el: '#app2',
data: {
message: 'heima'
}
})
</script>
</body>
</html>
如果全局过滤器和私有过滤器名字一致,此时按照“就近原则”,调用的是”私有过滤器“
使用全局过滤器对时间进行格式化
<!-- 使用名为dateFormat这个过滤器,使得时间进行格式化 -->
<td>{{ item.time | dateFormat }}</td>
<!-- 只要导入了 dayjs 的库文件,在 window 全局,就可以使用 dayjs() 方法了 -->
<script src="./lib/dayjs.min.js"></script>
<script>
// 声明格式化时间的全局过滤器
Vue.filter('dateFormat', function (time) {
// 1. 对 time 进行格式化处理,得到 YYYY-MM-DD HH:mm:ss
// 2. 把 格式化的结果,return 出去
// 直接调用 dayjs() 得到的是当前时间
// dayjs(给定的日期时间) 得到指定的日期
const dtStr = dayjs(time).format('YYYY-MM-DD HH:mm:ss')
return dtStr
})
</script>
2.3 连续调用多个过滤器 :过滤器可以串联地进行调用
2.4 过滤器传参:本质是 JavaScript 函数,因此可以接收参数
2.5 过滤器的兼容性:vue3.x中没有过滤器
5.品牌列表案例
1. 案例效果
3. 整体实现步骤
总结
6.侦听器
1. 什么是 watch 侦听器:允许开发者监视数据的变化,从而针对数据的变化做特定的操作
2.监听器定义:应该被定义到 watch 节点下
本质上是一个函数,要监视哪个数据的变化,就把数据名作为方法名即可
新值在前,旧值在后 username(newVal,oldVal)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="username">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/jquery-v3.6.0.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username: 'admin'
},
// 所有的侦听器,都应该被定义到 watch 节点下
watch: {
// 侦听器本质上是一个函数,要监视哪个数据的变化,就把数据名作为方法名即可
// 新值在前,旧值在后 username(newVal,oldVal)
username(newVal,oldVal) {
console.log('username进行监听',newVal,oldVal);
}
}
})
</script>
</body>
</html>
3. 使用 watch 检测用户名是否可用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="username">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/jquery-v3.6.0.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username: 'admin'
},
// 所有的侦听器,都应该被定义到 watch 节点下
watch: {
// 侦听器本质上是一个函数,要监视哪个数据的变化,就把数据名作为方法名即可
// 新值在前,旧值在后
username(newVal) {
if (newVal === '') return
// 1. 调用 jQuery 中的 Ajax 发起请求,判断 newVal 是否被占用!!!
$.get('https://www.escook.cn/api/finduser/' + newVal, function (result) {
console.log(result)
})
}
}
})
</script>
</body>
</html>
4. 对象格式监听器:immediate 选项(immediate 选项的默认值是 false)
默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使 用 immediate 选项。示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="username">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/jquery-v3.6.0.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
username: 'admin'
},
// 所有的侦听器,都应该被定义到 watch 节点下
watch: {
// 定义对象格式的侦听器
username: {
// 侦听器的处理函数
// 当监听到username的时候,会自动触发handler这个函数
handler(newVal, oldVal) {
console.log(newVal, oldVal)
},
// immediate 选项的默认值是 false
// immediate为true表示进入的时候自动触发监听
// immediate 的作用是:控制侦听器是否自动触发一次!
immediate: true
}
}
})
</script>
</body>
</html>
5. 对象格式监听器:deep 选项(监听对象中的属性值)
如果 watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 选 项,代码示例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 因为data数据中的info是一个对象,要进行深度拷贝才可以访问到 -->
<input type="text" v-model="info.username">
<input type="text" v-model="info.address.city">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/jquery-v3.6.0.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 用户的信息对象
info: {
username: 'admin',
address: {
city: '北京'
}
}
},
// 所有的侦听器,都应该被定义到 watch 节点下
watch: {
/* 并不会触发侦听器,因为属性是一个对象
info(newVal){
console.log(newVal);
}*/
//这个是侦听整个对象
info: {
handler(newVal) {
console.log(newVal)
},
// 开启深度监听,只要对象中任何一个属性变化了,都会触发“对象的侦听器”
deep: true
}
}
})
</script>
</body>
</html>
6.对象监听器和方法监听器的优缺点
1. 方法格式的侦听器
+ 缺点1:无法在刚进入页面的时候,自动触发!!!
+ 缺点2:如果侦听的是一个对象,如果对象中的属性发生了变化,不会触发侦听器!!!
2. 对象格式的侦听器
+ 好处1:可以通过 **immediate** 选项,让侦听器自动触发!!!
+ 好处2:可以通过 **deep** 选项,让侦听器深度监听对象中每个属性的变化!!!
7. 监听对象单个属性的变化
如果只想监听对象中单个属性的变化,则可以按照如下的方式定义 watch 侦听器:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 因为data数据中的info是一个对象,要进行深度拷贝才可以访问到 -->
<input type="text" v-model="info.username">
<input type="text" v-model="info.address.city">
</div>
<script src="./lib/vue-2.6.12.js"></script>
<script src="./lib/jquery-v3.6.0.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
// 用户的信息对象
info: {
username: 'admin',
address: {
city: '北京'
}
}
},
// 所有的侦听器,都应该被定义到 watch 节点下
watch: {
/* 并不会触发侦听器,因为属性是一个对象
info(newVal){
console.log(newVal);
}*/
// 如果要侦听的是对象的子属性的变化,则必须包裹一层单引号
'info.username'(newVal) {
console.log(newVal)
}
}
})
</script>
</body>
</html>
7.计算属性
1. 什么是计算属性:computed:{}
计算属性指的是通过一系列运算之后,最终得到一个属性值。
这个动态计算出来的属性值可以被模板结构或 methods 方法使用。示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="./lib/vue-2.6.12.js"></script>
<style>
.box {
width: 200px;
height: 200px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div id="app">
<div>
<span>R:</span>
<input type="text" v-model.number="r">
</div>
<div>
<span>G:</span>
<input type="text" v-model.number="g">
</div>
<div>
<span>B:</span>
<input type="text" v-model.number="b">
</div>
<hr>
<!-- 专门用户呈现颜色的 div 盒子 -->
<!-- 在属性身上,: 代表 v-bind: 属性绑定 -->
<!-- :style 代表动态绑定一个样式对象,它的值是一个 { } 样式对象 -->
<!-- 当前的样式对象中,只包含 backgroundColor 背景颜色 -->
<div class="box" :style="{ backgroundColor: `rgb(${r}, ${g}, ${b})` }">
{{ `rgb(${r}, ${g}, ${b})` }}
</div>
<button @click="show">按钮</button>
</div>
<script>
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
// 红色
r: 0,
// 绿色
g: 0,
// 蓝色
b: 0
},
methods: {
// 点击按钮,在终端显示最新的颜色
show() {
console.log(`rgb(${this.r}, ${this.g}, ${this.b})`)
}
},
});
</script>
</body>
</html>
2. 计算属性的特点
① 虽然计算属性在声明的时候被定义为方法,但是计算属性的本质是一个属性
② 计算属性会缓存计算的结果,只有计算属性依赖的数据变化时,才会重新进行运算
特点:
1. 定义的时候,要被定义为“方法”
2. 在使用计算属性的时候,当普通的属性使用即可好处:
1. 实现了代码的复用
2. 只要计算属性中依赖的数据源变化了,则计算属性会自动重新求值!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="./lib/vue-2.6.12.js"></script>
<style>
.box {
width: 200px;
height: 200px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div id="app">
<div>
<span>R:</span>
<input type="text" v-model.number="r">
</div>
<div>
<span>G:</span>
<input type="text" v-model.number="g">
</div>
<div>
<span>B:</span>
<input type="text" v-model.number="b">
</div>
<hr>
<!-- 专门用户呈现颜色的 div 盒子 -->
<!-- 在属性身上,: 代表 v-bind: 属性绑定 -->
<!-- :style 代表动态绑定一个样式对象,它的值是一个 { } 样式对象 -->
<!-- 当前的样式对象中,只包含 backgroundColor 背景颜色 -->
<div class="box" :style="{ backgroundColor: rgb }">
{{ rgb }}
</div>
<button @click="show">按钮</button>
</div>
<script>
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
// 红色
r: 0,
// 绿色
g: 0,
// 蓝色
b: 0
},
methods: {
// 点击按钮,在终端显示最新的颜色
show() {
console.log(this.rgb)
}
},
// 所有的计算属性,都要定义到 computed 节点之下
// 计算属性在定义的时候,要定义成“方法格式”
computed: {
// rgb 作为一个计算属性,被定义成了方法格式,
// 最终,在这个方法中,要返回一个生成好的 rgb(x,x,x) 的字符串
// rgb在声明的时候是一个函数,但是实际在vm对象中是一个属性,所以可以直接vm.rgb来调用
rgb() {
return `rgb(${this.r}, ${this.g}, ${this.b})`
}
}
});
console.log(vm)
</script>
</body>
</html>
8.axios数据请求
1. 什么是axios:是专注于网络数据请求的库
2. axios发起GET请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 导入axios -->
<script src="./lib/axios.js"></script>
<script>
// http://www.liulongbin.top:3006/api/getbooks
// 1. 调用 axios 方法得到的返回值是 Promise 对象
axios({
// 请求方式
method: 'GET',
// 请求的地址
url: 'http://www.liulongbin.top:3006/api/getbooks',
// URL 中的查询参数
}).then(function (result) {
// result是套完数据之后的
console.log(result)
})
</script>
</body>
</html>
3. axios传参
1. 直接使用axios发起GET请求:使用params属性
// 1. 调用 axios 方法得到的返回值是 Promise 对象
axios({
// 请求方式
method: 'GET',
// 请求的地址
url: 'http://www.liulongbin.top:3006/api/getbooks',
// URL 中的查询参数
params: {
id: 1
},
}).then(function (result) {
console.log(result)
})
2. 直接使用axios发起POST请求:使用data属性
// 1. 调用 axios 方法得到的返回值是 Promise 对象
axios({
// 请求方式
method: 'GET',
// 请求的地址
url: 'http://www.liulongbin.top:3006/api/getbooks',
// 请求体参数
data: {}
}).then(function (result) {
console.log(result)
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btnPost">发起post请求</button>
<script src="./lib/axios.js"></script>
<script>
document.querySelector('#btnPost').addEventListener('click',function(){
axios:({
method:'POST',
url:'http://www.liulongin.top:3006/api/post',
data:{
name:'zs',
age:20
}
}).then(function(result){
console.log(result);
})
})
</script>
</body>
</html>
4. 结合async和await调用axios
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btnPost">发起POST请求</button>
<button id="btnGet">发起GET请求</button>
<script src="./lib/axios.js"></script>
<script>
document.querySelector('#btnPost').addEventListener('click', async function () {
// 如果调用某个方法的返回值是 Promise 实例,则前面可以添加 await!
// await 只能用在被 async “修饰”的方法中
//const result=await axios---其中result不是真正的数据 真正的数据是result.data()
const { data } = await axios({
method: 'POST',
url: 'http://www.liulongbin.top:3006/api/post',
data: {
name: 'zs',
age: 20
}
})
console.log(data)
})
document.querySelector('#btnGet').addEventListener('click', async function () {
// 解构赋值的时候,使用 : 进行重命名
// 1. 调用 axios 之后,使用 async/await 进行简化
// 2. 使用解构赋值,从 axios 封装的大对象中,把 data 属性解构出来
// 3. 把解构出来的 data 属性,使用 冒号 进行重命名,一般都重命名为 { data: res }
const { data: res } = await axios({
method: 'GET',
url: 'http://www.liulongbin.top:3006/api/getbooks'
})
console.log(res.data)
})
// $.ajax() $.get() $.post()
// axios() axios.get() axios.post() axios.delete() axios.put()
</script>
</body>
</html>
5.axios.get
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btnGET">GET</button>
<script src="./lib/axios.js"></script>
<script>
document.querySelector('#btnGET').addEventListener('click', async function () {
/* axios.get('url地址', {
// GET 参数
params: {}
}) */
// 将data重命名为res
const { data: res } = await axios.get('http://www.liulongbin.top:3006/api/getbooks', {
params: { id: 1 }
})
console.log(res)
})
</script>
</body>
</html>
6.axios.post
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btnPOST">POST</button>
<script src="./lib/axios.js"></script>
<script>
document.querySelector('#btnPOST').addEventListener('click', async function () {
// axios.post('url', { /* POST 请求体数据 */ })
const { data: res } = await axios.post('http://www.liulongbin.top:3006/api/post', { name: 'zs', gender: '女' })
console.log(res)
})
</script>
</body>
</html>
9.vue-cli
1. 什么是单页面应用程序(SPA)
2. 什么是 vue-cli:自动搭建Webpack
3. 安装和使用:全局变量
4. vue 项目的运行流程
1.快速生成vue项目:vue create 项目名称(记得再要创建的地址(再当前地址上cmd)上进行执行,并且项目的名字不要有中文和空格)
4.1 了解src目录的构建
assets 文件夹:存放项目中用到的静态资源文件,例如:css 样式表、图片资源
components 文件夹:程序员封装的、可复用的组件,都要放到 components 目录下
main.js 是项目的入口文件。整个项目的运行,要先执行 main.js
App.vue 是项目的根组件。
4.2 vue项目运行的过程
在工程化的项目中,vue 要做的事情很单纯:通过 main.js(渲染的方式记录) 把 App.vue (要渲染的UI结构)渲染到 index.html (将app.vue渲染到index.html)的指定区域中。
4.3 vue-cli组件的基本使用
4.4 .$mount():作用和el属性完全一样
//创建Vue实例对象
new Vue({
//将写的这个实例挂载到app上
// el:'#app',
// 把render函数指定的组件,渲染到HTML页面中
render: h => h(Test),
}).$mount('#app')//跟上面的el:'#app' 效果一样
// Vue实例的$mount()方法,作用和el属性完全一样
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">{{username}}</div>
<script src="./lib/vue-2.6.12.js"></script>
<script>
const vm = new Vue({
data: {
username: 'admin'
}
})
// 指定调用app这个位置的元素
vm.$mount('#app')
</script>
</body>
</html>
10.vue组件
1. 什么是组件化开发
组件化开发指的是:根据封装的思想,把页面上可重用的 UI 结构封装为组件,从而方便项目的开发和维护.
2. vue 中的组件化开发
3. vue 组件(对UI结构的复用)的三个组成部分
3.1 template:声明组件的UI结构
vue 规定:每个组件对应的模板结构,需要定义到 <template> 节点中。
注意:
template 是 vue 提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的 DOM 元素
template 中只能包含唯一的根节点 【一个template只能包含唯一一个div】
<!-- 声明组件的UI结构 -->
<template>
<div class="test-box">
<h3>这是用户自定义的组件 -- {{ username }}</h3>
</div>
</template>
3.2 script:定义组件的行为(数据,调用方法)
vue 规定:开发者可以在<script>节点中封装组件的JavaScript业务逻辑
1. 必须写::默认导出。固定写法--exprot default{}
2.vue 组件中的 data 必须是一个函数,不能直接指向一个数据对象
.vue 组件中的 data 必须是函数
<!-- 定义组件的行为(数据,调用方法) -->
<script>
// 默认导出。固定写法!!!!!!!!
export default{
// datat数据源
// 注意:.vue组件中的data不能像之前一样,不能指向对象
// 注意:组件中的data必须是一个函数
// data:{
// username:'zs'
// }
// data:function(){}
data(){
// 这个return出去的{}中 可以定义数据
return {
username:'admin'
}
}
}
</script>
3.3 style :定义样式结构
vue 规定:组件内的<style>节点是可选的,开发者可以再<style>节点中编写样式美化当前组件的UI结构
<!-- 定义样式结构 -->
<style>
.test-box{
background-color: pink;
}
</style>
让 style 中支持 less 语法 :在style属性上加上[lang="less"]
<!-- 定义样式结构 -->
<style lang="less">
.test-box{
background-color: pink;
}
h3 {
color: red;}
</style>
4.组件中定义methods方法
5.组件之间的父子关系
4.1 使用组件的三个步骤
4.2 通过 components 注册的是私有子组件
4.3 注册全局组件:在main.js文件中进行注册【自己不能调用自己】
在 vue 项目的 main.js 入口文件中,通过 Vue.component() 方法,可以注册全局组件。
// 导入需要被全局注册的那个组件
import Count from '@/components/Count.vue'
// 注册
// 参数1:字符串格式,表示组件的“注册名称”,可以自己取
// 参数2:需要被全局注册的那个组件
Vue.component('MyCount',Count)
<!-- 因为在main.js文件中自己定义的名字就是MyCount所以标签名就为MyCount -->
<MyCount></MyCount>
5. 组件的 props:自定义属性
props 是组件的自定义属性,在封装通用组件的时候,合理地使用 props 可以极大的提高组件的复用性!
<script>
export default {
//props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
// 这个"init"可以自己取
props:['init'],
data(){
return {
count:0
}
},
methods:{
add(){
this.count+=1
}
}
}
</script>
5.1 props 是只读的:不能修改【可以将值转存到data中】
vue 规定:组件中封装的自定义属性是只读的,程序员不能直接修改 props 的值。否则会直接报错:
要想修改 props 的值,可以把 props 的值转存到 data 中,因为 data 中的数据都是可读可写的!
<template>
<div>
<h5>Count 组件</h5>
<p>count的值是:{{ count }}</p>
<button @click="count+=1">+1</button>
</div>
</template>
<script>
export default {
//props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
// 这个"init"可以自己取
// props中的数据,可以直接在模板结构中被使用
props:['init'],
data(){
return {
// 将props中的属性转存到count上
count:this.init
}
},
methods:{
show(){
console.log(this);
}
}
}
</script>
5.2 props 的 default 默认值:default:0
<template>
<div class="right-container">
<!-- 传入数字9 -->
<!-- <MyCount :init="9"></MyCount> -->
<!-- 用户没有输入数值,则使用props的默认值 -->
<MyCount ></MyCount>
</div>
</template>
<script>
export default {
//props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
// 这个"init"可以自己取
// props中的数据,可以直接在模板结构中被使用
// 注意:props是只读,必要直接修改props的值,否则终端报错
// props:['init'],
//数组格式的props不能指定默认值
// props:{
// 自定义属性A:{/*配置选项*/},
// 自定义属性A:{/*配置选项*/},
// 自定义属性A:{/*配置选项*/},
// }
props:{
init:{
// 如果外界使用Count组件的时候,没有传递init属性,则默认值生效
default:0
}
}
}
</script>
5.3 props 的 type 值类型:记得在标签中的init前加上冒号
<!-- 因为在main.js文件中自己定义的名字就是MyCount所以标签名就为MyCount -->
<MyCount :init="9"></MyCount>
<script>
export default {
//props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
// 这个"init"可以自己取
// props中的数据,可以直接在模板结构中被使用
// 注意:props是只读,必要直接修改props的值,否则终端报错
// props:['init'],
//数组格式的props不能指定默认值
// props:{
// 自定义属性A:{/*配置选项*/},
// 自定义属性A:{/*配置选项*/},
// 自定义属性A:{/*配置选项*/},
// }
props:{
init:{
// 如果外界使用Count组件的时候,没有传递init属性,则默认值生效
default:0,
// init的值类型必须是Number数字
// type:Number
}
}
}
</script>
5.4 props 的 required 必填项:如果用户没有输入属性值,则控制台报错
<!-- 因为在main.js文件中自己定义的名字就是MyCount所以标签名就为MyCount -->
<MyCount :init="9"></MyCount>
<script>
export default {
//props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
// 这个"init"可以自己取
// props中的数据,可以直接在模板结构中被使用
// 注意:props是只读,必要直接修改props的值,否则终端报错
// props:['init'],
//数组格式的props不能指定默认值
// props:{
// 自定义属性A:{/*配置选项*/},
// 自定义属性A:{/*配置选项*/},
// 自定义属性A:{/*配置选项*/},
// }
props:{
init:{
// 如果外界使用Count组件的时候,没有传递init属性,则默认值生效
default:0,
// init的值类型必须是Number数字
type:Number,
// 必填项校验,如果用户不传入init属性值,则强制报错
required:true
}
}
}
</script>
5.5 props和v-bind(“:”)使用自定义属性
6. 组件之间的样式冲突问题
默认情况下,写在 .vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。
导致组件之间样式冲突的根本原因是:
① 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的 ② 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素
6.1 思考:如何解决组件样式冲突的问题:【分配唯一的自定义属性】
为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域
6.2 style 节点的 scoped 属性:会自动添加上类选择器【用于解决组件之间的样式冲突】
为了提高开发效率和开发体验,vue 为 style 节点提供了 scoped 属性,从而防止组件之间的样式冲突问题
<style lang="less" scoped>
.left-container {
padding: 0 20px 20px;
background-color: orange;
min-height: 250px;
flex: 1;
}
h3 {
color: red;
}
</style>
6.3 /deep/ 样式穿透
如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样 式对子组件生效,可以使用 /deep/ 深度选择器
当使用第三方组件的时候,如果有修改组件默认样式的需求,需要用到/deep/
<style lang="less" scoped>
//没有加/deep/前:h5[data-v-3c83f0b7]
// 加上/deep/后:[data-v-3c83f0b7] h5 --->给父亲加上data-v-3c83f0b7这个类
// 当使用第三方组件的时候,如果有修改组件默认样式的需求,需要用到/deep/
// 这个/deep/就是通过父组件修改子组件
/deep/ h5 {
color: pink;
}
</style>
7.vue组件的实例对象
生成的.vue代码并不是直接到浏览器中进行实现,而且是通过package.json文件中的插件进行转换为js代码
在.vue文件中使用标签相当于new一个实例对象
<MyCount></MyCount>-->相当于创建了MyCounr这个实例
三、生命周期 & 数据共享
1.组件的生命周期
1. 生命周期 & 生命周期函数
注意:生命周期强调的是时间段,生命周期函数强调的是时间点。
3. 组件生命周期函数的分类
4. 生命周期图示
5.生命周期的实现过程
5.1 初步了解组件创建的过程
5.2 【组件的创建阶段】了解beforeCreate生命周期函数:组件中的props/data/methods不可用
<script>
export default {
props:['info'],
data(){
return {
message:'hello vue.js',
// 定义books数组,存储的是所有图书的列表数据,默认为空数组
books:[]
}
},
methods:{
show(){
console.log('调用了Test组件的show方法');
},
// 使用Ajax请求图书列表的数据
initBookList() {
const xhr = new XMLHttpRequest()
xhr.addEventListener('load', () => {
const result = JSON.parse(xhr.responseText)
// 获取对象
console.log(result)
this.books = result.data
})
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.send()
}
},
// 创建阶段的第一个生命周期---props,data,methods都处于不可用状态
beforeCreate(){
// console.log(this.info);//拿不到
// console.log(this.message);//拿不到
// this.show()//拿不到
}
}
</script>
5.3 【组件的创建阶段】了解created生命周期函数:组件中的props/data/methods可用
<template>
<div class="test-container">
<h3>Test.vue 组件 一共有{{ books.length }}本书</h3>
</div>
</template>
<script>
export default {
props:['info'],
data(){
return {
message:'hello vue.js',
// 定义books数组,存储的是所有图书的列表数据,默认为空数组
books:[]
}
},
methods:{
show(){
console.log('调用了Test组件的show方法');
},
// 使用Ajax请求图书列表的数据
initBookList() {
const xhr = new XMLHttpRequest()
xhr.addEventListener('load', () => {
const result = JSON.parse(xhr.responseText)
// 获取对象
console.log(result)
this.books = result.data
})
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.send()
}
},
//创建阶段的第二个生命周期---props,data,methods都处于可用状态
// 在这个状态下可以调用Ajax请求拿数据
// created生命周期函数,非常常用
// 经常在它里面,调用methods中的方法,请求服务器的数据
// 并且,把请求到的数据,转存到data中,供template模板渲染的时候使用
created(){
console.log(this.info);
console.log(this.message);
this.show(),
//可以访问到data数据
this.initBookList()
}
}
</script>
5.4【组件的创建阶段】 了解beforeMount生命周期函数: 未拿到DOM结构
<script>
export default {
props:['info'],
data(){
return {
message:'hello vue.js',
// 定义books数组,存储的是所有图书的列表数据,默认为空数组
books:[]
}
},
methods:{
show(){
console.log('调用了Test组件的show方法');
},
// 使用Ajax请求图书列表的数据
initBookList() {
const xhr = new XMLHttpRequest()
xhr.addEventListener('load', () => {
const result = JSON.parse(xhr.responseText)
// 获取对象
console.log(result)
this.books = result.data
})
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.send()
}
},
// 将要渲染DOM结构,但是没有拿到DOM元素
beforeMount(){
console.log('beforeMount');
const dom =document.querySelector('#myh3')
console.log(dom);//拿不到dom元素
}
}
</script>
5.5【组件的创建阶段】 了解mounted生命周期函数: 将DOM结构渲染到页面上了
<script>
export default {
props:['info'],
data(){
return {
message:'hello vue.js',
// 定义books数组,存储的是所有图书的列表数据,默认为空数组
books:[]
}
},
methods:{
show(){
console.log('调用了Test组件的show方法');
},
// 使用Ajax请求图书列表的数据
initBookList() {
const xhr = new XMLHttpRequest()
xhr.addEventListener('load', () => {
const result = JSON.parse(xhr.responseText)
// 获取对象
console.log(result)
this.books = result.data
})
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.send()
}
},
mounted(){
console.log(this.$el);
}
}
</script>
5.6 【组件的创建阶段】生命周期的示意图
5.7 【组件的运行阶段】生命周期函数
beforeupdate
<script>
export default {
props:['info'],
data(){
return {
message:'hello vue.js',
// 定义books数组,存储的是所有图书的列表数据,默认为空数组
books:[]
}
},
methods:{
show(){
console.log('调用了Test组件的show方法');
},
// 使用Ajax请求图书列表的数据
initBookList() {
const xhr = new XMLHttpRequest()
xhr.addEventListener('load', () => {
const result = JSON.parse(xhr.responseText)
// 获取对象
console.log(result)
this.books = result.data
})
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.send()
}
},
// 要触发beforeUpdata就要数据发生改变的时候
// 这个时候数据是新的,但是UI结构是旧的
beforeUpdate(){
console.log('beforeUpdate');
console.log(this.message);
const dom=document.querySelector('#pppp')
console.log(dom.innerHTML);//跟上面获取的message数据不一样
}
}
</script>
updated
<script>
export default {
props:['info'],
data(){
return {
message:'hello vue.js',
// 定义books数组,存储的是所有图书的列表数据,默认为空数组
books:[]
}
},
methods:{
show(){
console.log('调用了Test组件的show方法');
},
// 使用Ajax请求图书列表的数据
initBookList() {
const xhr = new XMLHttpRequest()
xhr.addEventListener('load', () => {
const result = JSON.parse(xhr.responseText)
// 获取对象
console.log(result)
this.books = result.data
})
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.send()
}
},
// 创建阶段的第一个生命周期---props,data,methods都处于不可用状态
beforeCreate(){
// console.log(this.info);
// console.log(this.message);
// this.show()
},
//创建阶段的第二个生命周期---props,data,methods都处于可用状态
// 在这个状态下可以调用Ajax请求拿数据
// created生命周期函数,非常常用
// 经常在它里面,调用methods中的方法,请求服务器的数据
// 并且,把请求到的数据,转存到data中,供template模板渲染的时候使用
created(){
// console.log(this.info);
// console.log(this.message);
// this.show(),
//可以访问到data数据
this.initBookList()
},
// 将要渲染DOM结构,但是没有拿到DOM元素
beforeMount(){
// console.log('beforeMount');
// const dom =document.querySelector('#myh3')
// console.log(dom);//拿不到dom元素
},
mounted(){
// console.log(this.$el);
},
// 当数据变化之后,为了能够操作到最新的 DOM 结构,必须把代码写到 updated 生命周期函数中
updated(){
console.log('beforeUpdate');
console.log(this.message);
const dom=document.querySelector('#dddd')
console.log(dom.innerHTML);//跟上面获取的message数据一样
}
}
</script>
5.8 【组件的销毁阶段】生命周期函数
<script>
export default {
props:['info'],
data(){
return {
message:'hello vue.js',
// 定义books数组,存储的是所有图书的列表数据,默认为空数组
books:[]
}
},
methods:{
show(){
console.log('调用了Test组件的show方法');
},
// 使用Ajax请求图书列表的数据
initBookList() {
const xhr = new XMLHttpRequest()
xhr.addEventListener('load', () => {
const result = JSON.parse(xhr.responseText)
// 获取对象
console.log(result)
this.books = result.data
})
xhr.open('GET', 'http://www.liulongbin.top:3006/api/getbooks')
xhr.send()
}
},
beforeDestroy() {
console.log('beforeDestroy')
this.message = 'aaa'
console.log(this.message)
},
destroyed() {
console.log('destroyed')
// this.message = 'aaa'
}
}
</script>
2.组件之间的数据共享
1. 组件之间的关系
2. 父子组件之间的数据共享
2.1 父组件向子组件共享数据:自定义属性 (:msg="messsage")
父组件向子组件共享数据需要使用自定义属性。示例代码如下:
父组件
<template>
<div class="app-container">
<h1>App 根组件</h1>
<hr />
<div class="box">
<!-- 渲染 Left 组件和 Right 组件 -->
<!-- 3. 以标签形式,使用注册好的组件 -->
<!-- 注意:如果没有使用v-bind绑定属性的话,则输出的是“msg 的值为message” -->
<!-- 表示将message这个字符串传入,而我们要接收的是message这个对应数据 -->
<Left :msg="message" :user="userinfo"></Left>
</div>
</div>
</template>
<script>
// 1.将其他组件导入
import Left from '@/components/Left.vue'
import { userInfo } from 'os';
// import Right from '@/components/Right.vue'
export default {
data(){
return {
message:'hello',
userinfo:{ name: 'wsc',age:'18'}
}
},
// 2. 注册组件
components:{
Left,
}
}
</script>
<style lang="less">
.app-container {
padding: 1px 20px 20px;
background-color: #efefef;
}
.box {
display: flex;
}
</style>
子组件
<template>
<div class="left-container">
<h3>Left 组件</h3>
<p>msg 的值为:{{ msg }}</p>
<p>user的值为:{{ user }}</p>
</div>
</template>
<script>
export default {
props:['msg','user']
}
</script>
<style lang="less">
.left-container {
padding: 0 20px 20px;
background-color: orange;
min-height: 250px;
flex: 1;
}
</style>
注意:不要修改props属性里面的值
对于简单数据类型来说:子组件是将父组件的传入的内容复制了一份,所以如果修改子组件中的内容,则父组件不会变
对于复杂数据类型来说:子组件指向父组件,当子组件发生改变的时候,父组件中的值也发生改变
2.2 子组件向父组件共享数据 :使用自定义事件
子组件向父组件共享数据使用自定义事件。
父组件
<template>
<div class="app-container">
<h1>App 根组件 --- {{ countFromSon }}</h1>
<hr />
<div class="box">
<!-- 渲染 Left 组件和 Right 组件 -->
<!-- 3. 以标签形式,使用注册好的组件 -->
<!-- 注意:如果没有使用v-bind绑定属性的话,则输出的是“msg 的值为message” -->
<!-- 表示将message这个字符串传入,而我们要接收的是message这个对应数据 -->
<Left :msg="message" :user="userinfo"></Left>
<Right @numchange="getNewCount"></Right>
</div>
</div>
</template>
<script>
// 1.将其他组件导入
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'
export default {
data(){
return {
message:'hello',
userinfo:{ name: 'wsc',age:'18'},
// 接收子组件传递过来的count
countFromSon:0
}
},
methods:{
// 获取子组件传递过来的数据
// val--是子组件传递过来的值
getNewCount(val){
console.log('numchange事件被触发了');
// 将子组件传递过来的数据重新赋值给父组件
this.countFromSon=val
}
},
// 2. 注册组件
components:{
Left,
Right
}
}
</script>
子组件
<template>
<div class="right-container">
<h3>Right 组件----{{ count }}</h3>
<button @click="add">+1</button>
</div>
</template>
<script>
export default {
data(){
return {
// 子组件自己的数据 将来希望把count的值传递给父组件
count:0
}
},
methods:{
add(){
// 让子组件的count值自增1
this.count+=1
// 把自增的结果,传给父组件
// 这个numchange是自己取的
// 自定义事件
this.$emit('numchange',this.count)
}
}
}
</script>
3. 兄弟组件之间/组件之间相离较远 的数据共享:EventBus
在 vue2.x 中,兄弟组件之间数据共享的方案是 EventBus。
EventBus 的使用步骤
1.在数据发送方data写入数据,然后再接收方data定义一个变量接收来自发送方发来的数据
2.再components文件中创建eventBus.js文件
import Vue from 'vue' // 将新创建的Vue对象向外暴露 export default new Vue()
3.再发送方创建一个DOM实例
<template> <div class="left-container"> <button @click="send">把诗歌发送出去</button> </div> </template>
4.将eventBus模块导入发送方和接收方
//1.导入eventBus.js模块 import bus from '@/components/eventBus.js' import bus from '@/components/eventBus.js'
5.在发送方中声明触发事件
<script> //1.导入eventBus.js模块 import bus from '@/components/eventBus.js' export default { props:['msg','user'], data(){ return { str:'鹅鹅鹅' } }, methods:{ send(){ //2.通过eventBus发送数据 bus.$emit('share',this.str) } } } </script>
6.在接收方中绑定自定义事件(跟data同级的)
<script> export default { data(){ return { // 子组件自己的数据 将来希望把count的值传递给父组件 count:0, //创建一个对象接收从Left组件接收过来的数据 msgfFromLeft:'' } }, created(val) { //2.为bus绑定自定义事件 bus.$on('share',(val)=>{ console.log('在Right组件中定义的share被触发了',val); // 接收从发送方发过来的数据 this.msgfFromLeft=val }) }, methods:{ add(){ // 让子组件的count值自增1 this.count+=1 // 把自增的结果,传给父组件 // 这个numchange是自己取的 // 自定义事件 this.$emit('numchange',this.count) }, } } </script>
3.ref引用
1. 什么是 ref 引用:在不依赖jQuery的作用下,使用DOM
ref 用来辅助开发者在不依赖于 jQuery 的情况下,获取 DOM 元素或组件的引用。
每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下, 组件的 $refs 指向一个空对象。
<script>
export default {
methods:{
showThis(){
//指向空对象
console.log(this);
}
}
}
</script>
2. 使用 $ref 引用 DOM 元素
如果想要使用 ref 引用页面上的 DOM 元素,则可以按照如下的方式进行操作:
<template>
<div class="app-container">
<h1 ref="myh12">App 根组件</h1>
<button @click="showThis">打印this</button>
<hr />
<div class="box">
<!-- 渲染 Left 组件和 Right 组件 -->
</div>
</div>
</template>
<script>
export default {
methods:{
showThis(){
//指向空对象
console.log(this);
// 拿到DOM元素
console.log(this.$refs.myh12);
// 给DOM添加样式
this.$refs.myh12.style.color='red'
}
}
}
</script>
<style lang="less">
.app-container {
padding: 1px 20px 20px;
background-color: #efefef;
}
.box {
display: flex;
}
</style>
3. 使用 ref 引用组件实例
如果想要使用 ref 引用页面上的组件实例,则可以按照如下的方式进行操作:
使用组件
<template>
<div class="app-container">
<h1 ref="myh12">App 根组件</h1>
<button @click="showThis">打印this</button>
<!-- 这里调用到Left组件中的this -->
<button @click="onReset" >重置Left组件的count值为0</button>
<hr />
<!-- 3.将组件渲染出来 -->
<div class="box">
<!-- 渲染 Left 组件和 Right 组件 -->
<!-- 给Left组件添加ref可以拿到Left组件中的引用 -->
<!-- 因为ref是拿DOM的方法,所以通过这个方法就可以拿到Left元素(这里将Left看作是DOM元素) -->
<Left ref="comLeft"></Left>
</div>
</div>
</template>
<script>
//1.导入组件
import Left from '@/components/Left.vue'
export default {
methods:{
showThis(){
//指向空对象
console.log(this);
// 拿到DOM元素
console.log(this.$refs.myh12);
// 给DOM添加样式
this.$refs.myh12.style.color='red'
},
onReset(){
// this.$refs.comLeft.resetCount()
// 等价于
this.$refs.comLeft.count=0
}
},
//2.注册组件
components:{
Left
}
}
</script>
<style lang="less">
.app-container {
padding: 1px 20px 20px;
background-color: #efefef;
}
.box {
display: flex;
}
</style>
被使用的组件
<template>
<div class="left-container">
<h3>Left 组件----{{ count }}</h3>
<button @click="count+=1">+1</button>
<button @click="resetCount">重置</button>
</div>
</template>
<script>
export default {
data(){
return {
count:0
}
},
methods:{
resetCount(){
this.count=0
}
}
}
</script>
<style lang="less">
.left-container {
padding: 0 20px 20px;
background-color: orange;
min-height: 250px;
flex: 1;
}
</style>
4. 控制文本框和按钮的按需切换
通过布尔值 inputVisible 来控制组件中的文本框与按钮的按需切换。示例代码如下:
<template>
<div class="app-container">
<h1 ref="myh12">App 根组件</h1>
<button @click="showThis">打印this</button>
<!-- 这里调用到Left组件中的this -->
<button @click="onReset" >重置Left组件的count值为0</button>
<hr />
<!-- 当v-if为true的时候,展示输入框 -->
<!-- 当文本框失去焦点的时候,展示按钮 使用了@blur -->
<input type="text" v-if="inputVisible" @blur="showButton">
<!-- 当点击按钮后,按钮消失,出现文本框 -->
<button v-else @click="showInput">展示输入框</button>
<hr>
<!-- 3.将组件渲染出来 -->
<div class="box">
<!-- 渲染 Left 组件和 Right 组件 -->
<!-- 给Left组件添加ref可以拿到Left组件中的引用 -->
<!-- 因为ref是拿DOM的方法,所以通过这个方法就可以拿到Left元素(这里将Left看作是DOM元素) -->
<Left ref="comLeft"></Left>
</div>
</div>
</template>
<script>
//1.导入组件
import Left from '@/components/Left.vue'
export default {
data(){
return {
// 控制输入框和按钮的按需切换
// 默认值为false,表示默认展示按钮,隐藏输入框
inputVisible:false
}
},
methods:{
showInput(){
this.inputVisible=true
},
showButton(){
this.inputVisible=false
},
showThis(){
//指向空对象
console.log(this);
// 拿到DOM元素
console.log(this.$refs.myh12);
// 给DOM添加样式
this.$refs.myh12.style.color='red'
},
onReset(){
// this.$refs.comLeft.resetCount()
// 等价于
this.$refs.comLeft.count=0
}
},
//2.注册组件
components:{
Left
}
}
</script>
<style lang="less">
.app-container {
padding: 1px 20px 20px;
background-color: #efefef;
}
.box {
display: flex;
}
</style>
5. 让文本框自动获得焦点
当文本框展示出来之后,如果希望它立即获得焦点,则可以为其添加 ref 引用,并调用原生 DOM 对象的 .focus() 方法即可。
6. this.$nextTick(cb) 方法
组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行。通俗的理解是:等组件的 DOM 更新完成之后,再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素
4.购物车案例
1. 案例效果
2. 实现步骤
1.导入Header组件
<template>
<div class="app-container">
<!-- 3.使用组件 -->
<!-- Header头部区域 -->
<Header></Header>
</div>
</template>
<script>
// 1.导入Header组件
// 导入需要的组件
import Header from '@/components/Header/Header.vue'
export default {
// 2.注册组件
components:{
Header
}
}
</script>
2.基于axios请求列表数据
1.安装axios,并在App.vue中导入axios
2.在methods方法中,定义initCartList函数请求列表数据
3.在created生命周期函数中,调用步骤2封装的initCartList函数
<template>
<div class="app-container">
<!-- 3.使用组件 -->
<!-- Header头部区域 -->
<Header></Header>
<h1>App 根组件</h1>
</div>
</template>
<script>
// 导入axios请求库
import axios from 'axios'
// 1.导入Header组件
// 导入需要的组件
import Header from '@/components/Header/Header.vue'
export default {
created(){
//调用请求数据的方法
// 当数据一旦创建好就调用下面的这个方法
this.initCartList();
},
methods:{
// 封装请求列表数据的方法--发起数据请求都是在生命周期created中进行请求的
async initCartList(){
// 调用axios的get方法,请求列表数据
// axios.get--得到的是一个对象
// 如果使用了await,则记得在函数名前面加上async
// data:res--是将获得的对象中,实际获得的数据是:对象.data
// data:res--表示对data这个属性进行重命名
const {data:res}=await axios.get('https://www.escook.cn/api/cart')
console.log(res);
}
},
// 2.注册组件
components:{
Header
}
}
</script>
<style lang="less" scoped>
.app-container {
padding-top: 45px;
padding-bottom: 50px;
}
</style>
3.请求回来的数据存放在data中
1.先在data中声明一个空的数组,将获取得到的数据存入
2.在initCartList函数中判断是否获取数据成功
3.获取成功后,将真正的数据存入
<script>
// 导入axios请求库
import axios from 'axios'
// 1.导入Header组件
// 导入需要的组件
import Header from '@/components/Header/Header.vue'
export default {
data(){
return {
list:[]
}
},
created(){
//调用请求数据的方法
// 当数据一旦创建好就调用下面的这个方法
this.initCartList();
},
methods:{
// 封装请求列表数据的方法--发起数据请求都是在生命周期created中进行请求的
async initCartList(){
// 调用axios的get方法,请求列表数据
// axios.get--得到的是一个对象
// 如果使用了await,则记得在函数名前面加上async
// data:res--是将获得的对象中,实际获得的数据是:对象.data
// data:res--表示对data这个属性进行重命名
const {data:res}=await axios.get('https://www.escook.cn/api/cart')
// 只要请求回来的数据,在页面中渲染期间需要用到,必须转存到data中
console.log(res);
if(res.status === 200){
// 如果数据请求成功,则将res中的list(存放数据的)赋值给data中list数组
this.list=res.list
}
}
},
// 2.注册组件
components:{
Header
}
}
</script>
4.循环渲染Goods组件
1.先将Goods组件引入App.vue文件中
2.组件的循环使用v-for,记得后面加上:key
<!-- 循环渲染每一个商品的信息 -->
<!-- 组件的循环使用v-for -->
<!-- 使用v-for记得加上key,key指的是id(唯一性) -->
<Goods v-for="item in list" :key="item.id"></Goods>
//2.导入Goods组件
import Goods from '@/components/Goods/Goods.vue'
// 2.注册组件
components:{
Header,
Goods,
}
5.为Goods组件封装title、pic属性、prices属性和state属性:【父组件向子组件传值】
主要思路:父向子传值(就是在App.vue文件中通过axios获得的数据传递给Goods组件进行渲染)
1.在Goods组件中声明props属性
2.在标签中(App.vue和Goods.vue中)进行动态绑定
注意点:
插值表达式"{{}}"不能用在属性中 只能使用动态绑定来实现 ":"
<h6 class="goods-title">{{title}}</h6> <!-- 插值表达式"{{}}"不能用在属性中 只能使用动态绑定来实现 ":" --> <!-- <img src="../../assets/logo.png" alt="" /> --> <img :src="pic" alt="" />
App.vue
<template>
<div class="app-container">
<!-- 3.使用组件 -->
<!-- Header头部区域 -->
<Header></Header>
<!-- 循环渲染每一个商品的信息 -->
<!-- 组件的循环使用v-for -->
<!-- 使用v-for记得加上key,key指的是id(唯一性) -->
<!-- 记得在title前面加上 ":" 要不然传入的是字符串 -->
<Goods v-for="item in list" :key="item.id" :title="item.goods_name" :pic="item.goods_img" :price="item.goods_price" :state="item.goods_state"></Goods>
</div>
</template>
Goods.vue
<template>
<div class="goods-container">
<!-- 左侧图片 -->
<div class="thumb">
<div class="custom-control custom-checkbox">
<!-- 复选框 -->
<input type="checkbox" class="custom-control-input" id="cb1" :checked="state" />
<label class="custom-control-label" for="cb1">
<!-- 商品的缩略图 -->
<!-- 插值表达式"{{}}"不能用在属性中 只能使用动态绑定来实现 ":" -->
<!-- <img src="../../assets/logo.png" alt="" /> -->
<img :src="pic" alt="" />
</label>
</div>
</div>
<!-- 右侧信息区域 -->
<div class="goods-info">
<!-- 商品标题 -->
<!-- 这里需要用到动态绑定 -->
<!-- <h6 class="goods-title">图片图片</h6> -->
<h6 class="goods-title">{{title}}</h6>
<div class="goods-info-bottom">
<!-- 商品价格 -->
<span class="goods-price">{{price}}</span>
<!-- 商品的数量 -->
</div>
</div>
</div>
</template>
<script>
export default {
props:{
// 要渲染的商品的标题
title:{
default:'',
type:String
},
pic:{
default:'',
// 图片是字符串类型
type:String
},
price:{
// 商品的单价
default:0,
type:Number
},
state:{
// 商品的勾选状态
// 默认为选中状态
default:true,
type:Boolean
}
}
}
</script>
6.关于自定义组件的属性传递问题【分析封装props两种方案的优缺点】
7.如何修改商品勾选状态【子组件向父组件传值】
8.自定义state-change事件
父组件:App.vue
<template>
<div class="app-container">
<!-- 3.使用组件 -->
<!-- Header头部区域 -->
<Header></Header>
<!-- 循环渲染每一个商品的信息 -->
<!-- 组件的循环使用v-for -->
<!-- 使用v-for记得加上key,key指的是id(唯一性) -->
<!-- 记得在title前面加上 ":" 要不然传入的是字符串 -->
<Goods v-for="item in list" :key="item.id" :title="item.goods_name" :pic="item.goods_img" :price="item.goods_price" :state="item.goods_state" :id="item.id" @state-change="getNewState"></Goods>
</div>
</template>
methods:{
},
// 接收子组件
getNewState(val){
console.log('父组件接收到数据了');
console.log(val);
}
子组件:Count.vue
<!-- 复选框 -->
<!-- 这里的checked不能使用v-model,因为props是只读属性,而v-model是双向数据绑定的 -->
<input type="checkbox" class="custom-control-input" id="cb1" :checked="state" @change="stateChange"/>
methods:{
// 只要复选框的选中状态发生了改变,就会调用这个出来函数
stateChange(e){
// console.log('ok');
// console.log(e);
// 获得最新的状态
const newState=e.target.checked
// console.log(newState);
// 触发自定义事件
this.$emit('state-change',{id:this.id,value:newState})
}
}
9.修改对应商品的勾选状态
父组件
// 接收子组件传递过来的数据
// e的格式是{id,value}
getNewState(e){
console.log('父组件接收到数据了');
// console.log(e);
// 遍历传入的数组,判断是否有符合条件的
this.list.some(item=>{
if(item.id===e.id){
//将商品的状态进行修改
item.goods_state=e.value
// 终止后续的状态
return true
}
})
}
10.底部的“全选”按钮是否选中:【计算属性:computed/父组件向子组件传递】
因为全选按钮要被其他按钮影响到,所以要使用计算属性
1.先判断是否将复选框勾选上---->计算属性:computed
2.【当小按钮全部选中后,大按钮也被选中】将在App.vue文件中获得步骤1的结果渲染到Footer组件中---->父组件向子组件传递(自定义事件)
3.【当大按钮被选中后,小按钮全部被选中】将在Footer组件中触发到的事件渲染到App.vue组件中-->子组件向父组件传递(自定义属性)
// 计算属性,用于底部"总计"复选框是否选中
computed:{
// 动态计算出全选的状态是true还是false
fullState(){
// 数组.every返回的是布尔值
return this.list.every(item=>item.goods_state===true)
}
},
<!-- Footer区域 -->
<Footer :isFull="fullState" ></Footer>
// 计算属性,用于底部"总计"复选框是否选中
computed:{
// 动态计算出全选的状态是true还是false
fullState(){
// 数组.every返回的是布尔值
return this.list.every(item=>item.goods_state===true)
}
},
<input type="checkbox" class="custom-control-input" id="cbFull" :checked="isFull" />
<!-- Footer区域 -->
<Footer :isFull="fullState" @full-change="getFullState"></Footer>
// 接收Footer子组件传递过来的全选按钮的状态
getFullState(e){
console.log('在App中拿到了全选的按钮');
console.log(e);
// 因为是要遍历小复选框中的每一项所以使用forEach
this.list.forEach(item=>item.goods_state=e)
},
<input type="checkbox" class="custom-control-input" id="cbFull" :checked="isFull" @change="fullChange" />
methods:{
// 监听到了全选状态的变化
fullChange(e){
// console.log(e.target.checked);
this.$emit('full-change',e.target.checked)
},
}
11.计算商品的总价格
1.先在父组件App.vue组件中先写出计算属性
2.然后将计算出来的结果渲染到Footer组件(子组件)
父组件
<template>
<div class="app-container">
<!-- 3.使用组件 -->
<!-- Header头部区域 -->
<Header></Header>
<p>{{ amt }}</p>
<!-- 循环渲染每一个商品的信息 -->
<!-- 组件的循环使用v-for -->
<!-- 使用v-for记得加上key,key指的是id(唯一性) -->
<!-- 记得在title前面加上 ":" 要不然传入的是字符串 -->
<Goods v-for="item in list" :key="item.id" :title="item.goods_name" :pic="item.goods_img" :price="item.goods_price" :state="item.goods_state" :id="item.id" @state-change="getNewState"></Goods>
<!-- Footer区域 -->
<Footer :isFull="fullState" :amount="amt" @full-change="getFullState"></Footer>
</div>
</template>
<script>
// 导入axios请求库
import axios from 'axios'
// 导入需要的组件
// 1.导入Header组件
import Header from '@/components/Header/Header.vue'
//2.导入Goods组件
import Goods from '@/components/Goods/Goods.vue'
//3.导入Footer组件
import Footer from '@/components/Footer/Footer.vue'
export default {
data(){
return {
list:[]
}
},
// 计算属性,用于底部"总计"复选框是否选中
computed:{
// 动态计算出全选的状态是true还是false
fullState(){
// 数组.every返回的是布尔值
return this.list.every(item=>item.goods_state===true)
},
// 已勾选商品的总价格
amt(){
// 1.先filter过滤
// 2.在reduce累加
return this.list
.filter(item=>item.goods_state)
.reduce((total,item)=>{
return total+=item.goods_price*item.goods_count
},0)
}
},
created(){
//调用请求数据的方法
// 当数据一旦创建好就调用下面的这个方法
this.initCartList();
},
methods:{
// 封装请求列表数据的方法--发起数据请求都是在生命周期created中进行请求的
async initCartList(){
// 调用axios的get方法,请求列表数据
// axios.get--得到的是一个对象
// 如果使用了await,则记得在函数名前面加上async
// data:res--是将获得的对象中,实际获得的数据是:对象.data
// data:res--表示对data这个属性进行重命名
const {data:res}=await axios.get('https://www.escook.cn/api/cart')
// 只要请求回来的数据,在页面中渲染期间需要用到,必须转存到data中
console.log(res);
if(res.status === 200){
// 如果数据请求成功,则将res中的list(存放数据的)赋值给data中list数组
this.list=res.list
}
},
// 接收子组件传递过来的数据
// e的格式是{id,value}
getNewState(e){
console.log('父组件接收到数据了');
// console.log(e);
// 遍历传入的数组,判断是否有符合条件的
this.list.some(item=>{
if(item.id===e.id){
//将商品的状态进行修改
item.goods_state=e.value
// 终止后续的状态
return true
}
})
},
// 接收Footer子组件传递过来的全选按钮的状态
getFullState(e){
console.log('在App中拿到了全选的按钮');
console.log(e);
// 因为是要遍历小复选框中的每一项所以使用forEach
this.list.forEach(item=>item.goods_state=e)
},
},
// 2.注册组件
components:{
Header,
Goods,
Footer,
}
}
</script>
<style lang="less" scoped>
.app-container {
padding-top: 45px;
padding-bottom: 50px;
}
</style>
子组件
<template>
<div class="footer-container">
<!-- 左侧的全选 -->
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="cbFull" :checked="isFull" @change="fullChange" />
<label class="custom-control-label" for="cbFull">全选</label>
</div>
<!-- 中间的合计 -->
<div>
<span>合计:</span>
<!-- toFixed---表示显示2位小数 -->
<span class="total-price">¥{{ amount.toFixed(2) }}</span>
</div>
<!-- 结算按钮 -->
<button type="button" class="btn btn-primary btn-settle">结算({{ 0 }})</button>
</div>
</template>
<script>
export default {
// 定义一个属性,用于接收父组件传递过来的复选框的勾选状态
props:{
// 全选状态
isFull:{
type:Boolean,
default:true
},
// 总价格
amount:{
type:Number,
default:0
},
},
methods:{
// 监听到了全选状态的变化
fullChange(e){
// console.log(e.target.checked);
this.$emit('full-change',e.target.checked)
},
}
}
</script>
<style lang="less" scoped>
.footer-container {
font-size: 12px;
height: 50px;
width: 100%;
border-top: 1px solid #efefef;
position: fixed;
bottom: 0;
background-color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
}
.custom-checkbox {
display: flex;
align-items: center;
}
#cbFull {
margin-right: 5px;
}
.btn-settle {
height: 80%;
min-width: 110px;
border-radius: 25px;
font-size: 12px;
}
.total-price {
font-weight: bold;
font-size: 14px;
color: red;
}
</style>
12.把购买数量传给counter组件
1.Counter组件是Goods的子组件
2.要从Goods组件中获得的数量传递给Counter组件,但是在Goods组件中得不到(因为没有数量属性),所以要先从App组件中获得,才能给Counter组件
2.1 将App组件中的数据传递给Goods组件
2.2 将Goods组件中的数据传递给Counter组件中
13.将在增删商品数量的结果传递给App组件
1.向App组件传递数据记得要传入id,则先在Counter中获得id属性[记得在Goods中将属性传递给Counter]
2.给Counter组件添加点击事件
3.创建EventBus.js文件,然后再发送方和接收方上加上EventBus
13.动态计算已勾选上商品数量
总结
四、动态组件 & 插槽 & 自定义指令
1.动态组件
1. 什么是动态组件:动态切换组件的显示与隐藏
2. 如何实现动态组件渲染:<components :is="要渲染的组件名">组件
vue 提供了一个内置的<components >组件,专门用来实现动态组件的渲染。【相当于一个占位符】
1.components标签是vue内置的,作用:组件的占位符
2.is属性的值,表示要渲染的组件的名字
3.is属性的,应该是组件再components节点下注册的名称
3.动态切换组件的展示与隐藏
<template>
<div class="app-container">
<h1>App 根组件</h1>
<hr />
<button @click="comName='Left'">展示Left</button>
<button @click="comName='Right'">展示Right</button>
<div class="box">
<!-- 渲染 Left 组件和 Right 组件 -->
<!-- 1.components标签是vue内置的,作用:组件的占位符 -->
<!-- 2.is属性的值,表示要渲染的组件的名字 -->
<!-- 3.is属性的,应该是组件再components节点下注册的名称 -->
<!-- 写死了 -->
<!-- <components is="Left"></components> -->
<!-- 动态绑定 -->
<components :is="comName"></components>
</div>
</div>
</template>
<script>
import Left from '@/components/Left.vue'
import Right from '@/components/Right.vue'
export default {
data(){
return {
// comName表示要进行展示的组件名字
comName:Left
}
},
components:{
Left,
Right,
}
}
</script>
<style lang="less">
.app-container {
padding: 1px 20px 20px;
background-color: #efefef;
}
.box {
display: flex;
}
</style>
4. 使用 keep-alive 保持状态
keep-alive可以把内部的组件进行缓存,而不是销毁组件.
默认情况下,切换动态组件时无法保持组件的状态。此时可以使用 vue 内置的<keep-alive> 组件保持动态组 件的状态。
<keep-alive>
<components :is="comName"></components>
</keep-alive>
5. keep-alive 对应的生命周期函数:被缓存的时候才可以使用
当组件被缓存时,会自动触发组件的 deactivated 生命周期函数。
当组件被激活时,会自动触发组件的 activated 生命周期函数。
1.当组件第一次被创建的时候,既会执行created生命周期,也会执行activated生命周期
2. 但是,当组件被激活的时候,只会触发activated生命周期,不会触发created,因为组件没有重新被创建
6. keep-alive 的 include/exclude 属性:指定哪一些组件可以/不被被缓存
include 属性用来指定:只有名称匹配的组件会被缓存。多个组件名之间使用英文的逗号分隔:
include:指定要进行缓存的组件 这里指定就缓存Left组件
exclude:指定不进行缓存的
include和exclude不能同时使用
7.了解组件注册名称和组件声明时name区别
当提供了name属性之后,组件的名称,就是name属性的值
对比
1.组件的"注册名称" components:{ Left,Right,} 的主要应用常见是:以标签的形式,把注册好的组件,渲染和使用到页面结构之中
2.组件声明时候的"name"名称的主要应用场景"结合<keep-alive>标签实现组件缓存功能;以及再调试工具中看到组件的name名称
2.插槽
1. 什么是插槽<slot>:占位符
插槽(Slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的 部分定义为插槽。
可以把插槽认为是组件封装期间,为用户预留的内容的占位符。
2. 体验插槽的基础用法
在封装组件时,可以通过<slot> 元素定义插槽,从而为用户预留内容占位符。
2.1 没有预留插槽的内容会被丢弃
如果在封装组件时没有预留任何<slot> 插槽,则用户提供的任何自定义内容都会被丢弃。
2.2 后备内容:如果用户输入内容则会将默认内容进行覆盖
封装组件时,可以为预留的<slot> 插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何 内容,则后备内容会生效。
3. 具名插槽:v-slot
如果在封装组件时需要预留多个插槽节点,则需要为每个<slot> 插槽指定具体的 name 名称。这种带有具体 名称的插槽叫做“具名插槽”。
vue官方规定:每一个slot插槽,都要有一个name名称
如果省略了slot的name属性,则有一个默认名称叫做default
默认情况下,在使用组件的时候,提供的内容会被填充到名字为default的插槽中
3.1 为具名插槽提供内容
在向具名插槽提供内容的时候,我们可以在一个<template> 元素上使用 v-slot 指令,并以 v-slot 的参数的 形式提供其名称。
1.如果要把内容填充到指定名称的插槽中,需要使用v-slot这个指令
2.v-slot:后面要跟上插槽的名字
3.v-slot:指令不能直接用在元素身上,必须用在template标签上,或者组件上
4.template这个标签,它是一个虚拟标签,只起到包裹性质作用,但是,不会被渲染为任何实际性质的html元素
5.v-slot简写形式为:#
<template v-slot:default>
<!-- 如果再Left组件中没有定义插槽,则下面的p标签会被忽略 -->
<!-- 默认情况下,在使用组件的时候,提供的内容会被填充到名字为default的插槽中 -->
<p>这是在Left组件的内容中声明的p标签</p>
</template>
3.2 具名插槽的简写形式:"#"
跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header:
<Article>
<template #title>
<h3>一首歌</h3>
</template>
<template #content>
<div>
<p>啊,大海,全是水。</p>
<p>啊,蜈蚣,全是腿。</p>
<p>啊,辣椒,净辣嘴。</p>
</div>
</template>
<template #author>
<div>作者</div>
</template>
</Article>
<template>
<div class="article-container">
<!-- 文章标题 -->
<div class="header-box">
<slot name="title"></slot>
</div>
<!-- 文章的内容 -->
<div class="content-box">
<!-- 在封装组件时,为预留的 <slot> 提供属性对应的值,这种用法,叫做 “作用域插槽” -->
<slot name="content"></slot>
</div>
<!-- 文章的作者 -->
<div class="footer-box">
<slot name="author"></slot>
</div>
</div>
</template>
4. 作用域插槽
在封装组件的过程中,可以为预留的 <slot> 插槽绑定 props 数据,这种带有 props 数据的 叫做“作用 域插槽”。
4.1 使用作用域插槽
可以使用 v-slot: 的形式,接收作用域插槽对外提供的数据。
在封装组件时,为预留的 <slot> 提供属性对应的值,这种用法,叫做 “作用域插槽”
<!-- 这个obj得到的是一个对象 -->
<!-- 用scope接收作用域插槽中的数据 -->
<template #content="scope">
<div>
<p>啊,大海,全是水。</p>
<p>啊,蜈蚣,全是腿。</p>
<p>啊,辣椒,净辣嘴。</p>
<!-- 拿到的是:{ "msg": "hello vue.js" } -->
<p>{{ scope }}</p>
</div>
</template>
<!-- 文章的内容 -->
<div class="content-box">
<!-- 在封装组件时,为预留的 <slot> 提供属性对应的值,这种用法,叫做 “作用域插槽” -->
<slot name="content" msg="hello vue.js"></slot>
</div>
4.2 解构插槽 Prop
作用域插槽对外提供的数据对象,可以使用解构赋值简化数据的接收过程。
<!-- <template #content="scope"> -->
<template #content="{msg,user}">
<div>
<p>啊,大海,全是水。</p>
<p>啊,蜈蚣,全是腿。</p>
<p>啊,辣椒,净辣嘴。</p>
<!-- 拿到的是:{ "msg": "hello vue.js" } -->
<p>{{ user.name }}</p>
</div>
</template>
<div class="content-box">
<!-- 在封装组件时,为预留的 <slot> 提供属性对应的值,这种用法,叫做 “作用域插槽” -->
<!-- msg="hello vue.js"--表示写入一个固定的值 -->
<!-- :user="userinfo"---表示动态绑定一个属性 -->
<slot name="content" msg="hello vue.js" :user="userinfo"></slot>
</div>
5.购物车案例改造
5.1 基于slot插槽改造购物车案例--在App组件中直接获得Counter组件
5.2 基于slot插槽改造购物车案例--在App组件中直接获得Counter组件中的数值
3.自定义指令
1. 什么是自定义指令
vue 官方提供了 v-text、v-for、v-model、v-if 等常用的指令。除此之外 vue 还允许开发者自定义指令。
2. 自定义指令的分类
3. 私有自定义指令:directives 节点
在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令。
4. 使用自定义指令
在使用自定义指令时,需要加上 v- 前缀。
<template>
<div class="app-container">
<h1 v-color>App 根组件</h1>
</div>
</template>
<script>
export default {
// 私有自定义指令的节点
directives:{
// v-color--(v-)是固定写法,实际名字为:color
// 定义名为color的自定义指令,指向一个配置对象
color:{
// 当指令第一次被绑定到元素的时候,会立即触发bind函数
// 形参中el表示当前指令所绑定到的那个DOM对象
bind(el){
// console.log('触发了v-color的bind 函数')
el.style.color='red'
}
}
}
}
</script>
5. 为自定义指令动态绑定参数值
在 template 结构中使用自定义指令时,可以通过等号(=)的方式,为当前指令动态绑定参数值
<h1 v-color="color">App 根组件</h1>
data(){
return {
color:'blue'
}
},
6. 通过 binding 获取指令的参数:bind(el,binding){}
在声明自定义指令时,可以通过形参中的第二个参数,来接收指令的参数值:
<h1 v-color="color">App 根组件</h1>
data(){
return {
color:'blue'
}
},
// 私有自定义指令的节点
directives:{
// v-color--(v-)是固定写法,实际名字为:color
// 定义名为color的自定义指令,指向一个配置对象
color:{
// 当指令第一次被绑定到元素的时候,会立即触发bind函数
// 形参中el表示当前指令所绑定到的那个DOM对象
bind(el,binding){
// console.log('触发了v-color的bind 函数')
// el.style.color='red'
// 当自定义指令中动态绑定了值
// binding---是一个对象
// console.log(binding);
el.style.color=binding.value
}
}
}
7.单独在属性值后面添加属性值
8. update 函数
bind 函数只调用 1 次:当指令第一次绑定到元素时调用,当 DOM 更新时 bind 函数不会被触发。 update 函 数会在每次 DOM 更新时被调用。
9. 函数简写
如果binding 和update 函数中的逻辑完全相同,则对象格式的自定义指令可以简写成函数格式:
10. 全局自定义指令:Vue.directive()【写在main.js中】
全局共享的自定义指令需要通过“Vue.directive()”进行声明
1.参数1:字符串,表示全局变量自定义指令的名字
2.参数2:对象,用l来接收指令的参数值
// 全局自定义指令
/*Vue.directive('color',{
bind(el,binding){
el.style.color=binding.value
},
update(el,binding){
el.style.color=binding.value
}
})*/
Vue.directive('color',function(el,binging){
el.style.color=binding.value
})
总结
五、ESLint:代码约束
1.了解eslintrc.js配置文件中rules规则
2.初步了解ESLint的语法规则
当报错的时候,去官网查找:ESLint - Pluggable JavaScript linter - ESLint中文
3.配置VsCode
3.1 ESlint
// ESLint组件的配置
"editor.codeActionsOnSave": {
"source.fixAll": true
},
3.2 prettier
"eslint.alwaysShowStatus":true,
"prettier.trailingComma": "none",
"prettier.semi":false,
// 每行文字个数超出此限制见过会被迫换行
"prettier.printWidth":300,
// 使用单引号替换双引号
"prettier.singleQuote": true,
"prettier.arrowParens": "avoid",
// 设置.vue文件中,HTML代码的格式化插件
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.ignoreProjectWarning": true,
"vetur.format.defaultFormatterOptions": {
"prettier": {
"trailingComma":"none",
"semi":false,
"singleQuote":true,
"arrowParens":"avoid",
"printWidth":300
},
"js-beautify-html": {
"wrap_attributes": "false"
}
},
3.3 在电脑上配置一个文件
文件内容为:
{"semi": false, "singleQuote": true, "printWidth": 300}
在setting.json文件中加上
"prettier.configPath": "C:\\Users\\小林\\.prettierrc",
4.配置默认格式化文档的方式
在vue文件下
在js文件下
出现保存自动加逗号,则在setting.json文件中将
5.演示axios的基本使用并发现问题
5.1 在装axios包的时候出现错误
则应该修改为:
npm i axios -S --legacy-peer-deps
5. 2 在components文件夹中起名字为Left-vue和Right-vue的两个组件
【注意点:一定要在文件名后面加上-vue要不然报错】
5.3 如果不想将名字修改为-vue,则修改.eslintrc.js文件
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'@vue/standard'
],
parserOptions: {
parser: '@babel/eslint-parser'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// 在方法的形参 () 之前,是否必须有空格
'space-before-function-paren': ['warn', 'never'],
'vue/multi-word-component-names': 'off'
}
}
6.Right.vue和Left.vue
<template>
<div class="right-container">
<h3>Right组件</h3>
<button @click="postInfo">发起 POST 请求</button>
</div>
</template>
<script>
import axios from 'axios'
export default {
methods: {
async postInfo() {
const { data: res } = await axios.post('http://www.liulongbin.top:3006/api/post', { name: 'zs', age: 20 })
console.log(res)
}
}
}
</script>
<style lang="less" scoped>
.right-container {
background-color: pink;
min-height: 200px;
flex:1;
}
</style>
<template>
<div class="left-container">
<h3>Left组件</h3>
<button @click="getInfo">发起 GET 请求</button>
</div>
</template>
<script>
import axios from 'axios'
export default {
methods: {
async getInfo() {
const { data: res } = await axios.get('http://www.liulongbin.top:3006/api/get')
console.log(res)
}
}
}
</script>
<style lang="less" scoped>
.left-container {
background-color: orange;
min-height: 200px;
flex:1;
}
</style>
7.把axios挂载到Vue的原型上并配置请求根路径
把axios挂载到Vue的原型上
把 axios 挂载到 Vue 原型上,有一个缺点:不利于 API 接口的复用!!!
配置请求根路径
main.js文件
import Vue from 'vue'
import App from './App.vue'
import axios from 'axios'
Vue.config.productionTip = false
// 全局配置 axios 的请求根路径
axios.defaults.baseURL = 'http://www.liulongbin.top:3006'
// 将axios属性挂载在Vue的原型上
// Vue.prototype.axios = axios
Vue.prototype.$http = axios
// 今后,在每个 .vue 组件中要发起请求,直接调用 this.$http.xxx
// 但是,把 axios 挂载到 Vue 原型上,有一个缺点:不利于 API 接口的复用!!!
new Vue({
render: (h) => h(App)
}).$mount('#app')
六、路由
1.前端路由的概念与原理
1. 什么是路由:路由(英文:router)就是对应关系
3. SPA (单页面开发)与前端路由
4. 什么是前端路由:#Hash 地址(锚链接)与组件之间的对应关系
5. 前端路由的工作方式
① 用户点击了页面上的路由链接
② 导致了 URL 地址栏中的 Hash 值发生了变化
③ 前端路由监听了到 Hash 地址的变化
④ 前端路由把当前 Hash 地址对应的组件渲染都浏览器中
前端路由,指的是 Hash 地址与组件之间的对应关系
6. 手动实现简易的前端路由
步骤1:通过 <component> 标签,结合 comName 动态渲染组件。示例代码如下:
步骤2:在 App.vue 组件中,为 <a>链接添加对应的hash值
步骤3:在 created 生命周期函数中,监听浏览器地址栏中 hash 地址的变化,动态切换要展示的组件的名称:
created(){
// 只要当前App组件一被创建,就立即监听window对象的onhashchange事件
// onhashchange---是拿到当前的哈希地址
window.onhashchange=()=>{
// location.hash---拿到的是根地址后面的具体地址===#/hmoe #/movie
console.log('监听到了hash地址的变化',location.hash);
switch(location.hash){
case '#/home':
this.comName='home'
break
case '#/movie':
this.comName = 'movie'
break
case '#/about':
this.comName = 'about'
break
}
}
2.vue-router的基本使用
1. 什么是 vue-router
2. vue-router 安装和配置的步骤
① 安装 vue-router 包
② 创建路由模块
③ 导入并挂载路由模块
④ 声明路由链接和占位符
2.1 在项目中安装 vue-router
2.2 创建路由模块:导入,调用,创建,共享(router/index.js )
在 src 源代码目录下,新建 router/index.js 路由模块
// src/router/index.js 就是当前项目的路由模块
import Vue from 'vue'
import VueRouter from 'vue-router'
// 把 VueRouter 安装为 Vue 项目的插件
// Vue.use() 函数的作用,就是来安装插件的
Vue.use(VueRouter)
// 创建路由的实例对象
const router=new VueRouter()
// 将创建好的实例对象向外暴露
export default router
2.3 导入并挂载路由模块(在 src/main.js)
在 src/main.js 入口文件中,导入并挂载路由模块。
import Vue from 'vue'
import App from './App.vue'
// 导入路由模块,目的:拿到路由的实例对象
import router from '@/router/index.js'
// 导入 bootstrap 样式
import 'bootstrap/dist/css/bootstrap.min.css'
// 全局样式
import '@/assets/global.css'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
// 在VUe项目中,想要把路由用起来,必须把路由实例对象,通过下面的方式进行挂载
// router:路由的实例对象
router:router
}).$mount('#app')
2.4 路由的基本用法
在进行模块化导入的时候,如果给定的是文件夹,则默认导入这个文件夹下,名字叫做index.js的文件
2.5 声明路由占位符
在 src/App.vue 组件中,使用 vue-router 提供的 <router-view> 声明路由占位符
<template>
<div class="app-container">
<h1>App2 组件</h1>
<a href="#/home">首页</a>
<a href="#/movie">电影</a>
<a href="#/about">关于</a>
<hr />
<!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
<!-- 它的作用很单纯:占位符 -->
<router-view></router-view>
</div>
</template>
2.6 使用router-link替代a链接
<!-- 当安装和配置了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>
3. 声明路由的匹配规则
在 src/router/index.js 路由模块中,通过 routes 数组声明路由的匹配规则。
// src/router/index.js 就是当前项目的路由模块
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'
// 把 VueRouter 安装为 Vue 项目的插件
// Vue.use() 函数的作用,就是来安装插件的
Vue.use(VueRouter)
// 创建路由的实例对象
const router=new VueRouter({
// routes是一个数组,作用:定义”hash地址“与”组件“之间的对应关系
// routes:[{path:'/具体的组件地址(不能再前面加”#“)',component:要展示的组件}]
routes:[
{path:'/home',component:Home},
{path:'/movie',component:Movie},
{path:'/about',component:About}
]
})
// 将创建好的实例对象向外暴露
export default router
3.vue-router的常见用法
1. 路由重定向:链接的强制跳转(redirect 属性)【在index.js文件中写的对应关系】
路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面。 通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:
// 创建路由的实例对象
const router=new VueRouter({
// routes是一个数组,作用:定义”hash地址“与”组件“之间的对应关系
// routes:[{path:'/具体的组件地址(不能再前面加”#“)',component:要展示的组件}]
routes:[
// 重定向的路由规则
{path:'/',redirect:'/home'},
// 路由规则
{path:'/home',component:Home},
{path:'/movie',component:Movie},
{path:'/about',component:About}
]
})
2. 嵌套路由:实现组件的嵌套展示
3.1 声明子路由链接和子路由占位符 (在About组件中写的)
子级路由链接
子级路由占位符
<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>
3.2 通过 children数组 属性声明子路由规则
在 src/router/index.js 路由模块中,导入需要的组件,并使用 children 属性声明子路由规则:
使用children属性中包裹的子路由path路径中不能加”/“
父路由的path必须加”/“
import Tab1 from '@/components/tabs/Tab1.vue'
import Tab2 from '@/components/tabs/Tab2.vue'
// 创建路由的实例对象
const router=new VueRouter({
// routes是一个数组,作用:定义”hash地址“与”组件“之间的对应关系
// routes:[{path:'/具体的组件地址(不能再前面加”#“)',component:要展示的组件}]
routes:[
// 重定向的路由规则
{path:'/',redirect:'/home'},
// 路由规则
{path:'/home',component:Home},
{path:'/movie',component:Movie},
{path:'/about',
component:About,
children:[
// 子路由规则
{path:'tab1',component:Tab1},
{path:'tab2',component:Tab2}
]}
]
})
3.3 默认子路由
1.使用redirect属性
// 创建路由的实例对象
const router=new VueRouter({
// routes是一个数组,作用:定义”hash地址“与”组件“之间的对应关系
// routes:[{path:'/具体的组件地址(不能再前面加”#“)',component:要展示的组件}]
routes:[
// 重定向的路由规则
{path:'/',redirect:'/home'},
// 路由规则
{path:'/home',component:Home},
{path:'/movie',component:Movie},
{path:'/about',
component:About,
redirect:'/about/tab1',
children:[
// 子路由规则
{path:'tab1',component:Tab1},
{path:'tab2',component:Tab2}
]}
]
})
2. 默认子路由
默认子路由:如果children数组中,某一个路由规则path值为空字符串,则这条路由规则,叫做“默认子路由”
<!-- 子级路由链接 -->
<router-link to="/about/">tab1</router-link>
// 创建路由的实例对象
const router=new VueRouter({
// routes是一个数组,作用:定义”hash地址“与”组件“之间的对应关系
// routes:[{path:'/具体的组件地址(不能再前面加”#“)',component:要展示的组件}]
routes:[
// 重定向的路由规则
{path:'/',redirect:'/home'},
// 路由规则
{path:'/home',component:Home},
{path:'/movie',component:Movie},
{path:'/about',
component:About,
redirect:'/about/tab1',
children:[
// 子路由规则
// 默认子路由:如果children数组中,某一个路由规则path值为空字符串,则这条路由规则,叫做“默认子路由”
// {path:'tab1',component:Tab1},
{path:'',component:Tab1},
{path:'tab2',component:Tab2}
]}
]
})
4. 动态路由匹配
4.1 动态路由的概念
动态路由指的是:把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。 在 vue-router 中使用英文的冒号(:)来定义路由的参数项。示例代码如下
4.2 $route.params 参数对象
在动态路由渲染出来的组件中,可以使用 this.$route.params.id名 对象访问到动态匹配的参数值。
注意:在hash地址中,/后面的参数项,叫做”路径参数“
在路由”参数对象“中,需要使用this.$route.parms来访问路径参数
4.3 使用 props 接收路由参数
为了简化路由参数的获取形式,vue-router 允许在路由规则中开启 props 传参(index.js)。
// props:true---表示为Movie这个组件开启自定义属性
// 可以为路由规则开启props传参,从而方便的拿到动态参数的值
{path:'/movie/:id',component:Movie,props:true},
// 接收props数组
props:['id']
<h3>Movie 组件---{{ id }}</h3>
4.4 query和fullPath
注意1:在hash地址中,/后面的参数项,叫做”路径参数“
在路由”参数对象“中,需要使用this.$route.parms来访问路径参数
注意2:在hash地址中,?后面的参数项,叫做”查询参数“
在路由”参数对象“中,需要使用this.$route.query来访问查询参数
注意3:在this.$route中,path只是路径部分,fullPath是完整的地址
例如:
/movie/2?name=zs&age=20 是fullPath的值
/movie/2 是fullPath的值
<router-link to="/movie/2?name=zs&age=20">雷神</router-link>
5. 声明式导航 & 编程式导航
5.1 vue-router 中的编程式导航 API
5.2 this.$router.push('hash地址):增加一条记录
调用 this.$router.push() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面。
<button @click="gotoLK">跳转到”洛基“页面</button>
methods:{
gotoLK(){
// 通过编程式导航API,导航跳转到指定的页面
this.$router.push('/movie/1')
}
}
5.3 this.$router.replace('hash地址'):替换掉当前的历史记录
调用 this.$router.replace() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面。
push 和 replace 的区别:
⚫ push 会增加一条历史记录
⚫ replace 不会增加历史记录,而是替换掉当前的历史记录
<button @click="gotoLK2">通过replace跳转到”洛基“页面</button>
gotoLK2(){
// replace---替换当前的页面,没办法回之前的页面
this.$router.replace('/movie/1')
}
5.4 this.$router.go
调用 this.$router.go() 方法,可以在浏览历史中前进和后退。
<button @click="goback">后退</button>
goback(){
// 返回前一个页面
// 如果后退的层数超过上限,则原地不动
// this.$router.go(-1);
this.$router.go(-2);
}
5.5 $router.go 的简化用法
<!-- 在行内使用编程式导航跳转的时候,this必须要省略,否则报错 -->
<!-- back()--后退一层 -->
<button @click="$router.back()">back 后退</button>
<!-- forward()--前进一层 -->
<button @click="$router.forward()">forward 前进</button>
6. 导航守卫:以控制路由的访问权限
6.1 全局前置守卫:全局生效
每次发生路由的导航跳转时,都会触发全局前置守卫。因此,在全局前置守卫中,程序员可以对每个路由进行 访问权限的控制:
6.2 守卫方法的 3 个形参
全局前置守卫的回调函数中接收 3 个形参
// 创建路由的实例对象
const router=new VueRouter()
// 为router实例对象,声明全局前置导航守卫
// 只要发生了路由的跳转,必然会触发beforEach指定的function回调函数
router.beforeEach(function(to,from,next){
// to-表示要访问的路由信息对象
// from-表示将要离开的路由信息对象
// next()函数表示放行,如果被调用就会报错
next()
})
6.3 next 函数的 3 种调用方式
当前用户拥有后台主页的访问权限,直接放行:next()
当前用户没有后台主页的访问权限,强制其跳转到登录页面:next('/login')
当前用户没有后台主页的访问权限,不允许跳转到后台主页:next(false)
6.4 控制后台主页的访问权限
// 为router实例对象,声明全局前置导航守卫
// 只要发生了路由的跳转,必然会触发beforEach指定的function回调函数
router.beforeEach(function(to,from,next){
// to-表示要访问的路由信息对象
// from-表示将要离开的路由信息对象
// 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') {
// 要访问后台主页,需要判断是否有 token
const token = localStorage.getItem('token')
if (token) {//表示拿到token值
next()
} else {
// 没有登录,强制跳转到登录页
next('/login')
}
}//表示不是要进入后台主页
else {
next()
}
})
4.后台管理案例
1. 案例效果
2. 案例用到的知识
2.1 配置路由
1.安装vue-router的包
2.在src中创建一个router的文件夹,在此文件夹中创建一个index.js
3.在main.js中导入路由
index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
// 将插件装上
Vue.use(VueRouter)
const router=new VueRouter()
export default router
main.js
// 导入路由模块
import router from '@/router/index.js'
new Vue({
render: h => h(App),
router:router
}).$mount('#app')
2.2 基于路由渲染登录组件
1.在inedx.js文件中导入需要进行链接的hash地址和组件
2.在App.vue中输入占位符
3.将进入“/”地址页面的时候,强制跳转到login页面(redirect)
{path:'/',redirect:'/login'}
index.js
// 导入需要的组件
import Login from '@/components/MyLogin.vue'
const router=new VueRouter({
// 路由规则,建立hash地址与组件的对应关系
routes:[
// 当进入/地址的时候强制跳转到login页面
{path:'/',redirect:'/login'},
{path:'/login',component:Login},
]
})
App.vue
<template>
<div>
<!-- 路由占位符 -->
<router-view></router-view>
</div>
</template>
2.3 模拟登录功能
1.给MyLogin组件输入data数据
2.分别给登录名和登录密码加上v-molde双向数据绑定
3.判断输入的是否与设置的一致
3.1 如果登录成功则记录token并且跳转页面
3.2 如果登录失败,则移除token
MyLogin.vue
<!-- 给登录名进行数据的双向绑定:v-modle -->
<input type="text" class="form-control ml-2" id="username" placeholder="请输入登录名称" autocomplete="off" v-model.trim="username">
<input type="password" class="form-control ml-2" id="password" placeholder="请输入登录密码" v-model.trim="passward">
<button type="button" class="btn btn-secondary mr-2" @click="reset">重置</button>
<button type="button" class="btn btn-primary" @click="login">登录</button>
<script>
export default {
name: 'MyLogin',
data(){
return {
username:'',
passward:''
}
},
methods:{
// 重置按钮
reset(){
this.username='',
this.passward=''
},
login(){
if(this.username==='admin' && this.passward==='666666'){
// 登录成功
// 1.存储token
localStorage.setItem('token','Bearer xxxx')
// 2.跳转到后台主页
this.$router.push('/home')
}else{
//登录失败
// 清除token
localStorage.removeItem('token')
}
}
}
}
</script>
2.4 实现后台主页的基础布局
1.在index.js文件中声明hash地址与组件的对应关系
// 导入需要的组件 import Home from '@/components/MyHome.vue' // 将插件装上 Vue.use(VueRouter) const router=new VueRouter({ // 路由规则,建立hash地址与组件的对应关系 routes:[ // 后台主页的路由规则 {path:'/home',component:Home} ] })
2.在MyHome.vue组件中编写页面的形式
<template> <div class="home-container"> <!-- 头部区域 --> <MyHeader></MyHeader> <!-- 页面主体区 --> <div class="home-main-box"> <!-- 左侧边栏 --> <MyAside></MyAside> <!-- 右侧内容主题区域 --> <div class="home-main-body"> 123 </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>
2.5 退出登录并控制访问权限
1.在退出登录组件中添加带点击事件的时候,删除token
<button type="button" class="btn btn-light" @click="loginout">退出登录</button> methods:{ loginout(){ // 1.清空token localStorage.removeItem('token') // 2.跳转到登录页面 this.$router.push('/login') } }
2.因为当未登录的时候可以访问home组件,所以要在index.js文件中使用全局守卫保护数据
// 全局前置守卫 router.beforeEach(function(to,from,next){ if(to.path==='/home'){ // 如果要访问的页面是home,则先进行判断是否已经登录 const token=localStorage.getItem('token') if(token){ next() }else{ next('/login') } }else{ next() } })
2.6 实现子路由的嵌套展示
1.给左侧添加<router-link>链接,并添加to属性【MyAside组件】
<!-- 左侧边栏列表 --> <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>
2.在右侧组件中添加占位符【MyHome组件】
<!-- 右侧内容主题区域 --> <div class="home-main-body"> <router-view></router-view> </div>
3.给地址为“/home”的地址添加子路由嵌套【children添加子路由】
const router=new VueRouter({ // 路由规则,建立hash地址与组件的对应关系 routes:[ // 当进入/地址的时候强制跳转到login页面 {path:'/',redirect:'/login'}, {path:'/login',component:Login}, // 后台主页的路由规则 {path:'/home',component:Home,children:[ // children中的path路径中不能加“/” { path: 'users', component: Users }, { path: 'rights', component: Rights }, { path: 'goods', component: Goods }, { path: 'orders', component: Orders }, { path: 'settings', component: Settings }, ]} ] })
2.7 点击进入用户详情页面
1.先将data数渲染到页面
<tbody> <!-- 使用循环拿到data中的数据进行渲染 --> <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="#">详情</a> </td> </tr> </tbody> <script> export default { name: 'MyUser', data() { return { // 用户列表数据 userlist: [ { id: 1, name: '嬴政', age: 18, position: '始皇帝' }, { id: 2, name: '李斯', age: 35, position: '丞相' }, { id: 3, name: '吕不韦', age: 50, position: '商人' }, { id: 4, name: '赵姬', age: 48, position: '王太后' } ] } } } </script>
2.当点击“详情”的时候进行跳转
<td> <!-- 阻止默认行为 --> <a href="#" @click.prevent="gotoDetail">详情</a> </td>
3.右边的“详情”应该跟左边的“用户管理”是兄弟,所以要设置路由规则,跟其平级【在index.js中】
// 详情页 import UserDetail from '@/components/user/MyUserDetail.vue' // 后台主页的路由规则 {path:'/home',component:Home,children:[ // children中的path路径中不能加“/” { path: 'users', component: Users }, { path: 'rights', component: Rights }, { path: 'goods', component: Goods }, { path: 'orders', component: Orders }, { path: 'settings', component: Settings }, // 用户详情页的路由规则 {path:'userinfo',component:UserDetail} ]}
4.补充gotoDetail函数
methods:{ gotoDetail(){ // console.log('ok'); this.$router.push('/home/userinfo') }, }
5. 设置后退按钮功能【在MyUserDetaile】
<button type="button" class="btn btn-light btn-sm" @click="$router.back()">后退</button>
2.8 升级用户详情页面的路由规则
1.因为如果单纯写路径不够完整,应该将id传入
<td> <!-- 阻止默认行为 --> <!-- 往gotoDetail中加入要进行跳转的id --> <a href="#" @click.prevent="gotoDetail(item.id)">详情</a> </td> methods:{ gotoDetail(id){ // console.log('ok'); this.$router.push('/home/userinfo/'+id) }, }
2.给路由规则添加动态绑定的id
// 用户详情页的路由规则 {path:'userinfo/:id',component:UserDetail}
3.在第1个页面拿到第一个页面应该出现的组件
2.9 当登录进去后要直接跳转到users页面【redirect】
// 后台主页的路由规则
{path:'/home',
component:Home,
// 添加重定向,强制从登录页面一进来直接进入/home/users页面
redirect:'/home/users',
children:[
// children中的path路径中不能加“/”
{ path: 'users', component: Users },
{ path: 'rights', component: Rights },
{ path: 'goods', component: Goods },
{ path: 'orders', component: Orders },
{ path: 'settings', component: Settings },
// 用户详情页的路由规则
{path:'userinfo/:id',component:UserDetail,props:true}
]}
2.10 如何控制多页面的权限【多写一个数组存放】
// 全局前置守卫
router.beforeEach(function(to,from,next){
const pathArr=['/home','/home/users','.home/rights']
if(pathArr.indexOf(to.path)!==-1){
// 如果要访问的页面是home,则先进行判断是否已经登录
const token=localStorage.getItem('token')
if(token){
next()
}else{
next('/login')
}
}else{
next()
}
})
总结
5. 完整的页面项目:Headline | 黑马头条 - 移动端
1.创建(vue create demo-toutiao)并梳理项目结构
1.先将自动生成的App组件中生成的代码删除
<template> <div> <h1>App 根组件</h1> </div> </template> <script> export default { name: 'App' } </script> <style lang="less" scoped> </style>
2.将views和components文件夹中的所有文件删除,并且将router文件夹中的index.js文件中的router中的内容清除
import Vue from 'vue' import VueRouter from 'vue-router' // 把VueRouter安装为Vue的插件 Vue.use(VueRouter) // 路由规则的数组 const routes = [ ] // 创建路由实例对象 const router = new VueRouter({ routes }) export default router
2.初始化-安装和配置Vant组件库
Vant 2 - 轻量、可靠的移动端组件库 (gitee.io)
1.使用Vant步骤:安装,配置,使用
npm i vant@latest-v2 -S --legacy-peer-deps
2.导入组件【在main.js文件中导入】
// 导入并安装Vant组件库 import Vant from 'vant' import 'vant/lib/index.css' Vue.use(Vant)
3.使用Tabbar组件并开启路由模式【因为实现了路由切换所以放在view文件夹下面】
1.初始化页面
<template> <div> <h1>Home 组件</h1> </div> </template> <script> export default { name: 'Home' } </script> <style lang="less" scoped> </style>
<template> <div> <h1>User 组件</h1> </div> </template> <script> export default { name: 'User' } </script> <style lang="less" scoped> </style>
2.在App组件中创建占位符
3.将Tabbar组件放入【放在App组件中】
<template> <div> <!-- 路由占位符 --> <!-- Tabbar区域 --> <van-tabbar v-model="active"> <van-tabbar-item icon="home-o" >首页</van-tabbar-item> <van-tabbar-item icon="user-o" >我的</van-tabbar-item> </van-tabbar> </div> </template> <script> export default { data () { return { active: 0 } } } </script>
4.添加路由模式
<template> <div> <!-- 路由占位符 --> <!-- Tabbar区域 --> <van-tabbar v-model="active"> <van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item> <van-tabbar-item icon="user-o" to="/user">我的</van-tabbar-item> </van-tabbar> </div> </template> <script> export default { data () { return { active: 0 } } } </script>
4.通过路由展示对应的Tabbar页面
1.将页面上定一个占位符【在App组件中定义】
<!-- 路由占位符 --> <router-view></router-view>
2.将组件和hash地址的对应关系写在router文件加下index.js文件中
import Vue from 'vue' import VueRouter from 'vue-router' // 导入需要的组件 import Home from '@/views/Home/Home.vue' import User from '@/views/User/User.vue' Vue.use(VueRouter) const routes = [ // 定义首页的路由规则 { path: '/', component: Home }, // 定义我的路由规则 { path: '/user', component: User } ] const router = new VueRouter({ routes }) export default router
5.使用Navbar导航栏组件
1.导入组件中的的模块【应该放在Home组件中】
<template> <div> <van-nav-bar title="标题" left-text="返回" right-text="按钮" left-arrow /> </div> </template>
2.设置标题为固定定位【:fixed="true"】
<template> <div> <van-nav-bar title="标题" :fixed="true"/> </div> </template>
3.将被标题遮挡住的内容进行显示【分别给div加上下边框】
6.覆盖Navbar的默认样式
1.使用调试工具进行修改
2.注意点:
7.了解获取列表数据的API接口
8.封装utils目录下的request模块【使用axios】
1.安装axios包
npm i axios -S -legacy-peer-deps
2.配置axois【多创建一个request.js文件】
import axios from 'axios' // request 就是小axios const request = axios.create({ // 指定请求的根路径 baseURL: 'https://applet-base-api-t.itheima.net' }) export default request
9.在Home组件中封装initArticleList方法【当进入Home页面的时候自动请求数据显示】
1.先在Home组件中导入request.js文件
// 导入request.js import reuqest from '@/utils/request.js'
2.先在data中声明数据
data () { return { // 页码值 page: 1, // 每一页显示多少条数据 limit: 10 } },
3.封装函数
methods: { // 封装获取文章列表数据的方法 async initArticleList () { // 发起GET请求,获取文章的列表数据 // 获取到的对象是Promise,所以要使用await和async const { data: res } = await reuqest.get('/articles', { // 请求参数 params: { _page: this.page, _limit: this.limit } }) console.log(res) } }
4.调用该函数【created要写在函数的声明前】
created () { this.initArticleList() },
10.文章列表--封装articleAPI模块
1.先在User组件中声明一个函数,当进入User页面的时候,获取5条数据
<script> // 导入 import request from '@/utils/request.js' export default { data () { return { page: 1, limit: 5 } }, created () { this.initArticleList() }, name: 'User', methods: { async initArticleList () { const { data: res } = await request.get('/articles', { params: { _page: this.page, _limit: this.limit } }) console.log(res) } } } </script>
2.因为重复写了调用接口的Promise所以直接写成一个js文件然后进行调用
3.在src文件夹下新建一个文件夹为api,然后再这个文件夹中新建一个articleAPI.js文件
4.因为获取到的是一个Promise对象,所以将这个Promise对象放在articleAPI.js文件中,如果需要则直接再外调用
api/articleAPI.js
// 文章相关的API接口,都封装到这个模块中 // 导入request组件 import request from '@/utils/request' // 向外按需导出一个API函数 /* 这是一个Promise对象 request.get('/articles', { params: { _page: this.page, _limit: this.limit } }) */ export const getArticleListAPI = function (_page, _limit) { // console.log('调用了getArticleListAPI这个函数') return request.get('/articles', { params: { _page: _page, _limit: _limit } }) }
User.vue
<script> // 导入 // import request from '@/utils/request.js' // 按需导入API接口 import { getArticleListAPI } from '@/api/articleAPI.js' // 此时获取到的result是一个Promise对象 // const result = getArticleListAPI(1, 5) // console.log(result) export default { data () { return { page: 1, limit: 5 } }, created () { this.initArticleList() }, name: 'User', methods: { async initArticleList () { // const { data: res } = await Promise对象 const { data: res } = await getArticleListAPI(this.page, this.limit) console.log(res) } } } </script>
Home.vue
<script> // 导入request.js // import reuqest from '@/utils/request.js' // 按需导入API接口 import { getArticleListAPI } from '@/api/articleAPI' export default { name: 'Home', data () { return { // 页码值 page: 1, // 每一页显示多少条数据 limit: 10 } }, created () { this.initArticleList() }, methods: { // 封装获取文章列表数据的方法 async initArticleList () { // 发起GET请求,获取文章的列表数据 // 获取到的对象是Promise,所以要使用await和async const { data: res } = await getArticleListAPI(this.page, this.limit) console.log(res) } } } </script>
11.文章列表--封装ArticleInfo组件
1.当我们拿到一份数据要再页面上进行渲染,则应该将这份数据存放再data里面
2.创建一个ArticleInfo的组件【存放再components文件夹下的】
<template> <div> <h1>ArticleInfo 组件</h1> </div> </template> <script> export default { name: 'ArticleInfo' } </script> <style lang="less" scoped> </style>
3.在Home组件中导入,注册使用ArticleInfo组件
<template> <div class="home-container"> <!-- 导入,注册,并使用ArticleInfo组件 --> <h1> {{ artlist.length }}</h1> <!-- 3.使用 ArticleInfo组件 --> <ArticleInfo></ArticleInfo> </div> </template> // 1.导入ArticleInfo组件 import ArticleInfo from '@/components/Article/ArticleInfo.vue' // 2.注册组件 components: { ArticleInfo }
4.动态循环ArticleInfo组件
<ArticleInfo v-for="item in artlist" :key="item.id"></ArticleInfo>
12.文章列表-为ArticleInfo组件封装props属性
1.在ArticleInfo组件中封装属性
ArticleInof组件
<!-- 标题 --> <span>{{title}}</span> <div class="label-box"> <span>作者{{ author}} {{cmtCount}}评论 发布日期 {{ time }}</span> <script> export default { name: 'ArticleInfo', // 自定义属性 props: { // 标题 title: { type: String, default: '' }, // 作者名字 author: { type: String, default: '' }, // 评论数 cmtCount: { // 通过数组形式,为当前属性定义多个可能的类型 type: [Number, String], default: 0 }, // 发布日期 time: { type: String, default: '' } } } </script>
Home组件
<!-- 导入,注册,并使用ArticleInfo组件 --> <!-- 3.使用 ArticleInfo组件 --> <ArticleInfo v-for="item in artlist" :key="item.id" :title="item.title" :author="item.aut_name" :cmtCount="item.comm_count" :time="item.pubdate" ></ArticleInfo>
13.文章列表--为ArticleInfo组件封装cover属性(封面图片)
1.定义cover属性
// 封面的信息对象 cover: { type: Object, // 通过default函数,返回cover属性的默认值 default: function () { //这个return的对象就是cover属性的默认值 return { type: 0 } } }
2.动态添加属性
3.在ArticleInfo组件进行判断是显示几张照片
4.给图片动态绑定
14.上拉加载更多--了解List组件的基本使用
Vant 2 - 轻量、可靠的移动端组件库 (gitee.io)
15.上拉加载更多--初步使用List组件
1.先将代码封装在<van-list></van-list>
2.初始化默认值和onLoad函数
// 是否正在加载下一页数据,如果loading为true 则不会反复触发load事件 // 每当下一页数据请求回来之后,千万要记得,把loading从true改为false // 因为刚刚进入页面的时候,不需要触发onLoad事件,所以应该将loading初始化为true,才不会进行数据请求 loading: true, // 所有数据是否加载完毕了,如果没有更多数据了,一定要把finished改为true finished: false // 只要onLoad被调用,就应该请求下一页数据 onLoad () { console.log('触发了load事件') }
3.因为初始化loading为true,当第一页数据渲染完之后,就应该将loading改为false,才可与进行访问第二页数据
async initArticleList () { // 发起GET请求,获取文章的列表数据 // 获取到的对象是Promise,所以要使用await和async const { data: res } = await getArticleListAPI(this.page, this.limit) // console.log(res) this.artlist = res // 当第一页渲染完,应该将loading改为false,等一下才可以访问第二页 this.loading = false },
16.上拉加载更多--实现上拉加载更多的效果
1.上拉触发事件
// 只要onLoad被调用,就应该请求下一页数据 onLoad () { // console.log('触发了load事件') // 1.让页码值+1 this.page++ // 2.重新请求接口获取数据 this.initArticleList() }
2.上面的写法会将第一页的数据进行覆盖,所以要使用将旧数据中的数据记录下来
async initArticleList () { // 发起GET请求,获取文章的列表数据 // 获取到的对象是Promise,所以要使用await和async const { data: res } = await getArticleListAPI(this.page, this.limit) // console.log(res) // this.artlist = res // 如果上拉加载更多,那么应该是 // this.artlist=[旧数据在前,新数据在后] this.artlist = [...this.artlist, ...res] // 当第一页渲染完,应该将loading改为false,等一下才可以访问第二页 this.loading = false },
3.页数的判断条件
// 封装获取文章列表数据的方法 async initArticleList () { // 发起GET请求,获取文章的列表数据 // 获取到的对象是Promise,所以要使用await和async const { data: res } = await getArticleListAPI(this.page, this.limit) // console.log(res) // this.artlist = res // 如果上拉加载更多,那么应该是 // this.artlist=[旧数据在前,新数据在后] this.artlist = [...this.artlist, ...res] // 当第一页渲染完,应该将loading改为false,等一下才可以访问第二页 this.loading = false if (res.length === 0) { // 证明没有下一页数据了,直接把finished改为true,表示数据加载完了 this.finished = true }
过程:
1.在触发o'n'Load的时候先让页码值+1
2.重新请求接口获取数据
3.对获取回来的数据进行拼接
4.判断是否有下一页
17.下拉刷新--实现下拉刷新的功能
1.将下拉刷新的模块导入
2.初始化onRefresh和onRefresh函数
// 是否下拉刷新 refreshing: false // 下拉刷新的处理函数 onRefresh () { // console.log('触发了下拉刷新') // 1.让页码值+1 this.page++ // 2.重新请求接口获取数据 this.initArticleList() }
3.在initArticleList函数内判断是要上拉刷新还是下拉刷新(因为数据的拼接顺序不一样)
if (isRefresh) { // 证明是下拉刷新:新数据在前,旧数据在后 this.artlist = [...res, ...this.artlist] } else { // 如果上拉加载更多,那么应该是 // this.artlist=[旧数据在前,新数据在后] this.artlist = [...this.artlist, ...res] } // 2.重新请求接口获取数据 // 只有当下拉刷新的时候才需要在调用该函数的时候传参 // async initArticleList (isRefresh) { this.initArticleList(true)
4.当我们下拉请求数据后应该将isLoading改为false,要不然没办法进行第二次数据请求
if (isRefresh) { // 证明是下拉刷新:新数据在前,旧数据在后 this.artlist = [...res, ...this.artlist] this.isLoading = false } else { // 如果上拉加载更多,那么应该是 // this.artlist=[旧数据在前,新数据在后] this.artlist = [...this.artlist, ...res] }
5.判断数据是否已经渲染完毕
<van-pull-refresh v-model="isLoading" :disabled="finished" @refresh="onRefresh"> <van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="onLoad" > <!-- 导入,注册,并使用ArticleInfo组件 --> <!-- 3.使用 ArticleInfo组件 --> <ArticleInfo v-for="item in artlist" :key="item.id" :title="item.title" :author="item.aut_name" :cmtCount="item.comm_count" :time="item.pubdate" :cover="item.cover" ></ArticleInfo> </van-list> </van-pull-refresh>
18.定制主题-说明Vant定制主题的核心原理【less变量的覆盖】
Vant 2 - 轻量、可靠的移动端组件库 (gitee.io)
1.在main.js文件中导入less文件
// 切记:为了能够覆盖默认的less变量,这里一定要把后缀名改为.less import 'vant/lib/index.less'
2.修改样式变量
// 这个文件是 vue-cli 创建出来的项目的配置文件 // 在 vue.config.js 这个配置文件中,可以对整个项目的打包、构建进行全局性的配置 // webpack 在进行打包的时候,底层用到了 node.js // 因此,在 vue.config.js 配置文件中,可以导入并使用 node.js 中的核心模块 const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, css: { loaderOptions: { less: { modifyVars: { // 直接覆盖变量 // 或者可以通过 less 文件覆盖(文件路径为绝对路径) hack: 'true; @import "your-less-file-path.less";' } } } } })
3.通过theme.less定制主题【在src文件夹根目录下创建一个theme.less文件】
// 在theme.less文件中,覆盖Vant官方的less 变量值 @blue:#007bff; // 覆盖Navbar的less样式 @nav-bar-background-color:@blue;
4.导入less文件【在vue.config.js文件中导入】
// 这个文件是 vue-cli 创建出来的项目的配置文件 // 在 vue.config.js 这个配置文件中,可以对整个项目的打包、构建进行全局性的配置 // webpack 在进行打包的时候,底层用到了 node.js // 因此,在 vue.config.js 配置文件中,可以导入并使用 node.js 中的核心模块 const path = require('path') // __dirname--当前的根目录 const themePath = path.join(__dirname, './src/theme.less') const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, css: { loaderOptions: { less: { modifyVars: { // 直接覆盖变量 // 或者可以通过 less 文件覆盖(文件路径为绝对路径) // ../ ./ theme.less // 从盘符开始的路径,叫做绝对路径 C:\\Users\liulongbin\\theme.less // hack: 'true; @import "绝对路径";'(外面是使用模板字符串) hack: `true; @import "${themePath}";` } } } } })
打包发布
1.npm run build【生成dist文件夹】
2.查看vue-cli文档
3.在vue.config.js文件中添加
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ publicPath: '', transpileDependencies: true })
4.重新run一下
六、ES6模块化与异步编程高级用法
1.ES6模块化
1. 回顾:node.js 中如何实现模块化
2. 前端模块化规范的分类
3. 什么是 ES6 模块化规范
ES6 模块化规范中定义:
⚫ 每个 js 文件都是一个独立的模块
⚫ 导入其它模块成员使用 import 关键字
⚫ 向外共享模块成员使用 export 关键字
4. 在 node.js 中体验 ES6 模块化
① 确保安装了 v14.15.1 或更高版本的 node.js
② 在 package.json 的根节点中添加 "type": "module" 节点
5. ES6 模块化的基本语法:默认导出【export default 默认导出的成员】
let n1=10;
let n2=20;
function show(){}
// 外界只能访问到n1和show,访问不到n2
export default{
n1,
show
}
注意事项
每个模块中,只允许使用唯一的一次 export default,否则会报错!
5. ES6 模块化的基本语法:默认导入【import 接收名称 from '模块标识符'】
// 记得加上后缀名
import m1 from './01.默认导出.js'
console.log(m1);
注意事项
默认导入时的接收名称可以任意名称,只要是合法的成员名称即可:(数字开头报错)
5.2 按需导出:【export 按需导出的成员】
export let s1 = 'aaa'
export let s2 = 'ccc'
export function say() {}
export default {
a: 20
}
5.2 按需导入:【import { s1 } from '模块标识符'】
import {s1, s2 , say} from './03.按需导出.js'
console.log(s1)
console.log(s2)
console.log(say)
5.2 按需导出与按需导入的注意事项
① 每个模块中可以使用多次按需导出(但是默认导出只能有一个)
② 按需导入的成员名称必须和按需导出的名称保持一致
③ 按需导入时,可以使用 as 关键字进行重命名
④ 按需导入可以和默认导入一起使用
5.3 直接导入并执行模块中的代码
如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。此时,可以直接导入并执行模 块代码,示例代码如下:
2.Promise
1. 回调地狱:多层回调函数的相互嵌套
回调地狱的缺点:
⚫ 代码耦合性太强,牵一发而动全身,难以维护
⚫ 大量冗余的代码相互嵌套,代码的可读性变差
1.1 如何解决回调地狱的问题
为了解决回调地狱的问题,ES6(ECMAScript 2015)中新增了 Promise 的概念。
1.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 => { })
⚫ 调用 .then() 方法时,成功的回调函数是必选的、失败的回调函数是可选的
2. 基于回调函数按顺序读取文件内容
3. 基于 then-fs 读取文件内容
由于 node.js 官方提供的 fs 模块仅支持以回调函数的方式读取文件,不支持 Promise 的调用方式。因此,需 要先运行如下的命令,安装 then-fs 这个第三方包,从而支持我们基于 Promise 的方式读取文件的内容:
3.1 then-fs 的基本使用
调用 then-fs 提供的 readFile() 方法,可以异步地读取文件的内容,它的返回值是 Promise 的实例对象。因 此可以调用 .then() 方法为每个 Promise 异步操作指定成功和失败之后的回调函数。示例代码如下:
注意:上述的代码无法保证文件的读取顺序,需要做进一步的改进!
import thenFs from 'then-fs'
// .then()中的失败回调函数可以不写
// thenFs.readFile('./files/1.txt', 'utf8')--返回的是一个Promise对象【是异步的,所以每一次输出的数值顺序不一样】
thenFs.readFile('./files/1.txt', 'utf8').then((r1) => {console.log(r1)})
thenFs.readFile('./files/2.txt', 'utf8').then((r2) => {console.log(r2)})
thenFs.readFile('./files/3.txt', 'utf8').then((r3) => {console.log(r3)})
3.2 .then() 方法的特性
如果上一个 .then() 方法中返回了一个新的 Promise 实例对象,则可以通过下一个 .then() 继续进行处理。通 过 .then() 方法的链式调用,就解决了回调地狱的问题。
3.3 基于 Promise 按顺序读取文件的内容
Promise 支持链式调用,从而来解决回调地狱的问题。示例代码如下:
import thenFs from 'then-fs'
thenFs
.readFile('./files/1.txt', 'utf8')
.then((r1) => {
console.log(r1)
//将第二个Promise对象作为第一个对象成功的回调函数的return返回值返回出去,然后再用.then()进行接收
return thenFs.readFile('./files/2.txt', 'utf8')
})
.then((r2) => {
console.log(r2)
return thenFs.readFile('./files/3.txt', 'utf8')
})
.then((r3) => {
console.log(r3)
})
3.4 通过 .catch 捕获错误
在 Promise 的链式操作中如果发生了错误,可以使用 Promise.prototype.catch 方法进行捕获和处理:
如果不希望前面的错误导致后续的 .then 无法正常执行,则可以将 .catch 的调用提前(放在最前面)
import thenFs from 'then-fs'
thenFs
.readFile('./files/11.txt', 'utf8')
// 把.catch()放再最前面,则就算出现错误,但是错误后面的then()不受到影响,还是可以继续执行
.catch((err) => {
console.log(err.message)
})
.then((r1) => {
console.log(r1)
//将第二个Promise对象作为第一个对象成功的回调函数的return返回值返回出去,然后再用.then()进行接收
return thenFs.readFile('./files/2.txt', 'utf8')
})
.then((r2) => {
console.log(r2)
return thenFs.readFile('./files/3.txt', 'utf8')
})
.then((r3) => {
console.log(r3)
})
3.5 Promise.all() 方法【等所有的异步操作全部结束后才会执行下一步的 .then 操作】
Promise.all() 方法会发起并行的 Promise 异步操作,等所有的异步操作全部结束后才会执行下一步的 .then 操作(等待机制)。
注意:数组中 Promise 实例的顺序, 就是最终结果的顺序!
import thenFs from 'then-fs'
const promiseArr = [
thenFs.readFile('./files/3.txt', 'utf8'),
thenFs.readFile('./files/2.txt', 'utf8'),
thenFs.readFile('./files/1.txt', 'utf8'),
]
// 数组中 Promise 实例的顺序, 就是最终结果的顺序
Promise.all(promiseArr).then(result => {
console.log(result)//333,222,111
})
3.6 Promise.race() 方法【只要任何一个异步操作完成,就立即执行下一步的 .then 操作】
Promise.race() 方法会发起并行的 Promise 异步操作,只要任何一个异步操作完成,就立即执行下一步的 .then 操作(赛跑机制)。
import thenFs from 'then-fs'
const promiseArr = [
thenFs.readFile('./files/3.txt', 'utf8'),
thenFs.readFile('./files/2.txt', 'utf8'),
thenFs.readFile('./files/1.txt', 'utf8'),
]
// 数组中 Promise 实例的顺序, 就是最终结果的顺序
Promise.race(promiseArr).then(result => {
console.log(result)//输出结果取决于哪一个文件读取较快
})
4. 基于 Promise 封装读文件的方法
方法的封装要求:
① 方法的名称要定义为 getFile
② 方法接收一个形参 fpath,表示要读取的文件的路径
③ 方法的返回值为 Promise 实例对象
4.1 getFile 方法的基本定义
4.2 创建具体的异步操作
如果想要创建具体的异步操作,则需要在 new Promise() 构造函数期间,传递一个 function 函数,将具体的 异步操作定义到 function 函数内部。示例代码如下:
function getFile(fpath) {
// return new Promise--创建了形式上的异步
return new Promise(function (resolve, reject) {
fs.readFile(fpath, 'utf8', (err, dataStr) => {
})
})
}
4.3 获取 .then 的两个实参
通过 .then() 指定的成功和失败的回调函数,可以在 function 的形参中进行接收,
4.4 调用 resolve(读取成功回调函数) 和 reject(读取失败回调函数) 回调函数
Promise 异步操作的结果,可以调用 resolve 或 reject 回调函数进行处理。
import fs from 'fs'
function getFile(fpath) {
// return new Promise--创建了形式上的异步
return new Promise(function (resolve, reject) {
fs.readFile(fpath, 'utf8', (err, dataStr) => {
// 如果读取失败,则调用失败的回调函数
if (err) return reject(err)
// 如果读取成功,则调用成功的回调函数
resolve(dataStr)
})
})
}
getFile('./files/11.txt')
.then((r1) => {
console.log(r1)
})
.catch((err) => console.log(err.message))
3.async/await
1. 什么是 async/await
async/await 是 ES8(ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作。在 async/await 出 现之前,开发者只能通过链式 .then() 的方式处理 Promise 异步操作。
.then 链式调用的优点: 解决了回调地狱的问题
.then 链式调用的缺点: 代码冗余、阅读性差、 不易理解
2. async/await 的基本使用
import thenFs from 'then-fs'
async function getAllFile() {
// thenFs.readFile--Promise 对象
// 被await修饰过的Promise对象就变成了一个普通变量
// 当函数内部有await修饰过的对象,构造函数外部要加上async
const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
console.log(r1)
const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
console.log(r2)
const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
console.log(r3)
}
getAllFile()
3. async/await 的使用注意事项
① 如果在 function 中使用了 await,则 function 必须被 async 修饰
② 在 async 方法中,第一个 await 之前的代码会同步执行,await 之后的代码会异步执行
import thenFs from 'then-fs'
console.log('A')
async function getAllFile() {
//同步执行,因为下面是异步的,所以先跳出这个函数
console.log('B')
// thenFs.readFile--Promise 对象
// 被await修饰过的Promise对象就变成了一个普通变量
// 当函数内部有await修饰过的对象,构造函数外部要加上async
const r1 = await thenFs.readFile('./files/1.txt', 'utf8')
console.log(r1)
const r2 = await thenFs.readFile('./files/2.txt', 'utf8')
console.log(r2)
const r3 = await thenFs.readFile('./files/3.txt', 'utf8')
console.log(r3)
console.log('D')
}
getAllFile()
console.log('C')
//输出结果为A B C 111 222 333 D
4.EventLoop
1. JavaScript 是单线程的语言:同一时间只能做一件事情
JavaScript 是一门单线程执行的编程语言。也就是说,同一时间只能做一件事情。
2. 同步任务和异步任务
为了防止某个耗时任务导致程序假死的问题,JavaScript 把待执行的任务分为了两类:
① 同步任务(synchronous)
⚫ 又叫做非耗时任务,指的是在主线程上排队执行的那些任务
⚫ 只有前一个任务执行完毕,才能执行后一个任务
② 异步任务(asynchronous)
⚫ 又叫做耗时任务,异步任务由 JavaScript 委托给宿主环境进行执行
⚫ 当异步任务执行完成后,会通知 JavaScript 主线程执行异步任务的回调函数
3. 同步任务和异步任务的执行过程
① 同步任务由 JavaScript 主线程次序执行
② 异步任务委托给宿主环境执行
③ 已完成的异步任务对应的回调函数,会被 加入到任务队列中等待执行
④ JavaScript 主线程的执行栈被清空后,会 读取任务队列中的回调函数,次序执行
⑤ JavaScript 主线程不断重复上面的第 4 步
4. EventLoop 的基本概念
JavaScript 主线程从“任务队列”中读取异步 任务的回调函数,放到执行栈中依次执行。这 个过程是循环不断的,所以整个的这种运行机 制又称为 EventLoop(事件循环)
4. 结合 EventLoop 分析输出的顺序
5.宏任务和微任务(异步任务的划分)
1. 什么是宏任务和微任务
JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:
2. 宏任务和微任务的执行顺序:宏任务-->微任务(交替执行)
每一个宏任务执行完之后,都会检查是否存在待执行的微任务, 如果有,则执行完所有微任务之后,再继续执行下一个宏任务。
3. 去银行办业务的场景
4. 分析以下代码输出的顺序
5. 经典面试题
console.log('1');
setTimeout(function(){
console.log('2');
new Promise(function(resolve){
console.log('3');
resolve()
}).then(function(){
console.log('4');
})
})
new Promise(function(resolve){
console.log('5');
resolve()
}).then(function(){
console.log('6');
})
setTimeout(function(){
console.log('7');
new Promise(function(resolve){
console.log('8');
resolve();
}).then(function(){
console.log('9');
})
})
6.API接口案例
1. 案例需求
2. 主要的实现步骤
3. 搭建项目的基本结构
4. 创建基本的服务器
//搭建一个express服务器
import express from 'express'
const app=express();
app.listen(20,()=>{
console.log('server running at http://127.0.0.1');
})
5. 创建 db 数据库操作模块
//数据库操作模块
import mysql from 'mysql2'
const pool=mysql.createPool({
host:'127.0.0.1',
port:3306,
database:'my_db_01',
user:'root',
password:'111111'
})
// 将pool以Promise的形式向外暴露
export default pool.promise()
6. 创建 user_ctrl 模块
//查询数据库中的数据
import db from '../db/index.js'
// 使用ES6的按需导出语法,将getAllUser方法导出去
async function getAllUser(req,res){
// db.query()--输入查询语句,并且返回的是一个Promise对象
// 索引为0是我们要的数据
const [rows]= await db.query('select id,username,nickname from ev_users')
// console.log(rows);
//将数据发送给客户端
res.send({
status:0,
message:'获取用户列表数据成功',
data:rows
})
}
getAllUser();
7. 创建 user_router 模块
//因为要使用路由对象,所以要导入express
import { express } from "express";
//因为要使用到user_ctrl中的函数,所以要导入
import { getAllUser } from '../controller/user_ctr.js'
//创建路由对象
const router=new express.Router();
//为路由挂载一个get
router.get('/user',getAllUser);
//将路由向外暴露
export default router
8. 导入并挂载路由模块
//搭建一个express服务器
import express from 'express'
//导入路由模块
import userRouter from './router/user_router.js'
const app=express();
//路由挂载
app.use('./api',userRouter);
app.listen(20,()=>{
console.log('server running at http://127.0.0.1');
})
9. 使用 try…catch 捕获异常
//查询数据库中的数据
import db from '../db/index.js'
// 使用ES6的按需导出语法,将getAllUser方法导出去
async function getAllUser(req,res){
// db.query()--输入查询语句,并且返回的是一个Promise对象
// 索引为0是我们要的数据
try{
const [rows]= await db.query('select id,username,nickname from ev_users')
// console.log(rows);
//将数据发送给客户端
res.send({
status:0,
message:'获取用户列表数据成功',
data:rows
})
// 使用catch要获取错误
}catch(err){
res.send({
status:1,
message:'获取用户数据失败',
desc:err.message
})
}
}
getAllUser();