tree组件的halfChecked值应该后端存储还是前端计算?

背景

昨天在做一个树形组件权限控制的功能,包括菜单和按钮的访问权限。
熟悉 ant design 组件库 tree 组件的都知道,如果一个父节点,他的子节点存在未选中的,那么这个父节点应该是一个半选(半选可以直观表示其子节点存在未选中)的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const treeData = [
{
title: '0-0',
key: '0-0',
children: [
{
title: '0-0-0',
key: '0-0-0',
children: [
{
title: '0-0-0-0',
key: '0-0-0-0',
},
{
title: '0-0-0-1',
key: '0-0-0-1',
},
{
title: '0-0-0-2',
key: '0-0-0-2',
},
],
},
],
},
];

如果节点 0-0-0-0 未选中,那么此时实际权限为 ['0-0-0-1', '0-0-0-2', '0-0-0', '0-0'],tree 组件为了父节点显示为半选效果,tree.checkedKeys 应该为 ['0-0-0-1', '0-0-0-2']

tree 组件 onCheck 事件可以得到 checkedKeys: ['0-0-0-1', '0-0-0-2']halfCheckedKeys: ['0-0-0', '0-0']

最简单的方案是前端传一个实际权限数组 permissionsList 和一个过滤掉半选状态的数组 filteredCheckedKeys 给后端,后端再返回给前端做回显即可。如下

1
2
const permissionsList = [...checkedKeys, ...halfCheckedKeys];
const filteredCheckedKeys = [...checkedKeys];

问题来了,后端觉得没必要保存两个字段,他只希望接收 permissionsList。后端如果不处理,只能前端计算 filteredCheckedKeys

考虑到用户量和权限树节点比较少,前端算就前端算,chatGpt 三下五除二就搞定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Function to recursively calculate halfCheckedKeys
function getHalfCheckedKeys(treeData, checkedKeys) {
const halfChecked = [];

// Helper function to recursively traverse the tree
function traverse(node) {
if (!node.children || node.children.length === 0) {
// Leaf node: Return if it's checked or not
return checkedKeys.includes(node.key) ? 'checked' : 'unchecked';
}

let allChecked = true;
let anyChecked = false;

// Recursively check all child nodes
node.children.forEach((child) => {
const childStatus = traverse(child);

if (childStatus === 'checked') {
anyChecked = true;
} else if (childStatus === 'unchecked') {
allChecked = false;
} else if (childStatus === 'half-checked') {
anyChecked = true;
allChecked = false;
}
});

if (allChecked) {
return 'checked';
} else if (anyChecked) {
halfChecked.push(node.key);
return 'half-checked';
} else {
return 'unchecked';
}
}

// Start traversing the tree
treeData.forEach(traverse);

return halfChecked;
}

// 示例已选中的 keys
const permissionsList = ['0-0-0-1', '0-0-0-2', '0-0-0', '0-0'];

// 获取 halfCheckedKeys
const halfCheckedKeys = getHalfCheckedKeys(treeData, permissionsList);

console.log('Half Checked Keys:', halfCheckedKeys); // 输出: ['0-0-0', '0-0']

// 过滤 permissionsList,排除 halfCheckedKeys 中的键
const filteredCheckedKeys = permissionsList.filter(
(key) => !halfCheckedKeys.includes(key),
);

到此,前端多了一步计算 getHalfCheckedKeys, 后端少存了一个字段。

但我不想就此结束,可以前端写,但前端不能没理由的写,于是我问了 chatgpt 两个方案的差异,如下:

总结对比

特性 前端动态计算 halfCheckedKeys 后端存储 halfCheckedKeys
性能 对大树结构可能性能较差 性能较好,前端无需计算
实现复杂度 前端需要复杂的递归计算 后端存储和同步逻辑复杂
一致性 基于前端状态,完全一致 依赖于前后端的状态同步
灵活性 可随时重新计算,无需后端依赖 必须依赖后端返回,灵活性较低
维护成本 代码复杂且随应用增长增加维护难度 增加存储和同步逻辑,前后端需同时维护
数据管理 前端本地管理,无需后端存储 后端需存储和管理 halfCheckedKeys

适用场景

方案一(前端动态计算)适用于

  • 权限树结构较小、节点较少时。
  • 前端有足够的性能来处理权限树。
  • 需要高交互性、即时反馈的场景,减少与后端的交互次数。

方案二(后端存储)适用于

  • 权限树结构非常庞大、节点数量较多时。
  • 前端性能较低或应用规模较大,减少前端计算压力。
  • 希望减少前端逻辑复杂度,后端统一管理和处理数据的场景。

最佳实践

  • 如果权限树数据规模较大,或者操作频繁,方案二(后端存储 halfCheckedKeys)会更具优势,提升前端性能和简化实现。
  • 如果权限树较小或者需要灵活性、即时反馈,方案一(前端动态计算)可能更适合,并且减少了后端的复杂性。

