2023 每日积累整理

[2023-01-03] 统计当前使用者git项目提交代码行数命令问题

基本命令

git log --since="2023-01-01" --before="2024-01-01" --author="$(git config --get user.email)" --pretty=tformat: --numstat | awk '{ add += $1 ; subs += $2 ; loc += $1 + $2 } END { printf "added lines: %s removed lines : %s total lines: %s\n",add,subs,loc }'

拓展:改为接受命令行执行 sh

# test.sh
# sh test.sh 2022-01-01 2023-01-01
start=$1
end=$2
author="$(git config --get user.email)"

echo "开始日期: ${start}"
echo "结束日期: ${end}"
echo "git user: ${author}"

git log --since="${start}" --before="${end}" --author="$(git config --get user.email)" --pretty=tformat: --numstat | awk '{ add += $1 ; subs += $2 ; loc1 += $1 - $2 ;loc2 += $1 + $2 } END { printf "新增: %s,删除: %s,净增: %s,总计修改: %s\n",add,subs,loc1,loc2 }'

小问题

命令最开始使用的是 gawk,使用时报错 command not found: gawk,更换为 awk 则成功运行。

[2023-01-04] 有关设置服务器定时任务的构想与步骤拆解

买了服务器不知道干啥用,想想怎么能有效把服务器利用起来。

步骤

  1. 编写简单的脚本 demo.sh,脚本内容为:df -h
  2. 获取运行结果,保存成文件 demo-res.txt参考
    • 每次输出内容如果相同,用日期 date 进行简单区分。
    • sh 日期格式化:date "+%Y-%m-%d %H:%M:%S"(注意需要使用 "" 将格式化文本包起来,留有空格会被认为是 2 个命令而报错)。参考
  3. 使用电子邮件发送 demo-res.txt
    • linux 下通常使用 mailx 发送邮件。
    • mailx 命令:echo "This is the mail body" | mail -s "Subject" 123@qq.com
    • mail -s "Hello from jsdig.com by shell" 123@qq.com
    • echo "hello,this is the content of mail.welcome to www.jsdig.com" | mail -s "Hello from jsdig.com by pipe" 123@qq.com
    • 卡住了,走不下去。
  4. 定时每天 11 点发送。
    • 使用 linux 常用命令 crontab,一般自带。
      • crontab 是否存在:rpm -qa | grep crontab
      • crontab 是否在运行:systemctl status crondactive (running) 表示在运行,inactive (dead) 表示未运行。
      • crontab 启动:systemctl start crond
      • 其余常用命令
    • crontab 常用写法
    • crontab 未正常运行原因参考
      • 练手过程中遇到的是路径问题,crontab 实际上是运行了的,但是因为 sh 脚本中 > demo-res.txt 是相对路径,所以写入到了 crontab 目录下的 demo-res.txt,修改为绝对路径则正常运行。
    • crontab 运行后的其他问题:
      • 有关 crontab -ecrontab /etc/crontab 执行问题:经测试两者分别独立,crontab -e 为当前用户计划,不需要也不能指定用户(会报错);crontab /etc/crontab 为总体定时计划,需要指定用户。
      • 运行 demo.sh 脚本后会莫名其妙的收到邮件,命令行提示 You have new mail。进入 /etc/mail 目录下发现 demo.sh 脚本的输出结果被输出到了运行用户名命名的文件内,原因不明。

sh 脚本

date "+%Y-%m-%d %H:%M:%S"> demo-res.txt
df -h >> demo-res.txt
cat demo-res.txt

后续扩展思考

  1. 服务器可以执行哪些脚本?
  2. 可以执行的是否只有脚本?
  3. 如何定时爬取 steam 打折游戏列表?
  4. 以日常维护来说,服务器需要哪些定时任务?
  5. 其他项目是否需要定时任务?

好文参考

[2023-01-09] 有关数据上报的学习与经验积累

参考:你只会用前端数据埋点 SDK 吗?

sdk 存在的意义 - 为什么需要前端数据埋点?

从个人体会,前端埋点的最大意义便是及早发现问题,进而解决问题。后端服务因为有服务器日志方便回溯数据与用户操作,对于问题的定位是非常简洁且高效的。但是前端页面上线后,只能通过用户的描述对操作复现,定位问题成本很高。

而且前端所谓直接应对用户的页面,所以一旦出现 bug 会被用户第一时间感知到。而因为单测编写难度(以及个人懒散),前端工程很多时候都是裸奔上线,解决 bug 只能靠手工测试。对于动辄千万行的代码而言,总有那么几条漏网之鱼。因此每次上线前后都是最担心的,不知道就弄了什么定时炸弹上去。

所以前端埋点的最大意义便是:

  • 错误收集
  • 性能搜集
  • 行为收集

重要性分先后。

前端错误收集

前端错误收集分两种:运行时错误与接口错误。

[2023-01-12] 文章阅读

git 取消文件 sh 脚本

# show-rm-git.sh
doc=$1
echo "即将删除 ${doc} 下的:"
git rm -r -n --cached "${doc}"
# rm-git.sh
doc=$1
echo "已删除 ${doc} 下的:"
git rm -r --cached "${doc}"

[2023-01-13] 有关流程图与架构图

[2023-01-14] nestjs 打包问题

nestjs 打包部署也是个老大难的问题,主要就是在 build 时 nestjs 并未将 node_modules/ 下的文件打入 dist 文件夹内,只是简单的进行了引用链接。常用的做法有:

  1. 将 nestjs 使用的依赖包打成 docker,然后以 docker 镜像 + dist 的方式进行使用。
  2. 整个项目打成 zip 包部署(体积比较大)。

这边文章提供了第三种方法,引入 webpack 将 node_modules/ 打入 dist 内的 main.js 中。

[2023-01-15] rbac 文章复读 & 数据库设计

RBAC用户、角色、权限、组设计方案

有关权限的细分,在权限管理系统数据库设计中权限设计的拓展。

