Front End Learning Resource

Collected some learning resources related to front-end development
前端
html、css
HTML5 新特性
绘画的 canvas 元素
媒介回放的 video 和 audio 元素
SVG
语义化标签 :header、main、footer、aside
拖放属性 :<div draggable="true"></div>
html5修改一些新的input输入特性,改善更好的输入控制和验证:
calendar、date、time、email、url、search
本地离线存储
css3 新特性
1.选择器
伪类选择器 、 属性选择器
2.圆角 border-radius
3.透明度 opacity: 0.1
4.盒阴影 box-shadow
5.弹性盒 display:flex
6.盒模型 box-sizing
border 和 padding 不会计算 在宽高中
7.过渡
8.渐变
9.透明度 rgba
BFC
块级格式化上下文
BFC是一块独立的渲染区域,可以将BFC看成是元素的一种属性,拥有了这种属性的元素就会使他的子元素与世隔绝,不会影响到外部其他元素
·设置浮动 float,不包括none
·设置定位(绝对定位或固定定位),absoulte 或者 fixed
·行内块显示模式,inline-block
·设置 overflow,即 hidden,auto,scroll
·表格单元格,table-cell
·弹性布局,flex
特性:
1.同一个 BFC 下外边距会发生折叠
2.BFC 可以阻止元素被浮动元素覆盖
3.BFC 可以清除浮动
回流和重绘
回流:
页面中元素的尺寸,布局,隐藏等改变而需要重新构建页面
1、第一次渲染页面(内容和文字等都发生改变)。
2、浏览器窗口尺寸的改变。
3、页面元素(图片,文字)位置和大小尺寸的改变。
4、新增和删除可见元素。
5、一些style属性发生变化导致元素变化:(如边框,下划线,css伪类)
重绘:
页面中元素属性发生改变,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color,则就叫称为重绘
区别:
他们的区别很大:
回流必将引起重绘,而重绘不一定会引起回流。
比如: 只有颜色改变的时候就只会发生重绘而不会引起回流
当页面布局和几何属性改变时就需要回流
比如:添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变——边距、填充、边框、宽度和高度,内容改变
如何避免和减少回流和重绘
1、减少dom和样式的修改,不多次修改样式
2、样式集中,尽量使用外部样式表和外部js,读写分离,这样有利于服务器和浏览器缓存
3、能使用css的就不用js操作样式:(每次js操作都会导致重绘或者回流)
4、页面中多次需要重排的元素,position使用 absolute 或 fixed
5.尽量不使用表格布局(无定宽高的表格宽高由最后一行决定,绘制到最后一行不合理的时候会反复进行计算回流)
溢出省略号
单行溢出隐藏
width:??px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis
多行溢出隐藏
display:-webkit-box;
overflow:hidden;
text-overflow:ellipsis;
-webkit-line-clamp:2:
水平垂直居中有哪些方法
1.绝对定位方法:确定了当前div的宽度,设置绝对定位,left为50% ,top为50%,
margin-left和margin-top的值为当前div宽度一半的负值 div { width:600px; height: 600px; background:red;
position: absolute; left:50%; top:50%; margin-left:-300px; margin-top:-300px; } 2.绝对定位
:设置绝对定位,上下左右为 0 ,margin :auto 3.弹性盒子 div { height:800px; display: flex;
justify-content: center; align-items:center; } 4.绝对定位 + transform (过渡),将 translate
设置为-50% (不知道宽高) div { position: absolute; left: 50%; top: 50%; transform:
translate(-50%,-50%) } 5.table 布局 将父元素设置 display:table-cell 子元素设置 display: inline-block
css 中实现元素隐藏的几种方法
1. display : none
2. visibility : hidden
3. opacity : 0
4. position : 利用定位将元素的top和left值设为足够大的负数,使它移出屏幕在屏幕上看不见
5. overflow : hidden
6. z-index: 负值
7. transform: scale(0,0):将元素缩放为 0
8. height、width 为 0
display:none 与 visibility:hidden 的区别
display:none , 元素会被隐藏,不会占空间
visibility:hidden,元素会被隐藏,但还是会占空间
CSS 中可继承与不可继承属性有哪些
无法继承:宽、高、padding、margin、border、background、定位
可以继承:font-size、line-height、color、text-align
定位
relative :相对定位,相对于其原来的位置进行定位
absolute :相对于没有定位以外的一个父元素进行定位
fixed :固定定位 ,相对于屏幕视⼝(viewport)的位置来指定元素位置
sticky :粘性定位
画一条 0.5px 的线
缩放
transform: scale(0.5,0.5);
设置元素为浮动后,display 的值
设置元素为浮动后,display 的值是 block
px、em、rem、vw/vh 各自代表的含义?
px:绝对单位,页面按精确像素展示
em:相对单位,如果自身定义了 font-size 按自身来计算,如果自身没有,则以最近父节点字体的大小,整个页面内1em 不是一个固定的值
rem:相对单位,可理解为root em, 相对根节点 html 的字体大小来计算
vh、vw:主要用于页面视口大小布局,在页面布局上更加方便简单
js
闭包 :有权访问另一个函数作用域中的变量的函数
特点:
函数嵌套函数
函数内部可以引用函数外部的参数和变量
参数和变量不会被垃圾回收机制回收
优点:避免全局污染 ,保护私有变量 ,延长了变量的生命周期
缺点:处理不当,容易造成内存泄漏
应用:防抖
js 垃圾回收机制
JS的垃圾回收机制是为了以防内存泄漏
JS有两种变量,全局变量和在函数中产生的局部变量。局部变量的生命周期在函数执行过后就结束了,此时便可将它引用的内存释放(即垃圾回收),但全局变量生命周期会持续到浏览器关闭页面
现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除(mark and sweep)、引用计数(reference counting)。
js 关键字
| break | 用于跳出循环。 |
|---|---|
| continue | 跳过循环中的一个迭代。 |
| do ... while | 执行一个语句块,在条件语句为 true 时继续执行该语句块。 |
| for | 在条件语句为 true 时,可以将代码块执行指定的次数。 |
| for ... in | 用于遍历数组或者对象的属性(对数组或者对象的属性进行循环操作)。 |
| function | 定义一个函数 |
| if ... else | 用于基于不同的条件来执行不同的动作。 |
| return | 返回结果,并退出函数 |
| switch | 用于基于不同的条件来执行不同的动作。 |
| throw | 抛出(生成)错误 。 |
| try | 实现错误处理,与 catch 一同使用。 |
| catch | 语句块,在 try 语句块执行出错时执行 catch 语句块。 |
| var | 声明一个变量。 |
| while | 当条件语句为 true 时,执行语句块。 |
JavaScript 变量生命周期
JavaScript 变量生命周期在它声明时初始化。
局部变量在函数执行完毕后销毁。
全局变量在页面关闭后销毁。
浅拷贝、深拷贝
基本数据类型:数据存储在栈中。
引用数据类型:数据存放在堆内存中,栈中存放了一个引用地址,指向堆内存中的数据。
浅拷贝是复制,两个对象指向同一个地址
深拷贝是新开栈,两个对象指向不同的地址
浅拷贝:只拷贝对象的引用、而不深层次的拷贝对象的值,引用指向的是同一个对象,多个对象指向 堆内存 中的同一对象,任何一个修改都会使得所有对象的值修改,因为它们公用一条数据
Object.assign 第一层是深拷贝,第二层是浅拷贝
concat()
slice()
深拷贝:将引用类型的值全部拷贝一份,形成一个新的引用类型,在堆内存中重新开辟一个空间,数据发生变化,不会修改原数据
拷贝的是他们的引用地址,数据发生变化,不会修改原数据
Object.assign() 这种方式第一层是深拷贝,第二层是浅拷贝
ES6扩展运算符
JSON.parse(JSON.stringify(obj1)); 但是这种方式存在弊端,会忽略undefined、symbol和函数
函数库 lodash 的_.cloneDeep方法
递归拷贝
字符串方法
split() 把字符串分割为子字符串数组
slice() 提取字符串的片断,并在新的字符串中返回被提取的部分
charAt() 返回指定索引位置的字符
concat() 连接两个或多个字符串,返回连接后的字符串
indexOf() 返回字符串中检索指定字符第一次出现的位置
match() 找到一个或多个正则表达式的匹配
replace() 替换与正则表达式匹配的子串
trim() 移除字符串首尾空白
toString() 返回字符串对象值
toUpperCase() 把字符串转换为大写
原型、原型链
原型:即构造函数的 prototype 属性,每个函数都有一个 prototype 属性,指向生成该函数的原型对象
原型链:每个对象都有一个 proto 属性,指向生成该对象的原型对象(这样我们就找到了是哪个对象生成了该对象)
原型链一般用于继承,原型链的核心就是依赖对象的 proto 的指向,当自身不存在属性时,就一层层往上找,直到找到 Object.prototype
因为 _proto_ 实质找的是 prototype ,所以我们只要找这个链条上的构造函数的原型(prototype)
继承: 让一个构造函数去继承另一个构造函数的属性和方法
1.原型链继承 :继承父类构造函数原型上的属性和方法
3.借用构造函数继承
3.组合继承: 原型链继承 和 借用构造函数继承
4.ES6继承方法 :
extends
super关键字,它指向父类的实例(即父类的this对象)。子类必须在constructor方法中调用super方法,否则新建实例时会报错
this 指向
全局作用域下,指向 window
函数作用域下:
非严格模式:指向 window
严格模式下,为 undefined
对象中,指向当前该对象
普通函数调用 :指向 window
构造函数调用 :指向 实例对象
事件绑定的方法:指向 事件的对象
箭头函数 :箭头函数中没有 this,this指向就是定义时所在的作用域中的 this 的值
apply,call,bind
apply,call,bind 改变函数运行时的指向,当第一个参数为null或undefind时,指向 window
apply,call 临时改变thi指向一次
apply : 可以传入数组,第一个参数:要绑定给this的值 第二个参数:参数列表(可以是数组),使用apply方法改变 this 指向后原函数会立即执行,且此方法只是临时改变thi指向一次。
call : 传入参数列表,改变函数指向并立即调用,第一个参数要:绑定给this的值,第二个参数必须是单个参数列表,以逗号分隔的方式,不能是数组
bind :不会调用函数,可以改变函数内部指向,返回绑定this之后的函数
bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入,call则必须一次性传入所有参数),但是它改变this指向后不会立即执行,而是返回一个永久改变this指向的函数。
这三个方法的主要区别在于如何传递参数和何时执行函数。apply和call立即执行函数,而bind则返回一个新的函数,可以在之后的任何时间调用。
apply,call,bind 三者的区别
三者都可以改变函数的this对象指向。
三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为 undefined 或 null,则默认指向全局window。
三者都可以传参,但是apply是数组,而 call、bind 是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入。
bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行
图片懒加载原理
在可视区域之外的图片默认不加载,随着页面的滚动,图片进入了可视区域的范围,则触发图片的加载显示
在图片没有进入可视区域时,先不给<img>的src赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给src赋值。
img 的 src 属性 ,初始化img 的时候,src不能是真实的图片地址,把真实地址存放在一个自定义属性中,例如 data-src 来存放
null 和 undefind 的区别
undefined
1、声明一个变量,但是没有赋值
2、访问对象上不存在的属性或者未定义的变量
3、函数定义了形参,但没有传递实参
4、使用 void 对表达式求值
null
1.如果定义的变量在将来用于保存对象,那么最好将该变量初始化为null,而不是其他值
2.当一个数据不再需要使用时,我们最好通过将其值设置为null来释放其引用,这个做法叫做解除引用
可以把 undefined 看作是空的变量,而 null 看作是空的对象
== 和 === 的区别
== 只比较它们的值是否相等(会进行隐式转换)
=== 会比较它们的值和数据类型是否相等
|| 和 ?? 的区别
||:适用于任何假值的情况,包括 null, undefined, false, 0, NaN, 空字符串 ''
??:仅适用于 null 和 undefined 的情况。
冒泡排序
相邻的数据进行两两比较,小数放在前面,大数放在后面,这样一趟下来,最小的数就被排在了第一位,第二趟也是如此,如此类推,直到所有的数据排序完成
ES6 新特性
1.let、const
2.结构赋值
3.扩展运算符
4.箭头函数
5.Promise
6.Set :不会出现重复的值
7.Map :键值对形式
8.async await
9.class类
10.ES6模块化
11.装饰器
1.let、const 和 var 的区别 :
var 声明的变量,没有“块级作用域”的限制;
let / const 声明的变量,具有“块级作用域”。
var 声明的变量存在“变量提升”,let / const没有。
let / const 有暂时性死区 ,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是 在声明之前就使用这些变量,就会报错
let / const 不允许重复声明
let 和 const 的区别 :const 声明的是常量,不能被修改。
2.解构赋值
如果解构不成功,变量的值就等于 undefined
解构赋初始值时,如果解构成功,则默认值失效,赋初始值时,严格等于 undefined ,才能赋初始值
1.数组解构赋值
let [a, b, c, d, e, f] = arr
2.对象解构赋值
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let obj = {
a: 1,
b: [1,2,3],
c: false,
d: {name: 'geekxia', age: 10 }
}
let { a, b, c, d, e } = obj
3.字符串的解构赋值
4.数值和布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
使用场景:
1.交换变量的值
let a = "hello";
let b = "world";
[a, b] = [b, a];
2.从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便
3.函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来
4.提取JSON数据数据
解构赋值对提取JSON对象中的数据,尤其有用
5.函数参数的默认值
6.遍历Map结构
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) { console.log(key + " is " + value); }
7.输入模块的指定方法
加载模块时,往往需要指定输入那些方法。解构赋值使得输入语句非常清晰
const { SourceMapConsumer, SourceNode } = require("source-map")
3.Promise
Promise 是异步编程的一种解决方案,是一个对象
Promise 有三种状态:pending (进行中) 、fulfilled (成功)、reject(失败)
状态改变只有两种可能:pending(进行中) => fulfilled (成功) / pending(进行中) => reject(失败)
基本语法:
Promise对象是一个构造函数,用来生成 Promise 实例,接收一个函数作为参数,函数中有两个参数(resole,reject),resole表示成功的回调,reject表示失败的回调,可以在回调后面调用 then()、cath()、finally( )
then()方法表示成功时的回调
cath()方法表示失败时的回调
finally()方法表示成功或失败都会执行的回调
4.字符串扩展
1.repeat() repeat 方法返回一个新字符串,表示将原字符串重复 n 次
参数如果是小数,会被取整
如果 repeat 的参数是负数或者 Infinity ,会报错
如果参数是0到-1之间的小数,则等同于0
参数 NaN 等同于0
如果 repeat 的参数是字符串,则会先转换成数字
5.async await
async 用来修饰函数,将函数的返回值封装成 Promise 对象
await 后面跟一个 Promise 对象(如果不是会转换成 Promise 对象),表达式返回 Promise 结果
GET 和 POST 请求的区别
1.浏览器回退的时候,GET请求不会重新请求,而 POST 请求会重新请求
2.GET请求会缓存,而 POST 不会
3.GET参数会暴露在地址栏上,相对不安全,而 POST 不会,相对安全
cookie 和 localstore 和 sessionstore 区别
1.存储大小的区别
cookie数据大小不能大于4K
localStorage 和 sessionStorage则可以达到5M
2.时效性
cookie在设置的有效期内一直有效
localStorage存储持久数据,只要不手动清除则一直存在
sessionStorage数据在当前浏览器关闭后就会被自动清除
3.数据与服务器间的交互方式
cookie的数据会自动传递到服务器端,服务器端也可以写cookie到客户端
localStorage和sessionStorage不会把数据自动传到服务器端,仅在本地存储
http 响应状态码
200 成功
300 重定向
301 永久重定向(请求资源的URL被永久的改变,新的URL会在响应的Location中给出)
302 临时重定向
304 未修改(协商缓存生效)。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
307 临时重定向。与302类似。使用GET请求重定向
400 客户端请求有语法错误,不能被服务器所理解
401 请求未经授权
403 服务器收到请求,但是拒绝提供服务
404 请求资源不存在,举个例子:输入了错误的URL。
405 方法不被允许
500 服务器发生不可预期的错误
503 服务器当前不能处理客户端的请求,一段时间后可能恢复正常
js 事件循环机制 (Event Loop)
事件循环是 js实现异步的一种方法,也是 js的执行机制,由调用栈、任务队列(宏任务和微任务)以及渲染步骤组成
JS是单线程的,浏览器在执行JS代码时先执行同步任务,再执行异步任务
**主线程任务——>微任务(promise)——>宏任务(setTimeout)——>宏任务里的微任务——>宏任务里的微任务中的宏任务——>直到任务全部完成**
调用栈(Call Stack)
用于存储同步任务的执行上下文(函数调用栈)
LIFO(后进先出)结构,代码按顺序执行
任务队列(Task Queue)
宏任务队列(Macrotask Queue):setTimeout、setInterval、DOM 事件回调、I/O 操作等
微任务队列(Microtask Queue):Promise.then、MutationObserver、queueMicrotask
渲染引擎(Render Pipeline)
负责页面渲染(样式计算、布局、绘制)
与事件循环协作,在适当时机更新界面
异步任务包括 宏任务 、 微任务
**宏任务**:
setTimeout
setInterval
主线程任务完成、所有微任务也完成的情况下就会立即执行
**微任务:**
promise
process.nextTick(node 独有)
HTTP 协议 、3 次握手,四次挥手
HTTP超文本传输协议,主要作用就是规定浏览器与服务器之间如何通信以及请求响应数据包的格式
三次握手(建立连接的一个过程)
1. 第一次:浏览器首先发送SYN码给服务器,请求和服务器建立连接。
2. 第二次:服务器接收到SYN码后,发送SYN+ACK码给浏览器,告诉浏览器已建立连接。
3. 第三次:浏览器接收ACK码,验证是否正确,若正确就建立数据连接,可以进行数据传输。
需要三次握手才能确认双方的接收与发送能力是否正常。
四次挥手(断开连接的过程)
1. 第一次:浏览器发送FIN码给服务器,告诉服务器,数据传输完成
2. 第二次:服务器接收到FIN码,然后发送ACK码给浏览器,告诉浏览器,你可以断开连接
3. 第三次:服务器,继续发送FIN + ACK码,告诉浏览器我的数据发送完毕
4. 第四次:浏览器接收到FIN码 + ACK码后,同样会发送ACK码给服务器,告诉服务器,我已接收到,你可以断开连接
http 和 https 的区别
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的 ssl 加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
防抖(debounce)
就是在触发事件后,在 n 秒内,函数只执行一次,如果在 n 秒内,又触发了事件,则会重新计算函数执行的时间
防抖函数分为非立即执行版和立即执行版。
非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
- search远程搜索框:防止用户不断输入过程中,不断请求资源,n秒内只发送1次,用防抖来节约资源
- 按钮提交场景,比如点赞,表单提交等,防止多次提交
- 监听 resize 触发时,调整浏览器窗口大小会不断触发 resize ,使用防抖可以让其只执行一次
节流
节流,就是指连续触发事件,但是在 n 秒 内只执行一次函数
- 拖拽场景:固定时间内只执行一次, 防止高频率的的触发位置变动
- 监听滚动事件:实现触底加载更多功能
- 屏幕尺寸变化时, 页面内容随之变动,执行相应的逻辑
new 操作符
- 创建一个全新的对象
- 这个对象的 __proto__ 属性 要指向 构造函数 的 原型 prototype
- 执行构造函数,使用 call/apply 改变 this 的指向
- 返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象。
(1)创建一个新对象
(2)将对象的__proto__属性指向构造函数的 prototype 属性
(3)将构造函数的作用域赋给新对象(因此this就指向了这个对象)
(4)返回新对象
描述浏览器输入 URL 到页面渲染的整个过程
浏览器的地址栏输入URL并按下回车;
DNS 解析:将域名解析成 IP 地址
生成 HTTP 请求报文
TCP 连接:TCP 三次握手;(三次握手的目的:为了防止已经失效的连接请求报文段突然又传送到了服务器端,从而产生错误)
发送 HTTP 请求;
服务器处理请求并返回 HTTP 报文;
浏览器解析渲染页面;
断开连接:TCP 四次挥手。
白屏
js 执行过程中的错误
资源错误
网络延迟,JS加载延迟 ,会阻塞页面
预防 :
1.骨架屏
2.loading
3.路由懒加载
4.SSR 服务端渲染
csrf
跨站点请求伪造 ,攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求
预防:
1、token验证
由于恶意用户无法窃取cookie,只能利用用户cookie模拟未携带token的http请求,那只要在服务器端进行token验证就可以防御xsrf的攻击。比如我们可以将token存储在localStorage中。
2、验证码
需要用户自己来填写验证码从而识别是否是用户主动发起的该请求。
其优点:简单粗暴、低成本
缺点:用户体验不好,需要多次验证。
3、Referer 字段
利用 HTTP 头中的 Referer 判断请求来源是否合法,Referer记录了该 HTTP 请求的来源地址。
xss
跨站脚本攻击,代码注入攻击,注入恶意脚本,利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全
预防:
1、输入过滤
对用户的输入进行过滤,包括text、post等等所有输入进行可靠性检测。
也可以制作一个白名单,一但输入中的字符不在白名单内,自动过滤掉。
2、转义
与正则的思想类似,对比如<、>等等特殊字符进行转义处理。
1.验证码
2.服务端渲染开启模板引擎自带的 HTML 转义功能
3.主动检测和发现,可使用 XSS 攻击字符串和自动扫描工具寻找潜在的 XSS 漏洞
数组去重
扩展运算符结合set集合 [...new Set(xxx)]
利用 双重for循环 ,结合 splice 方法
利用 filter() 去重
利用 for 循环 搭配 indexOf 去重
利用 for 循环结合 includes 实现数组去重
对象去除不必要字段
1.使用 lodash 库的 omit 方法:
安装 lodash 库并使用 omit 方法来去除不需要的字段。
这种方法简洁且易于维护,适用于需要处理多个字段的情况。
const userData = omit(userInfo, ['confirmPassword', 'captcha']);
2.使用 Object.keys 和 reduce:
通过 Object.keys 获取对象的所有键,然后使用 reduce 方法构建一个新的对象,排除不需要的字段。
这种方法灵活且可控,适用于复杂的字段管理。
const userData = Object.keys(userInfo).reduce((acc, key) => {
if (key !== 'confirmPassword' && key !== 'captcha') {
acc[key] = userInfo[key];
}
return acc;
}, {} as Partial<UserDto>);
3.使用 for...in 循环:
通过 for...in 循环遍历对象的所有属性,并构建一个新的对象,排除不需要的字段。
这种方法直观且易于理解,适用于简单的字段管理。
const userData: Partial<UserDto> = {};
for (const key in userInfo) {
if (key !== 'confirmPassword' && key !== 'captcha') {
userData[key] = userInfo[key];
}
}
4.使用 Object.assign 和 delete:
通过 Object.assign 创建一个新的对象,然后使用 delete 删除不需要的字段。
这种方法简单直接,适用于需要删除
const userData = Object.assign({}, userInfo);
delete userData.confirmPassword;
delete userData.captcha;
for 循环 和 forEach 循环的区别
forEach循环过程中不可以中断,for循环可以使用 关键字 break、return、continue 进行中断
return、break、continue 的区别
1、return 关键字并不是专门用于跳出循环的,return 的功能是结束一个方法。
2、continue 只是中止当前循环,接着开始下一次循环。
3、break 立即终止当前所在的循环或 switch 语句,并跳出循环体,会继续执行该循环之后的代码(如果有的话)
| 关键字 | 终止范围 | 适用场景 | 返回值 |
|---|---|---|---|
break | 当前循环或 switch | 循环、switch | 无 |
continue | 当前循环迭代 | 循环 | 无 |
return | 当前函数 | 函数内部 | 有(可指定) |
for in 和 for of 的区别
for in 适合遍历对象的属性,for of适合遍历数组
for in 循环出的是 key 值,for of循环出的是 value
for in 可以遍历可枚举属性,for of 遍历的是可迭代的
for of 不能直接遍历对象,需要配合 Object.keys() 搭配使用
什么是 SEO?你有哪些策略实现 SEO
SEO:搜索引擎优化,让用户更多地找到你
SEO优化的原则:尽量减少js、css功能,尽可能多地使用静态html
SEO策略:官网、移动官网
数据能用静态渲染,尽量使用渲染
h1-h6
尽量不要都使用div
html5语义化标签 header footer article nav aside
meta 元数据、关键词
title 标签
加上title属性
加上图片alt属性
服务端渲染
如何给 SPA(单页面应用程序)做 SEO
1.SSR服务端渲染
将组件或页面通过服务器生成 html,再返回给浏览器,如 nuxt.js
2.静态化
目前主流的静态化主要有两种:(1)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中(2)另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果
3.使用 Phantomjs 针对爬虫处理
原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML
浏览器缓存两大策略
1. 强制缓存:不会向服务器发送请求,直接命中内存中的缓存资源,从chrome Network中可以看到资源200且from disk cache或from memory cache。
强制缓存 就是不会向服务器发送请求,而是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程
2. 协商缓存:向服务器发送请求,服务器根据request header内的参数来判断是否需要更新此资源,如果不需要更新,服务器返回304的状态码,然后通知浏览器读取本地缓存。
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
总结:强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用强缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,使用则返回304,继续使用协商缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;
函数式编程
React16.8 和 Vue3.0
函数式编程使得代码单元相比面向对象来说更加独立,在tree shaking(移除 JavaScript 上下文中的未引用代码(dead-code))过程中更便于打包工具对未使用的代码进行过滤,对项目体积有一定的优化
函数式编程以函数为代码单元,相比于面向对象的方式减少了对this的控制,对于js这种this指向可能会发生改变的语言,一定程度上减轻了开发人员对于this指向问题的困扰
函数式有利于单元测试
数据结构
集合结构:该结构的数据元素间的关系是“属于同一个集合”
线性结构:该结构的数据元素之间存在着一对一的关系
树型结构:该结构的数据元素之间存在着一对多的关系
图形结构:该结构的数据元素之间存在着多对多的关系,也称网状结构
Vue
Vue 基本指令
v-if , 当它们的值是 false 时,元素被移除掉
v-show,当它的值是 false 时,元素只是通过 display:none 来隐藏元素
v-if 本质就是 创建和销毁元素
v-show 是通过 display:none css样式的方式来隐藏元素
v-for 和 v-if 的优先级:
v-for 的优先级更高,如果同时出现在同级每次渲染都会先执行循环再判断条件,由此可见对性能损耗比较大,可使用计算属性先筛选需要的数据
v-model 原理
v-bind:value
v-on:事件(input) 手动取值(e.target.value),并赋值
存在问题:v-model 和 value 有一个强绑定关系,如果子组件中有一个 input 原生标签,此时就会影响原生标签的 value 和 input 事件,想要支持多属性的话,需要使用.sync
vue3 统一 使用 v-model 进行多个数据双向绑定,废除了 model 组件选项。
计算属性、监听器及其二者区别
计算属性 computed
作用:对多个声明式变量进行复杂运算,以减少在指令使用复杂的表达式
特点:依赖于Vue的响应式系统,自己关联的声明式变量只要不发生变化,不会重新计算,具有缓存作用
原理:getter、setter 钩子函数
侦听器 watch
作用:用于监听一个变量的变化,可以监听哪些变量呢?
声明式变量
计算属性
路由$route
区别:
watch 可以进行异步操作,computed 不行
computed 依赖于Vue的响应式系统,具有缓存作用,watch没有
计算属性默认只有 getter属性,如果需要 setter 属性,改成对象的写法
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
watch 如果需要配置其他属性(深度监听),需要改成 对象的形式
watch: {
myName(newValue,oldValue){
// do someing
},
myNumber: {
handler(newVal, oldVal) {
console.log('newVal', newVal);
console.log('oldVal', oldVal);
},
//immediate为true时则立即触发回调函数;如果为false,则和上面的例子一样,不会立即执行回调。
immediate: true,
deep:true
}
}
methods 与 computed 区别
1、调用方式不同。computed直接以对象属性方式调用,不需要加括号,而 methods 必须要函数执行才可以得到结果。
2、绑定方式不同。methods与compute纯get方式都是单向绑定,不可以更改输入框中的值。compute的get与set方式是真正的双向绑定。
3、是否存在缓存。methods没有缓存,调用相同的值计算还是会重新计算。competed有缓存,在值不变的情况下不会再次计算,而是直接使用缓存中的值。
$nextTick
在下次 DOM 更新循环结束之后,执行延迟回调,获取更新后的 DOM
视图还没更新完,获取不到 dom 节点的信息
原理:Vue的异步更新策略,就是如果数据发生变化,Vue不会立刻更新 DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新
使用场景:1.created中获取DOM时
2.响应式数据变化后获取DOM更新的状态,比如 列表更新后的高度
nextTick 有一个参数 ,是回调函数,作用是,等待 视图更新后,再执行 回调函数中的代码
Vue 组件传参
父子组件通信:
父传子通过props
子传父通过自定义事件
兄弟组件通信:
事件总线 eventbus 是基于一个消息中心,订阅和发布信息的模式 $on $emit
vuex
跨组件通信:
provide / inject
可以通过 $parent 直接获取到父组件的实例,可以通过 $children 直接获取到子组件的实例
ref
vue 的 router 和 route 区别是什么
router
是 VueRouter 的一个对象,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由包含了许多关键的对象和属性。
$router.push
$router.replace
route
是一个跳转的路由对象,每一个路由都会有一个route对象,是一个局部的对象,可以获取对应的name,path,params,query等
$route.path
$route.params
$route.name
$route.query:含路由中查询参数的键值对
Vue 路由守卫
全局守卫(3):在所有路由导航之前或之后执行
全局前置守卫 beforeEach((to,from,next)=>{})
全局解析守卫 beforeResolve ((to,from,next)=>{})
全局后置钩子 afterEach((to,from)=>{}) 没有 next()
路由独享守卫(1): 在进入特定路由之前执行
beforeEnter:(to,from)=>{}
组件内守卫(3):在进入或离开特定组件时执行
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this`
// 因为当守卫执行时,组件实例还没被创建!
beforeRouteEnter
// 可以访问 this
beforeRouteUpdate
beforeRouteLeave
为什么 data 属性是一个函数而不是一个对象?
形成一份独立的作用域,不会受到其他实例对象数据的污染,避免变量全局污染
Vue 的响应式原理(双向绑定原理)
响应式发生在创建阶段,Data中的变量默认是不具备响应式原理的,Vue中 把 data 中的选项进行遍历,使用 Object.defineProperty 进行响应式数据劫持,并把这些转化为 getter/setter ,将劫持到的数据赋值给当前实例化对象上面,当data中的数据发生变化时触发 getter/ setter(钩子函数),然后通知 watcher,通过 watcher 进行视图更新,视图发生更新进而生成新的虚拟DOM
Vue 的生命周期
Vue的生命周期,总共有11种,常用的有8种,分为4个阶段,分别是:创建、挂载、更新、销毁阶段,
开始先实例化,初始化事件及钩子函数,
然后进入第一个阶段,创建阶段,beforeCreate ----> created ,响应式原理就发生在这个阶段,具体是通过对Data选项进行遍历,使用object.defineProperty进行响应式数据劫持,并把这些 转化为 getter/setter, 把劫持到的数据赋值到当前实例化对象上,当data中的数据发生变化时触发 getter/ setter(钩子函数),然后通知 watcher,通过 watcher 进行视图更新,视图发生更新进而生成新的虚拟DOM,,可以在创建完成时期(created),进行发请求,传数据,调接口,
创建阶段结束之后,就开始找视图结构,找到视图结构之后,
就进入了第二个阶段( beforeMount -----> mounted)挂载阶段,在这个阶段,创建虚拟DOM对象,然后把虚拟DOM对象替换成真实的数据,完成视图的渲染,挂载完成之后,可以进行调接口,开启定时器,长连接,DOM操作等。
当声明式变量发生变化时,就进入第三阶段( beforeUpdate ----> updated )更新阶段,再创建一个新的虚拟DOM,然后运用 diff算法,把新的和旧的虚拟DOM进行比较,找出两者之间变化的最小差异,标记为脏节点,然后 派发(patch) 给 Watcher,通过 Watcher 转化为真实的DOM,完成页面的渲染,只要声明式变量再次发生改变时,就会一直循环。
最后一个为销毁阶段(beforeDestroy ---> destroyed),在销毁之前,可以关闭定时器,关闭长连接,清除耗费内存的其他变量,最后销毁
动态组件 keep-alive
它是Vue内置组件,在Vue系统中可以直接使用
作用:被keep-alive所包裹的组件,不会“死”,不会被销毁
生命周期:被keep-alive包裹的组件,有两个特殊的生命周期
activated:当组件被激活时触发
deactivated:当组件被停用时触发
keep-alive 有三个属性 :
include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
max - 数字。最多可以缓存多少组件实例。
父子组件生命周期执行顺序
加载渲染过程:
父 beforeCreate -- 父 created -- 父 beforeMount --子 beforeCreate --子 created --子beforeMount -- 子 Mounted --父 Mounted
更新过程:
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
销毁阶段:
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
Vuex 工作流程
Vuex在工作中,主要用于解决组件之间数据共享的问题,当我们需要定义共享数据的时候,定义在状态管理的State中,页面中如果发生各种交互行为,需要修改state,我们可以通过Actions或者Mutations方法,当我们需要通过后端调接口时,我们就封装actions方法,在页面(组件)的 Mouted 生命周期或者Created 生命周期中,进行派发并触发(Dispatch)一个 action 方法,并把参数传递过去,得到后端异步数据后,我们在状态管理的Actions中commit(提交并触发)Mutations方法,来修改state,在页面中我们使用...mapState来使用数据,每次state更新,页面自动更新,如果页面中没有涉及到页面数据,只是同步数据,我们直接在页面事件处理器中,通过commit(提交并触发)Mutations方法,来修改state,state发生变化,页面自动更新
Vuex 数据持久化原理
vuex只是在内存保存状态,刷新之后就会丢失
1.使用插件(vue-persist、vue-persistedstate) 内部实现就是通过订阅(subscriber) mutation变化做统一处理,通过插件的选项控制哪些需要持久化
2.提交 mutation的时候同时存入 localStorage,store中把值取出来 作为state的初始值即可
subscriber 方法
Vue.use()做了什么工作
Vue.use() 是全局注册一个组件或者插件的方法。每次注册前,都会判断一下这个组件或者插件(plugins)是否注册过,如果注册过,就不会再次注册了。
第一,判断这个插件是否被注册过,如果已经注册了,不允许重复注册。如果插件没有被注册过,那么注册成功之后会给插件添加一个 installed 的属性,其值为true。Vue.use方法内部会检测插件的installed属性,从而避免重复注册插件.
第二,接收的plugin参数的限制是 Object | Function 两种类型之一。
1.如果是对象
该对象里面要有一个 install 方法
Vue.use就是调用里面的 install 方法 ,这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象,用于传入插件的配置
2.如果是一个 function
Vue.use时 function 会直接执行
作用 :
添加全局方法或者属性。如: vue-custom-element
添加全局资源:指令/过滤器/过渡/组件等。如 vue-touch
通过全局混入来添加一些组件选项。如 vue-router
添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现
一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
Vue 依赖收集
通过 Object.definedproperty,进行响应式数据劫持,来设置观察属性的 setter 和 getter
通过 getter 收集依赖,通过 setter 触发依赖更新
get事件在属性没有变化时触发并且还会触发dep收集依赖
set事件在属性发生变化触发并且触发dep收集依赖再触发Watch执行更新
跨域与浏览器同源策略
什么是跨域?
"协议://域名:端口",有任何一个不同就是跨域
什么是 浏览器同源策略?
1.浏览器同源策略,是一种安全机制,它的特点是,阻止Ajax进行跨域(非同源)下的数据请求
2.同源策略只在浏览器中才起作用,在Node服务器上使用ajax工具跨域请求,是没有任何问题
总结:只有在浏览器中,同源策略才起阻止ajax的跨域请求
如何解决跨域请求的问题?(常用有三种)
1.JSONP 原理:利用 script 的 src属性,浏览器不会阻止, 只能解决GET请求的跨域问题
2.CORS 在后端添加 headers 以允许被同源访问(后端配置允许跨域的源)
3.代理 前端代理、后端 Nginx/Apache代理
前端代理解决 "跨域请求" 的机制是怎样的呢?
1.让前端业务调接口,访问本地服务器(localhost:8080),如此就不跨域
2.本地服务器做了代理,当收到前端业务请求时,进行代理转发,相当于是node服务向远程服务器发送请求
3.node服务向远程发送请求时,跨域了,但是node环境中没有CORS同源策略
在本地开发环境下,可以使用前端代理,当上线后可以使用后端代理
Vue 的 diff 算法
比较只会在同层级进行, 不会跨层级比较
在diff比较的过程中,循环从两边向中间比较
比较方式:
diff整体策略为:深度优先,同层比较
比较只会在同层级进行, 不会跨层级比较
比较的过程中,循环从两边向中间收拢
Diff 运算的工作流程:(发生在更新阶段)
1.在挂载阶段,生成一个虚拟DOM,保存在内存中
2.在声明式数据发生变化时(更新阶段),Vue会生成一个新的虚拟DOM
3.使用diff(vm1,vm2),找出两个虚拟DOM之间变化最小差异,将其标记为脏节点
4.接着把脏节点patch到Watcher,使用真实DOM操作api更新视图
如何理解虚拟 Dom
1.虚拟DOM初始生成,发在挂载阶段beforeMoute--->Mouted
2.是一个json对象,是根据真实Dom结构生成的,用于描述真实DOM结构,保存在内存中
虚拟DOM的作用:给DOM更新提供了中间层,避免用户过渡地操作真实DOM,提高web性能
Vue性能高原因:Vue工作每次都会生成一个新的虚拟DOM对象,然后使用diff运算,对比新的和旧的虚拟DOM,找出他们之间的变化的最小差异,再通过真实的DOM操作,把最小差异更新到页面上
BSR 的优势和劣势有哪些?
BSR 客户端渲染(前后端分离):视图与数据的组装是在客户端完成的
目前主流的 React ,Vue, Angular 等 SPA 页面未经特殊处理均采用客户端渲染。最常见脚手架 create-react-app 生成的项目就是客户端渲染,查看源代码可以发现除了一个 bundle.js 以外看不到其他的内容,浏览器加载 bundle.js 后会"挂载" React 代码到 root 节点上,形成页面的渲染。
前后端分离,代码更容易维护
数据化应用,交互更加丰富
前端工程师讲价值更高
SEO有严重劣势
在ToB产品上应用更广泛
SSR 的优势和劣势有哪些?
前后端不分离,对后端的要求非常高
有利于SEO
对客户端的压力比较小,服务器压力较大
在ToC产品上应用比较广泛
hash 和 history 区别
1.原理不同 :
hash:通过监听浏览器的 hashchange()事件变化,查找对应的路由规则
history:利用 H5 的 history 中新增的两个API pushState()和 replaceState()和 一个 popstate() 事件 监听URL变化
history.pushState 浏览器历史纪录添加记录
history.replaceState修改浏览器历史纪录中当前纪录
history.popState 当 history 发生变化时触发
2.history 模式需要后端配合将所有访问都指向 index.html,否则用户刷新页面,会导致 404 错误
require 与 import 的区别
1.两者的加载方式不同,require是在运行时加载,而import 是在编译时加载
2.语法规范不同,require是 CommonJS/AMD规范,import是 ESMAScript6+ 规范
项目权限管理
一般分为 页面权限和按钮权限
方案 :前端方案、后端方案
Vue3.0 新特性
响应式原理
Vue2.0 响应式 使用 object.defindPrototy,对data上的数据进行遍历,进行响应式数据劫持,然后把劫持到的数据赋值给当前组件实例化对象上面 存在的问题: 1.对象动态新增属性、删除属性,界面不会自动更新(需要使用$set、$delete) 2.数组直接通过下标修改数值,界面不会自动更新(需要使用$set,或者splice方法) 3.必须要遍历所有的数据,还需要重写数组的方法,性能消耗也比较大 如何监听数组的变化: 原理就是重写数组的七个原始方法,当使用者执行这些方法时,我们就可以监听到数据的变化,然后做些更新操作,Vue3.0 的 响应式(Proxy) 直接对整个对象进行响应式数据劫持,并返回一个新的对象,就可以直接操作新的对象 可以直接监听数组的变化(`push`、`shift`、`splice`) 实现原理: 通过 Proxy(代理):拦截对象中的任意属性的变化,包括属性的读写、添加、删除等 通过 Reflect(反射):对被代理对象的属性进行操作 响应式数据丢失问题: 使用 reactive 定义的数据重新赋值 响应式数据被解构赋值(大多是 props 中的数据被解构赋值) 使用vuex的数据进行赋值 可以使用 toRefs() 和 toRef() 这两个工具函数 import { toRefs, toRef } from 'vue' // 将 `props` 转为一个其中全是 ref 的对象,然后解构 const { title } = toRefs(props) const title = toRef(props, 'title')性能优化
diff算法优化: Vue 2.0 是全量比较 Vue 3.0 新增静态标记,每次创建虚拟 DOM 的时候,会根据DOM中的内容,是否会发生变化,添加静态标记,只比较有静态标记的静态提升: Vue2.0中,无论元素是否发生变化,每次都会重新创建,然后渲染 Vue3.0中,对于不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用即可事件侦听器缓存、复用SSR(服务端渲染优化) : 当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染按需编译、体积更小
vite 实现原理:利用 ES6 的 import 会发送请求去加载文件的特性,拦截这些请求,做一下预编译,省去webpack冗长的打包时间Composition API
setup()函数为 Composition API 的入口,在 beforeCreated 之前执行 ,对象中的属性、方法需要 return 出去 setup()函数中也可以使用生命周期,setup()相当于
常用的组合式 Api:setup、ref、reactive、计算属性、侦听器、生命周期、响应式原理、自定义Hooks、toRef、toRefs
其他组合式 Api:shallowRef、shallowReactive、readonly、shallowReadonly、toRaw、markRawsetup 函数: ```js setup 函数有两个参数:props 和 context setup 函数中如果直接解构 props ,会出现丢失响应性,需要解构 `props` 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs()和 toRef() context(上下文对象)是非响应式的,可以安全地解构toRefs: 将 `props` 转为一个其中全是 ref 的对象,然后解构 const { title } = toRefs(props) toRef:将 `props` 的单个属性转为一个 ref const title = toRef(props, 'title')如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象 若要避免这种深层次的转换,请使用 shallowRef() 来替代 若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代更好兼容 TS
更先进的组件
- Fragment
- Teleport
- Suspense
DOM 更新时机
当你修改了响应式状态时,DOM 会被自动更新。
更新不是同步的。Vue 会在“nextTick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick()
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
- 一、变化了哪些细节
1、ref操作的变化。v2中使用ref属性访问DOM或组件实例,在this.$refs上访问它们;在v3中,使用ref这个组合API,配合ref属性来访问DOM或组件实例;当ref和v-for同时使用时,不再自动收集ref实例了,需要封装自定义收集方法。
2、异步组件的变化:v2中使用Promise模拟异步组件;在v3中,使用defineAsyncComponent定义异步组件。
3、$attrs的变化:v2中用于接收自定义属性们(但不包括class和style);v3中除了自定义属性,可以接收class和style了。使用 $attrs / setupCtx.attrs / useAttrs() 接收。
4、自定义指令的变化:v2中使用Vue.directive()定义全局指令;v3中使用app.directive()定义全局指令。v3中自定义指令的钩子们发生系列变化。局部指令仍然使用directives选项。
5、创建根实例的系列变化:v3中使用 createApp()创建根实例;创建根实例时如果用到data选项,data只能写成工厂函数;挂载根实例节点时只能使用$mount(),el选项没有了。
6、函数式组件的变化:v2中是支持函数式组件的,v3中对函数式组件的支持更加强大。
7、Vue构造器函数的变化:v2中可以使用Vue这个构造函数,所有全局API都放在Vue上。v3中,不能使用Vue构造函数了,只能使用createApp()来创建根实例。为什么在v3中要隐藏Vue这个构造函数呢?第一个原因,为了避免开发者操作原型链,这会影响Vue应用的性能;第二原因,是为了配合Webpack实现Vue层面上的"Tree-Shaking"功能。虽然在v3中我们不能在原型链上添加API了,在v3中推荐app.config.globalProperties来添加全局数据。
8、过滤动画的变化:v3中,编写自定义动画名时,使用.qf-enter-from表示进入动画的开始时刻,使用.qf-leave-from表示离开动画的开始时刻。当<transition>对多个元素执行动画时,无须再加key。
9、条件渲染的变化:v3中,v-if/v-else-if/v-else在任何地方使用时,都无须手动添加唯一的key值,v3中会自动为节点们加key。
10、render选项的变化:v2中,render: (h)=>h(App),也就是h函数在render函数的形参中。v3中,render函数没有h这个形参了,把h函数单独变成一个API。
11、v-if和v-for同时使用的变化:v2中不推荐它两一起用,如果一起用,v-for优化级更高。v3中,这两指令可以一起用,但v-if优先级总是更高。
12、watch监听器的变化:v3中,watch可以同时监听多个声明式变量的变化,调用watch()返回stop()方法(用于停止监听)。
13、接收props的变化:v2中接收props时,如果引用数据类型,default写成工厂函数,在这个工厂函数中可以访问this。在v3中,default工厂函数中没有this,但可以使用inject()。
14、v-model的变化:v3中,在HTML表单上,v-model用法没有变化,包括语法糖写法和v2是一样的。v3中,在自定义组件上,v-model等于:modelValue+@update:modelValue。在自定义组件上,可以同时使用多个v-model,像这样 <Form v-model:x='' v-model:y=''>。v3中,使用v-model时,可以支持自定义修饰符,在子组件中使用 xModifiers: { default: ()=>({})} 接收自定义修饰符。model选项被淘汰了。
15、在使用插槽时,即使是默认插槽,在使用时也要这样 <template #default>。
- 二、移除了哪些细节
1、移除了$listeners
2、移除了$children
3、移除了$on/$off/$once事件API,也就是说“事件总线”被移除了。
4、移除了过滤器,没有这个 app.filter(),也没有了filters选项。
5、移除了 Vue.config.keyCodes 自定义键盘码的功能。
6、移除了 el选项。
7、移除了 propsData选项(从new Vue()向App根组件传递初始化数据),在v3中的替代方案是createApp(App, {数据})的第二个参数。
8、移除了 v-on的.native 修饰符。在v2中,这个修饰符是用于解决移动端事件绑定的性能优化。
9、移除了 model选项。(在v2中,model选项用于自定义 v-model语法糖。)
- 三、新增了哪些细节
1、新增了emits选项、新增了defineEmits(),用于在子组件中接收自定义事件。
2、视图模板支持多节点了,类似React的Fragment功能。需要注意的是,因为组件支持多节点了,对$attrs/<transition>等功能有些影响。
3、新增了 getCurrentInstance() ,在组件中访问app根实例以及其全局数据。需要注意的是,这个API所获取到的app实例,和main.js中的那个app不是完全相同的。
4、新增了 app.provide() 这个全局API,向整个组件树中注入全局数据。在某种程度上讲,这种玩法可以替代app.config.globalProperties这种玩法。
5、新增了 nextTick() 这个API。在v3中,nextTick()也支持"Tree-Shaking"了。
6、新增了 <teleport to='HTML标签/id选择器'>,用于把嵌套的HTML渲染到to属性对应的DOM中去。<teleport>不支持对js的穿梭。
7、新增了 <suspense> 内置组件。用于给异步Vue组件添加交互提示。常常配合<transition> <keep-alive> <router-view> 一起用,给整个系统添加统一的交互(Loading...过渡动画、组件缓存等)。
8、新增了 v-memo 指令。用于性能优化。<div v-memo='[依赖列表]'></div> 有且仅有当依赖列表中的变量发变化时,其内部的视图结构才会更新。forceUpdate()可以做到强制v-memo更新。
9、在<style>中可以使用 v-bind了,v3给了我们第三种实现动态样式的玩法。<style module=''>可以实现样式模块化,给<style>添加命名空间。在视图模板中,使用"$style/样式模块名称"访问样式模块中的样式,在setup使用 useCssModule()可以访问样式模块中的新式。
10、新增了 expose选项 / defineExpose() ,用于把setup中的指定变量暴露出来,给其它组件进行访问与引用。原因是,在v3中,写setup()和<script setup>中的变量默认是隐藏的,无法被其它组件访问。所以,这两个新增的api就是解决这个问题的。
11、新增了 useSlots(),用于在子组件访问插槽作用域。和$slots作用一致。
Vue 3 中的插槽与 Vue 2 的区别
Vue 3 在插槽(slots)方面进行了多项改进,使其更加灵活和高效。以下是 Vue 3 中插槽的主要变化和新特性:
1. 编译优化
静态插槽提升:
Vue 3 会对静态插槽进行编译优化,减少运行时的开销。静态插槽的内容在编译时会被提取出来,避免每次渲染时重新创建。
<!-- 子组件 -->
<template>
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
</template>
<!-- 父组件 -->
<my-component>
<template #header>
<h1>Header</h1>
</template>
<p>Default content</p>
<template #footer>
<p>Footer</p>
</template>
</my-component>
2. 动态插槽名称
动态插槽名称:
Vue 3 支持使用动态插槽名称,这在 Vue 2 中是不支持的。
<my-component>
<template #[dynamicSlotName]>
<p>Dynamic Slot Content</p>
</template>
</my-component>
3. 作用域插槽的简化
简化的作用域插槽:
Vue 3 简化了作用域插槽的语法,使其更加直观和易用。
<!-- 子组件 -->
<template>
<div>
<slot :item="item"></slot>
</div>
</template>
<script>
export default {
data() {
return {
item: { name: 'Example' }
};
}
};
</script>
<!-- 父组件 -->
<my-component v-slot="{ item }">
<p>{{ item.name }}</p>
</my-component>
4. 默认插槽的简化
默认插槽的简化:
Vue 3 中,默认插槽的语法更加简洁,可以直接使用 v-slot 或简写 #。
<!-- 子组件 -->
<template>
<div>
<slot></slot>
</div>
</template>
<!-- 父组件 -->
<my-component>
<template v-slot:default>
<p>Default content</p>
</template>
</my-component>
或者简写为:
<my-component v-slot>
<p>Default content</p>
</my-component>
或者更简洁的简写:
<my-component #>
<p>Default content</p>
</my-component>
5. 性能优化
性能优化:
Vue 3 在插槽的渲染和更新方面进行了优化,减少了不必要的 DOM 操作,提高了性能。
例如,Vue 3 会更智能地处理插槽内容的变化,只更新必要的部分。
6. 组合式 API 支持
组合式 API 支持:
Vue 3 引入了组合式 API,可以在 setup 函数中更灵活地使用插槽。
<template>
<div>
<slot :item="item"></slot>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const item = ref({ name: 'Example' });
return { item };
}
};
</script>
<!-- 父组件 -->
<my-component v-slot="{ item }">
<p>{{ item.name }}</p>
</my-component>
总结
Vue 3 在插槽方面进行了多项改进,包括编译优化、动态插槽名称、简化的作用域插槽和默认插槽语法、性能优化以及组合式 API 支持。这些改进使得 Vue 3 的插槽更加灵活、高效和易用。希望这些信息对你有帮助!如果有更多问题,请随时提问。
vue3.0 新特性
零.Vue3 项目创建
创建 Vue3 的三种方式
- Vue-Cli
- Webpack
- Vite
我用 Vite (尤自创的方案),另外两种方法请自己搜索。
- Vite 的实现原理是利用 ES6 的 import 会发送请求去加载文件的特性,拦截这些请求,做一些预编译,省去 webpack 冗长的打包时间。
- 安装 Vite
npm install -g create-vite-app
1 - 利用 Vite 创建 Vue3 项目
create-vite-app projectName
1 cd projectName npm install npm run dev
一.vue 从 2.0 到 3.0 都改变了什么
Vue3.0 在去年 9 月正式发布。
为什么要升级 Vue3
使用 Vue2.x 的童鞋都熟悉,Vue2.x 中所有数据都是定义在 data 中,方法定义在 methods 中的,并且使用 this 来调用对应的数据和方法。那 Vue3.x 中就可以不这么玩了, 具体怎么玩我们后续再说, 先说一下 Vue2.x 版本这么写有什么缺陷,所以才会进行升级变更的。
回顾 Vue2.x 实现加减
<template>
<div class="homePage">
<p>count: {{ count }}</p>
<p>倍数: {{ multiple }}</p>
<div>
<button style="margin-right:10px" @click="increase">加1</button>
<button @click="decrease">减一</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
};
},
2
methods: {
increase() {
this.count++;
},
decrease() {
this.count++;
},
},
};
</script>
上面代码只是实现了对 count 的加减以及显示倍数, 就需要分别在 data、methods、computed 中进行操作,当我们增加一个需求,就会出现下图的情况:

