# 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 逻辑

1-table-base.png

# 最小化业务 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-base-duration.png

table 隐藏到显示 gif 图

table-base-6-8-s.gif

switch 从关到开 gif 图

table-base-switch-3-8-s.gif

为了验证我们自己写的耗时测试数据的准确性,这里在 switch 开关时,打开了 performance 录制,具体如下图

页面显示渲染耗时:4.524s,performance 中两个 Long Task:2.29s + 2.17,加上非 Long Task 部分,数据基本一致,因此我们自己写的耗时计算逻辑是基本准确的

table-base-switch-performance.gif

另外,开启 performance 录制时,比不录制时要稍微慢点。下面来开始优化吧!

# ref 改 shallowRef

# 理论依据与可行性分析

列表中的开关切换时,table 虽然只是一个节点发生了变化,但依旧触发了完整的 vue patch 比对更新逻辑,耗时较久。

来看一个官方的解释:渲染机制 | Vue.js (opens new window)

vue-render-logic.png

理论上,减少响应式数据依赖,就可以提升性能。

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?

element-table-error.png

做一些修改,让代码可以再我们自己的项目中跑起来,方便修改、调试源码

  1. 在 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
  1. 搜索 @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-ref-shallow-ref-duration.png

table 隐藏到显示 gif 图

table-ref-shallowRef.gif

switch 从关到开 gif 图

table-ref-shallowRef-switch.gif

# getColspanRealWidth 优化

当页面卡顿时,可以通过 performance 测试性能。下图是点击 switch 开关后的性能数据。可以看到

  • 有两个 Scripting 阻塞 longTask,1.89s + 1.73s,整体耗时 3.62s (performance开启时,会变慢一点)
  • 主要有两种耗时任务:紫色小块是 render 渲染耗时、绿色小块是 patch 比对耗时,一般 patch 是 vue 内部逻辑,比较难优化
  • 通过查看 render 相关耗时,找到 getColspanRealWidth 耗时 212.2ms,这里有优化的空间

switch-performance-test.png

我们来查看这个函数耗时的原因,主要是在 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

getColspanRealWidth-optimize.png

# 性能数据收集(耗时减少7-20%)

table 渲染、switch 切换测试耗时如下

get-width-optimize-perf.png

table 隐藏到显示 gif 图

get-width-optimize-table.gif

switch 从关到开 gif 图

get-width-optimize-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 基本没有了,应该是模板编译时静态节点标记后,更新时就不用比对了。

tooltip-static-node-test.png

基于这个思路,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 左右刷新

tooltip-optimize.png

# 性能数据收集(耗时减少80%左右)

table 渲染、switch 切换测试耗时如下

tooltip-optimize-pref.png

table 隐藏到显示 gif 图

tooltip-optimize-table.gif

switch 从关到开 gif 图

tooltip-optimize-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)

这本手册分为两个部分:

# vue 文档 - 不让人讨厌的广告风格 - 万维广告

万维广告成功客户案例 & 广告创意巡展 (opens new window)

# 书籍《深入浅出 Vue.js》- vue2 源码学习

大致看了一遍这本书,但细节还是不懂,核心的 patch 比对算法、模板编译、响应式这三块还不是很理解,待整理笔记

# web 安全 - ddos 防御相关

怎么防御 ddos 攻击,参考

# 什么是 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

于是想找个免费、无水印的,最后找到了 GIPHY CAPTURE,app store 中下载,免费、好用、可编辑、无水印。

GIPHY CAPTURE-use

# MacBook Pro 新品发布官网动画效果实现(2023年01月)

2-1-macbook-pro-video-1.gif

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
  });

下载完成后,效果如下图

download-img.png

我们稍加修改,就可以批量下载图片了。注意批量下载时,如果要存到一个不存在的文件夹,那么需要先手动创建该文件夹(当然也可以使用 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 张图片了

m2-chip-down.png

# 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)

man-gzip.png

注意 center os 7.x 版本中,不支持 -k 选项,但 macos 看了下是支持的

-k --keep don't delete input files during operation

# 服务器被 ddos 攻击后,怎么快速恢复服务,查看日志经验总结

个人服务器被 ddos 攻击了,8G 流量就挂了,然后腾讯云封服务器外网 ip,导致上面的所有服务异常。

ddos-1.png

什么是 DDOS 攻击

DDOS又称为分布式拒绝服务攻击,全称是Distributed Denial os Service。DDOS本是利用合理的请求造成资源过载,导致服务不可用。比如一个停车场总共有100个车位,当100个车位都停满车后,再有车想要停进来,就必须等已有的车先出去才行。如果已有的车一直不出去,那么停车场的入口就会排起长队,停车场的负荷过载,不能正常工作了,这种情况就是“拒绝服务”。

ip 大概在 ddos 攻击结束 1.5 小时后解封(攻击开始后 2 小时后)。

参考:DDOS攻击的那些事 (opens new window)

ddos-2.png

复盘

  • 动静服务最好分离,静态服务抗攻击一点,可以把静态的服务都放到第三方,比如 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访客记录这个轮子的价值在这里就体现出来了,在出问题后,可以捕捉到一些信息,比什么信息都没有好)

