Koa Router

中间件

中间件容器 负责不同组件和不同服务之间的交互,需要一个中间件负责统一的对服务使用

一个简单的中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const koa = require('koa');

const app = new koa();

const m1 = async (ctx, next) => {
console.log(1);
await next();
console.log(2);
}

const m2 = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
}
const m3 = async (ctx, next) => {
console.log(5);
await next();
console.log(6);
}

app
.use(m1)
.use(m2)
.use(m3)

app.listen(3000)

最终返回的结果为 1 3 5 2 4 6

也就是洋葱模型,由koa-compose模块来实现

实现洋葱模型的几个关键:

  • 统一的上下文 ctx
  • 操作先进后出 通过next控制
  • 有提前结束的机制

中间件类型

  • 应用级中间件 vue全局导航守卫
  • 路由级中间件 独享路由守卫
  • 错误处理中间件
  • 第三方中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const koa = require('koa');
const Router = require('koa-router')

const app = new koa();
const router = new Router();

const m1 = async (ctx, next) => {
// 应用级中间件最先被访问
console.log('应用级中间件');
//通过next进入路由级中间件
await next();
if (ctx.status == 404) {
ctx.body = '404'
}
}
// 路由级中间件会按照顺序访问
router.get('/', async (ctx, next) => {
console.log('路由级中间件1');
await next()
})

router.get('/', async (ctx, next) => {
console.log('路由级中间件2');
ctx.body = '路由'
})

app.use(router.routes());

app
.use(m1)

app.listen(3000)

koa 和 express 比较

express 通过connect添加中间件 封装了路由,视图, 异步处理使用callback (深层次的错误不能捕获)

koa 依赖于co模块,不包含任何中间件, 处理了回调 (使用了async await) 和错误处理(使用了try catch),

处理get post 请求参数

koa-body

静态资源中间件

koa-static

性能优化基础

  • prompt for unload

为卸载页面作准备, 释放js资源

  • DNS domainLookupStart domainLookupEnd

DNS 服务器会影响解析时间,DNS基于UDP协议

DNS (Domain Name System) 域名系统,用于将域名转换为IP

顶级域名: .com .org .club

域名资源记录: 域名服务商的配置记录 (3A,4A)

域名解析流程:

  • TCP

可能会产生ssl的握手(secureConnetionStart),需要放在nigix上处理

服务器的连接数会影响响应的速度,也受到物理距离的影响

  • request response

请求开始到响应开始包括,数据传输时间,服务器处理时间,服务器请求时间,服务器渲染时间

服务端优化: 服务端缓存,sql查询时间,服务端渲染,生成数据大小(使用压缩)

三次握手四次挥手

序列号seq:占4个字节,用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,第一个字节的编号由本地随机产生;给字节编上序号后,就给每一个报文段指派一个序号;序列号seq就是这个报文段中的第一个字节的数据编号。

确认号ack:占4个字节,期待收到对方下一个报文段的第一个数据字节的序号;序列号表示报文段携带数据的第一个字节的编号;而确认号指的是期望接收到下一个字节的编号;因此当前报文段最后一个字节的编号+1即为确认号。

确认ACK:占1位,仅当ACK=1时,确认号字段才有效。ACK=0时,确认号无效

同步SYN:连接建立时用于同步序号。当SYN=1,ACK=0时表示:这是一个连接请求报文段。若同意连接,则在响应报文段中使得SYN=1,ACK=1。因此,SYN=1表示这是一个连接请求,或连接接受报文。SYN这个标志位只有在TCP建产连接时才会被置1,握手完成后SYN标志位被置0。

终止FIN:用来释放一个连接。FIN=1表示:此报文段的发送方的数据已经发送完毕,并要求释放运输连接

PS:ACK、SYN和FIN这些大写的单词表示标志位,其值要么是1,要么是0;ack、seq小写的单词表示序号。

第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

  • 为什么连接的时候是三次握手,关闭的时候却是四次握手?

因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

  • 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

  • 为什么不能用两次握手进行连接?

3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

  • 如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

缓存策略

强制缓存

浏览器不会像服务器发送任何请求,直接从本地缓存中读取文件并返回Status Code: 200 OK

200 from disk cache: 不访问服务器,已经在之前的某个时间加载过该资源,直接从硬盘中读取缓存,关闭浏览器后,数据依然存在,此资源不会随着该页面的关闭而释放掉下次打开仍然会是from disk cache。