当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图, 每个颜色的方块表示一个功能:

甚至一个功能还有会依赖其他功能,全搅合在一起。
当这个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在 data、methods、computed 以及 mounted 中反复的跳转,这其中的的痛苦写过的都知道。
那我们就想啊, 如果可以按照逻辑进行分割,将上面这张图变成下边这张图,是不是就清晰很多了呢, 这样的代码可读性和可维护性都更高:

那么 vue2.x 版本给出的解决方案就是 Mixin, 但是使用 Mixin 也会遇到让人苦恼的问题:
- 命名冲突问题
- 不清楚暴露出来的变量的作用
- 逻辑重用到其他 component 经常遇到问题
所以,我们 Vue3.x 就推出了 Composition API 主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。接下来我们就重点认识 Composition API。
二. Composition API

2.1setup
setup 是 Vue3.x 新增的一个选项, 他是组件内使用 Composition API 的入口。
setup 执行时机:
export default defineComponent({
beforeCreate() {
console.log('----beforeCreate----')
},
created() {
console.log('----created----')
},
setup() {
console.log('----setup----')
},
})
注意:
1、由于在执行 setup 函数的时候,还没有执行 Created 生命周期方法,所以在 setup 函数中,无法使用 data 和 methods 的变量和方法
2、由于我们不能在 setup 函数中使用 data 和 methods,所以 Vue 为了避免我们错误的使用,直接将 setup 函数中的this修改成了 undefined
3、setup 函数只能是同步的不能是异步的
setup 参数
使用 setup 时,它接受两个参数:
1.props: 组件传入的属性
2.context
setup 中接受的 props 是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式。
错误代码示例, 这段代码会让 props 不再支持响应式:
// demo.vue
export default defineComponent ({
setup(props, context) {
const { name } = props
console.log(name)
},
})
那在开发中我们想要使用解构,还能保持 props 的响应式,有没有办法解决呢?大家可以思考一下,在后面 toRefs 学习的地方为大家解答。
接下来我们来说一下 setup 接受的第二个参数 context,我们前面说了 setup 中不能访问 Vue2 中最常用的 this 对象,所以 context 中就提供了 this 中最常用的三个属性:attrs、slot 和 emit,分别对应 Vue2.x 中的 $attr属性、slot插槽 和$emit 发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。
2.2 reactive、ref 与 toRefs
在 vue2.x 中, 定义数据都是在 data 中, 但是 Vue3.x 可以使用 reactive 和 ref 来进行数据定义。
一般我们使用 ref 来处理基础 js 的基本数据类型,使用 reactive 来代理一个对象,但 ref 也是可以定义对象的双向绑定:
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'Home',
setup: function () {
console.log('setup run')
const obj = ref({ count: 1, name: '张三' })
setTimeout(() => {
obj.value.count = obj.value.count + 1
obj.value.name = '李四'
}, 1000)
return {
obj,
}
},
beforeCreate: function () {
console.log('beforeCreate run ')
},
created: function () {
console.log('created run ')
},
})
注意:
- defineComponent 可以给组件的 setup 方法准确的参数类型定义.
- defineComponent 可以接受显式的自定义 props 接口或从属性验证对象中自动推断
- defineComponent 可以正确适配无 props、数组 props 等形式
- 引入 defineComponent() 以正确推断 setup() 组件的参数类型
我们将 obj.count 和 obj.name 绑定到页面上也是可以的;但是 reactive 函数确实可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean 等。
接下来使用代码展示一下 ref、reactive 的使用:
import { defineComponent, ref, reactive } from "vue";
setup: function () {
console.log("setup run");
const year = ref(0);
const user = reactive({ nickname: "lili", age: 20, gender: "男" });
setInterval(() => {
year.value++;
user.age++;
}, 1000);
return {
year,
user,
};
},
上面的代码中,我们绑定到页面是通过 user.name,user.age;这样写感觉很繁琐,我们能不能直接将 user 中的属性解构出来使用呢?答案是不能直接对 user 进行结构, 这样会消除它的响应式, 这里就和上面我们说 props 不能使用 ES6 直接解构就呼应上了。那我们就想使用解构后的数据怎么办,解决办法就是使用 toRefs。
toRefs 用于将一个 reactive 对象转化为属性全部为 ref 对象的普通对象。具体使用方式如下:
import { defineComponent, reactive, ref, toRefs } from 'vue'
export default defineComponent({
setup() {
const year = ref(0)
const user = reactive({ nickname: 'xiaofan', age: 26, gender: '女' })
setInterval(() => {
year.value++
user.age++
}, 1000)
return {
year,
// 使用reRefs
...toRefs(user),
}
},
})
<template>
<div class="homePage">
<p>第 {{ year }} 年</p>
<p>姓名: {{ nickname }}</p>
<p>年龄: {{ age }}</p>
</div>
</template>
reactive() 的局限性
1.有限的值类型
只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型
2.不能替换整个对象
由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
3.对解构操作不友好,丢失响应式,可以使用 toRefs
当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接
import { defineComponent, reactive, ref ,toRefs} from "vue";
export default defineComponent({
setup() {
const year = ref(0);
const user = reactive({ nickname: "xiaofan", age: 26, gender: "女" });
setInterval(() =>{
year.value ++
user.age ++
}, 1000)
return {
year,
// 使用reRefs
...toRefs(user)
}
},
});
2.3 生命周期钩子
我们可以直接看生命周期图来认识都有哪些生命周期钩子(图片是根据官网翻译后绘制的):

从图中我们可以看到 Vue3.0 新增了 setup,这个在前面我们也详细说了, 然后是将 Vue2.x 中的 beforeDestroy 名称变更成 beforeUnmount; destroyed 表更为 unmounted,作者说这么变更纯粹是为了更加语义化,因为一个组件是一个 mount 和 unmount 的过程。其他 Vue2 中的生命周期仍然保留。
上边生命周期图中并没包含全部的生命周期钩子, 还有其他的几个, 全部生命周期钩子如图所示:

