一只小强

天道酬勤 厚德载物


  • 首页

  • 标签24

  • 分类8

  • 归档30

  • 公益

  • 搜索

浅记koa的洋葱模型实现

发表于 2019-04-25 | 分类于 Nodejs

浅记koa的洋葱模型实现

本篇相关github代码地址

github地址:https://github.com/niexq/koaComposeTest

1.简介

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

2.安装

Koa 依赖 node v7.6.0 或 ES2015及更高版本和 async 方法支持.

你可以使用自己喜欢的版本管理器快速安装支持的 node 版本:

1
2
3
nvm install 7
npm i koa
node my-koa-app.js

3.中间件执行的洋葱模型

洋葱模型

4.中间件级联

Koa 中间件以更传统的方式级联。对比 Connect 的实现,通过一系列功能直接传递控制,直到一个返回,Koa 调用“下游”,然后控制流回“上游”。

5.疑问点:

  • 中间件如何加载
  • 中间件执行顺序
  • next是啥
  • context如何传递

6.koa中间件执行代码

下面以 “Hello World” 的响应作为示例,当请求开始时首先请求流通过四个中间件,然后继续移交控制给 response 中间件。当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。

启动服务器代码index.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

require('colors');
const Koa = require('koa2');
const app = new Koa();

const firstMiddleware = async (ctx, next) => {
console.error('第一个中间件执行开始')
await next();
console.error('第一个中间件执行结束')
};

const secondMiddleware = async (ctx, next) => {
console.error('第二个中间件执行开始')
await next();
console.error('第二个中间件执行结束')
};

const thirdMiddleware = async (ctx, next) => {
console.error('第三个中间件执行开始')
await next();
console.error('第三个中间件执行结束')
};

const fourthMiddleware = async (ctx, next) => {
console.error('第四个中间件执行开始')
await next();
console.error('第四个中间件执行结束')
};

app.use(firstMiddleware)

app.use(secondMiddleware)

app.use(thirdMiddleware)

app.use(fourthMiddleware)

// response
app.use(async (ctx, next) => {
console.log('准备响应');
ctx.body = 'Hello World';
console.log('已响应');
});

console.error('koa2 server start: '.blue, 'http://localhost:3001'.green);

app.listen(3000);

启动服务node index.js,浏览器中访问http://localhost:3001/
访问后,在启动的服务命令窗口输出的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
第一个中间件执行开始

第二个中间件执行开始

第三个中间件执行开始

第四个中间件执行开始

准备响应

已响应

第四个中间件执行结束

第三个中间件执行结束

第二个中间件执行结束

第一个中间件执行结束

7.源码分析

app.listen 创建node的http服务

重点关注this.callback(),this.callback()生成node的http服务请求回调函数

1
2
3
4
5
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

callback 返回node的http服务请求回调函数

1
2
3
4
5
6
7
8
9
10
11
12
callback() {
const fn = compose(this.middleware); // 重点关注此行代码

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res); // 此处创建context
return this.handleRequest(ctx, fn);
};

return handleRequest;
}

handleRequest 真正的请求回调函数

1
2
3
4
5
6
7
8
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror); // 此行代码也很关键
}

compose(this.middleware),此行代码处理中间件,继续跟踪app.use方法

app.use(function) 将给定的中间件方法添加到此应用程序

use方法源码

1
2
3
4
5
use(fn) {
...
this.middleware.push(fn);
return this;
}

app.use只负责将给定的中间件方法存入this.middleware数组中。

compose 中间件合成

koa-compose源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function compose (middleware) {
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)