200 form memory cache : 不访问服务器,一般已经加载过该资源且缓存在了内存当中,直接从内存中读取缓存。浏览器关闭后,数据将不存在(资源被释放掉了),再次打开相同的页面时,不会出现from memory cache。

header参数

Expires:过期时间,如果设置了时间,则浏览器会在设置的时间内直接读取缓存,不再请求

Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。

(1) max-age:用来设置资源(representations)可以被缓存多长时间,单位为秒;
(2) s-maxage:和max-age是一样的,不过它只针对代理服务器缓存而言;
(3)public:指示响应可被任何缓存区缓存;
(4)private:只能针对个人用户,而不能被代理服务器缓存;
(5)no-cache:强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。服务器接收到 请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。这个很容易让人产生误解,使人误 以为是响应不被缓存。实际上Cache-Control: no-cache是会被缓存的,只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性。
(6)no-store:禁止一切缓存(这个才是响应不被缓存的意思)。

cache-control是http1.1的头字段,expires是http1.0的头字段,如果expires和cache-control同时存在,cache-control会覆盖expires,建议两个都写。

协商缓存

向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;

header参数

Etag/If-None-Match:

Etag是属于HTTP 1.1属性,它是由服务器(Apache或者其他工具)生成返回给前端,用来帮助服务器控制Web端的缓存验证。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。

当资源过期时,浏览器发现响应头里有Etag,则再次像服务器请求时带上请求头if-none-match(值是Etag的值)。服务器收到请求进行比对,决定返回200或304

Last-Modifed/If-Modified-Since:

Last-Modified 浏览器向服务器发送资源最后的修改时间

If-Modified-Since 当资源过期时(浏览器判断Cache-Control标识的max-age过期),发现响应头具有Last-Modified声明,则再次向服务器请求时带上头if-modified-since,表示请求时间。服务器收到请求后发现有if-modified-since则与被请求资源的最后修改时间进行对比(Last-Modified),若最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;若最后修改时间较旧(小),说明资源无新修改,响应HTTP 304 走缓存。

Last-Modifed/If-Modified-Since的时间精度是秒,而Etag可以更精确。
Etag优先级是高于Last-Modifed的,所以服务器会优先验证Etag
Last-Modifed/If-Modified-Since是http1.0的头字段

开发环境处理

处理html

html-webpack-plugin

1
npm i --save-dev html-webpack-plugin@next //webpack5
1
2
3
4
5
plugins: [
new HtmlWebpackPlugin({
template:'**.html'
})
]

处理样式

1
yarn add -D sass-node less-loader css-loader style-loader

css-loader style-loader sass-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
minimize: false, //压缩css代码, 默认false
import: true, //禁止或启用@import, 默认true
url: true, //控制url/image-set的解析,会处理成require引入
}
}
, 'sass-loader']
}
]
},
}

处理图片资源

1
yarn add -D url-loader file-laoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 小图片以bash64的方式输出
// 可以处理通过import引入的样式中的图片,js文件中引入的图片文件
// 如果图片大于指定大小会默认交给file-loader处理
// file-loader 会使用options中的配置
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 5 * 1024,
outputPath: 'assets',
name: '[hash:8].[ext]',
}
}
]
},
// 处理html中的图片
// 把html字符串中的图片引变成require的形式,
// 最终还是需要使用file-loader 和 url-loader来处理
// 如果生成的src是[object Module]需要关闭,url-loader的模块化规范
{
test: /\.html$/i,
use: [{
loader: 'html-loader',
}]
},

处理字体资源

1
2
3
4
5
6
7
8
9
{
// exclude:/\.xx$/
test: /\.(ttf|eot|svg|woff|woff2)$/,
loader:'file-loader',
options:{
name:'[path][name].[ext]'
}

}

简单的devServer搭建

1
yarn add -D webpack-dev-server

webpack.config.js 可能因为 webpack-dev-serverwebpack版本不匹配导致错误

1
2
3
4
5
devServer:{
compress: true,
contentBase: path.join(__dirname, 'dist'),
open:true
}

可以通过单独的server.js文件来使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');


const config = require('./webpack.config.js');

const options = {
contentBase: './dist',
publicPath: '/',
hot: true,
writeToDisk: true,
open: true
};

webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);