后面又聊着聊着,后端也发现了还是后端存储这种方案会比较简单。如果这个权限树节点很多,getHalfCheckedKeys 的计算可能会导致前端页面卡顿。

性能优化-懒加载

懒加载是一种延迟加载的技术。初始时,只加载可视区域内的目标元素,滚动页面时,动态加载可见区域的目标元素。

常见的实现方式有 IntersectionObserverscroll两种,推荐使用 IntersectionObserver,因为他有更好的性能表现。

下面以图片懒加载为例,看看如何实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<div class="list">
<div class="list-item">
<img
src="data:image/png;base64,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"
data-src="http://c.hiphotos.baidu.com/image/pic/item/30adcbef76094b36de8a2fe5a1cc7cd98d109d99.jpg"
/>
</div>
<div class="list-item">
<img
src="data:image/png;base64,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"
data-src="http://h.hiphotos.baidu.com/image/pic/item/7c1ed21b0ef41bd5f2c2a9e953da81cb39db3d1d.jpg"
/>
</div>
<div class="list-item">
<img
src="data:image/png;base64,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"
data-src="http://g.hiphotos.baidu.com/image/pic/item/55e736d12f2eb938d5277fd5d0628535e5dd6f4a.jpg"
/>
</div>
<div class="list-item">
<img
src="data:image/png;base64,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"
data-src="http://e.hiphotos.baidu.com/image/pic/item/4e4a20a4462309f7e41f5cfe760e0cf3d6cad6ee.jpg"
/>
</div>
<div class="list-item">
<img
src="data:image/png;base64,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"
data-src="http://b.hiphotos.baidu.com/image/pic/item/9d82d158ccbf6c81b94575cfb93eb13533fa40a2.jpg"
/>
</div>
<div class="list-item">
<img
src="data:image/png;base64,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"
data-src="http://e.hiphotos.baidu.com/image/pic/item/4bed2e738bd4b31c1badd5a685d6277f9e2ff81e.jpg"
/>
</div>
</div>

小细节优化
src属性添加一张默认占位图(如果有使用组件库的话可以用骨架组件),体验上更加友好。

IntersectionObserver

1
var io = new IntersectionObserver(callback, [option]);

callback 是可见性变化时的回调函数,option 是配置对象。

相比 `scroll`,性能更好:
    - 节流处理:用户快速滚动页面,也只会触发少量的回调
    - 异步处理:回调函数是由浏览器在适当的时机(requestIdleCallback)调用的,不会阻塞页面渲染和用户交互
    - 跨线程处理:观察过程由浏览器内部线程完成,不会阻塞主线程
    - 精准度高:能够精准地检测目标元素和文档视口之间的交叉状态
判断目标元素是否在是视野内的逻辑简单,根据 item.isInterSection 布尔值判断即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const lazyImages = document.querySelectorAll('img[data-src]');
const options = {
root: null,
rootMargin: '0px',
threshold: 0.1,
};

const loadImage = function (image) {
image.src = image.dataset.src;
image.removeAttribute('data-src');
};

const observer = new IntersectionObserver(function (entries, observer) {
entries.forEach(function (entry) {
// isIntersecting是一个Boolean值,判断目标元素当前是否可见
if (entry.isIntersecting) {
loadImage(entry.target);
// 图片加载后即停止监听该元素
observer.unobserve(entry.target);
}
});
}, options);

lazyImages.forEach(function (image) {
observer.observe(image);
});

scroll(不推荐)

scroll 事件频繁触发,可能会影响页面的性能和用户体验
判断目标元素是否在视野内的计算麻烦
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
document.addEventListener('DOMContentLoaded', function () {
function lazyload() {
const lazyImages = document.querySelectorAll('.lazy[data-src]');

// 遍历检查所有懒加载图片
lazyImages.forEach(function (img) {
const rect = img.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
img.src = img.getAttribute('data-src');
img.removeAttribute('data-src');
}
});

// 无未加载图片时,移除相关事件监听
if (lazyImages.length == 0) {
document.removeEventListener('scroll', lazyload);
window.removeEventListener('resize', lazyload);
window.removeEventListener('orientationChange', lazyload);
}
}
// 添加事件监听
document.addEventListener('scroll', lazyload);
window.addEventListener('resize', lazyload);
window.addEventListener('orientationChange', lazyload);
});

小结

懒加载是一种动态加载技术,只有当目标元素进入视野时才会去加载目标元素。通常的实现方法有两种,scrollIntersectionObserver,监听 scroll 事件,会频繁触发回调函数,可能会造成页面卡顿,性能上和用户体验上不好。IntersectionObserver 利用 requestIdelCallback 在程序空闲时调用回调函数,异步处理,不会阻塞页面渲染和用户交互,还能够精准检测目标元素和文档视口之间的可见性。

开发环境代理 proxy

前端项目本地开发过程中,常常会遇到跨域问题,这是因为我们的本地服务域名为localhost,而后端服务一般都是在后端服务器上,所以两个不同的域名之间通信就会存在跨域,严谨点来说是受同源策略的影响。