visitors访问记录查看.png

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

ng-access-log.png

找到对应的 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 解压后的日志

cat-error-log-20230116.png

发现上图中 16 号 03:45 后的日志没有存放在这个文件中,且文件开头的 15 号的日志,所以又查看了 17 号的日志,如下图,ng 错误日志并没有特殊异常。

1 月 16 日,22:08 发起攻击时,没有任何记录,可惜 access log 被 rm 了,恢复又比较麻烦,就算了。下次出现问题,有了这次的经验后,下次查看日志就快了。

cat-error-log-20230117.png

# 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,如下图

oss-静态页面.png

# 2023-01-14

# 为什么在 windows chrome network 中同一个接口一次会有两条请求记录

因为,跨域的场景,浏览器会先发送一条 options 请求预检,来判断是否允许跨域。参考:利用koa来彻底理解web前端跨域问题 (opens new window)

跨域options请求预检

跨域options请求预检.png

跨域真实请求

跨域真实请求.png

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=模糊查询参数&currentPage=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

vivo-scroll-switch-phone-color.gif

原始链接: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("

解决办法参考:解决MySQL报错... right syntax to use near ‘password ‘XXX‘ at line 1...ERROR 1064 42000: You have an erro (opens new window)

下面是 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 配置问题,去掉即可

eslint-log查看.png

修改后,就 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

mysql-status.png

应该是不同的系统,配置不一样。导致异常。重点信息:

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;

mysql-sql_mode.png

# 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-fail.PNG

突然想起来,mongodb 数据库安装完成之后,接口服务并没有重启,于是重启了服务,重启后服务就正常了。

pm2 delete config.zuo11.com
pm2 start src/index.js -n 'config.zuo11.com'

再次访问 config.zuo11.com 短链接就可以正常的增删改查了。

config-server-ok.png

# 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 版本,可以很快的完成安装,并连接成功。

mongodb-4.4.18.PNG

# 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 文件就安装成了。

mongon-win.PNG

进入 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

win-shell-mongo.PNG

# 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

center-os-mongo.PNG

创建数据库、集合

# 创建数据库
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 校验,如下图

pre-commit-no-verify.png

git pre-commit 参考:Git - githooks Documentation (opens new window)

pre-commit-git.png

# 提交拦截有了 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.

参考:

测试项目中实际使用时,发现体验很差,并没有想象好,不知道是不是配置问题,我的配置如下

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 不是错误,因此是不会拦截的。

commit-eslint-1.png

这里我们修改下代码,加入一个 error 类型的语法错误写法,先运行 eslint 测试是否 ok

commit-eslint-2.png

然后再 git add . 再提交,看这次是否拦截正常。如下图,这次有 error,就直接拦截成功了。

commit-eslint-3.png

# 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)

vue-render-logic.png

虽然仅仅是一个 节点变更,但还是会重新创建新的 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 创建仓库

# 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 证书问题,这里记录下解决方法。

service-monitor.png

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 验证。

free-ssl-1.png

提交审核后,实际 2-3 分钟左右状态会变成已签发。然后就可以下载对应的证书了。(证书格式类型,我这里选择 nginx 服务器 + pem 文件类型)

free-ssl-2.png

下载完成后就是下面两个文件

free-ssl-3.png

然后登录到阿里云 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 证书是否生效。链接前面有个小锁就是正常的

free-ssl-4.png

配置 https 证书的目的是,激活 vuepress 的 pwa 离线访问服务,pwa 依赖 service worker,处于安全性方面的考虑,必须要 https 才支持。一般判断页面是否支持 pwa 可以使用 Chrome Lightouse 测试,如下图

pwa-1.png

配置证书成功后,我们使用 https://fe.zuo11.com (opens new window) 再来测试一下 pwa 是否生效。如下图,就标识 pwa 正常开启

pwa-2.png

然后,再手动跑一遍 service-monitor 测试,看是否正常。如下图, 监控服务测试通过。

service-monitor-success.png

上次更新: 2023/2/3 00:12:12