server.listen(9000, 'localhost', () => {
console.log('dev server listening on port 9000');
});

friendly-errors-webpack-plugin[https://www.npmjs.com/package/friendly-errors-webpack-plugin]

添加桌面通知
该插件没有桌面通知的原生支持,需要引入node-notifier,这样就可以了 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var NotifierPlugin = require('friendly-errors-webpack-plugin');
var notifier = require('node-notifier');
var ICON = path.join(__dirname, 'icon.png');

new NotifierPlugin({
onErrors: (severity, errors) => {
if (severity !== 'error') {
return;
}
const error = errors[0];
notifier.notify({
title: "Webpack error",
message: severity + ': ' + error.name,
subtitle: error.file || '',
icon: ICON
});
}
})
]

热部署代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
const fs = require("fs");
const path = require("path");
const vm = require("vm");

const handlerMap = {};
const hotsPath = path.join(__dirname, "hots");

// 加载文件代码 并 监听指定文件夹目录文件内容变动
const loadHandlers = async () => {
// 遍历出指定文件夹下的所有文件
const files = await new Promise((resolve, reject) => {
//https://nodejs.org/dist/latest-v14.x/docs/api/fs.html#fs_fs_readdir_path_options_callback
//异步读取指定目录下的文件和文件夹,返回一个数组
fs.readdir(hotsPath, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});
// 初始化加载所有文件 把每个文件结果缓存到handlerMap变量当中
for (let f in files) {
handlerMap[files[f]] = await loadHandler(path.join(hotsPath, files[f]));
}

// 监听指定文件夹的文件内容变动
await watchHandlers();
};

// 监视指定文件夹下的文件变动
const watchHandlers = async () => {
// 这里建议用chokidar的npm包代替文件夹监听
// 监听所有子文件夹
fs.watch(hotsPath, { recursive: true }, async (eventType, filename) => {
// 获取到每个文件的绝对路径
// 包一层require.resolve的原因,拼接好路径以后,它会主动去帮你判断这个路径下的文件是否存在
//使用require.resolve函数查询模块文件名时并不会加载该模块。
const targetFile = require.resolve(path.join(hotsPath, filename));
// 使用require加载一个模块后,模块的数据就会缓存到require.cache中,下次再加载相同模块,就会直接走require.cache
// 所以我们热加载部署,首要做的就是清除require.cache中对应文件的缓存
const cacheModule = require.cache[targetFile];
// 去除掉在require.cache缓存中parent对当前模块的引用,否则会引起内存泄露,具体解释可以看下面的文章
// 《记录一次由一行代码引发的“血案”》https://cnodejs.org/topic/5aaba2dc19b2e3db18959e63
// 《一行 delete require.cache 引发的内存泄漏血案》https://zhuanlan.zhihu.com/p/34702356
if (cacheModule.parent) {
cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);
}
// 清除指定路径对应模块的require.cache缓存
require.cache[targetFile] = null;

// 重新加载发生变动后的模块文件,实现热加载部署效果,并将重新加载后的结果,更新到handlerMap变量当中
const code = await loadHandler(targetFile)
handlerMap[filename] = code;
console.log("热部署文件:", filename, ",执行结果:", handlerMap);
});
};

// 加载指定文件的代码
const loadHandler = filename => {
return new Promise((resolve, reject) => {
//https://nodejs.org/dist/latest-v14.x/docs/api/fs.html#fs_fs_readfile_path_options_callback
//读取文件中的内容 默认返回buffer
//当目录为文件夹时抛出错误
fs.readFile(filename, (err, data) => {
if (err) {
resolve(null);
} else {
try {
// 使用vm模块的Script方法来预编译发生变化后的文件代码,检查语法错误,提前发现是否存在语法错误等报错
new vm.Script(data);
} catch (e) {
// 语法错误,编译失败
reject(e);
return;
}
// 编译通过后,重新require加载最新的代码
resolve(require(filename));
}
});
});
};

loadHandlers()

文件自动同步服务器

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env node

const shell = require('shelljs')
const chokidar = require('chokidar')
const home = require('user-home')
const path = require('path')

const WORKSPACE_PATH = path.resolve(home, 'Workspace');

const expectPath = path.join(__dirname, './expect.exp')
const to = 'root@144.xxx.xxx.92:/home/backup'

