Skip to main content

10-27

1 CSS中尽量不要使用gap属性,因为ios不支持

经过测试,gap属性在ios上不支持(Safari、Chrome等浏览器),虽然好用但无奈

/* 可以用margin来代替,排除第一个子元素的margin */
margin-left: 15px;

&:first-child{
margin-left: unset;
}

2 注意css的unset属性

如果CSS关键字 unset 从其父级继承,则将该属性重新设置为继承的值,如果没有继承父级样式,则将该属性重新设置为初始值。换句话说,在第一种情况下(继承属性)它的行为类似于inherit ,在第二种情况下(非继承属性)类似于initial

它类似于inherit和initial。

问题一:那清除一个样式,用什么呢?unset还是指定具体的值?

[1] unset.https://developer.mozilla.org/zh-CN/docs/Web/CSS/unset

3 index.js:1 Warning: Cannot update a component (LiquidityMining) while rendering a different component (OperationArea).

原因:在更新组件的时候,不能更新组件

示例错误代码

const aprFilterClick = useCallback(() => {
setAprSortDirection((prev) => {
if (prev == AprSortDirection.NO_SELECTED) {
// 进行排序、更换图标、回调逻辑
aprSortHandler(AprSortDirection.DOWN)
setCurrentFilterIcon(filterIconDown) //在setState中setState导致循环更新
return AprSortDirection.DOWN
} else if (prev == AprSortDirection.DOWN) {
aprSortHandler(AprSortDirection.UP)
setCurrentFilterIcon(filterIconUp)
return AprSortDirection.UP
} else {
// 未选中
aprSortHandler(AprSortDirection.NO_SELECTED)
setCurrentFilterIcon(filterIconNoSelected)
return AprSortDirection.NO_SELECTED
}
})
}, [aprSortHandler])

解决方案:用受控组件

5 关于uniapp使用vue3+vite+ts开发的感悟

目前uniapp对vue3和ts支持度挺好,但是第三方插件没及时更新,uview,router等库都只支持vue2。

且用vite的使用,有时不会热加载代码

总结

截止2021-10-31 22:31:38, 建议使用vue3进行开发,建议继续使用vue2的版本进行开发,不会遇到很多问题。

6 vue的computed通过this.方式更新,要实现set方法

如果其他地方要通过 this.show = true 更改show,则需要实现set方法

computed: {
show: {
get() {
const localVersion = uni.getStorageSync(VERSION)
const remoteVersion = this.$store.getters.version?.version

// 未比较过
if (!localVersion) return true

// 比较过version,则不再弹窗
if (this.comparedVersion) return false

// 比较
if (localVersion && remoteVersion) {
// 有更新,则显示弹窗
return isUpdateVersion(localVersion, remoteVersion)
}

return false
},
set(val) {
console.log('[](val):', val)
// this.show = val
return val
},
},
}

[1] https://blog.csdn.net/qq_35176916/article/details/86555080

7 判断版本号的方法

#有如下版本号,比较是否大于
2.1.0
1.0.1

如果只看1位,有三种情况:

  • 如果大于,那么则认为有更新
  • 如果小于,则认为没有更新
  • 如果等于,那么进入下一轮比较
function isUpdateVersion(o : string, n : string): boolean {
let len = o.length
if(len !== n.length) return false

for(let i=0; i<len; ++i) {
const oChar = o[i]
const nChar = n[i]

if(nChar > oChar){
return true
}else if(nChar < oChar){
return false
}else{
continue
}
}

//如果全相等
return false
}

8 MySQL的创建和删除-Nestjs

创建库

CREATE DATABASE  `databaseName` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE DATABASE `nestjsx_crud` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE DATABASE `yue-code-api` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

删除库

DROP database databaseName;

创建表

一般typeorm会自动根据实去创建表结构

删除表

#删除表tablename
DROP TABLE tableName ;

MySQL查询表结构