权限为单独的一张权限表:

  drop table if exists authorities;

    CREATE TABLE IF NOT EXISTS `authorities`(
   `id` INT UNSIGNED AUTO_INCREMENT,
   `name` VARCHAR(40) NOT NULL comment '权限名称' ,
   `type` VARCHAR(40) NOT NULL comment '权限类型',
   `status` TINYINT NOT NULL DEFAULT 0,
   PRIMARY KEY ( `id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

与原本相比,增加了 type 类型,保证权限并不仅仅以一种方式进行存在。有可能对菜单的访问权限、对页面元素的访问权限、对某类资源的访问权限,增加了灵活性。

增加功能操作表/限制表:

drop table if exists operations;

    CREATE TABLE IF NOT EXISTS `operations`(
   `id` INT UNSIGNED AUTO_INCREMENT ,
   `name` VARCHAR(40) NOT NULL comment '操作名称',
   `code` VARCHAR(40) NOT NULL comment '操作码',
   `url` VARCHAR(80) default '' comment '拦截 url',
   `father_id` INT UNSIGNED default 0 comment '父id',
   `status` TINYINT NOT NULL DEFAULT 0,
   PRIMARY KEY ( `id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

增加权限&操作关系表

drop table if exists authorities_operations;
CREATE TABLE IF NOT EXISTS `authorities_operations` (
 `a_id` INT UNSIGNED COMMENT '权限id',
 `o_id` INT UNSIGNED COMMENT '操作id',
 `status` TINYINT NOT NULL DEFAULT 0,
 CONSTRAINT `authorit_o`FOREIGN KEY  (`a_id`)  REFERENCES  `authorities` (`id`),
 CONSTRAINT `operation_a` FOREIGN KEY (`o_id`) REFERENCES `operations` (`id`)
)

扩展权限表后,可以对各种类型的资源进行控制。例如菜单、元素、文件:

drop table if exists menus;

    CREATE TABLE IF NOT EXISTS `menus`(
   `id` INT UNSIGNED AUTO_INCREMENT ,
   `name` VARCHAR(40) NOT NULL comment '菜单名称',
   `url` VARCHAR(80) default '' comment '菜单 url',
   `father_id` INT UNSIGNED default 0 comment '父级菜单 id',
   `status` TINYINT NOT NULL DEFAULT 0,
   PRIMARY KEY ( `id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

drop table if exists authorities_menus;

 CREATE TABLE IF NOT EXISTS  `authorities_menus` (
 `a_id` INT UNSIGNED COMMENT '权限id',
 `m_id` INT UNSIGNED COMMENT '菜单id',
 `status` TINYINT NOT NULL DEFAULT 0,
 CONSTRAINT `authority_m`FOREIGN KEY  (`a_id`)  REFERENCES  `authorities` (`id`),
 CONSTRAINT `menu_a` FOREIGN KEY (`m_id`) REFERENCES `menus` (`id`)
)

通过权限菜单表,可以对权限进行扩展。

  • 权限 - 菜单 - 权限_菜单。
  • 权限 - 元素 - 权限_元素。
  • 权限 - 文件 - 权限_文件。

参考

[2023-01-16] nestjs 部署探索

方法一:压缩打包法:连同依赖包和依赖配置一同打包进 zip 包。以 zip 包为基准提交 git。缺点:体积大。

tar -czvf dist.tar.gz ./dist ./node_modules ./.env

方法二:docker 化

  • Dockerfile

# Docker多阶段构建

### DEV环境 ###
FROM node:14.17.3 AS development

# 定位到容器工作目录
WORKDIR /usr/src/app
# 拷贝package.json
COPY package*.json ./


RUN npm install --unsafe-perm -ddd --registry=https://registry.npm.taobao.org
# RUN npm install glob rimraf
# RUN npm install --only=development
COPY . .
RUN npm run build


### PROD环境 ###
FROM node:14.17.3 as production

# ARG NODE_ENV=production
# ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --unsafe-perm -ddd --registry=https://registry.npm.taobao.org
RUN npm install pm2 -g
# RUN \
#   npm config set registry https://registry.npm.taobao.org \
#   && npm install --only=production

COPY . .

# COPY --from=development /usr/src/app/dist ./dist

CMD ["node", "dist/main"]
  • docker-compose.yml
version: '3.9'
services:
  dev:
    container_name: server-dev
    image: server-dev:1.0.0
    build:
      context: .
      target: development
      dockerfile: ./Dockerfile
    command: npm run start:debug
    # network_mode: host
    ports:
      - 8080:8080
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    restart: always
  prod:
    container_name: server-prod
    image: server-prod:1.0.0
    build:
      context: .
      target: production
      dockerfile: ./Dockerfile
    command: npm run start:prod
    network_mode: host
    # ports:
    #   - 8080:8080
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    restart: always

docker-compose 有版本限制,需要在开头 version 标明。版本不同的 docker

docker 好处是,打包体积确实小。但是 docker 源会比较大。

[2023-01-17] 有关 vue2 生命周期的复习与整理

生命周期钩子 组件状态 最佳实践
beforeCreated 实例初始化后,this 指向创建的实例。但是不能访问到 data、computed、watch 和 methods 上的方法和数据 常用于初始化非响应式变量
created 实例创建完成,可访问 data、computed、watch 和 methods 上的方法和数据未挂载到 DOM,不能访问 $el 属性,$ref 属性内容为空数组 常用于简单的 AJAX 请求,页面的初始化
beforeMount 在挂载开始之前被调用beforeMount 之前,会找到对应的 template,并编译成 render 函数
mounted 实例挂载到 DOM 上,此时可以通过 DOM API 获取到 DOM 节点,$ref 属性可以访问 常用于获取 VNode 信息和操作,AJAX 请求
beforeUpdate 响应式数据更新时调用,发生在 Virtual DOM 打补丁前 适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器
updated Virtual DOM 重新渲染和打补丁之后调用,组件 DOM 已经更新,可执行依赖于 DOM 的操作 避免在这个钩子函数中操作数据,可能陷入死循环
beforeDestroy 实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例引用 常用于销毁定时器、解绑全局事件、销毁插件对象等操作
destroyed 实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁

额外

[2023-01-18]

vue 锚点跳转

直接上代码

<template>
  <div>
    <div id="header">
      <el-button
        @click="toDetail(`${anchor}${anchor}${index}`)"
        v-for="(anchor, index) in anchors"
        :key="anchor"
        >to:{{ anchor }}{{ anchor }}{{ index }}</el-button
      >
    </div>

    <div
      v-for="(anchor, index) in anchors"
      :key="index"
      :id="`${anchor}${anchor}${index}`"
      style="width: 800px; height: 400px; border: 1px solid red; margin: 12px;position: relative;"
    >
      <h3 style="padding: 8px; border-bottom: 1px solid red; margin: 12px">
        {{ anchor }}-{{ anchor }}-{{ index }}
      </h3>
      <p style="margin: 8px">{{ anchor }}{{ anchor }}{{ index }}</p>
      <el-button @click="toDetail('header')" style="position: absolute;right: 12px;bottom: 12px;">回去</el-button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'DemoOne',
  data: () => ({
    temp: 'demo',
    anchors: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'],
  }),
  mounted() {
    const {
      hash = ''
    } = location

    if (hash && hash.slice) {
      this.toDetail(hash.slice(1))
    }
  },
  methods: {
    toDetail(anchorId) {
      console.log(anchorId)
      const scrollDom = document.getElementById(anchorId);
      if (scrollDom) {
        scrollDom.scrollIntoView({
          behavior: 'smooth',
        });
      }

    },
  },
};
</script>


<style>
</style>

另一种跳转方法

麻烦了些,但是可以避免 hash 路由与锚点冲突。

有关拆解的问题

let { result = 'default' } = {};
console.log(result); // 'default'

let { result = 'default' } = {result: undefined};
console.log(result); // null

let { result = 'default' } = {result: 1};
console.log(result); // 1

let { result = 'default' } = {result: null};
console.log(result); // null

所以后端没事儿别瞎鸡儿返回数据

有关如何在控制台输出 badge 格式的文字

var success='#54c22b'
var primary='#2873b2'
var label='#606060'
var error='red'
let showBadge = function (label ,text, labelColor, textColor) {
        console.log(
            '%c '.concat(label, ' %c ').concat(text, ' '), 
            'padding: 1px; border-radius: 3px 0 0 3px;color: #fff;background: '.concat(labelColor, ';'),
            'padding: 1px; border-radius: 0 3px 3px 0;color: #fff;background: '.concat(textColor, ';'))
      }
showBadge('label', 'txt',label, primary)

[2023-01-20] 为什么程序员讨厌低代码

为什么程序员讨厌低代码

目前接手维护低代码相关的一个系统,比较难受,主要原因有以下几点:

  • 入门成本高,尤其是框架底层经过大量的扩展,功能其实多如牛毛,没有一个人可以完全掌握。很多时候,会做一些重复的开发。
  • 码表混乱,尤其是页面级码表。统一不了码表,在码表扩充或者减少时,很容易漏掉某些页面。
  • 页面修改无法追溯,代码级的修改可以通过 git 进行方便的回滚、检出;配置文件不列入 git,所以改了什么东西完全看不出来。
  • 代码配置同步问题。包括测试环境和生产环境配置往往不同步,尤其是在一些小改动直接在生产环境修改而未同步给测试环境。测试环境如果不知道,依然以测试环境的最新版本版本进行开发,很容易上线配置后覆盖对应功能。其次多人协作开发时,如果不同需求对一个配置进行了修改,在没有 git 的情况下很容易互相覆盖。

[2023-01-28]补充:有关低代码平台 - 码表统一问题的吐槽&坑

今天在维护低代码平台的码表问题,有以下几个方面的大坑&需要注意的点:

  • 混乱问题:页面之间,码表互不相通,同 key 在多个页面内使用、维护困难;同页面内,有码表从接口获取、有码表从配置获取、有码表从本地代码获取,来源混乱;同页面内码表随组件走而不是页面走,相同 key 的码表多次出现。在缺少文档的情况下,后续维护人员很难掌握所有页面的所有码表。在码表变动时,难保不会出现出现遗漏的问题。
  • 部分码表获取接口未区分生产环境与测试环境,进行替换时很容易使用测试的变量覆盖正式变量。
  • 同一套码表在多人维护时,很容易未能充分共享造成重复工作 —— 比如对同一(从后端获取的)码表提供了多套接口。
  • 对于 label、key、value,有出现同 key 不同 value,同 label 不同 key 的情况,会造成很大程度上的理解困难和维护障碍。
  • 对于无用结构或者已废弃接口,在后来人接手又不敢删除的情况下,会造成配置的大量冗余。
  • 一个前端用起来很爽、维护起来很操蛋、后端很反感的结构:把码表设计为 map 结构,而不是 key-value 的数组(这是反面教材)。Map 的唯一好处是可以方便的使用 key 进行获取 value 获取,但是缺点却是灾难性的:对(java)后端,map 结构无法直观的用 swagger 展示,不如对象清晰便于维护(而且还要特意做一层对象 to map 的操作);对于前端,虽然提取 value 方便,但是填充 select 类组件需要做一步 map to array 的数据清洗操作。在低代码平台接口未能提供清洗服务的配置时,这份多余就变成了一个需要改变底层才能适配的开发难题。好孩子不要学。
  • 对于一些功能相近的菜单,前端代码可以使用 mixin 等方式进行引用,方便修改;配置需要复制两份,修改时也要改两份。

[2023-01-30]补充:惹了个乱子

今天更新配置时惹了个乱子,在替换配置的时候不慎把别的页面配置覆盖了另一个页面。在更新了时候,已经提交了配置。因为没有回溯系统,甚至连补救的机会都没有。

还好 qa 环境还有对应页面配置,复制过来调整一下把危机处理了。

针对这种情况,特意把配置存了一份到 git 里。

[2023-01-22] 有关新的一年技术研究和发展构想(暂定)。

  • vue3 源码学习项目研究 & 练手
  • 基于 vue3 的前端脚手架。
  • Ci/Cd、Jeknins groovy 设置打包与部署。
  • 组件整理 & 组件库建设。
  • 常用工具代码整理 & 工具库建设。
  • 基于 nestjs 的完整 rbac 权限控制服务端。
  • rap2 mock 优化:根据 request 参数返回不同的值。
  • docker 部署(包括前端部署与后端部署)。
  • 埋点 sdk 相关研究

[2023-01-25] Flutter 相关研究

今天看了 flutter 直播,发现 flutter 进化非常大。

[2023-01-30] 有关本地文档常驻构想&实现

平时经常有些文档或者常用命令需要查询,之前是使用 md 文档进行撰写与保存,但是使用起来有这么几个问题:

  • 需要打开新的 ide 窗口,并选择对应路径,麻烦。
  • md 文档在未预览情况下,可读性还是差了点。

    其实主要还是开 ide 选路径麻烦,查起来也麻烦。

考虑到便携(懒),准备做如下操作:

  • 把此类文档统一归到某个项目内。
  • 使用本地服务器的方式启动该项目,启动后可以直接在浏览器中查看。
  • 支持实时编辑,刷新预览。
  • 支持命令行一键启动&重载。

文档已经归纳到项目内,本地文档系统选择:docsify

指定 docsify 读取目录:docsify init ./docs

需要在 ./docs 目录下新建 _sidebar.md 并撰写目录,可是用 node fs 自动读取 docs 下文件并生成。

使用 npm init 初始化 pacakge.json 文件,增加 docsify 启动命令并指定端口:"serve": "docsify serve docs --port 12345"

docsify 支持实时修改&预览。

编写 docsify 启动脚本:run_docsify.sh

#!/bin/bash
out="$(lsof -nP | grep LISTEN | grep 12345)"
outs=(${out//\t/ })  
docsPid=${outs[1]}
if [ ! "$docsPid" = "" ]
then
    kill $docsPid
    echo "已经关闭 pid:${docsPid},正在重启..."
else
    echo '正在启动...'
fi
cd ~/docsify
npm run serve

修改 zsh 命令(或者 bash)vim ~/.zshrc,指定别名:

alias start='sh ~/docsify/run_docsify.sh'

重新 source 下:source ~/.zshrc

可以直接使用 start 启动文档服务。

[2023-02-02] 增加 node 版本运行方式

使用命令行启动,多少还是有些麻烦 —— 要多开一个命令行窗口。因此又使用 node exec 写了一段 js 代码,可以直接用 pm2 启动。

启动方式 优点 缺点
命令行 节省电脑资源,不用时可以关闭 需要多开一个命令行窗口
屏幕 省心,启动后不用特意开启/关闭 留在电脑内确实一直占资源

总的来说两种方式各有优劣吧。

pm2 启动 run-node.js


const util = require("util");
const child_process = require("child_process");
// 调用util.promisify方法,返回一个promise,如const { stdout, stderr } = await exec('rm -rf build')
const exec = util.promisify(child_process.exec);

const run = async () => {
  let docsifyRunningStatusStr;
  try {
    const { stdout } = await exec("lsof -i tcp:14841");
    console.log(stdout);
    if (!!stdout) {
      docsifyRunningStatusStr = stdout;
    }
  } catch (_) {
    console.log("正在启动 docs 服务...");
  }

  try {
    if (docsifyRunningStatusStr) {
      console.log("已有服务运行,正在关闭中...");
      const [_, pid] = docsifyRunningStatusStr
        .split(" ")
        .filter((str) => !!str);
      if (pid) {
        const { stdout: closeStatus } = await exec(`kill -9 ${pid}`);
        console.log(closeStatus);
        console.log(`已关闭进程:${pid}`);
      }
    }
  } catch (error) {
    console.log(error);
  }
  try {
    await exec(
      `cd ~/docsify`
    );
    exec(`docsify serve docs --port 14841`);
    console.log("服务已启动,端口 14841");
  } catch (error) {
    console.log("启动失败:");
    console.log(error);
  }
};
run();

[2023-02-06] 文章阅读与参考&工具库想法

[2023-02-07] 文章阅读与参考&组件库构想

  • 文章阅读&参考:
  • 组件库思考:
    • 常用 form,使用共同组建。
    • 常用 table ,带查询、分页、渲染。
    • 常用 select 表单项。
    • 多行文字一行显示/鼠标悬浮显示完整。

[2023-02-08] 有关 js 中的码表枚举化

开发中使用码表的时候,常用两种方式:

方案一:

const map = {
  1: '状态一',
  2: '状态二'.
  3: '状态三'
}

方案二:

const map = [
  {label:'状态一', value: 1 },
  {label:'状态二', value: 2},
  {label:'状态三', value:3 },
]

方案一的好处是,对于给定的 key val,可以很方便的替换 map[val] 获取值。但是一来方案一的扩展性有限,其次对于后端(尤其是 java 后端),返回一个 map 结构并不是标准的流程。

方案二的优缺点刚好和方案二想法,即对于给定的 key val,必须使用 map.find( it => it.value === val) 才能拿到特定的码表,还要处理找不到值(undefined)的情况;但是结构方便扩展,java 返回的也通常是此种类型。

但是方案一和方案二都无法解决的是,在除了值替换过程中的一些逻辑判断时提供足够的语义化支持。比如对给定的值 val,在 1,2,3 情况下要做特定的操作,有 val === 1/2/3 这就不够友好。且不说维护时会想半天 123 都是啥东西,如果需要对值进行修改,光改就要改半天。

因此应该对码表进行枚举化改造,并对枚举值提供 get 和转义操作,将所有的操作收敛到对应的常量方法中,方便对于状态的管理。

// 在文章的基础上进行了改造
function createEnum(definition) {
  const strToValueMap = {}
  const numToDescMap = {}
  for (const enumName of Object.keys(definition)) {
    const [value, desc] = definition[enumName]
    strToValueMap[enumName] = value
    numToDescMap[value] = desc
  }
  return {
    ...strToValueMap,
    toOptions() {
      return Object.keys(strToValueMap).map(key=> ({
        label: this.emun2Text(key),
        value: strToValueMap[key]
      }))
    },
    emun2Text(enumName) {
      return (definition[enumName] && definition[enumName][1]) || ''
    },
    value2Text(value) {
      return numToDescMap[value] || ''
    }
  }
}

然后有

// STATIS1/2/3 是瞎写的,实际应该是 APPROVE/REJECT 等更为有语义化的单词,方便代码阅读
export const STATIS = createEnum({
  STATIS1: [1, '状态1'],
  STATIS2: [2, '状态2'],
  STATIS3: [3, '状态3'],
})

对于 select 等组件需要的 options 选项

STATIS.toOptions()

对于需要转义的地方

STATIS.value2Text(val)

对于需要逻辑判断的地方

switch(val) {
  case STATIS.STATIS1:
    ....
  case STATIS.STATIS2:
    ....
  case STATIS.STATIS3:
    ....
  default:
    ....

}

当然,枚举化也有自己的问题。即对于灵活变化的码表或者量大的码表(比如城市码表、由接口获取的码表),无法使用给定的枚举值进行枚举化。还是要回归到方案二的老路上。

方案一可以通过方案二清洗数据转化的来,最好不要让后端直接返回。后端会炸毛,有结构变动或者扩充的时候前端后端会一起炸毛。

[2023-02-09] 有关 js 全角、半角字符统计的工具

function count(target) {
  if (typeof target === "number") {
    return `${target}`.length;
  }
  const str = `${target}`;
  let totalLength = 0;
  const list = str.split("");
  for (let i = 0; i < list.length; i++) {
    const s = list[i];
    if (s.match(/[\u0000-\u00ff]/g)) {
      //半角
      totalLength += 1;
    } else if (s.match(/[\u4e00-\u9fa5]/g)) {
      //中文
      totalLength += 2;
    } else if (s.match(/[\uff00-\uffff]/g)) {
      //全角
      totalLength += 2;
    }
  }
  return Math.floor(totalLength/ 2) ;
}

[2023-02-10] 有关 typescript 中 class 和 interface 的区别

[2023-02-13] 13 日总结

首先,恭喜 time 夺冠;其次,李少牛逼;第三,真 tm 像王道少年热血漫照进现实。

watch 中 deep、immediate 的区别

watch route.query 一直没成功,检查的时候发现忘了加 immediate。

  • deep:监听对象属性变动。
  • immediate:页面首次加载时,是不会执行的。只有值发生改变才会执行。

随手写的常用 dialog mixins

<script>
export default {
  name: 'DialogMixin',
  props: {
    value: {
      type: Boolean,
      required: true,
    },
  },
  data: () => ({
    visible: false,
  }),
  watch: {
    value(newVal) {
      if (this.visible !== newVal) {
        this.visible = newVal;
      }
    },
    visible(newVisiible) {
      if (this.value !== newVisiible) {
        this.$emit('input', newVisiible);
      }
      if (!newVisiible && typeof this.reset === 'function') {
        this.reset()
      }
    },
  },
}
</script>

<style>

</style>

随手写的 el-form 验证

export const validForm = (refForm) => {
  return new Promise((res) => {
    if (refForm.validate){
      refForm.validate((valid) => {
        res(valid);
      } )
    } else {
      res(false)
    }
  })
}

[2023-02-14] vue + typescript + vue-property-decorator 写法总结

有关 typescript 写 vue,最痛苦的是不自由。动不动编译器报错,动不动类型校验通不过。在习惯了信马由缰写代码的 js 后,使用 ts 无异于自插双眼。然而不可否认的是,强制的检查带来了强大的稳定性。以往写代码总是担心各种各样空指针问题,在 ts 中通过强类型校验(interface 声明)和?补偿保证代码高度的兼容了 undefined、null 等各种各样的奇葩问题。甚至在声明 data 中属性时如果不进行赋值便无法初始化这点,让人又爱又恨。然而当我放弃了各种擦边球和诡异角度,堂堂正正的在 data 中写清楚一个超级复杂的属性的数据结构时,我也理解了 ts 的这种做法。

Javascript 是爽了现在的自己,坑了别人(和将来维护的自己)。 Typescript 看起来是坑了现在的自己,但是在日后维护以及代码阅读上带来了非常明显的方便。

起码两个小时后回去对源代码修改的我,不用在从 network 里去扒拉数据结构了。

ts 写法模板


<!-- eslint-disable @typescript-eslint/camelcase -->
<template>
  <div>
    demo1
    {{props}}
    {{dataA}}
    <div @click="method1"></div>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
@Component({
  name: 'Demo1'
})
export default class Demo1 extends Vue {
  @Prop(Boolean) readonly propA?: boolean;

  @Prop({
    type: Object,
    default: () => ({})
  })
  readonly propB?: IDto

  @Prop({
    type: Array,
    required: true,
  })
  readonly propC?: [];


  dataA = false;
  dataB = 123;
  dataC: IDto = {
    key1: 111,
    key2: '222'
  }

  get computedA() {
    return dataA + parseInt(dataC.key2)
  }

  method1() {
    // ....
  }
  async  method2() {
    // ....
    this.$emit('change', this.dataB)
  }

  @Watch('dataA')
  onChangeDataA(newVal: boolean) {
    // ...
  }

  @Watch('dataC', {
    immediate: true,
    deep: true
  })
  onChangeDataC(newDataC: IDto) {
    // ....
  }
}
</script>

<style lang="scss">
</style>

[2023-02-15] 积累

有关加水印

const watermark = {}

const setWatermark = (text, sourceBody) => {
  const id =
    Math.random() * 10000 +
    '-' +
    Math.random() * 10000 +
    '/' +
    Math.random() * 10000

  if (document.getElementById(id) !== null) {
    document.body.removeChild(document.getElementById(id))
  }

  const can = document.createElement('canvas')
  can.width = 120 // 设置水印之间的左右间距
  can.height = 80 // 设置水印之间的上下间距

  const cans = can.getContext('2d')
  cans.rotate((-20 * Math.PI) / 180)
  cans.font = '15px Vedana'
  cans.fillStyle = 'rgba(0, 0, 0, .5)'
  cans.textAlign = 'left'
  cans.textBaseline = 'Middle'
  cans.fillText(text, can.width / 20, can.height)

  const water_div = document.createElement('div')
  water_div.id = id
  water_div.className = 'watermarkClass'
  water_div.style.pointerEvents = 'none'
  water_div.style.background =
    'url(' + can.toDataURL('image/png') + ') left top repeat'
  if (sourceBody) {
    water_div.style.width = '100%'
    water_div.style.height = '100%'
    sourceBody.appendChild(water_div)
  } else {
    water_div.style.top = '3px'
    water_div.style.left = '0px'
    water_div.style.position = 'fixed'
    water_div.style.zIndex = '100000'
    water_div.style.width = document.documentElement.clientWidth + 'px'
    water_div.style.height = document.documentElement.clientHeight + 'px'
    document.body.appendChild(water_div)
  }

  return id
}

/**
 *  该方法只允许调用一次
 *  @param:
 *  @text == 水印内容
 *  @sourceBody == 水印添加的位置,不传就是body
 * */
watermark.set = (text, sourceBody) => {
  const domArr = Array.from(document.getElementsByClassName('watermarkClass'))
  for (let i = 0; i < domArr.length; i++) {
    const element = domArr[i]
    element.remove()
  }
  if (text === '') {
    return
  }
  setWatermark(text, sourceBody)
}

export default watermark

使用(vue):

import watermark from './utils/watermark'
export default {
  name: 'Demo',
  mounted() {
    watermark.set('水印')
    watermark.set('水印', document.getElementById('app'))
    watermark.set('水印', this.$refs.demo)
  }
}

有关精度丢失的问题

今天联调时连续的几个任务都是获取失败,把 traceid 和任务 id 发给后端时后端一脸懵逼说这 taskid 哪里来的,后端数据库里根本查不到。

仔细查看 network,在 preivew 里看到的值是 23021510400029930,看 response 里返回的居然是 23021510400029928(这个是正确的)。后来查阅文章发现,对于 2^53 - 1 的数据,js 就有加错的风险。最后和后端商量,把返回值改成 string 类型,问题解决。

参考这里)

[2023-02-16] axios post 请求总结

今天给人擦屁股擦了一天,修了无数的 bug 就是因为之前人维护的时候格式不统一、字段不统一,尼玛连查询参数都能有 page 和 pageNo,真的心累。

  • 使用 post 发送 json 请求

    axios 默认发送 json 请求,并将非序列化的数据序列化

await axios.post('http://test/post', { name: 'John Doe' }, {
    headers: {
    'Content-Type': 'application/json' // 默认是这个,可以不用写
    }
});
  • 使用 post 发送 x-www-form-urlencoded
import qs from 'qs';
const params= { name: 'John Doe' }
axios({
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    data: qs.stringify(params),   // 用 qs 将js对象转换为字符串 'name=edward&age=25'
    url: 'http://test/post'
}).then(res => {
    console.log(res)
});
  • 使用 post 发送 formdata
import qs from 'qs';
const params= { name: 'John Doe' }
const file = new File('./2023每日积累.md')
const data = new FormData()
data.append('name', params.name)
data.append('file', file)
axios({
    method: 'POST',
    headers: { 'content-type': 'multipart/form-data' },
    data, 
    url: 'http://test/post'
}).then(res => {
    console.log(res)
});

[2023-02-17] chatgpt 研究

最近 chatgpt 比较火,跟风研究了一下。目前的结论是:虽然 chatgpt 能提供一些方便,但是想要代替人工还是远远不如。因为chatgpt只能解决(已发现的)问题,不能提出(未发现的)问题。chatgpt 在被我问到是否会写小说时,也大方的承认:我是可以生成一些文本,但是写小说是一个需要人类创意和想象力的任务 ,因此没有能力像人类作家一样创作出高质量的小说。

注册的话还是需要翻墙,以及需要处理注册时的地区问题。中国访问还是会受限(包括港澳台),而且注册时需要当地手机号。可以使用虚拟号码注册一个,也可以请当地朋友提供一个。目测是注册时需要,后期登陆不需要。

同时,还会检测地区设置,需要提前在控制台进行处理。

window.localStorage.removeItem(Object.keys(window.localStorage).find(i=>i.startsWith('@@auth0spajs')))

然后注册成功。

升级成 plus 版需要当地 visa card,$20 一个月。个人感觉挺便宜,如果开发出用法,后续想升级一个。

一个chatgpt的相关网站:chatgpt

2.19 论如何正确使用 chatgpt

有关 chatgpt,给出答案有效程度和提出的问题精准程度有直接关系。比如“Dota2火猫怎么玩”只会给出一些泛泛的回答,而“Dota2火猫在逆风情况下怎么针对对方的核心小黑”确实能给出一些有用的经验。因此从使用的角度来说,chatgpt 更像是提升工作效率的工具。而用好 chatgpt 的关键,就是问对正确的问题。

2.20 有关使用 chatgpt 询问的 dota2 答案

  • 提问:对于2号位火猫,3号位石鳞剑士,4号位森海飞霞,5号位冰女,能提供一个1号位吗?
  • 回答:

    • 如果您正在讨论的是Dota 2游戏中的角色阵容,那么以下是一些可能的1号位英雄选择:
    • 斧王(Axe):一个非常强悍的近战力量英雄,拥有高生命值、大量伤害和针对敌人的控制技能。
    • 帕克(Puck):一个灵活的魔法英雄,能够在战场上快速移动并使用多种技能进行攻击和防御。
    • 末日使者(Doom):一个强力的力量英雄,拥有高生命值和多种伤害技能,能够对敌人造成致命打击。
    • 露娜(Luna):一个远程敏捷英雄,拥有强力的魔法伤害技能和远程攻击能力。
    • 狙击手(Sniper):一个远程敏捷英雄,能够在远处对敌人进行攻击,拥有高额伤害和攻击速度。
  • 提问:那么我选择以上 4 位和1号位露娜,请评价一下这套阵容。包括该阵容有什么优缺点?此阵容的核心角色是谁?此阵容是适合抱团推进还是 gank 杀人的打法?这套阵容适合打哪些阵容,被那些阵容克制?

  • 回答:
    • 优点:
      • 较高的输出:露娜和火猫在早期就能提供不错的输出,并且在后期也能保持强力。
      • 较好的团队控制:石鳞剑士和冰晶室女都拥有能够限制敌人移动的控制技能,可以很好地协助火猫和露娜输出。
      • 较强的推进能力:露娜的月之祝福和火猫的火焰冲击可以快速清理小兵,增强推进能力。
    • 缺点:
      • 较弱的防御能力:该阵容缺乏坦克英雄,容易被敌方重型输出英雄秒杀。
      • 对魔法伤害的抗性较弱:除了火猫外,该阵容的英雄抗魔能力一般。
    • 核心角色:
      • 露娜:该阵容的核心输出英雄,能够在各个阶段提供可观的输出。
    • 打法:
      • 抱团推进:该阵容适合抱团推进,利用露娜的月之祝福和火猫的火焰冲击等技能快速清理小兵,打破对方防御塔,同时利用石鳞剑士和冰晶室女的控制技能阻挡敌人的反击。
      • Gank 杀人:该阵容的团队控制能力较强,因此在中期可以通过 Gank 的方式快速击杀敌方英雄。
    • 适合打哪些阵容,被哪些阵容克制:
      • 适合打面板相对薄弱的阵容,比如常见的三核阵容等。
      • 被控制和打乱节奏的阵容克制,比如一些具有高爆发和突进能力的阵容,比如斧王、风暴之灵等。

答案分析与个人见解:

首先毫无疑问,chatgpt 是理解了上下的连续问题,是以第一问的 4 位角色 + 第二位选择的露娜角色为基准分析答案。 其次,其选择和我的默认选项不谋而合。第一问我在提问前,心理给出的答案是露娜。同时在第二问里我自己给出的判断是以露娜为核心抱团推进,通过火猫的拉扯消耗然后滚滚强行先手 + 露娜主输出,冰女 + 森海飞霞提供相应的控制。 第三,chatgpt 确实点出了这个阵容的问题(我却没有意识到),因为这个阵容是没有反先手能力的。此阵容面对冲脸型打法,冰女火猫的控制距离太近,滚滚和松鼠的控制容易打偏。被先手留到人,也很难把人救回来。因此一旦接战必须默契配合,不能让对方把输出打出来(没人吃的起),属于很吃配合且上下限差距极大,顺风浪逆风狗。在抗压方面,确实略有不足。

2.23 有关一个坑爹的 typescript 问题(chatgpt还是很有用的!)

对声明过的 interface,有


interface ITer {
  a: string;
  b: string;
}

然后定义 c,并希望便利 c 通过属性名取 c 的值拼凑成新的数组时,有报错 Element implicitly has an 'any' type because expression of type 'string' can't be used to index type,有什么解决办法。

c = {a:1,b:2}
 Object.keys(c).map((key)=> `${key}=${c[key]}`)
// 报错走不下去

chatgpt 回答:

这个问题是由于 Typescript 默认情况下并不知道对象的属性名以及对应属性的类型,因此在使用 c[key] 这种方式获取对象属性值时,Typescript 无法推断出属性的类型,导致出现错误。

为了解决这个问题,我们可以通过类型断言的方式来告诉 Typescript,我们需要访问的属性名以及对应属性的类型是什么。具体实现可以使用如下方式:

interface ITer {
  a: string;
  b: string;
}

const c = { a: 1, b: 2 };

Object.keys(c).map((key) => `${key}=${(c as {[key: string]: any})[key]}`)

在这里,我们通过 as {[key: string]: any} 的方式告诉 Typescript,c 对象的属性名是字符串类型,属性值是任意类型。这样,Typescript 就可以顺利地通过类型检查,并且返回结果也是正确的。

当然,如果你确定 c 对象的属性类型是确定的,你也可以使用 as 的方式来告诉 Typescript 属性的确切类型,例如:

interface ITer {
  a: string;
  b: string;
}

interface IC {
  a: number;
  b: number;
}

const c: IC = { a: 1, b: 2 };

Object.keys(c).map((key) => `${key}=${(c as IC)[key]}`)

这里我们使用 as IC 的方式告诉 Typescript,c 对象的属性名为字符串类型,属性值分别为数字类型。

在 typescript 中,已知一个对象 c = {a: 1, b:2},我应该如何通过属性遍历c并获得一个数组 ['a=1', 'b=2']?

const c = {a: 1, b: 2};
const result = Object.entries(c).map(([key, value]) => `${key}=${value}`);
console.log(result); // ['a=1', 'b=2']

在上述代码中,Object.entries(c) 方法将对象 c 转换为 [key, value] 数组,然后使用 map 方法将其转换为需要的数组格式。

2.24 还是有关 ts 相关的问题,chatgpt 给出的回答

提问:this.aList.find(a => a.id === id) 中,typescript 报错 Property 'id' does not exist on type 'never',应该如何优化。

chatgpt 回答:

  1. 明确指定 this.aList 的类型,比如 aList: { id: number }[]。
  2. 使用类型断言将 this.aList 转为 any 类型,比如 (this.aList as any).find(a => a.id === id)。不过这种方法不够安全,因为可能会隐藏一些潜在的类型错误。
  3. 在 TypeScript 4.4 之后的版本中,可以使用可选链操作符 ?. 来避免这个问题,比如 this.aList?.find(a => a.id === id)。如果 this.aList 是空的,那么这个表达式会直接返回 undefined。

一般来说,第一种方法是最安全、最好的选择,因为它能够帮助你在编译期就发现类型错误。

我自己根据回答写出来的代码:

const aList = this.alist as  { id: number }[]
const target = this.aList.find(a => a.id  === id)

3.6 今日份的坑爹问题两则

问题 1 el-table 滚动条无法消除的问题

改了半天,各种 overflow-xwebkit 样式试了个遍,滚动条还在。

原因是最后一列因为有展开问题,宽度超标。原本分配 29% 70% 1%,1% 的宽度不够。结果改成 28% 70% 2%,问题消除。

问题 2 el-upload 拖拽到区域不上传也无反应

原因是 el-upload 中 drag 和 accept 不能共存,有 accept 的话 drag 会失效。

真见鬼。。。

3.7 有关分页功能的标准总结

  1. 使用 url 保存 page、size、以及其他参数(比如 name),并使用 t 时间戳触发 watch 事件。查询的应该是当前的 query 参数。
  2. 初始访问链接时,可能 url 没有 page 和 size 参数,需要用默认值补充;同时 url 中可能有无用的参数(比如转义后的数组 => 元素)
  3. 点击查询,应该以当前的查询条件 + 当前 page_size + page 1 跳转到对应链接。
  4. watch 时应当正确解析 URL 中保存的参数并进行查询。

3.9 docker 相关

使用 dockerfile build 镜像:docker build -t my-demo:v1 . 使用景镜像启动容器:docker run --name my-demo -p 12345:80 -d my-demo:v1

已经很久么见过服务器上只有 docker 没有 docker-compose 了。

3.11 一个构思,如何使用 el-upload 上传批量时进行线性上传?

使用 multiple="true" 时,upload 组件会短时间多频次的调用 before-upload。因此考虑在 before-upload 钩子内先将 file 添加进 file list,然后使用 settimeout 防抖机制出发 startuplaod。

当然,为了防止添加时触发上传,需要将 before-uplaod 设置为 false。

    handleBeforeUpload(file) {

      this.fileList.push(file)
      if (this.timer) {
        clearTimeout(this.timer)
      }
      this.timer = setTimeout(() => {
        this.start()
      }, 300)
      return false
    },

3.16 使用 python3 研究 openai 初步

今日份的操蛋事

def index(prompt):
    response = openai.Completion.create(
        model="text-davinci-003",
        prompt=prompt,
        temperature=0.6,
        )
    return response.choices[0].text


temp = index('hello, world')
print(temp)

使用官方 demo,一切正常、可以运行。然后获取到了回答:hi,there。 自己拉出来单写,居然报错:

AttributeError: partially initialized module 'openai' has no attribute 'Completion' (most likely due to a circular import)

原因不明,真特娘的见鬼哦。

3.17

编译的时候报错,但是运行没问题:start value has mixed support, consider using flex-start instead 是什么原因

解决:

使用 flex 布局时,尽量使用 flex-start 而不是 start。

3.18 New Bing国内使用教程

chatgpt vs newBing ?

3.20

  • 匹配 11.20.134.52 8888 adminadmin PEx P@Gp7Gd*Z_v 的正则
^(\d{1,3}\.){3}\d{1,3}\s+\d+\s+\S+\s+\S+\s+\S+$

3.21 csrf 攻击防护以及前端应对总结

1. 什么是 csrf 攻击?

CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击是一种网络安全漏洞,它利用了用户在不知情的情况下,执行了攻击者设计的非法请求,从而实现攻击者的恶意目的。

具体来说,攻击者会在某个网站上设置一个恶意网页或者广告,当用户访问该网页时,攻击者会利用一些技巧自动发送一些请求到另一个网站上,这些请求可能是伪造的、篡改的或者是一些用户并不知道的操作。由于用户已经登录了该网站,攻击者可以通过这些请求来执行一些操作,例如修改用户的个人信息、购买商品或者进行其他非法行为。

为了防止 CSRF 攻击,网站开发人员可以采取一些安全措施,例如添加验证码、限制用户请求来源、使用随机 token 验证等等。此外,用户也可以采取一些措施来保护自己的账户安全,例如定期修改密码、不在公共场合使用公共 Wi-Fi、不点击来自未知来源的链接等等。

举个例子

攻击者A访问 www.csrf-test.com -> 创建订单,填好后抓包

POST /csrf-test/create HTTP/1.1
Host: www.csrf-test.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: https://www.csrf-test.com/
Content-Type: application/x-www-form-urlencoded
Content-Length: 231
Origin: https://www.csrf-test.com
Connection: close
Cookie: SUB=lalalalademaxiya; SSO=0123456
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
price=1&name=test2

通过抓包可以发现如下信息:

  • 请求 url 是 /csrf-test/create
  • 请求 参数是:pricename

然后攻击者 A 构建自己的网页,生成攻击

<html>
 <body>
 <script>history.pushState('', '', '/')</script>
 <form action="https://www.csrf-test.com/csrf-test/create" method="POST">
 <input type="hidden" name="price" value="2" />
 <input type="hidden" name="name" value="test2" />
 </form>
 </body>
</html>

将此网页链接发送给用户 test2,用户 test2 点击后,用户 test 会在不知晓的情况下创建一个订单

2. 如何应对 csrf 攻击?

  • 前端:

    • 添加 csrf token 验证:网站可以在页面中添加一个 CSRF Token,每次请求时都需要将该 Token 一起提交到后端,后端会验证该 Token 是否合法,从而判断该请求是否为有效请求。
    • 限制请求来源:网站可以限制请求的来源域名或 IP 地址,只有来自特定域名或 IP 地址的请求才被允许通过。
    • 添加验证码:网站可以在某些敏感操作中添加验证码,以防止机器人自动提交请求。
  • 后端:

    • 检查 Referer 头信息:后端可以检查请求头中的 Referer 字段,判断请求是否来自同一个域名,从而防止跨站请求。
    • 添加 CSRF Token 验证:后端也可以添加 CSRF Token 验证,当请求到达后端时,后端会验证请求中的 CSRF Token 是否合法。
    • 添加随机 Token 验证:后端可以在用户登录时生成一个随机 Token,每次用户进行敏感操作时都需要将该 Token 一起提交到后端,后端会验证该 Token 是否合法。

为什么前端添加 csrf token 便可以避免 csrf 攻击?

对于攻击页面

<html>
 <body>
 <script>history.pushState('', '', '/')</script>
 <form action="https://www.csrf-test.com/csrf-test/create" method="POST">
 <input type="hidden" name="price" value="2" />
 <input type="hidden" name="name" value="test2" />
 </form>
 </body>
</html>

最大的问题便是用户在无意识点击进入后,便会发送请求(因为使用了 form 表单的自动提交 action)。而在使用 form 表单提交时,是无法携带请求头信息的,自然会被后端认为是非法请求而拦下。

虽然攻击者可以同样再抓包请求获取 token,但是只能使用 ajax 构建欺骗请求,是需要用户主动操作点击的,此其一。

其次 token 本身是有过期时间,攻击者费劲心思抓包获得 token 后,又可能因为 token 过期还是无法进行操作,此其二。

再次提示需要将 token 设置的短一些,不要设计长达一个月的过期时间!

3. 后端在 response header 中返回 csrf token,前端在 response 中获取 csrf token 并添加到 request header 中,应该如何编码?前端使用 vue 框架,使用 axios 进行 http 请求。

import axios from 'axios'
let csrfToken = ''
const getToken = () => csrfToken
// 在请求头中添加 CSRF Token
axios.interceptors.request.use((config) => {
  if (csrfToken) {
    config.headers['X-CSRF-Token'] = csrfToken  // 添加 CSRF Token 到请求头
  }
  return config
})

// 在响应头中获取 CSRF Token
axios.interceptors.response.use((response) => {
  csrfToken = response.headers['X-CSRF-Token']  // 从响应头中获取 CSRF Token
  return response
})

有关 token 更新的频率

经过与后端讨论协商,后端曾经提出每一个请求都要使用新的 token,而新的 token 从上一个请求的 response 进行获取。但是实际开发中,因为前端页面的接口往往是并行请求。

对于 a b c 三个接口,如果需要达成这个效果,必须在 axios 中构建一个线性的上传队列,以先进先出的原则等待上一个接口返回,强行将并行改为串行。一方面前端的改造有成本,另一方面在页面同时请求多个接口时,加载、渲染会有问题(变成)。

经过商定,与后端打成一致(气的差点打人):token 以 x 小时为准,在 x 小时内如果无请求和session对话,则设置 token 过期;最新的 token 以用在获取用户信息时一并返回,前端获取到后填入 request 请求头中。

4. vue 页面在 token 过期后需要重新获取 token,在获取 token 的过程中,如何保证页面刷新时还停留在当前页

在获取 Token 的过程中,如果页面需要重新加载,可以使用路由的导航守卫(Navigation Guard)来实现在获取 Token 前取消路由跳转,获取到 Token 后再继续路由跳转,以保证页面在获取 Token 的过程中不会跳转到其他页面。

具体实现可以使用 beforeEach 导航守卫,在守卫函数中添加获取 Token 的逻辑。示例如下

import router from './router'
import axios from 'axios'

router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('token')
  if (token) {
    // 如果存在 Token,则直接进入目标路由
    next()
  } else {
    try {
      // 获取 Token
      const response = await axios.get('/api/getToken')
      const token = response.data.token
      // 存储 Token
      localStorage.setItem('token', token)
      // 继续路由跳转
      next()
    } catch (error) {
      // 获取 Token 失败,跳转到登录页面
      next('/login')
    }
  }
})

有关 token 过期后的交互处理。

经过和产品确定,在 token 过期时,需要弹出确认弹窗告知用户会话已过期。用户点击重新登陆后,会重新刷新页面并定向至系统首页;点取消则请求发送不成功,用户停留原有页面。

拓展阅读

3.22 如何使用 css 给 div 增加内阴影

box-shadow: inset 0 0 10px #000;

box-shadow 属性包含四个值:inset,表示要添加内阴影;0 0,表示阴影的水平和垂直偏移量都为零;10px,表示阴影的模糊半径;以及 #000,表示阴影的颜色为黑色。

3.23

  • nginx 转发 /api/xxx 请求到 转发至 127.0.0.1/xxx
 location /api {
        rewrite ^/api/(.*)$ /$1 break;
        proxy_pass http://127.0.0.1;
    }

3.28 iView-input组建trim交互优化经验总结

问题描述&研究

对于 iView 组建库提供的 Input 组建,使用 v-model.trim 过滤输入框空格时,trim 行为有效。作为组件本身,使用 v-model.trim 组件 Input 确实对 value 进行了空格过滤操作;但此时实际绘制的 html 元素 input 依然允许输入并保存了输入的空格,造成了显示上的误解(因为 input 的本身 value 实际上是含有空格的)。

经过进一步使用原生 <input /> 标签进行验证,在使用 v-model.trim 过滤输入空格时,原生 input 也会有实际上过滤了空格但仍然显示为未过滤的情况。但原生 input 失焦后(似乎触发了重新绘制的动作),再次获取焦点会显示正确的值(iView 不会)。

而之所以使用 element 组件库时没有此错误的交互,是因为其在 input 组件中加入了 setNativeInputValuesetCursor 方法主动设置了 input 元素的值并进行了重绘

解决方法

关键词:$attr keyup.native

思路仿造 element,使用装饰组件包装 iView 的 Input 组件,在 value 变化后重置 Input 组件中 input 元素的值并重置光标位置。同时使用 v-bind="$attrs" 来保证装饰组件和 Input 组件具有相同的 Props 调用。

<template>
    <Input @keyup.native="clearBlank" v-model="iValue" v-bind="$attrs"></Input>
</template>

<script>
export default {
  name: 'TrimInput',
  props: {
    value: {
      type: [String, Number],
      default: '',
    },
    trim: {
        type: Boolean,
        default: false,
    }
  },
  data: () => ({
    iValue: '',
  }),
  methods: {
    clearBlank(){
        if(this.trim) {
            this.iValue=this.iValue.replace(/[, ]/g,'')
        }

    },
  },
  watch: {
    value: {
      handler(newVal) {
        this.iValue = newVal
      },
      immediate: true,
    },
    iValue: {
      handler(newVal) {
        this.$emit('input', newVal)
      },
      immediate: true,
    },
  },
}
</script>

<style></style>

扩展:$attrs 和 $listeners 分别代表什么

$attrs$listeners 都是 Vue 中的实例属性,用于处理父组件传递给子组件的属性和事件。

$attrs 属性包含了父组件传递给子组件但是没有在子组件 props 中声明的属性。在子组件中,你可以使用 v-bind="$attrs" 将这些属性绑定到子组件的一个元素上。

$listeners 属性包含了父组件传递给子组件但是没有在子组件中定义的事件。在子组件中,你可以使用 v-on="$listeners" 将这些事件绑定到子组件的一个元素上。

这两个属性可以帮助你在不显式声明所有属性和事件的情况下,将这些属性和事件传递给子组件。同时,它们还可以帮助你在子组件中将这些属性和事件绑定到特定的元素上,从而实现更灵活的组件设计。

扩展:v-model.trim 的实现原理是什么

v-model.trim 的实现原理其实很简单,它是通过在组件的 v-model 指令后面添加一个修饰符 .trim 来实现的。这个修饰符告诉 Vue,在绑定数据之前,先对数据进行去除两端空格的处理。

更具体地说,当你在组件上使用 v-model.trim 指令时,Vue 会为该组件创建一个双向绑定的属性和一个更新该属性的方法。例如,当你在一个 input 元素上使用 v-model.trim 时,Vue 会为该 input 元素创建一个 value 属性和一个 input 事件。当用户输入内容时,该 input 事件会被触发,从而更新 value 属性的值。而当组件的 value 属性被更新时,Vue 会在更新该属性之前,先对属性值进行去除两端空格的处理,然后再将处理后的值赋给 value 属性。

需要注意的是,v-model.trim 只会处理组件绑定的数据,不会改变用户输入的数据。例如,当用户在一个 input 元素中输入空格时,空格会被保留在输入框中,但是在组件绑定的数据中,空格会被去除。

这也是为什么原生 input 和 iView input 中会有显示错误的问题(实际值没问题),而 element 之所以没有显示错误是因为对此行为进行了优化。

总之,v-model.trim 只是一个语法糖,它通过在组件的 v-model 指令后面添加一个修饰符 .trim 来实现对数据的处理,从而简化了代码编写。

// 简单实现 trim
<template>
  <input
    :value="computedValue"
    @input="updateValue($event.target.value.trim())"
  >
</template>

<script>
export default {
  name: 'MyInput',
  props: {
    value: String
  },
  computed: {
    computedValue: {
      get() {
        return this.value
      },
      set(val) {
        this.$emit('input', val.trim())
      }
    }
  },
  methods: {
    updateValue(val) {
      this.$emit('input', val)
    }
  }
}
</script>

3.30 在 vue2 中计算属性 a 的值有更新,在内存中已经看到了值的变化,但是在界面中值未变,这是什么原因?

如果在 Vue 2 中计算属性的值有更新,但是界面中值未变,可能是因为计算属性的依赖没有被检测到更新。Vue 2.x 中计算属性的依赖是通过依赖收集的方式来自动跟踪的,当计算属性依赖的数据发生变化时,会触发计算属性的重新计算,进而更新界面。

具体原因可能有以下几种:

计算属性的依赖没有被检测到更新,可以尝试使用 Vue.set 或 this.$set 方法更新计算属性的依赖,以触发重新计算。

计算属性的依赖本身没有被响应式定义,例如对象或数组没有使用 Vue.setthis.$set 方法添加新的属性或元素,或者使用了 Object.assignArray.prototype.slice 等不会改变原数组或对象的方法更新了依赖,这样就无法触发计算属性的重新计算。可以使用Vue.setthis.$set 方法来添加新的属性或元素,或者使用 Vue.observable 方法将普通对象转换为响应式对象。

计算属性本身存在问题,例如计算属性的计算函数中有副作用,会导致计算属性的值被更新但是界面没有更新。此时需要检查计算函数中的代码,避免对其他数据产生影响。

总之,如果在 Vue 2 中计算属性的值有更新但是界面中值未变,需要仔细检查计算属性的依赖和计算函数,确保依赖被正确地响应式定义并且计算函数没有副作用。如果仍然无法解决问题,可以考虑使用 Vue 3 中的 Composition API 或 Vuex 等其他状态管理方案。

在 Vue 中,通过 Vue.set 或 this.$set 方法可以为对象或数组添加新的属性或元素,并且会触发依赖更新。具体使用方法如下:

假设有如下计算属性 computedProp,它依赖于一个对象 obj 的属性 prop:

computed: {
  computedProp() {
    return this.obj.prop;
  }
}

如果需要更新 obj 的 prop 属性,可以通过 Vue.set 或 this.$set 方法进行更新:

this.$set(this.obj, 'prop', newValue);

这样会触发 computedProp 的重新计算,从而更新界面。

需要注意的是,使用 Vue.set 或 this.$set 方法添加新的属性或元素时,需要确保对象或数组本身是响应式的,否则新添加的属性或元素不会触发依赖更新。如果对象或数组不是响应式的,可以通过 Vue.observable 方法将其转换为响应式对象。

例如,如果 obj 不是响应式的,可以通过如下方式转换为响应式对象:

this.obj = Vue.observable(this.obj);

这样就可以使用 Vue.set 或 this.$set 方法添加新的属性或元素并触发依赖更新了。

4.3 js获取cookie信息相关经验总结

function getCookie(name) {
  const cookies = document.cookie.split('; ');
  const regex = new RegExp(`^${name}=`);
  for (let i = 0; i < cookies.length; i++) {
    if (cookies[i].match(regex)) {
      const cookie = cookies[i].trim().split('=')[1];
      return cookie
    }
  }
  return null;
}

const token = getCookie('token');
console.log(token);

不能获取指定 domain 的 cookie,获取的 cookie 只是 name=value 形式的键值对。不过服务端可以在设置的时候设置指定的 domain,比如 .b.com,这样在 .a.b.com.b.com 下都能获取找这条 cookie。 如果在设置 cookie 的时候,不要设置 domain。cookie 会使用默认当前域名,而且子域名不能访问。

  1. 域名错误:获取 cookie 的域名应该与设置 cookie 时指定的域名完全匹配(包括子域名)。如果指定的域名不正确,就无法匹配到相应的 cookie。
  2. cookie 被设置为 HttpOnly:如果 cookie 的 HttpOnly 属性设置为 true,那么它将无法通过 JavaScript 访问,这是为了防止跨站点脚本攻击(XSS)。
  3. cookie 已过期或被删除:如果 cookie 的过期时间早于当前时间,或者它已经被删除,那么它就无法获取到了。
  1. 直接问服务端。
  2. 发送请求,查看(chrome 浏览器) Network - Headers - Response Headers - Set-Cookie,如果含有 HttpOnly 则说明服务端已经这样设置了 cookie。

HttpOnly 相关:cookie 等级设置

  1. Secure:只有 https 可以获取 cookie 信息,http 不行。
  2. HttpOnly:禁止使用 js 获取 cookie 信息。
  3. SameSite:检查 cookie request 来源是否与发布 cookie 的来源相同。

前端需要特别关注的攻击方式

有趣的是,虽然时前端需要关注的方式,但是关键的卡点却不是前端。

  1. xss 攻击,需要设置 httponly。
  2. csrf 攻击,需要设置 token、严格限制 referer。

备选方案:vue-cookies

4.4 npm install node-sass 相关问题

能否在 package 或者 package-lock 中修改 node-gyp 的源?

可以在 package.json 文件或者 npmrc 配置文件中配置 node-gyp 的源。

pacakge:

{
  "scripts": {
    "install": "npm_config_node_gyp_mirror=https://npm.taobao.org/mirrors/node-gyp npm install"
  }
}

npmrc:

npm_config_node_gyp_mirror=https://npm.taobao.org/mirrors/node-gyp

Vue项目中刷新当前页面

<template>
  <div id="app">
    <router-view v-if="isRouterAlive"/>
  </div>
</template>

<script>
  export default {
    name: 'App',
    provide(){
      return{
        reload:this.reload
      }
    },

    data(){
      return{
        isRouterAlive:true
      }
    },

    methods:{
      reload(){
        this.isRouterAlive = false;
        this.$nextTick(function () {
          this.isRouterAlive = true;
        });
      }
    },
  }
</script>

注入&使用:

export default {
  inject:['reload'],  //注入依赖
}

this.reload();

使用reload方法时刷新页面可能会使作用域中的其他代码不会执行,所以尽量单独使用reload方法,或将其他全局的代码放在reload方法中一起调用。

4.12 jest 与 vue 单元测试研究

jest

4.14 有关复杂度优化的方法

复杂度优化方法 优化方法2

  • 删除 try catch
  • 用正确的 if else
  • 三元运算符 +2

4.17 vue 组件间共享数据

// 创建 EventBus 实例
import Vue from 'vue'

const EventBus = new Vue()

const install = (app) => {
  app.prototype.$EventBus = EventBus
}

export default install

// 在组件 A 中发出事件
export default {
  methods: {
    updateX() {
      // 发出事件,传递数据
      this.$EventBus.$emit('x-updated', 'new value');
    }
  }
}

// 在组件 B 中监听事件并处理更新
export default {
  data() {
    return {
      x: ''
    }
  },
  created() {
    // 监听事件,处理更新
    this.$EventBus.$on('x-updated', (newValue) => {
      // 更新组件 B 中的 x 属性
      this.x = newValue;
    });
  }

   beforeDestroy() {
    // 组件销毁前取消事件监听
    this.$EventBus.$off('my-event', this.handleEvent);
  }
}

4.19 劫持切换 ElRadioButton 行为并保留状态

对于 el-radio-group 以及 el-radio-button 组件,只有统一的 @input,并无向 el-tabbefore-leave 的拦截方法或 event 事件。而在点击 el-radio-button 时,会自动更新 el-radio-group 中使用 v-model 绑定的值。手动埋入 @change 事件,会得到 el-radiolabel 的值,无法获得 windows 事件。

解决思路

使用 el-radio 包裹的 div 进行事件劫持,阻止点击事件冒泡到 el-radio;同时,只在同意的情况下手动给 iValue 赋值。

<template>
  <el-radio-group v-model="iValue">
      <el-radio v-for="(id) in ids" :label="id"  @change="handleRadioChange">
      <div @click="($event) => handleChange($event, id)"></div>
      </el-radio>
    </el-radio-group>
</template>
<script>
export default {
    props: {
        value: {
            type: String,
            default: '',
        },
    },
    data: () => ({
        iValue: '',
    }),
    methods: {
        handleRadioChange(val) {
            console.log(val) // 会输出 uid
        },
        handleChange(event, uid) {
            event.preventDefault(); // 起手阻拦所有事件向上 mock
            event.stopPropagation();
            MessageBox.confirm('是否切换', '警告', {
                confirmButtonText: '确认',
                cancelButtonText: '取消',
                type: 'warning'
            })
            .then(() => {
                this.iValue = uid; // 同意切换后手动给 iValue 赋值
            })
            .catch(() => { 
                // 取消不修改 iValue 的值
            });
        }
    }
}
</script>

4.23 另一个版本的 vue ts

<template>
  <div>{{ message }}</div>
</template>

<script lang="ts">
import Vue from 'vue';
209474+
export default Vue.extend({
  props: {
    propMessage: String,
  },
  data() {
    return {
      message: 'Hello Vue!',
    };
  },
  computed: {
    reversedMessage(): string {
      return this.message.split('').reverse().join('');
    },
  },
  methods: {
    reverseMessage(): void {
      this.message = this.message.split('').reverse().join('');
    },
  },
  mounted() {
    console.log('Component mounted.');
  },
  watch: {
    propMessage(newValue: string, oldValue: string) {
      console.log(`propMessage changed from ${oldValue} to ${newValue}.`);
    },
  },
});
</script>
enum Color {
  Red = "#f00",
  Green = "#0f0",
  Blue = "#00f",
}

let c: Color = Color.Green;
console.log(c); // 输出 "#0f0"

4.27 如何下载 blob 文件

request 需要增加 {responseType: 'blob' }

结果处理

      const data =  await getBlob() 
      if (data.status === 200) {
        const blob = data.data
        const fileNames = decodeURIComponent(data.headers['content-disposition']);
        const fileName = fileNames.split('filename=')[1];
        this.handleBlob(blob, fileName);
      }
     handleBlob  (blob, fileName)  {
      const elink = document.createElement('a');

      elink.download = fileName;
      elink.style.display = 'none';
      elink.href = URL.createObjectURL(blob);
      document.body.appendChild(elink);
      elink.click();
      URL.revokeObjectURL(elink.href);
      document.body.removeChild(elink);
    },
axios({
  url: 'http://example.com/somefile',
  method: 'GET',
  responseType: 'blob'
})
  .then((response) => {
    const url = window.URL.createObjectURL(new Blob([response.data]));
    const link = document.createElement('a');
    link.href = url;
    link.setAttribute('download', 'filename.xlsx');
    document.body.appendChild(link);
    link.click();
  });

使用 {responseType: 'blob' } 后,response 自动会变成 blob 格式,不需要再转换 —— 再转换反而会出错。

环境变量配置相关

  • 使用 cross-env 最简单
  • package 命令 "build": "cross-env NODE_ENV=production 打包命令",

有关上传切片的问题

给文件 md5 加密时,如果一次性加载进内存会有卡顿情况,应当使用分段 md5 的方式分批计算。可直接使用 spark-md5

有关 px 转 rem 的问题

px 转 rem,你还在手算么?

重量计算

y/(40 + x) 1.19 1.08 = 0.7

上传 sdk 的思考 —— 完成一个上传 sdk 需要哪些关键方法?

  • 限制:包括大小、宽高、格式
  • 计算 md5,使用分段计算的方式
  • 分片
  • 上传进度
  • 上传完成
  • 上传完成后的轮询(如果需要转码)
  • 最终结果

css 实现九宫格的几种方式

css

取消promise

取消promise

有关视频加载的问题

vue video src改变 页面不刷新的解决方法, vue强制重新渲染

使用 key 处理时,不能使用 index。因为 index 会固定保留 0、1...,依然会有视频缓存的情况。

下载视频

如何使用js实现下载视频,并且不跳转页面


// 测试视频
const video_url = 'https://media.w3.org/2010/05/sintel/trailer.mp4';

// 下载函数
function download(url,name){
    fetch(url)
    .then(res => res.blob())
    .then(blob => {
        const a = document.createElement("a");
        const objectUrl = window.URL.createObjectURL(blob);
        a.download = name;
        a.href = objectUrl;
        a.click();
        window.URL.revokeObjectURL(objectUrl);
        a.remove();
    })
}

// 调用下载
download(video_url, ('test_video'+Date.now()))

nginx 匹配任意非 "/" 路径

location ~ ^/[^/]*$ {
    # 匹配任意非 "/" 路径的请求
    # 处理逻辑...
}

可以使用通配符字符 来匹配任意非 "/" 路径。 ^//$ 是一个正则表达式,用于匹配以 / 开头,后面跟着任意非 "/" 字符的路径。例如,它可以匹配 /example、/test123 等路径,但不匹配 /example/test、/path/to/file 等包含多个 "/" 的路径。 nginx 这样配置,则前端必须使用 hash route

vue2 项目使用 vue3 规则报错的问题

'plugin:vue/recommended(essential)' 替换为 'plugin:vue/base'

一系列参考文章

jwt token 相关

flutter 打包

https://juejin.cn/post/7207078219215929402

查看 mac 磁盘使用情况

查看 mac 磁盘使用情况

说实在的,居然不会...

校验开源库async-validator用法

依赖注入

一个有关 console.log 的插件

woo-ui组件库

如何在JavaScript项目中使用TypeScript的能力

匹配正则时,需要使用 i 来进行区分是否需要校验大小写

/(https):\/\/([\w.]+\/?)\S*/g 会匹配 https

/(https):\/\/([\w.]+\/?)\S*/ig 会匹配 https 和 HTTPS,虽然 HTTPS://baidu.com 也能访问就是了。‘

dart 服务器

nestjs 与微服务

excel 导入 mysql

vue2 slot 写法

results matching ""

    No results matching ""