chokidar
.watch('./index.js', {
ignored: (path) => path.includes('node_modules') || path.includes('.git')
})
.on('all', (event, path) => {
console.log(event, path);
shell.exec(`expect ${expectPath} ${path} ${to}`)
});

expect.exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#expect 不能写错
#!/usr/bin/expect

set from [lindex $argv 0]
set to [lindex $argv 1]

set timeout 30

spawn bash -c "scp $from $to"

# 特殊符号需要转移 回车符号不需要
# \ 需转义为 \\\
# } 需转义为 \}
# [ 需转义为 \[
# $ 需转义为 \\\$
# ` 需转义为 \`
# " 需转义为 \\\"
# ( 需转义为 \(


expect {
"*password:" { send -- "j\(S7?xx#ky6\[0n=R\r" }
}

# 必须要加这一句,在执行完毕后把持交互状态,把控制台,这时候就可以进行你想要进行的操作了。如果没有这一句,在登陆完成之后就会退出,而不是留在远程终端上。
interact

性能优化

性能优化开篇

Html 数量控制 -> 压缩合并(30k为标准,请求变少) -> 开启CDN, gzip,brotli压缩方式 -> 服务端开启keep-alive http2 -> 测速 -> 缓存

离线缓存

localStorage 5M 低端机型,不能占用过高

script 放在底部,不会影响dom的解析,但是会影响dom的渲染

1
2
3
4
5
6
<body>
<h1>标题</h1>
<script>
alert(0)
</script>
</body>

同样css不会影响dom的解析,但是影响dom的渲染

1
2
3
4
5
6
7
8
9
10
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./css">
</head>

<body>
<h1>标题</h1>
</body>

css 加载会阻塞js的执行,因为不知道js中是否使用了css, 但是不会影响dom的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./css">
</head>

<body>
<h1>标题</h1>
<script>
alert(0)
</script>
</body>

css的加载不会影响dom ready,但是如果css,下面有js脚本,则不会执行dom ready,因为不知道是否使用了css样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
document.addEventlister('DomContentloaded',()=>{console.log('ready')})
</script>
<link rel="stylesheet" href="./css">
</head>

<body>
<h1>标题</h1>
<!-- <script>
alert(0)
</script> -->
</body>

渲染中的性能优化

CLI

命令行工具

基于文本查看,处理,操作计算机上面文件的程序

开发环境

webpack 打包 编译 图片压缩

命令

vue [options]

主命令 + 子命令 + 参数

init generate a new project from a template
list list available official templates
build prototype a new project
create (for v3 warning only)
help [cmd] display help for [cmd]

通过用户的配置拉取远程的模板,来生成模板

vue list 查看模板的种类

Available official templates:
★ browserify - A full-featured Browserify + vueify setup with hot-reload, linting & unit testing.
★ browserify-simple - A simple Browserify + vueify setup for quick prototyping.
★ pwa - PWA template for vue-cli based on the webpack template
★ simple - The simplest possible Vue setup in a single HTML file
★ webpack - A full-featured Webpack + vue-loader setup with hot reload, linting, testing & css extraction.
★ webpack-simple - A simple Webpack + vue-loader setup for quick prototyping.

borwserify 是有另一种打包规范,用于处理使用CMD打包规范,引用的模块

初始化

vue help init 查看初始化命令

Usage: vue-init [project-name]

Options:
-c, –clone use git clone
–offline use cached template
-h, –help output usage information
Examples:

# create a new project with an official template
$ vue init webpack my-project

# create a new project straight from a github template
$ vue init username/repo my-project

流程:

  • vue 输入命令 命令内部初始化

  • 获取用户配置:交互

  • 解析配置 基础配置+用户配置=配置

  • 生成项目文件 通过配置来拉取远程官方模板 + 自己写的模板
    自己写的模板必须包含 :meta.js / json
    prompts=>收集弹框信息
    helpers=>模板引擎的扩展
    complete=>钩子函数
    metalsmith=>站点生成器 通过配置和模板生成文件

    还必须包含模板文件:template

  • 完善工作

  • commander 完整的 node.js 命令行解决方案

  • Inquirer 弹窗交互

  • chalk 命令行美化工具

  • ora 命令行加载动画

  • execa 持续集成 提交到主分支

  • lerna 处理多包相互依赖