假设后端的完整 api 路径:https://pruduct.com/api/chat/user/list,前端在localhost下想要去访问这个 api

有个重要的前提是,后端需要在响应头设置'Access-Control-Allow-Origin': '*' ,该属性表示允许任何的源都有权限访问资源,*表示通配符。
如果后端没有设置这个属性,就算前端设置了本地代理proxy也是无法成功访问的。

后端设置了Access-Control-Allow-Origin后,其实本地可以不需要 proxy 也不会产生跨域问题,因为已经允许了任何源都有权限访问。

下面这个示例直接写死 baseURL 发起请求,不会跨域:

1
2
3
4
5
const request = axios.create({
baseURL: 'https://pruduct.com',
});

request('/api/chat/user/list');

那什么场景需要使用proxy代理呢?

以下场景,proxy就派上了用场:

  • 想在本地调用/打包不同环境的数据,不想手动修改 request baseURL
  • 本地需要同时调用多个服务(测试环境/正式环境,可能还有一些第三方环境)的接口,写死 baseURL无法满足要求

来看下如何使用proxy解决这两个问题

脚本调用/打包不同服务

如果不想手动修改 request baseURL,可以通过脚本调用不同的环境变量来实现,假如环境变量为API_BASE_URL

1
2
3
const request = axios.create({
baseURL: API_BASE_URL,
});
1
2
3
4
"scripts": {
"dev": "cross-env UMI_ENV=test max dev",
"dev:online": "cross-env UMI_ENV=online max dev",
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// .umirc.online.ts 生产环境配置文件
export default {
define: {
API_BASE_URL: 'https://pruduct.com', // 正式环境
},
};

// .umirc.test.ts 测试环境配置文件
export default {
define: {
API_BASE_URL: 'https://test.com', // 测试环境
},
};

同时调用多个服务

就好比想同时请求两个服务时,/api/user这个接口在https://pruduct.com下,/test/userhttps://test.com下,本地代理就派上用场了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const request = axios.create({
baseURL: '/',
})

proxy: {
'/api': {
target: 'https://pruduct.com',
changeOrigin: true,
},
'/test': {
target: 'https://test.com',
changeOrigin: true,
}
}

proxy 怎么用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
API_BASE_URL='https://pruduct.com'

// 组件发起请求
const request = axios.create({
baseURL: '/',
})
const apiUrl = '/api/user'
request(apiUrl)

proxy: {
'/api': {
target: API_BASE_URL,
changeOrigin: true,
},
}

发起/api/user请求时,匹配到 proxy 对象的路径/api后,那么实际请求地址为target + /api/user,即 https://pruduct.com + /api/user

应该有部分人以为 proxy 匹配路径必须是后端定义的,其实不是。
假设前端想自定义路径,或者说后端没有统一的路径,那么就可以自定义匹配路径

1
2
3
4
5
6
7
8
9
10
11
12
13
API_BASE_URL='https://pruduct.com'

// 组件发起请求
request('/customApiPrefix/api/user')

proxy: {
'/customApiPrefix': {
target: API_BASE_URL,
changeOrigin: true,
// 重写匹配的字段,如果不想出现在请求路径上,可以重写为""
pathRewrite: { "^/customApiPrefix": "" }
},
}

实际请求地址还是为https://pruduct.com + /api/user

小结:
开发过程中出现跨域是因为受同源策略的约束影响,localhost不能与后端服务通信,想要通信,后端需要设置允许请求的源'Access-Control-Allow-Origin': '*'。设置'Access-Control-Allow-Origin': '*'后,如果后端服务只有一个,可以写死baseUrl无需 proxy。如果有多个服务,那么通过proxy和环境变量的配置,可以让我们在开发或打包时更方便。

CSS Padding-Top 实现自适应容器的宽高比例保持一致

在网页设计中,我们经常需要创建一个具有固定宽高比(如 16:9 或 9:16)的容器。然而,当屏幕大小发生变化时,如何让这个容器自适应调整,并且保持固定的宽高比呢?

通常,我们可能会用 JavaScript 来实现这一效果。具体来说,先将容器宽度设置为 100%,然后通过 JavaScript 获取容器的宽度(element.offsetWidth),再根据比例计算出对应的高度值。然而,这种方法需要监听窗口大小的变化,并频繁操作 DOM,对性能并不友好。

幸运的是,我们可以通过 CSS 的 padding-toppadding-bottom 属性来实现这一效果,无需使用 JavaScript。

固定比例的工作原理

  • padding-top 的值是按百分比设置的,例如 padding-top: 56.25% 对应于 16:9 的比例。
  • 由于百分比值是相对于容器的宽度计算的,这样可以确保当容器的宽度变化时,padding-top 会自动调整容器的高度,从而保持宽高比。

具体实现

以下是一个示例代码:

1
2
3
<div class="container">
<div class="content"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.container {
position: relative;
width: 100%;
padding-top: calc(
9 / 16 * 100%
); /* 16:9的比例。如果是9:16的比例,改为16 / 9 * 100% */
}