// 关键函数dispatch
function dispatch (i) {
// 验证给定的中间件方法中,不能多次next()
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

compose方法中,递归调用dispatch函数,它将遍历整个middleware,然后将context和dispatch(i + 1)传给middleware中的方法, 这里的dispatch(i + 1)就是中间件方法的第二个入参next,通过next巧妙的把下一个中间件fn作为next的返回值。

8.疑问解答

至此就以上4点疑问就都可以解释了:

  • 中间件如何加载(通过app.use方法存入this.middleware数组中,然后通过compose方法串联)
  • 中间件执行顺序(dispatch(0),存在this.middleware数组里的中间件方法先进先执行,next()执行后转交下一中间件)
  • next是啥(next是一个以下一个中间件为返回值的方法)
  • context如何传递(context就在Promise.resolve(fn(context, dispatch.bind(null, i + 1)))一直传递)

9.结合洋葱模型,koa中间件执行效果

中间件执行效果图

使用node.js+socket.io+koa+reactjs搭建WebSocket简单的实时通信

发表于 2019-04-19 | 分类于 Nodejs

使用node.js+socket.io+koa+reactjs搭建WebSocket简单的实时通信

本篇相关github代码地址

服务端地址:https://github.com/niexq/webSocketTest

客户端地址:https://github.com/niexq/websocketClientTest

1.服务器端相关配置

1.1 安装koa

1
npm install --save koa

1.2 安装socket.io

1
npm install --save socket.io

1.3 index.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
require('colors');
const Koa = require('koa');
const app = new Koa();
const server = require('http').createServer(app.callback());
const io = require('socket.io')(server);


io.of('/chat').on('connection', function(socket) {

...

socket.emit('request', /* */); // emit an event to the socket

socket.on('reply', function(){ /* */ }); // listen to the event

socket.emit('news', { hello: 'world' });

socket.on('my other event', function (data) {
console.log(data);
});

});

server.listen(8899);
console.error('websocket test server start'.green);

2.客户端相关配置

2.1 可直接用create-react-app或ant方式

1
2
3
4
5
npx create-react-app websocket-client-test

cd websocket-client-test

npm start

2.2. 安装socket.io-client

1
npm install --save socket.io-client

2.3 封装WebSocket组件,在应用入口js中引入

WebSocket.js

1
2
3
export default class WebSocket extends Component {
...
}

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class App extends Component {

...

render () {
return (
<div>
...
<WebSocket />
</div>
)
}
}

lib/websocket.js,websocket相关方法封装

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

import io from 'socket.io-client';
import _ from 'lodash';
import { getCurrentUser } from './utils';

import { Socket } from 'dgram';

const env = process.env.NODE_ENV || 'development';
const config = require(`./config.${env}`);

export const CHATWEBSOCKET = 'CHATWEBSOCKET'
export const NEWSWEBSOCKET = 'NEWSWEBSOCKET'


const chatWebSocket = io(config.chatWebSocket,{
autoConnect: false
});


const newsWebSocket = io(config.newsWebSocket,{
autoConnect: false
});

export function chatSocketBind(cb){
console.error('~~~~~chatWebSocket', chatWebSocket);
if(!chatWebSocket.connected) chatWebSocket.open();

// 把当前用户名称发给服务器缓存下来,然后服务器给指定用户发信息(按业务需要,可以把当前用户信息包括token信息发给服务器缓存下来,然后服务器给指定用户发信息)
const { id, token } = getCurrentUser();
if(!id || !token) return;
chatWebSocket.emit('chatSocketBind', id, token);

// 绑定chat服务器发来的notify消息
if(_.isFunction(cb)) chatWebSocket.on('notify', (...args) => cb(...args));

// chatWebSocket.emit('firstMessageType', '嗨,我要建立websocket协议,需要chat服务')
}

export function newsSocketBind(cb){
if(!newsWebSocket.connected) newsWebSocket.open();

// 把当前用户名称发给服务器缓存下来,然后服务器给指定用户发信息
const { id, token } = getCurrentUser();
if(!id || !token) return;
newsWebSocket.emit('newsSocketBind', id, token);

// 绑定news服务器发来的notify消息
if(_.isFunction(cb)) newsWebSocket.on('notify', (...args) => cb(...args));
}

export function sendMessage({ serviceType, messageType, message }) {
const { id, name, token } = getCurrentUser();
if(!id || !token) return;

switch(serviceType) {
case CHATWEBSOCKET:
chatWebSocket.emit(messageType, id, name, message);
break;
case NEWSWEBSOCKET:
newsWebSocket.emit(messageType, id, name, message);
break;
default:
break;
}
}

3.最终操作效果图
png1

篇外:HTML5 WebSocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

png

有关更多详细信息,请参阅:

socket.io

HTML5 WebSocket

vscode部分常用快捷键

发表于 2019-04-19 | 分类于 vscode

vscode部分常用快捷键(以官网pdf文件为主,文末附图)

此文是vscode,windows下默认快捷方式,用户自己可通过【首选项-设置】自定义快捷方式

1.默认快捷方式

1、注释:

单行注释:[ctrl+k,ctrl+c] 或 ctrl+/

取消单行注释:[ctrl+k,ctrl+u] (按下ctrl不放,再按k + u)

多行注释:[alt+shift+A]

多行注释:/**

2、移动行:alt+up/down 3、显示/隐藏左侧目录栏 ctrl + b 4、复制当前行:ctrl + c 5、删除当前行:ctrl + x 6、控制台终端显示与隐藏:ctrl + ~

7、查找文件/安装vs code 插件地址:ctrl + p

8、代码格式化:shift + alt +f

9、新建一个窗口 : ctrl + shift + n 10、行增加缩进: ctrl + [ 11、行减少缩进: ctrl + ]

12、裁剪尾随空格(去掉一行的末尾那些没用的空格) : ctrl + shift + x

13、字体放大/缩小: ctrl + ( + 或 - )

14、拆分编辑器 : ctrl + 1/2/3

15、切换窗口 : ctrl + shift + left/right

16、关闭编辑器窗口 : ctrl + w

17、关闭所有窗口 : ctrl + k + w

18、切换全屏 : F11

19、自动换行 : alt + z

20、显示git : ctrl + shift + g

21、全局查找文件:ctrl + shift + f

22、显示相关插件的命令(如:git log):ctrl + shift + p

23、选中文字:shift + left / right / up / down

24、折叠代码: ctrl + k + 0-9 (0是完全折叠)

25、展开代码: ctrl + k + j (完全展开代码)

26、删除行 : ctrl + shift + k

27、快速切换主题:ctrl + k / ctrl + t

28、快速回到顶部 : ctrl + home 29、快速回到底部 : ctrl + end

30、格式化选定代码 :ctrl + k / ctrl +f

31、选中代码 : shift + 鼠标左键

32、多行同时添加内容(光标) :ctrl + alt + up/down

33、全局替换:ctrl + shift + h

34、当前文件替换:ctrl + h

35、打开最近打开的文件:ctrl + r

36、打开新的命令窗:ctrl + shift + c

37、打开指定文件:ctrl + p

38、新建空文件:ctrl + o

40、当前文件查找:ctrl + f

2.官方windows下快捷方式pdf,mac下,Ctrl对应command,大部分一样

官方windows下快捷方式

3.官方Linux下快捷方式pdf

官方Linux下快捷方式

篇外:vscode部分插件

1、Auto Rename Tag 修改html标签,自动帮你完成尾部闭合标签的同步修改,和webstorm一样。

2、Auto Close Tag 自动闭合HTML标签

4、Beautiful 格式化代码的工具

5、Dash Dash是MacOS的API文档浏览器和代码段管理器

6、Ejs Snippets ejs 代码提示

7、ESLint 检查javascript语法错误与提示

8、File Navigator 快速查找文件

9、Git History(git log) 查看git log

10、Gulp Snippets 写gulp时用到,gulp语法提示。

11、HTML CSS Support 在HTML标签上写class智能提示当前项目所支持的样式

12、HTML Snippets 超级好用且初级的H5代码片段以及提示

13、Debug for Chrome 让vs code映射chrome的debug功能,静态页面都可以用vscode来打断点调试、配饰稍微复杂一点

14、Document this Js的注释模板

15、jQuery Code Snippets jquery提示工具

16、Html2jade html模板转pug模板

17、JS-CSS-HTML Formatter 格式化

18、Npm intellisense require 时的包提示工具

19、Open in browser 打开默认浏览器

20、One Dark Theme 一个vs code的主题

21、Path Intellisense 自动路径补全、默认不带这个功能

22、Project Manager 多个项目之间快速切换的工具

23、Pug(Jade) snippets pug语法提示

24、React Components 根据文件名创建反应组件代码。

25、React Native Tools reactNative工具类为React Native项目提供了开发环境。

26、Stylelint css/sass代码审查

27、Typings auto installer 安装vscode 的代码提示依赖库,基于typtings的

28、View In Browser  默认浏览器查看HTML文件(快捷键Ctrl+F1可以修改)

29、Vscode-icons 让vscode资源目录加上图标、必备

30、VueHelper Vue2代码段(包括Vue2 api、vue-router2、vuex2)

31、Vue 2 Snippets vue必备vue代码提示

32、Vue-color vue语法高亮主题

33、Auto-Open Markdown Preview markdown文件自动开启预览

34、EverMonkey 印象笔记

35、atom one dark atom的一个高亮主题

36、SQL Server 一款命令行版操作数据库

37、 GitLens git所有操作基本都内置

38、Prettify JSON 一款JSON格式化插件

39、TODO Parser 解析项目文件中todo

(强烈推荐部分)
有关更多详细信息,请参阅:

vscode

vscode, Ctrl + 鼠标左键实现文件跳转

发表于 2019-04-19 | 分类于 vscode

vscode, Ctrl + 鼠标左键实现文件跳转

在项目根目录增加jsconfig.json文件

jsconfig.json文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"compilerOptions": {
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"api/*": ["src/api/*"],
"components/*": ["src/components/*"],
"language/*": ["src/language/*"],
"lib/*": ["src/lib/*"],
"mock/*": ["src/mock/*"],
"routes/*": ["src/routes/*"],
"static/*": ["src/static/*"],
}
},
"exclude": [
"node_modules"
]
}

配置效果图

配置效果图

篇外

注:Alt + ←(方向左键)来返回到原来的位置

有关更多详细信息,请参阅:

jsconfig

vscode,根据元素标签,显示展开收起

发表于 2019-04-19 | 分类于 vscode

vscode,根据元素标签,显示展开收起

首选项-设置(可用图形化直接搜索,或用JSON格式显示)

用户设置(User Settings)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"[javascript]": {
"editor.foldingStrategy": "indentation"
},
"[javascriptreact]": {
"editor.foldingStrategy": "indentation"
},
"[typescript]": {
"editor.foldingStrategy": "indentation"
},
"[typescriptreact]": {
"editor.foldingStrategy": "indentation"
}
}

foldingStrategy:控制计算折叠范围的策略。auto 将使用语言特定的折叠策略 (若可用)。indentation 将使用基于缩进的折叠策略。

配置效果图

配置效果图

配置后,编码区效果图

https://note.youdao.com/yws/res/871/WEBRESOURCEf1eb048f7fe84fff5a3cc0edad8ab618

有关更多详细信息,请参阅:

settings

使用nodemailer发送邮件

发表于 2019-04-12 | 分类于 Nodejs

使用nodemailer发送邮件

1.简介

nodemailer是一个nodejs应用的邮件服务模块

logo

  • 官网地址:https://nodemailer.com
  • GitHub地址:https://github.com/nodemailer/nodemailer
  • 本篇文档GitHub地址: https://github.com/niexq/nodemailerTest

2.nodemailer功能特性

  • 支持Unicode编码,包括emoji
  • 支持Window系统环境
  • 支持HTML内容和纯文本内容
  • 支持附件
  • 支持HTML内容中嵌入图片附件
  • 支持TLS/STARTTLS安全的邮件发送
  • 除了支持内置的transport方法,还支持其他插件实现的transport方法
  • 支持DKIM签署消息
  • 支持自定义插件处理消息
  • 支持XOAUTH2登录验证
  • 支持SMTP连接代理
  • 支持ES6
  • 支持从ethereal.email自动生成的电子邮件测试帐户

3.要求

  • node.js v6.0.0或更高版本

4.安装

1
npm install nodemailer --save

5.官方示例

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
"use strict";
const nodemailer = require("nodemailer");

// async..await is not allowed in global scope, must use a wrapper
async function main(){

// Generate test SMTP service account from ethereal.email
// Only needed if you don't have a real mail account for testing
let testAccount = await nodemailer.createTestAccount();

// create reusable transporter object using the default SMTP transport
let transporter = nodemailer.createTransport({
host: "smtp.ethereal.email",
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: testAccount.user, // generated ethereal user
pass: testAccount.pass // generated ethereal password
}
});

// send mail with defined transport object
let info = await transporter.sendMail({
from: '"Fred Foo 👻" <foo@example.com>', // sender address
to: "bar@example.com, baz@example.com", // list of receivers
subject: "Hello ✔", // Subject line
text: "Hello world?", // plain text body
html: "<b>Hello world?</b>" // html body
});

console.log("Message sent: %s", info.messageId);
// Message sent: <b658f8ca-6296-ccf4-8306-87d57a0b4321@example.com>

// Preview only available when sending through an Ethereal account
console.log("Preview URL: %s", nodemailer.getTestMessageUrl(info));
// Preview URL: https://ethereal.email/message/WaQKMgKddxQDoou...
}

main().catch(console.error);
  • 这是一个生成了Ethereal的账户,发送纯文本和HTML正文电子邮件的完整示例。

6.使用自己qq邮箱或163邮箱发送邮件,发送多人邮箱

  • 备注:使用qq邮箱发送需要开启POP3/SMTP服务,复制授权码配置在pass中。163邮箱默认已开通。
  • 6.1 .env文件配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    RUN_LEVEL           = production

    MAIL_HOST = smtp.exmail.qq.com
    MAIL_SERVICE = QQ
    MAIL_PORT = 465
    MAIL_SECURE = true
    MAIL_SECURE_CONNECTION = true
    MAIL_USER = ******@qq.com
    MAIL_PASS = ******
    MAIL_NICKNAME = 👻测试nodemailer发邮件👻
    MAIL_TO = ******@gmail.com,******@163.com,******@qq.com
  • 6.2 config.js文件配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    'use strict';

    const argv = require('yargs').argv;
    require('dotenv').config({
    path: argv.env,
    });
    const env = process.env;

    module.exports = {
    mail: {
    // host: env.MAIL_HOST, // 官方demo方式,暂时注释
    service: env.MAIL_SERVICE, // 使用内置传输发送邮件 查看支持列表:https://nodemailer.com/smtp/well-known
    port: env.MAIL_PORT,
    // secure: env.MAIL_SECURE, // 官方demo方式,暂时注释
    secureConnection: env.MAIL_SECURE_CONNECTION,
    auth: {
    user: env.MAIL_USER,
    pass: env.MAIL_PASS, // 如果是使用QQ邮箱发送,此密码不是邮箱账户的密码而是授权码。
    },
    from: `${env.MAIL_NICKNAME}<${env.MAIL_USER}>`,
    to: env.MAIL_TO,
    },
    };
  • 6.3 lib/sendMail.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 nodemailer = require('nodemailer');
    const mailconf = require('../config').mail;

    // create reusable transporter object using the default SMTP transport
    const transporter = nodemailer.createTransport(mailconf);

    // setup email data with unicode symbols
    const mailOptions = {
    from: mailconf.from, // sender address
    to: mailconf.to, // list of receivers
    subject: '测试标题', // Subject line
    text: 'Hello world?', // plain text body
    html: '<b>Hello world?</b>', // html body
    };

    // send mail with defined transport object
    const sendMail = ({ message = '来自测试nodemailer发邮件', subject = '这个一个默认的测试标题' }) => {
    if (process.env.RUN_LEVEL !== 'production') { // 测试环境可跳过
    return;
    }
    mailOptions.subject = subject;
    mailOptions.html = `<b>${message}</b>`;
    mailOptions.text = `${message}`;
    transporter.sendMail(mailOptions, (error, info) => {
    if (error) {
    return console.log(error);
    }
    console.log(`发送成功,messageId为:${info.messageId}`.green);
    });
    };
  • 6.4 index.js文件代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    require('colors');

    const sendMail = require('./lib/sendMail');

    // 使用封装好的发送邮件方法(可按项目需求,需要的地方调用此方法)
    sendMail({
    message: '这是邮件测试内容',
    subject: '邮件标题hello world'
    });


    console.log('send mail start'.blue);
  • 使用163邮箱发送雷同,运行程序node index.js,成功将返回发送成功,messageId为**。

有关更多详细信息,请参阅:

SMTP transport

Message configuration

HTML Email 编写指南

Git Commit message更优雅的操作

发表于 2019-04-12 | 分类于 Git

Git Commit message更优雅的操作

优雅的Commit message对团队协作开发及其重要。

Git每次提交代码,都要写 Commit message,否则提交不了。

1
git commit -m "hello world"

上面代码的-m参数,就是用来指定 commit message 的。

如果一行不够,可以只执行git commit,就会跳出文本编辑器,让你写多行。

1
$ git commit

基本上,你写什么都行(这里,这里和这里)。

png1

但是,一般来说,commit message 应该清晰明了,说明本次提交的目的。

png2

目前,社区有多种 Commit message 的写法规范。
本文介绍Angular 规范(见上图),这是目前使用最广的写法,比较合理和系统化,并且有配套的工具。

1、Commit message 的作用

格式化的Commit message,有几个好处。

  • 1.1 提供更多的历史信息,方便快速浏览。

比如,下面的命令显示上次发布后的变动,每个commit占据一行。你只看行首,就知道某次 commit 的目的。

1
$ git log <last tag> HEAD --pretty=format:%s

png3

  • 1.2 可以过滤某些commit(比如文档改动),便于快速查找信息。

比如,下面的命令仅仅显示本次发布新增加的功能。

1
$ git log <last release> HEAD --grep feature

  • 1.3 可以直接从commit生成Change log。

Change Log 是发布新版本时,用来说明与上一个版本差异的文档,详见后文。

png4

2、Commit message 的格式

每次提交,Commit message 都包括三个部分:Header,Body 和 Footer。

1
2
3
4
5
<type>(<scope>): <subject>
// 空一行
<body>
// 空一行
<footer>

其中,Header 是必需的,Body 和 Footer 可以省略。

不管是哪一个部分,任何一行都不得超过72个字符(或100个字符)。这是为了避免自动换行影响美观。

  • 2.1 Header
    Header部分只有一行,包括三个字段:type(必需)、scope(可选)和subject(必需)。

    • 2.1.1 type

      type用于说明 commit 的类别,只允许使用下面7个标识。


      • feat:新功能(feature)

      • fix:修补bug

      • docs:文档(documentation)

      • style: 格式(不影响代码运行的变动)

      • refactor:重构(即不是新增功能,也不是修改bug的代码变动)

      • test:增加测试

      • chore:构建过程或辅助工具的变动

      如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中。其他情况(docs、chore、style、refactor、test)由你决定,要不要放入 Change log,建议是不要。

    • 2.1.2 scope

      scope用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。

    • 2.1.3 subject
      subject是 commit 目的的简短描述,不超过50个字符。


      • 以动词开头,使用第一人称现在时,比如change,而不是changed或changes

      • 第一个字母小写

      • 结尾不加句号(.)

  • 2.2 Body
    Body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

    1
    2
    3
    4
    5
    6
    7
    More detailed explanatory text, if necessary.  Wrap it to 
    about 72 characters or so.

    Further paragraphs come after blank lines.

    - Bullet points are okay, too
    - Use a hanging indent

有两个注意点。

(1)使用第一人称现在时,比如使用change而不是changed或changes。

(2)应该说明代码变动的动机,以及与以前行为的对比。

  • 2.3 Footer
    Footer 部分只用于两种情况。

    • 2.3.1 不兼容变动

      如果当前代码与上一个版本不兼容,则 Footer 部分以BREAKING CHANGE开头,后面是对变动的描述、以及变动理由和迁移方法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      BREAKING CHANGE: isolate scope bindings definition has changed.

      To migrate the code follow the example below:

      Before:

      scope: {
      myAttr: 'attribute',
      }

      After:

      scope: {
      myAttr: '@',
      }

      The removed `inject` wasn't generaly useful for directives so there should be no code using it.
    • 2.3.2 关闭 Issue

      如果当前 commit 针对某个issue,那么可以在 Footer 部分关闭这个 issue 。

      1
      Closes #234
也可以一次关闭多个 issue 。

1
Closes #123, #245, #992
  • 2.4 Revert

还有一种特殊情况,如果当前 commit 用于撤销以前的 commit,则必须以revert:开头,后面跟着被撤销 Commit 的 Header。

1
2
3
revert: feat(pencil): add 'graphiteWidth' option

This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

Body部分的格式是固定的,必须写成This reverts commit <hash>.,其中的hash是被撤销 commit 的 SHA 标识符。

如果当前 commit 与被撤销的 commit,在同一个发布(release)里面,那么它们都不会出现在 Change log 里面。如果两者在不同的发布,那么当前 commit,会出现在 Change log 的Reverts小标题下面。

3、Commitizen

Commitizen是一个撰写合格 Commit message 的工具。

1
npm install -g commitizen

然后,在项目目录里,运行下面的命令,使其支持 Angular 的 Commit message 格式。

1
commitizen init cz-conventional-changelog --save --save-exact

以后,凡是用到git commit命令,一律改为使用git cz。这时,就会出现选项,用来生成符合格式的 Commit message。

或创建一个快捷命令

1
2
3
4
...
"scripts": {
"commit": "npx git-cz"
}

png5

4、validate-commit-msg

validate-commit-msg 用于检查 Node 项目的 Commit message 是否符合格式。

它的安装是手动的。首先,拷贝下面这个JS文件,放入你的代码库。文件名可以取为validate-commit-msg.js。

接着,把这个脚本加入 Git 的 hook。下面是在package.json里面使用 ghooks,把这个脚本加为commit-msg时运行。

1
2
3
4
5
"config": {
"ghooks": {
"commit-msg": "./validate-commit-msg.js"
}
}

然后,每次git commit的时候,这个脚本就会自动检查 Commit message 是否合格。如果不合格,就会报错。

1
2
3
4

$ git add -A
$ git commit -m "edit markdown"
INVALID COMMIT MSG: does not match "<type>(<scope>): <subject>" ! was: edit markdown
篇外:5、生成 Change log

如果你的所有 Commit 都符合 Angular 格式,那么发布新版本时, Change log 就可以用脚本自动生成(例1,例2,例3)。

生成的文档包括以下三个部分。


  • New features

  • Bug fixes

  • Breaking changes.

每个部分都会罗列相关的 commit ,并且有指向这些 commit 的链接。当然,生成的文档允许手动修改,所以发布前,你还可以添加其他内容。

conventional-changelog 就是生成 Change log 的工具,运行下面的命令即可。

1
2
3
npm install -g conventional-changelog
cd my-project
conventional-changelog -p angular -i CHANGELOG.md -w

上面命令不会覆盖以前的 Change log,只会在CHANGELOG.md的头部加上自从上次发布以来的变动。

如果你想生成所有发布的 Change log,要改为运行下面的命令。

1
conventional-changelog -p angular -i CHANGELOG.md -w -r 0

为了方便使用,可以将其写入package.json的scripts字段。

1
2
3
4
5
{
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -w -r 0"
}
}

以后,直接运行下面的命令即可。

1
npm run changelog
有关更多详细信息,请参阅:

本篇参阅

git其他相关资料

setState简述

发表于 2019-04-12 | 分类于 Reactjs

用于更新用户界面以响应事件处理器和处理服务器数据的主要方式:setState()

1
setState(updater[, callback])

setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。

将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

setState() 并不总是立即更新组件,它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。

除非 shouldComponentUpdate() 返回 false,否则 setState() 将始终执行重新渲染操作。如果可变对象被使用,且无法在 shouldComponentUpdate() 中实现条件渲染,那么仅在新旧状态不一时调用 setState()可以避免不必要的重新渲染

1.参数一为带有形参的 updater 函数:

1
(prevState, props) => stateChange

prevState 是对应用变化时组件状态的引用。当然,它不应直接被修改。你应该使用基于 prevState 和 props 构建的新对象来表示变化。例如,假设我们想根据 props.step 来增加 state:

1
2
3
this.setState((prevState, props) => {
return {counter: prevState.counter + props.step};
});

updater 函数中接收的 prevState 和 props 都保证为最新。updater 的返回值会与 state 进行浅合并。

2.setState() 的第二个参数为可选的回调函数,它将在 setState 完成合并并重新渲染组件后执行。通常建议使用 componentDidUpdate() 来代替此方式。

1
2
3
4
5
this.setState((prevState, props) => {
return {counter: prevState.counter + props.step};
}, () => {
console.log('~~~~~counter', this.state.counter); // 更新后的counter
});

3.setState() 的第一个参数除了接受函数外,还可以接受对象类型:

1
setState(stateChange[, callback])

stateChange 会将传入的对象浅层合并到新的 state 中,例如,调整购物车商品数:

1
this.setState({quantity: 2})

这种形式的 setState() 也是异步的,并且在同一周期内会对多个 setState 进行批处理。例如,如果在同一周期内多次设置商品数量增加,则相当于:

1
2
3
4
5
6
Object.assign(
previousState,
{quantity: state.quantity + 1},
{quantity: state.quantity + 1},
...
)

后调用的 setState() 将覆盖同一周期内先调用 setState 的值,因此商品数仅增加一次。如果后续状态取决于当前状态,建议使用 updater 函数的形式代替:

1
2
3
this.setState((state) => {
return {quantity: state.quantity + 1};
});

篇外总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
handleGoodsImport = () => {
this.setState(prevState => ({
bulkGoodsOpen: !prevState.bulkGoodsOpen,
uploadStatus: '0'
}), () => {
console.log('~~~bulkGoodsOpen', this.state.bulkGoodsOpen)
})
} // good
handleGoodsImport = () => {
this.setState({
bulkGoodsOpen: !this.state.bulkGoodsOpen,
uploadStatus: '0'
}, () => {
console.log('~~~bulkGoodsOpen', this.state.bulkGoodsOpen)
})
} // bad
有关更多详细信息,请参阅:

本篇参阅

State 和生命周期指南

深入学习:何时以及为什么 setState() 会批量执行?

深入:为什么不直接更新 this.state?

nodejs项目,从.env文件加载环境变量

发表于 2019-04-01 | 分类于 Nodejs

nodejs项目,从.env文件加载环境变量,ditenv简介

1.简介

  • dotenv就是一个可以使得Node.js项目,从文件中加载环境变量的NPM包。安装dotenv后,我们只需要将程序的环境变量配置写在.env文件中。其作用将.env配置文件解析为json对象,并对其中的key-value键值对通过process.env将其赋值为环境变量。之后便可通过process.env[key],process.env.MONGO_URI获取对应环境变量。

2.安装

1
2
3
4
5
# with npm
npm install dotenv

# or with Yarn
yarn add dotenv

3.使用

  • 一般在项目入口文件index.js中,尽早引入dotenv

    1
    require('dotenv').config();
  • 在项目的根目录中创建.env文件,在新行中以name=value的形式添加特定于环境的变量,例如:

    1
    2
    3
    4
    5
    MYSQL_HOST=localhost
    MYSQL_USER=root
    MYSQL_PASSWORD=xxxxxx
    MONGO_URI=user:password@localhost:27017/data_develop?authSource=admin
    MONGO_POOLSIZE=5
  • 现在process.env拥有您在.env文件中定义的键和值, 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const db = require('db')
    db.connect({
    host: process.env.MYSQL_HOST,
    username: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD
    })

    ...

    const mongoose = require('mongoose');
    const mongoose = require('mongoose');
    mongoose.connect(`mongodb://${process.env.MONGO_URI}`, {
    poolSize: process.env.MONGO_POOLSIZE,
    autoIndex: false
    });

4.其他实用选择项

  • 如果包含环境变量的文件位于其他位置,则可以指定自定义路径。
  • Path,Default: path.resolve(process.cwd(), ‘.env’)
    1
    2
    3
    4
    const argv = require('yargs').argv;
    require('dotenv').config({
    path: argv.env
    });

5.dotenv源码:dotenv/lib/main.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
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

const fs = require('fs')
const path = require('path')

function log (message /*: string */) {
console.log(`[dotenv][DEBUG] ${message}`)
}

