学习视频:
- java开发所需的前端技术全教程
推荐资料:
- JavaScript 教程 | 菜鸟教程 (runoob.com)
文章目录
- HTML
- 元素
- 文本
- 1、标题 Heading
- 2、段落 Paragraph
- 3、列表 List
- 4、超链接 Anchor
- 多媒体
- 1、Image
- 2、Video
- 3、Audio
- 表单
- 常见的表单项
- 数据格式小结
- CSS
- 1、选择器
- 2、属性和值
- 3、布局
- JavaScript
- 变量与数据类型
- 声明变量
- let
- const
- var
- 基本类型
- undefined 和 null
- string
- number 和 bigint
- boolean
- 对象类型
- Function 函数
- 定义函数
- 默认参数
- 匿名函数
- 箭头函数
- 函数是对象
- 函数作用域
- 闭包
- Object
- 语法
- JSON
- 动态类型
- 运算符与表达式
- 1) ===
- 2) ||
- 3) ?? 与 ?.
- ??
- ?.
- 4) ...
- 5) [] {}
- []
- {}
- 控制语句
- for in
- for of
- API 前端案例
- 搭建前端服务器
- 一、查找元素
- Vue 3
- TypeScript
- 动态类型的问题
- 使用入门
- 类型
- 标注位置
- 标注变量
- 标注参数
- 标注返回值
- 复杂类型
- type
- interface
- 可选属性
- 鸭子类型
- 方法类型
- 字面量类型
- nullish 类型
- 泛型
- 类
- 基本语法
- 方法 & 只读属性
- get,set
- 类与接口
- 继承与接口
- 方法重写
- Vue3 基础
- 环境准备
- 创建项目
- 安装 devtools
- 修改端口
- 配置代理
- 项目结构
- Vue 组件
- main.ts
- ref 与 reactive
- 属性绑定
- 事件绑定
- 表单绑定
- 计算属性
- xhr
- axios
- 基本用法
- 环境变量
- baseURL
- 拦截器
- 条件与列表
- 监听器
- vueuse
- useRequest
- usePagination
- 子组件
- Vue3 进阶
- Ant-design-vue
- 表格
- 分页
- 搜索、删除、新增、修改
- 全局消息
- 表单校验
- vue-router
- 安装
- 创建 router
- 动态导入
- 嵌套路由
- 重定向
- pinia
- 安装
- 定义Store
HTML
使用
!
+Tab
键可生成 HTML 模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
声明为 HTML5 文档<html>
元素是 HTML 页面的根元素<head>
元素包含了文档的元(meta)数据,如<meta charset="utf-8">
定义网页编码格式为 utf-8。<title>
元素描述了文档的标题<body>
元素包含了可见的页面内容<h1>
元素定义一个大标题<p>
元素定义一个段落
元素
文本
1、标题 Heading
<h1>1号标题</h1>
<h2>2号标题</h2>
<h3>3号标题</h3>
<h4>4号标题</h4>
<h5>5号标题</h5>
<h6>6号标题</h6>
2、段落 Paragraph
<p>段落</p>
3、列表 List
无序列表 unordered list
<ul>
<li>列表项1</li>
<li>列表项2</li>
<li>列表项3</li>
</ul>
有序列表
<ol>
<li>列表项1</li>
<li>列表项2</li>
<li>列表项3</li>
</ol>
多级列表
<ul>
<li>
深圳市
<ul>
<li>南山区</li>
<li>福田区</li>
<li>龙岗区</li>
</ul>
</li>
<li>
广州市
<ul>
<li>番禺区</li>
</ul>
</li>
</ul>
4、超链接 Anchor
<a href="resource/page.html">本地网页</a>
多媒体
1、Image
<img src="文件路径">
2、Video
<video src="文件路径"></video>
3、Audio
<audio src="文件路径"></audio>
表单
表单的作用:收集用户填入的数据,并将这些数据提交给服务器
<form action="服务器地址" method="请求方式" enctype="数据格式">
<!-- 表单项 -->
<input type="text" name="uesrname">
<input type="submit" value="提交按钮">
</form>
- method 请求方式有:
- get (默认)提交时,数据跟在 URL 地址之后
- post 提交时,数据在请求体内
- enctype 在 post 请求时,指定请求体的数据格式
- application/x-www-form-urlencoded(默认)
- multipart/form-data
- 其中表单项提供多种收集数据的方式
- 有 name 属性的表单项数据,才会被发送给服务器
常见的表单项
文本框
<input type="text" name="uesrname">
密码框
<input type="password" name="password">
隐藏框
<input type="hidden" name="id">
日期框
<input type="date" name="birthday">
单选(name 要一致)
<input type="radio" name="sex" value="男" checked>
<input type="radio" name="sex" value="女">
多选(name 要一致,后端用 List 接收)
<input type="checkbox" name="fav" value="唱歌">
<input type="checkbox" name="fav" value="逛街">
<input type="checkbox" name="fav" value="游戏">
文件上传
<input type="file" name="avatar">
数据格式小结
客户端发送:
- 编码
- application/x-www-form-urlencoded :url 编码
- application/json:utf-8 编码
- multipart/form-data:每部分编码可以不同
- 表单只支持以 application/x-www-form-urlencoded 和 multipart/form-data 格式发送数据
- 文件上传需要用 multipart/form-data 格式
- js 代码可以支持任意格式发送数据
服务端接收:
- 对 application/x-www-form-urlencoded 和 multipart/form-data 格式的数据,Spring 接收方式是统一的,只需要用 java bean 的属性名对应请求参数名即可
- 对于 applicaiton/json 格式的数据,Spring 接收需要使用 @RequestBody 注解 + java bean 的方式
CSS
CSS 即 Cascading Style Sheets 层叠样式表,它描述了网页的表现与展示效果。
1、选择器
-
type 选择器:根据标签名进行匹配(元素选择器)
-
class 选择器:根据元素的 class 属性进行匹配
-
id 选择器:根据元素的 id 属性进行匹配
2、属性和值
- background-color : red;
- …
- display
3、布局
与布局相关的 html 元素
- div
- template
/* 元素type选择器 */
p {
background-color:aqua;
}
/* 元素class选择器 */
.c1 {
background-color:cornflowerblue;
}
/* id选择器 */
#p3 {
background-color:red;
}
JavaScript
它是一种脚本语言,可以用来更改页面内容,控制多媒体,制作图像、动画等等。
- js 代码位置
<script>
// js 代码
</script>
- 引入 js 脚本
<script src="js脚本路径"></script>
- 注意,到了框架之后,引入方式会有不同
变量与数据类型
声明变量
let
let 变量名 = 值;
let 声明的变量可以被多次赋值,例如
let a = 100; // 初始值是 100
a = 200; // ok, 被重新赋值为 200
const
const 修饰的叫常量,只能赋值一次
const b = 300; // 初始值是 300
b = 400; // error, 不能再次赋值
const 并不意味着它引用的内容不可修改,例如
const c = [1,2,3];
c[2] = 4; // ok, 数组内容被修改成 [1,2,4]
c = [5,6]; // error, 不能再次赋值
var
var 声明的变量可以被多次赋值,例如
var f = 100;
f = 200;
let
与var
的区别体现在作用域不同。
如果函数外层引用的是 let 变量,那么外层普通的 {} 也会作为作用域边界,最外层的 let 也占一个 script 作用域
let x = 10;
if(true) {
let y = 20;
function b() {
console.log(x,y);
}
console.dir(b);
}
如果函数外层引用的是 var 变量,外层普通的 {} 不会视为边界
var x = 10;
if(true) {
var y = 20;
function b() {
console.log(x,y);
}
console.dir(b);
}
如果 var 变量出现了重名,则他俩会被视为同一作用域中的同一个变量
var e = 10;
if(true) {
var e = 20; // 相当于 e = 20
console.log(e); // 打印 20
}
console.log(e); // 因为是同一个变量,还是打印 20
如果是 let,则视为两个作用域中的两个变量
let e = 10;
if(true) {
let e = 20;
console.log(e); // 打印 20
}
console.log(e); // 打印 10
要想里面的 e 和外面的 e 能区分开来,最简单的办法是改成 let,或者用函数来界定作用域范围
var e = 10;
if(true) {
function b() {
var e = 20;
console.log(e);
}
b();
}
console.log(e);
基本类型
undefined 和 null
- 执行表达式或函数,没有返回结果,出现 undefined
- 访问数组不存在的元素,访问对象不存在的属性,出现 undefined
- 定义变量,没有初始化,出现 undefined
console.log(1); // 函数没有返回值, 结果是 undefined
let a = 10; // 表达式没有返回值, 结果是 undefined
let b = [1,2,3];
console.log(b[10]); // 数组未定义元素是 undefined
let c = {"name":"张三"};
console.log(c.age); // 对象未定义属性是 undefined
let d;
console.log(d); // 变量未初始化是 undefined
二者共同点
- 都没有属性、方法
- 二者合称 Nullish
二者区别
- undefined 由 js 产生
- null 由程序员提供
string
js 字符串三种写法
let a = "hello"; // 双引号
let b = "world"; // 单引号
let c = `hello`; // 反引号
html 代码如下,用 java 和 js 中的字符串如何表示?
<a href="1.html">超链接</a>
java 显得比较繁琐
String s1 = "<a href=\"1.html\">超链接</a>";
String s2 = """
<a href="1.html">超链接</a>""";
js 就比较灵活
let s1 = '<a href="1.html">超链接</a>';
let s2 = `<a href="1.html">超链接</a>`;
模板字符串(Template strings)
需求:拼接 URI 的请求参数,如
/test?name=zhang&age=18
/test?name=li&age=20
传统方法拼接
let name = ; // zhang li ...
let age = ; // 18 20 ...
let uri = "/test?name=" + name + "&age=" + age;
模板字符串方式
let name = ; // zhang li ...
let age = ; // 18 20 ...
let uri = `/test?name=${name}&age=${age}`;
number 和 bigint
number 类型标识的是双精度浮动小数,例如
10 / 3; // 结果 3.3333333333333335
既然是浮点小数,那么可以除零
10 / 0; // 结果 Infinity 正无穷大
-10 / 0; // 结果 -Infinity 负无穷大
浮点小数都有运算精度问题,例如
2.0 - 1.1; // 结果 0.8999999999999999
字符串转数字
parseInt("10"); // 结果是数字 10
parseInt("10.5"); // 结果是数字 10, 去除了小数部分
parseInt("10") / 3; // 结果仍视为 number 浮点数, 因此结果为 3.3333333333333335
parseInt("abc"); // 转换失败,结果是特殊值 NaN (Not a Number)
要表示真正的整数,需要用 bigint,数字的结尾用 n 表示它是一个 bigint 类型
10n / 3n; // 结果 3n, 按整数除法处理
boolean
- Truthy
- Falsy
在 js 中,并不是 boolean 才能用于条件判断,你可以在 if 语句中使用【数字】、【字符串】… 作为判断条件
let b = 1;
if(b) { // true
console.log("进入了");
}
这时就有一个规则,当需要条件判断时,这个值被当作 true 还是 false,当作 true 的值归类为 truthy,当作 false 的值归类为 falsy
下面值都是 falsy
false
Nullish (null, undefined)
0, 0n, NaN
"" '' ``
即长度为零的字符串
剩余的值绝大部分都是 truthy
有几个容易被当作 falsy 实际是 truthy 的
"false", "0"
即字符串的 false 和 字符串的零[]
空数组{}
空对象
对象类型
Function 函数
定义函数
function 函数名(参数) {
// 函数体
return 结果;
}
js 中的函数调用特点:对参数的类型和个数都没有限制,例如
add('a', 'b'); // 返回 ab
add(4, 5, 6); // 返回 9, 第三个参数没有被用到, 不会报错
add(1); // 返回 NaN, 这时 b 没有定义是 undefined, undefined 做数学运算结果就是 NaN
默认参数
java 中(spring)要实现默认参数的效果得这么做:
@RestController
public class MyController {
@RequestMapping("/page")
@ResponseBody
public void page(
@RequestParam(defaultValue="1") int page,
@RequestParam(defaultValue="10") int size){
// ...
}
}
js
function pagination(page = 1, size = 10) {
console.log(page, size);
}
匿名函数
(function (参数) {
// 函数体
return 结果;
})
第一种场景:定义完毕后立刻调用
(function(a,b){
return a + b;
})(1,2)
第二种场景:作为其它对象的方法,例如
页面有元素
<p id="p1">点我啊</p>
此元素有一个 onclick 方法,会在鼠标单击这个元素后被执行,onclick 方法刚开始是 null,需要赋值后才能使用
document.getElementById("p1").onclick = (function(){
console.log("鼠标单击了...");
});
箭头函数
(参数) => {
// 函数体
return 结果;
}
- 如果没有参数,() 还是要保留
- 如果只有一个参数,() 可以省略
- 如果函数体内只有一行代码,{} 可以省略
- 如果这一行代码就是结果,return 可以省略
document.getElementById("p1").onclick = () => console.log("aa");
函数是对象
- 可以参与赋值,例,具名函数也能参与赋值
function abc() {
console.log("bb");
}
document.getElementById("p1").onclick = abc;
- 可以作为方法参数
function aa() {
console.log('aa')
}
function bb(fn) { // fn 将来可以是一个函数对象
console.log('bb')
fn(); // 调用函数对象
}
bb(aa);
- 可以作为方法返回值
function c() {
console.log("c");
function d() {
console.log("d");
}
return d;
}
c()()
函数作用域
函数可以嵌套(js 代码中很常见,只是嵌套的形式更多是匿名函数,箭头函数)
function a() {
function b() {
}
}
- 以函数为分界线划定作用域,所有函数之外是全局作用域
- 查找变量时,由内向外查找
- 在内层作用域找到变量,就会停止查找,不会再找外层
- 所有作用域都找不到变量,报错
- 作用域本质上是函数对象的属性,可以通过 console.dir 来查看调试
var x = 10;
function a() {
var y = 20;
function b() {
console.log(x, y);
}
b();
}
a();
闭包
var x = 10;
function a() {
var y = 20;
function b() {
console.log(x,y);
}
return b;
}
a()(); // 在外面执行了 b
- 函数定义时,它的作用域已经确定好了,因此无论函数将来去了哪,都能从它的作用域中找到当时那些变量
- 闭包就是指函数能够访问自己的作用域中变量
Object
语法
let obj = {
属性名: 值,
方法名: 函数,
get 属性名() {},
set 属性名(新值) {}
}
例1
let stu1 = {
name: "小明",
age: 18,
study: function() {
console.log(this.name + "爱学习");
}
}
例2
let name = "小黑";
let age = 20;
let study = function() {
console.log(this.name + "爱学习");
}
let stu2 = { name, age, study }
例3(重点)
let stu3 = {
name: "小白",
age: 18,
study() {
console.log(this.name + "爱学习");
}
}
- 注意:对象方法这么写,仅限于对象内部
例4
let stu4 = {
_name: null, /*类似于java中私有成员变量*/
get name() {
console.log("进入了get");
return this._name;
},
set name(name) {
console.log("进入了set");
this._name = name;
}
}
调用 get,set
stu4.name = "小白"; // set
console.log(stu4.name); // get
JSON
一个 json 对象可以长这样:
{
"name":"张三",
"age":18
}
一个 js 对象长这样:
{
name:"张三",
age:18
}
- 本质不同
- json 对象本质上是个字符串,它的职责是作为客户端和服务器之间传递数据的一种格式
- js 对象是切切实实的对象,可以有属性方法
- 语法细节不同
- json 中只能有 null、true|false、数字、字符串(只有双引号)、对象、数组
- json 中不能有除以上的其它 js 对象的特性,如方法等
- json 中的属性必须用双引号引起来
json 字符串与 js 对象的转换
JSON.parse(json字符串); // 返回js对象
JSON.stringify(js对象); // 返回json字符串
动态类型
静态类型语言,如 Java,值有类型,变量也有类型、赋值给变量时,类型要相符
int a = 10;
String b = "abc";
int c = "abc"; // 错误
而 js 属于动态类型语言,值有类型,但变量没有类型,赋值给变量时,没要求
let a = 200;
let b = 100;
b = 'abc';
b = true;
动态类型看起来比较灵活,但变量没有类型,会给后期维护带来困难,例如
function test(obj) {
// obj 的类型未知,必须根据不同类型做出相应的容错处理
}
于是后续出现了 typescript。
运算符与表达式
+ - * / % **
+= -= *= /= %= **=
++ --
- 位运算、移位运算
== != > >= < <=
=== !==
&& || !
?? ?.
...
- 解构赋值
1) ===
严格相等运算符,用作逻辑判等
1 == 1 // 返回 true
1 == '1' // 返回 true,会先将右侧的字符串转为数字,再做比较
1 === '1' // 返回 false,类型不等,直接返回 false
typeof 查看某个值的类型
typeof 1 // 返回 'number'
typeof '1' // 返回 'string'
2) ||
需求,如果参数 n 没有传递,给它一个【男】
推荐做法
function test(n = '男') {
console.log(n);
}
你可能的做法
function test(n) {
if(n === undefined) {
n = '男';
}
console.log(n);
}
还可能是这样
function test(n) {
n = (n === undefined) ? '男' : n;
console.log(n);
}
一些老旧代码中可能的做法(不推荐)
function test(n) {
n = n || '男';
console.log(n);
}
它的语法是
值1 || 值2
如果值1 是 Truthy,返回值1,如果值1 是 Falsy 返回值 2
3) ?? 与 ?.
??
需求,如果参数 n 没有传递或是 null,给它一个【男】
如果用传统办法
function test(n) {
if(n === undefined || n === null) {
n = '男';
}
console.log(n);
}
用 ??
function test(n) {
n = n ?? '男';
console.log(n);
}
语法
值1 ?? 值2
- 值1 是 nullish,返回值2
- 值1 不是 nullish,返回值1
?.
需求,函数参数是一个对象,可能包含有子属性
例如,参数可能是
let stu1 = {
name:"张三",
address: {
city: '北京'
}
};
let stu2 = {
name:"李四"
}
let stu3 = {
name:"李四",
address: null
}
现在要访问子属性(有问题)
function test(stu) {
console.log(stu.address.city)
}
现在希望当某个属性是 nullish 时,短路并返回 undefined,可以用 ?.
function test(stu) {
console.log(stu.address?.city)
}
用传统办法
function test(stu) {
if(stu.address === undefined || stu.address === null) {
console.log(undefined);
return;
}
console.log(stu.address.city)
}
4) …
展开运算符
作用1:打散数组,把元素传递给多个参数
let arr = [1,2,3];
function test(a,b,c) {
console.log(a,b,c);
}
需求,把数组元素依次传递给函数参数
传统写法
test(arr[0],arr[1],arr[2]); // 输出 1,2,3
展开运算符写法
test(...arr); // 输出 1,2,3
- 打散可以理解为【去掉了】数组外侧的中括号,只剩下数组元素
作用2:复制数组或对象
数组
let arr1 = [1,2,3];
let arr2 = [...arr1]; // 复制数组
对象
let obj1 = {name:'张三', age: 18};
let obj2 = {...obj1}; // 复制对象
注意:展开运算符复制属于浅拷贝,例如
let o1 = {name:'张三', address: {city: '北京'} }
let o2 = {...o1};
作用3:合并数组或对象
合并数组
let a1 = [1,2];
let a2 = [3,4];
let b1 = [...a1,...a2]; // 结果 [1,2,3,4]
let b2 = [...a2,5,...a1] // 结果 [3,4,5,1,2]
合并对象
let o1 = {name:'张三'};
let o2 = {age:18};
let o3 = {name:'李四'};
let n1 = {...o1, ...o2}; // 结果 {name:'张三',age:18}
let n2 = {...o3, ...o2, ...o1}; // 结果{name:'李四',age:18}
- 复制对象时出现同名属性,后面的会覆盖前面的
5) [] {}
解构赋值
[]
用在声明变量时
let arr = [1,2,3];
let [a, b, c] = arr; // 结果 a=1, b=2, c=3
用在声明参数时
let arr = [1,2,3];
function test([a,b,c]) {
console.log(a,b,c) // 结果 a=1, b=2, c=3
}
test(arr);
{}
用在声明变量时
let obj = {name:"张三", age:18};
let {name,age} = obj; // 结果 name=张三, age=18
用在声明参数时
let obj = {name:"张三", age:18};
function test({name, age}) {
console.log(name, age); // 结果 name=张三, age=18
}
test(obj)
控制语句
for in
主要用来遍历对象
let father = {name:'张三', age:18, study:function(){}};
for(const n in father) {
console.log(n);
}
- 其中 const n 代表遍历出来的属性名
- 注意1:方法名也能被遍历出来(它其实也算一种特殊属性)
- 注意2:遍历子对象时,父对象的属性会跟着遍历出来
let son = Object.create(father);
son.sex = "男";
for(const n in son) {
console.log(n);
}
- 注意3:在 for in 内获取属性值,要使用 [] 语法,而不能用 . 语法
for(const n in son) {
console.log(n, son[n]);
}
for of
主要用来遍历数组,也可以是其它可迭代对象,如 Map,Set 等
let a1 = [1,2,3];
for(const i of a1) {
console.log(i);
}
let a2 = [
{name:'张三', age:18},
{name:'李四', age:20},
{name:'王五', age:22}
];
for(const obj of a2) {
console.log(obj.name, obj.age);
}
for(const {name,age} of a2) {
console.log(name, age);
}
API 前端案例
搭建前端服务器
新建一个保存项目的 client 文件夹,进入文件夹执行
npm install express --save-dev
修改 package.json 文件
{
"type": "module",
"devDependencies": {
"express": "^4.18.1"
}
}
- 其中 devDependencies 是 npm install --save-dev 添加的
编写 main.js 代码
import express from 'express'
const app = express()
app.use(express.static('./')) //设置静态资源路径
app.listen(7070)
执行 js 代码(运行前端服务器)
node main.js
一、查找元素
document.getElementById
:根据 id 值查找一个元素;[document|元素].querySelector
:根据选择器查找第一个匹配元素;[document|元素].querySelectorAll
:根据选择器查找所有匹配元素;
<div>
<div class="title">学生列表</div>
<div class="thead">
<div class="row bold">
<div class="col">编号</div>
<div class="col">姓名</div>
<div class="col">性别</div>
<div class="col">年龄</div>
</div>
</div>
<div class="tbody">
<div class="row">
<div class="col">1</div>
<div class="col">张三</div>
<div class="col">男</div>
<div class="col">18</div>
</div>
</div>
</div>
执行
document.querySelector('.title'); // 找到 <div class="title">学生列表</div>
执行
document.querySelector('.col'); // 找到 <div class="col">编号</div>
执行
document.querySelectorAll('.col');
/*
找到的是一个集合
<div class="col">编号</div>
<div class="col">姓名</div>
<div class="col">性别</div>
<div class="col">年龄</div>
<div class="col">1</div>
<div class="col">张三</div>
<div class="col">男</div>
<div class="col">18</div>
*/
执行
const thead = document.querySelector('.thead');
// 只在 thead 元素范围内找
thead.querySelectorAll('.col');
/*
找到的是一个集合
<div class="col">编号</div>
<div class="col">姓名</div>
<div class="col">性别</div>
<div class="col">年龄</div>
*/
根据 id 属性查找既可以用
document.getElementById("id值")
也可以用
document.querySelector("#id值")
Vue 3
TypeScript
动态类型的问题
前面我们讲过 js 属于动态类型语言,例如
function test(obj) {
}
obj 可能只是个字符串
test('hello, world')
obj 也有可能是个函数
test(()=>console.log('hello, world'))
obj 类型不确定,就给后期使用者带来了麻烦,一旦参数传不对,代码就崩溃了
- 动态类型意味着:运行代码时才知道发生什么
- 静态类型意味着:在代码运行前,就对它的行为做出预测
下面的 typescript 代码,就在代码运行前对参数加入了约束限制
//限制了参数只能做string那些事
function test(msg : string) {
console.log(msg.toUpperCase());
}
//限制了参数只能做函数那些事
function test(msg : Function) {
msg()
}
使用入门
安装 typescript 编译器
npm install -g typescript
编写 ts 代码
function hello(msg: string) {
console.log(msg)
}
hello('hello,world')
执行 tsc 编译命令之前,先创建一个 tsconfig.json 文件,避免 “函数实现重复” 报错。
tsc --init
执行 tsc 编译命令
tsc xxx.ts
编译生成 js 代码,编译后进行了类型擦除
function hello(msg) {
console.log(msg);
}
hello('hello,world');
再来一个例子,用 interface 定义用户类型
interface User {
name: string,
age: number
}
function test(u: User): void {
console.log(u.name)
console.log(u.age)
}
test({ name: 'zhangs', age: 18 })
编译后
function test(u) {
console.log(u.name);
console.log(u.age);
}
test({ name: 'zhangs', age: 18 });
可见,typescript 属于编译时实施类型检查(静态类型)的技术
类型
类型 | 例 | 备注 |
---|---|---|
字符串类型 | string | |
数字类型 | number | |
布尔类型 | boolean | |
数组类型 | number[],string[], boolean[] 依此类推 | |
任意类型 | any | 相当于又回到了没有类型的时代 |
复杂类型 | type 与 interface | |
函数类型 | () => void | 对函数的参数和返回值进行说明 |
字面量类型 | “a”|“b”|“c” | 限制变量或参数的取值 |
nullish类型 | null 与 undefined | |
泛型 | <T> ,<T extends 父类型> |
标注位置
标注变量
let message: string = 'hello,world'
一般可以省略,因为可以根据后面的字面量推断出前面变量类型
let message = 'hello,world'
标注参数
function greet(name: string) {
}
标注返回值
一般也可以省略,因为可以根据返回值做类型推断
function add(a: number, b: number) : number {
return a + b
}
复杂类型
type
type Cat = {
name: string,
age: number
}
const c1: Cat = { name: '小白', age: 1 }
const c2: Cat = { name: '小花' } // 错误: 缺少 age 属性
const c3: Cat = { name: '小黑', age: 1, sex: '公' } // 错误: 多出 sex 属性
interface
interface Cat {
name: string,
age: number
}
const c1: Cat = { name: '小白', age: 1 }
const c2: Cat = { name: '小花' } // 错误: 缺少 age 属性
const c3: Cat = { name: '小黑', age: 1, sex: '公' } // 错误: 多出 sex 属性
可选属性
如果需要某个属性可选,可以用下面的语法
interface Cat {
name: string,
age?: number
}
const c1: Cat = { name: '小白', age: 1 }
const c2: Cat = { name: '小花' } // 正确: age 属性可选
可选属性要注意处理 undefined 值
鸭子类型
interface Cat {
name: string
}
function test(cat: Cat) {
console.log(cat.name)
}
const c1 = { name: '小白', age: 1 }
test(c1)
const c1 并没有声明类型为 Cat,但它与 Cat 类型有一样的属性,也可以被当作是 Cat 类型
方法类型
interface Api {
foo(): void,
bar(str: string): string
}
function test(api: Api) {
api.foo()
console.log(api.bar('hello'))
}
test({
foo() { console.log('ok') },
bar(str: string) { return str.toUpperCase() }
})
字面量类型
function printText(s: string, alignment: "left" | "right" | "center") {
console.log(s, alignment)
}
printText('hello', 'left')
printText('hello', 'aaa') // 错误: 取值只能是 left | right | center
nullish 类型
function test(x?: string | null) {
console.log(x?.toUpperCase())
}
test('aaa')
test(null)
test()
x?: string | null 表示可能是 undefined 或者是 string 或者是 null
泛型
interface Ref<T> {
value: T
}
const r1: Ref<string> = { value: 'hello' }
const r2: Ref<number> = { value: 123 }
const r3: Ref<boolean> = { value: true }
- 泛型的要点就是
<类型参数>
,把【类型】也当作一个变化的要素,像参数一样传递过来,这样就可以派生出结构相似的新类型
函数定义也支持泛型
function ref<T>(n: T): Ref<T> {
return { value: n }
}
const v1 = ref("hello"); // Ref<string>
const v2 = ref(123.3333); // Ref<number>
v1.value.toLocaleLowerCase()
v2.value.toFixed(2)
类
关于 TypeScript 与 JavaScript 中的类语法不是重点,class 相关语法只是起到辅助作用,更重要的是前面讲的 interface
基本语法
class User {
name: string;
constructor(name: string) {
this.name = name
}
}
const u = new User('张三')
其实会被编译成这个样子(默认 --target=es3)
var User = /** @class */ (function () {
function User(name) {
this.name = name;
}
return User;
}());
var u = new User('张三');
所以 js 中的 class,并不等价于 java 中的 class,它还是基于原型实现的,原理参考第二章(036、037)
方法 & 只读属性
readonly 是 typescript 特有的,表示该属性只读
class User {
readonly name: string;
constructor(name: string) {
this.name = name
}
study() {
console.log(`[${this.name}]正在学习`)
}
}
const u = new User('张三')
u.study()
get,set
class User {
_name: string;
constructor(name: string) {
this._name = name
}
get name() {
return this._name
}
set name(name: string) {
this._name = name
}
}
const u = new User('张三')
console.log(u.name)
u.name = '李四'
console.log(u.name)
- 注意,需要在编译时加上
tsc --target es6 .\xxx.ts
选项 - es6 等价于 es2015,再此之上还有 es2016 … es2022
类与接口
interface User {
name: string
study(course: string): void
}
class UserImpl implements User {
name: string;
constructor(name: string) {
this.name = name
}
study(course: string) {
console.log(`[${this.name}]正在学习[${course}]`)
}
foo() { }
}
const user: User = new UserImpl('张三')
user.study('Typescript')
user.foo() // 错误,必须是接口中定义的方法
继承与接口
interface Flyable {
fly(): void
}
class Animal {
name: string;
constructor(name: string) {
this.name = name
}
}
class Bird extends Animal implements Flyable {
fly() {
console.log(`${this.name}在飞翔`)
}
}
const b: Flyable & Animal = new Bird("小花")
b.fly()
- Flyable & Animal 表示变量是 flyable 类型,同时也是 Animal 类型
方法重写
class Father {
study(): void {
console.log(`father study`)
}
}
class Son extends Father {
study(): void {
super.study()
console.log(`son study`)
}
}
const f: Father = new Son()
f.study()
Vue3 基础
技术选型
- Vue
- 选项式 API 还是 组合式 API✔️
- HTML 还是 单文件组件✔️
- 语法
- javascript 还是 typescript✔️
- 构建工具
- @vue/cli 还是 vite✔️
- 路由
- vue-router✔️
- 共享存储
- vuex 还是 pinia✔️
- 视图组件
- ElementUI 还是 Antdv✔️
环境准备
创建项目
采用 vite 作为前端项目的打包,构建工具
npm init vite@latest
按提示操作
cd 项目目录
npm install
npm run dev
访问 localhost:5173
安装 devtools
devtools 插件网址:https://devtools.vuejs.org/guide/installation.html
修改端口
打开项目根目录下 vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 7070
}
})
文档地址:配置 Vite {#configuring-vite} | Vite中文网 (vitejs.cn)
配置代理
为了避免前后端服务器联调时, fetch、xhr 请求产生跨域问题,需要配置代理,同样是修改项目根目录下 vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 7070,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
项目结构
index.html
package.json
tsconfig.json
vite.config.ts
├─public
└─src
├─assets
├─components
├─model
├─router
├─store
└─views
- index.html 为主页面
- package.json npm 配置文件
- tsconfig.json typescript 配置文件
- vite.config.ts vite 配置文件
- public 静态资源
- src/components 可重用组件
- src/model 模型定义
- src/router 路由
- src/store 共享存储
- src/views 视图组件
Vue 组件
Vue 的组件文件以 .vue 结尾,每个组件由三部分组成
<script setup lang="ts"></script>
<template></template>
<style scoped></style>
- script 代码部分,控制模板的数据来源和行为
- template 模板部分,由它生成 html 代码
- style 样式部分,一般不咋关心
根组件是 src/App.vue,先来个 Hello,world 例子
<script setup lang="ts">
import { ref } from "vue";
let msg = ref("hello"); // 把数据变成响应式的
function change() {
msg.value = "world";
console.log(msg);
}
</script>
<template>
<h1>{{ msg }}</h1>
<input type="button" value="修改msg" @click="change" />
</template>
- {{msg}} 用来把一个变量绑定到页面上某个位置
- 绑定的变量必须用 ref 函数来封装
- ref 返回的是【响应式】数据,即数据一旦变化,页面展示也跟着变化
main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
- createApp 是创建一个 Vue 应用程序,它接收的参数 App 即之前我们看到的根组件
- mount 就是把根组件生成的 html 代码片段【挂载】到 index.html 中 id 为 app 的 html 元素上
ref 与 reactive
vue 提供了两个函数,都可以将数据变为【响应式】的
<script setup lang="ts">
import { ref, reactive } from 'vue'
const msg = ref('Hello, World')
const user = reactive({ name: '张三' })
</script>
<template>
<h2>{{msg}}</h2>
<h2>{{user.name}}</h2>
</template>
- ref 能将任意类型的数据变为【响应式】的
- reactive 只能将对象类型变为【响应式】,对基本类型无效(例如 string,number,boolean)
还有一点不同:
<script setup lang="ts">
import { ref, reactive } from 'vue'
const u1 = ref({ name: '张三' })
const u2 = reactive({ name: '张三' })
function test() {
console.log(u1.value)
console.log(u2)
}
test()
</script>
<template>
<h2>{{u1.name}}</h2>
<h2>{{u2.name}}</h2>
</template>
- 在 template 模板中使用 ref 包装的数据,直接写【变量名】就可以了
- 但在代码中要使用 ref 包装的数据,必须用【变量名.value】才能访问到
属性绑定
<script setup lang="ts">
import { ref } from 'vue'
const path = ref('/src/assets/vue.svg')
</script>
<template>
<img :src="path" alt=""> <!-- 加个冒号: -->
</template>
- 【
:属性名
】用来将标签属性与【响应式】变量绑定
事件绑定
<!-- 事件绑定 -->
<script setup lang="ts">
import {ref} from 'vue'
const count = ref(0)
function inc() {
count.value++;
}
function dec() {
count.value--;
}
</script>
<template>
<input type="button" value="+" @click="inc">
<h2>{{ count }}</h2>
<input type="button" value="-" @click="dec">
</template>
表单绑定
<script setup lang="ts">
import { ref } from "vue";
const user = ref({
name:'张三',
age:18,
sex:'男',
fav:['游泳','打球']
})
function saveUser() {
console.log(user.value)
}
</script>
<template>
<div class="outer">
<div>
<label for="">请输入姓名</label>
<input type="text" v-model="user.name"/>
</div>
<div>
<label for="">请输入年龄</label>
<input type="text" v-model="user.age"/>
</div>
<div>
<label for="">请选择性别</label>
男 <input type="radio" value="男" v-model="user.sex"/>
女 <input type="radio" value="女" v-model="user.sex"/>
</div>
<div>
<label for="">请选择爱好</label>
游泳 <input type="checkbox" value="游泳" v-model="user.fav"/>
打球 <input type="checkbox" value="打球" v-model="user.fav"/>
健身 <input type="checkbox" value="健身" v-model="user.fav"/>
</div>
<div>
<input type="button" value="保存" @click="saveUser">
</div>
</div>
</template>
<style scoped>
div {
margin-bottom: 8px;
}
.outer {
width: 100%;
position: relative;
padding-left: 80px;
}
label {
text-align: left;
width: 100px;
display: inline-block;
position: absolute;
left :0;
}
</style>
- 用
v-model
实现双向绑定:- javascript 数据可以同步到表单标签
- 反过来用户在表单标签输入的新值也会同步到 javascript 这边
- 双向绑定只适用于表单这种带【输入】功能的标签,其它标签的数据绑定,单向就足够了
- 复选框这种标签,双向绑定的 javascript 数据类型一般用数组
计算属性
有时在数据展示时要做简单的计算
<script setup lang="ts">
import { ref } from 'vue'
const firstName = ref('三')
const lastName = ref('张')
</script>
<template>
<h2>{{lastName + firstName}}</h2>
<h3>{{lastName + firstName}}</h3>
<h4>{{lastName + firstName}}</h4>
</template>
看起来较为繁琐,可以用计算属性改进
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('三')
const lastName = ref('张')
const fullName = computed(() => {
console.log('enter')
return lastName.value + firstName.value
})
</script>
<template>
<h2>{{fullName}}</h2>
<h3>{{fullName}}</h3>
<h4>{{fullName}}</h4>
</template>
- fullName 即为计算属性,它具备缓存功能,即 firstName 和 lastName 的值发生了变化,才会重新计算
- 如果用函数实现相同功能,则没有缓存功能
<script setup lang="ts">
import { ref } from 'vue'
const firstName = ref('三')
const lastName = ref('张')
function fullName() {
console.log('enter')
return lastName.value + firstName.value
}
</script>
<template>
<h2>{{fullName()}}</h2>
<h3>{{fullName()}}</h3>
<h4>{{fullName()}}</h4>
</template>
xhr
浏览器中有两套 API 可以和后端交互,发送请求、接收响应,fetch api 前面我们已经介绍过了,另一套 api 是 xhr,基本用法如下
const xhr = new XMLHttpRequest();
//当响应返回时,会触发 onload 事件
xhr.onload = function() {
//2.接收响应
console.log(xhr.response)
}
//1.发请求
xhr.open('GET', "http://localhost:8080/api/students")
xhr.responseType = 'json'
xhr.send()
但这套 api 虽然功能强大,但比较老,不直接支持 Promise,因此有必要对其进行改造
function get(url: string) {
return new Promise((resolve, reject)=>{
const xhr = new XMLHttpRequest()
xhr.onload = function() {
if(xhr.status === 200){
resolve(xhr.response)
} else if(xhr.status === 404) {
reject(xhr.response)
} // 其它情况也需考虑,这里简化处理
}
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.send()
})
}
- Promise 对象适合用来封装异步操作,并可以配合 await 一齐使用
- Promise 在构造时,需要一个箭头函数,箭头函数有两个参数 resolve 和 reject
- resolve 是异步操作成功时被调用,把成功的结果传递给它,最后会作为 await 的结果返回
- reject 在异步操作失败时被调用,把失败的结果传递给它,最后在 catch 块被捉住
- await 会一直等到 Promise 内调用了 resolve 或 reject 才会继续向下运行
axios
基本用法
axios 就是对 xhr api 的封装,手法与前面例子类似
安装依赖
npm install axios
一个简单的例子
<script setup lang="ts">
import axios from 'axios'
import {ref, onMounted} from 'vue'
const count = ref(0)
async function getData() {
const resp = await axios.get('http://localhost:8080/source/getIndexInfo/2')
count.value = resp.data.data.moduleList.length
}
onMounted(() => { //页面组件加载完成后执行
getData()
})
</script>
<template>
<h2>数量为 {{ count }}</h2>
</template>
onMounted 指 vue 组件生成的 html 代码片段,挂载完毕后被执行
再来看一个 post 例子
<script setup lang="ts">
import { ref } from "vue";
import axios from "axios";
const student = ref({
name: '',
sex: '男',
age: 18
})
async function addStudent() {
console.log(student.value)
//第二个参数是请求体数据,数据格式默认是json,后端使用@RequestBody接收
const resp = await axios.post('/api/students', student.value)
console.log(resp.data.data)
}
</script>
<template>
<div>
<div>
<input type="text" placeholder="请输入姓名" v-model="student.name"/>
</div>
<div>
<label for="">请选择性别</label>
男 <input type="radio" value="男" v-model="student.sex"/>
女 <input type="radio" value="女" v-model="student.sex"/>
</div>
<div>
<input type="number" placeholder="请输入年龄" v-model="student.age"/>
</div>
<div>
<input type="button" value="添加" @click="addStudent"/>
</div>
</div>
</template>
<style scoped>
div {
font-size: 14px;
}
</style>
环境变量
- 开发环境下,联调的后端服务器地址是
http://localhost:8080
, - 上线改为生产环境后,后端服务器地址为
http://itheima.com
这就要求我们区分开发环境和生产环境,这件事交给构建工具 vite 来做
默认情况下,vite 支持上面两种环境,分别对应根目录下两个配置文件
- .env.development - 开发环境
- .env.production - 生产环境
针对以上需求,分别在两个文件中加入
VITE_BACKEND_API_BASE_URL = 'http://localhost:8080' //.env.development
VITE_BACKEND_API_BASE_URL = 'http://itheima.com' //.env.production
然后在代码中使用 vite 给我们提供的特殊对象 import.meta.env
,就可以获取到 VITE_BACKEND_API_BASE_URL
在不同环境下的值
import.meta.env.VITE_BACKEND_API_BASE_URL
默认情况下,不能智能提示自定义的环境变量,做如下配置:新增文件 src/env.d.ts
并添加如下内容
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_BACKEND_API_BASE_URL: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
- 参考文档地址 环境变量和模式 | Vite 官方中文文档 (vitejs.dev)
baseURL
可以自己创建一个 axios 对象,方便添加默认设置,新建文件 /src/api/request.ts
// 创建新的 axios 对象
import axios from 'axios'
const _axios = axios.create({
baseURL: import.meta.env.VITE_BACKEND_API_BASE_URL
})
export default _axios
然后在其它组件中引用这个 ts 文件,例如 /src/views/E8.vue,就不用自己拼接路径前缀了
<script setup lang="ts">
import axios from '../api/request' //导入自己写的axios
// ...
await axios.post('/api/students', ...) //无需写地址前缀
</script>
拦截器
// 创建新的 axios 对象
import axios from 'axios'
const _axios = axios.create({
baseURL: import.meta.env.VITE_BACKEND_API_BASE_URL
})
// 请求拦截器
_axios.interceptors.request.use(
(config)=>{ // 请求成功时的处理,统一添加请求头(如JWT)
config.headers = {
Authorization: 'aaa.bbb.ccc'
}
return config
},
(error)=>{ // 请求出错时的处理
return Promise.reject(error)
}
)
// 响应拦截器
_axios.interceptors.response.use(
(response)=>{ // 状态码 2xx
// 这里的code是自定义的错误码
if(response.data.code === 200) {
return response
}
else if(response.data.code === 401) {
// 情况1
return Promise.resolve({})
}
// ...
},
(error)=>{ // 状态码 4xx,5xx
console.error(error) //统一异常处理
if(error.response.status === 400) {
// 情况1
} else if(error.response.status === 401) {
// 情况2
}
// ...
// return Promise.reject(error)
return Promise.resolve({})
}
)
export default _axios
处理响应时,又分成两种情况
- 后端返回的是标准响应状态码,这时会走响应拦截器第二个箭头函数,用 error.response.status 做分支判断
- 后端返回的响应状态码总是200,用自定义错误码表示出错,这时会走响应拦截器第一个箭头函数,用 response.data.code 做分支判断
另外
- Promise.reject(error) 类似于将异常继续向上抛出,异常由调用者(Vue组件)来配合 try … catch 来处理
- Promise.resolve({}) 表示错误已解决,返回一个空对象,调用者中接到这个空对象时,需要配合 ?. 来避免访问不存在的属性
条件与列表
首先,新增模型数据 '/src/model/Model8080.ts'
export interface Student {
id: number;
name: string;
sex: string;
age: number;
}
// 如果 spring 错误,返回的对象格式
export interface SpringError {
timestamp: string,
status: number,
error: string,
message: string,
path: string
}
// 如果 spring 成功,返回 list 情况
export interface SpringList<T> {
data: T[],
message?: string,
code: number
}
// 如果 spring 成功,返回 page 情况
export interface SpringPage<T> {
data: { list: T[], total: number },
message?: string,
code: number
}
// 如果 spring 成功,返回 string 情况
export interface SpringString {
data: string,
message?: string,
code: number
}
import { AxiosResponse } from 'axios'
export interface AxiosRespError extends AxiosResponse<SpringError> { }
export interface AxiosRespList<T> extends AxiosResponse<SpringList<T>> { }
export interface AxiosRespPage<T> extends AxiosResponse<SpringPage<T>> { }
export interface AxiosRespString extends AxiosResponse<SpringString> { }
- AxiosRespPage 代表分页时的响应类型
- AxiosRespList 代表返回集合时的响应类型
- AxiosRespString 代表返回字符串时的响应类型
- AxiosRespError 代表 Spring 出错时时的响应类型
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import axios from "../api/request";
import { Student, SpringList } from "../model/Model8080";
// 说明 students 数组类型为 Student[]
const students = ref<Student[]>([]);
async function getStudents() {
// 说明 resp.data 类型是 SpringList<Student>
const resp = await axios.get<SpringList<Student>>("/api/students");
console.log(resp.data.data);
students.value = resp.data.data;
}
onMounted(() => getStudents());
</script>
<template>
<div class="outer">
<div class="title">学生列表</div>
<div class="thead">
<div class="row bold">
<div class="col">编号</div>
<div class="col">姓名</div>
<div class="col">性别</div>
<div class="col">年龄</div>
</div>
</div>
<div class="tbody">
<div v-if="students.length === 0">暂无数据</div>
<template v-else>
<div class="row" v-for="s of students" :key="s.id">
<div class="col">{{ s.id }}</div>
<div class="col">{{ s.name }}</div>
<div class="col">{{ s.sex }}</div>
<div class="col">{{ s.age }}</div>
</div>
</template>
</div>
</div>
</template>
<style scoped>
......
</style>
- 加入泛型是为了更好的提示
- v-if 与 v-else 不能和 v-for 处于同一标签
- template 标签还有一个用途,就是用它少生成一层真正 html 代码
- 可以看到将结果封装为响应式数据还是比较繁琐的,后面会使用 useRequest 改进
监听器
利用监听器,可以在【响应式】的基础上添加一些副作用,把更多的东西变成【响应式的】
-
原本只是数据变化 => 页面更新
-
watch 可以在数据变化时 => 其它更新
<template>
<input type="text" v-model="name" />
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
function useStorage(name: string) {
const data = ref(sessionStorage.getItem(name) ?? "");
// 参数1:要监听的数据
// 参数2:数据变化时要执行的箭头函数
watch(data, (newValue) => {
sessionStorage.setItem(name, newValue);
});
return data;
}
const name = useStorage("name");
</script>
- 名称为 useXXXX 的函数,作用是返回带扩展功能的【响应式】数据
- localStorage 即使浏览器关闭,数据还在
- sessionStorage 数据工作在浏览器活动期间
vueuse
安装
npm install @vueuse/core
一些函数的用法
<template>
<h3>X: {{x}}</h3>
<h3>Y: {{y}}</h3>
<h3>{{count}}</h3>
<input type="button" @click="inc()" value="+">
<input type="button" @click="dec()" value="-">
<input type="text" v-model="name">
</template>
<script setup lang="ts">
import { useMouse, useCounter, useStorage } from '@vueuse/core'
const {x, y} = useMouse() //获取鼠标坐标
const {count, inc, dec} = useCounter() //使用计数器
const name = useStorage("name", "") //使用存储
</script>
useRequest
响应式的 axios 封装,官网地址 一个 Vue 请求库 | VueRequest (attojs.org)
首先安装 vue-request
npm install vue-request@next
组件使用
<template>
<h3 v-if="students.length === 0">暂无数据</h3>
<ul v-else>
<li v-for="s of students" :key="s.id">
<span>{{s.name}}</span>
<span>{{s.sex}}</span>
<span>{{s.age}}</span>
</li>
</ul>
</template>
<script setup lang="ts">
import axios from "../api/request"
import { useRequest } from 'vue-request'
import { computed } from 'vue'
import { AxiosRespList, Student } from '../model/Model8080'
// data 代表就是 axios 的响应对象
const { data } = useRequest<AxiosRespList<Student>>(() => axios.get('/api/students'))
const students = computed(()=>{
return data?.value?.data.data || []
})
</script>
<style scoped>
......
</style>
- data.value 的取值一开始是 undefined,随着响应返回变成 axios 的响应对象
- 用 computed 进行适配
usePagination
在 src/model/Model8080.ts 中补充类型说明
export interface StudentQueryDto {
name?: string,
sex?: string,
age?: string, // 18,20
page: number,
size: number
}
- js 中类似于 18,20 这样以逗号分隔字符串,会在 get 传参时转换为 java 中的整数数组
编写组件
<template>
<input type="text" placeholder="请输入姓名" v-model="dto.name">
<select v-model="dto.sex">
<option value="" selected>请选择性别</option>
<option value="男">男</option>
<option value="女">女</option>
</select>
<input type="text" placeholder="请输入年龄范围" v-model="dto.age">
<br>
<input type="text" placeholder="请输入页码" v-model="dto.page">
<input type="text" placeholder="请输入页大小" v-model="dto.size">
<input type="button" value="搜索" @click="search">
<hr>
<h3 v-if="students.length === 0">暂无数据</h3>
<ul v-else>
<li v-for="s of students" :key="s.id">
<span>{{s.name}}</span>
<span>{{s.sex}}</span>
<span>{{s.age}}</span>
</li>
</ul>
<hr>
总记录数{{total}} 总页数{{totalPage}}
</template>
<script setup lang="ts">
import axios from "../api/request"
import { usePagination } from 'vue-request'
import { computed, ref } from 'vue'
import { AxiosRespPage, Student, StudentQueryDto } from '../model/Model8080'
const dto = ref<StudentQueryDto>({name:'', sex:'', age:'', page:1, size:5})
// data 代表就是 axios 的响应对象
// 泛型参数1: 响应类型
// 泛型参数2: 请求类型
const { data, total, totalPage, run } = usePagination<AxiosRespPage<Student>, StudentQueryDto[]>(
(d) => axios.get('/api/students/q', {params: d}), // 箭头函数
{
defaultParams: [ dto.value ], // 默认参数, 会作为参数传递给上面的箭头函数
pagination: {
currentKey: 'page', // 指明当前页属性
pageSizeKey: 'size', // 指明页大小属性
totalKey: 'data.data.total' // 指明总记录数属性
}
} // 选项
)
const students = computed(()=>{
return data?.value?.data.data.list || []
})
function search() {
run(dto.value) // 会作为参数传递给usePagination的箭头函数
}
</script>
<style scoped>
......
</style>
- usePagination 只需要定义一次,后续还想用它内部的 axios 发请求,只需调用 run 函数
子组件
子组件可以用于抽取公共组件。
定义子组件 Child1
<template>
<div class="container">
<div class="card">
<div>
<p class="name">{{name}}</p>
<p class="location">{{country}}</p>
</div>
<img :src="avatar || '/src/assets/vue.svg'"/>
</div>
</div>
</template>
<script setup lang="ts">
// 定义属性, 编译宏
defineProps<{name:string,country:string,avatar?:string}>()
</script>
<style scoped>
......
</style>
父组件引用
<template>
<Child1 name="张三" country="中国" avatar="/src/assets/vue.svg"></Child1>
<Child1 name="李四" country="印度" avatar="/vite.svg"></Child1>
<Child1 name="王五" country="韩国" ></Child1>
</template>
<script lang="ts" setup>
import Child1 from '../components/Child1.vue';
</script>
通用型代码
<!-- 父组件 -->
<template>
<Child1 v-for="u of users"
:name="u.name.first"
:country="u.location.country"
:avatar="u.picture.medium"
:key="u.login.username"></Child1>
</template>
<script setup lang="ts">
import axios from "axios";
import { useRequest } from "vue-request";
import { computed } from "vue";
import { AxiosRespResults } from '../model/ModelRandomUser'
import Child1 from "../components/Child1.vue";
const { data } = useRequest<AxiosRespResults>(
()=>axios.get('https://randomuser.me/api/?results=3')
)
const users = computed(()=>{
return data.value?.data.results || []
})
</script>
Vue3 进阶
组件总览 - Ant Design Vue (antdv.com)
Ant-design-vue
添加必要插件
npm install ant-design-vue
- ant-design-vue 组件库插件
引入 antdv 功能,修改 main.ts(全局导入)
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
createApp(App).use(antd).mount('#app')
表格
<template>
<a-table :columns="colums" :dataSource="studens" rowKey="id"></a-table>
</template>
<script setup lang="ts">
import axios from "../api/request";
import { ref, computed } from "vue";
import { useRequest } from "vue-request";
import { AxiosRespList, Student } from "../model/Model8080";
const {data} = useRequest<AxiosRespList<Student>>(
() => {
return axios.get('/api/students')
}
)
const studens = computed(()=>{
return data.value?.data.data || []
})
const colums = ref([
{
title: "编号",
dataIndex: "id"
},
{
title: "姓名",
dataIndex: "name"
},
{
title: "性别",
dataIndex: "sex"
},
{
title: "年龄",
dataIndex: "age"
}
])
</script>
分页
<template>
<a-table :columns="colums" :dataSource="studens" rowKey="id"
:pagination="pagination" @change="tableChange"></a-table>
</template>
<script setup lang="ts">
import axios from "../api/request";
import { ref, computed } from "vue";
import { usePagination } from "vue-request";
import { AxiosRespPage, Student, StudentQueryDto } from "../model/Model8080";
import { PaginationProps } from "ant-design-vue";
const dto = ref({page: 1, size: 5})
const {data, total, run} = usePagination<AxiosRespPage<Student>, StudentQueryDto[]>(
(d) => axios.get('/api/students/query', {params:d}),
{
defaultParams: [dto.value],
pagination: {
currentKey: "page",
pageSizeKey: "size",
totalKey: "data.data.total"
}
}
)
//在页号或页大小改变时调用
function tableChange(paginationProps: PaginationProps) {
console.log(paginationProps)
dto.value.page = paginationProps.current ?? 1
dto.value.size = paginationProps.pageSize ?? 5
run(dto.value)
}
//分页选项
const pagination = computed<PaginationProps>(()=>{
return {
current: dto.value.page, //当前页
pageSize: dto.value.size, //页大小
total: total.value, //总记录数
showSizeChanger: true, //展示页大小设置框
pageSizeOptions: ["3", "5", "10", "20", "50", "100"] //自定义页大小
}
})
const studens = computed(()=>{
return data.value?.data.data.list || []
})
const colums = ref([
{
title: "编号",
dataIndex: "id"
},
{
title: "姓名",
dataIndex: "name"
},
{
title: "性别",
dataIndex: "sex"
},
{
title: "年龄",
dataIndex: "age"
}
])
</script>
搜索、删除、新增、修改
<template>
<a-row>
<a-col :span="2">
<a-button type="primary" size="small" @click="onInsert">新增</a-button>
</a-col>
<a-col :span="2">
<a-popconfirm title="确认要删除选中学生吗?"
ok-text="确定" cancel-text="取消" @confirm="onDeleteIds"
@visibleChange="onVisibleChange" :visible="visible">
<a-button type="primary" size="small">删除选中</a-button>
</a-popconfirm>
</a-col>
<a-col :span="6"></a-col>
<a-col :span="4">
<!-- 内容跟普通的HTML标签绑定,直接用v-model -->
<!-- 内容跟vue组件绑定,要使用v-model:value -->
<a-input placeholder="输姓名" size="small" v-model:value="dto.name"></a-input>
</a-col>
<a-col :span="4">
<a-select placeholder="选性别" :allowClear="true" size="small" v-model:value="dto.sex">
<a-select-option value="男">男</a-select-option>
<a-select-option value="女">女</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select placeholder="选年龄" :allowClear="true" size="small" v-model:value="dto.age">
<a-select-option value="0,20">20以下</a-select-option>
<a-select-option value="21,30">21~30</a-select-option>
<a-select-option value="31,40">31~40</a-select-option>
<a-select-option value="40,120">40以上</a-select-option>
</a-select>
</a-col>
<a-col :span="2">
<a-button @click="tableChange" type="primary" size="small">搜索</a-button>
</a-col>
</a-row>
<hr/>
<a-table :columns="colums" :dataSource="studens" rowKey="id"
:pagination="pagination" @change="tableChange"
:row-selection="{selectedRowKeys:ids, onChange:onSelectChange}">
<template #bodyCell="{column, record}">
<template v-if="column.dataIndex === 'name'">
{{ record.name + (record.sex === '男' ? '(大侠)' : '(女侠)') }}
</template>
<template v-else-if="column.dataIndex === 'operation'">
<a @click="onUpdate(record)">修改</a>
<a-divider type="vertical"></a-divider>
<a-popconfirm title="确认要删除该学生吗?" ok-text="确定" cancel-text="取消"
@confirm="onDelete(record.id)">
<a>删除</a>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- <A4Save :id="id" :dto="saveDto" :visible="saveVisible"></A4Save> -->
<a-modal :visible="saveVisible" :title="title" @ok="onOk" @cancel="onCancel">
<a-form>
<a-form-item label="编号" v-if="id">
<a-input readonly v-bind:value="id"></a-input>
</a-form-item>
<a-form-item label="姓名">
<a-input v-model:value="saveDto.name"></a-input>
</a-form-item>
<a-form-item label="性别">
<a-radio-group v-model:value="saveDto.sex">
<a-radio-button value="男">男</a-radio-button>
<a-radio-button value="女">女</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="年龄">
<a-input-number v-model:value="saveDto.age"></a-input-number>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import axios from "../api/request";
import { ref, computed } from "vue";
import { usePagination, useRequest } from "vue-request";
import { AxiosRespPage, Student, StudentQueryDto, AxiosRespString, StudentSaveDto } from "../model/Model8080";
import { PaginationProps } from "ant-design-vue";
import A4Save from "./A4Save.vue";
const dto = ref({page: 1, size: 5, name: "", sex: null, age: null})
//====================新增修改功能======================
const id = ref(0)
const saveDto = ref({name:'', sex:'男', age:20})
const saveVisible = ref(false)
const title = ref('新增学生')
const {runAsync:insert} = useRequest<AxiosRespString, StudentSaveDto[]>(
(insertDto) => axios.post('/api/students', insertDto),
{
manual: true
}
)
const {runAsync:update} = useRequest<AxiosRespString, StudentSaveDto[]>(
(updateDto) => axios.put(`/api/students/${id.value}`, updateDto),
{
manual: true
}
)
function onInsert() {
//恢复初始值
id.value = 0
saveDto.value = {name:'', sex:'男', age:20}
title.value = '新增学生'
saveVisible.value = true
}
function onUpdate(record: Student) {
saveVisible.value = true
id.value = record.id
// saveDto.value.name = record.name
// saveDto.value.sex = record.sex
// saveDto.value.age = record.age
Object.assign(saveDto.value, record)
title.value = '修改学生'
}
async function onOk() {
saveVisible.value = false
if (id.value === 0) {
await insert(saveDto.value)
} else {
await update(saveDto.value)
}
search(dto.value)
}
function onCancel() {
saveVisible.value = false
}
//==================搜索功能============================
//默认组件加载时就会执行一次
const {data, total, run:search} = usePagination<AxiosRespPage<Student>, StudentQueryDto[]>(
(d) => axios.get('/api/students/query', {params:d}),
{
defaultParams: [dto.value],
pagination: {
currentKey: "page",
pageSizeKey: "size",
totalKey: "data.data.total"
}
}
)
//在页号或页大小改变时调用
function tableChange(paginationProps: PaginationProps) {
console.log(paginationProps)
dto.value.page = paginationProps.current ?? 1
dto.value.size = paginationProps.pageSize ?? 5
search(dto.value)
}
//分页选项
const pagination = computed<PaginationProps>(()=>{
return {
current: dto.value.page, //当前页
pageSize: dto.value.size, //页大小
total: total.value, //总记录数
showSizeChanger: true, //展示页大小设置框
pageSizeOptions: ["3", "5", "10", "20", "50", "100"] //自定义页大小
}
})
const studens = computed(()=>{
return data.value?.data.data.list || []
})
//==========================删除功能=========================
async function onDelete(id:number) {
await deleteById(id) //使用await使得deleteById方法响应之后再执行search方法
search(dto.value)
}
const {runAsync:deleteById} = useRequest<AxiosRespString, number[]>(
(id) => axios.delete(`/api/students/${id}`),
{
manual: true, //表示手动调用请求,而不是组件加载就执行一次
}
)
//======================删除选中功能===========================
const ids = ref<number[]>([])
function onSelectChange(keys:number[]) {
console.log(keys)
ids.value = keys
}
async function onDeleteIds() {
await deleteByIds(ids.value)
ids.value = []
search(dto.value)
}
const {runAsync:deleteByIds} = useRequest<AxiosRespString, number[][]>(
(ids) => axios.delete('/api/students', {data:ids}), //data对应着请求体中的数据
{
manual: true
}
)
const visible = ref(false)
function onVisibleChange(v:boolean) {
if(!v) { // 希望隐藏
visible.value = false
} else { // 希望显示
visible.value = ids.value.length > 0
}
}
//==============================================================
const colums = ref([
{
title: "编号",
dataIndex: "id"
},
{
title: "姓名",
dataIndex: "name"
},
{
title: "性别",
dataIndex: "sex"
},
{
title: "年龄",
dataIndex: "age"
},
{
title: "操作",
dataIndex: "operation"
}
])
</script>
全局消息
在 request.ts 中对响应消息统一处理
import { message } from 'ant-design-vue'
// ...
// 响应拦截器
_axios.interceptors.response.use(
(response)=>{ // 状态码 2xx
if(response.data.message) {
message.success(response.data.message, 3)
}
// ...
},
(error)=>{ // 状态码 > 2xx, 400,401,403,404,500
// ...
}
)
表单校验
<template>
<a-modal :visible="visible" :title="title" @ok="onOk" @cancel="onCancel">
<a-form>
<a-form-item label="编号" v-if="id">
<a-input readonly v-model:value="id"></a-input>
</a-form-item>
<a-form-item label="姓名" v-bind="validateInfos.name"> <!-- 绑定错误校验信息 -->
<a-input v-model:value="dto.name"></a-input>
</a-form-item>
<a-form-item label="性别" v-bind="validateInfos.sex">
<a-radio-group v-model:value="dto.sex">
<a-radio-button value="男">男</a-radio-button>
<a-radio-button value="女">女</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="年龄" v-bind="validateInfos.age">
<a-input-number v-model:value="dto.age"></a-input-number>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import axios from "../api/request";
import { ref, computed } from "vue";
import { useRequest } from "vue-request";
import { StudentSaveDto, AxiosRespString } from "../model/Model8080";
import { Form } from 'ant-design-vue'
//......
async function onOk() {
try {
//先进行数据校验
await validate()
saveVisible.value = false
if (id.value === 0) {
await insert(saveDto.value)
} else {
await update(saveDto.value)
}
search(dto.value)
} catch(e) {
console.error(e)
}
}
//======================数据校验============================
const rules = ref({
name: [
{required: true, message: "姓名不能为空"},
{min: 2, message: "字符数至少为2"}
],
sex: [
{required: true, message: "性别不能为空"}
],
age: [
{required: true, message: "年龄不能为空"},
{min: 10, message: "年龄最小为10岁", type: "number"},
{max: 120, message: "年龄最大为120岁", type: "number"}
]
})
// 参数1: 待校验的数据
// 参数2: 校验规则
const { validateInfos, validate } = Form.useForm(saveDto, rules)
</script>
vue-router
安装
npm install vue-router@4
创建 router
首先创建一个 /src/router/a5router.ts 文件,在其中定义路由
import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue'
// 路由 => 路径和组件之间的对应关系
const routes = [
{
path: '/a1',
component: A51
},
{
path: '/a2',
component: A52
}
]
const router = createRouter({
history: createWebHashHistory(), // 路径格式
routes: routes // 路由数组
})
export default router
-
createWebHashHistory 是用 # 符号作为【单页面】跳转技术,上面两个路由访问时路径格式为
- http://localhost:7070/#/a1
- http://localhost:7070/#/a2
-
每个路由都有两个必须属性
-
path:路径
-
component:组件
-
-
createRouter 用来创建 router 对象,作为默认导出
需要在 main.ts 中导入 router 对象:
// ...
import A5 from './views/A5.vue' // vue-router
import router from './router/a5router'
createApp(A5).use(antdv).use(router).mount('#app')
A5 是根组件,不必在 router 中定义,但需要在其中定义 router-view,用来控制路由跳转后,A51、A52 这些组件的显示位置,内容如下
<template>
<div class="a5">
<router-view></router-view>
</div>
</template>
动态导入
import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue' //静态导入
const routes = [
// ...
{
path: '/a3',
component: () => import('../views/A53.vue') //动态导入
}
]
- 用 import 关键字导入,效果是打包时会将组件的 js 代码都打包成一个大的 js 文件,如果组件非常多,会影响页面加载速度
- 而 import 函数导入(动态导入),则是按需加载,即
- 当路由跳转到 /a3 路径时,才会去加载 A53 组件对应的 js 代码
- vue-router 官方推荐采用动态导入
嵌套路由
如果希望再嵌套更深层次的路由跳转,例如:希望在 A53 组件内再进行路由跳转
首先,修改 A53.vue
<template>
<div class="a53">
<router-view></router-view>
</div>
</template>
其次,再修改 /src/router/a5router.ts 文件 内容
import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue'
const routes = [
// ...
{
path: '/a3',
component: () => import('../views/A53.vue'),
children: [ //子组件
{
path: 'student',
component: () => import('../views/A531.vue')
},
{
path: 'teacher',
component: () => import('../views/A532.vue')
}
]
}
]
重定向
- 用法1
import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue'
const routes = [
// ...
{
path: '/a3',
component: () => import('../views/A53.vue'),
redirect: '/a3/student', // 重定向到另外路径
children: [
{
path: 'student',
component: () => import('../views/A531.vue')
},
{
path: 'teacher',
component: () => import('../views/A532.vue')
}
]
}
]
// ...
效果是,页面输入 /a3,紧接着会重定向跳转到 /a3/student
- 用法2
import {createRouter, createWebHashHistory} from 'vue-router'
import A51 from '../views/A51.vue'
import A52 from '../views/A52.vue'
const routes = [
{
path: '/a1',
component: A51
},
{
path: '/a2',
component: A52
},
// ...
{
path: '/:pathMatcher(.*)*', // 可以匹配剩余的路径
redirect: '/a2'
}
]
// ...
效果是,当页面输入一个不存在路径 /aaa 时,会被 path: '/:pathMatcher(.*)*'
匹配到,然后重定向跳转到 A52 组件(404页面)去。
pinia
作用:组件间共享数据。
需求:在组件 p1 里更新了数据,主页组件也同步更新显示
- storage 虽然可以实现多个组件的数据共享,但是需要【主动访问】才能获取更新后的数据
- 本例中由于没有涉及主页组件的 mounted 操作,因此并不会【主动】获取 storage 的数据
安装
npm install pinia
在 main.ts 中引入
import { createPinia } from 'pinia'
// ...
createApp(A6).use(antdv).use(router).use(createPinia()).mount('#app')
定义Store
再新建 store 目录来管理共享数据,下面是 /src/store/UserInfo.ts
import axios from '../api/request'
import { defineStore } from "pinia"
import { UserInfoDto } from '../model/Model8080'
export const useUserInfo = defineStore('userInfo', {
state: () => {
return { username: '', name: '', sex: '' }
},
actions: {
async get(username: string) {
const resp = await axios.get(`/api/info/${username}`)
Object.assign(this, resp.data.data)
},
async update(dto: UserInfoDto) {
await axios.post('/api/info', dto)
Object.assign(this, dto)
}
}
})
-
定义了 useUserInfo 函数,用来获取共享数据,它可能用于多个组件
- 命名习惯上,函数变量以 use 打头
-
state 定义数据格式
-
actions 定义操作数据的方法
-
get 方法用来获取用户信息
-
update 方法用来修改用户信息
-
-
由于 useRequest 必须放在 setup 函数内,这里简化起见,直接使用了 axios
获取用户信息
<template>
<div class="a6main">
<a-layout>
<a-layout-header>
<span>{{serverUsername}} 【{{userInfo.name}} - {{userInfo.sex}}】</span>
</a-layout-header>
<a-layout>
<!-- ... -->
</a-layout>
</a-layout>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import AIcon from '../components/AIcon3' // jsx icon 组件
import { serverMenus, serverUsername } from '../router/a6router'
import { useUserInfo } from '../store/UserInfo'
const userInfo = useUserInfo()
onMounted(()=>{
userInfo.get(serverUsername.value)
})
</script>
修改用户信息
<template>
<div class="a6p1">
<h3>修改用户信息</h3>
<hr>
<a-form>
<a-form-item label="用户名">
<a-input readonly v-model:value="dto.username"></a-input>
</a-form-item>
<a-form-item label="姓名" v-bind="validateInfos.name">
<a-input v-model:value="dto.name"></a-input>
</a-form-item>
<a-form-item label="性别">
<a-radio-group v-model:value="dto.sex">
<a-radio-button value="男">男</a-radio-button>
<a-radio-button value="女">女</a-radio-button>
</a-radio-group>
</a-form-item>
</a-form>
<a-button type="primary" @click="onClick">确定</a-button>
</div>
</template>
<script setup lang="ts">
import { Form } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { UserInfoDto } from '../model/Model8080'
import { useUserInfo } from '../store/UserInfo';
const dto = ref<UserInfoDto>({ username: '', name: '', sex: '' })
const userInfo = useUserInfo()
onMounted(()=>{
Object.assign(dto.value, userInfo)
})
const rules = ref({
name: [
{required: true, message:'姓名必填'}
]
})
const { validateInfos, validate } = Form.useForm(dto, rules)
async function onClick() {
try {
await validate()
await userInfo.update(dto.value)
} catch (e) {
console.error(e)
}
}
</script>
- 不能直接把 userInfo 绑定到表单,需要 dto 中转一下
- userInfo.update 和 useInfo.get 返回的都是 Promise 对象,可以配合 await 一起用