.content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: burlywood; /* 测试显示用 */
}

在上面的代码中,我们给容器元素添加了 padding-top: calc(9 / 16 * 100%),这样可以实现容器的高度根据宽高比例自动调整,而无需借助 JavaScript 设置高度。

理解 padding-top 如何影响高度

在标准盒模型下,padding-top 的值会增加容器的总高度,因为它会在内容的上方增加额外的空间。但这里我们利用了 padding-top 相对于容器宽度计算这一特性,创造了一种新的高度比例。

当我们在容器上设置 padding-top 时,这个 padding-top 是按照容器的宽度百分比计算的,因此当容器宽度变化时,这个 padding-top 也会相应变化,从而调整容器的总高度,以维持比例。

盒模型示例

在标准盒模型中:

1
<p class="p" id="container"></p>
1
2
3
4
5
.p {
box-sizing: content-box;
padding-top: 50px;
height: 10px;
}

假设元素的高度为 10px,内边距为 50px:

  • 标准盒模型 box-sizing: content-box

    1
    2
    3
    element = document.getElementById('container');
    element.style.height = '10px';
    element.offsetHeight = 60; // 10px 内容高度 + 50px 内边距
  • 怪异盒模型 box-sizing: border-box

    1
    2
    3
    element = document.getElementById('container');
    element.style.height = '10px';
    element.offsetHeight = 50; // 高度包含内边距,实际内容高度减少

    element.offsetHeight = content + 垂直内边距 + border

padding-toppadding-bottom 的相同点

  • 都是根据容器的宽度来计算。
  • 都能用于实现宽高比固定的效果。

为什么常用 padding-top

  • padding-top 通常用于从顶部开始的布局,这样内容可以从顶部开始定位。
  • 视觉上更直观,特别是在内容从顶部开始流动的情况下。

容器占位,更好的用户体验

当加载图片时,未加载完成前会占位,防止后续内容被顶上去,提升用户体验。

1
2
3
4
5
<img
src="https://img1.baidu.com/it/u=1571454416,119170533&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
alt=""
/>
<div>被顶的文字</div>

在上面的代码中,当图片尚未加载到页面上时,<div>被顶的文字</div> 将出现在顶部位置。但是,当图片加载完成后,<div>被顶的文字</div> 将被顶到图片后面,这可能会给用户带来不良的视觉体验。

通过使用 padding-toppadding-bottom 技巧,我们可以确保容器的位置被保留,后续元素也不会被顶到。这将提升用户的使用体验。

1
2
3
4
5
6
7
8
<div class="container">
<img
class="content"
src="https://img1.baidu.com/it/u=1571454416,119170533&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500"
alt=""
/>
</div>
<div>不会被顶的文字</div>

通过这种方法,我们可以在不借助 JavaScript 的情况下,实现具有固定宽高比的自适应容器,简化代码并提升性能。希望这篇文章能帮助你更好地理解和应用 CSS 的 padding-toppadding-bottom 技巧。

javascritp 双精度浮点数 精度问题

遇到的问题

今天遇到一个问题,场景如下:
后端返回一个 uuid 为 1763118905011392500 ,我带 1763118905011392500 去查数据时,接口报错提示: uuid 不存在,我问后端怎么回事,后端查了一下这个 uuid,发现数据库确实不存在值为 1763118905011392500 的 uuid。

谷歌了一下,发现是 javascript 使用双精度浮点数表示数字,最大安全整数是 2^53 - 1 ,即 9007199254740991

浏览器中可以接收的值的大小取决于浏览器的 javascript 引擎的实现。大多数浏览器都遵循 javascript 标准。

当浏览器接收到大于最大安全整数的值时,超出这个范围的整数值将会失去精度,可能会被舍入。

也就是说,后端给我的值大于 2^53-1 ,浏览器将丢失精度的值保存了下来,前端将丢失精度的值传递给后端去查询数据时,那必然找不到该值。

找到了原因后,接下来了解下 双精度浮点数 ,以及如何解决这个问题。

双精度浮点数

浮点数是什么?

在 JavaScript 中,浮点数是一种用于表示小数的数据类型。浮点数采用 IEEE 754 标准表示,通常被称为双精度浮点数,即使用 64 位(8 字节)来表示一个浮点数。这意味着无论数字的大小如何,它们在内存中占用的空间都是相同的,即 64 位或者 8 个字节。
这种格式可以表示小数(小数是指非整数的有理数,其中包含了小数点及其后面的数字部分)部分,而不仅仅是整数部分,因此可以用来存储小数值。

双精度浮点数是什么?

双精度浮点数是一种用于表示实数的数字表示方法,它在计算机科学中被广泛使用。它的名称中的“双精度”表示它的存储空间是单精度浮点数(32 位)的两倍,因此可以存储更大范围和更高精度的数字。
具体来说,双精度浮点数使用 64 位(8 字节)来存储一个数字,这 64 位被划分为三部分:

  1. 符号位(1 位):用于表示数字的正负。
  2. 指数部分(11 位):用于表示数字的指数部分,决定了数字的数量级。
  3. 尾数部分(52 位):用于表示数字的小数部分,决定了数字的精度。