const NEWLINE = '\n'
const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/
const RE_NEWLINES = /\\n/g

// Parses src into an Object
function parse (src /*: string | Buffer */, options /*: ?DotenvParseOptions */) /*: DotenvParseOutput */ {
const debug = Boolean(options && options.debug)
const obj = {}

// convert Buffers before splitting into lines and processing
src.toString().split(NEWLINE).forEach(function (line, idx) {
// matching "KEY' and 'VAL' in 'KEY=VAL'
const keyValueArr = line.match(RE_INI_KEY_VAL)
// matched?
if (keyValueArr != null) {
const key = keyValueArr[1]
// default undefined or missing values to empty string
let val = (keyValueArr[2] || '')
const end = val.length - 1
const isDoubleQuoted = val[0] === '"' && val[end] === '"'
const isSingleQuoted = val[0] === "'" && val[end] === "'"

// if single or double quoted, remove quotes
if (isSingleQuoted || isDoubleQuoted) {
val = val.substring(1, end)

// if double quoted, expand newlines
if (isDoubleQuoted) {
val = val.replace(RE_NEWLINES, NEWLINE)
}
} else {
// remove surrounding whitespace
val = val.trim()
}

obj[key] = val
} else if (debug) {
log(`did not match key and value when parsing line ${idx + 1}: ${line}`)
}
})

return obj
}

