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吗?
不可以,后者覆盖前者,但可以使用多个 andWhere
和 orWhere
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的关系和联查
关系有三种,一对一,一对多(多对一),多对多
联查有两种:内联和左联
注意:leftJoin
和 leftJoinAndSelect
的区别,前者返回内容中不包含关系,后者包含
问题一:@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