# 2023年01月
# 2023-01-29
# element-plus table 性能优化,减少 85% 渲染耗时
参考掘金:vue3 table 性能优化,减少 85% 渲染耗时 (opens new window)
前段时间公司有一个比较重要的模块从 vue2 升级到 vue3,升级后发现 element-plus table 的性能相比 vue2 版本下降非常严重。
自定义列全部勾选的场景下(20 行 x 180 列),列表中的开关切换,耗时从原先的 400-500 毫秒下降到 7-8 秒,严重影响用户体验,经过较长时间的性能测试、debug,以碰运气的方式,找到了几处比较核心的优化点。下面通过一个简易的 demo 来模拟我们业务场景,并逐一介绍这些优化点,以及为什么这样优化。
先来看一下 20 行 x 180 列场景下各个优化点的性能测试数据,为排除偶然性,每个场景都会测 3 次。
优化类型 | table 整体渲染耗时 | switch 切换耗时 |
---|---|---|
未优化前 | 6.59s(6.71s、6.49s、6.577s) | 3.982s(3.966s、3.947s、4.033s) |
data 与 columns 从 ref 改 shallowRef 后(耗时减少 17-20%) | 5.18s(5.063s、5.104s、5.363s) | 3.3s(3.175s、3.029s、3.122s) |
getColspanRealWidth 优化(耗时减少 7-20%) | 4.843(4.728s、4.703s 、5.098s) | 2.65s(2.636s、2.645s、2.671s) |
业务优化 - 去除tooltip disable属性后(耗时减少 80%) | 1.008s(1.032s、0.997s、0.994s) | 0.514s(0.517s、0.53s、0.495s) |
大致优化内容如下
- 修改 table 源码,将 data 与 columns 从 ref 改为 shallowRef。
- 修改 table 源码,getColspanRealWidth 函数中响应式数据优化。
- 业务优化:去掉 el-tooltip disabled 属性,改为 if。
# 准备工作
首先初始化一个 vue3 项目,引入 element-plus,并使用 el-table 实现一个 20 行 * 180 列表格。
- 20 行 + 180 列:2 个固定列(一个文本、一个 switch),178 个通过 for 循环创建的自定义列
- 一个显示/隐藏 table 的 switch 开关,用于测试 table 从隐藏到显示,渲染耗时
- 自定义列中有一个 el-tooltip + disabled 逻辑
# 最小化业务 demo 创建
核心 table 代码代码如下,完整代码参见:table-base | table-performance-demo (opens new window)
<el-table
v-if="showTable"
:data="tableData"
style="width: 100%; height: 500px; overflow: scroll"
>
<el-table-column prop="info" label="信息" width="80" fixed />
<el-table-column prop="status" label="状态" width="80" fixed>
<template #default="scope">
<el-switch v-model="scope.row.status" @change="statusChange" />
</template>
</el-table-column>
<el-table-column
v-for="item in customColumns"
:key="item.prop"
:prop="item.prop"
:label="item.label"
>
<template #default="scope">
<el-tooltip
placement="top-start"
:disabled="!(item.prop === 'column1' && scope.row[item.prop])"
>
<template #content>
<span>{{ "tooltip显示" + scope.row[item.prop] }}</span>
</template>
<span>{{ scope.row[item.prop] }}</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<script lang="ts" setup>
// 假数据逻辑
const customColCount = 178; // 自定义列数
const rowCount = 20; // 行数
onBeforeMount(() => {
// 初始化自定义列数据
let temp = [];
for (let i = 0; i < customColCount; i++) {
temp.push({ prop: `column${i + 1}`, label: `第${i + 1}列` });
}
customColumns.value = temp;
// 初始化表格数据
let dataTemp = [];
for (let i = 0; i < rowCount; i++) {
let row: any = { info: `第${i + 1}行`, status: true };
i === 0 && (row.status = false);
for (let j = 0; j < customColCount + 2; j++) {
row[`column${j + 1}`] = `第${i + 1}行${j + 1}列`;
}
dataTemp.push(row);
}
tableData.value = dataTemp;
});
</script>
# 渲染耗时计算逻辑
渲染耗时计算逻辑如下,利用 script 阻塞,来计算渲染耗时
/*
<div v-loading="showLoading" element-loading-text="数据加载中...">
<p>
当前显示:{{ `${rowCount}行${customColCount + 2}列` }}, 显示/隐藏 table:
<el-switch :model-value="showTable" @click="switchTableShow"></el-switch>
</p>
<el-table v-if="showTable"> .... </el-table>
</div>
*/
// 显示/隐藏 table,计算 table 渲染耗时
const switchTableShow = () => {
// 先展示 loading
showLoading.value = true;
// 200ms 后再修改 table 是否显示,防止和 loading 合并到一个渲染周期,导致 loading 不显示
setTimeout(() => {
let startTime = +new Date();
showTable.value = !showTable.value; // 修改 table 显示,会形成 script 阻塞
showLoading.value = false; // 这里的 loading 关闭,会在 table 阻塞完成后渲染关闭 dom
// 创建一个宏任务,等上面阻塞的微任务执行完成后,再显示计算耗时
setTimeout(() => {
let endTime = +new Date();
ElMessage.success(`渲染耗时:${(endTime - startTime) / 1000}s`);
}, 0);
}, 200);
};
# 耗时测试,与 performance 数据对比
table 渲染、switch 切换测试耗时如下
table 隐藏到显示 gif 图
switch 从关到开 gif 图
为了验证我们自己写的耗时测试数据的准确性,这里在 switch 开关时,打开了 performance 录制,具体如下图
页面显示渲染耗时:4.524s,performance 中两个 Long Task:2.29s + 2.17,加上非 Long Task 部分,数据基本一致,因此我们自己写的耗时计算逻辑是基本准确的
另外,开启 performance 录制时,比不录制时要稍微慢点。下面来开始优化吧!
# ref 改 shallowRef
# 理论依据与可行性分析
列表中的开关切换时,table 虽然只是一个节点发生了变化,但依旧触发了完整的 vue patch 比对更新逻辑,耗时较久。
来看一个官方的解释:渲染机制 | Vue.js (opens new window)
理论上,减少响应式数据依赖,就可以提升性能。
shallowRef() 是 ref() 的浅层作用形式。仅当 xx.value 发生变更时,才触发响应更新,减少深层次的响应依赖,可以提升 patch 比对性能。参考 指南 - 减少大型不可变结构的响应性开销 (opens new window)
const state = shallowRef({ count: 1 })
// shallowRef 不会触发更改,如果 state 为 ref 时,是可以触发更新的。
state.value.count = 2
// shallowRef 会触发更改
state.value = { count: 2 }
这里主要修改两种数据从 ref 到 shallowRef
// src/table/src/store/watcher.ts
function useWatcher<T>() {
const data: Ref<T[]> = shallowRef([]); // table data 数据
const columns: Ref<TableColumnCtx<T>[]> = shallowRef([]); // 列数据
// ...
}
这里有个问题,把 data、columns 改为 shallowRef 对功能会不会有影响?
- 首选,每次列表数据更新,我们业务逻辑都会去请求列表,设置 list.value = xxx 可以触发 shallowRef 更新。
- 经过测试,就算是 switch 开关 v-model 绑定的 scope.row.status 变更也可以正常更新。
- 手动点击测试选中、排序、分页等均未发现异常。
基于以上三点,在我们业务中,这个修改是可行的。提醒:如果想在你自己的项目中使用该优化,需要先做好测试。
下面来看具体修改细节
# 拷贝 element-plus table 源码到当前项目
当前最新的版本是 2.2.8,打开 element-plus/releases (opens new window),下载最新版本代码,将 table 目录(element-plus-2.2.28/packages/components/table
) copy 到项目中的 src/table 下,删除目中无用的 __test__
测试目录
新开一个路由,/new 指定到一个新增的 table 组件内,相比原先 table 组件,只增加一行代码,当前组件内使用我们自定义修改的 table。完整代码参见:2-table-use-source | table-performance-demo (opens new window)
import ElTable from "@/table/src/table.vue";
引入后报错 [plugin:vite:import-analysis] Failed to resolve import "@element-plus/directives" from "src\table\src\table.vue". Does the file exist?
做一些修改,让代码可以再我们自己的项目中跑起来,方便修改、调试源码
- 在 table 目录中搜索 @element-plus 相关关键字,并进行批量替换
// @element-plus/directives => element-plus/es/directives/index
// @element-plus/hooks => element-plus/es/hooks/index
// @element-plus/utils => element-plus/es/utils/index
- 搜索
@element-plus/components
改为直接从 'element-plus' 引入
// 比如:
import ElCheckbox from '@element-plus/components/checkbox'
// 改为
import { ElCheckbox } from 'element-plus'
// 注意:资源类的可以不用改,比如 import "@element-plus/components/base/style/css";
# 修改源码 - ref 改为 shallowRef
在 src/table/src/store/watcher.ts 中,将 data 和 columns 数据从 ref 改为 shallowRef,具体代码参:table-ref-shallowRef | table-performance-demo (opens new window)
// src/table/src/store/watcher.ts
function useWatcher<T>() {
const data: Ref<T[]> = shallowRef([]);
const _data: Ref<T[]> = shallowRef([]);
const _columns: Ref<TableColumnCtx<T>[]> = shallowRef([]);
const columns: Ref<TableColumnCtx<T>[]> = shallowRef([]);
// ...
}
另外在 中 表格前面增加下面一行,标记调用的是我们修改的 table 组件
<!-- src/table/src/table.vue 表格顶部增加下面一行 --->
<p style="color: red">来自 table 源码</p>
<!-- 内部逻辑 -->
<div :class="ns.e('inner-wrapper')" :style="tableInnerStyle">
<!-- ... -->
</div>
# 性能数据收集(耗时减少17-20%)
table 渲染、switch 切换测试耗时如下
table 隐藏到显示 gif 图
switch 从关到开 gif 图
# getColspanRealWidth 优化
当页面卡顿时,可以通过 performance 测试性能。下图是点击 switch 开关后的性能数据。可以看到
- 有两个 Scripting 阻塞 longTask,1.89s + 1.73s,整体耗时 3.62s (performance开启时,会变慢一点)
- 主要有两种耗时任务:紫色小块是 render 渲染耗时、绿色小块是 patch 比对耗时,一般 patch 是 vue 内部逻辑,比较难优化
- 通过查看 render 相关耗时,找到 getColspanRealWidth 耗时 212.2ms,这里有优化的空间
我们来查看这个函数耗时的原因,主要是在 tr 渲染时调用该函数,计算每列的宽度
// src\table\src\table-body\render-helper.ts
columns.value.map((column, cellIndex) => {
// ...
columnData.realWidth = getColspanRealWidth(
columns.value,
colspan,
cellIndex
);
// ...
})
具体实现如下,只用到了 realWidth, width 属性,且 columns.value 是响应式依赖,可以修改为非响应式数据,看是否能减少耗时。
// src\table\src\table-body\styles-helper.ts
const getColspanRealWidth = (
columns: TableColumnCtx<T>[],
colspan: number,
index: number
): number => {
if (colspan < 1) {
return columns[index].realWidth
}
const widthArr = columns
.map(({ realWidth, width }) => realWidth || width)
.slice(index, index + colspan)
return Number(
widthArr.reduce((acc, width) => Number(acc) + Number(width), -1)
)
}
这里我们新建 optimizeColumns 变量,存储函数中使用的 realWidth 和 width,将这个非响应式数据传入到 getColspanRealWidth 函数内部使用。完整代码参见 getColspanRealWidth-optimize | table-performance-demo (opens new window)
// src\table\src\table-body\render-helper.ts
const optimizeColumns = columns.value.map((item) => {
return { realWidth: item.realWidth, width: item.width };
});
columns.value.map((column, cellIndex) => {
// ...
columnData.realWidth = getColspanRealWidth(
optimizeColumns, // 传入函数内部时,使用非响应式数据
colspan,
cellIndex
);
// ...
})
# 耗时从 200ms 下降到 0.7ms
修改好后再次测试性能,惊喜的发现,这个函数的耗时从 200ms+ 下降到 1ms 内,render 性能明显提升。1.54s + 1.45s = 2.99s
# 性能数据收集(耗时减少7-20%)
table 渲染、switch 切换测试耗时如下
table 隐藏到显示 gif 图
switch 从关到开 gif 图
# 业务优化 tooltip disabled 改 if
经过上面的优化后,我们意识到,即使是很细微的响应式数据优化,也会对性能带来较大影响。那业务逻辑中是否也存在这样的数据呢?
于是采用注释 + 将 el-table-column 插槽换成静态节点 <span>123</span>
的方法,测试具体是哪里耗时较长,然后针对性优化。
经过测试,发现将自定义列中的 el-tooltip 换成静态节点后,性能有极大提升。
<el-table-column
v-for="item in customColumns"
:key="item.prop"
:prop="item.prop"
:label="item.label"
>
<template #default="scope">
<!-- <el-tooltip
placement="top-start"
:disabled="!(item.prop === 'column1' && scope.row[item.prop])"
>
<template #content>
<span>{{ "tooltip显示" + scope.row[item.prop] }}</span>
</template>
<span>{{ scope.row[item.prop] }}</span>
</el-tooltip> -->
<span>123</span>
</template>
</el-table-column>
如下图,switch 开关切换耗时从 2.7s 左右减少到 0.5s 左右。performance 面板可以看到 patch 基本没有了,应该是模板编译时静态节点标记后,更新时就不用比对了。
基于这个思路,el-tooltip 组件会成倍的增加 patch 比对耗时,减少这个节点数量即可增强性能。
为了少些一些代码,el-tooltip 使用 disabled 属性,用于在特定场景下隐藏 tooltip,这一部分数据可以不使用 el-tooltip 节点,改动如下,使用 v-if 替换 disabled 属性功能,这样虽然会有重复代码,但可以减少节点数。
<template #default="scope">
<!--
<el-tooltip
placement="top-start"
:disabled="!(item.prop === 'column1' && scope.row[item.prop])"
>
<template #content>
<span>{{ "tooltip显示" + scope.row[item.prop] }}</span>
</template>
<span>{{ scope.row[item.prop] }}</span>
</el-tooltip>
-->
<span v-if="!(item.prop === 'column1' && scope.row[item.prop])">
{{ scope.row[item.prop] }}
</span>
<el-tooltip v-else placement="top-start">
<template #content>
<span>{{ "tooltip显示" + scope.row[item.prop] }}</span>
</template>
<span>{{ scope.row[item.prop] }}</span>
</el-tooltip>
</template>
再次测试性能,可以看到性能并没有下降多少,switch 开关切换可以做到 0.5s 左右刷新
# 性能数据收集(耗时减少80%左右)
table 渲染、switch 切换测试耗时如下
table 隐藏到显示 gif 图
switch 从关到开 gif 图
# 总结
在 vue3 项目中,响应式数据这块要特别注意。当遇到比较慢的场景时,建议采用如下方法进行性能优化:
- 使用 performance 分析性能瓶颈,或者自己写一个性能耗时逻辑,这样在做性能优化时有数据参考。
- 针对业务代码较多场景,采用注释 + 替换成静态节点方法排查耗时较长的逻辑,针对性优化。
- 另外,也可以使用 Vue devtools 调试工具,也可以测试组件渲染耗时,排查响应式数据问题
# 参考
# let 解构声明变量时,右侧必须做 || {} 处理
vue2 升级 vue3 插件时,v-infinite-scroll 组件引入,会报 TypeError: Cannot destructure property 'container' of 'el[SCOPE]' as it is undefined.
,查看源码,infinite-scroll - element-plus (opens new window) 发现这里缺少值异常处理。
unmounted(el) {
// 这里有问题,没有做判空处理,el[SCOPE] 可能是 undefined,会报错
const { container, onScroll } = el[SCOPE]
container?.removeEventListener('scroll', onScroll)
destroyObserver(el)
},
针对 let 或 const 结构定义变量时,表达式右侧必须是对象。需要做异常处理
const { container, onScroll } = el[SCOPE] || {} // 这样就不会报错了
这里只是一个位置,这个文件有多个这样的定义。
# 项目内配置 eslint 插件保存后自动 fix
项目下,新建 .vscode 目录,创建 settings.json 文件,写入如下内容。这样,就算项目组其他人员 vscode 没有配置保存后自动 fix,在当前项目也可以保存后 fix。
{
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
注意 vue3 脚手架默认生成的项目中,.gitignore 会忽略这个目录下的文件,可能需要修改下
# .gitignore 文件修改
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
# 2023-01-26
# Babel 手册(babel 学系教程 11.5k star)
这本手册分为两个部分:
- 用户手册 (opens new window)-如何安装/配置 Babel 及相关内容。
- 插件手册 (opens new window)-如何为 Babel 创建插件。
# vue 文档 - 不让人讨厌的广告风格 - 万维广告
万维广告成功客户案例 & 广告创意巡展 (opens new window)
# 书籍《深入浅出 Vue.js》- vue2 源码学习
大致看了一遍这本书,但细节还是不懂,核心的 patch 比对算法、模板编译、响应式这三块还不是很理解,待整理笔记
- Vue2 源码总结梳理(有思维导图、面试题问答) (opens new window)
- 聊聊vue2.5的patch过程(diff算法) (opens new window)
- 聊聊 Vue 的双端 diff 算法 (opens new window)
- 为什么说 Vue 的响应式更新精确到组件级别?(原理深度解析) (opens new window)
- 林三心画了8张图,最通俗易懂的Vue3响应式核心原理解析 (opens new window)
- 转Vue 3前再读一次Vue2源码 (opens new window)
- 他的 github blog 仓库文章质量不错 https://github.com/amandakelake/blog (opens new window)
# web 安全 - ddos 防御相关
怎么防御 ddos 攻击,参考
- DDOS攻击事件记录 网站遭到DDOS攻击及处理 (opens new window)
- nginx防止DDOS攻击配置(一) (opens new window)
- nginx防止DDOS攻击配置(二) (opens new window)
# 什么是 cc 攻击
如果攻击者并没有打算攻陷 http://a.com
或从它偷取数据,而是频繁向 http://a.com
发送消耗大量资源的请求,比如请求会使用大量数据库查询的接口,或上传大量文件,导致正常服务无法响应。这种方式叫做CC攻击(ChallengeCollapsar)。如果攻击者并没有打算攻陷 http://a.com
或从它偷取数据,而是频繁向http://a.com
发送消耗大量资源的请求,比如请求会使用大量数据库查询的接口,或上传大量文件,导致正常服务无法响应。这种方式叫做CC攻击(Challenge Collapsar, 直译:挑战黑洞)。
# 什么是 WAF
WAF入门扫盲篇(一遍就懂) (opens new window)
# linux 教程
linux 入门 - 菜鸟教程 (opens new window)
# 2023-01-22
# mac gif 录制无水印、免费
之前用的 Gifox,挺好用的,但就是免费版,有水印。
于是想找个免费、无水印的,最后找到了 GIPHY CAPTURE,app store 中下载,免费、好用、可编辑、无水印。
# MacBook Pro 新品发布官网动画效果实现(2023年01月)
canvas 播放动画帧、video 动画
体验地址:nice.zuo11.com (opens new window)
代码:MacBook Pro 新品发布官网动画效果实现(2023年01月) (opens new window)
# node.js+axios 批量下载网页静态图片资源
先试试下载一张图片
const axios = require('axios');
const fs = require('fs')
const path = require('path')
let BASE_URL = 'https://www.apple.com/105/media/us/macbook-pro-14-and-16/2022/1baf5961-c793-48e7-9efd-0d23cac1e101/anim/m2_pro/medium/medium_0051.jpg'
axios.get(BASE_URL, {
responseType: 'arraybuffer'
})
.then(function (response) {
// handle success
let fileNameArr = BASE_URL.split('/')
let fileName = fileNameArr[fileNameArr.length - 1] // medium_0051.jpg
fs.writeFileSync(path.resolve(__dirname, `./${fileName}`), response.data)
})
.catch(function (error) {
// handle error
console.log(error);
})
.finally(function () {
// always executed
});
下载完成后,效果如下图
我们稍加修改,就可以批量下载图片了。注意批量下载时,如果要存到一个不存在的文件夹,那么需要先手动创建该文件夹(当然也可以使用 fs API 创建)
以 macbook pro 官网为例,下载 m2 pro 和 m2 max 各 53 张图片,代码如下
// batch-index.js
// M2 pro 芯片切换动画 0000.jpg => 0052.png
// https://www.apple.com/105/media/us/macbook-pro-14-and-16/2022/1baf5961-c793-48e7-9efd-0d23cac1e101/anim/m2_pro/medium/medium_0051.jpg
// M2 max 芯片切换
// https://www.apple.com/105/media/us/macbook-pro-14-and-16/2022/1baf5961-c793-48e7-9efd-0d23cac1e101/anim/m2_max/medium/medium_0000.jpg
const axios = require('axios');
const fs = require('fs')
const path = require('path')
/**
* 获取静态图片链接
* @param {*} mode m2_pro 或 m2_max
* @param {*} numStr '00' => '52'
*/
let getFileUrl = (mode, numStr) => `https://www.apple.com/105/media/us/macbook-pro-14-and-16/2022/1baf5961-c793-48e7-9efd-0d23cac1e101/anim/${mode}/medium/medium_00${numStr}.jpg`
const downloadImgFromUrl = (mode, fileUrl) => {
axios.get(fileUrl, {
responseType: 'arraybuffer'
})
.then(function (response) {
// handle success
let fileNameArr = fileUrl.split('/')
let fileName = fileNameArr[fileNameArr.length - 1]
fs.writeFileSync(path.resolve(__dirname, `./download/${mode}/${fileName}`), response.data)
console.log('下载完成')
})
.catch(function (error) {
// handle error
console.log(error);
})
.finally(function () {
// always executed
});
}
for (let i = 0, len = 52; i <= len; i++) {
let numStr = i + ''
if (numStr.length < 2) {
numStr = `0${numStr}` // 0 => '00', 9 => '09'
}
downloadImgFromUrl('m2_pro', getFileUrl('m2_pro', numStr))
downloadImgFromUrl('m2_max', getFileUrl('m2_max', numStr))
}
这样就可以拿到 106 张图片了
# 2023-01-17
# 6 行代码实现局域网快速传图片/视频/文件
上个视频用 windows 电脑录制好后,需要传到 mac 电脑上剪辑,用微信发比较慢,又没有 U 盘,于是想着,通过局域网来传输视频。
思路:koa.js 支持静态服务,本地电脑开启一个静态服务,目录指向需要传送的文件,其他电脑通过局域网 ip 径访问对应服务,即可拿到文件。
创建 index.js 文件,写入如下代码
const Koa = require('koa')
const KoaStatic = require('koa-static')
const path = require('path')
const app = new Koa()
app.use(new KoaStatic(path.resolve(__dirname, './files')))
app.listen(8081, () => console.log('服务开启于 8081 端口'))
1、安装依赖:如果没有安装 node,先安装 node,安装好后,在 index.js 目录下 npm init; npm install
2、将需要传送的文件访到 files 目录下,node index.js 运行服务
3、访问 http://127.0.0.1:8081/2023-w2.mp4 (opens new window) 测试是否能正常访问
4、查看本机 ip,假设是 192.168.1.8,其他电脑通过 http://192.168.1.8:8081/2023-w2.mp4 (opens new window) 即可下载对应文件
缺点:一次仅支持单个文件,对于多个文件可以先压缩成一个 zip 再传输
# 2023-01-16
# 怎么解压 gz 文件
gzip 命令用法(gzip -h)
注意 center os 7.x 版本中,不支持 -k 选项,但 macos 看了下是支持的
-k --keep don't delete input files during operation
# 服务器被 ddos 攻击后,怎么快速恢复服务,查看日志经验总结
个人服务器被 ddos 攻击了,8G 流量就挂了,然后腾讯云封服务器外网 ip,导致上面的所有服务异常。
什么是 DDOS 攻击
DDOS又称为分布式拒绝服务攻击,全称是Distributed Denial os Service。DDOS本是利用合理的请求造成资源过载,导致服务不可用。比如一个停车场总共有100个车位,当100个车位都停满车后,再有车想要停进来,就必须等已有的车先出去才行。如果已有的车一直不出去,那么停车场的入口就会排起长队,停车场的负荷过载,不能正常工作了,这种情况就是“拒绝服务”。
ip 大概在 ddos 攻击结束 1.5 小时后解封(攻击开始后 2 小时后)。
参考:DDOS攻击的那些事 (opens new window)
复盘
- 动静服务最好分离,静态服务抗攻击一点,可以把静态的服务都放到第三方,比如 github、gitee、阿里云 OSS 等。这次在 oss 的服务没问题,自己服务的静态服务都挂了。
- 购买 ddos 防护产品(一般价格较高,非重要服务或预算有限忽略)
- 关于服务的快速恢复
- 更换服务器 ip,再更新域名 dns 解析。
- 弄一台新的服务器,快速部署服务,再更新 dns 解析
- 记录每个服务在新服务器上部署时,依赖的环境、安装步骤,方便后面快速恢复。
- 部署过程是否可以脚本化
- 排查日志,追溯源头(腾讯云 1 月 16 日,22:08 发送通知,被攻击)
- visitors 统计系统日志查看,如下图,有人使用 https://site.ip138.com (opens new window) 查看 zuo11.com 站点解析 ip 后,visitors 服务就挂了,时间点也对的上,都是新用户,分散在 4 个地区。(visitors访客记录这个轮子的价值在这里就体现出来了,在出问题后,可以捕捉到一些信息,比什么信息都没有好)
mysql> select id,ip,region,count,referer,time from base where time like '%2023-01-16%' order by time desc limit 15;
+-------+-----------------+------------------------------+-------+---------------------------------------+---------------------+
| id | ip | region | count | referer | time |
+-------+-----------------+------------------------------+-------+---------------------------------------+---------------------+
| 72172 | 117.165.208.47 | 江西省吉安市 移动 | 0 | https://site.ip138.com/www.zuo11.com/ | 2023-01-16 22:07:59 |
| 72171 | 27.227.175.62 | 甘肃省兰州市 电信 | 0 | https://site.ip138.com/www.zuo11.com/ | 2023-01-16 22:07:39 |
| 72170 | 223.146.137.67 | 湖南省衡阳市 电信 | 0 | https://site.ip138.com/www.zuo11.com/ | 2023-01-16 22:07:36 |
| 72169 | 223.146.137.67 | 湖南省衡阳市 电信 | 0 | https://site.ip138.com/www.zuo11.com/ | 2023-01-16 22:07:35 |
| 72168 | 223.198.82.167 | 海南省儋州市 电信 | 0 | https://site.ip138.com/www.zuo11.com/ | 2023-01-16 22:07:27 |
| 72167 | 123.131.144.178 | 山东省临沂市 联通 | 0 | | 2023-01-16 22:07:24 |
| 72166 | 39.144.142.15 | 中国移动 | 0 | https://m.baidu.com/ | 2023-01-16 21:58:46 |
| 72165 | 220.196.160.101 | 江苏省常州市 联通 | 0 | | 2023-01-16 21:43:25 |
| 72164 | 124.78.115.166 | 上海市浦东新区 电信 | 0 | http://www.zuo11.com/ | 2023-01-16 21:33:54 |
| 72163 | 124.78.115.166 | 上海市浦东新区 电信 | 0 | | 2023-01-16 21:33:45 |
| 72162 | 220.196.160.75 | 江苏省常州市 联通 | 0 | | 2023-01-16 21:06:19 |
| 72161 | 220.196.160.95 | 江苏省常州市 联通 | 0 | | 2023-01-16 21:03:00 |
| 72160 | | 广东省广州市 电信 | 0 | https://www.google.com/ | 2023-01-16 21:01:19 |
| 72159 | 139.162.29.108 | 新加坡 | 0 | https://www.google.com/ | 2023-01-16 21:01:18 |
| 72158 | 220.196.160.101 | 江苏省常州市 联通 | 0 | | 2023-01-16 20:58:49 |
+-------+-----------------+------------------------------+-------+---------------------------------------+---------------------+
15 rows in set (0.18 sec)
对应 ua
Mozilla/5.0 (Linux; Android 12; M2104K10AC Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36 T7/13.22 SP-engine/2.60.0 baiduboxapp/13.22.0.10 (Baidu; P1 12) NABar/1.0
Mozilla/5.0 (Linux; Android 12; TAS-AN00 Build/HUAWEITAS-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36 T7/13.22 BDOS/1.0 (HarmonyOS 3.0.0) SP-engine/2.60.0 baiduboxapp/13.22.0.10 (Baidu; P1 12) NABar/1.0
Mozilla/5.0 (Linux; Android 11; HarmonyOS; CTR-AL00; HMSCore 6.9.0.302) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.88 HuaweiBrowser/13.0.1.301 Mobile Safari/537.36
Mozilla/5.0 (Linux; Android 11; HarmonyOS; CTR-AL00; HMSCore 6.9.0.302) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.88 HuaweiBrowser/13.0.1.301 Mobile Safari/537.36
Mozilla/5.0 (Linux; U; Android 10; zh-cn; Mi 10 Build/QKQ1.191117.002) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36 XiaoMi/MiuiBrowser/17.4.80113 swan-mibrowser
- nginx 日志查看,在 vistors 访客记录汇总,被攻击当天最后一条记录是 22:07:59 后面的记录就没了,查看 ng 看是否有记录一些信息
# linux 服务器 nginx 日志查看
# ng 目录 /etc/nginx
# 在 /etc/nginx/nginx.conf 配置中,查询 log 关键字
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log main;
查看 nginx 日志文件
ls /var/log/nginx/
# 或者
cd /var/log/nginx/
ls
找到对应的 gz 文件进行解压
# 解压文件 -d --decompress uncompress files
gzip -d error.log-20230116.gz
gzip -d access.log-20230116.gz
注意解压后的文件就是 file 类型,不是目录,直接 cat + grep 即可搜索指定内容
# 查看 01月17号日志文件中 10 点数据
cat access.log-20230117 | grep '2023:10'
这里我不小心把 access.log-20230116.gz 解压后的文件不小心删了,需要注意 gzip -d 解压后,gz 文件会被删除...于是查看了 error.log-20230116.gz 解压后的日志
发现上图中 16 号 03:45 后的日志没有存放在这个文件中,且文件开头的 15 号的日志,所以又查看了 17 号的日志,如下图,ng 错误日志并没有特殊异常。
1 月 16 日,22:08 发起攻击时,没有任何记录,可惜 access log 被 rm 了,恢复又比较麻烦,就算了。下次出现问题,有了这次的经验后,下次查看日志就快了。
# 2023-01-15
# s.zuo11.com 短链接服务,根据配置中心配置,匹配路径跳转到对应页面
对应项目地址:https://github.com/zuoxiaobai/s.zuo11.com (opens new window)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dev-zuo短链接服务</title>
</head>
<body>
<script>
fetch("http://zuo11.com:5000/share/shortLink/list?_id=63c430837fc644a8c1b0e9fd&pageSize=100")
.then((res) => {
console.log(res);
return res.json();
})
.then((res) => {
let list = res?.data?.list || []
let info = {}
list.forEach((item) => {
info[item.shortLink] = item.redirect
})
console.log(info)
if (info[location.pathname]) {
window.location.href = info[location.pathname]
} else {
document.write('不存在短链接 ' + location.pathname)
}
})
.catch(e => {
alert('短链接服务接口异常', e?.message);
})
</script>
</body>
</html>
# Vue3+Node.js jwt 登录、接口鉴权实现
jwt 即 json web token,介绍参考:JSON Web Token 入门教程 -阮一峰 (opens new window)
1、点击登录后,查询用户名+密码是否正确,如果正确,使用 jwt 生成一个 token,返回给前端。用于之后请求的凭证,后端不保存 token。
// koa.js 登录接口逻辑
const jwt = require('jsonwebtoken');
const privateKey = 'xxxx_privateKey';
// /user/login 接口判断登录成功后,生成 token 并返回
let token = jwt.sign(
{
exp: Math.floor(Date.now() / 1000) + 60 * 60, // 有效期 1h,单位 s
data: { name, _id: userInfo._id } // 将用户 id、name 等必要信息存放到 token 中,一般下次请求时通过 token 获取用户 id
},
privateKey
);
ctx.body = {
code: 0,
data: { ...userInfo, token }, // 将 token 返回给前端,前端下次所有请求,都需需要将 token 放到请求头里面
msg: '成功'
};
2、前端请求拦截逻辑中,在请求头里面携带 token 信息,为了防止刷新页面后,token 丢失,需要将 token 存入 localStorage 或 cookie 中
// 前端请求登录接口返回成功后, 将返回的 token 存到 localStorage
const onSubmit = async () => {
const res: any = await axios.post("/user/login", {
name: form.name,
password: form.password,
});
if (res.code === 0) {
ElMessage.success("登录成功");
Object.assign(accountInfo, res.data); // 将用户信息/token 写入 pinia 状态管理
localStorage.setItem("config-fe-token", res?.data?.token); // 防止刷新页面后 token 丢失
router.push({ name: "home" });
}
};
// axios 全局请求拦截
instance.interceptors.request.use(
async function (config) {
console.log("request 拦截: ", config);
// config.headers = { token: accountInfo.token };
// 防止刷新后,状态管理数据清空,导致找不到 token
config.headers = {
token: localStorage.getItem("config-fe-token"),
};
return config; // 用来请求的参数
}
);
3、服务器端 koa 全局拦截中间件,jwt 鉴权,成功后,解析出 token 携带的用户 id,进行逻辑处理
// 鉴权,判断是否有登录权限
app.use(async (ctx, next) => {
console.log(ctx.path); // /shortLink/list、/shortLink/add
let whiteList = ['/user/login'];
// 如果不需要鉴权,直接继续,不拦截
if (whiteList.includes(ctx.path) || ctx.path.startsWith('/share/') || ctx.userInfo) {
await next();
return;
}
let { token } = ctx.request.header;
try {
let decoded = jwt.verify(token, privateKey);
// console.log('decode', decoded.data, typeof decoded.data); // { name: 'admin', _id: '63c3babac401a248bd88988a' } Object
ctx.userInfo = { ...decoded.data, token }; // JSON.parse 会异常,导致中断,所以加 catch
console.log('解析成功', ctx.userInfo);
await next();
} catch (err) {
ctx.body = {
code: -1,
msg: '未登录,请先登录',
plainMsg: err.name + ': ' + err.message
};
}
});
后端会将 token 过期,或 token 异常的场景拦截,并返回未登录 -1 的 code。前端在 axios 响应拦截位置,拦截对应的错误码,并跳转到登录页面
instance.interceptors.response.use(
function (response) {
console.log("响应拦截", response);
const { code, msg, plainMsg } = response.data;
if (code !== 0) {
ElMessage.error(plainMsg ? `${msg}: ${plainMsg}` : msg);
// 如果是没有登录,跳转到登录页面
if (code === NOT_LOGIN_CODE) {
router.push("/login");
}
}
return response.data; // 过滤掉除data参数外的其它参数,响应接收到的值。
}
);
# 阿里云OSS前端静态页面不能通过访问目录直接知道 index.html 需要访问全路径问题
在将 nice-func (opens new window) 项目部署到 nice.zuo11.com 时,将对应的静态文件放到阿里云对应的 OSS 后,发现 nice.zuo11.com 并不能访问,只能通过全路径 http://nice.zuo11.com/index.html (opens new window) 来访问。
因为除了将 自定义域名添加 CNAME 解析到 OSS 外,还需要在 OSS 管理对应域名,另外对于静态页面,需要配置静态页面入口,才能通过访问目录直接访问 indeex.html,如下图
# 2023-01-14
# 为什么在 windows chrome network 中同一个接口一次会有两条请求记录
因为,跨域的场景,浏览器会先发送一条 options 请求预检,来判断是否允许跨域。参考:利用koa来彻底理解web前端跨域问题 (opens new window)
跨域options请求预检
跨域真实请求
koa 允许跨域中间件
/**
* 允许跨域
* 作者:一只正在成长的程序猿
* 链接:https://juejin.cn/post/6844904042196533255
*/
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', '*');
ctx.set('Access-Control-Allow-Methods', '*');
// 处理请求预检
if (ctx.method == 'OPTIONS') {
ctx.body = 200;
} else {
await next();
}
});
# 用不同的前端开发框架开发同一个功能10次,框架入门练手小项目
我用不同的前端框架开发一个功能10次--哪个才是最好的JS框架呢 (opens new window)
这个项目内容比较少,不包含 UI 框架/接口请求功能,于是想着用之前做的配置中心功能,来做学习框架的练手项目
目前是基于 vue3+ts+vite+ElementPlus+sass+axios 来实现的,后面会用 vue2,react 等框架来再次实现该前端功能。地址:https://github.com/zuoxiaobai/fe-framework-study (opens new window)
配置中心-短链接管理(增删改查/分页/新增修改弹窗/模糊查询/防抖)
相关接口
- 获取配置列表 GET /shortLink/list?t=1673710063475&queryText=模糊查询参数¤tPage=1&pageSize=20
- 新增配置 POST /shortLink/add 参数:{ redirect: "23232323", shortLink: "2323" }
- 修改配置 POST /shortLink/edit 参数:{ _id: "63bee1d8c0351e3aa068e124", redirect: "23232323", shortLink: "2323" }
- 删除配置 POST /shortLink/del 参数:{ _id: "63bee1d8c0351e3aa068e124" }
# 向下滚动切换手机颜色效果实现 gsap+ScrollTrigger
原始链接:iQOO Neo7 - vivo官方网站 (opens new window)
通过审查元素,大致了解元素变动逻辑,然后学习 gsap + ScrollTrigger 实现该功能,访问地址 nice.zuo11.com (opens new window),可以看到实现的效果,代码地址:https://github.com/zuoxiaobai/nice-func (opens new window)
# img 标签和 div background image 都可以显示图片,使用 css 背景图片加载图片有什么好处
css 设置图片可以通过媒体查询指定不同的分辨率使用不用的图片,不依赖 JS
@media (max-width: 1440px)
.iqooneo7-color .umx-stickyBox .color-box .umx-figure .f-mask figure.umx-f1 {
background-image: url(../images/iqooneo7-color-img1-md.png);
background-size: 461px 605px;
background-position: center;
background-repeat: no-repeat;
width: 461px;
height: 605px;
}
@media (max-width: 1600px)
.iqooneo7-color .umx-stickyBox .color-box .umx-figure .f-mask figure.umx-f1 {
background-image: url(../images/iqooneo7-color-img1-lg.png);
background-size: 491px 644px;
background-position: center;
background-repeat: no-repeat;
width: 491px;
height: 644px;
}
# 2023-01-12
# mysql 停止服务,查看 mysql 是否已经启动,mysql 修改密码
linux 下 mysql 服务器操作命令
# 停止服务
service mysqld stop
# 开启服务
service mysqld start
# 重启服务
service mysqld restart
# 查看 mysql 运行状态/是否启动
service mysqld status
mysql 修改密码,先来看 v5.x 版本修改方式,参考:MySQL修改密码的3种方式 (opens new window)
mysql> set password for root@localhost = password('xxx');
#ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'password('xxx')' at line 1
8.x 版本使用这个命令会提示上面的错误,关键字:"right syntax to use near 'password("
下面是 8.x 版本修改命令
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '新密码';
修改时发现提示如下错误
Operation ALTER USER failed for 'root'@'localhost'
需要把 localhost 换成 %,参考:MySQL修改密码报错ERROR 1396 (HY000): Operation ALTER USER failed for ‘root‘@‘localhost‘ (opens new window)
mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '新密码';
Query OK, 0 rows affected (0.01 sec)
mysql> select user,host from user where user='root';
+------+------+
| user | host |
+------+------+
| root | % |
+------+------+
1 row in set (0.00 sec)
重启后还是异常,提示
52|visitor | [Nest] 17558 - 01/12/2023, 2:36:07 AM ERROR [ExceptionsHandler] Access denied for user 'root'@'localhost' (using password: YES)
52|visitor | Error: Access denied for user 'root'@'localhost' (using password: YES)
后面重启了接口服务就正常了。
另外,mysql 只有 root@% 没有 root@localhost 怎么回事?
参考:mysql里面的root@%是什么?与root@localhost的区别是什么? (opens new window)
@后面的%通配任意地址
localhost指代本地机器
一个指明root用户名许可从任意地址访问
一个指明root用户仅允许本地登录
# 2023-01-11
# vue3+vite 在 linux 服务器构建/部署不成功的问题
使用 zuo-deploy 自动化更新部署时,发现前后端代码都不是最新的,后面发现原因是
本地 package-lock.json 文件修改,执行 git pull 不成功,但依旧会继续运行 shell,走 build 命令,提示部署成功。
但 git pull 没拉取最新的代码,就没部署成功。
原因:本地 node 版本和服务器 node 版本不一致,导致上一次安装时,文件就变更了,远程改动也修改了 package-lock.json 导致 git pull 拉取失败
处理方法,在 部署 shell 脚本中,git pull 前,加上 rm -rf package-lock.json 即可。
# 首次部署
# cd /root;
# git clone git@github.com:zuoxiaobai/zuo-config-fe.git;
# npm install
# npm run build
# 持续集成
cd /root/zuo-config-fe;
#npm install
git config --global core.quotepath false # 防止中文乱码
echo "git pull"
rm -rf package-lock.json
git pull
git log -1 # 查看最近一次提交 log
npm run build;
echo '部署完成'
另外最近还发现,部署时 npm run build 后 dist 文件中只有 favicon.ico,并没有 assets 文件,后面把整个文件删了,重新 git clone 再部署才行,对应脚本
cd /root;
rm -rf zuo-config-fe
git clone git@github.com:zuoxiaobai/zuo-config-fe.git;
cd zuo-config-fe
git config --global core.quotepath false; # 防止中文乱码
git log -1; # 查看最近一次提交 log
npm install
npm run build
echo '部署完成';
虽然每次都会把之前的文件删除,重新 git clone, npm install,但部署不会出问题,稳定性好。
# koa+mongodb node 项目引入 eslint+prettier 保存后自动 fix
翻看之前的笔记,按照之前记录的来配置:node 项目从 0 到 1 引入 eslint + prettier, 支持 es module (opens new window)
配置好后提示:Parsing error: The keyword 'const' is reserved
是 .eslintrc 中 module.exports 拼写错误
另外配置后,eslint 并没有生效,通过 OUTPUT ESLlint log 查询,发现是 jest 配置问题,去掉即可
修改后,就 OK 了,配置如下
module.exports = {
env: {
node: true,
es2021: true
},
plugins: ['prettier'],
extends: ['eslint:recommended', 'prettier'],
parserOptions: {
ecmaVersion: 13,
sourceType: 'module'
},
rules: {
'prettier/prettier': 'error'
}
};
# mongodb 模糊查询、分页查询、查询数量相关命令
- 模糊查询使用 { $regex: 模糊查询字符串 },类似于 mysql like '%模糊查询字符串%'
- 分页使用 limit(20).skip(20)
- 查询总数使用 find().count()
下面是 nodejs 操作 mongodb 模糊查询+分页逻辑
let queryRule = {};
if (queryStr) {
queryRule = {
$or: [
{ shortLink: { $regex: queryStr } }, // ,模糊查询 queryStr
{ redirect: { $regex: queryStr } },
],
};
}
// 页码 - skip数据量 - 公式
// 1 0 (pageIndex - 1) * pageSize
// 2 pageSize
// 3
const list = await db
.collection("short-link")
.find(queryRule)
.limit(pageSize)
.skip((pageIndex - 1) * pageSize)
.toArray();
const total = await db.collection("short-link").find(queryRule).count();
# visitors nest.js 接口异常排查 this is incompatible with sql_mode=only_full_group_by
登录到服务器,查询日志。
SELECT *,count(*) as pageCount from base where siteId = '183281668cc3440449274d1f93c04de6' GROUP BY uuid ORDER BY time desc LIMIT 0,20;
52|visitors | [Nest] 17558 - 01/11/2023, 11:24:27 PM ERROR [ExceptionsHandler] Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'zuo_statistics.base.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
52|visitors | QueryFailedError: Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'zuo_statistics.base.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
本地指定该 sql 是正常的,但服务器执行该语句提示异常。(直接使用 mysql -uroot -p 登录到 shell,use 对应 db,执行该语句。)
mysql> status 查看版本,本地开发电脑是 8.0.27,Linux 服务器版本是 8.0.28
Server version: 8.0.27 MySQL Community Server - GPL
应该是不同的系统,配置不一样。导致异常。重点信息:
this is incompatible with sql_mode=only_full_group_by
手动用命令设置(不推荐,推荐下面配置文件的方式)
mysql> SET sql_mode ='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
ERROR 1231 (42000): Variable 'sql_mode' can't be set to the value of 'NO_AUTO_CREATE_USER'
mysql> SET sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'
推荐直接修改配置文件,参考:MySQL错误-this is incompatible with sql_mode=only_full_group_by完美解决方案 (opens new window)
查看配置 cat /etc/my.cnf,如果存在,就修改配置,增加 sql-mode 配置
[root@VM-0-13-centos ~]#
[root@VM-0-13-centos ~]# cat /etc/my.cnf
# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/8.0/en/server-configuration-defaults.html
[mysqld]
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# 之前的配置 ....
sql-mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
重启 mysql,让配置生效
service mysqld restart
检查 sql_mode 是否配置成功
# 进入 mysql shell
select @@GLOBAL.sql_mode;
# win激活
https://github.com/zbezj/HEU_KMS_Activator/releases (opens new window)
# 2023-01-10
# mongodb Node.js 提示 Topology is closed
在没有安装 mongodb 数据库之前,接口返回 Topology is closed,但是 linux 服务器上安装完成后,mongo 命令行方式,是可以查询到数据的。
网上查了一些,都没有用,于是想到看看 pm2 log,接口日志。如下图
pm2 logs --lines 300m config.zuo11.com # 查看 config.zuo11.com 服务最近 300 行 log
提示 MongooseServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017
突然想起来,mongodb 数据库安装完成之后,接口服务并没有重启,于是重启了服务,重启后服务就正常了。
pm2 delete config.zuo11.com
pm2 start src/index.js -n 'config.zuo11.com'
再次访问 config.zuo11.com 短链接就可以正常的增删改查了。
# MongoDB 命令行操作
在 linux 服务器进行操作时,可能需要使用命令
# 连接/创建数据库, use 数据库名称
use mp-cloud-db
# 创建集合 db.createCollection("集合名称")
db.createCollection("sales")
# 查询集合数据(文档)db.集合名称.find(可选查询条件对象,projectFileds字段过滤对象 select a,b from)
# 以易读的方式来读取数据,可以使用 pretty() 方法
# https://www.mongodb.com/docs/manual/tutorial/getting-started/
db.sales.find()
db.sales.find().pretty()
db.col.find({"likes": {$gt:50}, $or: [{"by": "菜鸟教程"},{"title": "MongoDB 教程"}]}).pretty() # 'where likes>50 AND (by = '菜鸟教程' OR title = 'MongoDB 教程')'
# 集合数据(文档)新增 db.集合名称.insert(document) https://www.runoob.com/mongodb/mongodb-insert.html
db.sales.insert({name: 1})
db.sales.insertMany([{name: 1},{name:2}] ) // 多条文档数据
# 改 db.collection.update(<query>,<update>,options)
db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB'}}) # 仅修改第一个记录
db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB'}},{multi:true})
# 删
db.col.remove({'title':'MongoDB 教程'})
参考:
# mongodb 怎么部署到 linux/centeros 安装 mongodb
首先 Linux 安装 mongodb 建议先在 windows 下安装好 mongodb 后,通过命令行方式连接操作 mongodb,熟悉后,方便在 Linux 命令行中操作
安装教程参考:
注意点:建议不要使用新的 6.x 版本,安装 4.x 版本。因为 6.0 开始,/bin/ 目录下不再有 mongo.exe/mongo 文件,网上 90% 教程都失效。需要额外在安装 mongosh (opens new window) shell 工具。
mongodb 4.x 版本也没什么,腾讯云数据库用的也是这个版本。另外 6.x 版本在安装使用过程中,比较麻烦,按照网上的教程基本启动不起来服务。使用 4.x 版本,可以很快的完成安装,并连接成功。
# windows 安装过程记录
进入 https://www.mongodb.com/try/download/community (opens new window) 选择 4.x 版本 msi 下载后安装,选择 custom 自定义安装目录,一般会安装在 C:\Program Files\MongoDB 目录下。
安装完成后,进入 C:\Program Files\MongoDB\Server\4.4\bin 目录,可以看到 mongo.exe,mongod.exe 文件就安装成了。
进入 C 盘,创建数据目录 data,data 目录下创建文件夹 db,路径为 C:/data/db
运行 mongodb 服务,并指定数据目录
# 进入 C:\Program Files\MongoDB\Server\4.4\bin,运行如下命令
./mongod --dbpath c:\data\db
连接 mongodb,运行下面的命令,能够进入 shell,就成功了
# 进入 C:\Program Files\MongoDB\Server\4.4\bin
./mongo
# CenterOs 7.x 安装 mongodb 记录
# 升级基础库
sudo yum install libcurl openssl
# 下载安装包,链接来源:在 https://www.mongodb.com/try/download/community 选择 4.x centeros7.x 版本,copy link
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.4.18.tgz
# 解压缩
tar -zxvf mongodb-linux-x86_64-rhel70-4.4.18.tgz
# 移动到 /usr/local/mongodb4 目录
mv mongodb-linux-x86_64-rhel70-4.4.18 /usr/local/mongodb4
# 设置环境变量(任何目录都可以使用/usr/local/mongodb4/bin/下面的命令)
export PATH=/usr/local/mongodb4/bin:$PATH
创建数据/log存储目录
sudo mkdir -p /var/lib/mongo # 数据存储目录:/var/lib/mongo
sudo mkdir -p /var/log/mongodb # 日志文件目录:/var/log/mongodb
启动 Mongodb 服务
mongod --dbpath /var/lib/mongo --logpath /var/log/mongodb/mongod.log --fork
# 连接 mongodb
mongo # 执行后,如果出现 shell 就连接成功了,如下图
# 如果上面命令不行,进入到 对应的 bin 目录再运行
cd /usr/local/mongodb4/bin
./mongo
创建数据库、集合
# 创建数据库
use zuo-config
# 创建集合
db.createCollection('short-link')
# 2023-01-08
# mongodb 怎么使用 id 更新
在使用自带的 _id 进行更新时,发现更新数目一直是 0,_id 比较特殊,需要使用 ObjectId 包裹。这个函数在 Node.js 中可以通过 mongodb 包来获取
const { ObjectId} = require('mongodb')
async edit(payload) {
const db = mongodbCore.getDb()
const { shortLink, redirect, _id } = payload
const updateResult = await db.collection('short-link').updateOne({
_id: ObjectId(_id)
}, {
$set: { shortLink, redirect }
})
return updateResult
}
async del(id) {
const db = mongodbCore.getDb()
const deleteResult = await db.collection('short-link').deleteMany({
_id: ObjectId(id)
})
return deleteResult
}
# vue3+vite 项目怎么配置开发环境,生成环境使用不同的 api 前缀
vue3+vite 项目中可以使用 import.meta.env.xxx 来获取不同环境配置文件信息,开发环境对应文件 .env.development,生产环境对应 .env.development 文件。
以 axios 根据不同环境使用不同 base url 为例,其中 baseURL 从 import.meta.env.VITE_BASE_URL 获取
import axios from "axios";
console.log("axios base url", import.meta.env.VITE_BASE_URL);
// console.log("axios base url", process.env.VUE_APP_BASE_URL);
const instance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 60000,
headers: { "X-Custom-Header": "foobar" },
});
对应配置文件
// 根目录 .env.development 文件
VITE_BASE_URL = 'http://127.0.0.1:5000'
部署后生产环境
// 根目录 .env.production 文件
VITE_BASE_URL = 'http://zuo11.com:5000'
# 2023-01-07
# git commit 时怎么跳过 husky eslint 提交校验
提交校验时,husky 通过 git 自带的 pre-commit 来拦截错误,这里我们可以在 git commit 时加上 --no-verify 选项,来跳过 pre-commit 校验,如下图
git pre-commit 参考:Git - githooks Documentation (opens new window)
# 提交拦截有了 husky 为什么还需要使用 lint-staged
实际测试过程中,体验感觉比较差,文档少,运行时 terminal 闪动看起来比较乱,这里暂时不用
.husky/pre-commit 执行的 eslint . 会把所有代码都跑一遍,如果项目较大,会比较耗时。
lint-staged 可以在每次提交前,只校验将要提交的代码(git add . 添加到 staged 代码)
Use lint-staged to only run formatting on changed files We’re using Prettier right in our pre-commit hook and specifying . which means it’s going to run on all files every time. We can use a tool called lint-staged (opens new window), which allows us to still run our Git hooks with Husky, but it will only run on files that are staged.
参考:
- 三、项目集成 husky 与 lint-staged - 掘金 (opens new window)
- How to Add Commit Hooks to Git with Husky to Automate Code Tasks (opens new window)
测试项目中实际使用时,发现体验很差,并没有想象好,不知道是不是配置问题,我的配置如下
1、修改 ./husky/pre-commit 内容为 npm test
2、package.json scripts 里面增加 "test": "lint-staged"
3、配置 lint-staged
"lint-staged": {
"src/**/*.ts": [
"eslint --ext .tsx,.ts --fix ./src",
"prettier --write"
]
}
# 为什么 ./husky/pre-commit 中 eslint 检测有异常但仍然 commit 成功了?
在测试 eslint 提交拦截时,故意关掉了 eslint 插件的保存后自动 fix,然后添加了一些缩进有问题的代码。测试提交时,有报错,但 commit 居然成功了?信息如下:
> eslint . --ext .vue,.js --ignore-path .gitignore
# C:\Users\Administrator\Desktop\clone\zuo-config-fe\src\App.vue
# 4:1 warning Delete `⏎⏎⏎` prettier/prettier
# ✖ 1 problem (0 errors, 1 warning)
# 0 errors and 1 warning potentially fixable with the `--fix` option
# [main f20538b] feat(v0.1.0): update commit check hooks(eslint)
由于 git commit 时 check 的信息没有高亮,导致只注意到有一个问题。
手动跑 npx run lint-check 命令后,很清晰的看到只是一个warning 不是错误,因此是不会拦截的。
这里我们修改下代码,加入一个 error 类型的语法错误写法,先运行 eslint 测试是否 ok
然后再 git add . 再提交,看这次是否拦截正常。如下图,这次有 error,就直接拦截成功了。
# github 远程仓库地址修改后,本地项目怎么修改 remote
一般远程仓库地址变更后,要么从新 git clone 一份最新代码,要么修改 remote 地址。
git remote --help # 查看帮助文档
git remote show origin # 查看 remote 地址
# * remote origin
# Fetch URL: git@github.com:zuoxiaobai/zuo-config-server.git
# Push URL: git@github.com:zuoxiaobai/zuo-config-server.git
# 修改 remote 地址
git remote set-url origin git@github.com:zuoxiaobai/zuo-config-fe.git
# 查看是否修改成功
git remote show origin
# 2023-01-06
# 核心业务从vue2+ElementUI升级到vue3+ElementPlus居然更卡了?
背景:核心业务,只是将技术栈进行了更新,业务逻辑基本没动,但有两个位置比之前明显变卡了。
1、Element Plus el-table 170列20行数据卡顿明显,某一行 switch 开关切换后,要 6-8 秒才能更新好,明显感觉卡。
原因排查:使用 Performance 录制分析,发现 table 中虽然只是一个节点发生了变化,但依旧触发了完整的 vue patch 比对更新逻辑,耗时较久。
为什么会这样呢?vue3 理论上在性能方面比 vue2 要好,但实际效果却更差?
来看一个官方的解释:渲染机制 | Vue.js (opens new window)
虽然仅仅是一个 节点变更,但还是会重新创建新的 vnode,然后进行比对更新,当节点很多的时候,就会很慢,导致卡顿。
虽然 vue3 在 template 编译到 render 时,会有静态提升等手段减少虚拟dom比对,但这种只是针对静态节点,像我们项目中的表格列都是动态的,支持列勾选、排序。这种情况 vue3 的这种渲染优化基本没用。
怎么解决呢?
vue3 相比 vue2 数据劫持从 defineProperty 变成了 proxy,看起来更先进了,但节点较多时,可能会有更大的开销。
- vue2 里面,如果 template 中想显示数据,就需要在 data 中声明。这个响应拦截是 vue 内部自动完成的。
- vue3 中 setup 写法里面,数据声明可以用 ref、reactive 包裹,或者直接 const 定义一个变量(template里可以使用,只会初次渲染,更新后不会同步到页面),三种形式都可以在 template 中直接使用,要注意,尽量减少变量使用 ref、reactive 包裹,只有在 template 中使用,并且会变更的数据才需要这样声明。如果滥用,可能就会比 vue2 还卡,这是一个优化方向。
2、级联选择器+有勾选项,多个这种组件的场景,弹窗打开速度比之前慢 1-2s。
应该也是节点较多导致的,同上。
# element-plus table 太卡,使用 vxe-table 虚拟列表代替?
vxe-table 虽然在 demo 中 10w行2000列都很丝滑。但在我们实际的业务场景中,170列+20 滚动的时候会有点小卡,不是很丝滑。为什么?
首先:demo 中单元格都是存文本,没有业务逻辑,且高度一致、且固定。
但实际业务场景中,单元格内容较复杂,比如包含编辑、进度条、开关等复杂显示业务逻辑。另外高度是不一样的,这点非常致命(横向滚动时滚动中间部分,只渲染中间部分的节点,是一个高度,滚到最左侧节点内容不一样,高度动态的,导致滚动过程中高度会有变更,出现小卡的情况)
另外,有展开行的情况,不支持虚拟滚动,就是普通滚动,也就是说,有展开行的场景,如果列较多,直接卡到爆,这种情况就需要改 vxe-table 的源码了,来实现 expand 支持虚拟滚动。参考:vxe-table v4 展开行 (opens new window)
# 从 0 到 1 实现一个配置中心
核心价值:不发版也能解决某些问题。
比如:
- 公告配置、文档配置、菜单配置、白名单、黑名单、审核
- 权限配置、频控配置、定时任务配置、热更新 js,热更新 js 等
- 案例:某个错误码过滤了上报,不方面收集信息,可配置错误码上报,修改过滤配置后,重试即可上报数据
具体功能
1、config.xxx.com 前端页面可以新增/修改配置,技术栈:vue3+ts
2、接口服务,技术栈: koa+mongodb
- 前端配置后,存储到数据库相关接口
- 提供给外部使用时,获取对应配置的接口
比如:
- 短链接配置 - 前端配置 a.xx.com 跳转到 baidu.com 后,通过接口 GET /config/shortLink 可以获取到数据
{ path: 'a.xx.com', redirct: 'baidu.com' }
- mock 接口配置 - 配置 api 接口
/config/mock/配置的自定义接口路径
使用 GET 请求时 返回 { a: '1' } JSON 数据。然后 koa-router 去监听/config/mock/*
路由,根据 后面的路径,去数据库查询对应的配置。其中请求method 也是可以配置的,这样就是一个 mock 假数据接口生成公告呢。甚至针对不同的接口传参可以有不同的自定义逻辑,比如分页返回不同的数据。
项目划分,在 github 创建仓库
- 前端 vue3+ts: zuo-config-fe (opens new window)
- 使用 npm init vue@latest 创建好的 vue3-base 脚手架 Vue3 + Vite + TypeScript + Vue Router + Pinia + Vitest + ESLint + Prettier (opens new window),npm install, npm run dev 测试 eslint 保存后自动 fix 是否正常
- 安装 husky + commitlint 提交前校验 eslint + commit 信息是否符合规范,参考:husky + commitlint 提交校验 (opens new window),测试非规范 commit 消息是否可以提交成功。
- 接口 koa+mongodb: zuo-config-server (opens new window)
# 2023-01-05
# 访问2022.zuo11.com跳转到第三方长链接
前段时间总结记录在语雀,但链接比较长 https://www.yuque.com/guoqzuo/csm14e/owtn4c7dma0g1s71 (opens new window), 不利于分享,怎么使用自定义域名重定向到该网站呢?
目标:访问 2022.zuo11.com 时,重定向到该页面。
思路
- 域名解析到指定服务器
- 修改 nginx 配置,访问域名时解析到对应的静态问题
- 编写重定向静态文件 index.html(跳转方法使用 meta 标签的 http-equiv="refresh" 跳转)
操作步骤
1、登录到域名管理后端,比如阿里云。配置 2022.zuo11.com 二级域名,添加一个解析。解析到对应的服务器。 访问 2022.zuo11.com 时,DNS 就会解析到我们指定的服务器
主机记录 | 记录类型 | 记录值 |
---|---|---|
2022 | A | 服务器 IP 地址 |
这里和 OSS 对象存储重定向不一样,不能配置 CNAME 解析,因为记录值,需要是域名(host),不能有后缀路径
2、服务器配置使用自己写的 zuo-deploy Linux 操作面板进行配置
登录到 zuo-deploy 管理后台,在 Nginx/Https证书管理里面,修改 nginx 配置,增加 2022.zuo11.com 解析。修改保存后,点击重启 nginx 服务,让配置生效。
server {
listen 80;
server_name 2022.zuo11.com;
charset utf-8;
location / {
root /root/2022.zuo11.com;
index index.html index.htm;
}
}
当访问 2022.zuo11.com 时,会读取服务器 /root/2022.zuo11.com 目录下的 index.html 内容并响应
3、在 zuo-deploy 管理后台,实时终端里,创建该项目以及文件。使用命令行方式,需要有一定的 linux 命令行基础。
先来看跳转原理,使用 baidu.com 重定向到 www.baidu.com 中使用的 meta 方式跳转。参考:nginx以及koa实现301跳转:xx.com重定向到www.xx.com - dev-zuo 技术日常 (opens new window)
这种方法的好处是,就算浏览器不支持 JS,也会执行,纯 html 实现。
<html>
<meta http-equiv="refresh" content="0;url=需要跳转到的长链接">
</html>
linux 命令
# 分为两步
# 1. 创建 2022.zuo11.com 目录
cd /root; mkdir 2022.zuo11.com;
# 2. 创建重定向 index.html 文件,使用 echo 加 >> (类似管道) 创建文件,并写入内容
echo `<html><meta http-equiv="refresh" content="0;url=需要跳转到的长链接"></html>` >> /root/2022.zuo11.com/index.html
这样就 OK 了。
zuo-deploy 提供的实时终端里,可直接执行上面的命令。完成目录、文件创建,不需要登录到 linux 服务器。
查看文件内容,看文件是否写入成功
cat /root/2022.zuo11.com/index.html
配置好后,访问 2022.zuo11.com 就可以自动跳转 https://www.yuque.com/guoqzuo/csm14e/owtn4c7dma0g1s71 (opens new window) 了。
# https 域名提示 certificate has expired 怎么解决
最近自己写的服务监控 service-monitor 邮件推送提示 https://fe.zuo11.com (opens new window) 访问异常,提示 certificate has expired。证书过期失效。应该是 https 证书问题,这里记录下解决方法。
fe.zuo11.com 部署方式是 阿里云 OSS,可能是 OSS 里面设置的证书过期了。解决思路
1、https 证书失效,申请免费的新的 fe.zuo11.com https 证书,一般免费的是一年有效期
2、登录到 oss 管理后台,上传更新新申请 https 证书
登录到对应域名的 阿里云 账户控制台,访问:数字证书管理服务/SSL 证书 (opens new window),可以申请 20 个免费的 https 证书
申请 fe.zuo11.com https 证书,一般如果域名在当前账号,可以自动 dns 验证。
提交审核后,实际 2-3 分钟左右状态会变成已签发。然后就可以下载对应的证书了。(证书格式类型,我这里选择 nginx 服务器 + pem 文件类型)
下载完成后就是下面两个文件
然后登录到阿里云 OSS 控制台,替换对应的 https 证书文件即可
在 Bucket 配置 - 域名管理中替换 https 证书,如果 oss 服务和 SSL 证书在一个账号,可以直接选择,
如果不在一个账号,需要手动上传,其中证书文件是 .pem 后缀文件,私钥文件是 .key 文件,上传即可。配置完成后,提示 15 分钟生效,等一会儿
实际 3-5 分钟,测试 https 是否生效,可以直接访问 https://fe.zuo11.com (opens new window),也可以使用
curl -v https://fe.zuo11.com
看是否正常返回内容,即可片段 https 证书是否生效。链接前面有个小锁就是正常的
配置 https 证书的目的是,激活 vuepress 的 pwa 离线访问服务,pwa 依赖 service worker,处于安全性方面的考虑,必须要 https 才支持。一般判断页面是否支持 pwa 可以使用 Chrome Lightouse 测试,如下图
配置证书成功后,我们使用 https://fe.zuo11.com (opens new window) 再来测试一下 pwa 是否生效。如下图,就标识 pwa 正常开启
然后,再手动跑一遍 service-monitor 测试,看是否正常。如下图, 监控服务测试通过。
← 2023 年 02 月 2022 →