DESC tableName;

[1] MySQL 删除数据库.https://www.runoob.com/mysql/mysql-drop-database.html

9 Dayjs解析和格式化

//注意使用esm导入要全部导入,因为dayjs本身是cjs模块
import * as dayjs from 'dayjs'

//解析为时间戳
dayjs('2021-11-02 08:57:28').unix()
=>

//格式化
dayjs(1318781876406).format("YYYY-MM-DD HH:mm:ss");
=>

问题一:dayjs和momentjs有什么区别?谁更好用和流行?

[1] 轻量级js日期和时间操作库day.js.http://www.ptbird.cn/day-js.html

10 Typeorm条件筛选

问题一:typeorm可以连用多个where吗?

不可以,后者覆盖前者,但可以使用多个 andWhereorWhere

createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName: "Timber" })
.orWhere("user.lastName = :lastName", { lastName: "Saw" });
=>

SELECT ... FROM users user WHERE user.firstName = 'Timber' OR user.lastName = 'Saw'

甚至可以再嵌套一层

createQueryBuilder("user")
.where("user.registered = :registered", { registered: true })
.andWhere(new Brackets(qb => {
qb.where("user.firstName = :firstName", { firstName: "Timber" })
.orWhere("user.lastName = :lastName", { lastName: "Saw" })
=>
SELECT ... FROM users user WHERE user.registered = true AND (user.firstName = 'Timber' OR user.lastName = 'Saw')

问题二:时间范围筛选

注意:查询时间一般通过timestamp去查询,所有表结构需要timestamp字段

this.recordRepository
.createQueryBuilder('record')
.where('record.strategy_name LIKE :param')
.andWhere('timestamp BETWEEN :start AND :end')
.setParameters({
param: '%' + name + '%',
start: startTime,
end: endTime,
})
.orderBy('record.id', 'ASC')
.getMany();

问题三:分页查询

前端查询参数

{
page: 1,
limit: 15,
}

后端实现

const page = params.page
const limit = params.limit
this.recordRepository
.createQueryBuilder("user")
.skip(limit * (page-1))
.take(limit)
.getMany();

//获取总数,要通过Raw(原始数据)去获取
const total = await this.recordRepository
.createQueryBuilder()
.select('COUNT(*)', 'count')
.getRawOne()
=>
{count: 99}

问题四:批量插入、删除

//插入
await getConnection()
.createQueryBuilder()
.insert()
.into(User)
.values([{ firstName: "Timber", lastName: "Saw" }, { firstName: "Phantom", lastName: "Lancer" }])
.execute();

//删除
await getConnection()
.createQueryBuilder()
.delete()
.from(User)
.where("user.name IN (:...names)", { names: [ "Timber", "Cristal", "Lina" ] })
.execute();

注意,批量操作具有原子性,一个失败则失败

问题5: 查询总数

通过 COUNT 运算符计算总数,并通过Raw获取原始数据

//获取总数,要通过Raw(原始数据)去获取
const total = await this.recordRepository
.createQueryBuilder()
.select('COUNT(*)', 'count')
.getRawOne()
=>
{count: 99}

问题6:过滤

既要通过名称过滤,又要通过时间过滤,后续还可能有其他过滤 那么可以分步判断查询

let queryBuilder = this.recordRepository.createQueryBuilder('record');

// 过滤名字
if (name)
queryBuilder = queryBuilder
.where('record.strategy_name LIKE :param')
.setParameters({
param: '%' + name + '%',
});

// 过滤时间
queryBuilder = queryBuilder
.andWhere('timestamp BETWEEN :start AND :end')
.setParameters({
start: startTime,
end: endTime,
});

// 分页、排序
queryBuilder = queryBuilder
.skip(limit * (page - 1))
.take(limit)
.orderBy('record.id', 'ASC');

// 拿到结果
const queryData = await queryBuilder.getMany();

[1] nestjs typeorm 条件筛选、排序、分页 常见查询功能的实现.https://blog.csdn.net/landiyaaa/article/details/104730677

[2] TypeORM 中文文档.https://typeorm.bootcss.com/delete-query-builder

[3] typeorm 如何查询时间段数据.https://segmentfault.com/q/1010000039847499

[4] typeorm 模糊查询.https://www.jianshu.com/p/0d1f3547782f

[5] TypeORM 的基本使用(一对多,多对一,关系图).https://111hunter.github.io/2020-04-10-typeorm/

11 Nestjs统一响应和异常

统一响应利用 拦截器(interceptor)

统一异常利用 异常过滤器(filter)

import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';
import { responseLogger } from 'src/logger';
import { Request, Response } from 'express';

enum Methods {
GET = 'get',
POST = 'post',
PUT = 'put',
DELETE = 'delete',
PATCH = 'patch',
}

@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
//这里可以做一些请求参数处理

return next.handle().pipe(
map((data) => {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();

const statusCode = response.statusCode;
const url = request.originalUrl;
const res = {
statusCode,
msg: null,
success: true,
data,
};

responseLogger.info(url, res);
return res;
}),
);
}
}

import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { errorLogger } from 'src/logger';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();

const url = request.originalUrl;
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;

const msg = exception.message;

const errorResponse = {
statusCode: status,
msg,
success: false,
data: null,
};

// 设置返回的状态码、请求头、发送错误信息
response
.status(status)
.header('Content-Type', 'application/json; charset=utf-8')
.send(errorResponse);

//记录日志
errorLogger.error(url, errorResponse);
}
}