目录
  • docs 文档

  • scripts 脚本

  • packages => lerna

  • lib 核心逻辑

  • package.json
    bin 放置用户自定义命令

    为什么没有全局安装的命令可以使用 npm run 来执行,但是不能直接调用?

    如果一个命令想要全局执行,需要添加到全局的环境变量 PATH 中

    在执 npm run 或者 yarn 的时候,会自动在node_modules中查找需要执行的文件,通过npm link 软连接,添加到全局的环境变量PATH中,在执行完成后再删除

    全局安装的命令,会安装在 /usr/local/node12.18.4/lib/node_modules/,执行的时候会自动link,和scripts中写的命令同理

    bin 文件夹下面的命令需要手动link,在入口文件改变时需要重新link

    所以写一个命令的步骤: 1)创建bin文件夹,添加文件 2) 文件头部添加 #!/usr/bin/env node 表示可执行文件 3) link 到全局

vue-cli

vue-list 请求模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env node

const logger = require('../lib/logger')
const request = require('request')
const chalk = require('chalk')

// 监听ctrl + c退出事件
process.on('exit', () => {
console.log()
})

//请求模板使用到的包
// 如果没有报错,拼接包的名字 返回上面的列表
request({
url: 'https://api.github.com/users/vuejs-templates/repos',
headers: {
'User-Agent': 'vue-cli'
}
}, (err, res, body) => {
if (err) logger.fatal(err)
const requestBody = JSON.parse(body)
if (Array.isArray(requestBody)) {
console.log(' Available official templates:')
console.log()
requestBody.forEach(repo => {
console.log(
' ' + chalk.yellow('★') +
' ' + chalk.blue(repo.name) +
' - ' + repo.description)
})
} else {
console.error(requestBody.message)
}
})

vue-init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/usr/bin/env node

const download = require('download-git-repo')
const program = require('commander')
const exists = require('fs').existsSync
const path = require('path')
const ora = require('ora')
const home = require('user-home')
const tildify = require('tildify')
const chalk = require('chalk')
const inquirer = require('inquirer')
const rm = require('rimraf').sync
const logger = require('../lib/logger')
const generate = require('../lib/generate')
const checkVersion = require('../lib/check-version')
const warnings = require('../lib/warnings')
const localPath = require('../lib/local-path')

const isLocalPath = localPath.isLocalPath
const getTemplatePath = localPath.getTemplatePath

program
.usage('<template-name> [project-name]')
.option('-c, --clone', 'use git clone')
.option('--offline', 'use cached template')

/**
* Help.
*/

program.on('--help', () => {
console.log(' Examples:')
console.log()
console.log(chalk.gray(' # create a new project with an official template'))
console.log(' $ vue init webpack my-project')
console.log()
console.log(chalk.gray(' # create a new project straight from a github template'))
console.log(' $ vue init username/repo my-project')
console.log()
})

/**
* Help.
*/

function help () {
program.parse(process.argv)
if (program.args.length < 1) return program.help()
}
help()

/**
* Settings.
*/

let template = program.args[0]
const hasSlash = template.indexOf('/') > -1
const rawName = program.args[1]
const inPlace = !rawName || rawName === '.'
const name = inPlace ? path.relative('../', process.cwd()) : rawName
const to = path.resolve(rawName || '.')
const clone = program.clone || false

const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
if (program.offline) {
console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
template = tmp
}

/**
* Padding.
*/

console.log()
process.on('exit', () => {
console.log()
})

if (inPlace || exists(to)) {
inquirer.prompt([{
type: 'confirm',
message: inPlace
? 'Generate project in current directory?'
: 'Target directory exists. Continue?',
name: 'ok'
}]).then(answers => {
if (answers.ok) {
run()
}
}).catch(logger.fatal)
} else {
run()
}

/**
* Check, download and generate the project.
*/

function run () {
// check if template is local
if (isLocalPath(template)) {
const templatePath = getTemplatePath(template)
if (exists(templatePath)) {
generate(name, templatePath, to, err => {
if (err) logger.fatal(err)
console.log()
logger.success('Generated "%s".', name)
})
} else {
logger.fatal('Local template "%s" not found.', template)
}
} else {
checkVersion(() => {
if (!hasSlash) {
// use official templates
const officialTemplate = 'vuejs-templates/' + template
if (template.indexOf('#') !== -1) {
downloadAndGenerate(officialTemplate)
} else {
if (template.indexOf('-2.0') !== -1) {
warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
return
}

// warnings.v2BranchIsNowDefault(template, inPlace ? '' : name)
downloadAndGenerate(officialTemplate)
}
} else {
downloadAndGenerate(template)
}
})
}
}