在科学计数法中,一个数字通常表示为 M × 10^E 的形式,其中 M 是尾数(即小数部分),E 是指数(表示这个数需要移动的小数点位数)

64 位(8 字节)是什么意思?

64 位(8 字节)是指在计算机中用来表示数据的一种存储方式。它表示一个数据单元占用的存储空间大小。具体来说,64 位意味着这个数据单元可以存储 64 位二进制数字,即包含了 64 个二进制位。

1
2
3
4
5
6
7
Bit-比特 Byte-字节 KB-千字节 MB-兆字节 GB-吉字节 TB-太字节

1 Byte = 8 Bits
1 GM = 1024 MB
1 TB = 1024 GB

一个二进制位的大小称为一比特,也就是最小单位

解决方法

js 有没有什么办法可以表示大于 2^53 - 1 的数呢?有的,就是 BigInt 类型,它可以表示任意大的整数。

要表示 2^53,可以直接使用 BigInt 类型的字面量赋值,如下所示:

1
2
3
4
const bigIntNumber = 2n ** 53n;
console.log(bigIntNumber); // 输出:9007199254740992n

console.log(typeof bigIntNumber); // 'bigint'

这里的 2n 表示一个 BigInt 数字 2,** 表示幂运算符,53n 表示一个 BigInt 数字 53。

但在因为 Java 原生不支持 BitInt 类型,所以不能使用 BitInt 来处理。

所以解决方案有以下两种:

  1. 将 uuid 改为 2^53-1 下的值,也就是小于等于 15 位数
  2. 或者将 uuid 字符串化,如'1763118905011392500'

小结

js 标准中,使用双精度浮点数表示数字,最大安全整数为 2^53 - 1。js number 类型无法精准表示超出最大安全数的值,可用 BigInt 类型表示。如果后端给的数值超出最大安全值,可以采用”字符串化”方案,解决前后端交互时精度丢失的问题。

思考:
3 和 99 占用的存储空间大小一样吗?

Nginx+pm2 部署项目

继续 Nginx 入门章节,本章主要讲解基本的 nginx 文件配置和启动一个 nodejs 服务。其中涉及到前端资源存放,环境域名配置、默认端口处理等。

如果顺利,我们将可以访问 http://138.128.222.180/。如果使用域名,则还需要备案才能访问 http://www.weilunaichat.xyz

网站域名配置

简单介绍下,申请个域名,添加一个记录集,将域名指向 IP 地址,也就是指向存放前端资源文件的服务器 IP 地址。
如果使用域名,还需要进行备案才能进行访问。

备案:todo

nginx 配置文件

编辑 nginx 配置文件,使用 http 默认端口号 80 作为网站访问地址的端口号。也就是说,如果我们使用 80 端口 server 块配置资源,页面访问时 url 则不需要带上端口。

第一段 location 代码块中,root设置为前端资源文件所在路径,index设置为 html 文件名
第二段 location,proxy_pass设置为后端服务地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80;
server_name localhost; // 这里可以使用域名

location / {
root html/frontend/chatgpt-web; // 目录位置,相对于Nginx的安装目录
index index.html index.htm;
# try_files $uri $uri/ /index.html; // 如果是history路由,需要匹配兜底文件
}

location /api {
# 反向代理,代理本机器3002端口
# 如果有其他服务,就指向其他服务
proxy_pass 127.0.0.1:3002;
}
}

启动 nginx

进入 nginx 安装目录下的 sbin 目录,并执行 nginx 文件,即执行./nginx
pic.1709045583924

没有任何提示就表示启动成功了。此时已经可以访问 http://124.70.17887.156

启动后端服务

本地开发时,我们启动项目时,会启动 localhost 作为本地服务器。那么,想要在服务器上启动服务,我们需要在服务器上安装启动服务的工具,再执行启动脚本即可。

以启动后端 nodejs 服务来说,先在服务器上安装 nodejs,再执行启动服务脚本

安装 nodejs

第一步仍然需要下载对应的二进制文件:

wget -c https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-x64.tar.xz

提取文件:

tar -xvf node-v18.16.0-linux-x64.tar.xz

解压之后得到的文件夹就是已经安装好的 Nodejs 了,为了方便我们可以把它重命名一下(在这里我把它放到了

用户根目录下面的 app 的文件夹里,你也可以换成其他路径):

mv node-v18.16.0-linux-x64 ~/app/nodejs

然而,这种方式安装的 Nodejs 并不完美。首先,Nodejs 的命令 node 和 npm 并不能在全局使用。为了解决这个问题,我们需要建立两个软链接:

sudo ln -s ~/app/nodejs/bin/node /usr/local/bin/node

sudo ln -s ~/app/nodejs/bin/npm /usr/local/bin/npm