如何使用呢?一般都用于全局

// 统一响应和异常
app.useGlobalInterceptors(new TransformInterceptor());
app.useGlobalFilters(new HttpExceptionFilter());

12 Nestjs日志记录

利用 log4js 进行日志记录

import * as log4js from 'log4js';
import * as fs from 'fs-extra';
import { join } from 'path';

const LOG_DIR_NAME = '../../logs';

fs.ensureDirSync(join(__dirname, LOG_DIR_NAME));
void ['request', 'response', 'error'].forEach((t) => {
fs.ensureDirSync(join(__dirname, LOG_DIR_NAME, t));
});

const resolvePath = (dir, filename) =>
join(__dirname, LOG_DIR_NAME, dir, filename);

const commonCinfig = {
type: 'dateFile',
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true,
};

log4js.configure({
appenders: {
request: {
...commonCinfig,
filename: resolvePath('request', 'request.log'),
category: 'request',
},
response: {
...commonCinfig,
filename: resolvePath('response', 'response.log'),
category: 'response',
},
error: {
...commonCinfig,
filename: resolvePath('error', 'error.log'),
category: 'error',
},
},
categories: {
default: { appenders: ['request'], level: 'info' },
response: { appenders: ['response'], level: 'info' },
error: { appenders: ['error'], level: 'info' },
},
});

export const requestLogger = log4js.getLogger('request');
export const responseLogger = log4js.getLogger('response');
export const errorLogger = log4js.getLogger('error');

如何使用呢?直接导入使用

import { responseLogger } from 'src/logger';

responseLogger.info(url, res);

:record_button: 记录结果

//error.log.-2021-11-02.log

[2021-11-02T18:04:56.389] [ERROR] error - /api/record/search {
statusCode: 400,
msg: '[err](开始时间和结束时间期望为数字)',
success: false,
data: null
}
[2021-11-02T18:05:17.702] [ERROR] error - /api/record/search { statusCode: 400, msg: '开始时间和结束时间期望为数字', success: false, data: null }
[2021-11-02T18:06:01.586] [ERROR] error - /api/record/search { statusCode: 400, msg: '开始时间和结束时间期望为数字', success: false, data: null }
[2021-11-02T18:09:08.661] [ERROR] error - /api/record/search { statusCode: 400, msg: '开始时间和结束时间期望为数字', success: false, data: null }
[2021-11-02T18:15:05.240] [ERROR] error - /api/record/9999 {
statusCode: 400,
msg: '[参数错误]参数示例:[1,2,3]',
success: false,
data: null
}