/**
* Download a generate from a template repo.
*
* @param {String} template
*/

function downloadAndGenerate (template) {
const spinner = ora('downloading template')
spinner.start()
// Remove if local template exists
if (exists(tmp)) rm(tmp)
download(template, tmp, { clone }, err => {
spinner.stop()
if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
generate(name, tmp, to, err => {
if (err) logger.fatal(err)
console.log()
logger.success('Generated "%s".', name)
})
})
}

O(03).数组中重复的数字

LeetCode

注意

暴力解法

使用 map 或 set

1
2
3
4
5
6
7
var findRepeatNumber = function (nums) {
var map = new map();
for (let n of nums) {
if (map[n]) return n;
map[n] = true
}
};

原地替换

  • 题目描述 (长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内 ), 说明每个元素都应该与自己的下标相等,都有自己的位置

  • 通过循环把每个元素放回自己的位置,如果发现被相同的元素占用,表示重复

1
2
3
4
5
6
7
8
9
10
11
var findRepeatNumber = function (nums) {
var index = 0;
while (nums[index] !== undefined) {
var temp = nums[index];
if (temp === index) { index += 1; continue };
if (nums[temp] === temp) return temp;
nums[index] = nums[temp];
nums[temp] = temp;
}
return -1;
};

复杂度分析

  • 时间复杂度:

  • 空间复杂度:

BFF 实践

BFF

BFF,即 Backend For Frontend(服务于前端的后端),也就是服务器设计 API 时会考虑前端的使用,并在服务端直接进行业务逻辑的处理,又称为用户体验适配器。BFF 只是一种逻辑分层,而非一种技术,虽然 BFF 是一个新名词,但它的理念由来已久。

通常一个页面的请求包含了多个不同的请求,用于页面组件的渲染

同时为了保障 Android,iOS,以及 Web 端的不同需求,需要为不同的平台写不同的 API 接口,而每当值发生一些变化时,需要 Android,iOS,Web 做出修改。

有了 BFF 这一层时,我们就不需要考虑系统后端的迁移。后端发生的变化都可以在 BFF 层做一些响应的修改

BFF 场景

多端应用

我们在设计 API 时会考虑到不同设备的需求,也就是为不同的设备提供不同的 API,虽然它们可能是实现相同的功能,但因为不同设备的特殊性,它们对服务端的 API 访问也各有其特点,需要区别处理。

服务聚合

随着微服务的兴起,原本在同一个进程内运行的业务流程被拆分到了不同的服务中。这在增加业务灵活性的同时,也让前端的调用变得更复杂。BFF 的出现为前端应用提供了一个对业务服务调用的聚合点,它屏蔽了复杂的服务调用链,让前端可以聚焦在所需要的数据上,而不用关注底层提供这些数据的服务。

实战中的玩法

访问控制

例如,服务中的权限控制,将所有服务中的权限控制集中在 BFF 层,使下层服务更加纯粹和独立。

应用缓存

项目中时常存在一些需要缓存的临时数据,此时 BFF 作为业务的汇聚点,距离用户请求最近,遂将该缓存操作放在 BFF 层。

第三方入口

在业务中需要与第三交互时,将该交互放在 BFF 层,这样可以只暴露必要信息给第三方,从而便于控制第三方的访问。

初始化项目

项目目录结构划分

package.json 生命周期 并行执行

安装webpack

1
yarn add -D webpack-cli webpack

package.json 文件添加

1
2
3
4
"scripts": {
"test": "echo test",
"pretest": "echo pretest"
},

执行 yarn test

1
2
3
4
5
6
yarn run v1.22.10
$ echo pretest
pretest
$ echo test
test
Done in 0.05s.

并行执行,不能保证顺序

1
2
3
4
5
"scripts": {
"test1": "echo test1",
"test2": "echo test2",
"test": "yarn test1 & yarn test2"
}
1
2
3
4
5
6
7
8
yarn test
yarn run v1.22.10
$ yarn test1 & yarn test2
$ echo test2
$ echo test1
test2
test1
Done in 0.25s.
scripy