其中,~/app/nodejs 是我们刚才安装的 Nodejs 的路径,在建立软链接的时候要注意区别。

现在,node 和 npm 可以在全局使用了,同样输入命令 node -v 来检查 Nodejs 是否安装成功:

1
2
$ node -v
v18.16.0 # 出现了对应的版本号信息,说明安装成功

还有一个问题,在 npm 下全局安装的模块无法直接在 bash 中执行。例如,我们要在 npm 中安装 pnpm:

1
2
3
$ npm install -g pnpm
$ pnpm
bash: pnpm: command not found...

为了解决这个问题,我们需要在 Linux 上手动配置环境变量。编辑 /etc/profile 文件:
sudo vi /etc/profile
在文件的底部,添加下面两行代码:
export NODE_HOME=~/app/nodejs/bin
export PATH=$NODE_HOME:$PATH
跟刚才一样,~/app/nodejs 是我们安装的 Nodejs 的路径。
现在,npm 全局安装的模块也可以使用了:

1
2
$ pnpm -v
8.15.3

至此,Nodejs 的安装已经顺利完成,我们可以在服务器上对 Nodejs 为所欲为了!

启动服务

执行启动脚本,pnpm start
pic.1709532090132

接口也可以访问了

pic.1709532156349

现在可以通过域名访问了,但是还是存在一个问题,就是我们刚才执行的 pnpm start 命令,必须打开命令行才有效,一旦关闭命令行,进程也终止了。这时候可以借助 pm2 来解决这个问题。

pm2

PM2 是一个用于管理 Node.js 应用程序的生产过程的流行工具,可以用它部署 运行 监控 应用。
安装:npm i pm2 -g

介绍
进入项目所在目录,执行如下命令启动 node 服务:

1
pm2 start npm --name yourName -- run server
  • pm2 start: 这是 PM2 的命令,用于启动一个 Node.js 应用。
  • npm: 这是 Node.js 的包管理器,用于管理 JavaScript 包和依赖项。
  • --name yourName: 这是一个可选的参数,用于指定应用程序的名称。在这里,您可以将”yourName”替换为您的应用程序的实际名称。
  • -- : 这是一个分隔符,它告诉 npm 后面的内容是要传递给 npm 的命令。
  • run server: 这是 npm 的子命令,它告诉 npm 运行名为”server”的脚本。通常,”server”是一个在package.json文件中定义的脚本名称。例如,如果您的package.json文件包含以下内容:

对于我们的 nodejs 服务,启动脚本为pnpm start,如果我们使用 pm2 来启动的话,就执行pm2 start pnpm --name chatgpt-web-server -- run start
pic.1709045710219
成功了,现在就算我们关闭了终端,nodejs 服务也会一直启动着。

pm2 常用命令

1
2
3
4
5
6
$ pm2 list
$ pm2 show app_name
$ pm2 start app_name
$ pm2 stop app_name
$ pm2 delete app_name
$ pm2 restart app_name

Nginx安装和启动

nginx 是一个轻量 web 服务器。

安装

查看服务器是什么系统

1
uname -a

我是 Linux 系统,可使用 yum 软件包管理器

第一步:先安装 PCRE pcre-devel 和 Zlib,配置 nginx 的时候会用到这两个东西

1
2
yum install -y pcre pcre-devel
yum install -y zlib zlib-devel

第二步:安装 nginx1.18.0

  1. 下载 nginx 文件
1
wget -c https://nginx.org/download/nginx-1.18.0.tar.gz

Linux 系统中的 wget 是一个下载文件的工具

  1. 解压并进入 nginx 目录
1
2
tar -zxvf nginx-1.18.0.tar.gz
cd nginx-1.18.0
  1. 使用 nginx 的默认配置
1
./configure
  1. 编译安装
1
2
make
make install

nginx 已经安装完成了

查找 nginx 安装路径:

whereis nginx
pic.1709045302233

启动 nginx 服务器。进入 nginx 安装目录下的 sbin 目录,并执行 nginx 文件,即执行./nginx
pic.1709045387708


没有任何提示就表示启动成功了。访问机器 ip,如果显示 welcome to nginx 则表示 nginx 运行成功了。
pic.1709045415961

如果还无法访问服务器 ip 地址,以我的华为云服务器为例,还需要添加安全组规则。http 协议默认 80 端口(https 默认端口为 443),我们添加一个 80 端口:

pic.1709045483441

添加后即可成功访问 nginx 欢迎页面。

查看配置文件目录。这个语句也可以验证 nginx.conf 文件是否正确。正确的格式会提示 test is successful

1
./nginx -t

pic.1709045506823

nginx 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
查看Nginx安装位置
whereis nginx
常见位置:
/etc/nginx/conf
/usr/local/etc/nginx
/opt/homebrew/etc/nginx

启动
启动代码格式:nginx安装目录地址 -c nginx配置文件地址
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
./nginx
没有任何提示消息就表示成功了

查看Nginx进程
ps -ef|grep nginx
master 是主进程(唯一),worker是工作进程