13 Nestjs导入、导出excel-exceljs

导入(常用):从前端上传文件,解析excel内容

导出:从服务器读取excel文件,以buffer的形式返回给前端

应用:1.从前端批量导入excel数据到数据库中;2.读取服务器excel文件,让前端展示excel内容

核心代码:

import * as ExcelJS from 'exceljs';

async function upload(file: Express.Multer.File) {
const { buffer } = file; // file为前端上传的excel
const workbook = new ExcelJS.Workbook();

await workbook.xlsx.load(buffer); // 加载buffer文件
const worksheet = workbook.getWorksheet(1); // 获取excel表格的第一个sheet
const result = [];

worksheet.eachRow((row, rowNumber) => {
// 第一行是表头,故从第二行获取数据
if (rowNumber > 1) {
let target = null;
row.eachCell((cell, colNumber) => {
//cell 单元格
//cell.value 单元格的值
//colNumber 单元格编号,从1开始
//下面就可以从cell中获取相应编号的值了

target = getProjectCell(target, colNumber, cell.value);
});

target && result.push(target);
}
});

console.log(result); // result就是我们提取excel需要导入数据库的数据
}

function getProjectCell(colNumber : number, value : any){
const result = {};

const handler = {
1: (target, value) => {
target.id = value;
},
2: (target, value) => {
target.name = value;
},
3: (target, value) => {
target.region = value;
},
4: (target, value) => {
target.orderNumber = value;
},
5: (target, value) => {
target.organization = value;
},
6: (target, value) => {
target.contact = value;
},
7: (target, value) => {
target.phone = value;
},
8: (target, value) => {
target.buildAt = value;
},
9: (target, value) => {
target.remark = value;
},
};
handler[key] && handler[key](result, value);

return result
}

[1] NestJs导入导出excel文件-ExcelJs插件.https://blog.csdn.net/guanfeii/article/details/116304759

14 Typeorm的关系和联查

关系有三种,一对一,一对多(多对一),多对多

联查有两种:内联和左联

注意:leftJoinleftJoinAndSelect 的区别,前者返回内容中不包含关系,后者包含

问题一:@JoinColumn有什么用?

在一对一关系中: @JoinColumn ,这是必选项并且只能在关系的一侧设置。 你设置@JoinColumn的哪一方,哪一方的表将包含一个"relation id"和目标实体表的外键。

而在一对多的关系中: @JoinColumn ,这是可选项,默认在ManyToOne那一方

问题二:左连接与内连接

左连接只需要瞒足左边条件即可返回,而内连接左右两边同时满足才会返回

15 自动化部署Nestjs-GithubActions+Docker+pm2

总体思想为:Git服务器项目打包,打包镜像,上传镜像,再登录远端服务器,拉取镜像,运行镜像。

一共可分为三部分

⚠️ 目前不太熟悉pm2,

问题一:如何手动配置eslint?如何利用pritter进行自动格式化?

第一部分:GitAction配置

#.github/workflows/build_dev_to_16..15.yml

# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: build_dev_to_16..15

on: workflow_dispatch