使用scripty拆分复杂命令

1
yarn add -D scripty

package.json

1
2
3
4
5
"scripts": {
"test:one": "scripty",
"test:two": "scripty",
"test": "scripty"
}

按照命令建立文件夹

执行 yarn test test文件夹下面的所有命令

package.json 定义公共参数

package.json

1
2
3
"config":{
"port":9999
}

通过变量在命令配置中使用

script/test/one.sh

1
echo $npm_package_config_port

执行 yarn test 打印端口

1
2
3
4
5
6
7
8
9
yarn run v1.22.10
$ scripty
scripty > Executing "/home/supreme/Workspace/mvc/scripts/test/one.sh":

9999
scripty > Executing "/home/supreme/Workspace/mvc/scripts/test/two.sh":

2
Done in 0.13s.
jscpd 代码重复率检查
1
yarn add -D jscpd

添加配置文件.jscpd.json

1
2
3
4
{
"threshold": 0,
"reporters": ["html", "console"]
}

scripts

1
jscpd --min-lines 1 --output ./doc/jscpd --pattern "src/**/*.js"
webpack配置

建立配置文件夹 config,通过内置方法拿到配置参数

1
process.env.NODE_ENV

安装 yargs,以对象的形式更方便的获取命令参数

安装 webpack-merge 合并公共配置和定制配置

art-template

使用art-template作为后端模板,用于服务端渲染html
配置和swig 模板类似

1
yarn add art-template koa-art-template
打包思路

通过webpack打包前端代码

html-webpack-plugin 处理前端模板,放到指定位置

因为模板中插入js代码所以需要通过编写插件处理

后端模板通过 gulp 处理

处理模块化规范,删除无用的代码

webpack.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const glob = require('glob')
const { argv } = require('yargs')
const path = require('path')
const files = glob.sync("./src/web/views/**/*.js")
const htmls = glob.sync("./src/web/views/**/*.art")
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const CustomInjectPlugin = require('./src/service/config/CustomInjectPlugin');
const mode = argv.mode;
if (!files.length) return;

const entry = files.reduce((entry, path) => {
const match = path.match(/(\w+)\/(\w+)\.js$/);
entry[match[1]] = match.input;
return entry;
}, {});

const htmlPlugin = htmls.map(path => {
const match = path.match(/(\w+)\/(\w+)\.art$/);
return new HtmlWebpackPlugin({
filename: `${match[1]}.art`,
template: match.input,
hash: true,
chunks: ['runtime', match[1]],
inject: false
})
})
module.exports = {
entry,
mode,
output: {
filename: '[name]_[contentHash].js',
path: __dirname + '/dist'
}
plugins: [
new CleanWebpackPlugin(),
...htmlPlugin,
new CustomInjectPlugin()
],
optimization: {
runtimeChunk: {
name: 'runtime'
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
}
}
]
},
resolve: {
alias: {
"@": path.resolve(__dirname, 'src/web')
}
}
}

CustomInjectPlugin.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const HtmlWebpackPlugin = require('html-webpack-plugin');
const pluginName = 'CustomInjectPlugin';

class CustomInjectPlugin {
js = ''
apply(compiler) {
compiler.hooks.compilation.tap('pluginName', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
pluginName,
(data, cb) => {
const { assets: { js } } = data;
this.js = js.map(src => src.replace('@', './')).join('');
cb(null, data)
}
)
// Static Plugin interface |compilation |HOOK NAME | register listener
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
pluginName, // <-- Set a meaningful name here for stacktraces
(data, cb) => {
const { html } = data;
data.html = html.replace(/!script!/, this.js);
this.js = ''
cb(null, data)
}
)
})
}
}

module.exports = CustomInjectPlugin;

gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const { series, src, dest } = require('gulp');
var plugins = require('gulp-load-plugins')();
const replace = require('@rollup/plugin-replace');

const jspath = './src/service/**/*.js';

// 开发环境 监听文件变化, 处理模块化规范
function es6(cb) {
plugins.watch(jspath, { ignoreInitial: false },
function () {
return src(jspath)
.pipe(plugins.babel({
plugins: [
"@babel/plugin-transform-modules-commonjs",
"@babel/plugin-transform-runtime"
]
}))
.pipe(dest('dist'))
}
)
.pipe(dest('build'));
return cb()
}