查看端口情况
lsof -i:80
-i:80 表示只看80端口

杀死进程
kill -QUIT pid

Nginx停止或重启。进入nginx可执行目录sbin下,输入以下命令
./nginx -s signal
quit :优雅停止
stop :立即停止
reload :重载配置文件
reopen :重新打开日志文件

https://segmentfault.com/a/1190000012297511
https://bbs.huaweicloud.com/blogs/304663
https://www.cnblogs.com/ghzjm/p/10677599.html

为什么需要虚拟DOM

先说结论:

  1. 操作真实 DOM 的代价是巨大的
  2. 虚拟 DOM 为框架自动优化 DOM 操作提供了可能

什么是虚拟 DOM

以 JS 的形式描述真实的 DOM 结构。

操作真实 DOM 存在什么问题

从浏览器内核结构来说,在内存中,DOM 模块独占一块内存,这和 JS 引擎所管理的内存并无直接关系,也就是说 JS 引擎不能直接操作真实 DOM 树
为了给 JS 提供操作 DOM 树的能力,浏览器在全局对象上为 JS 封装了一个document对象,该对象上封装了大量操作 DOM 的接口,这些接口都是 c++实现的。

1
window.document.getElementById();

当我们调用这个函数时,JS 引擎没有直接和 DOM 操作交互,而是由浏览器来操作 DOM,再由浏览器把操作结果返回给 JS 引擎。所以我们说操作真实 DOM 的代价是比较大的(还涉及到 c++和 javascript 数据结构转换的问题)。

早期模板引擎(EJS)存在的问题

模板引擎的思想,让我们只需要关注数据层的变化。但是还存在以下问题:

  • 每次数据变化都是重新渲染整个列表视图
  • 当数据量巨大时,性能不尽人意

虚拟 DOM 解决了什么问题

为了解决模板引擎的问题,虚拟 DOM 出生了。
既然模板解决方案每次数据变化都是重新渲染整个列表视图,那只操作变化部分的 DOM 不就好了。既然操作真实 DOM 性能损耗巨大,那操作假的不就好了。
对比下:

  • 模板引擎的渲染:数据 + 模板 –> 直接渲染为真实 DOM –> 挂载至页面
  • 虚拟 DOM 的渲染:数据 + 模板 –> 虚拟 DOM –> 真实 DOM –> 挂载至页面

虚拟 DOM 带来的好处

  • 页面性能的提升:只处理变化的 DOM,减少操作真实 DOM 的次数
  • 开发效率的提升:react 中的 jsx、vue 模板语法
  • 跨平台得以实现:一份代码,多端使用

react-this.setState是同步还是异步的

  1. 如果需要依赖更新后的值,如果是classComponent,我们可以在componentDidUpdate中执行,或者this.setState的第二个参数。如果是functionComponent,我们可以在useEffect中执行
  2. 不同模式下的 react,这个答案是不一样的

本文讨论第二点

react 的三种模式:

  • legacy,react 当前使用的模式,ReactDOM.render
  • blocking,开启部分concurrent模式特性的中间模式
  • concurrent,react v18 启用。任务中断/任务优先级都是针对concurrent模式

lagecy 模式

lagecy 模式下,this.setState触发的更新是异步的。因为它会命中batchedUpdates函数。

batchedUpdates

定义:
batchedUpdates 表示批处理,react 会将多次**this.setState**合并为一次更新,并异步执行。这样只会触发一次render函数,以此提高性能。

实现:
fn:包含this.setState的函数,比如点击事件、生命周期等。
react 内部有个batchedUpdates 函数,在执行这个fn之前,会给全局变量executionContext附加上BatchedContex这个 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function batchedUpdates(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= BatchedContext;

try {
return fn(a);
} finally {
executionContext = prevExecutionContext;

if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}

然后它会执行这个 fn,执行完之后,会把BatchedContexexecutionContext去除。

如果全局变量executionContext包含了BatchedContext,他就会认为这是一次批处理。批处理中的setState会被合并为一次更新。
classComponent 为例
例子一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { PureComponent } from 'react';

export default class MyClassCmp extends PureComponent {
constructor() {
super();
this.state = {
count: 0,
};

this.onAdd = this.onAdd.bind(this);
}

onAdd() {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
console.log('after', this.state.count); // 输入 0
}

componentDidUpdate() {
console.log('componentDidMount', this.state.count); // 输出 1
}

render() {
const { count } = this.state;
return (
<div>
count: {count}
<button onClick={this.onAdd}>add</button>
</div>
);
}
}

onAdd中调用this.setState,会被认为是批处理,并不能马上获取到最新的count。可以在componentDidUpdate钩子中或者this.setState的第二个参数中获取到最新值。

如何跳出batchedUpdates

如果fn中触发的this.setState是异步执行的话,等this.setState执行的时候,全局的executionContext就已经不存在BatchedContext,他就会跳出批处理。
跳出批处理后,每次调度更新都会执行scheduleUpdateOnFiber函数,函数内部有段逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// react V < 18
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (lane === SyncLane) {
// 省略其他代码...

if (executionContext === noContext) {
flushSyncCallbackQueue();
}
}
}