env:
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
DOCKER_ACCESS_NAME: ${{ secrets.DOCKER_ACCESS_NAME }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
HOST_NAME_15: ${{ secrets.HOST_NAME_15 }}
ADMIN_HOST_15: ${{ secrets.ADMIN_HOST_15 }}

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [12.x]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: restore yarn
uses: actions/cache@v2
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: build dev
run: ./sh/build_dev_to_16..15.sh
- name: deploy dev
run: ./sh/deploy_dev_to_16..15.sh

第二部分:打包脚本

#build_dev_to_16..15.sh

#!/bin/sh
yarn

yarn build

docker build -t coinflow/convert-dashboard-api:dev .

docker login --username $DOCKER_ACCESS_NAME -p $DOCKER_ACCESS_TOKEN

docker push coinflow/convert-dashboard-api:dev

注意:记得给sh脚本加上可执行权限

chmod +x ./sh/*.sh

第三部分:部署脚本

分为两部分:第一部分是gitaction的脚本,第二部分是远端服务器的脚本

#deploy_dev_to_16..15.sh
#以下是gitaction服务器进行的操作

#!/bin/sh
eval $(ssh-agent -s)

#将ssh private key 放入当前服务器,这样才可以登录远端服务器
echo "$PRIVATE_KEY" > deploy.key

mkdir -p ~/.ssh
chmod 0600 deploy.key
ssh-add deploy.key

echo "Host *\n\tStrictHostKeyChecking no\n\n" >> ~/.ssh/config

#复制一些东西
scp sh/deploy_dev_docker_pull.sh $HOST_NAME_15@$ADMIN_HOST_15:~/sh/
scp docker-compose-dev.yml $HOST_NAME_15@$ADMIN_HOST_15:~/sh/

#登录远端服务器并执行命令
ssh $HOST_NAME_15@$ADMIN_HOST_15 "cd sh && sh deploy_dev_docker_pull.sh"
#deploy_dev_docker_pull.sh
#以下是远端服务器进行的操作

docker login --username lend -p $DOCKER_ACCESS_TOKEN

#删除原有镜像
docker rmi -f coinflow/convert-dashboard-api:dev
docker rmi -f coinflow/convert-dashboard-web:dev

#拉取服务器容器并启动
docker-compose -f docker-compose-dev.yml pull
docker-compose -f docker-compose-dev.yml down
docker-compose -f docker-compose-dev.yml up -d

下面是一些附加文件,如DockerFile,docker-compose.yml

#DockerFile

FROM keymetrics/pm2:latest-alpine

# 暴露端口
EXPOSE 9991

WORKDIR /data/release/convert-dashboard-api

# 创建目录
RUN mkdir -p /data/release/convert-dashboard-api

# 复制源码
COPY . /data/release/convert-dashboard-api

# 容器启动时,启动应用服务
CMD ["pm2-runtime", "ecosystem.config.js", "--only", "convert-dashboard-api"]
#docker-compose-dev.yml

version: '3'

services:
web:
image: coinflow/convert-dashboard-web:dev
container_name: web
restart: always
ports:
- '9990:80'
privileged: true
api:
image: coinflow/convert-dashboard-api:dev
container_name: api
restart: always
ports:
- '9010:9991'
privileged: true

networks:
lend_default:
driver: bridge

使用pm2,主要就是配置ecosystem文件,配置名称,启动入口,实例个数等,具体配置请查看 PM2

//ecosystem.config.js

module.exports = {
apps: [
{
name: 'convert-dashboard-api',
script: './dist/main.js',
instances: 1,
env: {
NODE_ENV: 'development',
},
env_production: {
NODE_ENV: 'production',
},
exec_mode: 'cluster',
combine_logs: true,
},
],
};

附:pm2常用命令

命令说明
pm2 list查看应用列表
pm2 start target启动应用
pm2 stop target停止应用
pm2 reload target重载应用(在启动新实例之前,原有实例的进程会一个一个消灭)
pm2 restart target重启应用(先消灭原有实例的所有进程,然后启动新实例)
pm2 delete target删除应用(从pm2管理中移除掉)
pm2 kill杀死所有进程
pm2 -h查看所有命令

注意:target表示id或者name或者file,如果有配置文件,直接pm2 start即可

[1] 使用 PM2 在 Docker 上部署 Node.js Web 应用.https://yorkyu.cn/pm2-deploy-nodejs-on-docker-1f8acea34fa4.html