// Populates process.env from .env file
function config (options /*: ?DotenvConfigOptions */) /*: DotenvConfigOutput */ {
let dotenvPath = path.resolve(process.cwd(), '.env')
let encoding /*: string */ = 'utf8'
let debug = false

if (options) {
if (options.path != null) {
dotenvPath = options.path
}
if (options.encoding != null) {
encoding = options.encoding
}
if (options.debug != null) {
debug = true
}
}

try {
// specifying an encoding returns a string instead of a buffer
const parsed = parse(fs.readFileSync(dotenvPath, { encoding }), { debug })

Object.keys(parsed).forEach(function (key) {
if (!process.env.hasOwnProperty(key)) {
process.env[key] = parsed[key]
} else if (debug) {
log(`"${key}" is already defined in \`process.env\` and will not be overwritten`)
}
})

return { parsed }
} catch (e) {
return { error: e }
}
}

module.exports.config = config
module.exports.parse = parse

Git SSH Key生成添加

发表于 2019-04-01 | 分类于 Git

Git SSH Key生成添加(假设本地已安装Git)

执行命令步骤:

1.跳转到.ssh目录;[如果没有对应的文件夹,则执行 mkdir ~/.ssh]

1
cd ~/.ssh/

2.设置git用户名和邮箱

1
2
git config --global user.name "xq12345"
git config --global user.email "xq12345@qq.com"