// 这里不用关心,只是单纯了解下18版本源码
// react V18
// flushSyncCallbacksOnlyInLegacyMode 命名更语义化了
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode && // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!ReactCurrentActQueue$1.isBatchingLegacy
) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}

也就是说,如果executionContext什么都没有的话,会执行fulshSyncCallbackQueue函数,同步的执行这次更新。

例子二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
onAdd () {
// this.setState({ count: this.state.count+1 })
// this.setState({ count: this.state.count+1 })
// console.log('after', this.state.count); // 输入 0

setTimeout(() => {
this.setState({ count: this.state.count+1 })
this.setState({ count: this.state.count+1 })
console.log('after', this.state.count); // 输入 2
})
}

componentDidUpdate() {
console.log('componentDidMount', this.state.count); // 输出两次: 1 2
}

concurrent 模式

ReactDOM.render(<App />, rootNode)变为为ReactDOM.createRoot(rootNode).render(<App />),这样就切换到了concurrent 模式下。
concurrent模式下,例子一的表现结果与laecy模式一致。
例子二,after 的输出 从2变为0componentDidMount里的输出两次1和2 变为输出一次1,这是为什么呢?

这是因为,执行scheduleUpdateOnFiber函数的前提是本次更新的优先级是同步的优先级lane === SyncLane。那什么是同步的优先级呢,也就是ReactDOM.render创建的应用的更新都是同步的优先级,而concurrent模式创建的应用有不同的优先级,所以不会命中fulshSyncCallbackQueue,即同步的更新。

总结

  • lagecy 模式命中 batchedUpdates时异步
  • lagecy 模式未命中batchedUpdates时同步
  • concurrent 模式都是异步

react更新流程

我们了解了 react 的Scheduler-Reconciler-Renderer架构体系

  • Scheduler负责任务的优先级调度
  • Reconciler工作的工作阶段被称为render阶段。因为在该阶段会调用render方法
  • Renderer工作的阶段被称为commit阶段。commit阶段会把render阶段提交的信息渲染到页面上。

前面已经介绍了render阶段commit阶段render阶段完成后会进入commit阶段,而render阶段之前就是触发状态更新阶段

render 阶段前阶段

状态更新的整个调用路径的关键节点:

触发状态更新

在 react 中,有以下方法可以触发状态更新:

  • this.setState
  • this.focusUpdate
  • ReactDOM.render
  • useState
  • useReducer

每次触发状态更新都会走一遍render阶段前阶段->render阶段->commit阶段这个流程。

创建 Update 对象

react中,有多种触发状态更新的方法,他们是如何保持同一套状态更新机制呢?
每次状态更新都会创建一个保存更新状态相关内容的对象,我们叫他Update。在render阶段会根据Update计算新的state

从 fiber 到 root,得到 rootFiber

这个阶段,触发状态更新的fiber上已经包含了Update对象。
我们知道,render阶段是从rootFiber开始向下遍历。那么如何从触发状态更新的fiber得到rootFiber呢?
答案是:调用markUpdateLaneFromFiberToRoot
这个方法的工作:从触发状态更新的fiber一直向上遍历到rootFiber,得到rootFiber,并返回rootFiber

调度更新

现在我们拥有了一个rootFiber,该rootFiber对应的fiber树中的某个fiber节点包含一个Update。接下来通知Scheduler根据更新的优先级,决定以同步还是异步的方式调度本次更新。
调用的方法是ensureRootIsScheduled
这个方法会根据优先级调度回调函数执行,这里调度的回调函数为:

1
2
performSyncWorkOnRoot.bind(null, root); // 同步的回调函数
performConcurrentWorkOnRoot.bind(null, root); // 异步的回调函数

这个回调函数也是render阶段的入口函数。

render 阶段

同步或异步调度本次更新,根据 rootFiber 得到 fiber 树

commit 阶段

副作用对应的DOM操作在 commit 阶段执行。
执行 DOM 操作前:

  • 处理DOM节点渲染后的autoFocus/blur逻辑
  • 会调用getSnapshotBeforeUpdate,能在操作 DOM 前捕获 DOM 信息(如滚动位置)
  • 异步调度useEffect

执行 DOM 操作:

  • 通过一次插入 DOM 操作将整颗DOM树插入页面。
  • 执行useEffect、useLayoutEffect销毁函数
  • 会调用componentWillUnmount
  • 解绑 ref

执行 DOM 操作后:

  • 会调用useLayoutEffect回调函数
  • 调度useEffect,在Layout阶段完成后再异步执行useEffect回调函数
  • 调用this.setState的第二个参数回调函数

useLayoutEffect从上一次更新的销毁函数调用到本次更新的回调函数调用是同步执行的。
useEffect则需要先调用,在Layout阶段完成后再异步执行。

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×