function es6dev() {
return src(jspath)
.pipe(plugins.babel({
ignore: ['./src/service/config/index.js'],
plugins: [
//处理模块化规范
"@babel/plugin-transform-modules-commonjs",
"@babel/plugin-transform-runtime"
]
}))
.pipe(dest('dist'))
}

function codeClean(cb) {
return src(jspath)
// transform the files here.
.pipe(plugins.rollup({
// any option supported by Rollup can be set here.
input: './src/service/config/index.js',
output: {
format: 'cjs'
},
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
}))
.pipe(dest('dist'))
}

exports.dev = series(es6);
exports.default = series(es6dev, codeClean);

sonarQube

最后更新

2024-12-11

安装数据库

安装 postgresql[文档]

1
apt install postgresql

创建 schema

1
2
3
4
5
6
7
# 登录数据库
sudo -u postgres psql

# 创建数据库,必须使用 UTF-8 编码
CREATE DATABASE sonarqube_db ENCODING 'UTF8';
# 链接到新的数据库
\c sonarqube_db # 进入数据库

创建用户 sonarqube

1
CREATE USER sonarqube WITH PASSWORD 'YourSecurePassword';

创建 scheme,并赋予 sonarqube 用户所有权限

1
CREATE SCHEMA sonarqube_schema AUTHORIZATION sonarqube;

由于没有使用默认的 schema,必须要设置 search_path

1
ALTER USER sonarqube SET search_path to sonarqube_schema;

配置 java 环境

安装 java 环境,[文档]

安装依赖包

1
apt install -y wget apt-transport-https gpg

下载 Eclipse Adoptium GPG 密钥

1
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /etc/apt/trusted.gpg.d/adoptium.gpg > /dev/null

配置库信息

1
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list

安装

1
2
apt update
apt-get install temurin-21-jdk

安装前环境准备

确认信息

一个进程可能拥有的最大内存映射区域数(vm.max_map_count)大于等于 524288。
打开的文件描述符的最大数目(fs.file-max)大于或等于 131072。
运行 SonarQube Server 的用户至少可以打开 131072 个文件描述符
运行 SonarQube Server 的用户至少可以打开 8192 个线程

使用以下命令查看信息

1
2
3
4
5
6
7
sysctl vm.max_map_count

sysctl fs.file-max

ulimit -n

ulimit -u

修改配置

1
2
3
4
5
6
# 创建一个新的配置文件
/etc/sysctl.d/99-sonarqube.conf

# 添加
vm.max_map_count=524288
fs.file-max=131072
1
2
3
4
5
6
7
# 创建一个新的配置文件
/etc/security/limits.d/99-sonarqube.conf

# 添加
sonarqube   -   nofile   262144

sonarqube   -   nproc    16384

在 Linux 内核上启用 seccomp

1
2
3
4
5
6
grep SECCOMP /boot/config-$(uname -r)

# 如果您的内核有seccomp,将看到以下内容
# CONFIG_HAVE_ARCH_SECCOMP_FILTER=y
# CONFIG_SECCOMP_FILTER=y
# CONFIG_SECCOMP=y

安装 sonar

下载,并解压,路径中不能有 .开头的文件夹

1
unzip sonarqube-25.3.0.104237.zip

编辑数据库链接信息

1
2
3
4
5
vi <sonarqubeHome>/conf/sonar.properties

sonar.jdbc.username=sonarqube
sonar.jdbc.password=mypassword
sonar.jdbc.url=jdbc:postgresql://localhost:5432/sonarqube_db?currentSchema=sonarqube_schema

配置 Elasticsearch 存储路径

1
2
3
4
vi <sonarqubeHome>/conf/sonar.properties

sonar.path.data=/var/sonarqube/data
sonar.path.temp=/var/sonarqube/temp

启动服务

1
2
3
4
<sonarqubeHome>/bin/linux-x86-64/sonar.sh start

# http://localhost:9000
# admin/admin

FAQ

检查日志
1
cat <sonarqubeHome>/logs/sonar.log
Startup error: ‘can not run elasticsearch as root’

修改 sonar 安装目录权限

1
2
sudo chown -R sonar:sonar /opt/sonarqube-25.3.0.104237/
sudo chown -R sonar:sonar /var/sonarqube
Process exited with exit value [ElasticSearch]: 143

143 错误 99% 与前端启动相关,检查 web.log

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信