3.生成SSH密钥过程;

1
sh-keygen -t rsa -C "xq12345@qq.com"

4.查看是否生成SSH密钥;[两个对应文件:id_rsa(私钥)、id_rsa.pub(公钥)]

1
ls

5.登陆远程仓库,复制id_rsa.pub里面的内容添加到远程仓库SSH keys中。

1
vim id_rsa.pub

为什么设置SSH到远程仓库?

使用密码登录,每次都必须输入密码,非常麻烦。好在SSH还提供了公钥登录,可以省去输入密码的步骤。

  • 所谓”公钥登录”,原理很简单,就是用户将自己的公钥储存在远程主机上。登录的时候,远程主机会向用户发送一段随机字符串,用户用自己的私钥加密后,再发回来。远程主机用事先储存的公钥进行解密,如果成功,就证明用户是可信的,直接允许登录shell,不再要求密码,这种方法要求用户必须提供自己的公钥。

篇外:啥是SSH?

定义

SSH(安全外壳协议),SSH 为 Secure Shell 的缩写,SSH是一种网络协议,用于计算机之间的加密登录。

功能

SSH能够保证安全,原因在于它采用了公钥加密。

  • 传统的网络服务程序,如:ftp、pop和telnet在本质上都是不安全的,因为它们在网络上用明文传送口令和数据,别有用心的人非常容易就可以截获这些口令和数据。而且,这些服务程序的安全验证方式也是有其弱点的, 就是很容易受到“中间人”(man-in-the-middle)这种方式的攻击。所谓“中间人”的攻击方式, 就是“中间人”冒充真正的服务器接收你传给服务器的数据,然后再冒充你把数据传给真正的服务器。服务器和你之间的数据传送被“中间人”一转手做了手脚之后,就会出现很严重的问题。通过使用SSH,你可以把所有传输的数据进行加密,这样”中间人”这种攻击方式就不可能实现了,而且也能够防止DNS欺骗和IP欺骗。使用SSH,还有一个额外的好处就是传输的数据是经过压缩的,所以可以加快传输的速度。SSH有很多功能,它既可以代替Telnet,又可以为FTP、PoP、甚至为PPP提供一个安全的”通道”

中间人攻击

SSH协议登录过程:远程主机收到用户的登录请求,把自己的公钥发给用户 –> 用户使用这个公钥,将登录密码加密后,发送回来 –> 远程主机用自己的私钥,解密登录密码,如果密码正确,就同意用户登录

  • 这个过程本身是安全的,但是实施的时候存在一个风险:如果有人截获了登录请求,然后冒充远程主机,将伪造的公钥发给用户,那么用户很难辨别真伪。因为不像https协议,SSH协议的公钥是没有证书中心(CA)公证的,也就是说,都是自己签发的。
    可以设想,如果攻击者插在用户与远程主机之间(比如在公共的wifi区域),用伪造的公钥,获取用户的登录密码。再用这个密码登录远程主机,那么SSH的安全机制就荡然无存了。这种风险就是著名的”中间人攻击”(Man-in-the-middle attack)。
123
niexq

niexq

30 日志
8 分类
24 标签
GitHub E-Mail StackOverflow
友情链接
  • hexo
  • ruanyifeng
  • w3school
  • runoob
浙ICP备20011079号 © 2021 niexq
0%