我们可以看到 beforeCreate 和 created 被 setup 替换了(但是 Vue3 中你仍然可以使用, 因为 Vue3 是向下兼容的, 也就是你实际使用的是 vue2 的)。其次,钩子命名都增加了 on; Vue3.x 还新增用于调试的钩子函数 onRenderTriggered 和 onRenderTricked
下面我们简单使用几个钩子, 方便大家学习如何使用,Vue3.x 中的钩子是需要从 vue 中导入的:
import { defineComponent, onBeforeMount, onMounted, onBeforeUpdate,onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered
} from "vue";
export default defineComponent({
// beforeCreate和created是vue2的
beforeCreate() {
console.log("------beforeCreate-----");
},
created() {
console.log("------created-----");
},
setup() {
console.log("------setup-----");
// vue3.x生命周期写在setup中
onBeforeMount(() => {
console.log("------onBeforeMount-----");
});
onMounted(() => {
console.log("------onMounted-----");
});
// 调试哪些数据发生了变化
onRenderTriggered((event) =>{
console.log("------onRenderTriggered-----",event);
})
},
});
2.4 watch 与 watchEffect 的用法
watch 函数用来侦听特定的数据源,并在回调函数中执行副作用。默认情况是惰性的,也就是说仅在侦听的源数据变更时才执行回调。
watch(source, callback, options) 参数说明:
source:可以支持 string,Object,Function,Array; 用于指定要侦听的响应式变量
callback: 执行的回调函数
options:支持 deep、immediate 和 flush 选项。
接下来我会分别介绍这个三个参数都是如何使用的, 如果你对 watch 的使用不明白的请往下看:
a. 侦听 reactive 定义的数据
watch(
() => user.age,
(newAge, oldAge) => {
console.log(oldAge, newAge);
}
);
b. 侦听 ref 定义的数据
const year = ref(0)
setTimeout(() =>{
year.value ++
},1000)
watch(year, (newVal, oldVal) =>{
console.log("新值:", newVal, "老值:", oldVal);
})
c. 侦听多个数据
上面两个例子中,我们分别使用了两个 watch, 当我们需要侦听多个数据源时, 可以进行合并, 同时侦听多个数据:
watch([() => state.age, year], ([curAge, newYear], [preAge, oldYear]) => {
console.log("新值:", curAge, "老值:", preAge);
console.log("新值:", newYear, "老值:", oldYear);
});
侦听复杂的嵌套对象
我们实际开发中,复杂数据随处可见, 比如:
const state = reactive({
room: {
id: 100,
attrs: {
size: "140平方米",
type:"三室两厅"
},
},
});
watch(() => state.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
如果不使用第三个参数 deep:true, 是无法监听到数据变化的。
前面我们提到,默认情况下,watch 是惰性的, 那什么情况下不是惰性的, 可以立即执行回调函数呢?其实使用也很简单, 给第三个参数中设置 immediate: true 即可。关于 flush 配置,还在学习,后期会补充
stop 停止监听
我们在组件中创建的 watch 监听,会在组件被销毁时自动停止。如果在组件销毁之前我们想要停止掉某个监听, 可以调用 watch()函数的返回值,操作如下:
const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
setTimeout(()=>{
// 停止监听
stopWatchRoom()
}, 3000)
还有一个监听函数 watchEffect,在我看来 watch 已经能满足监听的需求,为什么还要有 watchEffect 呢?虽然我没有 get 到它的必要性,但是还是要介绍一下 watchEffect,首先看看它的使用和 watch 究竟有何不同。
import { defineComponent, ref, reactive, toRefs, watchEffect } from "vue";
export default defineComponent({
setup() {
const state = reactive({ nickname: "xiaofan", age: 20 });
let year = ref(0)
setInterval(() =>{
state.age++
year.value++
},1000)
watchEffect(() => {
console.log(state);
console.log(year);
}
);
return {
...toRefs(state)
}
},
});
执行结果首先打印一次 state 和 year 值;然后每隔一秒,打印 state 和 year 值。
从上面的代码可以看出, 并没有像 watch 一样需要先传入依赖,watchEffect 会自动收集依赖, 只要指定一个回调函数。在组件初始化时, 会先执行一次来收集依赖, 然后当收集到的依赖中数据发生变化时, 就会再次执行回调函数。所以总结对比如下:
watchEffect 不需要手动传入依赖
watchEffect 会先执行一次用来自动收集依赖
watchEffect 无法获取到变化前的值, 只能获取变化后的值
其实我们也能进行自定义封装。
2.5 自定义 Hooks ***
Vue2.x 实现加减的例子, 这里可以将其封装成一个 hook, 我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。
useCount.js 实现:
import { ref, computed } from "vue";
export default function useCount(initValue = 1) {
const count = ref(initValue);
const increase = (delta) => {
if (typeof delta !== "undefined") {
count.value += delta;
} else {
count.value += 1;
}
};
const multiple = computed(() => count.value * 2)
const decrease = (delta) => {
if (typeof delta !== "undefined") {
count.value -= delta;
} else {
count.value -= 1;
}
};
return {
count,
multiple,
increase,
decrease,
};
}
接下来看一下在组件中使用 useCount 这个 hook:
<template>
<div>
aaa
<p>count: {{ count }}</p>
<p>倍数: {{ multiple }}</p>
<div>
<button @click="increase()">加1</button>
<button @click="decrease()">减一</button>
</div>
</div>
</template>
<script>
import { defineComponent, ref, reactive, toRefs, watch } from "vue";
import useCount from "./useCount";
export default defineComponent({
name: "About",
setup() {
const { count, multiple, increase, decrease } = useCount(10);
return {
count,
multiple,
increase,
decrease,
};
},
});
</script>
Vue3 的方式可以很明确的看出,将 count 相关的逻辑聚合在一起, 看起来舒服多了, 而且 useCount 还可以扩展更多的功能。
三.简单对比 vue2.x 与 vue3.x 响应式
Vue2.0 响应式原理
响应化过程需要遍历 data,props 等,消耗较大
不支持 Set/Map、class、数组等类型
新加或删除属性无法监听
数组响应化需要额外实现
对应的修改语法有限制
Vue3.0 响应式原理:使用 ES6 的 Proxy 来解决这些问题。
通过 Proxy 代理,来拦截对 data 的一系列的操作。
Object.defineProperty 只能劫持对象的属性, 而 Proxy 是直接代理对象
由于 Object.defineProperty 只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是 Proxy 直接代理对象, 不需要遍历操作
Object.defineProperty 对新增属性需要手动进行 Observe
因为 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用 Object.defineProperty 进行劫持。也就是 Vue2.x 中给数组和对象新增属性时,需要使用$set 才能保证新增的属性也是响应式的, $set 内部也是通过调用 Object.defineProperty 去处理的。
四.Teleport 传送门
相当于 react 的 Protal
Teleport 是 Vue3.x 新推出的功能, 没听过这个词的小伙伴可能会感到陌生;翻译过来是传送的意思,可能还是觉得不知所以,没事下边我就给大家形象的描述一下。
Teleport 是什么呢? Teleport 就像是哆啦 A 梦中的「任意门」,任意门的作用就是可以将人瞬间传送到另一个地方。有了这个认识,我们再来看一下为什么需要用到 Teleport 的特性呢,看一个小例子:

在子组件 Header 中使用到 Dialog 组件,我们实际开发中经常会在类似的情形下使用到 Dialog ,此时 Dialog 就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index 和样式都变得困难。
Dialog 从用户感知的层面,应该是一个独立的组件,从 dom 结构应该完全剥离 Vue 顶层组件挂载的 DOM;同时还可以使用到 Vue 组件内的状态(data 或者 props)的值。简单来说就是,即希望继续在组件内部使用 Dialog,又希望渲染的 DOM 结构不嵌套在组件的 DOM 中。
此时就需要 Teleport 上场,我们可以用
接下来就举个小例子,看看 Teleport 的使用方式
Teleport 的使用 我们希望 Dialog 渲染的 dom 和顶层组件是兄弟节点关系, 在 index.html 文件中定义一个供挂载的元素:
<body>
<div id="app"></div>
+ <div id="dialog"></div>
</body>
定义一个 Dialog 组件 Dialog.vue, 留意 to 属性, 与上面的 id 选择器一致:
<template>
<teleport to="#dialog">
<div class="dialog">
<div class="dialog_wrapper">
<div class="dialog_header" v-if="title">
<slot name="header">
<span>{{title}}</span>
</slot>
</div>
</div>
<div class="dialog_content">
<slot></slot>
</div>
<div class="dialog_footer">
<slot name="footer"></slot>
</div>
</div>
</teleport>
</template>
最后在一个子组件 Header.vue 中使用 Dialog 组件,这里主要演示 Teleport 的使用,不相关的代码就省略了。header 组件
<div class="header">
...
<navbar />
+ <Dialog v-if="dialogVisible"></Dialog>
</div>
...
五.Suspense react 中的 lazy suspense
Suspense 是 Vue3.x 中新增的特性, 那它有什么用呢?别急,我们通过 Vue2.x 中的一些场景来认识它的作用。
Vue2.x 中应该经常遇到这样的场景:
<template>
<div>
<div v-if="!loading">
...
</div>
<div v-if="loading">
加载中...
</div>
</div>
</template>
在前后端交互获取数据时, 是一个异步过程,一般我们都会提供一个加载中的动画,当数据返回时配合 v-if 来控制数据显示。
Vue3.x 新出的内置组件 Suspense, 它提供两个 template slot, 刚开始会渲染一个 fallback 状态下的内容, 直到到达某个条件后才会渲染 default 状态的正式内容, 通过使用 Suspense 组件进行展示异步渲染就更加的简单。:::warning 如果使用 Suspense, 要返回一个 promise :::Suspense 组件的使用:
<Suspense>
<template #default>
<async-component></async-component>
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</Suspense>
asyncComponent.vue:
<template>
<div>
<h4>这个是一个异步加载数据</h4>
<p>用户名:{{user.nickname}}</p>
<p>年龄:{{user.age}}</p>
</div>
</template>
<script>
import { defineComponent } from "vue"
import axios from "axios"
export default defineComponent({
setup(){
const rawData = await axios.get("http://xxx.xinp.cn/user")
return {
user: rawData.data
}
}
})
</script>
从上面代码来看,Suspense 只是一个带插槽的组件,只是它的插槽指定了 default 和 fallback 两种状态。
六. 片段(Fragment)
在 Vue2.x 中, template 中只允许有一个根节点:
<template>
<div>
<span></span>
<span></span>
</div>
</template>
但是在 Vue3.x 中,你可以直接写多个根节点, 是不是很爽:
<template>
<span></span>
<span></span>
</template>
七.更好的 Tree-Shaking
Vue3.x 在考虑到 tree-shaking 的基础上重构了全局和内部 API, 表现结果就是现在的全局 API 需要通过 ES Module 的引用方式进行具名引用, 比如在 Vue2.x 中,我们要使用 nextTick:
// vue2.x import Vue from "vue"
Vue.nextTick(()=>{ ... }) Vue.nextTick() 是一个从 Vue 对象直接暴露出来的全局 API,其实 $nextTick() 只是 Vue.nextTick() 的一个简易包装,只是为了方便而把后者的回调函数的 this 绑定到了当前的实例。虽然我们借助 webpack 的 tree-shaking,但是不管我们实际上是否使用 Vue.nextTick(),最终都会进入我们的生产代码, 因为 Vue 实例是作为单个对象导出的, 打包器无法坚持出代码总使用了对象的哪些属性。
在 Vue3.x 中改写成这样:
import { nextTick } from "vue"
nextTick(() =>{ ... })
八 .受影响的 API
这是一个比较大的变化, 因为以前的全局 API 现在只能通过具名导入,这一更改会对以下 API 有影响:
Vue.nextTick
Vue.observable(用 Vue.reactive 替换)
Vue.version
Vue.compile(仅限完整版本时可用)
Vue.set(仅在 2.x 兼容版本中可用)
Vue.delete(与上同)
九.内置工具
除了上面的 API 外, 还有许多内置的组件
以上仅适用于 ES Modules builds,用于支持 tree-shaking 的绑定器——UMD 构建仍然包括所有特性,并暴露 Vue 全局变量上的所有内容 (编译器将生成适当的输出,以使用全局外的 api 而不是导入)。
十.变更
10.1 slot 具名插槽语法
在 Vue2.x 中, 具名插槽的写法:
<!-- 子组件中:-->
<slot name="title"></slot>
在父组件中使用:
<template slot="title">
<h1>歌曲:成都</h1>
<template>
如果我们要在 slot 上面绑定数据,可以使用作用域插槽,实现如下:
// 子组件
<slot name="content" :data="data"></slot>
export default {
data(){
return{
data:["走过来人来人往","不喜欢也得欣赏","陪伴是最长情的告白"]
}
}
}
<!-- 父组件中使用 -->
<template slot="content" slot-scope="scoped">
<div v-for="item in scoped.data">{{item}}</div>
<template>
在 Vue2.x 中具名插槽和作用域插槽分别使用 slot 和 slot-scope 来实现, 在 Vue3.0 中将 slot 和 slot-scope 进行了合并同意使用。
Vue3.0 中 v-slot:
<!-- 父组件中使用 -->
<template v-slot:content="scoped">
<div v-for="item in scoped.data">{{item}}</div>
</template>
<!-- 也可以简写成: -->
<template #content="{data}">
<div v-for="item in data">{{item}}</div>
</template>
10.2 自定义指令
首先回顾一下 Vue 2 中实现一个自定义指令:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
在 Vue 2 中, 自定义指令通过以下几个可选钩子创建:
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
在 Vue 3 中对自定义指令的 API 进行了更加语义化的修改, 就如组件生命周期变更一样, 都是为了更好的语义化, 变更如下:

所以在 Vue3 中, 可以这样来自定义指令:
const { createApp } from "vue"
const app = createApp({})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
然后可以在模板中任何元素上使用新的 v-focus 指令, 如下:
<input v-focus />
10.3 v-model 升级
在使用 Vue 3 之前就了解到 v-model 发生了很大的变化, 使用过了之后才真正的 get 到这些变化, 我们先纵观一下发生了哪些变化, 然后再针对的说一下如何使用:
变更:在自定义组件上使用 v-model 时, 属性以及事件的默认名称变了
变更:v-bind 的.sync 修饰符在 Vue 3 中又被去掉了, 合并到了 v-model 里
新增:同一组件可以同时设置多个 v-model
新增:开发者可以自定义 v-model 修饰符
有点懵?别着急,往下看 在 Vue2 中, 在组件上使用 v-model 其实就相当于传递了 value 属性, 并触发了 input 事件:
<!-- Vue 2 -->
<search-input v-model="searchValue"><search-input>
<!-- 相当于 -->
<search-input :value="searchValue" @input="searchValue=$event"><search-input>
这时 v-model 只能绑定在组件的 value 属性上,那我们就不开心了, 我们就像给自己的组件用一个别的属性,并且我们不想通过触发 input 来更新值,在.async 出来之前,Vue 2 中这样实现:
// 子组件:searchInput.vue
export default {
model:{
prop: 'search',
event:'change'
}
}
修改后, searchInput 组件使用 v-model 就相当于这样:
<search-input v-model="searchValue"><search-input>
<!-- 相当于 -->
<search-input :search="searchValue" @change="searchValue=$event"><search-input>
但是在实际开发中,有些场景我们可能需要对一个 prop 进行“双向绑定”, 这里以最常见的 modal 为例子:modal 挺合适属性双向绑定的,外部可以控制组件的 visible 显示或者隐藏,组件内部关闭可以控制 visible 属性隐藏,同时 visible 属性同步传输到外部。组件内部, 当我们关闭 modal 时, 在子组件中以 update:PropName 模式触发事件:
this.$emit('update:visible', false)
然后在父组件中可以监听这个事件进行数据更新:
<modal :visible="isVisible" @update:visible="isVisible = $event"></modal>
此时我们也可以使用 v-bind.async 来简化实现:
<modal :visible.async="isVisible"></modal>
上面回顾了 Vue2 中 v-model 实现以及组件属性的双向绑定,那么在 Vue 3 中应该怎样实现的呢?
在 Vue3 中,在自定义组件上使用 v-model,相当于传递一个 modelValue 属性, 同时触发一个 update:modelValue 事件:
<modal v-model="isVisible"></modal>
<!-- 相当于 -->
<modal :modelValue="isVisible" @update:modelValue="isVisible = $event"></modal>
如果要绑定属性名, 只需要给 v-model 传递一个参数就行, 同时可以绑定多个 v-model:
<modal v-model:visible="isVisible" v-model:content="content"></modal>
<!-- 相当于 -->
<modal
:visible="isVisible"
:content="content"
@update:visible="isVisible"
@update:content="content"
/>
不知道你有没有发现,这个写法完全没有.async 什么事儿了, 所以啊,Vue 3 中又抛弃了.async 写法, 统一使用 v-model
10.4 异步组件
Vue3 中 使用 defineAsyncComponent 定义异步组件,配置选项 component 替换为 loader ,Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise,用法如下:
<template>
<!-- 异步组件的使用 -->
<AsyncPage />
</tempate>
<script>
import { defineAsyncComponent } from "vue";
export default {
components: {
// 无配置项异步组件
AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),
// 有配置项异步组件
AsyncPageWithOptions: defineAsyncComponent({
loader: () => import(".NextPage.vue"),
delay: 200,
timeout: 3000,
errorComponent: () => import("./ErrorComponent.vue"),
loadingComponent: () => import("./LoadingComponent.vue"),
})
},
}
</script>
十一.压缩包体积更小,性能更好
当前最小化并被压缩的 Vue 运行时大小约为 20kB(2.6.10 版为 22.8kB)。Vue 3.0 捆绑包的大小大约会减少一半,即只有 10kB!
vue3.0 六大亮点:
1.vue3.0 比 vue2.0 快 1.2~2 倍
diff 算法优化
vue2 中虚拟 dom 是进行全量对比的
vue3 中新增了静态表示 PatchFlag
在与上次的虚拟节点进行对比时,只对比带有 patch flag 的节点
并且可以通过 flag 的信息得知当前节点要对比的具体内容
静态提升
vue2 中无论元素是否参与更新,每次都会重新创建
vue3 中对于不参与更新的元素,只会被创建一次,之后会在每次渲染时被不停的调用
事件侦听缓存
默认情况下 onClick 会被视为动态绑定,所以每次都会去追踪他的变化,但因为是同一个函数,所以没有追中变化,直接缓存起来复用即可。
2.按需变异,体积比 vue2 更小
3.CompositionAPI 组合 api 类似于 react hooks
4.更好的支持 ts
5.暴露了自定义渲染 api
6.Fragment(<></>)、Teleport(Protal)、Suspense 等更先进的组件
十二.节点打 Tag
静态节点
<span>value</span>
动态节点
<span>{{value}}</span>
vue3.0 底层,会自动识别某个节点是否是动态的,如果是动态的会自动生成标识(不同的动态会有不同的标识对应,如内容文本的动态,或者 id 的动态),从而在每次更新 dom 时,直接跳过哪些静态的节点,直接定位到动态的节点,大大节省效率。
十三.事件开缓存
一般为一个节点设置了监听时间,每次页面进行更新,就会重新生成新的监听函数,启用了 cacheHandlers,就会在第一次更新的时候进行自动识别是否可以缓存,如果可以就进行缓存,这样页面更新就不需要重新生成,尤其是在组件上,极大地减少了子组件的不必要刷新和资源消耗。
Vue 性能优化
1.Vue代码层面的优化
代码层面的优化:
1.v-if 和 v-show 区分使用场景
2.computed 和 watch 区分使用场景
3. v-for 遍历 ,加 key 值,避免使用 index 作为 key 值
v-for的同时,避免使用 v-if
4.长列表性能优化 : Object.freeze() 冻结一个对象,不进行响应式数据劫持
5.事件销毁
6.图片懒加载 vue-lazyload
7.路由懒加载
8.第三方插件按需引入
9.优化无限列表性能
10.服务端渲染 SSR or 预渲染
2.webpack配置层面的优化
1.webpack 对图片进行压缩
image-webpack-loader
2.减少 ES6 转为 ES5 的冗余代码
3.提取公共样式等等
为什么通常不使用 index 作为 key?
使用 index 作为 key 时,可能存在DOM元素没有发生改变,只是顺序发生了改变,但是此时的 key 的值就发生了改变,根据domdiff算法,就会更新所有key变化的DOM元素
React 性能优化
列表项使用 key 属性
使用碎片
函数式组件中:
父组件一旦发生render渲染,子组件一定也会执行render渲染,在React中如何避免不必要的render
使用 React.memo 高阶函数包装组件,React.memo 可以实现类似于 shouldComponentUpdate 或者PureComponent 的效果
使用 useMemo 精细化的管控,useMemo 控制的则是是否需要重复执行某一段逻辑,而React.memo 控制是否需要重渲染一个组件
使用 useCallBack ,返回一个函数
useMemo,返回的的是一个值
在列表需要频繁变动时,使用唯一 id 作为 key,而不是数组下标。
必要时通过改变 CSS 样式隐藏显示组件,而不是通过条件判断显示隐藏组件。
使用 Suspense 和 lazy 进行懒加载
避免使用内联函数
使用内联函数,则每次调用render函数时都会创建一个新的函数实例
使用 React Fragments 避免额外标记
使用 Immutable
懒加载组件
事件绑定方式
服务端渲染
React
jsx
如何理解JSX?
1、JSX = JavaScript XML,这是一种语法糖
2、JSX语法,是可选的,但是 React建议使用
3、JSX语法,浏览器不支持,使用@babel/preset-react进行编译。Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。
4、JSX元素,是对象,也是变量,是表达式,不是字符串
5、JSX可以任意嵌套,语法是使用 {} 包裹jsx元素
6、JSX中使用 {/* 注释内容 */}
7、在JSX中,可以使用表达式(表达式也要使用{}包起来),但不能使用语句
8、JSX元素的动态属性,也要使用 {} 包裹
9、在JSX中有三个属性的变化:className,htmlFor, tabIndex
10、在JSX中,新增属性有:key,ref,dangerouslySetInnerHTML
11、JSX可防注入攻击(XSS)
12、自定义组件必须以大写字母开头。
13、JSX支持属性展开语法 <Child {...obj} />。
14、布尔类型、Null 以及 Undefined 在JSX中会被忽略,不能当作文本进行渲染
15、JSX是不可变对象,当JSX元素需要更新时,我们的做法是创建一个新JSX对象,render方法将重新执行并完成DOM渲染(背后的运行机理:this.setState()修改声明式变量,一旦声明式变量发生变化,React系统会生成新JSX对象,对应生成新的虚拟DOM,进而触发Diff运算,找出两个虚拟DOM变化的最小差异,最后把这个最小差异render到DOM上)
React类(组件) ==实例化后==》JSX元素(React元素)
TestJsx 是React类(组件)
<TestJsx/> React元素(JSX元素)
jsx是 JavaScript XML,这是一种语法糖,是 react 推荐使用的语法
jsx浏览器默认不支持这种语法,需要使用 @babel/preset/react 进行转译,背后会使用 react.createElement()这个函数进行调用,转译成浏览器能识别的语法
jsx 是不可变对象,当jsx需要更新时,react会重新生成一个新的jsx对象,render方法将重新执行并完成DOM渲染(背后的运行机理:this.setState()修改声明式变量,一旦声明式变量发生变化,React系统会生成新JSX对象,对应生成新的虚拟DOM,进而触发Diff运算,找出两个虚拟DOM之间变化的最小差异,最后把这个最小差异 render 到DOM上)
jsx 动态属性需要使用 花括号 {} 包裹 ,类选择器需要使用 className
自定义组件必须以大写字母开头
JSX支持属性展开语法 <Child {...obj} />
布尔类型、Null 以及 Undefined 在JSX中会被忽略,不能当作文本进行渲染
渲染机制
渲染顺序:
组件函数的执行顺序是父 → 子
父组件的 render 函数先执行,生成子组件的 React 元素。
子组件随后开始渲染
useEffect 的执行顺序是子 → 父
子组件的 useEffect 回调先执行,父组件的 useEffect 后执行。
原因:React 按组件树的渲染顺序收集副作用,但执行时是反向的(类似栈的先进后出)。
React 的渲染机制是当父组件的 state 或 props 发生变化时,会重新渲染组件及其子组件
通过 React.memo 或 shouldComponentUpdate 跳过不必要的子组件渲染
父组件触发渲染
↓
父组件 render() 执行
↓
生成子组件的 React 元素
↓
子组件 render() 执行
↓
子组件 useEffect 回调被计划执行
↓
父组件 useEffect 回调被计划执行
↓
浏览器绘制 DOM(提交阶段)
↓
按子 → 父顺序执行 useEffect 回调
注意事项:
避免在子组件中依赖父组件的副作用
如果父组件的 useEffect 修改了状态,子组件可能需要通过 useEffect 的依赖项监听变化。
优化子组件渲染
使用 React.memo 或 useMemo 避免不必要的子组件渲染。
避免在渲染函数中执行副作用
副作用应放在 useEffect 中,而不是渲染函数内。
state
如何定义state?只能在构造器中定义。
如何进一步理解state?
1、state是声明式变量,它被 this.setState()修改时,react 会生成一个新的jsx对象,对应会生成一个新的虚拟DOM,并触diff运算,找出他们之间变化的最小差异,通过 render 重新渲染,最终更新DOM视图。
2、state的定义发生构造器中,但是在构造器中不能使用this.setState()
3、要想改变视图,一定要使用this.setState()来修改state。
4、在React中,可以直接修改state,但是这不会触发diff运算,因此视图不会更新。
5、重要原则:当我们修改state时,要考虑新值是否由旧值计算而来,如果是建议使用this.setState(fn)的方式进行修改;如果不是,可以使用this.setState({})
6、this.setState()这个方法是异步的。但是在定时器内部(宏任务)使用this.setState()时,它却是同步的。(微任务==》异步 宏任务==》同步)
7、this.setState({}, fn),当setState()这个异步操作完成时,第二回调函数会执行,在其中可以拿到最新的state( 类似于VUe的监听器 )。
8、当多个this.setState()一起调用时,会被React自动合并成一个setState()操作,触发一次diff运算,一次render()。
9、this.setState()在修改声明式变量时,是一种浅合并。某个声明式变量被修改,不会影响其它声明式变量。
10、state是当组件实例的私有数据,state数据可向子元素传递,而不能向父元素传递,这被称为“React单向数据流”。
state 是声明式变量,要定义在构造器中
在React中,可以直接修改state,但是这不会触发diff运算,因此视图不会更新,要想改变视图,一定要使用 this.setState() 来修改state,但是在构造器中不能使用 this.setState()
当使用this.setState()修改声明式变量时,react 会生成一个新的 jsx对象,对应会生成一个新的虚拟DOM,然后触发 diff 运算 ,并找出他们之间变化的最小差异,通过 render 重新渲染,最终更新DOM视图。
this.setState()这个方法是异步的,但是在定时器内部(宏任务)使用 this.setState()时,它却是同步的。(微任务==》异步 宏任务==》同步)
重要原则:当我们修改 state 时,要考虑新值是否由旧值计算而来,如果是建议使用 this.setState(fn) 的方式进行修改;如果不是,可以使用this.setState({}),this.setState({} || fn ,fn ),第二个参数为一个函数,在其中可以拿到修改的,最新的 state( 类似于VUe的监听器 )
当多个this.setState() 一起调用时,会被 React 自动合并成一个 setState() 操作,触发一次diff运算,一次render()。
this.setState()在修改声明式变量时,是一种浅合并。某个声明式变量被修改,不会影响其它声明式变量。
state是当组件实例的私有数据,state数据可向子元素传递,而不能向父元素传递,这被称为“React单向数据流”
如何进一步理解 props
1、在React开发中,props的作用远远比state更强大
2、在类组件和函数式组件中,都默认有props
3、props是父子组件之间的通信纽带
4、props是不能修改的,因为 React 函数式组件使用的是 纯函数(唯一的输入,唯一的输出,入参不能修改)一个函数的返回结果只依赖于它的参数,并且在执行的过程中没有副作用,我们就把该函数称作纯函数
5、props可以传递任何数据类型,还可以传递事件函数和JSX元素、组件
6、props和state不能交叉赋值,它们没有任何关联
7、最新的React中,props验证是由一个第三库prop-types来实现的
state 和 props 的区别
1. state 可以 通过 setState 或 useState 进行修改,而 props 是外部传入的数据,不能修改
2. props的功能非常强大,可以传递任何数据类型,还可以传递事件函数和JSX元素、组件
3. react一个重要的开发原则 :一切外部数据都要经过 props 传递进来
生命周期
三个阶段 :
1.装载阶段:
React组件实例化时,调用constrouctor()
在这个生命周期中,不能使用this.setState()-
在这个生命周期中,不能使用副作用(调接口、dom操作、定时器、长连接)
不能把props和state交叉赋值
componentDidMount()
相当于是vue中的mounted()
它表示DOM结构在浏览器中渲染已完成
在这里,可以使用任何的副作用(调接口、定时器、DOM操作、长连接。。。)
在这里,可以使用this.setState()
render()
是类组件中唯一的一个必须要有的生命周期
这个render函数必须要有return,return结果只要满足jsx语法都可以。
它的return返回jsx默认只能是单一根节点,但是在Fragment的语法支持下,可以返回多个兄弟节点。
Fragment碎片的写法:<React.Fragment></React.Fragment>,简写成<></>
在return之前,可以做任意的业务逻辑(但不能使用this.setState())
每当this.setState修改声明式变量时,会触发diff运算,进而触发render()重新渲染。
render()这个生命周期,在装载阶段和更新阶段都会运行。
当render()返回null时,不影响生命周期的正常运行。
2.更新阶段(2)
shouldComponentUpdate()
它相当于是一个开关,如果它返回true则更新机制正常执行,如果返回false则更新机制停止.
在vue中是没有的,所在React面试经常问题。
它存在的意义:可以用于性能优化。但是基本上用不到,最新的解决方案是使用PureComponent。
理论上这个生命周期的作用是:用于精细地控制声明式变量的更新问题,如果被变化的声明式变量参与了视图渲染则返回true;如果被变化的声明式变量没有直接或间接参与视图渲染则返回false,以减少diff运算。
render()
componentDidUpdate()
相当于是vue中的updated()
它表示DOM结构渲染更新已完成,只发生在更新阶段
在这里,可以执行大多数的副作用,但是不建议
在这里,可以使用this.setState(),但是必须要有终止条件判断,避免死循环。
3.销毁阶段
componentWillUnmount()
相当于是vue中的beforeDestroy()
一般在这里清除定时器、长连接等其它占用内存的变量
在这里一定不可以使用this.setState()
类组件与函数式组件有什么区别
类组件,要用 class 关键字来定义,它有state状态,有生命周期、有this,有ref,有上下文。类组件的缺点是相对于函数式组件来说,运行速度相对较慢、性能较差。
函数式组件,默认啥都没有(除了props),也就是说默认没有类组件那些特性。函数式组件的好处是运行速度较快,性能更好。(使用 Hooks(v16.8)可以模拟出像类组件一样的众多特性)
类组件使用要实例化,而函数组件直接执行取返回结果即可
表单绑定(React 表单是单向绑定的)
1、受控表单:指的是表单的 value/checked 属性由 state 控制 ,绑定 onChange 事件,手动取值
2、非受控表单:指的是表单的value/checked属性不由state控制。
原则:在React开发中尽可能地都使用受控表单,但是有一个例外,就是文件上传
<input type="file" />
状态提升(是一种数据共享的理念)
要解决的问题:多个组件之间数据共享的问题。
怎么做?具体的做法是,找到这几个组件的最近的父组件,把需要共享的状态数据定义在父组件中。
组合 vs 继承
组合和继承,都是 组件复用 的一种思想(方式), 但是 React 官方推荐使用 组合模式 来实现组件的复用。
组合的语法基础是:props.children 和 render props(自定义属性可以传递 React元素(jsx元素))
在父组件中间 插入标签 ,子组件直接使用 props.children 进行渲染
render props ==> props 可以传递任何数据类型,还可以传递 事件函数 和 JSX元素、组件(*),子组件可以直接通过 props 进行调用
如果使用“继承”思想,如何复用组件?思路如下:
class QfModel extends React.Component {} // 基类
class QfDeleteModal extends QfModal {} // 删除类型Modal
class QfConfirmModal extends QfModal {}
class QfDeleteSmallModal extends QfDeleteModal = {}
conText(上下文)
作用:自上而下地向组件树中注入数据 (数据共享)
注意:在上下文的消费者(实际上就是那些被上下文包裹的组件)中不能修改上下文
怎么使用上下文呢?
1、使用 React.createContext() 创建上下文对象
2、使用上下文对象上的 <xxx.Provider value={xxx}></Provider> 组件,向React组件树注入数据
<ThemeCtx.Provider value={theme}>
<Layout />
</ThemeCtx.Provider>
3、使用上下文对象上的 <xxx.Consumer>{()=>()}</xxx.Consumer> 组件,使用上下文数据
类组件写法:
<ThemeCtx.Consumer>
{
theme => (
<div style={{background:theme.background,color:theme.color}}>
<h1>测试上下文</h1>
</div>
)
}
</ ThemeCtx.Consumer>
在函数式组件中如何使用上下文数据呢?
第1种语法:return (<Consumer>{ ctx => <jsx />}</Consumer>)
第2种语法:使用 useContext('上下文对象') 访问上下文数据
上下文在哪些第三库中会用到呢?React-Router,Mobx,Redux
高阶组件
高阶组件是一种 代码逻辑复用 的高级技巧,结合了 React 的组合特性,在很多第三方库 路由、mobx、redux
中有 用到,也叫高阶函数,本质上是一个函数(纯函数)
作用:高阶组件(也被称之为容器组件),是用来修饰、装修UI组件的,实现业务逻辑的复用。
语法详解:高阶组件(高阶函数)接受一个 UI组件(React类) 作为入参,返回一个新的UI组件(React类)
export default WrapComponent => { return props=>(
<WrapComponent {...props} dialog="{dialog}" img="{img}" />
) } 注意点: 当一个UI组件被多个高阶组件修饰时,props会丢失,需要使用 ...props,保留高阶组件修饰后的
props 语法:hocFn(UIComponent){return
<NewUIComponent {...this.props}
>} 属性继承 使用原则:一个高阶组件,一般只复用一个可以复用的逻辑。 类组件中可以使用 装饰器语法 //
1、什么是高阶组件? // - 高阶组件(Higher Order
Component)本质上是纯函数,它接收一个React组件作为入参,返回一个新的React组件。所以,高阶组件也常常叫做高阶函数。
// -
两个概念:这个被入参的React组件,常常被称之为“UI组件”。这个高阶组件(纯函数),被称之为“容器组件”。
// - 语法1:const hoc1 = UI => (新组件) // - 语法2:const hoc2 = (...args) => (UI => (新组件)) //
-
什么是纯函数?如果一个函数,其函数体中不对入参做任何修改,并且相同入参永远得到相同的返回值,这样的函数就是纯函数。
// 面试题:在React中哪些场景下会用到纯函数?函数式组件、高阶组件、Redux的reducer函数。 //
2、高阶组件的意义 // -
高阶组件不是什么新语法(不是新API),而是一种代码复用技巧,是类组件编程的产物。 // -
作用:代码逻辑的复用。 // -
理解:高阶组件实际上是在对入参(UI组件)进行若干的修饰,比如给UI组件添加公共的视图结构、公共的方法或属性等。
// - 面试题:你在工作中有没有封装过高阶组件?解决过什么具体的问题?(权限设计、注入工具方法) //
3、关于封装高阶组件的若干注意事项 // - 在封装高阶组件时,都要做“属性继承”,避免props丢失。 // -
在封装高阶组件时,如果配合ref一起使用,可能遇到其它“坑”,请参考文档中的说明。
</NewUIComponent>
如何创建 refs
refs是提供一种访问在render方法中创建DOM节点或者React元素的方法,在典型的数据流中,props是父子组件交互的唯一方式,想要修改子组件,需要使用新的props重新渲染它,某些情况下,在典型的数据流外,强制修改子代,这个时候可以使用refs
我们可以在组件添加一个ref属性来使用,该属性是一个回调函数,接收作为其第一个参数的底层DOM元素或组件挂载实例
通过React.createRef()创建的,并通过ref 属性附加到react元素,在构造组件中,
通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们
import React, { createRef } from 'react';
const caseDetailRef = createRef();
或者 使用 useRef
caseDetailRef.current?.xxx
对Dom元素的焦点控制、内容选择、控制
对Dom元素的内容设置及媒体播放
对Dom元素的操作和对组件实例的操作
集成第三方 DOM 库
1、ref属性(其一是一种快捷的DOM访问方式,其二用于引用一个变量)
2、ref的三种使用场景
- 场景1:ref作用在普通的HTML节点上,返回的是DOM对象
- 场景2:ref作用在类组件上,返回的是类组件实例对象
- 场景3:ref作用在函数式组件上(默认会报错,原因是函数式组件没有实例对象),配合forwardRef转发可以访问函数式组件内部的DOM对象。
3、ref三种语法
语法1:<div ref='box' /> // this.refs.box
语法2:<div ref={dom=>this.boxRef=dom} /> // 使用ref={callback} 手动收集ref对象(DOM对象、类组件实例)
语法3:<div ref={createRef()的返回值} /> // const ref = createRef() 通过ref.current来访问DOM对象或类组件实例
注意:以上三种语法,不仅可以作用在HTML节点上,作用在类组件上也是没有问题的。
4、特殊场景;ref配合forwardRef一起,作用在函数式组件上!
- 当ref属性作用在“自定义的函数式组件”上时,默认会报错。
- 这时就要用到React.forwardRef((props,ref)=>(<jsx/>))来解决问题。这时访问的不是函数式组件实例,而函数式组件内部的某一个变量(DOM节点,也可能是一个普通的非DOM变量)
- 语法:const FC = forwardRef((props, ref)=>(<jsx ref={ref}/>))
- 补充:后面在讲 Hooks API 时,还有两个 useRef / useImperativeHandle 和ref使用息息相关!
Hooks (React 16.8 新增)
useState
作用:用于定义声明式变量,模拟类组件中的 state
语法:const [msg, setMsg] = useState('')
useState在定义声明式变量时,一定要赋初始值
useState定义的声明变量,要使用 set*系列方法去更新,不建议直接修改
useEffect
如何理解副作用?你可以这么理解,只要不是生成JSX的业务代码,都可以认为是副作用
副作用包括定时器、调接口、长连接、DOM操作、第三库的初始化等
作用:模拟类组件中生命周期的特性
语法:useEffect(()=>{return ()=>{}}, [])
useEffect 可以看做是 componentDidMount,componentWillUnmount 和 componentDidUpdate 这三个函数的组合
useEffect 有两个参数 ,第一个参数为一个函数 (相当于生命周期里面的 componentDidMount )这个函数里面还可以 return 一个函数(这个 return 的函数相当于 componentWillUnmount),第二个参数为一个数组,相当于 componentDidUpdate ,(可以把相关联的声明式变量放在里面,每当变量发生变化。就会重新执行)
在函数组件中,useEffect是用于执行副作用的主要钩子。当涉及到父子组件时,子组件的useEffect会在父组件的useEffect之前执行
常用 Hooks
useState
useEffect
useContext
useMemo 计算属性
useCallback
useRef
const inputRef = useRef<any>(null);
inputRef.current.blur()
默认情况下,每当一个组件中重新渲染,其中的函数都会重新声明。这样就会导致一个问题:如果该组件将这个函数传递给子组件,那么子组件也会重新渲染,但有时这样的渲染是不必要的
相同点:useCallback 和 useMemo 都是性能优化的手段,类似于类组件中的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate, 判定该组件的 props 和 state 是否有变化,从而避免每次父组件render时都去重新渲染子组件。
useCallback 和 useMemo 的区别是useCallback返回一个函数,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时都重新渲染这个子组件
const renderButton = useCallback(
() => (
<Button type="link">
{buttonText}
</Button>
),
[buttonText] // 当buttonText改变时才重新渲染renderButton
);
useCallback 的返回值是函数,所以适合对组件的事件响应函数进行处理,避免每次执行都生成新的函数,进而导致子组件属性不同而重新渲染。
useMemo 因为返回值是任意值,所以可用用来做高耗时操作的缓存处理,比如组件需要的一些子组件,比较耗时的纯数值计算等。
简单概括就是:useCallback 用来处理事件函数,useMemo 用来缓存 DOM
路由 Hooks
useParams:获取参数(内部封装)
useHistory:通过js api操作路由跳转 push方法
useLocation: 查看当前路由
useRouteMatch: 挂钩尝试以与<Route>相同的方式匹配当前URL。在无需实际呈现<Route>的情况下访问匹配数据最有用。
Redux Hooks
用于 替代 connect 这个高阶组件
useSelector 相当于 mapStateToProps
useDispatch 相当于 mapDispatchToProps
Hooks 使用需要注意什么?
1.版本: 16.8 新增的特性
2.只能在函数式组件中使用
3.只能在函数式组件的最顶层使用hooks,而不能在for循环、if等语句下面使用hooks
因为函数组件中可以使用useState声明多个state,hooks的状态管理依赖于数组,hooks重新渲染的时候依赖于固有的顺序,如在for循环、if等语句下使用hooks就有可能在重渲染时改变hooks的固有顺序,导致bug。这与hooks的实现原理有关系。
如何避免组件的重新渲染?
React.memo() 这可以防止不必要地重新渲染函数组件
PureComponent 这可以防止不必要地重新渲染类组件
这两种方法都依赖于对传递给组件的props的浅比较,如果 props 没有改变,那么组件将不会重新渲染。虽然这两种工具都非常有用,但是浅比较会带来额外的性能损失,因此如果使用不当,这两种方法都会对性能产生负面影响。
Mobx

状态管理(数据管理)
Flux(是一套数据架构的思想)是 Facebook 提出的
Vuex、Mobx、Redux它们都是 Flux 思想指导下的一种具体的解决方案
状态管理工具:可预测状态的数据容器。
在React技术栈:mobx 和 redux
一般情况下,小项目可以考虑使用 mobx 6 & mobx-react 7
如果是大项目,建议使用 redux & react-redux
原则:在React规范中,一切外部数据都要从props进入组件,所以几乎是一定要使用状态管理工具。
如何使用:
从 mobx-react 中,引用 Provider
在App根组件中,使用<Provider store={store}} /> ,注入 store
在React组件中,使用 inject('要使用的store中的数据')(observer(props=>(<div></div>)))
- 以下mobx(v6) + mobx-react(v7) 用ES6语法、函数式组件为例,说明集成mobx架构的一般步骤:
- 1、安装mobx(v6),用面向对象语法编写store和子store的代码逻辑,参见store目录。
- 2、安装mobx-react(v7),在App根组件中,使用<Provider {...store} />
- 3、在React组件中,使用 inject('stort')(observer(props=>(<div></div>)))
先使用 observer() 将组件变为观察者,然后再使用 inject ()注入需要共享的数据
语法:inject('store')observer((UIComponent))
observer(UIComponent) 它的作用是把React组件变成观察者。
特点:当mobx中被观察的数据发生变化时,观察者自动更新。
inject('store')(UIComponent) 它的作用是注入mobx中的状态数据
特点:一旦注入成功,在props上就可以直接访问。
// 【[mobx6]定义子Store第一种写法】 makeAutoObservable
import { makeAutoObservable } from 'mobx'
// makeAutoObservable(this) 作用是把当前对象上的成员属性变成observable变量,把成员方法变成action方法。
export default class UserStore {
constructor() {
makeAutoObservable(this)
}
token = 'token'
changeMsg(payload) {
this.token = payload
}
}
// 【[mobx6]定义子Store第二种写法】
// makeObservable 根据开发者的需求,把指定的成员属性变成observable变量,把成员方法变成action。
// 当在action中需要用到同步的async/await时,建议使用 flow,编写generator的语法。
// observable 它用于把一个变量变成可观察的,当它变化时,在观察者中可以自动更新
// action 表示把一个方法变成action,它可以直接修改observable变量
// computed 用于get操作,是计算属性,当它所关联的observable变量发生变化时,会重新计算
import { makeObservable, action, computed, observable } from 'mobx'
export default class TodoStore {
constructor() {
makeObservable(this, {
msg: observable,
changeMsg: action,
length: computed,
list: observable,
updateList: action,
})
}
msg = 'hello mobx 2'
changeMsg(payload) {
this.msg = payload
}
get length() {
return this.msg.length
}
}
React 中的组件间通信
- 父子组件通信 props
- 自定义事件
- 使用 context 上下文
- 使用 mobx、redux 状态管理
- 状态提升
create-react-app 设置代理
1.在 package.json 中 直接配置 proxy字段
2.下载中间件 http-proxy-middleware
在 src 目录下,新建 setupProxy.js 文件
redux


redux 用于定义 store 的
react-redux 提供高阶组件(connect) 和 Hooks 写法 ,用于把 react 和 redux 连接起来
Redux 一个可预测状态的数据容器,它是基于Flux思想而开源的项目
第一个3,指的是三个api:createStore / combineReducers / applyMiddleware
createStore 用来创建 store
combineReducers 用来合并多个 reducers
applyMiddleware 使用中间件
第二个3,指的是Redux工作流程中的三个核心概念,分别 Store、 Action(Dispatch)、 View
第三个3,指的是Store的三个特点:Store单一数据源、Store是只读的、Store 只能通过 Reducer 纯函数进行修改
技术栈:Redux / React-Redux / Redux-Thunk / Redux-Saga ....
三个 Api : createStore( ) / combineReducers( ) / applyMiddleware( )
import { createStore } from 'redux'
createStore( reducer,{},middleware(中间件)) 创建 store
combineReducers({}) 合并多个 reducer纯函数
import xxx from xxx
const rootReducer = combineReducers({
study,
count,
music,
user,
article
})
const store = createStore(
rootReducer,
applyMiddleware(thunk)
)
export default store
connect 高阶组件写法
使用 createState(reducer)创建 Store ,然后 在 react-redux 中 引入 Provider 组件,引入 Store,并在 Provider 组件上注入 Store ,然后继续在
react-redux 引入高阶组件 connect
connect 语法 :connect(fn1 ,fn2)(UIComponent)
fn1,fn2 两个都要返回一个对象
connect(mapStateToProps ,mapDispatchToProps)(UIComponent)
mapStateToProps 和 mapDispatchToProps 两个都要返回一个对象
mapStateToProps 的形参 是一个 store , // 把状态管理中的 state 映射到 props 上 ,然后就可以在 props 上进行访问
mapDispatchToProps 的形参是一个 dispatch
// 把状态管理中的 state 映射到 props 上 ,然后就可以在 props 上进行访问
const mapStateToProps = (store) => {
return {
msg: store.msg,
}
}
const mapDispatchToProps = (dispatch) => {
return {
changeMsg: () => dispatch({}),
}
}
页面使用
export default connect(mapStateToProps, mapDispatchToProps)(UIComponent)
reducer 纯函数,用于修改 store 数据
let initState = {
msg: 'hello redux',
}
// reducer 是一个纯函数,唯一的输入得到唯一的输出
// 在 redux 中 ,只能使用 reducer 来修改 store(state)
// 深入理解 action ,它只能是一个 Plain Object(一个对象,可以被深复制的)。它只能通过 diapatch (action={}),派发到 Store 中
function reducer(state = initState, action) {
// 因为 store 是 只读的,所以要进行深复制
let newState = { ...state }
switch (action.type) {
case '':
// do something
break
default:
// do something
}
return state
}
const store = createStore(reducer)
export default store
redux Hook 写法,可以用来代替 connect 这个高阶组件
import { useSelector, useDispatch } from 'react-redux'
// useSelector 相当于 mapStateToProps
const msg = useSelector((store) => store.study.msg)
// useDispatch 相当于 mapDispatchToProps
const dispatch = useDispatch()
dispatch(msgChange('hello world'))
redux-thunk 中间件
在 redux 中,dispatch 是同步的,它负责向 store 中派发 plain action object
redux-thunk 用于解决 redux 不支持异步 actions的问题
redux 只支持同步的 action ,需要使用中间件(redux-thunk 或者 redux-saga)解决异步 action
用于异步的 生成器, 必须要 return 一个函数,然后用 redux-thunk
redux-thunk 这个中间件,在 View - Store 之间起作用,它用于判断 action 是不是 函数(fn), 如果是就构建一个 plain object ,发送给 store
redux,实际上发生了 两次 dispatch ,第一次 redux 什么都没做,第二次才是真正后端数据放入 redux 中
const store = createStore( reducer , applyMiddleware ( thunk ) )
当需要使用多个 中间件时
import { compose } from 'redux'
const store = createStore(
reducer ,
compose( applyMiddleware ( thunk ), applyMiddleware ( xxx ))
)
compose 解决 使用多个中间件时,嵌套过多的问题
applyMiddleware ( thunk )( xxx )
applyMiddleware 返回本身
中间件(Middlewa)
其本质上一个函数,对 store.dispatch 方法进行了改造,在发出 action 和执行 reducer这两步之间,添加了其他功能
redux-thunk:用于异步操作
redux-logger:用于日志记录
代码分割
loadable 是一个函数
import loadable from "@loadable/component"
const TestJsx = loadable(()=>import('./study/TestJsx'))
React Fiber --- React 背后的算法
React 15 中的 virtualDOM,这是旧的协调算法
React Fiber 是 React 16 中新的协调算法
react fiber是通过 requestIdleCallback 这个api去控制的组件渲染的“进度条”
react 无法精确更新,所以需要react fiber把组件渲染工作切片
Redux-toolkit
npm install typescript @types/react @types/node redux react-redux @reduxjs/toolkit
持久化处理
src / store / slice / counterSlice.ts;
import { createSlice } from "@reduxjs/toolkit";
interface CounterState {
value: number;
}
interface stateType {
actions: () => void;
counter: CounterState;
}
export const counter = createSlice({
name: "counter",
initialState: {
value: 100,
},
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// action 接收页面传过来的参数
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const value = (state: stateType): number => {
return state.counter.value;
};
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counter.actions;
export default counter.reducer;
src / store / store.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit'
// 持久化
import { persistReducer, persistStore } from 'redux-persist'
// defaults to localStorage for web
import storage from 'redux-persist/lib/storage'
//
import counter from './slice/counterSlice'
const persistConfig = {
key: 'root', // key to identify your persisted data
storage, // where you want to store the data (e.g., localStorage)
whitelist: ['counter'], // state slices you want to persist, if not all
}
const reducer = combineReducers({
counter,
})
const persistedReducer = persistReducer(persistConfig, reducer)
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
})
export const persistor = persistStore(store)
在根文件(index.tsx | main.tsx | _app.tsx)
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { store, persistor } from "../store/store";
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Component {...pageProps} />
</PersistGate>
</Provider>
react diff 算法 的原理是什么
传统diff算法通过循环递归对节点进行依次对比,然后判断每个节点的状态以及要执行的操作,效率低下,算法复杂度高,达到 O(n^3),react将 diff算法 进行一个优化,复杂度降为O(n)
React中使用「三个层级策略」对传统的diff算法进行了优化
主要遵循三个层级的策略:tree层级、component层级、element层级
Tree层级:DOM节点跨层级的操作不做优化,只会对相同层级的节点进行比较,只有删除、创建操作,没有移动操作
Component层级:同一个类的组件,会继续往下diff运算,更新节点属性,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新的
如果确切知道该组件可以不用更新,可以使用react生命周期函数 shouldComponentUpdate() 方法进行判断优化,阻断不必要的 render
Element层级:同层级的单个节点进行比较,(对于比较同一层级的节点们,每个节点在对应的层级用唯一的key作为标识)用唯一的 key 作为标识,通过 key 可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置
tree 层级:

components 层级:

vue 的 diff 算法与 react 的 diff 算法的区别
vue 的 diff 算法
在对新旧虚拟dom进行比较时,是从节点的 两侧向中间 对比;如果节点的 key值 与 元素类型 相同,属性值不同,就会认为是不同节点,就会删除重建
react 的 diff 算法
在对新旧虚拟dom进行比较时,是从节点的 左侧 开始对比,就好比将新老虚拟dom放入两个栈中,一对多依次对比;如果节点的 key值 与 元素类型 相同,属性值不同,react会认为是同类型节点,只是修改节点属性
react diff算法也有明显的不足与待优化的地方:
应该尽量减少将最后一个节点移动到列表首位的操作
当一个集合把最后一个节点移动到最前面,react会把前面的节点依次向后移动,而Vue只会把最后一个节点放在最前面,这样的操作来看,Vue的diff性能是高于react的
两种 diff 算法的相同点
都只进行同级比较,忽略了跨级操作;常见的现象就是对数组或者对象中的深层元素进行修改时,视图层监测不到其变化,故不会对dom进行更新,需调用一些特质方法来告诉视图层需要更新dom
i18n-next(国际化)
react-query
react-query 是一个用于在 React 应用中管理服务器状态的库。它提供了一组强大的工具来处理数据获取、缓存、同步和更新,简化了与服务器交互的复杂性。react-query 通过 hooks 提供了简单且声明式的 API,使得数据获取和管理变得更加容易和高效。
主要功能
- 数据获取和缓存:自动缓存数据,并在需要时重新获取。
- 后台数据同步:在后台自动同步数据,保持数据最新。
- 错误处理:提供内置的错误处理机制。
- 分页和无限滚动:支持分页和无限滚动的数据获取。
- 数据预取:在用户需要之前预取数据,提高用户体验。
主要 Hooks
useQuery:用于获取数据。useMutation:用于提交数据或执行其他副作用。useQueryClient:用于管理和操作查询客户端。
示例
1. 安装 react-query
首先,安装 react-query 和 react-query-devtools(可选,用于开发调试):
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools
2. 配置 QueryClient
在应用的根组件中配置 QueryClient 和 QueryClientProvider:
import React from 'react'
import ReactDOM from 'react-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import App from './App'
const queryClient = new QueryClient()
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>,
document.getElementById('root'),
)
3. 使用 useQuery 获取数据
在组件中使用 useQuery 获取数据:
import React from 'react'
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
const fetchTodos = async () => {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/todos')
return data
}
const Todos = () => {
const { data, error, isLoading } = useQuery(['todos'], fetchTodos)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
export default Todos
4. 使用 useMutation 提交数据
在组件中使用 useMutation 提交数据:
import React, { useState } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import axios from 'axios'
const addTodo = async (newTodo) => {
const { data } = await axios.post('https://jsonplaceholder.typicode.com/todos', newTodo)
return data
}
const AddTodo = () => {
const [title, setTitle] = useState('')
const queryClient = useQueryClient()
const mutation = useMutation(addTodo, {
onSuccess: () => {
// 在成功后无效化并重新获取 todos 查询
queryClient.invalidateQueries(['todos'])
},
})
const handleSubmit = (e) => {
e.preventDefault()
mutation.mutate({ title, completed: false })
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add Todo</button>
</form>
)
}
export default AddTodo
总结
react-query 是一个强大的工具,可以简化 React 应用中的数据获取和管理。通过使用 useQuery 和 useMutation 等 hooks,可以轻松地处理数据获取、缓存、同步和更新,提高开发效率和用户体验。
找到具有 1 个许可证类型的类似代码
framer-motion
用于 React 的动画库
LazyMotion:用于懒加载 framer-motion 的动画特性,以减少初始加载的包大小。
domMax:framer-motion 提供的一个特性集,包含了所有 DOM 相关的动画特性。
m:framer-motion 提供的一个用于创建动画元素的组件。
react-activaction
实现类似于 vue 中 keep-alive 的作用
import {Active} from "react-activaction"
dva
对 redux 做了一些封装,提供了 6 个重要api,把很多路由 redux 逻辑封装起来
app.use() 配置 hooks 或者 插件(插件最终返回的是 hooks)
app.model() 注册 model
app.unmodel()
app.replaceModel()
app.router()
app.start()
作用:
1.通信问题
2.数据流
dva = React-Router + Redux + Redux-saga
umi
基于dva, 对 react 做了 2次封装 ,开箱即用,插件繁荣
umirc.ts 或者 config/config.js 为配置文件
哪些情况下不适合:
react 16.8 以下
node 10 以下
有很强的的 webpack 自主观念,自定义 webpack配置
自定义路由方案
应用:ant design pro
Vue 和 React 的区别
1.模板渲染方式的不同
React是通过 JSX 渲染模板
Vue是通过一种 拓展的HTML语法 进行渲染
2.组件逻辑复用的方式不同
Vue 使用 mixins(混合)
React 使用 Hoc(高阶组件:高阶组件本质上是一个函数,函数内部返回一个组件)
3.vue 是双向绑定的响应式系统,数据发生变化,视图自动更新,而 react 是单向数据流,没有双向绑定,需要使用 this.setState 修改才能触发 diff 运算 ,视图更新
4.组件通信的区别
Vue中使用 provide/inject 实现跨组件的数据传递
React 中使用 context(上下文)实现跨组件的数据传递
数据流向的不同。react从诞生开始就推崇单向数据流,而Vue是双向数据流
数据变化的实现原理不同。react使用的是不可变数据,而Vue使用的是可变的数据
组件化通信的不同。react中我们通过使用回调函数来进行通信的,而Vue中子组件向父组件传递消息有两种方式:事件和回调函数
diff算法不同。react主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。Vue 使用双向指针,边对比,边更新DOM
谈一谈 vue 和 react 的区别?
(1)从编程范式的角度讲
- 在vue-loader、vue-template-compiler的支持下,vue可以采用SFC单文件组织的方式实现组件化;vue有指令,使用指令能够方便地渲染视图,vue表单是双向绑定的;vue组件是基于选项式的编程,常用选项有生命周期、计算属性、侦听器等;vue的组件库十分繁荣,自定义属性、自定义事件、自定义插槽是vue组件化的三大基础。众多社区中的vue轮子,在vue架构中被Vue.use注册即可使用。
- react的语法基础是JSX,react中没有指令,元素渲染、条件渲染、列表渲染、动态样式都是基于JSX语法的。在webpack环境中,要安装@babel/core、@babel/preset-react等,实现对JSX的编译。React表单是单向绑定的,推荐使用受控表单。组件封装可以是类组件,也可以函数式组件,其中props是React组件化的核心。
(2)从组件通信的角度讲
- 在vue组件通信中,跨组件通信的手段极其丰富且灵活,常用的通信方案有父子组件通信、ref通信、事件总线、provide/inject、parent/children、listeners/attrs、slot插槽通信等。除此之外,在vue中还可以使用vuex 或 mobx 来实现跨组件通信。总体上来讲,vue的组件通信极其灵活,自上而下、自下而上都是容易实现的;也正是因为过于灵活,这会“诱惑”开发者容易滥用通信手段,导致vue项目呈现出“易开发、难维护”的现状。
- 在react中数据是单向数据流,在组件树中数据只能自上而下地分发和传递。state是组件自有的状态数据,props是父级组件传递过来的数据。在react中最最基本的通信方案是状态提升,还有React上下文也可以实现自上而下的数据流。鉴于react这种数据流的特性,即使集成了Redux仍然会呈现出单向数据流的特征,因此React数据流更容易被管理,配合Redux一起更适合做中大型的项目开发。
(3)从底层原理的角度讲
- vue支持指令是因为背后有vue-template-compiler这个编译器的支持,把带有指令的视图模板转化成AST抽象语法树,进一步转化成虚拟DOM。vue的响应式原理是使用了 Object.defineProperty 进行了数据劫持,数据劫持发生vue组件的创建阶段,vue的响应式原理和mobx状态管理的响应式原理相似,这种响应式实现最早出现在 knockout 框架。如果要手写一个简单版本的vue,需要实现Compiler类(用于模板编译)、Watcher类(用于更新视图)、Dep类(用于依赖收集)、Observer类(用于数据劫持)、Vue类(构造函数)等。
- react自v16以后发生了很多变化,v16以后底层的“虚拟DOM”不再是简单JSON数据了,React采用了最新的Fiber(双向链表)的数据结构,作为“协调”(Diff)运算的基础数据。React背后还提供了强大的 react-reconciler 和 scheduler 库实现Fiber链表的生成、协调与调度。相比vue组件,react在较大组件方面的性能更高。如果要手写一个简易版本的React,其核心要实现以下功能,createElement(用于创建元素)、createDOM/updateDOM(用于创建和更新DOM)、render/workLoop(用于生成Fiber和协调运算)、commitWork(用于提交)等,如果还有支持Hooks,还得封闭Hooks相关的方法。
(4)从社区发展和未来展望的角度讲
- vue生态繁荣,用户基础大。vue3.0和vite的诞生给vue生态增加了新的生命力,同时也给vue开发者带来了空前的挑战。vue3.0众多新特性,以组合API、更友好地支持TS为代表,使得vue3.0的写法更加灵活。上手vue3.0并不难,但,要想写出健壮的可维护性更强的vue3.0代码,并不容易,这需要广大的前端开发者有更强大的前端基础功,对MVVM有深刻的理解和沉淀。
- react生态稳步向前,背后有强大的Facebook开发团队,从类组件编程向Hooks编程的转化指明了前进的方向。React(v18)呼之欲出,让前端开发者对React更具信心。在国内,阿里系的React开源项目繁荣,给以开发者足够的信心,至少三五年内深耕React仍然大有可为。
BEM
是 块(block)、元素(element)、修饰符(modifier)的简写 block 代表了更高级别的抽象或组件。
block__element 代表 .block 的后代,用于形成一个完整的 .block 的整体。 block--modifier 代表 .block
的不同状态或不同版本。 例如按钮状态 : &--primary {background: blue;} &--success {background:
green;}
Sass / Less / CSS-in-JS / Tailwind 的区别
Sass/Less CSS-in-JS Tailwind
编译阶段 构建时生成 CSS 运行时动态生成 构建时生成工具类
作用域 全局/局部 组件级隔离 全局工具类
学习曲线 中等(需学新语法) 高(需结合 JS) 低(记忆工具类)
生态扩展 插件丰富 依赖 JS 框架 插件系统完善
适用规模 中大型项目 组件化项目 全规模项目
webpack
如何理解 webpack?
1.它是一个构建工具,是众多流行脚手架的首要选择,也是一个打包器,作用是把前端模块编译成浏览器能识别的HTML + CSS +JS
2.在 webpack 眼中,一切皆模块
3.有四个核心概念 :
entry (入口) : 指定打包的入口文件
output (出口) : 指定打包后的文件名、文件路径(加 chunkhash 值,解决“代浏览器缓存”导致用户端代码不更新的问题)、自动清除旧的打包目录等
loader(加载器) : 用于定义模块编译的规则,在 module 中的 rules 中定义 有两个选项 test 和 use (有先后顺序,先执行的要放在后面)
一些常用的loader:
babel-loader:匹配到 .js/.jsx文件 ,利用babel 中的 preset 将语法转换成浏览器能识别的 语法
style-loader:把css样式添加到 DOM 树中
css-loader :加载 .css 文件
less-loader :用于加载 .less 文件,交给 less 编译器进行编译,less编译器会把 .less文件编译成 .css 文件
sass-loader:用于加载 .sass 文件,交给 sass编译器进行编译,sass编译器会把 .scss文件编译成 .css 文件
loader 的背后,常常需要安装对应的编译器(js--》babel ,sass,less)来编译对应的文件
plugin :解决loader无法实现的其他事
一些常用的 plugin :
HtmlWebpackPlugin(用于把打包后的js文件自动插入到index.html中)
HappyPack (开启多线程babel编译构建)
ESLintWebpackPlugin(ESLint)
配置绝对路径 (@):
resole 里面有个选项叫 alias
省略后缀文件: extensions
4.webpack 可以做哪些事:1.构建开发环境 、2.上线打包
Webpack是一个构建工具(rollup、gulp),是众多流行脚手架的首要选择。
* Webpack是一个打包器(打包工具):入口 => 过程1 => 过程2 => [...] => 出口
* 在Webpack的眼中,一切文件皆模块(这些模块有助于提高前端开发效率)
* Webpack使用各种 loaders 和 plugins,把各种模块编译打包成浏览器能识别的静态资源。
- webpack 全局安装、本地安装,这是webpack核心包,提供了一些重要API和内置插件。
- webpack-cli 全局安装、本地安装,提供了很多有用的命令行工具。
- webpack-dev-server 全局安装、本地安装,配合webpack提供本地node服务器、HMR热更新服务。
- html-webpack-plugin 用于自动合并bundle和指定的html页面
- cross-env 用于向node运行时环境中添加环境变量
- webpack-merge 用于合并webpack配置选项的。
- clean-webpack-plugin 在v4中用于自动清理掉dist中的旧代码
@babel (javaScript 编译器):
1. 预设(Presets)用于大版本语法编译
// 将 ES6 语法转换成 ES5 语法
@babel/preset-env
// 将 jsx 语法转换成浏览器能识别的语法
@babel/preset-react
// 将 ts 语法转换成浏览器能识别的语法
@babel/preset-typescript
2.插件(plugins) 弥补 Presets 无法编译某些细节语法
webpack是前端工程化一个很重要的构建工具,是众多脚手架的首要选择,同时也是一个打包器,作用就是把 前端模块 编译成浏览器能识别的 html、css、js 文件
在 webpack 眼中,一切皆模块
有四个重要核心概念 :分别是 entry、output、loaders、plugins
entry(入口,必须要有,可以配置多个) :可以在这里配置 入口文件 ,抽离vendors(第三方重要的包从业务代码抽离出来)
output(出口,必须要有,只能配置一个):可以在这里配置 打包后的 path(文件路径) 、publicPath、filename(文件名,加 chunkhash )、chunkFilename,clearr :true(打包后自动清除旧的打包文件)library、libraryTarget、chunkLoadingGlobal、globalObject、pathinfo、assetModuleFilename
loaders :用于定义模块编译规则的 在 module 中 的 rules 中 配置规则 test 和 use 字段
用于加载各种各样的文件模块,并使用相应的编译器对这些模块进行编译
常用的 loaders 有 :babel-loader 、vue-loader、less-loader、sass-loader、css-loader、style-loader
babel-loader是干嘛的呢? babel 是 JavaScript 编译器,主要用于语法编译的,他有两个核心概念:
presets(预设)和 plugins (插件)
presets(预设)是用于大版本语法编译的 :
例如 jsx语法,浏览器识别不了,就需要使用 @babel-preset-react,背后会使用 react.createElement()这个函数
ts语法使用 @babel-preset-typescript 进行编译
Es6语法使用 @babel-preset-env 将ES6语法转译成ES5语法
Vue语法使用 vue-loader ,然后配合 VueLoaderPlugin 插件
plugins(插件)弥补 presets 无法编译的细节语法:例如 装饰器语法
// 为兼容样式穿透/deep/,不使用 sass,而使用 node-sass
less-loader :项目中配置使用 less ,需要安装两个依赖:less-loader、less
less-loader主要是用于 加载 .less文件的,然后交给 less编译器 进行编译,less编译器会把 .less文件编译成 .css文件
css-loader 用于 加载 .css文件
style-loader 用于 将 css样式 添加到DOM树中
当 use 多个loader时,有先后顺序,先起作用的 loader 要写在后面
use: ['style-loader', 'css-loader', 'sass-loader']
module: {
rules: [
{
// 开发环境使用 style-loader 将 CSS 嵌入 DOM,生产环境提取 CSS 到单独的文件
test: /\.(sc|c)ss$/,
use: [
isDev && 'style-loader',
isProd && MiniCssExtractPlugin.loader,
'css-loader',
'scoped-css-loader',
{
loader: 'sass-loader',
// 为兼容样式穿透/deep/,不使用 sass,而使用 node-sass
options: {
implementation: require('node-sass'),
additionalData: '@import "~@/assets/styles/variables.scss";',
},
},
].filter(Boolean),
},
{
test: /\.(js|ts|tsx)$/,
include: paths.src,
use: [
{
loader: 'babel-loader',
options: {
// 使用默认的缓存目录读取构建结果,避免每次执行时重新编译可能产生的高性能消耗
cacheDirectory: true,
configFile: paths.babelFile,
},
},
{
// Webpack 从下到上执行,ts-loader 编译完后再把产物交由 babel-loader 处理
loader: 'ts-loader',
options: {
// 跳过 TypeScript 类型推导和检查,在 ForkTsCheckerWebpackPlugin 中执行,提高编译效率
transpileOnly: true,
// 开发环境下,启用热更新
getCustomTransformers: () => ({
before: isDev ? [ReactRefreshTypeScript()] : [],
}),
},
},
],
},
{
// 使用 Webpack 5 的资源模块 API 处理
test: /\.(bmp|png|jpe?g|gif)$/,
type: 'asset',
parser: {
// 小于 4kb 的文件,当作 inline 模块,否则作为 resource 模块
dataUrlCondition: { maxSize: 4 * 1024 },
},
},
{
// 处理svg文件,去除图片内的fill参数,使svg可以切换颜色
test: /\.svg$/,
include: paths.svgIcons,
use: [
{
loader: 'file-loader',
},
// {
// loader: 'svgo-loader',
// options: {
// plugins: [{
// name: 'removeAttrs',
// params: {
// attrs: '(fill|stroke)',
// },
// }],
// },
// },
],
},
],
},
plugins的作用是 : 解决loader无法实现的其他事情
html-webpack-plugin :用于把打包后的js文件自动插入到index.html中
eslint-webpack-plugin :配置eslint(开发环境)
webpack-bundle-analyzer :打包分析工具
mini-css-extract-plugin
除了这 4 个核心配置之外,还有一些其他配置:
mode :'development' 用于指定环境 development(开发环境) 、 production(生产环境)
devtool: 'inline-source-map', // 解决控制台报错时“行号对不住”的问题,如果不添加该选项,浏览器控制台也会出现黄色警告
devServer 开发环境的配置,可以配置 热更新、端口、是否、自动打开浏览器、定静态资源目录、代理等等
devServer: {
// 端口
port: 8080,
// 开启热更新(HMR = hot module replacement)
// 热更新,只对main.js往后的依赖才起作用
// 实际上,开启一台socket服务器,当代码发生变化时,通知客户端socket进行更新
hot: true,
// 自动打开浏览器
open: true,
// 用于指定静态资源目录(本地服务器)
contentBase: path.resolve(__dirname, '../public'),
// 跳过host检查
historyApiFallback: true,
// 当程序报错时,把错误信息覆盖到视图层之上
overlay: true,
headers: {
// 开启跨源资源共享
'Access-Control-Allow-Origin': '*',
},
// 代理
proxy: {
'/soso': {
target: 'https://c.y.qq.com',
changeOrigin: true
}
}
},
resolve :{
// 别名,例如 用于配置 绝对路径 (@)
alias :
// 用于省略后缀文件
extensions :['.tsx', '.ts', '.js', '.json'],
// 指定模块的查找路径,避免逐层查找
modules: ['node_modules', paths.nodeModules],
// 取消对符号链接位置的解析
symlinks: false,
// 复用 tsconfig.json 的 paths 别名设置
// const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
plugins: [new TsconfigPathsPlugin()],
}
-D 或 --save-dev 用于开发依赖。
-S 或 --save 用于生产依赖。
babel-loader 加载 js 文件
@babel/core 进行编译转译
vite
vite 基于原生模块 ES Modules 实现
Vite 需要 Node.js 版本 14.18+,16+
Vite 实现热更新的方式也大同小异,主要是通过创建 WebSocket 建立浏览器与服务器建立通信,通过监听文件的改变向客户端发出消息,客户端对应不同的文件进行不同的操作的更新
vite 和 webpack 的区别
Vite 先启动服务,按需加载 (并不会先编译所有的代码文件)
Webpack 则是先全部打包,再启动服务 (webpack dev server)
webpack 这类工具的做法是将所有模块提前编译、打包进bundle里,换句话说,不管模块是否会被执行,都要被编译和打包到bundle里。随着项目
越来越大,打包后的bundle也越来越大,打包的速度自然会越来越慢
vite 实现原理:利用 ES6 的 import 会发送请求去加载文件的特性,拦截这些请求,做一下预编译,省去webpack冗长的打包时间,在生产模式下,vite使用 Rollup进行打包,提供更好的tree-shaking,代码压缩和性能优化。
https://www.jianshu.com/p/34b6f7751ceb
在浏览器中直接看到上报的类型错误,可以使用 vite-plugin-checker
使用 "skipLibCheck": true 来缓解这个错误
配置上的差异:
入口配置 : Webpack是entry,而Vite是root或者index.html作为入口
Loader : Webpack需要各种loader处理文件,Vite则是利用 esbuild或Rollup插件
在Webpack中处理CSS需要用style-loader和css-loader,而Vite直接支持,只需在vite.config.js中配置css.modules
| 特性 | Webpack | Vite |
|---|---|---|
| 构建理念 | 基于静态模块分析,全量打包 | 基于浏览器原生 ESM,按需编译 |
| 开发模式 | 启动时全量打包,HMR 需重新编译模块 | 启动时仅编译入口文件,HMR 基于浏览器 ESM |
| 打包工具 | 自研打包机制 | 开发环境用 esbuild,生产环境用 Rollup |
| 开发速度 | 项目越大启动越慢,HMR 延迟高 | 秒级启动,HMR 极速响应 |
| 配置复杂度 | 高,需手动配置 Loader/Plugin | 低,大部分功能开箱即用 |
| 生态系统 | 成熟,插件生态丰富 | 较新,但兼容 Rollup/Webpack 生态 |
| 浏览器兼容性 | 通过 Polyfill 支持旧浏览器 | 默认面向现代浏览器,需插件支持旧环境 |
具体配置对比:
入口配置
Webpack 支持多入口、动态入口,需显式定义// webpack.config.js module.exports = { entry: './src/main.js', }
Vite 默认以index.html为入口,通过<script type="module">加载 JS// vite.config.js export default { root: './src', build: { rollupOptions: { input: './src/main.js', }, }, }输出配置
Webpack 需手动配置输出目录、文件名和公共路径output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', publicPath: '/', }
Vite 默认输出到dist目录,静态资源自动分类build: { outDir: 'dist', assetsDir: 'assets', sourcemap: true, }文件处理(Loader vs Transform)
Webpack(需 Loader)module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.ts$/, use: 'ts-loader' }, ], }
Vite(内置支持 + Rollup 插件)// 内置支持 CSS/TS,无需额外配置 // 扩展能力通过插件: import vue from '@vitejs/plugin-vue' export default { plugins: [vue()], }开发服务器
Webpack 在 devServer 中配置,需单独配置 hotdevServer: { port: 3000, hot: true, proxy: { '/api': 'http://localhost:8080' }, }
Vite 在 server 中配置,但 Vite 的服务器基于原生 ESM,无需配置hotserver: { port: 3000, proxy: { '/api': 'http://localhost:8080' }, }CSS 处理
Webpack 需要单独安装 loder,来处理// 需安装 loader: npm install css-loader style-loader sass-loader --save-dev
Vite 支持 CSS Modules(通过*.module.css命名约定)// 直接支持 CSS/SCSS,仅需安装预处理器: npm install sass --save-dev环境变量
webpack// 使用 DefinePlugin 或 .env 文件(需插件) plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), }), ],
vite// 自动加载 .env 文件,通过 import.meta.env 访问 // 需以 VITE_ 前缀暴露变量(如 VITE_API_KEY)生产构建
webpackmode: 'production', optimization: { splitChunks: { chunks: 'all' }, minimize: true, },
vitebuild: { minify: 'esbuild', // 或 'terser' cssCodeSplit: true, target: 'esnext', }
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
// 插件
plugins: [vue()],
// 服务器配置
server: {
port: 3000, // 指定开发服务器端口
open: true, // 自动打开浏览器
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
// 构建配置
build: {
outDir: 'dist', // 指定输出目录
sourcemap: true, // 生成 source map 文件
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue'],
},
},
},
},
// 解析配置
resolve: {
alias: {
'@': '/src', // 设置路径别名
},
},
esbuild: {
// 移除 console.log 和 debugger 语句
pure: viteEnv.VITE_DROP_CONSOLE ? ['console.log', 'debugger'] : [],
},
// global css
css: {
preprocessorOptions: {
less: {
// modifyVars: {
// "primary-color": "#1DA57A",
// },
javascriptEnabled: true,
additionalData: `@import "@/styles/var.less";`,
},
},
},
})
:::
TypeScript
TypeScript是JaveScript的超集
有利于代码的静态分析
有利于发现错误
更好的编译器支持,提供语法提示和自动补全
核心:
1.除了有 JaveScript 的一些数据类型之外,新增了很多额外的数据类型
基本:元组、枚举、any、void
高级:Never 、 Object
元组:本质上就是长度固定且每个元素的数据类型也固定的数组(元组不能越界,越界会报错)
any:任何类型
void类型:表示没有类型(例如函数没有返回值时)
枚举类型:类,最好大写
Never:不存在的值的类型
函数重载:方法名相同,参数类型、参数个数、函数返回值三者有一个不同,就是重载
接口(interface): 定义一个接口(对象、函数的类型(形状))
泛型(generics)是指在定义函数、接口或类或别名的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
接口可以继承一个类
2.更加完整的面向对象编程
特点:封装、继承、多态(多种形态)
修饰符 :
public 所修饰的属性或方法,在其他类中可以被正常访问
private 所修饰的属性或方法,在其他类中是不能被访问的
protected 所修饰的属性和方法,只能在子类中被访问
readonly 只读属性关键字,只允许出现在属性声明或索引签名中。
抽象类 : abstract 用于定义抽象类和其中的抽象方法
不允许被实例化,不能 new
抽象类中的抽象方法必须被子类实现
类实现接口 : 把共有的特性,提取成接口(interfaces),用 implements 关键字来实现
一个类可以实现多个接口
接口继承接口
接口继承类
泛型约束 : 在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法
interface Lengthwise {
length: number
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length)
return arg
}
如果一个变量要套用另一个函数类型,有一个小技巧,就是使用 typeof 运算符
function add(x: number, y: number) {
return x + y;
}
const myAdd: typeof add = function (x, y) {
return x + y;
};
keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。
enum MyEnum {
A = "a",
B = "b",
}
// 'A'|'B'
type Foo = keyof typeof MyEnum;
如果要返回 Enum 所有的成员值,可以使用 in 运算符
enum MyEnum {
A = "a",
B = "b",
}
// { a:any, b: any }
type Foo = { [key in MyEnum]: any };
typescript 中的 interface 和 type 区别
相同点:
都可以描述一个 对象 或者 函数 的类型
不同点:
type 可以声明 基本类型别名,联合类型,元组等类型 ,而 interface 不行
interface 可以继承其他类型,type 不支持继承
interface 支持 同名合并,type 不支持。
type 能使用 in 关键字生成映射类型,但 interface 不行。
复用(继承):
type Point = {
x: number;
y: number;
};
type Coordinate = Point & {
z: number;
};
interface Point {
x: number;
y: number;
};
interface Coordinate extends Point {
z: number;
}
Omit 和 Pick 分别用于排除和选择类型中的属性
微前端
概念:
微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署
作用:
拆分和细化
将这些庞大应用进行拆分,并随之解耦,每个部分可以单独进行维护和部署,提升效率。
整合历史系统
微前端可以将这些旧系统进行整合,在基本不修改来逻辑的同时来同时兼容新老两套系统并行运行
主工程加上一些子工程
iframe
Single-Spa
Qiankun
微信公众号
实际上就是可以使用 jssdk(wxsdk)的 H5页面
技术栈 :
HTML + CSS + JQ
Vue移动端
React移动端
1.订阅号(适用于个人)
一天能群发一条消息
2.服务号(适用于企业、政府或其他组织)
一个月只能发送四条消息
Git
Git 命令
git branch 查看本地所有分支
git branch -r 查看远程所有分支
git branch -a 查看本地和远程所有分支
git merge <分支名> 合并分支
git merge --abort 合并分支出现冲突时,取消合并,一切回到合并前的状态
git branch <新分支名> 基于当前分支,新建一个分支
git checkout --orphan <新分支名> 新建一个空分支(会保留之前分支的所有文件)
git branch -D <分支名> 删除本地某个分支
git push <远程库名> :<分支名> 删除远程某个分支
git branch <新分支名称> <提交ID> 从提交历史恢复某个删掉的某个分支
git branch -m <原分支名> <新分支名> 分支更名
git checkout <分支名> 切换到本地某个分支
git checkout <远程库名>/<分支名> 切换到线上某个分支
git checkout -b <新分支名> 把基于当前分支新建分支,并切换为这个分支
暂存更改
暂存当前更改: 在您当前的分支上,运行以下命令:
git stash
这将把您当前的更改(包括已暂存和未暂存的更改)保存到一个栈中,并恢复到最后一次提交的状态。
查看暂存的更改: 您可以使用以下命令查看当前的 stash 列表:
git stash list
切换分支
切换到其他分支: 现在您可以安全地切换到其他分支:
git checkout your-branch-name
恢复更改
恢复暂存的更改: 切换到目标分支后,您可以恢复之前暂存的更改:
git stash apply
这将应用最近的 stash。如果您有多个 stash,可以指定要应用的 stash,例如:
git stash apply stash@{1}
删除已应用的 stash(可选): 如果您已经成功应用了 stash,并且不再需要它,可以使用以下命令删除它:
git stash drop stash@{0}
或者,如果您想清空所有的 stash,可以使用:
git stash clear
在 Git 中,如果您已经合并了一个分支,但希望取消这个合并操作,可以根据合并的状态采取不同的措施。以下是几种常见的情况和相应的解决方法:
1. 合并尚未提交
如果您在合并分支后还没有提交(即处于合并冲突状态或只是暂时合并),您可以使用以下命令取消合并:
git merge --abort
这个命令会撤销合并操作,并将您的工作目录恢复到合并之前的状态。
2. 合并已提交
如果您已经提交了合并,您可以使用 git reset 或 git revert 来取消合并。
使用 git reset
如果您想完全撤销合并并删除合并提交,可以使用 git reset。请注意,这会丢失合并后的所有更改。
git reset --hard HEAD~1
这个命令将您的当前分支重置到上一个提交(即合并之前的状态)。请谨慎使用,因为这会丢失所有未保存的更改。
使用 git revert
如果您希望保留历史记录并仅撤销合并的更改,可以使用 git revert。这将创建一个新的提交,反转合并的更改。
git revert -m 1 <merge_commit_hash>
在这里,<merge_commit_hash> 是您要撤销的合并提交的哈希值。-m 1 表示选择主父分支(通常是合并的第一个父分支)。
3. 查看合并提交的哈希值
如果您不确定合并提交的哈希值,可以使用以下命令查看提交历史:
git log --oneline
这将显示提交的简短列表,您可以找到合并提交的哈希值。
总结
如果合并尚未提交,使用 git merge --abort。
如果合并已提交,使用 git reset --hard HEAD~1 来完全撤销合并,或使用 git revert -m 1 <merge_commit_hash> 来反转合并的更改并保留历史记录。
git rebase 和 git merge 的区别
git rebase 和 git merge 都是 Git 用来 合并分支代码 基本命令
git rebase 和 git merge 一样都是用于从一个分支获取并且合并到当前分支
使用 git merge,进行两条分支的合并,将两个分支的历史联系在一起,会产生一个额外的合并提交记录
使用 git rebase 会将整个分支移动到另一个分支上,有效地整合了所有分支上的提交,不会产生额外的合并提交记录
Git Action
CI:持续集成(Continuous Integration)
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
run: npm install
- name: Build project
run: npm run build
- name: Run tests
run: npm run test
name: 工作流的名称。
on: 触发工作流的事件。这里设置为在 main 分支的推送和拉取请求时触发。
jobs: 定义工作流中的任务。这里定义了一个名为 build 的任务。
runs-on: 指定运行任务的操作系统。这里使用 ubuntu-latest。
steps: 定义任务中的步骤。
Checkout code: 使用 actions/checkout@v2 动作检出代码。
Set up Node.js: 使用 actions/setup-node@v2 动作设置 Node.js 版本。
Install dependencies: 安装项目依赖。
Build project: 构建项目。
Run tests: 运行测试。
ci.yml:用于通用的持续集成(CI)任务,如代码构建、测试等。
cd.yml:用于持续交付(CD)任务,如部署到生产环境。
lint.yml:用于代码风格检查和静态分析。
test.yml:用于运行各种测试用例。
release.yml:用于版本发布流程。
dependency-review.yml:用于依赖项审查。
pr-checks.yml:用于Pull Request相关的检查。
移动端 300ms 延迟事件处理方法
在移动端,300ms 延迟是指在用户点击元素(如按钮)后,浏览器会等待 300ms,以便判断用户是否要进行双击操作。这种延迟可能会导致用户体验不佳,尤其是在需要快速响应的应用中。为了解决这个问题,可以采用以下几种方法:
1. 使用 touchstart 事件
可以使用 touchstart 事件替代 click 事件,因为 touchstart 事件的响应速度更快。示例代码如下:
const button = document.getElementById('myButton');
button.addEventListener('touchstart', function(event) {
// 处理点击事件
console.log('Button clicked!');
event.preventDefault(); // 防止后续的 click 事件
});
2. 使用 FastClick 库
FastClick 是一个轻量级的 JavaScript 库,可以消除移动端的 300ms 延迟。你只需在项目中引入 FastClick,并在页面加载时初始化它。
<script src="path/to/fastclick.js"></script><script>
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
</script>
3. 使用 CSS touch-action 属性
在某些情况下,可以通过 CSS 的 touch-action 属性来控制触摸行为,从而减少延迟。虽然这不是直接消除 300ms 延迟的方法,但可以帮助改善用户体验。
button {
touch-action: manipulation; /* 允许快速点击 */
}
4. 使用 Pointer Events
现代浏览器支持 Pointer Events,它可以统一处理鼠标和触摸事件。使用 Pointer Events 可以避免 300ms 延迟。
const button = document.getElementById('myButton');
button.addEventListener('pointerdown', function(event) {
// 处理点击事件
console.log('Button clicked!');
});
5. 使用 setTimeout 解决方案
如果你无法使用上述方法,可以通过 setTimeout 来延迟处理 click 事件,从而避免 300ms 延迟的影响。
const button = document.getElementById('myButton');
button.addEventListener('click', function(event) {
setTimeout(function() {
// 处理点击事件
console.log('Button clicked!');
}, 0);
});
以上方法可以有效地解决移动端 300ms 延迟的问题。根据你的项目需求和兼容性要求,可以选择最适合的方法来实现更好的用户体验。
国际化
ant design 使用 ConfigProvider 组件,全局化配置 引入对应语言包
<ConfigProvider locale="{locale}">
<Page />
</ConfigProvider>
element ui 中使用 vue-i18n@5.x ant design vue 中也可以使用 LocaleProvider 组件
Node
基于谷歌V8引擎的 jsvescript 运行时环境
事件驱动
非阻塞的IO
Express
先安装 express
cnpm i express -S
GET 请求:
/:id 使用 req.params
router.get('/getUserInfoById/:id', UserController.getUserInfoById);
?id=id 使用 req.query
router.get('/getUserInfoById', UserController.getUserInfoById);
POST 请求 使用 req.body
//创建服务器
var express = require('express')
var app = express( )
// 读取静态资源
app.use(express.static('public'))
//请求
app.get('/*',function(req,res){
res.json({err:0,msg:'succuese'})
})
//监听服务
app.listen(9002,function(){
//
})
路由
var express = require('express')
var router = express.Router()
router.post('/login', function () {
//接收入参
//如果是 GET 请求,入参放在 req.params 中
//如果是 POST 请求,入参放在 req.body 中
//返回給前端
res.json({ err: 0, msg: 'succuese' })
})
module.export = router
路由页面使用
var userRouter = require('./router/user')
// 使用中间件
//第一个参数为 前缀 ,第二个为 模块
app.use(‘/api/user’,userRouter)
MongoDB
MongoDB 集群:
https://cloud.mongodb.com/v2/6709e5d8c282206f5b0864ef#/overview
https://www.mongodb.com/zh-cn/docs/compass/current/connect/
https://zhuanlan.zhihu.com/p/347990778
账号:
edwin-wb-li
密码:
nVZFU5nwD28sDkdk
1.下载,安装
https://www.mongodb.com/try/download/community
2.配置环境变量
https://blog.csdn.net/wangcuiling_123/article/details/78591084
3..在命令行 启动 mongod (路径为 mongodb目录下的data目录 )
mongod --dbpath "F:\mongodb\data"
另开一个命令行,验证是否启动成功
mongo //如何打印版本信息则成功
然后
show dbs
*** express | koa 项目启动不了的话,切换 node 版本看看,版本不宜过高 ***
mongo shell
//进入数据库
use xxx
// 查看当前运行端口
db.getMongo()
// 增
db.xxx.inserOne()
db.xxx.inserMany()
// 删
db.xxx.deleteOne()
db.xxx.deleteMany()
// 查
db.xxx.find() // 查出所有
db.xxx.find().count().then(()=>{}) // .count()获取 total
db.xxx.findOne()
db.xxx.findById()
db.user.find().limit(5) //只查询前5条记录
db.user.find().skip(10) //查询10条以后的所有数据
db.user.find().skip(5).limit(5) //查询第6~10条记录
// 改
db.xxx.updateOne({},{$set:{}})
db.xxx.updateMany({},{$set:{}})
在项目中使用,写接口
cnpm i mongoose -S
连接 数据库
// connect.js
const mongoose = require('mongoose')
//连接数据库中的 gp5
mongoose.connect('mongodb://localhost:27017/gp5', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
const db = mongoose.connection
//验证是否成功
db.once('open', () => {
console.log('连接成功')
})
db.on('error', () => {
console.log('连接失败')
})
在页面中直接 require 进来就行
require(./connect.js)
编写增删改查接口
var express = require('express')
// 引入 mongoose
var mongoose = require('mongoose')
//增删改查操作的对象是集合
//定义一个 Schema
var userSchema = new mongoose.Schema({
user: String,
age : Number,
class:String
})
// 第一个参数为 表的名字(一定要是复数),第二个为 Schema
var userModel = mongoose.model('users',userSchema)
var router = express.Router()
//定义 GET 请求接口
router.get('/list',(req,res)=>{
// 从 req.query 中 ,取出 GET 请求的 入参
let { page,size } = req.query
page = parseInt( page || 1 )
size = parseInt( size || 10 )
userModel.find().skip((page-1)*size).limit(size).then(docs=>{
// 查询成功 ,以 json形式 返回給前端
res.json({err:0,msg:'succuese',data:docs})
}).catch(err=>{
})
})
})
//定义 POST 请求接口
router.post('/add',(req,res)=>{
// 从 req.body 中 ,取出 post 请求的 入参
let { user,age } = req.body
// 存入数据库
userModel.insertMany([{user,age}]).then(docs=>{
// 查询成功 ,以 json形式 返回給前端
res.json({err:0,msg:'添加用户成功'})
})
})
})
module.export = router
express 项目搭建
npm install -g express-generator
express --view=ejs myapp
Koa
中间件
在http请求和响应中间的处理程序
koa-router
koa-body
koa-static
洋葱圈模型
递归原理
ctx.request.query 可以简写为 ctx.query
ctx.request.body 可以简写为 ctx.body
错误处理:
1.没有通过 ctx.body 返回数据时,koa默认会帮你返回 404 错误
2.ctx.throw
3.500,运行时错误,自动抛出错误
4.中间件 koa-json-error
Nginx
在服务器查看 nginx 运行状态: systemctl status nginx
启动: sudo systemctl start nginx
重启: sudo systemctl reload nginx
开机自启动: sudo systemctl enable nginx
sudo systemctl restart nginx
nginx -t 检查配置文件是否有语法错误
nginx -s reload 热加载,重新加载配置文件
nginx -s stop 快速关闭
nginx -s quit 等待工作进程处理完成后关闭
user root;
# 工作进程的数量
worker_processes 1;
events {
worker_connections 1024; # 每个工作进程连接数
}
http {
include mime.types;
default_type application/octet-stream;
# 日志格式
log_format access '$remote_addr - $remote_user [$time_local] $host "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$clientip"';
access_log /srv/log/nginx/access.log access; # 日志输出目录
gzip on;
sendfile on;
# 链接超时时间,自动断开
keepalive_timeout 60;
server {
# 监听端口
listen 80;
# 指定服务器的IP地址或域名
server_name api.weiibn.shop;
# 指定 项目
root /var/www/express-api/public;
index index.html;
location / {
try_files $uri $uri/ @proxy_to_app;
#autoindex on; # 启用目录列表,方便调试
}
location @proxy_to_app {
proxy_pass http://127.0.0.1:3000; # 代理到 Express 应用
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# 处理静态资源
location /static/ {
alias /var/www/express-api/static/;
autoindex on; # 可选,启用目录列表
try_files $uri $uri/ =404;
}
# 指定404错误页面
error_page 404 /404.html;
location = /404.html {
internal;
}
}
# 引入其他的配置文件
include servers/*;
}
当你的 项目处于 root 文件夹下时,访问 静态资源错误,出现 403:
1.建议把项目 移到别的 目录夹 例如 /var/www/ 目录夹下;
2.修改nginx.conf配置,第一行改为 user root(暂不建议);
server {
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
#
# listen 443 ssl default_server;
# listen [::]:443 ssl default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
root /var/www/html;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
# pass PHP scripts to FastCGI server
#
#location ~ \.php$ {
# include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
# fastcgi_pass unix:/run/php/php7.4-fpm.sock;
# # With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# Express
server {
listen 80;
# 指定服务器的IP地址或域名
server_name api.weiibn.shop;
root /var/www/express-api/public;
index index.html;
location / {
try_files $uri $uri/ @proxy_to_app;
#autoindex on; # 启用目录列表,方便调试
}
location @proxy_to_app {
proxy_pass http://127.0.0.1:3000; # 代理到 Express 应用
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# 处理静态资源
location /static/ {
alias /var/www/express-api/static/;
autoindex on; # 可选,启用目录列表
try_files $uri $uri/ =404;
}
# 指定404错误页面
error_page 404 /404.html;
location = /404.html {
internal;
}
}
# Nest
server {
listen 80;
# 指定服务器的IP地址或域名
server_name nest-api.weiibn.shop;
root /var/www/nest-api/public;
index index.html;
location / {
try_files $uri $uri/ @proxy_to_app;
#autoindex on; # 启用目录列表,方便调试
}
location @proxy_to_app {
proxy_pass http://127.0.0.1:3010; # 代理到 Nest 应用
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# 处理静态资源
location /static/ {
alias /var/www/nest-api/public/;
autoindex on; # 可选,启用目录列表
try_files $uri $uri/ =404;
}
# 指定404错误页面
error_page 404 /404.html;
location = /404.html {
internal;
}
}
# Angular
server {
listen 80;
# 指定服务器的IP地址或域名
server_name angular.weiibn.shop;
root /var/www/angular-antd-express-admin;
index index.html;
location / {
try_files $uri $uri/ /index.html;
#autoindex on; # 启用目录列表,方便调试
}
# 处理静态资源
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
try_files $uri =404; # 尝试访问静态资源
expires 30d; # 设置缓存过期时间
add_header Cache-Control "public, max-age=2592000"; # 设置缓存控制
}
location /api/ {
proxy_pass http://api.weiibn.shop; # 代理
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# CORS headers
add_header 'Access-Control-Allow-Origin' '*' always; # 允许所有域名访问
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; # 允许的请求方法
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, x-requested-with' always; # 包含 x-requested-with # 允许的请求头
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, x-requested-with' always; # 包含 x-requested-with
add_header 'Content-Length' 0;
return 204; # 返回204 No Content
}
}
# 处理静态资源
#location /imgs/ {
# alias /var/www/angular-antd-express-admin/imgs;
# autoindex on; # 可选,启用目录列表
# try_files $uri $uri/ =404;
#}
# 指定404错误页面
error_page 404 /404.html;
location = /404.html {
internal;
}
}
# Angular
server {
listen 80;
# 指定服务器的IP地址或域名
server_name weiibn.shop;
root /var/www/angular-antd-express-admin;
index index.html;
location / {
try_files $uri $uri/ /index.html;
#autoindex on; # 启用目录列表,方便调试
}
# 处理静态资源
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
try_files $uri =404; # 尝试访问静态资源
expires 30d; # 设置缓存过期时间
add_header Cache-Control "public, max-age=2592000"; # 设置缓存控制
}
location /api/ {
proxy_pass http://api.weiibn.shop; # 代理
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# CORS headers
add_header 'Access-Control-Allow-Origin' '*' always; # 允许所有域名访问
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; # 允许的请求方法
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, x-requested-with' always; # 包含 x-requested-with # 允许的请求头
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, x-requested-with' always; # 包含 x-requested-with
add_header 'Content-Length' 0;
return 204; # 返回204 No Content
}
}
# 处理静态资源
#location /imgs/ {
# alias /var/www/angular-antd-express-admin/imgs;
# autoindex on; # 可选,启用目录列表
# try_files $uri $uri/ =404;
#}
# 指定404错误页面
error_page 404 /404.html;
location = /404.html {
internal;
}
}
# Docs
server {
listen 80;
# 指定服务器的IP地址或域名
server_name docs.weiibn.shop;
root /var/www/docs;
index index.html;
location / {
try_files $uri $uri/ /index.html;
#autoindex on; # 启用目录列表,方便调试
}
# 处理静态资源
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
try_files $uri =404; # 尝试访问静态资源
expires 30d; # 设置缓存过期时间
add_header Cache-Control "public, max-age=2592000"; # 设置缓存控制
}
# 处理静态资源
#location /imgs/ {
# alias /var/www/angular-antd-express-admin/imgs;
# autoindex on; # 可选,启用目录列表
# try_files $uri $uri/ =404;
#}
# 指定404错误页面
error_page 404 /404.html;
location = /404.html {
internal;
}
}
# Vue
server {
listen 80;
# 指定服务器的IP地址或域名
server_name vue.weiibn.shop;
root /var/www/vue;
index index.html;
location / {
try_files $uri $uri/ /index.html;
#autoindex on; # 启用目录列表,方便调试
}
# 处理静态资源
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
try_files $uri =404; # 尝试访问静态资源
expires 30d; # 设置缓存过期时间
add_header Cache-Control "public, max-age=2592000"; # 设置缓存控制
}
#location /api/ {
# proxy_pass http://api.weiibn.shop; # 代理
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_cache_bypass $http_upgrade;
# # CORS headers
# add_header 'Access-Control-Allow-Origin' '*' always; # 允许所有域名访问
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; # 允许的请求方法
# add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, x-requested-with' always; # 包含 x-requested-with # 允许的请求头
# # 处理预检请求
# if ($request_method = 'OPTIONS') {
# add_header 'Access-Control-Allow-Origin' '*' always;
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
# add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, x-requested-with' always; # 包含 x-requested-with
# add_header 'Content-Length' 0;
# return 204; # 返回204 No Content
# }
#}
# 处理静态资源
#location /imgs/ {
# alias /var/www/angular-antd-express-admin/imgs;
# autoindex on; # 可选,启用目录列表
# try_files $uri $uri/ =404;
#}
# 指定404错误页面
error_page 404 /404.html;
location = /404.html {
internal;
}
}
# React
server {
listen 80;
# 指定服务器的IP地址或域名
server_name react.weiibn.shop;
root /var/www/react;
index index.html;
location / {
try_files $uri $uri/ /index.html;
#autoindex on; # 启用目录列表,方便调试
}
# 处理静态资源
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
try_files $uri =404; # 尝试访问静态资源
expires 30d; # 设置缓存过期时间
add_header Cache-Control "public, max-age=2592000"; # 设置缓存控制
}
#location /api/ {
# proxy_pass http://api.weiibn.shop; # 代理
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_cache_bypass $http_upgrade;
# # CORS headers
# add_header 'Access-Control-Allow-Origin' '*' always; # 允许所有域名访问
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; # 允许的请求方法
# add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, x-requested-with' always; # 包含 x-requested-with # 允许的请求头
# # 处理预检请求
# if ($request_method = 'OPTIONS') {
# add_header 'Access-Control-Allow-Origin' '*' always;
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
# add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, x-requested-with' always; # 包含 x-requested-with
# add_header 'Content-Length' 0;
# return 204; # 返回204 No Content
# }
#}
# 处理静态资源
#location /imgs/ {
# alias /var/www/angular-antd-express-admin/imgs;
# autoindex on; # 可选,启用目录列表
# try_files $uri $uri/ =404;
#}
# 指定404错误页面
error_page 404 /404.html;
location = /404.html {
internal;
}
}
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
Pm2
用于 服务器上 启动 express 项目
npm i pm2 -g
配置文件:ecosystem.config.js , 在别的地方使用 process.env.xxxx来使用当前设置好的变量
当你使用 PM2 启动项目时,PM2 会优先读取 ecosystem.config.js 文件中的配置
module.exports = {
apps: [{
name: 'express-api',
script: './src/index.js',
instances: '1', 这里指定了1个实例
exec_mode: 'cluster',
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production'
}
}]
};
启动:
pm2 start ecosystem.config.js
// 在配置 文件中设置好环境变量后
pm2 start ecosystem.config.js --env development
pm2 start ecosystem.config.js --env production
保存当前服务
pm2 save
查看 运行日志
pm2 logs
停止所有:
pm2 delete all
查看当前运行实例:
pm2 list
停止应用程序:
pm2 stop app.js
1. 手动重启
如果你只是简单地修改了代码,可以使用以下命令手动重启应用程序:
pm2 restart <app_name_or_id>
2.自动监视
pm2 start app.js --watch
3.平滑重启
pm2 reload <app_name_or_id>
4.显示详细信息
pm2 show <app_name_or_id>
ecosystem.config.js、.env、.env.development .env.production
1. ecosystem.config.js
用途:主要用于配置 PM2(一个进程管理工具),用于管理 Node.js 应用程序的启动、监控和集群。
内容:通常包含应用程序的启动脚本、环境变量、实例数量、日志路径等配置。
module.exports = {
apps: [{
name: 'my-app',
script: 'dist/main.js',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 8080
}
}]
};
2. .env
用途:用于定义全局环境变量,这些变量在所有环境中(开发、测试、生产)都会被加载。
内容:通常包含一些通用的配置,如数据库连接字符串、API 密钥等。
3. .env.development
用途:用于定义开发环境(development)特定的环境变量。
内容:通常包含开发环境特有的配置,如开发服务器端口、调试信息等。
4. .env.production
用途:用于定义生产环境(production)特定的环境变量。
内容:通常包含生产环境特有的配置,如生产服务器端口、日志级别等。
关系和加载顺序
加载顺序:通常,环境变量的加载顺序是从 .env 开始,然后根据 NODE_ENV 的值加载相应的 .env.development 或 .env.production 文件。
优先级:.env.development 和 .env.production 中的变量会覆盖 .env 中的同名变量。
工具支持:许多项目使用 dotenv 或 dotenv-webpack 等库来加载这些环境变量文件。例如,在 ecosystem.config.js 中,你可以通过 env 和 env_production 来设置不同的环境变量。
Nest
$ npm i @nestjs/cli -g
$ nest new project-name
项目结构
prisam // 数据库相关
src
├─ auth // 授权登陆模块
│ ├─ auth.controller.ts
│ ├─ auth.guard.ts // 守卫
│ ├─ auth.interface.ts // 存放局部的该模块的类型声明
│ ├─ auth.module.ts
│ ├─ auth.service.ts
│ ├─ dto
│ │ ├─ sign-in.dto.ts
│ ├─ entities
│ │ └─ refresh-token.entity.ts
├─ common // 全局通用模块
| ├─ configs // 全局配置
| ├─ constants // 定义一些常量
| ├─ decorators // 全局装饰器
| ├─ filters // 全局过滤器
| ├─ interceptors // 全局拦截器
| ├─ interfaces // 全局类型声明
| ├─ services // 全局公共服务
| ├─ * // 其他
├─ utils // 工具函数, 尽量存放纯函数
├─ app.controller.spec.ts //单元测试样例
├─ app.controller.ts //带有单个基本控制器示例。
├─ app.module.ts //定义应用程序的模块结构,包括导入其他模块、控制器和服务等
├─ app.service.ts //带有单个方法的基本服务
├─ main.ts //应用程序入口文件,负责启动服务器,配置服务器选项(如端口号、全局中间件等)它使用 NestFactory 用来创建 Nest 应用实例。
my-nestjs-app/
├── src/
│ ├── app.module.ts
│ ├── main.ts
│ ├── modules/ // 用于存放各个模块
│ │ ├── auth/
│ │ │ ├── auth.controller.ts
│ │ │ ├── auth.module.ts
│ │ │ ├── auth.service.ts
│ │ ├── users/
│ │ │ ├── users.controller.ts
│ │ │ ├── users.module.ts
│ │ │ ├── users.service.ts
│ ├── common/ // 用于存放通用的守卫、拦截器、管道等
│ │ ├── guards/
│ │ ├── interceptors/
│ │ ├── pipes/
│ │ ├── validators/
│ ├── config/ // 用于存放配置文件和服务
│ │ ├── config.service.ts
│ │ ├── config.module.ts
│ ├── providers/ // 用于存放数据提供者
│ │ ├── database.provider.ts
│ ├── interfaces/ // 用于存放接口定义
│ │ ├── user.interface.ts
│ ├── dto/ // 用于存放数据传输对象
│ │ ├── create-user.dto.ts
│ │ ├── update-user.dto.ts
│ ├── enums/ // 用于存放枚举类型
│ │ ├── user-status.enum.ts
│ ├── exceptions/ // 用于存放自定义异常
│ │ ├── bad-request.exception.ts
│ ├── utils/ // 用于存放辅助函数
│ │ ├── helper.ts
│ ├── app.service.ts
│ ├── app.controller.ts
├── test/
│ ├── app.e2e-spec.ts
│ ├── jest-e2e.json
├── .env
├── .gitignore
├── jest.config.js
├── package.json
├── tsconfig.json
├── tsconfig.build.json
├── README.md
DTO(Data Transfer Object): 数据传输对象,用于在对象和 API 之间传输数据。
Guard: 守卫,用于实现权限控制和访问验证。
Module: 模块,NestJS 的基本组织单位,用于组织和管理控制器、服务等。
Service: 服务,包含主要的业务逻辑,通常被注入到控制器中。
Entity: 实体,用于定义数据库模型,通常与 ORM(对象关系映射)一起使用。
Interceptor: 拦截器在 NestJS 中是一个用 @Injectable() 装饰器注释的类,并实现 NestInterceptor 接口。拦截器用于在函数执行之前或之后执行一些操作,例如日志记录、异常处理、数据转换等。
Reflector: Reflector 主要用于元数据的反射和操作。在拦截器中,Reflector 可以用于获取方法或类上设置的自定义元数据,从而允许更灵活的操作。
Modules 是核心容器,负责组织和管理控制器和服务。
Controllers 接收 HTTP 请求并调用服务中的方法。
Services 封装业务逻辑,可以在多个控制器或服务间共享。
跨模块引用 需要通过 exports 和 imports 来实现依赖注入。
// app.module.ts
import { Module } from '@nestjs/common'
import { CatsController } from './cats/cats.controller'
import { CatsService } from './cats/cats.service'
@Module({
imports: [], // 导入其他模块
// 这里存放 定义好的 Controller ,注册控制器
controllers: [CatsController],
// 这里存放 定义好的 Service,注册服务
providers: [CatsService],
exports: [AuthService], // 导出服务供其他模块使用
})
export class AppModule {}
// app.controller.ts
import { Controller, Get } from "@nestjs/common";
// cats 为 api 为前缀
@Controller("cats")
export class CatsController {
// 具体路由路径
@Get("/getList")
// 处理函数(名称随意)
findAll(): string {
return "This action returns all cats";
}
}
// 完整路由路径为 /cats/getList
@Request(),@Req() | req |
|---|---|
@Response(),@Res() | res |
@Next() | next |
@Session() | req.session |
@Param(key?: string) | req.params/req.params[key] |
@Body(key?: string) | req.body/req.body[key] |
@Query(key?: string) | req.query/req.query[key] |
@Headers(name?: string) | req.headers/req.headers[name] |
@Ip() | req.ip |
@HostParam() | req.hosts |
| @Header('Cache-Control', 'none') | res.header() |
| @HttpCode(401) | 状态码 |
| @Redirect('https://nestjs.com', 301) | res.redirect() 重定向 |
4xx 客户端错误
| 异常类 | HTTP 状态码 | 典型使用场景 | 代码示例 |
|---|---|---|---|
BadRequestException | 400 | 参数校验失败/请求格式错误 | throw new BadRequestException('无效的用户名') |
UnauthorizedException | 401 | 未携带有效凭证(如 JWT 过期) | throw new UnauthorizedException('请重新登录') |
ForbiddenException | 403 | 权限不足(如普通用户访问管理员接口) | throw new ForbiddenException('没有操作权限') |
NotFoundException | 404 | 资源不存在(如当前代码中的 Role not found) | throw new NotFoundException(\User ID ${id} 不存在) |
ConflictException | 409 | 资源冲突(如用户名重复) | throw new ConflictException('该邮箱已被注册') |
二、5xx 服务端错误
| 异常类 | HTTP 状态码 | 使用场景 |
|---|---|---|
InternalServerErrorException | 500 | 未明确处理的意外错误(默认异常) |
ServiceUnavailableException | 503 | 服务暂时不可用(如维护模式) |
在 NestJS 中编写一个接口时,通常按照以下顺序进行
- DTO (Data Transfer Object):
- 定义数据传输对象,用于验证请求参数和响应数据的结构。
- DTO 是数据传输对象,用于在不同层之间传输数据。通常用于验证(如
class-validator提供的@IsString、@IsEmail等)和转换输入数据,确保接收到的数据符合预期格式 - 例如,创建一个
CreateUserDto来定义创建用户时所需的字段和验证规则。@IsNumber() 确保字段值是一个数字 @IsOptional() 表示该字段是可选的 @IsString() 确保字段值是一个字符串 @MinLength(4) 确保字段值的最小长度为 4 @Length(4, 16) 确保字段值的长度在 4 到 16 之间 @IsIn([1, 2, 3]) 确保字段值在 [1, 2, 3] 范围内 @IsBoolean() 确保字段值是一个布尔值 @IsArray() 确保字段值是一个数组 @ArrayNotEmpty() 确保数组不为空 @IsNotEmpty() 确保字段值不为空 ---------------------------------------------- import { ApiProperty } from '@nestjs/swagger'; import { ArrayNotEmpty, IsArray, IsBoolean, IsEmail, IsIn, IsNotEmpty, IsNumber, IsOptional, IsString, Length, MinLength, } from 'class-validator'; export class CreateUserDto { @ApiProperty({ description: '用户id', required: false }) @IsNumber() @IsOptional() id: number; @ApiProperty({ description: '用户名', required: true }) @IsString() @MinLength(4, { message: '用户名至少需要四位' }) @IsNotEmpty({ message: '用户名不能为空' }) username: string; @ApiProperty({ description: '密码', required: true }) @IsString() @Length(4, 16, { message: '密码长度4-16位' }) @IsNotEmpty({ message: '密码不能为空' }) password: string; @ApiProperty({ description: '昵称' }) @IsString() @IsOptional() nickname: string; @ApiProperty({ description: '性别' }) @IsIn([1, 2, 3], { message: '性别不合法' }) @IsNumber() @IsOptional() sex: number; @ApiProperty({ description: '手机号' }) @IsString() @IsOptional() phone: string; @ApiProperty({ description: '邮箱' }) @IsEmail() @IsOptional() email: string; @ApiProperty({ description: '用户类型', default: 2 }) @IsIn([0, 1, 2], { message: '用户类型不合法' }) @IsNumber() @IsNotEmpty({ message: '用户类型不能为空' }) userType: number; @ApiProperty({ description: '用户状态' }) @IsBoolean() @IsOptional() status: boolean; @ApiProperty({ description: '用户描述' }) @IsString() @IsOptional() description: string; @ApiProperty({ description: '用户角色', example: [1, 2, 3], type: [Number] }) @IsArray() @ArrayNotEmpty({ message: '用户角色不能为空' }) roleIds: number[]; }
- Entity:
- 实体类 是 ORM(对象关系映射)框架(如 TypeORM)中的一个概念,用于表示数据库中的表。每个实体类对应数据库中的一个表,其属性对应表中的列
- 数据库操作:用于定义数据库表结构,进行 CRUD(创建、读取、更新、删除)操作。
- 数据持久化:将应用程序中的对象状态持久化到数据库中
- 如果项目中使用了数据库,定义实体类来表示数据库中的表结构。
- 例如,创建一个
User实体类来表示用户表。Entity、Column 和 PrimaryGeneratedColumn 是 typeorm 提供的装饰器,用于定义实体类及其属性 @Entity() 装饰器标记 User 类为一个实体类,对应数据库中的一个表 @Entity() export class User {} @PrimaryGeneratedColumn() 装饰器标记 id 属性为主键,并且自动生成(通常是自增的) @PrimaryGeneratedColumn() id: number; @Column() 装饰器标记 name、email 和 password 属性为表中的列 @Column({ length: 45, default: null, unique: false, comment: '手机号', nullable: false }) phone: string; comment:添加注释,描述字段的用途 default:设置默认值 unique:确保该字段的值是不是唯一的 nullable:确保该字段能不能为 null length:设置字段的最大长度 type:设置字段的数据类型。 @ManyToOne() / @OneToMany() 用于定义一对多或多对一的关系 @ManyToOne(() => User, user => user.posts) author: User; @OneToMany(() => Post, post => post.author) posts: Post[]; @ManyToMany() / @JoinTable() 用于定义多对多的关系。 @ManyToMany(() => Tag, tag => tag.posts) @JoinTable() tags: Tag[]; @CreateDateColumn() / @UpdateDateColumn() 用于自动记录创建时间和更新时间 @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; @Unique() 用于定义唯一约束 @Entity() @Unique(['email']) export class User { @Column() email: string; } @Index() 用于创建索引 @Entity() export class User { @Column() @Index() email: string; } @BeforeInsert() / @BeforeUpdate() / @AfterInsert() / @AfterUpdate() 用于定义生命周期钩子,在插入或更新数据前后执行某些操作 @BeforeInsert() hashPassword() { this.password = hash(this.password); } ----------------------------------------------- import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; import { Logs } from './logs.entity'; import { Roles } from './roles.entity'; @Entity() export class User { @PrimaryGeneratedColumn({ comment: '用户id' }) id: number; @Column({ comment: '用户名', unique: true, nullable: false }) username: string; @Column({ comment: '密码', nullable: false }) password: string; @Column({ comment: '昵称', default: null, unique: false }) nickname: string; @Column({ comment: '性别,男性为1,女性为2,未知为3', default: 1 }) sex: number; @Column({ length: 45, default: null, unique: false, comment: '手机号' }) phone: string; @Column({ length: 45, default: null, unique: false, comment: '邮箱' }) email: string; @Column({ type: 'int', default: 2, unique: false, comment: '用户类型:0超级管理员,1管理员,2普通用户', }) userType: number; @Column({ type: 'boolean', default: true, unique: false, comment: '用户状态' }) status: boolean; @Column({ comment: '用户描述', nullable: true }) description: string; @CreateDateColumn({ comment: '创建时间' }) createtime: string; @UpdateDateColumn({ comment: '更新时间' }) //自动生成并自动更新列 updatetime: string; @OneToMany(() => Logs, (logs) => logs.user) logs: Logs[]; @ManyToMany(() => Roles, (roles) => roles.users) @JoinTable({ name: 'users_roles' }) roles: Roles[]; }
- Service:
- 定义服务层逻辑,处理业务逻辑和数据操作。
- 例如,创建一个
UserService来处理与用户相关的业务逻辑,如创建用户、查询用户等。
- Controller:
- 定义控制器,处理 HTTP 请求和响应。
- 例如,创建一个
UserController来定义路由和处理请求的方法。
- Module:
- 定义模块,将相关的控制器、服务、实体等组件组织在一起。
- 例如,创建一个
UserModule来注册UserController和UserService。
@nestjs/swagger
@ApiProperty:用于生成 Swagger 文档,描述字段的用途和是否必填,默认值是啥等......
import { ApiProperty } from '@nestjs/swagger';
......................
@ApiProperty({ description: '用户id', required: false })
nest 配置单元测试
测试用例报错:Cannot find module '@/entities/user/user.entity' from 'src/modules/user/user.service.ts'
原因:
模块别名配置缺失:@/entities/user/user.entity 使用了模块别名(alias),但你的 Jest 配置中没有设置相应的模块别名解析。Jest 默认无法识别这种路径别名。
解决方案:
1.在 Jest 配置中添加模块别名解析,需要在 jest 配置中添加 moduleNameMapper 字段来映射别名到实际路径
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": ".",
"testRegex": "src/test/.*(spec|e2e-spec)\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"src/test/**/*.(t|j)s",
"!src/test/**/*.ts"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node",
"moduleNameMapper": {
"@/(.*)": "<rootDir>/src/$1"
}
}
2.确保 TypeScript 编译器也配置了别名,在 tsconfig.json 中已经配置了路径别名,例如:
tsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
nest 部署到服务器
npm run build
把打包后的 dist | package.json | package-lock.json | .env | public | ecosystem.config.js(路径指向 /dist/main.js 文件 ) 上传到服务器
使用 pm2 启动 (可以把启动命令加到 package.json 里面)
NestJS 和 Spring 的区别
NestJS 和 Spring 在语法上有显著的区别,主要因为它们分别基于不同的编程语言和技术栈:
- 编程语言:
- NestJS:使用 TypeScript,这是一种静态类型的 JavaScript 超集,提供了类、接口、泛型等现代编程特性。
- Spring:使用 Java,一种广泛使用的面向对象编程语言。
- 模块化:
- NestJS
@Module
装饰器来定义模块,模块中可以包含控制器、服务、提供者等。typescriptimport { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule {} - Spring
使用 @Configuration 注解来定义配置类,配置类中可以包含 @Bean 方法来定义组件。javaimport org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean public MyService myService() { return new MyService(); } }
- NestJS
- 控制器:
- NestJS
:使用@Controller
装饰器来定义控制器,控制器中可以包含路由处理方法。typescriptimport { Controller, Get } from '@nestjs/common'; @Controller('api') export class AppController { @Get('hello') getHello(): string { return 'Hello World!'; } } - Spring
:使用@RestController
注解来定义控制器,控制器中可以包含@GetMapping
等注解来定义路由。javaimport org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AppController { @GetMapping("/api/hello") public String getHello() { return "Hello World!"; } }
- NestJS
- 服务:
- NestJS
:使用@Injectable
装饰器来定义服务,服务中可以包含业务逻辑。typescriptimport { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getData(): string { return 'Data from service'; } } - Spring
:使用@Service
注解来定义服务,服务中可以包含业务逻辑。javaimport org.springframework.stereotype.Service; @Service public class AppService { public String getData() { return "Data from service"; } }
- NestJS
- 依赖注入:
- NestJS:使用 @Inject 装饰器或构造函数注入来实现依赖注入。
import { Injectable } from '@nestjs/common'; import { AppService } from './app.service'; @Controller('api') export class AppController { constructor(private readonly appService: AppService) {} @Get('data') getData(): string { return this.appService.getData(); } } - Spring:使用 @Autowired 注解来实现依赖注入。
javaimport org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AppController { private final AppService appService; @Autowired public AppController(AppService appService) { this.appService = appService; } @GetMapping("/api/data") public String getData() { return appService.getData(); } }
- NestJS:使用 @Inject 装饰器或构造函数注入来实现依赖注入。
Biome
Biome 是一个快速的格式化工具
Biome 是一个高性能的 Linter
# 安装
npm install --save-dev --save-exact @biomejs/biome
MySQL
#服务器上安装 MySQL
https://blog.csdn.net/weixin_45626288/article/details/133220238
https://cn.linux-console.net/?p=30557
#查看 mysql 运行状态
sudo systemctl status mysql
#重新加载 mysql 配置
sudo systemctl restart mysql
#登录MySQL shell 验证root密码:
mysql -u root -p
#给用户远程连接权限
#测试远程连接
mysql -u root -p -h 120.77.175.75
如果遇到连接超时 :
参考下面防火墙端口配置,把端口打开
服务器防火墙(ufw && firewalld)
ufw 和 firewall-cmd 是两种常用的防火墙管理工具,分别用于不同的 Linux 发行版。以下是它们的详细介绍和使用方法:
ufw (Uncomplicated Firewall) 是 Ubuntu 和其他基于 Debian 的发行版上常用的防火墙管理工具。它旨在简化 iptables 的配置。
#安装和启用 ufw
sudo apt update
sudo apt install ufw
sudo ufw enable
#允许端口
sudo ufw allow 22 # 允许 SSH 访问
sudo ufw allow 80 # 允许 HTTP 访问
sudo ufw allow 443 # 允许 HTTPS 访问
sudo ufw allow 3306 # 允许 MySQL 访问
#每次新增端口都要重启
sudo ufw reload
#开放端口范围
sudo ufw allow 3000:9000/tcp
#拒绝端口
sudo ufw deny 21 # 拒绝 FTP 访问
#查看状态
sudo ufw status
#禁用 ufw
sudo ufw disable
#删除规则
sudo ufw delete allow 22
--------------------------------------------------------------------------
firewall-cmd 是 CentOS、RHEL 和 Fedora 上常用的防火墙管理工具,基于 firewalld 服务。
#安装和启用 Firewalld
sudo yum install firewalld
sudo systemctl start firewalld
sudo systemctl enable firewalld
#查看防火墙开放端口:
firewall-cmd --list-ports
#开启指定端口
sudo firewall-cmd --zone=public --add-port=3306/tcp --permanent
sudo firewall-cmd --permanent --add-port=22/tcp # 允许 SSH 访问
sudo firewall-cmd --permanent --add-port=80/tcp # 允许 HTTP 访问
sudo firewall-cmd --permanent --add-port=443/tcp # 允许 HTTPS 访问
sudo firewall-cmd --permanent --add-port=3306/tcp # 允许 MySQL 访问
#拒绝端口
sudo firewall-cmd --permanent --remove-port=21/tcp # 拒绝 FTP 访问
#重启
firewall-cmd --reload
#查看状态
sudo firewall-cmd --state
#禁用 Firewalld
sudo systemctl stop firewalld
sudo systemctl disable firewalld
Redis
https://blog.csdn.net/heyl163_/article/details/132981470
在 Ubuntu 上安装 Redis
1.更新系统包
sudo apt update
sudo apt upgrade -y
2.安装 Redis
sudo apt install redis-server -y
3.启动 Redis 服务并设置为开机自启动
sudo systemctl start redis-server
sudo systemctl enable redis-server
4.验证 Redis 安装
redis-cli ping
# 检查 Redis 服务是否正在运行(如果显示 active (running 则为 运行中)
sudo systemctl status redis-server
# 如果 Redis 服务未运行,可以启动它
sudo systemctl start redis-server
# 设置为开机自启动
sudo systemctl enable redis-server
# 停止 Redis 服务
sudo systemctl stop redis
# 默认端口:6379
Docker
安装下载:https://blog.csdn.net/u011278722/article/details/137673353
Docker 是一个可以将 环境与程序一起打包,并运行的软件
Docker 核心概念:
镜像(Image):只读模板,用于创建容器(如 nginx:latest)。
容器(Container):镜像的运行实例,轻量级且隔离的进程环境。
仓库(Registry):存放镜像的地方(如 Docker Hub)
Dockerfile:一个文本文件,包含构建镜像的指令(如安装依赖、复制文件等)
基础镜像(Base Image)
Docker 常用命令
# 描述了要做哪些事情,一些命令行
dockerFile
docker build -----> 才会执行 dockerFile 里面的内容,将这些 (程序和环境) 打包成一个类似于压缩包东西,统称为 容器镜像(Container Image),然后就可以将这个 容器镜像 传到任意服务器,对这个压缩包 进行 解压 (docker run),就可以同时运行环境和程序
那怎么将 容器镜像 传到那么多 服务器上呢?
docker registry (镜像仓库) ---> 通过 docker push 将镜像推到仓库,有需要就可以使用 docker pull 将镜像拉取
基于 docker registry 就可以 搭建 官方(DockerHub) 或 私人镜像
容器(Container) : 相当于独立的 应用程序 和 独立的环境,例如 使用 docker pull 拉取镜像,使用 docker run 运行,就 可以得到一个 独立的 应用程序 和 独立的 应用程序 和 独立的环境
可以在 应用操作系统上 跑多个 容器,相互之间不干扰
# 验证 Docker 安装
sudo docker --version
# 验证 docker-compose
sudo docker-compose -v
# 启动 Docker 服务并设置为开机自启动
sudo systemctl start docker
sudo systemctl enable docker
# 查看所有 docker 镜像
sudo docker images
# 运行测试容器 ( 安装docker 之后,自带的)
sudo docker run hello-world
# 重启 docker 服务
sudo systemctl daemon-reload
sudo systemctl restart docker
如果你在中国大陆,访问 Docker Hub 可能会比较慢或不稳定。你可以配置 Docker 使用国内的镜像加速器,例如阿里云的镜像加速器。
配置阿里云镜像加速器
登录到阿里云容器镜像服务控制台:https://cr.console.aliyun.com/
在左侧菜单中选择“镜像加速器”,获取你的专属加速器地址。
编辑 Docker 的配置文件 /etc/docker/daemon.json
{
"registry-mirrors": ["https://pxvsi98i.mirror.aliyuncs.com","https://docker.m.daocloud.io"]
}
docker-compose
# 新建配置文件
在文件夹中新建一个 docker-compose.yml 文件
# 启动
docker-compose up -d
docker pull 和 docker-compose 的区别
`docker pull` 和 `docker-compose` 是 Docker 生态系统中的两个不同工具,它们在使用场景和功能上有所不同。
### `docker pull`
`docker pull` 命令用于从 Docker Hub 或其他 Docker 镜像仓库中拉取镜像。这个命令只会下载镜像,不会启动容器。
#### 使用 `docker pull` 的步骤
1. **拉取 SonarQube 镜像**
```sh
docker pull sonarqube:9.9.8-community
```
2. **拉取 PostgreSQL 镜像**
```sh
docker pull postgres:13
```
3. **运行 SonarQube 容器**
```sh
docker run -d --name sonarqube -p 9000:9000 sonarqube:9.9.8-community
```
4. **运行 PostgreSQL 容器**
```sh
docker run -d --name sonarqube_db -e POSTGRES_USER=sonar -e POSTGRES_PASSWORD=sonar -e POSTGRES_DB=sonarqube postgres:13
```
5. **连接 SonarQube 和 PostgreSQL**
你需要手动配置 SonarQube 连接到 PostgreSQL 数据库,这可能需要一些额外的步骤和配置。
### `docker-compose`
`docker-compose` 是一个用于定义和运行多容器 Docker 应用程序的工具。它通过一个 YAML 文件(通常命名为 `docker-compose.yml`)来描述应用程序的服务、网络和卷。使用 `docker-compose` 可以更方便地管理和启动多个相关的容器。
#### 使用 `docker-compose` 的步骤
1. **创建 Docker Compose 文件**
在你的服务器上创建一个目录来存放 SonarQube 和 PostgreSQL 的配置文件和数据,例如 `/opt/sonarqube`。然后在该目录下创建一个 `docker-compose.yml` 文件。
```sh
mkdir -p /opt/sonarqube
cd /opt/sonarqube
```
创建 `docker-compose.yml` 文件:
```yaml
version: "3"
services:
sonarqube:
image: sonarqube:9.9.8-community
container_name: sonarqube
ports:
- "9000:9000"
environment:
- SONAR_JDBC_URL=jdbc:postgresql://db:5432/sonarqube
- SONAR_JDBC_USERNAME=sonar
- SONAR_JDBC_PASSWORD=sonar
depends_on:
- db
volumes:
- sonarqube_conf:/opt/sonarqube/conf
- sonarqube_data:/opt/sonarqube/data
- sonarqube_logs:/opt/sonarqube/logs
- sonarqube_extensions:/opt/sonarqube/extensions
db:
image: postgres:13
container_name: sonarqube_db
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
POSTGRES_DB: sonarqube
volumes:
- postgresql:/var/lib/postgresql
- postgresql_data:/var/lib/postgresql/data
volumes:
sonarqube_conf:
sonarqube_data:
sonarqube_logs:
sonarqube_extensions:
postgresql:
postgresql_data:
```
2. **启动 SonarQube 和 PostgreSQL**
在 `/opt/sonarqube` 目录下运行以下命令启动 SonarQube 和 PostgreSQL:
```sh
docker-compose up -d
```
- **`docker pull`**:
- 只下载镜像,不启动容器。
- 需要手动运行和配置容器。
- 适用于简单的单容器应用或需要手动控制容器的场景。
- **`docker-compose`**:
- 定义和运行多容器应用程序。
- 通过一个 YAML 文件描述服务、网络和卷。
- 适用于复杂的多容器应用,简化了容器的管理和配置。
- 可以通过一个命令启动、停止和管理整个应用程序的多个容器。
### 总结
如果你只是想下载 SonarQube 镜像并手动运行它,可以使用 `docker pull` 命令。但如果你需要运行一个包含多个容器的复杂应用程序(例如 SonarQube 和 PostgreSQL),使用 `docker-compose` 会更加方便和高效。`docker-compose` 允许你通过一个 YAML 文件定义和管理多个相关的容器,简化了容器的启动和配置过程。通过上述步骤,你可以选择适合你的工具来部署和管理 SonarQube 和 PostgreSQL。
Jenkins
# 可以参考这些教程
https://blog.csdn.net/qq_47072529/article/details/144031835
https://developer.aliyun.com/article/892646
https://juejin.cn/post/7107561305849987086
#需要先安装 jdk(暂不需要,docker安装的 Jenkins 镜像,自带 jdk ):
#sudo apt install openjdk-11-jdk -y
#检测 jdk 安装:
java -version
#ubutun 上使用 docker 安装
#允许你在 Docker 容器中运行 Docker 容器(貌似不运行此命令也没事)
docker image pull docker:dind
#安装指定版本
docker pull jenkins/jenkins:2.463
#安装稳定版本
docker pull jenkins/jenkins:lts
#重启 Jenkins 服务 (容器 id 可以 使用 docker ps 查看)
docker stop 容器ID
docker start 容器ID
docker run -d -p 8099:8080 -p 50099:50000 -v /usr/local/jenkins:/var/jenkins_home --name myjenkins jenkinsci/blueocean
docker run:这是 Docker 命令,用于运行一个新的容器实例。
-d:以分离模式(后台模式)运行容器。
-p 8099:8080:将主机的 8099 端口映射到容器的 8080 端口。8080 端口是 Jenkins Web 界面的默认端口。通过访问 http://your-server-ip:8099 可以访问 Jenkins Web 界面。
-p 50099:50000:将主机的 50099 端口映射到容器的 50000 端口。50000 端口是 Jenkins 用于构建代理(build agents)的默认端口。
-v /usr/local/jenkins:/var/jenkins_home:将主机的 /usr/local/jenkins 目录挂载到容器的 /var/jenkins_home 目录。这样可以持久化 Jenkins 的数据,即使容器被删除,数据也不会丢失。
--name myjenkins:为容器指定一个名称 myjenkins。这样可以方便地管理和引用该容器。
jenkinsci/blueocean:指定要运行的 Docker 镜像。jenkinsci/blueocean 是 Jenkins Blue Ocean 的官方 Docker 镜像。
# 查找秘钥,用于初始化,首次登录 Jenkins 页面
docker exec myjenkins cat /var/jenkins_home/secrets/initialAdminPassword
6ea8fdd74bb244e38f962b2d39d047d4
initialAdminPassword: 6e190b3987d6462d9c0716a2f5738908
5c75fb731350931357b878ca814b10d893edf87cd3f7bed8cc8dcf3d626ba960
以下命令是在服务器上安装 Jenkins后的相关命令,docker上安装的不适用
#查看 jenkins 是否在运行
sudo systemctl status jenkins.service
#如果 Jenkins 服务未运行
sudo systemctl start jenkins.service
#停止 Jenkins 服务
sudo systemctl stop jenkins.service
#重启 Jenkins 服务
sudo systemctl restart jenkins.service
#启用 Jenkins 服务开机自启动
sudo systemctl enable jenkins.service
#禁用 Jenkins 服务开机自启动
sudo systemctl disable jenkins.service
docker 运行 Jenkins 命令:
# 请查看当前 Jenkins 镜像版本(启动时,使用对应的镜像版本部署,我的是 2.462 版本)
docker images jenkins/jenkins
# 启动 Jenkins 容器(并映射端口)
docker run -d --restart unless-stopped -p 8099:8080 -p 50099:50000 -v /usr/local/jenkins:/var/jenkins_home --name myjenkins jenkins/jenkins:2.462
# 检查 Jenkins 容器状态
docker ps -a
# 停止 Jenkins 容器
docker stop myjenkins
# 启动已停止的 Jenkins 容器
docker start myjenkins
# 重启 Jenkins 容器
docker restart myjenkins
# 查看 Jenkins 容器日志
docker logs myjenkins
# 进入 Jenkins 容器
docker exec -it myjenkins /bin/bash
# 以 root 用户身份进入 Jenkins 容器
docker exec -it --user root myjenkins /bin/bash
# 停止 Jenkins 容器
docker stop myjenkins
# 删除 Jenkins 容器
docker rm myjenkins
# 检查容器是否已删除
docker ps -a
# 查看本地的 Jenkins 镜像版本
docker images jenkins/jenkins
#删除 Jenkins 镜像(可选)
docker rmi jenkinsci/blueocean
#删除所有停止的容器(可选)
docker container prune
#删除未使用的镜像(可选)
docker image prune
# 查看当前 myjenkins 容器的 id
docker exec -it myjenkins id
# Jenkins 面板
root 5210qqwanQQQ5210
http://120.77.175.75:8099
http://120.77.175.75:8088
# 如果访问 Jenkins 面板失败,请查看 Jenkins 是否启动中
docker ps -a
docker start myjenkins
# 部署 jenkins 一直显示 Please wait while Jenkins is getting ready to work …
https://updates.jenkins.io/update-center.json
# 开机自启动
docker run -d --restart unless-stopped -p 8099:8080 -p 50099:50000 -v /usr/local/jenkins:/var/jenkins_home --name myjenkins jenkins/jenkins:lts
# jenkinsci/blueocean
用途:
jenkinsci/blueocean 镜像包含了 Jenkins 和 Blue Ocean 插件。Blue Ocean 是一个现代化的 Jenkins 用户界面,旨在提供更好的用户体验,特别是对于流水线(Pipeline)视图。
特点:
包含 Blue Ocean 插件。
提供更现代化的用户界面。
适合需要使用 Blue Ocean 的用户。
# jenkins/jenkins:lts
用途:
jenkins/jenkins:lts 镜像是 Jenkins 的长期支持(LTS)版本。LTS 版本提供了稳定性和长期支持,适合生产环境使用。
特点:
不包含 Blue Ocean 插件(可以手动安装)。
提供稳定的 Jenkins 版本。
适合需要稳定性和长期支持的用户。
选择哪个镜像
如果你需要使用 Blue Ocean 插件:选择 jenkinsci/blueocean 镜像,它已经包含了 Blue Ocean 插件,开箱即用。
docker run -d --restart unless-stopped -p 8099:8080 -p 50099:50000 -v /usr/local/jenkins:/var/jenkins_home --name myjenkins jenkinsci/blueocean
如果你需要稳定的 Jenkins 版本:选择 jenkins/jenkins:lts 镜像,它提供了稳定的长期支持版本。如果需要 Blue Ocean 插件,可以手动安装。
docker run -d --restart unless-stopped -p 8099:8080 -p 50099:50000 -v /usr/local/jenkins:/var/jenkins_home --name myjenkins jenkins/jenkins:lts
d5d76bcce4eb43f98fdc381e16cb5894
如果是用 docker 部署的容器,都需要在 容器中,自行安装,例如 Git、Maven 等
Jenkins 页面配置 jdk
因为是在 docker 部署的 Jenkins ,所以 路径要填写 Jenkins 容器下的的路径
# 以root身份进入 Jenkins 容器
docker exec -it --user root myjenkins /bin/bash
# 查看 jdk 版本(如果已经安装)
jave -version
# 出现版本号后,可查看 jdk 安装位置
which java
# 执行 which java ,会出现 jdk 路径,取 bin 之前的路径: /opt/java/openjdk
/opt/java/openjdk/bin/java
# 在 Jenkins 页面 配置 jdk 路径
# 别名
openjdk
# JAVA_HOME
/opt/java/openjdk
Jenkins 页面配置 Maven
# 以root身份进入 Jenkins 容器
docker exec -it --user root myjenkins /bin/bash
# 安装 wget
apt install -y wget
# 下载 Apache Maven 压缩包
wget https://downloads.apache.org/maven/maven-3/3.8.8/binaries/apache-maven-3.8.8-bin.tar.gz
# 解压 Apache Maven 压缩包到 /opt/ 目录
tar -xzvf apache-maven-3.8.8-bin.tar.gz -C /opt/
# 配置Jenkins密钥
docker exec -u root -it jenkins bash
# 如果没有 .ssh ,可以新建一个
cd /root/.ssh/
# 执行生成秘钥,输入后一路回车
ssh-keygen -t rsa -C "root"
# 公钥,放git仓库里面
cat id_rsa.pub
# 私钥,放jenkins里面
cat id_rsa
# id_rsa.pub 公钥内容
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDgJu9dLVD1/ksIqCH09PvAxF7PS2TvX43St4Sy1jCmLDEvJsRNWnCNCrT3csWKUMNi+WwKUPifxWHeCbm2C8uBgintHNAEwTbGN2a7erPbU6oxT9lfjPTALC4mFYBlBRhbyoHaUMrvuEMLdG74wS37I4A1FhPJEEkxcZSubac3c+E+DlW6rwxydZLZLbjigG3ZtDPUA9AvV6ifL36vucG61n+jynq8CWwYVojDI9ajJ9Nv+zP56cVuGNrrzAYYUmvXTJpVF49Xx2Wyg+ctdwKRtHvvXmcOep4ngkxw7M0yjnnx/6w3gJMxRJNS00CQ1agqwSvxqyBIaq824DC5kvySbaYHT5I4DWx5gkA1cdEUbiXt9sesgXA/yIdoU6fi7Xg24hKG/wG8cBUfoE6pG/vgTot2ulvh5UYP0TnT8YQkyJQN9CYNCMJBGfzoVZVtTYr4QpU1OQal3MjTj2qYxnoMqwpRttfGmlNZshgJto/Cn+gWqrKDaoPjbnkPe6oGhJE= root
# id_rsa 私钥内容(全部复制)
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA4CbvXS1Q9f5LCKgh9PT7wMRez0tk71+N0reEstYwpiwxLybETVpw
jQq093LFilDDYvlsClD4n8Vh3gm5tgvLgYIp7RzQBME2xjdmu3qz21OqMU/ZX4z0wCwuJh
WAZQUYW8qB2lDK77hDC3Ru+MEt+yOANRYTyRBJMXGUrm2nN3PhPg5Vuq8McnWS2S244oBt
2bQz1APQL1eony9+r7nButZ/o8p6vAlsGFaIwyPWoyfTb/sz+enFbhja68wGGFJr10yaVR
ePV8dlsoPnLXcCkbR7715nDnqeJ4JMcOzNMo558f+sN4CTMUSTUtNAkNWoKsEr8asgSGqv
NuAwuZL8km2mB0+SOA1seYJANXHRFG4l7fbHrIFwP8iHaFOn4u14NuIShv8BvHAVH6BOqR
v74E6Ldrpb4eVGD9E50/GEJMiUDfQmDQjCQRn86FWVbU2K+EKVNTkGpdzI049qmMZ6DKsK
UbbXxppTWbIYCbaPwp/oFqqyg2qD4255D3uqBoSRAAAFgAwru8EMK7vBAAAAB3NzaC1yc2
EAAAGBAOAm710tUPX+SwioIfT0+8DEXs9LZO9fjdK3hLLWMKYsMS8mxE1acI0KtPdyxYpQ
w2L5bApQ+J/FYd4JubYLy4GCKe0c0ATBNsY3Zrt6s9tTqjFP2V+M9MAsLiYVgGUFGFvKgd
pQyu+4Qwt0bvjBLfsjgDUWE8kQSTFxlK5tpzdz4T4OVbqvDHJ1ktktuOKAbdm0M9QD0C9X
qJ8vfq+5wbrWf6PKerwJbBhWiMMj1qMn02/7M/npxW4Y2uvMBhhSa9dMmlUXj1fHZbKD5y
13ApG0e+9eZw56nieCTHDszTKOefH/rDeAkzFEk1LTQJDVqCrBK/GrIEhqrzbgMLmS/JJt
pgdPkjgNbHmCQDVx0RRuJe32x6yBcD/Ih2hTp+LteDbiEob/AbxwFR+gTqkb++BOi3a6W+
HlRg/ROdPxhCTIlA30Jg0IwkEZ/OhVlW1NivhClTU5BqXcyNOPapjGegyrClG218aaU1my
GAm2j8Kf6BaqsoNqg+NueQ97qgaEkQAAAAMBAAEAAAGATlM8nBYMLtErffxmVTbPy8WyjY
P+YqjEoYeQHn+64y4ZB61ntdHu91Hw93CK93COPcUDTD2MCyRJNPG4C5VmiLwnk1vYknjD
f5L3WnafZ3n+RBnvTrSuB4LIo3Uq9uHHZ7fFFTjbGNYN03BdoJ28R//ZOnOa+UywDs9uEV
9uqXFcGTegHEozssFDzZDtBHS1LfgKjkDA2/IpEWrivVvPBs8SLfVAWvl95KlEQ8//neZ6
OyyPtnnKC9UylmemD5Sow9UjUMGXF9RJzys/jCyZLpytREaCM3NbAFBdZRJJ9jrPHiuxBH
h7ElaPAt7WpONlT7nGWYkEwo+/IEnbZ/eT64+hsvDr12wwxgRA80S7HauHLXXalXrCppVB
7zteI4ukg8S9XTEZwSlFg6SvVuKD3QMQrLVKsaJ7SMQlAa1J+wUfMHq+ViF6C89lFvuPAX
sQcZQnN5AsWKee5nprFwsoBhuuJlVoTXhSyCNM0+87F4y+NSWKEerRx2Osn8oAK4rjAAAA
wQCw+xm6VdU7kbtTP8ngZSyIbamiZ0WuvTGY3EH8TkKPVwMlx70rvnVm1pbmDq0umyEozV
vRWcKGNWHn8/Pd+oEF7HA90IpJhpofxcuVoCZcWkWR98GDU9HKVI48VbJNb4RLfy7N00Ey
fMOXlwAMvNrOwNkbosbTqywuJRXhXk8uU1JekfCzKJ/kkJQUfRxL0ZOS0HpvcE8APbzi/C
jPdxQ0igL+NQ56WMLDh+cdTij2iu4TnMJbz+1iWf7POgOP/Q8AAADBAPOBF6btZ/ROX6K2
vepkhJmQljR3D47SBwOeIXdOq6T0Y6BdMN13ZyF+WRIdGpFgPhIYdmWh2v7ULVI+pbqJ8b
hG2g2/s6tbzLJhklrb670tsPiiSNgC90IwHKB/VbKsHuOQ+kAJvkBzhx2oN49yPHkctrd0
QRKaJgW9AuiGpMAuArng0RxyOdE0KDXz1vC+Vv0PSif8muwnG9Vqf+xnjiQHl3KPj/xcIw
TvEpnTiwju6nL6ePQVCDt4DCrfLo2GvwAAAMEA66edG7IPzsvNFXlYgpJeL4WEf1nlkgfA
G8yg6uvOeksITnZIzKQPN326qhWjNYNlmeEcZk8rGaiklJuZaFyzPgzeyEjWC1UcoDsRKR
HgJj4J0iT6vkcltdhmFHwOAo8NU9ElfcShgCrIXKkSt6YmoiWWkr/DwAbfuZkYBI+VAA+A
SnIbkOe71u7ywgk7Aya7gujsRRYlR1FC+NHNNNmV721kCYr0Yup5O+p/4aFgU6zvgrmVrH
S24nY1w+O4OpivAAAABHJvb3QBAgMEBQY=
-----END OPENSSH PRIVATE KEY-----
# Jenkins2.346新建项目时没有Maven项目选项
在插件管理中下载 Maven Integration
# GitHub(Jenkins-token(暂未使用)):
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
# GitHub Secret(Jenkins的 Secret Text Credentials):
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
# GitHub Personal Access Token(new)
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
# GitHub Personal Access Token
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
# Jenkins 与 GitHub + webhooks 关联 (配置自动化构建)
https://blog.csdn.net/qq_21768483/article/details/80177920
# Jenkins 与 Github 关联后,拉取的项目所在位置(仅限于我的服务器)
/usr/local/jenkins/workspace/xx
https://blog.csdn.net/qq_47072529/article/details/144031835
Jenkinsfile
Jenkinsfile 使用 Groovy 语法定义流水线
Agent 声明
指定执行环境(如 agent any 表示任意可用节点)
Stages 与 Steps
分阶段定义构建流程,每个 stage 包含具体的 steps(如构建、测试、部署)
SCM 检出逻辑
通过 checkout scm 自动拉取 GitHub 仓库代码,与 Jenkins 的 SCM 配置联动
pipeline {
agent any // 在任何可用节点上运行
tools {
nodejs 'NodeJS 20' // 使用 Jenkins 全局配置的 Node.js 名称(需提前在 Jenkins 安装 NodeJS 插件,并在系统管理>全局配置>NodeJs)
}
environment {
// ---------- 基础配置 ----------
NODE_ENV = 'production' // 默认环境变量
NPM_REGISTRY = 'https://registry.npmmirror.com' // 国内镜像加速
APP_NAME = 'my-app' // 应用名称(用于镜像命名)
DOCKER_REGISTRY = 'registry.cn-hangzhou.aliyuncs.com' // Docker 镜像仓库地址
// ---------- 敏感信息(需提前在 Jenkins Credentials 中配置) ----------
DOCKER_CREDS = credentials('docker-hub-credentials') // Docker 仓库账号密码(ID:docker-hub-credentials)
SSH_CREDS = credentials('server-ssh-key') // 服务器 SSH 私钥(ID:server-ssh-key)
}
stages {
// 阶段1:代码检出 -----------------------------------------------
stage('Checkout') {
steps {
checkout([
$class: 'GitSCM',
branches: [[name: "*/${GIT_BRANCH}"]], // 根据触发分支拉取代码
extensions: [],
userRemoteConfigs: [[
url: 'git@github.com:yourusername/your-repo.git',
credentialsId: 'github-ssh' // GitHub SSH 密钥凭据 ID
]]
])
sh 'git log -1 --pretty=%B' // 打印最新提交信息(调试用)
}
}
// 阶段2:依赖安装 -----------------------------------------------
stage('Install Dependencies') {
steps {
sh """
npm config set registry ${NPM_REGISTRY} # 使用国内镜像
npm install --prefer-offline --verbose # 优先使用本地缓存
"""
// 可选:缓存 node_modules(需安装 Jenkins Workspace Cache 插件)
// cache(includes: 'node_modules/**', path: 'node_modules', cacheId: 'node-modules')
}
}
// 阶段3:代码质量检查 --------------------------------------------
stage('Code Quality') {
parallel { // 并行执行 Lint 和测试
stage('Lint') {
steps {
sh 'npm run lint' // ESLint/TSLint 检查
}
}
stage('Unit Test') {
steps {
sh 'npm run test:cov' // 运行测试并生成覆盖率报告
junit 'test-results/*.xml' // 收集 JUnit 格式测试报告
publishHTML(target: [ // 发布 HTML 覆盖率报告
reportDir: 'coverage/lcov-report',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
}
}
}
// 阶段4:构建项目 -----------------------------------------------
stage('Build') {
steps {
// 前端项目构建(生成 dist/)
sh 'npm run build'
// Nest.js 项目构建(生成 dist/)
// sh 'npm run build'
archiveArtifacts artifacts: 'dist/**/*' // 归档构建产物(可选)
}
}
// 阶段5:打包与部署(根据项目类型选择一种方案) ----------------------
stage('Deploy') {
when {
anyOf {
branch 'main' // main 分支触发生产部署
branch 'release' // release 分支触发预发布部署
}
}
steps {
script {
// ---------- 部署方案1:Docker 容器化部署 ----------
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-hub-credentials') {
def image = docker.build("${APP_NAME}:${GIT_COMMIT}")
image.push() // 推送镜像到仓库
}
// 通过 SSH 在服务器上拉取镜像并重启容器
sshagent(['server-ssh-key']) {
sh """
ssh -o StrictHostKeyChecking=no user@server "
docker pull ${DOCKER_REGISTRY}/${APP_NAME}:${GIT_COMMIT}
docker stop ${APP_NAME} || true
docker rm ${APP_NAME} || true
docker run -d --name ${APP_NAME} -p 3000:3000 ${DOCKER_REGISTRY}/${APP_NAME}:${GIT_COMMIT}
"
"""
}
// ---------- 部署方案2:静态文件同步到服务器 ----------
/*
sshagent(['server-ssh-key']) {
sh """
rsync -avz --delete dist/ user@server:/var/www/${APP_NAME}/
ssh user@server "systemctl restart nginx"
"""
}
*/
// ---------- 部署方案3:Kubernetes 滚动更新 ----------
/*
withKubeConfig([credentialsId: 'k8s-cluster-creds']) {
sh "kubectl set image deployment/${APP_NAME} ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${GIT_COMMIT}"
}
*/
}
}
}
}
post {
always {
// 清理工作空间(可选)
cleanWs()
// 发送构建结果通知
script {
def status = currentBuild.currentResult
def message = "构建结果: ${status}\n项目: ${APP_NAME}\n分支: ${GIT_BRANCH}\n详情: ${BUILD_URL}"
// Slack 通知
slackSend channel: '#dev-ops', message: message
// 邮件通知
emailext (
subject: "[Jenkins] ${APP_NAME} 构建结果 - ${status}",
body: message,
to: 'team@example.com'
)
}
}
}
}
Jenkins 配置邮箱通知
https://www.aliuyun.com.cn/news/ask/474.html
https://1aha.com/%E9%82%AE%E7%AE%B1%E6%8E%88%E6%9D%83%E7%A0%81smtp%E6%9C%8D%E5%8A%A1%E5%99%A8/#elementor-toc__heading-anchor-5
https://blog.csdn.net/GLYX5717/article/details/119747860
def status = currentBuild.currentResult
def message = "构建结果: ${status} <br/>项目: ${APP_NAME} <br/>分支: ${GIT_BRANCH} <br/>详情: ${BUILD_URL}"
// Slack 通知
slackSend channel: '#dev-ops', message: message
// 邮件通知
// emailext (
// subject: "[Jenkins] ${APP_NAME} 构建结果 - ${status}",
// body: message,
// to: 'a15277019572@aliyun.com'
// )
(Jenkins 自动化构建邮件,无需回复!)<br/>
项目名称:$PROJECT_NAME<br/>
构建编号:第 # $BUILD_NUMBER 次构建<br/>
构建分支:${GIT_BRANCH} <br/>
触发原因:${CAUSE} <br/>
构建结果:$BUILD_STATUS <br/>
构建详情:${BUILD_URL}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>${PROJECT_NAME} - Build #${BUILD_NUMBER} - ${BUILD_STATUS}</title>
<style>
.status-success {
color: #0b610b;
}
.status-failure {
color: #cc0000;
}
.status-unstable {
color: #ffa500;
}
table {
border-collapse: collapse;
width: 90%;
margin: 20px auto;
}
td {
padding: 10px;
border: 1px solid #ddd;
}
</style>
</head>
<body>
<h2>构建通知:${PROJECT_NAME}</h2>
<!-- 构建概览 -->
<table>
<tr>
<td colspan="2" class="status-${BUILD_STATUS.toLowerCase()}">
<strong>构建状态:${BUILD_STATUS}</strong>
</td>
</tr>
<tr>
<td width="30%">项目名称</td>
<td>${PROJECT_NAME}</td>
</tr>
<tr>
<td>构建编号</td>
<td>#${BUILD_NUMBER}</td>
</tr>
<tr>
<td>触发原因</td>
<td>${CAUSE}</td>
</tr>
<tr>
<td>持续时间</td>
<td>${BUILD_DURATION}</td>
</tr>
</table>
<!-- 详情链接 -->
<h3>相关链接</h3>
<ul>
<li>构建控制台:<a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
<li>构建详情页:<a href="${BUILD_URL}">${BUILD_URL}</a></li>
<li>测试报告:<a href="${PROJECT_URL}allure">${PROJECT_URL}allure</a></li>
</ul>
<!-- 变更记录 -->
<h3>代码变更</h3>
${CHANGES_SINCE_LAST_SUCCESS, reverse=true, format="
<li>%d [%a] %m</li>
"}
<p>完整变更记录:<a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></p>
<!-- 附加信息 -->
<div style="color:#666; margin-top:20px;">
<hr />
<p>本邮件由Jenkins自动发送,请勿直接回复</p>
<p>发送时间:${BUILD_TIMESTAMP}</p>
</div>
</body>
</html>
pipeline
权限报错问题:https://blog.csdn.net/iteye_10392/article/details/136156027
Jenkins将构建生成的程序包部署到服务器的常用方法有:
1. Rsync over SSH
使用rsync命令通过SSH把程序包复制到服务器上。在Jenkins的“执行shell脚本”构建后操作中执行rsync命令,指定本地和远程服务器的程序包目录即可。
2. SFTP
使用SFTP客户端(Jenkins自带的内置SFTP服务或第三方插件提供的服务)上传程序包到服务器。在构建后操作的“Send files or execute commands over SSH”中填写远程服务器信息和程序包目录,上传文件即可。
3. SSH 执行命令
直接在“执行shell脚本”中使用scp或sftp命令上传程序包。或者使用ssh登录到服务器,并使用tar/unzip解压命令部署程序包。
4.FTP 上传
如果服务器支持FTP服务,可以使用Jenkins自带的FTP功能上传程序包。在“Send files or execute commands over FTP”构建后操作中指定服务器信息,上传文件。
5. API 部署
如果有发布程序包的API,可以在构建后调用API将程序包部署到服务器。通过“Invoke Webhook”或“Invoke REST API”等插件来调用部署API完成上传。
除此之外,更高级的方法还有:
使用Ansible、SaltStack等编排工具自动化部署程序包。
使用容器(Docker等)来打包应用和环境,然后在另一个Jenkins流水线中拉取并启动容器。
使用懒加载的中间件、CDN来部署程序包。服务启动时才会将程序包拉取运行。
所以,综上Jenkins主要通过以下方式将构建的程序包部署到服务器:
- rsync+SSH
- SFTP上传
- SSH执行命令
- FTP上传
- 调用部署API
- 使用自动化运维工具来部署
- 构建Docker镜像,并在其他流水线中运行
根据自己的实际情况选择最适合的方案,可以实现Jenkins的CI与CD,完成从代码到部署上线的全流程自动化
SonarCloud
https://sonarcloud.io/organizations/edwin-wb-li/projects
# project Key
edwin-wb-li
# organization name
Edwin-WB-Li
# SONAR_TOKEN
561a1800153b7cc85c8bc6d1298e910ece1327e9
postgres
# docker 安装 postgres
docker pull postgres
# 运行 , 默认运行在 5432 端口
docker run -d --name sonarqube_db -e POSTGRES_USER=sonar -e POSTGRES_PASSWORD=5210qqwanQQQ5210 -e POSTGRES_DB=sonarqube -v /opt/postgresql/data:/var/lib/postgresql/data postgres
# 秘钥
eda85b31815b8ab52cb0906f1fe27323e0a8131eca4dcd94ff66e6528524eede
sonarqube
# 拉取镜像
docker pull sonarqube:9.9.8-community
# 运行 sonarqube 容器
docker run -d --name sonarqube -p 9000:9000 -e SONAR_JDBC_URL=jdbc:postgresql://sonarqube_db:5432/sonarqube -e SONAR_JDBC_USERNAME=sonar -e SONAR_JDBC_PASSWORD=5210qqwanQQQ5210 --link sonarqube_db -v /opt/sonarqube/conf:/opt/sonarqube/conf -v /opt/sonarqube/data:/opt/sonarqube/data -v /opt/sonarqube/logs:/opt/sonarqube/logs -v /opt/sonarqube/extensions:/opt/sonarqube/extensions sonarqube:9.9.8-community
docker run -d --name sonarqube \
-p 9000:9000 \
-e SONAR_JDBC_URL=jdbc:postgresql://sonarqube_db:5432/sonarqube \
-e SONAR_JDBC_USERNAME=sonar \
-e SONAR_JDBC_PASSWORD=sonar \
--link sonarqube_db \
-v /opt/sonarqube/conf:/opt/sonarqube/conf \
-v /opt/sonarqube/data:/opt/sonarqube/data \
-v /opt/sonarqube/logs:/opt/sonarqube/logs \
-v /opt/sonarqube/extensions:/opt/sonarqube/extensions \
sonarqube:9.9.8-community
# 秘钥
ba8af9cc52d5500c317a93d44312ca3564904543f35f1e2c2d3b91bdf7a37ca6
d66544a27ad196cf01c153719224e8bf8d7c0483198f04eac01a32c406c3febc
6ed0ef84b769305970d484dedf53caae60715d9f5e4a916795efead14797ee92
查看服务器所占用的内存
# ubutubu 系统。
ps aux --sort=-%mem | head -n 10
# 或者
free -h
查看服务器所安装的工具所在位置
which node
服务器上查看某个目录的权限及所有权
# 查看目录本身的权限和所属用户/组
ls -ld /usr/local/jenkins/
drwxr-xr-x 5 1000 1000 4096 May 10 10:00 /usr/local/jenkins/
drwxr-xr-x:权限标识(d 表示目录,后续为所有者、组、其他用户的权限)。
1000 1000:所有者用户 UID 和组 GID。
/usr/local/jenkins/:目标目录。
# 查看当前用户的 UID 和 GID (进入该目录直接输入id)
id
uid=0(root) gid=0(root) groups=0(root),987(docker)
Vim
进入插入模式:按 i
删除:d
退出插入模式:按 Esc
退出:先按 ESC ,然后按 : 就能输入命令
保存并退出::wq
退出不保存::q!
删除当前行:dd
撤销:u
重做:Ctrl + r
权限控制
一、路由级权限控制 (制用户能否访问特定页面(路由))
1.静态路由表 + 角色过滤 (根据用户角色动态生成可访问的路由列表)
2.动态路由加载 (登录后从后端获取权限路由配置,动态添加路由)
优点:
直接阻止未授权页面访问,安全性较高。
适用于SPA(单页应用)的页面级权限管理。
缺点:
动态路由实现复杂,需处理路由懒加载和状态同步。
刷新页面时需重新获取权限数据,可能短暂出现路由丢失。
二、组件级权限控制(控制用户能否看到某个组件)
1.高阶组件(HOC)封装
2.自定义Hook
优点:
细粒度控制到组件层级,灵活性高。
代码复用性强,逻辑集中管理。
缺点:
若滥用可能导致组件树嵌套过深。
需注意权限数据更新的同步问题(如用户登出后权限未及时清除)。
三、 按钮/操作级权限控制
目标:控制用户能否执行特定操作(如按钮显示/隐藏)
1.封装权限组件
2.指令式控制(Vue示例)
优点:
最小粒度控制,精准管理用户操作。
提升用户体验,避免用户看到不可操作的UI元素。
缺点:
若权限规则复杂,可能导致模板中大量权限判断代码。
仅隐藏UI元素不够安全,需后端接口配合校验。
四、 数据权限控制
目标:控制用户能看到哪些数据(如列表字段、数据范围)。
1.根据权限过滤接口数据
2.后端返回字段级权限标记
优点:
实现复杂业务场景下的数据可见性控制。
结合后端可做到字段级权限管理。
缺点:
数据过滤在前端可能泄露敏感信息(需后端配合过滤更安全)。
逻辑复杂度高,维护成本大。
五、 后端配合的权限校验
核心原则:前端权限控制仅为体验优化,必须依赖后端校验。
1.接口权限校验(后端接口根据用户Token或角色返回401/403状态码、前端统一拦截无权限请求)
2.角色/权限模型(RBAC/ABAC)
RBAC(基于角色的访问控制):用户关联角色,角色关联权限。
ABAC(基于属性的访问控制):通过用户属性(部门、职位等)动态计算权限。
Node 区分开发环境和生产环境
cross-env 和 dotenv 是两个不同的 npm 包,它们在 Node.js 项目中用于不同的目的,但有时会一起使用以实现环境变量的管理。以下是它们的关系和区别:
dotenv:
用途:用于加载.env文件的库,读取.env文件中的环境变量并注入到 process.env 中,通过 process.env.xxx 访问,在Node项目中确实需要 dotenv 来加载.env文件
使用场景:通常用于开发环境中,将敏感信息或配置项存储在 .env 文件中,而不是硬编码在代码里。
require('dotenv').config(); // 默认加载根目录的.env文件
// 或指定路径
require('dotenv').config({ path: '.env.development' });
cross-env:
用途:设置和使用跨平台的环境变量。
使用场景:确保在不同操作系统(如 Windows、macOS、Linux)上都能正确设置环境变量,特别是在 npm 脚本中。
关系:
cross-env
1. 安装 cross-env
cnpm i cross-env -D
cnpm i webpack-merge
2. 可以在命令行设置环境命令
"start:dev": "cross-env NODE_ENV=development nodemon ./src/index.js",
"start:prod": "cross-env NODE_ENV=production nodemon ./src/index.js",
3. 项目启动后,可以通过 process.env.NODE_ENV 判断 环境
console.log('环境', process.env.NODE_ENV)
配合使用:
在某些情况下,你可能希望在 npm 脚本中使用 cross-env 来设置一些环境变量,同时使用 dotenv 来加载 .env 文件中的变量。
例如,在 package.json 中:
"scripts": {
"start": "cross-env NODE_ENV=production node server.js"
}
同时在 server.js 中使用 dotenv 加载其他环境变量:
require('dotenv').config();
console.log(process.env.SOME_VARIABLE_FROM_DOTENV);
独立使用:
它们也可以独立使用,具体取决于你的项目需求。如果你只需要加载 .env 文件中的变量,那么只需要 dotenv;如果你需要在不同平台上设置环境变量,那么可以使用 cross-env。
总结来说,cross-env 主要用于跨平台设置环境变量(packsjson中,设置npm 脚本),而 dotenv 用于从文件中加载环境变量。两者可以结合使用,也可以根据需要单独使用。
BMap
1.注册
2.申请密钥
3.在 index.html中,以 cdn 方式引入,加密钥
4.设置容器,设宽高
5.new 地图实例
小程序
1.下载微信开发者工具
2.加入到小程序管理后台中的项目成员中,有登录的权限
需求:
Tabbar
Navbar
登录 wx.login()
授权地理定位 scope.useLocation
获取用户信息、手机号
小程序支付 wx.requestpayment() 在小程序后台接入公司已有的微信支付账号
转发、下拉刷新、触底加载
路由跳转、路由传参
常用组件:swipe、picker、scroll-view
登录流程:
在 onLaunch ()生命周期 中使用 wx.login( ) 获取 code(有效时间5分钟) ,使用 wx.request( ),调接口 发送code ,用code换取token,...(后端),后端返回 token ,前端拿到 token存储在 storage ,以后每次发起业务请求 ,携带 token,后端再检查 token ,最后返回业务数据
生命周期:
onLoad: 首次进入页面加载时触发,可以在 onLoad 的参数中获取打开当前页面路径中的参数
onUnLoad:页面卸载时触发
onReady:页面首次渲染完成时触发
onShow: 加载完成后,后台切换到前台或重新进入页面时触发
onHide:前台切换到后台或进入其他页面时触发
onPullDownRefresh:监听用户下拉动作
onReachBottom:触底事件
onShareAppMessage:用户点击右上角转发
打包上线:
代码体积:
1.分包技术
2.图片的使用(icon类小图片不需要加速、UI界面类一定要加速)
各类通讯接口:
1.api请求
2.websocket
3.文件上传
4.文件下载
uniapp
第三方登录:
调用 uni.login({}) api ,传入对应的参数 ,拿到 openid
支付:
调用 uni.requestPayment
# uniapp简介
- uniapp:跨平台技术(一套前端代码,打包成多个平台的应用)
- uniapp:各种小程序、android app、ios app、快应用、webapp(h5)等等。
- 现在呈现的市场趋势是:很多中小型公司用uniapp做android app。(uniapp做小程序的公司比较少)
- 2018年9月,uniapp开源了(发布了)。
- 开发特点:一种是vue2风格、另一种是vue3风格(2021年8月左右)。
# 微信小程序开发
- 原生的微信小程序开发(四种文件)
- Wepy微信小程序官方的小程序框架:https://wepyjs.github.io/wepy-docs/2.x/#/
- mpvue做微信小程序:http://mpvue.com/
- Taro(v3)(react风格)
- uniapp(vue2风格)
# Uniapp知识点(文档是最好的学习资源)
- 会安装HbuilderX软件(App版本)、会使用这个软件工具。
- 会创建uniapp项目(参考官网)不建议使用命令行创建。
- 要知道项目中每个文件的作用(manifest.json这个文件特别重要,切换vue版本)。
- 安装Vuex(在uniapp中已经内置了,自已会配置目录文件、编写vuex代码)
- 理解uniapp基于配置的特点(类似微信小程序风格),比如会配置tabBar、navigator。。
- 跨平台条件编辑:https://uniapp.dcloud.io/platform
- 组件使用:uniapp内置组件、h5标签、第三方UI组件(比如uni-ui、uview-ui)。。。
- 集成sass(在uniapp官网上注册开发账号,在HbuilderX登录、在插件市场中找到sass进行安装)
- 使用 uView 组件
- uView官网:https://www.uviewui.com/
- 样式导入:https://www.uviewui.com/components/downloadSetting.html
- 小程序调试:提前安装好各种小程序开发者工具,把HBX中运行好的代码导入到开发者工具中即可。
- 华为手机驱动:https://www.hihonor.com/cn/support/suite/
- 小米手机驱动:http://www.mi.com/c/service/download
- 苹果手机驱动:https://support.apple.com/kb/DL1816?viewlocale=zh_CN&locale=zh_CN
- 手机上的“开发者设置”要打开!
- 混合开发
- 打开一个具体的APP,哪些页面是混合的(前端做的)样式复杂、长页面;如果这个页面有文件上传之类的表单,不建议使用混合。
- App和混合页面是怎么通信的?从App到混合页面使用查询参数,从混合页面向App通信使用postMessage()。
- App打包(以android为例)
- 要在dcloud平台注册一个公司账号、实名备案、创建一个uniapp的应用(得到uniapp appid)。
- 把uniapp中各种url改成线上真实的路径(如果有图片资源需要处理,也要注意图片的问题)
- 生成app证书:安装java se8环境(配环境变量),使用keytool生成签名证书。
- 打包前,在manifest.json(界面视图、源码视图),添加Logo、勾选当前uniapp中用到的功能模块、权限列表。
- 如果当前uniapp中还用到了第三方工具(地图、支付、天气预报),也都是在manifest.json中进行权限配置。
- 如果当前uniapp中还用到了dcloud官方的付费功能,都也要在manifest.json中配置。。。
- 在HBX中选中项目,点击“发行->原生App打包”,按照要求填写信息,进行原生打包。
- 在HBX中,点击“发行->查看App打包状态”,可以得到.apk包的下载地址。
uniapp 实现下了刷新:
需要用到uni.onPullDownRefresh 和 uni.stopPullDownRefresh这个两个函数
1.在pages.json文件里找到需要下拉刷新的页面pages节点,并在 style 选项中开启enablePullDownRefresh。
https://blog.csdn.net/qq_45659769/article/details/119515064
Taro
# Taro简介
- Taro:是一个跨平台的技术栈===>微信小程序、支付宝小程序、头条小程序、ReactNative(安卓App、IOS App)、QQ轻应用、WebApp(H5)
- Taro呈现的发展趋势:中小型用Taro做微信小程序。
- 2020年8月:之前的版本是Taro(v2)只支持React语法风格;之后迭代成Taro(v3)版本,支持vue(2)(3)、React、ReactHooks风格。
- Taro优势:支持跨平台开发(一套代码可以打包成多种不同的应用程序)、Taro大大地降低了小程序开发的成果。
- Taro劣势:虽然支持跨平台,但要写很多兼容性代码,导致代码臃肿、可维护性不高;Taro中技术更新比较滞后。
# Taro生态
- 为什么要学习Taro?因为很多中小型公司在用Taro做微信小程序。还有一个原因是Taro非常适合学习(Taro可以搭建出很多种不同风格架构、编程风格)。
- 两个UI构架:NutUI(v3)、TarUI(v2)
- NutUI(v3)只支持taro-vue3的开发风格
- TarUI(v2)对taro-react风格不太友好(应该用不了),建议提前使用 taro-ui@next。
- 两个框架:NervJS(京东内部研发的MVVM框架)、preact(迷你的React框架)。
- nervjs:https://nerv.aotu.io/
- preact:https://preactjs.com/
- 京东小程序。
- 和微信小程序几乎差不多。
外网面板地址: http://119.23.211.60:8888/202e2e45
内网面板地址: http://172.17.56.118:8888/202e2e45
username: fbtnbmcx
password: 3283cb5f
Electron
- 跨平台技术:做桌面应用 Electorn / NW.js
- Electorn 本质上是 Node.js + 谷歌浏览器的内核,所以它可支持 election 代码逻辑,还可以渲染 Web 页面。
- 桌面应用技术栈:Electorn 是壳子,在这个壳子里面可以使用任意的 web 前端技术(jQuery/Vue/React/Angular)
- 虽然 Electron 中有浏览器的内核,但没有 CORS 同源策略,所以在 electron 开发中,可以直接调接口。
# 搭建 Electron 环境
`
mkdir electron-app
cd electron-app
npm init
cnpm i electron -g
cnpm i electron -S
`
- mian.js 是 electron 的主线程,一般用于创建窗口。。。
- preload.js 是 electron 的渲染线程。。。
- 运行桌面应用:`electron .`
# 手动搭建 Vue(装饰器)环境
`
cnpm i webpack -D
cnpm i webpack-dev-server -D
cnpm i @babel/coo......
cnpm i vue-loader babel-load.....
cnpm i html-webpack-plugin ....
`
`
cnpm i vue vue-router vuex -S
cnpm i vue-property-decorator vuex-class -S
`
- webpack.config.js 中配置入口、出口、loaders、plugins。。。
- babel.config.js 对 babel 进行若干配置,还要支持装饰器语法。。。
- vue 装饰器编程范式,参考 src 目录。
# 环境运行
- 在 election 中区分开发和生产环境,使用到了`electron-is-dev`
- 运行项目时希望先启动 vue 本地服务,再启动桌面应用,使用到了`concurrently`
# Electorn 打包
- 首先要对前端项目进行打包(前端项目打包的注意事项和普通 web 项目打包一致)
- 进一步打包 electron 项目(打包工具推荐使用 electorn-builder)
- 打包步骤:https://github.com/electron-userland/electron-builder
- 打包注意:对 package.json 文件进行各种配置,尤其"build"配置。
- build 配置参考:https://www.electron.build/configuration/configuration#configuration
React Native
# 环境搭建
- 不建议使用 create-react-native-app
- 建议使用 react-native
```
cnpm i react-native -g
react-native init MyApp
```
# ReactNative简介
- 来自于 Facebook 这家公司
- 背后依赖于 React,所以react版本是多少,你就可以使用怎么的react语法。
- RN 本质上提供了一套移动端的UI(Widget)、移动布局样式规范、操作手机系统的API。
- RN一套代码,可以打包成android app、ios app,所以RN也是一个跨平台的APP技术。
# 从技术的角度提一下简历技巧
- 亮点(学历、专业、年龄、排版、求职方向)=>你是一个有潜力、很用心、很细心。
- 技术点(突出某一个技术栈,避免什么都会、技术点版本、开发工具)=>你是技术综合能力强、并且是一个比较专注的人。
- 项目点(对照你找的项目,把这个项目的功能点罗列出来,越多越多)=>功能难点、功能亮点
# ReactNative学习方法
- 文档教程:看react-native文档,这是最完整教程。
- 环境搭建:学习建议snack中敲代码;如果是真实开发建议用mac搭建原生环境。
- 开源代码:自已去社区中找RN的开源项目,研究代码结构(架构)。
- 引申借鉴:学会了RN,之后学Flutter也比较容易。(如果一定要建议,建议研究一下Flutter)
# 开发环境搭建
- 沙盒环境:使用 expo-cli创建项目,在手机安卓expo app,通过手机扫码进行运行和调试。
- 原生环境:要考虑是windows环境,还是mac环境。(以mac为便说明RN的开发环境搭建)
- 开发和调试android应用:Node、JDK、Android Studio、Watchman
- 开发和调试ios应用:Node、Watchman、Xcode(mac电脑上一个开发工具)和CocoaPods(包管理器)。
- expo工具介绍:
- expo-cli 对国内用户不太友好,不建议使用。
- snack:在线的expo环境,在这个网站可以编写RN代码。地址:https://snack.expo.dev/
- 如何学习RN?一边阅读RN文档,一边在snake中进行在线编程。
- RN内置组件和原生(Android/IOS)关系,在公司中原生可以编写组件,给RN使用。
- RN语法基础,完全遵从React风格(类组件、函数式组件)。
- RN中表单也是单向绑定(受控组件)。
- RN常用的列表组件有 SessionList、FlatList,支持下拉刷新、触底加载等多种功能。
- 在RN样式,建议使用 Flex布局、百分比布局,样式单位默认就是 px。如果在样式层面需要做兼容性,要配合系统API来实现。
- 在RN中,不建议使用背景图片,建议使用 ImageBackground;普通图片使用 Image 组件。
- 在RN中有一个非常重要的合成事件 onPress,在 Button、Touch* 系列组件上使用。
- 动画使用 Animated 这个API。
- 在RN开发,并不是任意的js语法都支持,没必须刻意安装@babel来支持各种语法。
- RN官方推荐使用 fetch 进行网络编程,当然你可以使用 axios。
- 以下是常见的RN面试题:
- 谈一谈你对RN的理解及其编程范式。
- 你常用的RN第三方库有哪些?
- RN中路由系统是怎么样的?
- RN环境搭建。。。。
- RN中如何实现混合开发?RN程序和H5之间如何通信?
- RN项目你遇到的难点和挑战。。。。
- RN项目的打包流程、上线流程。。。
# RN路由系统
- react-router-native (不建议使用)
- react-native-navigation(仅供参考)
- react-navigation(官方推荐):https://reactnavigation.org/
- 入门使用 react-navigation
- 配置路由容器、底部Tabbar、配置Screen路由。
```
import { NavigationContainer } from '@react-navigation/native'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
const Tab = createBottomTabNavigator()
import { createNativeStackNavigator } from '@react-navigation/native-stack'
const Stack = createNativeStackNavigator()
const Home = () => (
<Tab.Navigator>
<Tab.Screen name='Home' component={Home} />
<Tab.Screen name='Find' component={Find} />
</Tab.Navigator>
)
const App = () => (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name='Index' component={TabBar} options={{title:'首页'}} />
<Stack.Screen name='Login' component={Login} />
</Stack.Navigator>
</NavigationContainer>
)
```
- 使用路由API:凡是被Screen修饰过的组件,其props上都有route路由信息、navigation路由API。
- 在最新的react-navigation(v6),还可以使用 useNavigation、useRoute等路由Hooks进行编程。
# RN生态其它工具推荐
- 混合开发:react-native-webview
- 日期组件:react-native-datepicker 、react-native-calendars
- 设备信息:react-native-device-info
- ICON图标:react-native-vector-icons
- 温馨提示:社区中一些RN第三方包安装后,一般都要在ios目录进行链接操作`pod install`。
# Mac电脑调试ios App
- 在XCode中打开 RN 项目的 ios 目录、执行build编译。
- 在RN项目的ios目录下 `pod install` 链接ios原生依赖包。
- 在RN项目的根目录下 `npm run ios` 启动Xcode模拟器进行调试。
Next
CSR 客户端渲染(当客户端访问CSR页面时,服务端不调接口、不编译React代码,客户端接收资源代码,再执行各种副作用,在客户端完成渲染工作,这就像前后端分离的写法)
SSR 服务端渲染(当客户端访问SSR页面时,服务端先使用getServerSideProps调接口,再和React组件进行组装生成静态HTML,然后返回给客户端,客户端直接渲染,这就像传统的前端不分离的写法)
SSG 静态渲染(当执行npm run build构建打包,服务端使用getStaticPaths路由数据,再使用getStaticProps调接口,根据路由数组和React组件生成若干静态HTML页面。以npm start部署后,这个静态资源文件可以被访问!)
app 应用路由器
pages 页面路由器
public 要服务的静态资产
src 可选应用程序源文件夹
app 目录下,默认要写成 page.js
pages 目录下,默认要写成 index.js
// api
app | pages 文件夹下 ,api 文件夹下的文件, 默认要写成 route.js ,路径就是文件名
res.status(code)- 设置状态代码的函数。code必须是有效的HTTP 状态代码
res.json(body)- 发送 JSON 响应。body必须是可序列化的对象
res.send(body)- 发送 HTTP 响应。body可以是string、object或Buffer
Next.js 框架中的中间件(Middleware)是一种强大的功能,它允许你在HTTP请求到达页面或API路由之前对其进行拦截和处理
error.js 文件 主要用于定义 错误信息处理
layout.js 文件 主要用于定义 全局样式和组件、导航和页眉页脚、SEO 和元信息、权限和访问控制、响应式设计、代码分割和性能优化
用法:定义好 layout.js文件,填写内容 ,在别的组件中引入 layout 组件,用其包裹住当前组件就行
路由组:在 app | pages 文件夹下,使用()包裹住的文件,不会被添加到路由中
1.路由分组
2.静态元数据
在HTML的head元素中的meta和link标签),这对于提升搜索引擎优化(SEO)
3.动态元数据
4.
预渲染 / 静态站点生成(SSG)
如果页面使用静态生成,则页面 HTML 是在构建时生成的。这意味着在生产中,页面 HTML 是在运行 next build 时生成的。然后,该 HTML 将在每个请求中重复使用。它可以由 CDN 缓存。
要在预渲染时获取此数据,Next.js 允许你从同一文件 export(导出) 一个名为 getStaticProps 的 async(异步) 函数。该函数在构建时被调用,并允许你在预渲染时将获取的数据作为 props 参数传递给页面
不带数据的静态生成:不需要获取任何外部数据来进行预渲染。在这种情况下,Next.js 在构建期间为每个页面生成一个 HTML 文件
带数据的静态生成: 有些页面需要获取外部数据进行预渲染
1.页面 内容 取决于外部数据:getStaticProps
2.页面 paths(路径)取决于外部数据 使用 getStaticPaths 、(通常还要同时使用 getStaticProps)
动态路由
例如详情页,先使用 getStaticPaths 获取 id ,然后使用 getStaticProps 获取数据
使用场景:
渲染页面所需的数据可在用户请求之前的构建时提供
页面必须预渲染(用于 SEO)并且速度非常快 — getStaticProps 生成 HTML 和 JSON 文件,这两个文件都可以由 CDN 缓存以提高性能
getStaticProps 始终在 next build 期间运行,始终在服务器上运行,从不在客户端上运行
服务端渲染 / 动态渲染 (SSR)
页面需要预渲染经常更新的数据(从外部 API 获取)
要对 page(页面)使用服务器端渲染,你需要 export 一个名为 getServerSideProps 的 async 函数,获取数据,并将其以 props 形式传递给组件。服务器将在每次页面请求时调用此函数
如果页面使用服务器端渲染,则每个请求都会生成页面 HTML。
要对页面使用服务器端渲染,你需要 export 一个名为 getServerSideProps 的 async 函数。服务器将在每次请求时调用此函数。
例如,假设你的页面需要预渲染经常更新的数据(从外部 API 获取)。你可以编写 getServerSideProps 来获取此数据并将其传递给 Page
使用场景:
如果你需要渲染依赖于个性化用户数据或只能在请求时获知的信息的页面,则应使用 getServerSideProps。例如,authorization 标头或地理位置。
如果你不需要在请求时获取数据,或者希望缓存数据和预渲染 HTML,我们建议使用 getStaticProps。
在 getServerSideProps 内使用缓存标头 (Cache-Control) 来缓存动态响应。例如,使用 stale-while-revalidate。
export async function getServerSideProps({ req, res }) {
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
)
return {
props: {},
}
}
自动静态优化
页面中如果缺少 getServerSideProps 和 getInitialProps ,页面没有阻塞数据要求,Next.js 会自动确定页面是静态的(可以预渲染),通过将页面预渲染为静态 HTML 来自动静态优化你的页面
Angular
项目结构
开源推荐:
https://github.com/huajian123/ng-antd-admin
my-app/
├── e2e/ 端到端测试相关的文件
│ ├── src/
│ │ └── e2e.ts
│ └── tsconfig.e2e.json
├── node_modules/
├── projects/ 如果你的项目中有多个 Angular 库或应用,它们会被放在这个目录下
├── src/
│ ├── assets/
│ ├── environments/ 环境配置文件
│ ├── main.ts
│ ├── polyfills.ts 环境配置文件
│ ├── styles.css
│ ├── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ ├── browser/
│ │ └── ...
│ └── app/
│ ├── app.component.html
│ ├── app.component.spec.ts
│ ├── app.component.style
│ ├── app.component.ts
│ ├── app.module.ts //根模块,通常包含应用的根组件和其他共享模块。
│ ├── core // 文件夹通常包含了应用的核心功能和服务,
比如认证服务、HTTP 客户端拦截器、全局错误处理、应用配置等
Services:全局服务,如 HTTP 服务、认证服务等。
Interceptors:HTTP 请求和响应拦截器。
Guards:路由守卫,控制导航权限。
Models:应用的数据模型。
Pipes:全局管道,用于数据转换。
Directives:全局指令,如自定义属性或结构型指令。
Utilities:工具类和函数,如日期处理、字符串操作等。
│ │ ├── ...
│ ├── shared/ 文件夹则通常包含了可以在多个特性模块之间共享的组件和服务
这些组件和服务不是应用的核心部分,但是它们被多个特性模块复用
Components:通用组件,如按钮、模态对话框、表格等。
Services:辅助服务,如通知服务、状态管理服务等。
Pipes:通用管道,如日期格式化、货币格式化等。
Directives:通用指令,如自定义验证指令等。
Models:共享的数据模型或接口。
Utilities:共享的工具类和函数
│ │ ├── ...
│ ├── ...
│ └── index.html
├── angular.json // Angular CLI 的配置文件
├── karma.conf.js
├── package.json
├── README.md
├── tsconfig.json
└── tslint.json
组件 (xxx.component.ts)
负责 UI 的显示和逻辑处理。
处理用户交互事件。
管理局部状态。
模块 (xxx.module.ts)
用于组织和配置应用的结构。模块是用来组织组件、指令和服务等的一组集合。通过模块,你可以定义应用的结构和配置,使用 @NgModule
声明组件和其他可重用的类。
导入其他模块提供的功能。
导出模块内的组件以便在其他地方使用。
路由模块 (xxx-routing.module.ts)
用于管理应用的导航和 URL 路由规则。
导入 RouterModule 并使用 forChild 或 forRoot 方法配置路由。
导出 RouterModule 以便其他模块可以使用
基本语法
https://cloud.tencent.com/developer/article/1767882
https://angular.cn/tutorials/learn-angular/12-enable-routing
https://juejin.cn/post/7028957117839048712
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
// 判断 (控制流语句,新语法) 也可以使用 *ngIf
@if (isLoggedIn) {
<p>Welcome back, Friend!</p>
}
@else { ... }
// 循环 (控制流语句) 也可以使用 *ngFor
@for(item of array, track item.id){
{{item.name}}
// 绑定方法
<button type="button" (click)="deleteHero(hero)">{{ hero.name }}</button>
}
`,
//另一种写法(推荐)
templateUrl: './xxx.component.html',
})
export class AppComponent {
// 定义变量(可以使用 private 和 public 修饰符,不写,默认为 public)
isLoggedIn = true
}
模板变量:使用 # 定义,使用时,直接用该名称
// 定义
<input #phone placeholder="phone number" />
// 使用
<!-- phone refers to the input element; pass its `value` to an event handler -->
<button type="button" (click)="callPhone(phone.value)">Call</button>
属性绑定:将属性名称用方括号括起来
<img alt="photo" [src]="imageURL">
双向数据绑定 [(ngModel)]: 同时绑定输入和输出,常用于表单控件
import {FormsModule} from '@angular/forms';
<input [(ngModel)]="username">
事件绑定:使用圆括号语法 () 绑定事件
<button type="button" (click)="deleteHero(hero)">{{ hero.name }}</button>
插槽:
<ng-content> 与 原生 <slot> 元素类似
是一个特殊的占位符,用于告诉 Angular 在哪里渲染内容
Angular 的编译器在构建时会处理所有 <ng-content> 元素
你不能在运行时插入、删除或修改 <ng-content>
你不能向 <ng-content> 添加 指令、样式或任意属性
多个内容占位符,在 <ng-content> 中使用 select 属性,相当于起新的标签名
<ng-content select="card-body"></ng-content>
然后就可以使用这个名称作为组件 <card-body />
管道(Pipes)
类似于 Vue2 中的 过滤器
管道是用于在模板中转换数据的函数。一般来说,管道是不会产生副作用的「纯」函数
管道是一种用于将数据转换为特定格式的机制,例如日期、货币和百分比、大写、小写
import { DatePipe } from '@angular/common';
@Component({
imports: [ DatePipe ]
})
<div class="left-timestamp">{{ message.timestamp | date: 'yyyy-MM-dd HH:mm' }}</div>
// 自定义管道
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'customPipe',
pure: true // 默认为 true
})
export class CustomPipe implements PipeTransform {
transform(value: string, prefix: string): string {
return `${prefix}: ${value}`;
}
}
服务 (Services)
用于封装业务逻辑,如数据获取、认证等。
通常使用 @Injectable 装饰器定义,并通过依赖注入系统在整个应用中共享。
定义:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MyService {
constructor() { }
// 服务的方法
doSomething(): void {
console.log('Doing something...');
}
}
使用:
import { Component } from '@angular/core';
import { MyService } from './my.service';
import { Component, inject,TemplateRef,ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-my-component',
template: `
<p>Component using the service.</p>
`,
})
export class MyComponent {
// 第一种方式
constructor(private myService: MyService) {
this.myService.doSomething();
}
// 第二种方式
// 使用 inject 函数 (推荐) / @inject(装饰器)
private myService = inject(MyService)
// 模板引用,用于访问模板。
private templateRef = inject(TemplateRef)
// 视图容器引用,用于插入或清除视图。
private viewContainerRef = inject(ViewContainerRef)
......
onChangeHandle(){
this.myService.doSomething()
}
}
模块 (Modules)
Angular应用由模块组成,模块定义了一组相关的组件和服务。
使用 @NgModule 装饰器来定义模块。
模块可以导入其他模块,从而实现功能的组合和复用。
路由 (Routing)
用于处理应用内的导航和URL映射。
使用 RouterModule 和 Routes 配置路由。
依赖注入 (Dependency Injection)
Angular使用依赖注入系统来管理组件和服务的生命周期。
依赖关系在模块的 providers 数组中声明。
图片优化:
NgOptimizedImage 指令
是 Angular 15 引入的一个强大工具,可以帮助开发者轻松实现图像优化。
通过响应式图像加载、懒加载、图像格式优化、错误处理和加载指示器等功能,可以显著提升应用的性能和用户体验
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div>
<h1>NgOptimizedImage 示例</h1>
<img
src="assets/images/image-320w.jpg"
srcset="assets/images/image-320w.jpg 320w, assets/images/image-640w.jpg 640w"
sizes="(max-width: 640px) 320px, 640px"
ngOptimizedImage
error="assets/images/error-image.jpg"
loading="assets/images/loading-indicator.gif"
/>
</div>
`,
})
export class AppComponent {}
Angular 指令
1.组件指令 (Component Directives)
每个组件都有自己的视图和逻辑。组件通过 @Component 装饰器定义,并且必须至少有一个模板。
@Component({
selector: 'app-websocket-chat',
templateUrl: './websocket-chat.component.html',
styleUrls: ['./websocket-chat.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ NzInputModule]
})
2.属性型指令 (Attribute Directives)
改变现有元素的外观或行为,但不改变其结构。它们通过添加或修改DOM元素的属性或添加事件监听器来工作,可以响应数据绑定或用户交互
ngClass用于动态添加CSS类,通常以ng前缀开始,但在实际使用时可以省略这个前缀。
NgClass 添加和删除一组 CSS 类。
NgStyle 添加和删除一组 HTML 样式。
[(NgModel)] 为 HTML 表单元素添加双向数据绑定。
自定义属性型指令通常通过实现 Directive类 并使用 @Directive 装饰器来定义
3.结构型指令(Structural Directives) (v18版本已很少使用,可以使用 控制流语句)
用于改变DOM的结构,即它们可以添加、删除或移动DOM元素
*ngIf 根据条件添加或移除 DOM元素,为 true 显示,为 假或者 null 则不显示
使用 @if @else 代替
*ngFor 重复渲染一个列表的每个项,简写形式 : let item of items
<ng-container *ngFor="let item of list?.dependencies | keyvalue">
<nz-descriptions-item [nzTitle]="item.key">
{{ item.value }}
</nz-descriptions-item>
</ng-container>
使用 @for 代替
@for (theme of themes; track $index) {
.................
}
ChangeDetectorRef
当异步获取数据后,想要立即更新视图,可以使用这个,有点类似于 Vue2 中的 $nextick, react 中的 useEffect
import { inject, ChangeDetectorRef } from '@angular/core';
.........................................
private cdr = inject(ChangeDetectorRef);
........................................
// 评论列表
getCommentsList(): void {
this.commentsService
.getCommentsList()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(res => {
this.listData = res;
this.cdr.detectChanges(); // 手动触发变更检测
});
}
// 立即触发一次变更检测,适用于局部组件。
detectChanges()
// 标记当前组件及其祖先组件在下一个变更检测周期中进行检查。适用于 OnPush 策略的组件
markForCheck()
// 将当前组件从变更检测树中分离,直到重新附加。适用于性能优化,避免不必要的变更检测。
detach()
// 将当前组件重新附加到变更检测树中。
reattach()
// 检查当前组件是否有未处理的变化。如果发现未处理的变化,会抛出异常。主要用于调试。
checkNoChanges()
常见使用场景:
1.异步操作后更新视图: 使用 detectChanges()
2.OnPush 策略的组件:使用 markForCheck()
当组件使用 OnPush 变更检测策略时,只有当输入属性发生变化或事件处理器被调用时才会进行变更检测。在这种情况下,如果组件内部发生了变化,需要手动触发变更检测。
import { Component, ChangeDetectionStrategy,ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-on-push-component',
template: `<p>{{ message }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
message: string;
constructor(private cdr: ChangeDetectorRef) {
this.message = 'Initial message';
}
updateMessage() {
this.message = 'Updated message';
this.cdr.markForCheck(); // 标记组件进行变更检测
}
}
3.性能优化
在某些高性能要求的场景下,可以暂时将组件从变更检测树中分离,以避免不必要的变更检测。
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-performance-component',
template: `<p>{{ message }}</p>`
})
export class PerformanceComponent {
message: string;
constructor(private cdr: ChangeDetectorRef) {
this.message = 'Initial message';
}
startHeavyOperation() {
this.cdr.detach(); // 分离组件
// 执行耗时操作
setTimeout(() => {
this.message = 'Operation completed';
this.cdr.reattach(); // 重新附加组件
this.cdr.detectChanges(); // 手动触发变更检测
}, 5000);
}
}
Angular 生命周期
生命周期(10个):
使用生命周期时,需要先引入对应生命周期,然后 使用 implements, 实现对应的生命周期接口
这是因为生命周期钩子方法(如 ngOnInit, ngAfterViewInit 等)是定义在特定的生命周期接口中的
export class AppComponent implements OnInit {
......
ngOnInit(): void {
this.analytics.trackPageViews();
this.seoService.trackCanonicalChanges();
}
}
constructor --> ngOnChanges --> ngOnInit(只调用一次) --> ngDoCheck --> ngAfterContentInit(只调用一次) --> ngAfterContentCheckd --> ngOnViewInit(只调用一次) --> ngOnViewChecked --> ngOnDestroy
-----------------------------------------------------------------------------------------
ngOnChanges()
作用:当组件的输入属性(通过 @Input() 装饰器标记的属性)发生变化时调用。
调用时机:在首次渲染前,以及后续每次输入属性更新时。
@Input() purChoosePeople: LazySelPeopleEnum = LazySelPeopleEnum.YiLin;
@Input() purChooseAnimal: LazySelAnimalEnum = LazySelAnimalEnum.YiLin;
.....................................................................
ngOnChanges(changes: SimpleChanges): void {
console.log(changes[purChoosePeople]);
console.log(changes[purChooseAnimal]);
}
ngOnInit()
类似于 Vue中的 mounted() 、React 中的 useEffect()
作用:在组件初始化时调用,通常用于执行一次性的初始化操作,如订阅服务、加载数据等。
调用时机:在首次 ngOnChanges() 执行后调用,保证了所有输入属性都已经有了初始值。
ngDoCheck()
作用:自定义检测逻辑,用于检查组件内部状态的更改,可以用于检测 Angular 自身无法检测的变化。
调用时机:在每个变更检测周期中调用,即每次 Angular 运行变更检测时。
ngAfterContentInit()
作用:在组件的内容(通过 @ContentChild 或 @ContentChildren 标记的指令)初始化完成后调用。
这个钩子非常适合用于执行依赖于内容初始化的操作,包括 DOM 操作。
调用时机:在首次 ngDoCheck() 调用后,确保了组件视图中的投影内容已经可用。
ngAfterViewInit()
作用:
在组件视图及其子视图初始化完成后调用
适合于访问组件视图中的本地元素引用 (@ViewChild 和 @ViewChildren)。
调用时机:在首次 ngAfterContentInit() 调用后,确保了组件及其子组件的视图已经完全初始化。
ngAfterViewChecked()
作用:在每次变更检测周期结束后调用,可以用来检查视图是否与模型一致。
调用时机:在每次 ngDoCheck() 调用后。
ngOnDestroy()
作用:在组件即将被销毁前调用,通常用于清理资源,如取消订阅、释放定时器等。
调用时机:在组件销毁前调用。
-------------------------------------------------------------------------------------------------------------------------------
ngOnChanges()
当 Angular(重新)设置数据绑定输入属性时响应。 该方法接受当前和上一属性值的 SimpleChanges 对象,
意发生的非常频繁,所以你在这里执行的任何操作都会显著影响性能
在 ngOnInit() 之前以及所绑定的一个或多个输入属性的值发生变化时都会调用
如果组件绑定过输入属性,那么在 ngOnInit() 之前以及所绑定的一个或多个输入属性的值发生变化时都会调用。 注意,如果你的组件没有输入属性,或者你使用它时没有提供任何输入属性,那么框架就不会调用
.......... .......... .......... ..........
@Input() password: string | undefined;
ngOnChanges(changes: SimpleChanges): void {
if (changes['password']) {
this.calculatePasswordStrength();
}
}
ngOnInit()
在 Angular 第一次显示数据绑定和设置指令/组件的输入属性之后,初始化指令/组件。
在第一轮 ngOnChanges() 完成之后调用,只调用一次。[请求数据时使用]
ngDoCheck()
检测,并在发生 Angular 无法或不愿意自己检测的变化时作出反应。
在每个变更检测周期中,紧跟在 ngOnChanges() 和 ngOnInit() 后面调用。
ngAfterContentInit() 用于 DOM 操作
当 Angular 把外部内容投影进组件/指令的视图之后调用。
第一次 ngDoCheck() 之后调用,只调用一次。
在首次内容投影完成后调用。当组件使用 @ContentChild 或 @ContentChildren 时有用。
ngAfterContentChecked()
每当 Angular 完成被投影组件内容的变更检测之后调用。
ngAfterContentInit() 和每次 ngDoCheck() 之后调用
ngAfterViewInit() 用于 DOM 操作
在视图初始化完成后调用。适合于访问组件视图中的本地元素引用 (@ViewChild 和 @ViewChildren)。
当 Angular 初始化完组件视图及其子视图之后调用。
第一次 ngAfterContentChecked() 之后调用,只调用一次
ngAfterViewChecked()
每当 Angular 做完组件视图和子视图的变更检测之后调用。
ngAfterViewInit() 和每次 ngAfterContentChecked() 之后调用。
ngOnDestroy()
每当 Angular 每次销毁指令/组件之前调用并清扫。 在这儿反订阅可观察对象和分离事件处理器,以防内存泄漏。
在 Angular 销毁指令/组件之前调用。
Angular 装饰器
@NgModule (Angular v14 版本以后已很少使用,直接在组件中 import 就行)
作用:定义一个模块,用于组织组件和服务。
属性:
declarations: 声明属于该模块的组件和指令。
imports: 导入其他模块。
exports: 导出可以在其他模块中使用的组件和指令。
providers: 提供服务。
@Directive
作用:定义一个自定义指令。
属性:
selector: 指令的选择器。
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {}
@Input
作用:标记一个属性为输入属性,允许从父组件传递数据。
@Component({
selector: 'app-child',
template: `<p>{{ message }}</p>`
})
export class ChildComponent {
@Input() message: string;
}
@Output
作用:标记一个属性为输出属性,允许向父组件发送事件。
@Component({
selector: 'app-child',
template: `<button (click)="onClick()">Click me</button>`
})
export class ChildComponent {
@Output() clicked = new EventEmitter<string>();
onClick() {
this.clicked.emit('Button clicked');
}
}
@HostBinding
作用:绑定宿主元素的属性、类或样式
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@HostBinding('style.backgroundColor') backgroundColor = 'yellow';
}
@HostListener
作用:监听宿主元素的事件。
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@HostListener('mouseenter') onMouseEnter() {
this.backgroundColor = 'yellow';
}
@HostListener('mouseleave') onMouseLeave() {
this.backgroundColor = '';
}
}
@Pipe
作用:定义一个自定义管道。管道(相当于Vue的过滤器):引入之后,使用 | 符号
属性:
name: 管道的名称
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'uppercase'
})
export class UppercasePipe implements PipeTransform {
transform(value: string): string {
return value.toUpperCase();
}
}
@Injectable()
作用:用于标记一个类,使其可以通过依赖注入系统被实例化。
依赖注入(Dependency Injection, DI)是 Angular 的核心功能之一,
它可以帮助管理组件和服务之间的依赖关系,使得代码更加模块化和可维护。
用于标记服务类,使其成为 Angular 依赖注入系统的一部分。
通常用于服务类,但也可以用于其他类。
可以指定服务的提供范围。
属性:
providedIn 属性,可以指定服务的提供范围,
例如全局范围('root')或特定模块范围(AppModule)
@Injectable({
providedIn: 'root' // 或者指定一个模块
})
@Inject: (新版本可以使用 inject 函数 代替)
用于显式指定依赖项的令牌。
通常用于 constructor 参数。
用于注入非类类型的依赖项
从 Angular 14 开始,引入了一个新的 inject 函数,
它提供了一种更简洁的方式来注入依赖项。
这个函数允许你直接在 constructor 中使用依赖项而无需显式地使用 @Inject 装饰器
内容查询:
从父组件中选择子组件或指令。这两个装饰器主要用于处理投影的内容(即通过 <ng-content> 投影进来的子视图
@ContentChild
用于选择单个子组件或指令实例。它接受一个选项对象来指定查询的条件,比如读取特定的属性
用于在组件或指令中查询和获取投影到组件中的内容
即通过 <ng-content> 投影的内容)中的某个特定元素或指令实例。
@Component({
selector: 'app-card-table-wrap',
template: `<div><ng-content></ng-content></div>`
})
export class CardTableWrapComponent {
@ContentChild('myTemplate') myTemplate: TemplateRef<any>;
}
@ContentChildren
用于选择多个子组件或指令实例。
它返回的是一个 QueryList,这是一个可观察的列表,当视图发生变化时会自动更新
@Component({
selector: 'app-card-table-wrap',
template: `<div><ng-content></ng-content></div>`
})
export class CardTableWrapComponent {
@ContentChildren('myTemplate') myTemplates: QueryList<TemplateRef<any>>;
}
建议在 ngAfterContentInit 或 ngAfterContentChecked 生命周期钩子中使用这些装饰器获取的数据。
如果需要在构造函数中立即使用这些数据,可以考虑使用 AfterContentInit 或 AfterContentChecked 接口,并在相应的生命周期钩子方法中访问这些数据
视图查询:
允许你在组件类中访问模板内的子视图中的指令、组件实例或 DOM 元素
@ViewChild
查询组件模板中的某个特定元素或指令实例
用于从父组件中选择单个子视图中的组件实例或 DOM 元素。它通常用于访问模板内的子组件或元素
查询的结果通常在 ngAfterViewInit 生命周期钩子中可用
@Component({
selector: 'app-parent',
template: `<app-child *ngFor="let item of items" #childRef></app-child>`
})
export class ParentComponent {
@ViewChildren('childRef') childRefs: QueryList<ChildComponent>;
}
@ViewChild (***)
用于获取单个子视图中的组件实例或 DOM 元素。它可以接受一个选项对象来指定查询的条件,比如读取特定的属性
@ViewChildren
用于获取多个子视图中的组件实例或 DOM 元素,它返回的是一个 QueryList,
这是一个可观察的列表,当视图发生变化时会自动更新
Angular 路由
import { Router, RouterLink } from '@angular/router';
..............
impores:[
RouterLink
]
private router = inject(Router);
..............
路由方法:
// navigate 方法用于导航到指定的路由。它可以接受一个数组作为参数,数组中的第一个元素是路由路径,后续元素是路由参数。
// 示例方法:带参数的导航
navigateToUser(userId: string) {
this.router.navigate(['/user', userId]);
}
// 示例方法:相对路径导航
navigateRelative() {
this.router.navigate(['../sibling'], { relativeTo: this.route });
}
queryParams:用于传递查询参数。
fragment:用于指定 URL 的片段标识符。
relativeTo:用于指定相对路径的起点
this.router.navigate(['/user', '123'], {
queryParams: { tab: 'profile' },
fragment: 'anchor',
relativeTo: this.route
});
// navigateByUrl 方法用于通过完整的 URL 字符串进行导航。
this.router.navigateByUrl('login/login-form');
<a class="password-login" routerLink="/login/login-form">使用已有帐号登陆
Angular 监听路由变化
可以使用 ActivatedRoute 服务来监听路由变化
import { Component, OnInit,inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.css']
})
export class MyComponent implements OnInit {
private route = inject(ActivatedRoute)
ngOnInit() {
this.route.params.subscribe(params => {
console.log('Params:', params);
});
this.route.queryParams.subscribe(queryParams => {
console.log('Query Params:', queryParams);
});
this.route.fragment.subscribe(fragment => {
console.log('Fragment:', fragment);
});
}
}
Angular 组件通信
1.父子组件通信 (两个装饰器都是子组件使用)
@Input() 装饰器: 类似于 props
父组件中传递,子组件中使用 @Input()接收
@Output: 从子组件到父组件的通信路径,请在类属性上使用 @Output 装饰器,并将其赋值为 EventEmitter 类型
// 子组件
@Component({
...
template: '<button (click)="sendMessage()">Send Message</button>'
})
class ChildComponent {
@Output() messageEvent = new EventEmitter<string>();
sendMessage() {
this.messageEvent.emit('Hello from child!');
}
}
// 父组件
<app-child (messageEvent)="receiveMessage($event)"></app-child>
2.兄弟组件通信
共享服务 (Shared Service): 可以创建一个服务并通过依赖注入来共享数据或方法。
Injectable
Inject
BehaviorSubject
RxJS Observables: 使用 RxJS 的 BehaviorSubject 或 ReplaySubject 等来传递数据。
3.跨层级组件通信
共享服务 (Shared Service): 适用于任何层级的组件之间的通信。
RxJS Observables: 同样适用于任何层级的组件之间的通信。
4.直接访问子组件
ViewChild 或 ContentChild: 父组件可以直接访问子组件的实例。
5.Angular 14以后新增 context
import { createContext, useContext } from '@angular/core';
const AppContext = createContext('App context');
@Component({
selector: 'app-root',
template: `<app-child></app-child>`
})
export class AppComponent {
context = useContext(AppContext, 'Initial context value');
}
@Component({
selector: 'app-child',
template: `<div>{{ context }}</div>`
})
export class ChildComponent {
context = useContext(AppContext);
}
6.Angular Router
通过路由参数:
使用路由配置来传递参数。
示例:
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: 'detail/:id', component: DetailComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
在组件中使用:
使用 ActivatedRoute 获取路由参数。
示例:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-detail',
template: `<div>Detail ID: {{ id }}</div>`
})
export class DetailComponent implements OnInit {
id: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.id = this.route.snapshot.paramMap.get('id');
}
}
Angular 中 实现类似于 Vue 中监听器的功能
使用 RxJS 的 Subject: 通过创建一个 BehaviorSubject 来共享状态,并在父组件中订阅这个状态
1.创建一个 BehaviorSubject 来共享状态。
子组件通过 updateSharedValue 方法更新状态。
父组件通过订阅 sharedValue$ 来监听值的变化。
2.使用 @Output 和事件:
子组件通过 @Output 发射事件。
父组件通过 (valueChanged) 绑定事件处理器来监听值的变化。
使用 RxJS 的 Subject:
// 可以 定义一个 Service
// 创建一个 BehaviorSubject 来共享状态
// SharedData.Service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class SharedDataService {
private _sharedValue = new BehaviorSubject<string>('Initial Value');
sharedValue$ = this._sharedValue.asObservable();
constructor() {}
updateSharedValue(value: string) {
this._sharedValue.next(value);
}
}
-----------------------------------------------------------------------------------------
// 子组件
import { Component, OnInit } from '@angular/core';
import { SharedDataService } from '../services/shared-data.service';
@Component({
selector: 'app-child',
template: `
<input [(ngModel)]="inputValue" (ngModelChange)="onInputChange($event)" />
`
})
export class ChildComponent implements OnInit {
inputValue = '';
constructor(private sharedData: SharedDataService) {}
// 用于处理输入框 (<input>) 的值发生变化时的逻辑
onInputChange(event: any) {
this.inputValue = event.target.value;
this.sharedData.updateSharedValue(this.inputValue);
}
ngOnInit() {
console.log('ChildComponent initialized');
}
}
-----------------------------------------------------------------------------------------
// 父组件
import { Component, OnInit } from '@angular/core';
import { SharedDataService } from '../services/shared-data.service';
@Component({
selector: 'app-parent',
template: `
<div>
<p>Parent Value: {{ parentValue }}</p>
<app-child></app-child>
<button (click)="updateValue()">Update Value</button>
</div>
`
})
export class ParentComponent implements OnInit {
parentValue = '';
constructor(private sharedData: SharedDataService) {}
ngOnInit() {
this.sharedData.sharedValue$.subscribe(value => {
this.parentValue = value;
console.log('Value changed:', value);
});
}
updateValue(): void {
this.sharedData.updateSharedValue('New Value');
}
}
Angular 中 constructor 的作用
1.依赖注入 (Dependency Injection):
constructor 是 Angular 的依赖注入系统识别依赖项的主要位置。当你在 constructor 中声明了一个服务或其他类的参数时,Angular 的依赖注入系统会自动解析这些依赖并将其实例化对象注入到构造函数中。
例如,如果你有一个服务 MyService,你可以在组件的 constructor 中声明它,Angular 会自动为你注入这个服务的实例。
2.初始化工作:
constructor 也可以用来做一些基本的初始化工作,比如设置一些初始值或者调用服务中的方法来获取初始数据。
但是,一般建议不要在 constructor 中执行复杂的逻辑或副作用操作,因为这可能会导致代码难以理解和维护。
3.私有化成员变量:
有时你可能希望某些成员变量只在类内部可见。虽然 TypeScript 和 ES6 不支持私有构造函数,但你可以利用 constructor 来初始化私有成员变量。
例如,你可以在 constructor 中初始化私有变量,并在类内部使用这些变量。
为什么在服务类中使用 constructor?
1.依赖注入:
在服务类中使用 constructor 的主要原因是进行依赖注入。服务通常需要与其他服务或外部资源交互,因此需要在 constructor 中声明这些依赖。
例如,一个服务可能需要从 HTTP 客户端获取数据,这时你可以在服务的 constructor 中注入 HttpClient。
2.初始化:
服务类中的 constructor 也可以用来初始化一些基本的状态或设置默认值。
总结起来,constructor 在 Angular 中主要用于依赖注入和初始化工作。对于服务类而言,依赖注入尤为重要,因为它使得服务可以轻松地与其他服务或外部资源进行交互,同时也提高了代码的可测试性和可维护性。
Angular v14 到 v18 的变化
1.去除 @NgModule
改为 可选的功能
2.去除 Decorator(装饰器)
例如 @InJect 改为 InJect()函数
3.去除 结构型指令
4.引入新的控制流指令
@if
@for
@switch
RxJS
RxJS是一种流式编程库,它允许您使用 Observable 对象来处理异步和事件驱动的编程任务。
在Angular中,RxJS常用于处理HTTP请求、数据流和事件处理等任务
Angular 本身就大量依赖于 RxJS 来处理各种异步任务,如 HTTP 请求、表单验证、状态管理等。
核心概念
1.Observable(可观察对象):
它是一个异步数据流的发布者。
当创建了一个 Observable 后,你可以订阅它来接收数据流中的值。
Observable 可以发射多个值,并且可以在任何时候完成或者错误。
2.Observer(观察者):
它是接收 Observable 发射的数据的对象。
Observer 有三个方法:next、error 和 complete。
next 方法接收 Observable 发出的正常值。
error 方法接收 Observable 发生的错误。
complete 方法在 Observable 完成时调用。
3.Subscription(订阅):
订阅是 Observer 和 Observable 之间的连接。
当你订阅一个 Observable 时,你会得到一个 Subscription 对象,它有一个 unsubscribe 方法可以用来取消订阅。
4.Operators(操作符):
操作符是用来处理 Observable 数据流的方法。
例如,map 用于转换数据,filter 用于过滤数据,concatMap 用于扁平化数据流等。
应用场景:
HTTP 请求:Angular 的 HttpClient 模块返回的是 Observable,可以方便地处理异步数据。
表单验证:FormsModule 和 ReactiveFormsModule 提供了基于 Observable 的表单验证机制。
状态管理:虽然 Angular 自身没有内置状态管理库,但 RxJS 可以用来构建简单的状态管理解决方案。
路由守卫:如 canDeactivate 守卫,可以使用 RxJS 来处理异步逻辑,决定是否允许用户离开当前路由。
服务间通信:使用 Subjects 或 BehaviorSubjects 来在组件和服务之间共享数据。
Angular 路由守卫
1.CanActivate(*):在激活路由前执行,用于决定是否允许导航到该路由、用于检查用户是否有权限访问该路由
2.CanActivateChild:在子路由激活之前执行逻辑,用于验证用户是否有权限访问子路由。
3.CanDeactivate(*):在离开当前路由之前执行,用于决定是否允许离开当前路由。
4.Resolve(*):在激活路由前执行,用于在导航完成前获取数据、用于预加载数据
5.CanLoad(*):在懒加载模块前执行,用于控制模块是否可以被加载
6.CanMatch:在匹配路由前执行,用于控制路由是否可以被匹配
实现方式:
路由守卫通常是一个实现了特定接口的类。
需要在路由配置中注册这些守卫。
Angular 的变化检测机制
类似于 diff 算法
1.Angular 使用了一种称为“脏检查”的机制来进行变化检测。确定组件的状态是否发生了变化。当应用程序状态发生变化时,Angular 会遍历组件树来检查是否有任何需要更新的视图。
2.Angular 通过 DefaultKeyValueDiffer 和 DefaultIterableDiffer,这两个类来检查对象和数组的变化。
这些类用于比较两个对象或两个数组之间的差异,从而决定是否需要更新视图。
3.变更检测触发:
变更检测通常在事件处理程序、定时器回调、HTTP 请求完成等异步任务完成后自动触发。
也可以通过调用 detectChanges 方法手动触发变更检测。
Angular 的编译模式
JIT (Just-In-Time) 编译:在运行时编译模板。
AOT (Ahead-Of-Time) 编译:在构建时编译模板,提高了应用的启动速度。(默认编译方式)
由于应用程序是在浏览器内部运行之前进行编译的,因此浏览器会加载可执行代码并立即呈现应用程序,从而加快了呈现速度。
在AOT编译中,编译器将与应用程序一起发送外部HTML和CSS文件,从而消除了对那些源文件的单独AJAX请求,从而减少了ajax请求。
开发人员可以在构建阶段检测并处理错误,这有助于最大程度地减少错误。
AOT编译器将HTML和模板添加到JS文件中,然后再在浏览器中运行。 因此,没有多余的HTML文件可读取,从而为应用程序提供了更好的安全性。
更快的启动性能:因为大部分编译工作已经预先完成,所以用户在访问应用时不需要等待编译,这显著减少了初始加载时间。
更小的payload:AOT编译器能够消除未使用的代码和优化静态标记,从而减小最终的JavaScript文件大小。
更好的安全性:AOT编译有助于防止某些类型的注入攻击,因为它消除了对动态代码执行的需求。
更少的运行时错误:许多常见的模板错误在AOT编译期间就可以被检测出来,而不是在用户的设备上运行时才出现。
Angular 的双向绑定原理
通过脏数据检查(Dirty checking)来实现
Angular 的双向绑定是通过变更检测机制和 Zone.js 的结合来实现的。当数据模型发生变化时,Angular 会自动更新视图
Promise 和 Observable 的区别
首先新版本的anuglar是推荐使用 Observable (属于RxJS),其次,对于Observable对象,可以使用.toPromise()转化为Promise对象。
Promise,无论是否调用then。promise都会立即执行;而observables只是被创建,当调用(subscribe)的时候才会被执行。
Promise返回一个值;Observable返回0至N个值。所以Promise对应的操作符是.then(),Observable对应的是.subscribe
Observable,还额外支持 map,filter,reduce 和相似的操作符
Observable 可以取消,Promise不可以
Angular 中的 Zone.js
Zone.js 是一个 JavaScript 库,它为 JavaScript 的异步操作提供了统一的管理方式
1.异步操作拦截:
Zone.js 可以拦截 JavaScript 中的所有异步操作。
当异步操作开始时,Zone.js 会创建一个新的 Zone 并进入该 Zone。
当异步操作完成时,Zone.js 会退出当前 Zone 并返回到父 Zone。
2.变更检测触发:
在 Angular 中,每当一个异步操作完成时,Zone.js 会通知 Angular 执行变更检测。
这意味着 Angular 会在每个异步操作完成后检查数据模型是否有变化,并根据需要更新视图。
3.生命周期钩子调用:
Zone.js 还可以用来管理 Angular 组件的生命周期钩子方法,如 ngOnInit, ngOnDestroy 等。
当 Angular 组件初始化或销毁时,Zone.js 会确保这些生命周期钩子方法在正确的 Zone 内被调用。
4.错误处理:
Zone.js 可以帮助捕获和处理 JavaScript 中的错误,尤其是在异步操作中。
通过 Zone.js,可以更容易地追踪和调试错误。
Angular 性能优化
1.使用 OnPush 策略
OnPush 是另一种变化检测策略,它只在组件的输入属性发生变化时才执行变化检测。
当组件声明为 OnPush 模式时,Angular 只会在以下两种情况之一时才会运行变化检测:
a. 组件的输入属性发生了变化。
b. 组件的生命周期钩子被调用,或者有异步操作(如 HTTP 请求)完成。
这种策略大大减少了变化检测的工作量,因为只有受影响的组件及其直接子组件需要被检测。
2.避免不必要的更改检测
在不涉及任何数据绑定的函数中,避免使用 setTimeout()、setInterval() 或其他异步操作,因为这些操作会导致全局范围内的变化检测。
使用 ChangeDetectorRef.detach() 方法手动断开组件的变化检测连接,然后在适当的时候重新连接
3.使用 ChangeDetectionStrategy.OnPush 和 immutable 数据:
如果你的数据是不可变的(即你不会修改对象本身,而是创建一个新的副本来表示数据的更改),那么你可以结合 OnPush 策略来提高性能。
不可变的数据使得 Angular 可以通过简单的引用比较来判断数据是否发生了变化,而不是进行深度比较。
4.减少作用域内数据的数量:
减少组件内部作用域内的数据数量可以降低变化检测的时间复杂度。
避免在组件中使用大量的数组或对象,因为这会导致更多的数据比较。
5.利用 RxJS 操作符:
使用 RxJS 操作符,如 debounceTime()、switchMap() 等,可以控制何时触发变化检测。
懒加载组件:
对于不经常使用的组件,考虑使用路由懒加载,这样可以延迟加载这些组件,直到真正需要它们时再进行变化检测。
6.资源优化:
压缩和合并静态资源,如CSS和JavaScript文件。
使用CDN加速内容分发。
利用HTTP缓存和浏览器缓存。
Angular 中实现国际化
使用 @angular/localize
Angular CLI
ng new my-app
ng serve --open
Angular 实现文件上传预览
1.PDF 文件预览
npm i ng2-pdf-viewer -S
import { PdfViewerModule } from 'ng2-pdf-viewer';
.......................
handlePreview = async (file: NzUploadFile): Promise<void> => {
if (file.type.includes('application/pdf')) {
const name = file.response.data.files[0] ?? '';
this.pdfSrc = `/static/${name}`;
this.previewVisible = true;
console.log(this.pdfSrc);
}
}
..........................
// html
<pdf-viewer
[src]="pdfSrc"
[render-text]="true"
[original-size]="false"
style="width: 100%; height: 500px"
/>
Angular、Vue、React 的区别
1.基本语法
判断:
Angular用 @if / *ngIf,Vue中使用v-if / v-show,React中使用jsx语法,三元运算符,&&符号...
属性绑定:
Angular用[], Vue中使用冒号(v-bind简写),React中使用 {}
事件绑定:
Angular用( ),Vue中用 @(v-on的简写),React使用 onClick
循环:
Angular用 @for(v15)/ *ngFor
@for(item of array, track item.id){
{{item.name}}
}
<li *ngFor="let item of items">
{{ item }}
</li>
Vue使用 v-for
React使用 map方法
插槽:
Angular中用 <ng-content select='my-content' />
Vue中使用 <solt />
React中使用 props.children
2.语法和模板
Vue.js使用了类似于HTML的模板语法,使得开发者可以声明性地定义UI。
Angular使用了 TypeScript,并使用了一种特殊的模板语言,模板中嵌入了指令和绑定。
React使用了 JSX(JavaScript XML),将UI组件的结构和逻辑组合在JavaScript代码中,这使得UI更接近JavaScript。
3.状态管理
Vue.js提供了Vuex,一个用于状态管理的官方库,使得在大型应用中管理状态更容易。
Angular内置了RxJS,可用于处理应用程序的状态和异步操作。
React通常与Redux或Mobx等第三方状态管理库一起使用。
4.数据请求
Angular中使用 HttpClient 模块,支持 Angular 的依赖注入,与 Angular 的其他特性如管道、RxJS 操作符等紧密集成,使得处理数据流更加容易, 默认支持 CORS 和 CSRF 保护,这在使用 Axios 时需要手动配置。
Vue、React则使用 axios
Angular 路由守卫和 Vue 路由守卫之间的差别
- 实现方式:
- Angular:路由守卫通常是一个实现了特定接口的类,需要在路由配置中注册。
- Vue:路由守卫通常是函数,可以直接在路由配置或组件内部定义。
- 类型:
- Angular:提供了
CanActivate、CanDeactivate、Resolve和CanLoad四种类型的守卫。 - Vue:提供了全局守卫(3)、路由独享守卫(3)和组件内守卫(1)三种类型的守卫。
- Angular:提供了
- 灵活性:
- Angular:守卫类的实现方式提供了更高的灵活性和复用性。
- Vue:守卫函数的实现方式更加简洁,适合快速开发。
- 应用场景:
- Angular:适合大型企业级应用,需要更细粒度的控制和复用。
- Vue:适合中小型项目,需要快速实现路由守卫功能。
Angular 中的 ng-content 和 Vue 中的插槽 以及 React 中的 props.children 之间的区别
Angular中的 ng-content
基本功能:
<ng-content> 是 Angular 中的内容投影机制,允许你在组件模板中插入外部内容。
通过 <ng-content>,你可以将父组件中的内容传递到子组件中指定的位置。
属性:
可以使用 select 属性来指定投影内容的选择器,实现更细粒度的控制。
例如:<ng-content select=".header"></ng-content> 只会投影带有 .header 类的内容。
// 子组件
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content select=".header"></ng-content>
<ng-content select=".body"></ng-content>
<ng-content select=".footer"></ng-content>
</div>
`
})
export class CardComponent {}
<!-- 父组件 -->
<app-card>
<div class="header">Header</div>
<div class="body">Body</div>
<div class="footer">Footer</div>
</app-card>
Vue 的插槽
基本功能:
Vue 的插槽(slots)也用于内容投影,但提供了更多的灵活性和功能。
支持默认插槽、具名插槽和作用域插槽。
1.默认插槽:不需要指定名称
<!-- 子组件 -->
<template>
<div class="card">
<slot></slot>
</div>
</template>
<!-- 父组件 -->
<app-card>
Default content
</app-card>
2.具名插槽: 允许你定义多个插槽,并通过 name 属性区分
<!-- 子组件 -->
<template>
<div class="card">
<slot name="header"></slot>
<slot name="body"></slot>
<slot name="footer"></slot>
</div>
</template>
<!-- 父组件 -->
<app-card>
<template v-slot:header>Header</template>
<template v-slot:body>Body</template>
<template v-slot:footer>Footer</template>
</app-card>
3.作用域插槽:允许子组件向父组件传递数据
<!-- 子组件 -->
<template>
<div class="card">
<slot :item="item"></slot>
</div>
</template>
<script>
export default {
data() {
return {
item: { name: 'Example' }
};
}
};
</script>
<!-- 父组件 -->
<app-card v-slot:default="slotProps">
{{ slotProps.item.name }}
</app-card>
React 的 props.children
基本功能:
props.children 是 React 中传递子组件内容的标准方式。
它可以是任何有效的 React 节点,包括文本、组件、数组等。
// 子组件
const Card = ({ children }) => (
<div className="card">
{children}
</div>
);
// 父组件
<Card>
<div>Header</div>
<div>Body</div>
<div>Footer</div>
</Card>
对比总结
功能丰富性:
Vue 的插槽功能最为丰富,支持默认插槽、具名插槽和作用域插槽,提供了高度的灵活性。
React 的 props.children 简单直接,适用于大多数场景,但缺乏 Vue 那样的细粒度控制。
Angular 的 <ng-content> 功能介于两者之间,支持选择器,但不如 Vue 的插槽灵活。
使用场景:
如果你需要复杂的、多样的内容投影,Vue 的插槽可能是最佳选择。
如果你只需要简单的内容传递,React 的 props.children 和 Angular 的 <ng-content> 都能满足需求。
VitePress
项目结构
静态站点生成器
my-vitepress/
├── .vitepress/ // VitePress 配置文件、开发服务器缓存、构建输出和可选主题自定义代码的预留位置
│ ├── config/ // 存放配置文件
│ │ └── shared.mts // 共享的配置文件,如题目中提到的文件。
│ ├── public/ // 存放静态资源文件,如图标、图片等
│ │ └── favicon.ico
│ ├── style.css // 自定义的全局样式文件
│ └── theme.ts // 自定义主题配置文件
├── .gitignore
├── README.md
├── docs/
│ ├── guide/ // 指南文档目录
│ │ └── intro.md
│ ├── api/ // API 文档目录
│ │ └── components.md // 组件相关的 API 文档
│ └── changelog.md // 变更日志文档。
├── package.json
├── tsconfig.json TypeScript // 配置文件,定义了项目的编译选项。
└── vite.config.ts Vite // 配置文件,用于自定义 Vite 行为。
vitepress 目录之外的 Markdown 文件被视为源文件
VitePress 使用 基于文件的路由:每个 .md 文件将在相同的路径被编译成为 .html 文件
.
├─ docs # 项目根目录
│ ├─ .vitepress # 配置目录
│ ├─ getting-started.md
│ └─ index.md
└─ ...
路由
链接页面,可以使用绝对路径和相对路径,可以省略后缀名
[Getting Started](./getting-started)
Jest 单元测试
import {cleanup,render,waitFor,screen,fireEvent} from '@testing-library/react'
import {MemoryRoter,Routes,Router} from 'react-router-dom'
jest.mock('')
jest.mock('react-router-dom',()=>{
useNavigate:jest.fn(),
useLocation:jest.fn(),
useParams:()=> {a:1,b:2,具体参数}
useResolvedPath:jest.fn()
})
jest.mock('react-i18next',()=>{
useTranslation:()=>({
t:(key: string): string => key,
}),
})
jest.mock('formik',()=>{
useFormikContext:()=>{
return {
setFieldValue: jest.fn(),
values: ......
}
},
})
const component = 组件...
describe('test xxx',()=>{
test('xxx',()=>{
beforeEach(()=>{
})
after(()=>{
cleanup()
})
})
//快照
test('xxxx',()=>{
const { container } = render(component)
expect(container).toMatchSnapshot('....Snapshot')
})
// 模拟接口
test(‘xxxx’,async()=>{
xx(接口 as jest.Mock).mockResolvedValue({
// 接口构造
payload:{
work:{
xxxxxxx
}
}
})
xx(接口 as jest.Mock).mockImplentation({
// 接口构造
return Promise.resolve (
xxxx(数据)
)
})
// 模拟接口是否调用
await waitFor(async()=>{
// 断言是否调用
expect(xxx(接口名称)).toBeCalled()
const restBtn = screen.getByTestId('reset-btn')
// 模拟点击
fireEvent.click(resBtn)
const confirmPage = screen.getByRole('dialog')
// 模拟点击
fireEvent.click(confirmPage)
const confirmBtn = screen.getElementByTagName('button')[0]
})
})
})
// jest.config.ts
import type { Cinfig } from "jest";
const config: Config = {
testEnvironment: "jsdom",
// testEnvironment: '<rootDir>/jest/custom-environment.cjs',
setupFilesAfterEnv: ["@testing-library/jest-dom"],
// setupFiles: ['<rootDir>/jest.setup.cjs'], // 新增配置项
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
"\\.(png|jpg|jpeg|gif|svg|webp)$": "<rootDir>/__mocks__/fileMock.js",
"^!!raw-loader!": "<rootDir>/__mocks__/rawLoaderMock.js",
},
transform: {
"^.+\\.(js|jsx|ts|tsx)$": [
"babel-jest",
{ configFile: "./babel.config.cjs" },
],
"^.+\\.(png|jpg|jpeg|gif|svg|webp)$": "jest-transform-stub",
},
testMatch: ["**/__tests__/**/*.test.[jt]s?(x)"],
transformIgnorePatterns: [
"node_modules/(?!(.*\\.mjs$|vite|react-markdown|@ant-design|@fullcalendar)/)",
],
testEnvironmentOptions: {
resources: "usable",
runScripts: "dangerously",
},
globals: {
"import.meta.env": {
VITE_API_URL: "http://nest-api.weiibn.shop",
VITE_ENABLE_MOCK: "true",
VITE_API_BASE_URL: "http://test-api.example.com",
},
},
};
// babel.config.cjs
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: { node: 'current' },
modules: 'auto',
useBuiltIns: 'entry',
corejs: 3.37,
shippedProposals: true,
},
],
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
plugins: [
'transform-import-meta',
[
'@babel/plugin-transform-modules-commonjs',
{
loose: true,
// allowTopLevelThis: true,
// 新增 meta 属性处理
meta: {
importMeta: true, // 显式启用 import.meta 支持
},
},
],
],
}
命令行:
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.cjs --detectOpenHandles --runInBand",
"test:coverage": "jest --coverage",
jest babel-jest jest-environment-jsdom ts-jest jest-transform-stub @types/jest @testing-library/react @testing-library/user-event
"@babel/core": "7.24.4",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
"@babel/plugin-transform-runtime": "^7.26.10",
"@babel/preset-env": "7.24.4",
"@babel/preset-react": "7.23.3",
"@babel/preset-typescript": "7.23.3",
"@babel/runtime": "^7.26.10",
Vercel 部署
## Vercel 部署
v4.0.8 加入了 Vercel 配置文件,可以直接在 Vercel 下部署了,不需要自己的服务器
### 操作方法
1. fork 此项目
2. 在 Vercel 官网点击 `New Project`
3. 点击 `Import Git Repository` 并选择你 fork 的此项目并点击`import`
4. 点击 `PERSONAL ACCOUNT` 的 `select`
5. 直接点`Continue`
6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
SQL
# 创建数据库
CREATE DATABASE database_name;
# 选择数据库
USE database_name;
# 创建表
CREATE TABLE table_name (
column1 datatype constraints,
column2 datatype constraints,
...
);
# 插入数据
INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...);
# 查询数据
SELECT column1, column2 FROM table_name WHERE condition;
# 更新数据
UPDATE table_name SET column1 = new_value WHERE condition;
# 删除数据
DELETE FROM table_name WHERE condition;
# 删除表:
DROP TABLE IF EXISTS table_name;
# 删除数据库
DROP DATABASE IF EXISTS database_name;
# 查看表结构
DESC table_name;
# 创建索引
CREATE INDEX index_name ON table_name (column);
# 删除索引
DROP INDEX index_name ON table_name;
# 视图操作
# 创建视图
CREATE VIEW view_name AS SELECT column1, column2 FROM table_name WHERE condition;
# 删除视图
DROP VIEW IF EXISTS view_name;
事务操作:
# 开始事务
BEGIN;
# 提交事务
COMMIT;
# 回滚事务
ROLLBACK;
权限控制:
# 给用户授予权限
GRANT privileges ON table_name TO user;
# 撤销权限
REVOKE privileges ON table_name FROM user;
请注意,这些命令适用于大多数SQL数据库系统,但具体语法可能因数据库引擎(如MySQL、PostgreSQL、SQL Server等)的不同而略有差异。
启动服务:net start mysql84
使用 sequelize 包连接数据库
testone 测试管理平台系统
四个模块:
测试用例管理:
手工用例库:新建用例库(查看、删除、编辑用例库)
用例库详情:测试用例、用例评审、测试计划
测试用例:(列表模式、脑图模式)
新建用例:富文本编辑器 wangeditor/editor
(新建/编辑 用例信息(用例标题、步骤、前置条件、有效性、优先级类型、附件......))、
关联需求、
关联缺陷、
关联测试
导入用例(excel导入、xmind导入)
导出用例(excel文件导出、feature文件导出)、
模块列表(树结构)(新建子模块、删除、重命名、移动)
用例评审:新建评审(选择评审人、通知方法)、查看(选择用例、评审用例(通过、未通过))、删除、编辑
测试计划:新建测试计划(选择负责人、通知方法)、查看(选择用例、设置结果(未测、成功、失败、重测、阻塞、跳过)、查看测试报告)、删除、编辑
自动化用例库
测试任务:
任务列表(增删改查)
新建任务:直接新建(测试计划编排:选择测试类型:手工测试、终端自动化测试(终端-Android-云测、优测、单测)、选择被测产物(从蓝盾获取构建产物))
测试报告
测试配置
内审千里眼系统
首页模块:搜索框
小工具列表:
企业族谱、企业路径穿透、企业关系校验、企业位置关系、企业与员工关系、内部人员查询、外部人员查询、供应商利益冲突、供应商围标串标、部门合作风险、团伙关联
自定义小工具
企业详情页:
合作信息:
工商注册的基本信息 :注册时间 、资本、参保人数、法人、地址、经营范围 、曾用名、营业期限
账户信息 :首次登记时间 (在腾讯业务中第一次设立账号的时间 )、登记行业 (业务准入时登记的行业和类目,如商户类目、广告推广行业等 )、账户数量、关联公司数量、投放产品数量、处罚参数、是否加黑
供应商信息 :是否供应商 、是否三证合一 、中标/参标次数 、准入BG/类别/当前状态
合同信息 :首次签订时间 、合同到期时间、 合同金额(付 )、合同金额(收 )、实付金额 、实收金额 、合同数量 、合作部门数量
---
业务信息 :
账户分布(个)(该公司账户在腾讯各业务的分布情况 )、合同分布(份)(该公司合同在腾讯各部门的分布情况 )
财务概况:交易类型分布 (epo/cost/upay的对外付款情况,基于费用类型分布 )、付款部门分布
月结支出 、月结收入
企业产品信息 :产品分布 个数、产品状态
关系图谱 :当前公司的子公司、股东、任职的电话、证件号
工商信息:
工商注册信息:法定代表人 、注册金额 、实收资本 、注册时间 、信用代码 、参保人数 、组织机构代码 、所属省份 、注册号 、所属行业、 公司性质、检验日期 、
高管信息 :职位、姓名
企业族谱 :上下结构,股东(投资比例)、子公司(投资比例)
股权信息 :股东、持股金额、持股比例
投资信息:投资公司、持股金额、持股比例
工商风险扫描信息
工商变更信息
风险信息:
风险标签分类(echarts)、业务处罚情况(echarts)
公司关联风险图谱:当前公司与其他公司存在的关联信息
关键事件 :
创办时间、第一次付款时间、第一份合同签订时间 、最近合同签订时间 、准入时间
关键数据
干系人图谱 (左右结构):企业主要人员: 法人、经理、董事、
合同干系人 : 部门
供应商干系人 :部门
历史参标信息
首页,由两部分,一个搜索框,一个小工具列表 ,搜索到公司姓名,点击之后就会跳转到企业详情页面,这个详情页面有五个页面:合作信息页面、关系图谱页面、工商信息页面、风险信息页面、关键事件页面
合作信息:
有工商注册基本信息、
合同信息、
供应商信息、
业务信息(echarts 饼状图):账户分布、合同分布
财务概况(echarts 折线图):月结收入、月结支出、
产品信息 (echarts 饼状图):产品分布、产品状态
关系图谱:这个图谱采用了 antv-g6 这一个图可视化引擎组件库,采用辐射布局(以一个点为中心,向四周辐射),以该公司为中心点,展示 子公司、股东、邮箱、电话、证件号等信息
工商信息:
工商注册信息的详细信息
高管信息
股权信息
投资信息
企业族谱:紧凑树布局( 根节点在中间,垂直对称布局 ),上面展示了 股东、下面展示了子公司
工商变更信息
法院案件
风险信息:
风险标签分类(echarts 饼图)
公司关联风险图谱 :采用 辐射布局,以该公司为中心,展示了与其他公司关联的风险信息
关键事件
关键数据
干系人图谱 :紧凑树布局(根节点在中间,水平对称布局 ):展示企业主要人员、合同干系人、供应商干系人、
历史参标信息
小工具列表:
企业族谱、企业路径穿透、企业关系校验、企业位置关系、企业与员工关系、内部人员查询、外部人员查询、供应商利益冲突、供应商围标串标、部门合作风险、团伙关联
反舞弊业务系统
首页:
快捷入口
我的工作台:我的待办、我的已办
公告通知
帮助文档
业务接口人
相关咨询
个人中心模块:
我的待办 : 待办的审批申请 、待办的案件
我的已办 :已办的审批申请、已办的案件、已办的线索
信息查询模块:
线索管理模块 :
线索录入 :线索录入 、提交,待审批通过后,立案,形成案件
线索查询 : 线索列表
案件管理模块 :
案件查询
案件详情页 :
当前案件进度(立案、结案、关闭、归档)
案件基本信息 :案件编号、线索编号、案件类型、地区、时间、来源、名称、涉案BG、主办调查员 、协办调查员 、案发原因-控制缺陷 、案发原因-个人动机 、举报内容 、涉案金额 、挽回损失 、涉案部门
涉案违规人员:员工、证件信息、违规程度、违规类型、违规行为、涉案金额、认定金额、挽回金额
涉案其他人员:员工 、证件信息、违规程度、违规类型、违规行为、涉案金额、认定金额、挽回金额
涉案管理者:供应商名称、统一信用代码、合作状态
涉案供应商、
必要操作 :
人事征集
调查报告
结案报告
案件编辑页
涉案主体查询 :涉案供应商、涉案人员
涉案物品查询 :
案件归档查询
名单管理模块:
发文名单处理
供应商处理
宣导培训模块:
问卷查询
培训查询
系统管理模块:
公告管理
邮件管理
用户管理:
用户查询
添加用户
角色管理:
反舞弊业务系统 :为反舞弊调查组员工提供帮助