Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

连接协议对比

常见的几种连接协议:

  • SSH2(默认,相对于SSH1进行了加密算法的改进,使用最广泛)
  • SSH1
  • Telnet
  • Telnet/SSL
  • Rlogin
  • Serial
  • TAPI

在出现 SSH 之前,系统管理员需要登入远程服务器执行系统管理任务时,都是用 telnet 来实现的,telnet 协议底层使用 TCP 协议,端口号 23,采用明文密码传送,在传送过程中对数据也不加密,很容易被不怀好意的人在网络上监听到密码。

同样,在 SSH 工具出现之前 R 系列命令也很流行(由于这些命令都以字母 r 开头,故把这些命令合称为 R 系列命令,R 是 remote 的意思),比如 rexec 是用来执行远程服务器上的命令的,和 telnet 的区别是 telnet 需要先登录远程服务器再实行相关的命令,而 R 系列命令可以把登录和执行命令并登出系统的操作整合在一起。这样就不需要为在远程服务器上执行一个命令而特地登录服务器了。

SSH 全称 Secure SHell,顾名思义就是非常安全的 shell 的意思,SSH 协议是 IETF(Internet Engineering Task Force) 的 Network Working Group 所制定的一种协议。SSH 的主要目的是用来取代传统的 telnet 和 R 系列命令rloginrshrexec 等)远程登录和远程执行命令的工具,实现对远程登录和远程执行命令加密。防止由于网络监听而出现的密码泄漏,对系统构成威胁。

SSH 是一种加密协议,不仅在登录过程中对密码进行加密传送,而且对登录后执行的命令的数据也进行加密,这样即使别人在网络上监听并截获了你的数据包,他也看不到其中的内容。SSH 协议底层使用 TCP 协议,端口号 22

鉴权方式对比

不同于 telnet 只支持 Password 密码鉴权,SSH 同时支持以下几种鉴权方式(Authentication):

  • Password(密码)
  • Public Key(公钥)
  • Keyboard Interactive(键盘交互)
  • GSSAPI

目前 SSH 最常用的鉴权方式有 Password 和 Public key 。Public Key 非对称(asymmetric)鉴权认证使用一对相关联的 Key Pair(一个公钥 Public Key,一个私钥 Private Key)来代替传统的密码(Password)。顾名思义,Public Key 是用来公开的,可以将其放到 SSH 服务器自己的帐号中,而 Private Key 只能由自己保管,用来证明自己身份。

使用 Public Key 加密过的数据只有用与之相对应的 Private Key 才能解密。这样在鉴权的过程中,Public Key 拥有者便可以通过 Public Key 加密一些东西发送给对应的 Private Key 拥有者,如果在通信的双方都拥有对方的 Public Key(自己的 Private Key 只由自己保管),那么就可以通过这对 Key Pair 来安全地交换信息,从而实现相互鉴权。

OpenSSH

OpenSSH 是 SSH 协议的免费开源实现。OpenSSH 套件由以下工具集组成:

远程操作工具:

  • ssh(替代 telnetrlogin
  • scp(替代 rcp
  • sftp(替代 ftp

公私钥管理工具:

  • ssh-add Tool which adds private keys to the authentication agent.
  • ssh-keygen Key generation tool.
  • ssh-keysign Helper program for host-based authentication.
  • ssh-keyscan Utility for gathering public host keys from a number of hosts.

客户端工具:

  • ssh-agent An authentication agent that can store private keys.

服务端工具:

  • sshd 一个运行于服务端的独立守护进程(standalone daemon)
  • sftp-server SFTP 服务器

常用命令

当管理的服务器较多时,ssh 远程需要频繁的输入用户名、密码、服务器 IP,操作非常繁琐,下面介绍一些命令结合配置以简化操作。

ssh

ssh 命令用法:

1
$ ssh [-p port] [user@]hostname [command]

有时候输入 ssh 的参数繁琐,一旦服务器较多,要一个个记住并且敲入时非常低效。因此 ssh 提供了配置文件的方式简化命令行选项。ssh 依序从下列来源中获取配置,最先获取的值将优先使用:

  1. 命令行选项(command-line options)
  2. 用户配置文件 ~/.ssh/config
  3. 系统配置文件 /etc/ssh/ssh_config

常用配置项如下:

1
2
3
4
5
Host    别名
HostName 主机名
Port 端口
User 用户名
IdentityFile 密钥文件的路径

通过配置,ssh 远程命令简化如下:

1
$ ssh 别名

例如:

1
$ ssh pc2 /sbin/ifconfig

pc2 是从 ~/.ssh/config 中获取的 hostname 别名。

ssh-keygen

在使用 ssh 进行远程登录时,由于默认使用的是 Password 鉴权方式,因此每次登录都需要输入密码,操作麻烦。下面介绍使用 Public Key 鉴权方式实现免密登录

一、创建一对公私钥:

1
2
$ ssh-keygen -t rsa -C who@where
询问密码时,保持为空并回车

二、启动 SSH 认证代理程序:

1
2
$ eval `ssh-agent`
Agent pid 1760

三、添加私钥:

1
2
3
4
$ ssh-add ~/.ssh/id_rsa
Identity added: ~/.ssh/id_rsa
$ ssh-add -l
2048 8a:63:12:ae:b1:4c:be:03:e7:7f:92:3e:e5:44:56:bb ~/.ssh/id_rsa (RSA)

四、将公钥上传到服务端,添加到被登录帐户可信列表文件

1
$ scp ~/.ssh/id_rsa.pub who@where:~/.ssh/authorized_keys

五、修改服务端文件权限:

1
2
$ chmod 700 ~/.ssh
$ chmod 600 ~/.ssh/*

之后再使用 ssh 登录时,客户端的 ssh-agent 会发送私钥去和服务端上的公钥做匹配,如果匹配成功就可以免密登录了。

ssh-add

ssh-add 命令常见用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
usage: ssh-add [options] [file ...]
Options:
-l List fingerprints of all identities.
-E hash Specify hash algorithm used for fingerprints.
-L List public key parameters of all identities.
-k Load only keys and not certificates.
-c Require confirmation to sign using identities
-t life Set lifetime (in seconds) when adding identities.
-d Delete identity.
-D Delete all identities.
-x Lock agent.
-X Unlock agent.
-s pkcs11 Add keys from PKCS#11 provider.
-e pkcs11 Remove keys provided by PKCS#11 provider.

例如:

1
2
3
4
5
$ ssh-add -D
All identities removed.

$ ssh-add -l
The agent has no identities.

参考:http://linux.101hacks.com/unix/ssh-add/

scp

scp 命令常用参数:

1
2
3
-r 递归复制(用以传输文件夹)
-p 传输时保留文件权限及时间戳
-C 传输时进行数据压缩

可以结合 bash 的 for 循环实现批量 scp 目录:

1
2
3
4
5
6
7
8
#!/bin/bash

HOST_IP=('192.168.0.1' '192.168.0.2' '192.168.0.3')

for ip in ${HOST_IP[@]}
do
scp -rp /some/files ${ip}:/some/
done

相关文件

SSH 相关文件和配置:

文件 描述
~/.ssh/id_rsa.pub 公钥(Public Key)
~/.ssh/id_rsa 私钥(Private Key)
~/.ssh/known_hosts 位于客户端的公钥列表文件,首次与目标主机建立 SSH 连接时,需要添加对方的公钥到这个文件以便后续通信
《关于 Linux 中 known_hosts 文件的必知必会》
~/.ssh/authorized_keys 位于服务端的公钥列表文件,列出了所有被允许登录进来的可信公钥信息(Lists the public keys that are permitted for logging in)
~/.ssh/config 客户端用户配置文件,可以通过 man ssh_config 命令查看帮助。
/etc/ssh/ssh_config 客户端系统配置文件
/etc/ssh/sshd_config 服务端配置文件

ssh banner

https://en.wikipedia.org/wiki/ASCII_art

保持会话不掉线

1
2
3
Host *
ServerAliveInterval 240
ServerAliveCountMax 2

ServerAliveInterval 设置了客户端在发送保持连接信号之前的等待时间。

OpenSSL

OpenSSL 加密库 ( libcrypto) 实现了各种 Internet 标准中使用的各种加密算法。该库提供的服务被 TLS 和 CMS 的 OpenSSL 实现使用,它们也被用于实现许多其它第三方产品和协议。

该库的功能包括对称加密、公钥加密、密钥协商、证书处理、加密散列函数、加密伪随机数生成器、消息身份验证代码 (MAC)、密钥派生函数 (KDF) 和各种实用程序。

OpenSSL

Generate Private and Public Key

https://www.openssl.org/docs/man3.0/man1/openssl.html

  1. Create Private Key
1
openssl genrsa -out rsa_private_key.pem 2048
  1. Generate Public Key
1
openssl rsa -in rsa_private_key.pem -out rsa_public_key.pem -pubout
  1. Encode Private Key to PKCS#8
1
openssl pkcs8 -topk8 -in rsa_private_key.pem -out pkcs8_rsa_private_key.pem -nocrypt

For signature, please use pkcs8_rsa_private_key.pem (result of step 3).

rsync

批量 scp 的缺点是会全量同步,且删除行为无法同步,可以用 rsync 命令优化:

1
2
3
4
5
拉取:
$ rsync [option...] [user@]host:src... [dest]

推送:
$ rsync [option...] src... [user@]host:dest

如果双方都修改了同一文件的同一个地方,rsync 不管源和目标的修改时间谁先谁后,而是以源作为基准去覆盖目标文件。

常用参数:

  • -a:归档模式,等价于 -rlptgoD(不包括 -H, -A, -X
    • -r, --recursive:递归遍历目录
    • -l, --links:复制软链接(symbolic link, symlinks)
    • -p, --perms:保留权限
    • -t, --times:保留修改时间
    • -g, --group:保留属组
    • -o, --owner:保留属主
    • -D:等价于:
      • --devices:保留设备文件
      • --specials:保留特殊文件
  • -v, --verbose:详细输出信息
  • -H, --hard-links:保留硬链接(hard links)

批量 rsync

1
2
3
4
5
6
7
8
#!/bin/bash

HOST_IP=('192.168.0.1' '192.168.0.2' '192.168.0.3')

for ip in ${HOST_IP[@]}
do
rsync -avH --delete /some/* ${ip}:/some/
done

进行如下文件操作测试:

  • 新增文件:web-banner-20170717.jpg
  • 删除文件:web-banner-20170716.jpg

从输出可见,只会增量同步并删除指定的文件:

1
2
3
4
sending incremental file list
resmarket/static/site/v1/img/banner/
resmarket/static/site/v1/img/banner/web-banner-20170717.jpg
deleting resmarket/static/site/v1/img/banner/web-banner-20170716.jpg

参考

https://en.wikipedia.org/wiki/Telnet

https://en.wikipedia.org/wiki/Secure_Shell

在 Linux 上保护 SSH 服务器连接的 8 种方法 | 良许 Linux

5 Unix / Linux ssh-add Command Examples to Add SSH Key to Agent

ssh keygen 中生成的 randomart image 是什么

SSH Not Working In MacOS Ventura: How To Fix

https://wizardzines.com/comics/ssh/
SSH

《rsync同步的艺术》–linux命令五分钟系列之四十二

https://wizardzines.com/comics/openssl/
OpenSSL

某段时期前端技术选型上使用过 Require.js 解决前端模块化的问题,下面整理了一些简单实践:

Require.js 使用

第一步,按功能将 JS 分门别类:

  • dist/
  • app/
    • js/
      • require_config.js
      • entry/
        • main.js
      • module/
        • sub.js
      • lib/
        • require.js
        • zepto.js
      • util/
    • css/
    • img/
    • ……
本地路径 功能描述
js/entry 各功能主模块(主入口)
js/module 各功能子模块
js/lib 第三方库
js/util 自定义库

第二步,参考《Patterns for separating config from the main module》将 RequireJS 配置项从各功能主模块中剥离出来,放到 js/require_config.js 统一管理:

1
2
3
4
5
6
7
8
9
10
11
/**
* 定义 RequireJS 全局配置
*/
var require = {
paths: {
mod: '/contextpath/js/module',
wgt: '/contextpath/js/widget'
lib: '/contextpath/js/lib',
util: '/contextpath/js/util'
}
};

第三步,在页面中引入配置文件及入口文件,注意先后顺序:

1
2
3
4
<!-- 先注册配置 -->
<script src="js/require_config.js"></script>
<!-- 然后引入 require.js ,并载入主模块 main.js -->
<script src="js/lib/require.js" data-main="js/entry/main.js"></script>

第四步,使用 define() 函数编写子模块 js/module/sub.js。引入所需的模块,如 zepto:

1
2
3
4
5
6
/**
* 子模块依赖 zepto
*/
define(['lib/zepto'], function($) {
...
});

第五步,使用 require() 函数编写主模块 js/entry/main.js

1
2
3
4
5
6
/**
* 主模块依赖 sub.js
*/
require(['mod/sub'], function(sub) {
...
});

通过 AMD 规范定义的两个关键函数 define()require() ,我们可以很轻松的在老版本 ES5 上实现模块化功能,解决依赖关系混乱和全局变量的问题。

Require.js 构建

需要注意的是,模块拆分之后脚本文件数量会变多,HTTP 请求也会相应增多。使用 RequireJS 的优化工具 r.js 合并压缩相关联的脚本文件,可以解决这个问题。

参考

JavaScript 模块化技术的起源:《A JavaScript Module Pattern

JavaScript 模块化技术的一些高级特性:《JavaScript Module Pattern: In-Depth》(中文版

RequireJS 的一些入门用法参考:

一张图简要描述 Spring MVC 的处理流程:

Spring MVC

  • Spring MVC 的核心前端控制器 DispatcherServlet 接收 HTTP 请求并询问 Handler mapping 该请求应该转发到哪个 Controller 方法。
  • Controller 业务处理完毕,返回 逻辑视图名(通常是一个字符串)
  • 最后 viewResolver 解析逻辑视图名并返回相应的 View,如 JSP、FreeMarker。

实现一个 Controller

下面介绍编写控制器过程中常用的注解:

定义一个控制器

@Controller

Traditional MVC controller relying on a view technology to perform server-side rendering.

@RestController

RESTful web service controller simply populates and returns a object that will be written directly to the HTTP response as JSON. Thanks to Spring’s HTTP message converter support, you don’t need to do this conversion manually. Because Jackson 2 is on the classpath, Spring’s MappingJackson2HttpMessageConverter is automatically chosen to convert the object to JSON.

It’s shorthand for @Controller and @ResponseBody rolled together.

映射请求

@RequestMapping

@RequestMapping 用于将 HTTP 请求映射到指定的 Controller 类或方法。

一、可匹配的请求属性如下:

  • value 用于匹配指定的请求路径,例如:value = "/index"
  • method 用于匹配指定的请求方法,例如:method = RequestMethod.POST
  • consumes 用于匹配指定的请求头 Content-Type,例如:consumes = MediaType.APPLICATION_JSON_UTF8_VALUE
  • produces 用于匹配指定的请求头 Accept,例如:produces = MediaType.APPLICATION_JSON_UTF8_VALUE
  • params 用于匹配指定的请求参数,例如:
    • 匹配存在:params = "myParam"
    • 匹配不存在:params = "!myParam"
    • 匹配指定参数值:params = "myParam=myValue"
  • headers 用于匹配指定的请求头,例如:
    • 匹配存在:headers = "myHeader"
    • 匹配不存在:headers = "!myHeader"
    • 匹配指定值:headers = "myHeader=myValue"

尽管你可以使用媒体类型通配符(例如:content-type=text/*accept=xxx)去匹配 Content-TypeAccept 请求头,但还是更推荐使用 consumesproduces 属性。因为它们专门用于此目的。

二、Spring Framework 4.3 还引入了五个等价的变体注解,相当于 @RequestMapping 注解与 method 属性的组合使用:

1
2
3
4
5
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping

三、标注了 @RequestMapping 注解的方法可以拥有非常灵活的方法签名。支持以下参数类型/注解、返回类型/注解:

方法参数类型

@RequestMapping 注解的方法,参数可以是下列任一类型:

Request / Response

用于访问当前 javax.servlet.http.HttpServletRequest / javax.servlet.http.HttpServletResponse

1
2
3
4
5
@RequestMapping("/index")
public void go(HttpServletRequest request, HttpServletResponse response) {
request.getHeader("host"); // 读取指定 HTTP 请求头
response.getWriter().write("hello world"); // 浏览器将会显示:hello world
}

使用这类方法参数有点类似于传统的 Servlet 编程。

InputStream / Reader

用于访问当前请求内容的 java.io.InputStream / java.io.Reader

OutputStream / Writer

用于生成当前响应内容的 java.io.OutputStream / java.io.Writer

1
2
3
4
@RequestMapping("/index")
public void go(Writer writer) {
writer.write("hello world"); // 浏览器将会显示:hello world
}

Session

用于访问当前 javax.servlet.http.HttpSession

1
2
3
4
@RequestMapping("/index")
public void go(HttpSession session) {
session.getAttribute("xxx"); // 读取指定 Session 值
}

HttpEntity<?>

HttpEntity<?> 用于同时访问 HTTP 请求头和请求体(HTTP request headers and contents)

1
2
3
4
5
6
@RequestMapping("/index")
public void go(HttpEntity<String> httpEntity) {
String body = httpEntity.getBody();
HttpHeaders headers = httpEntity.getHeaders();
String host = headers.getFirst("host");
}

Map / Model / ModelMap

Map / Model / ModelMap 用于在 Controller 层填充将会暴露给 View 层的 Model 对象。

方法参数注解

尽管使用常规类型的方法参数更接近于人们所熟悉的传统 Servlet 编程,但在 Spring 编程中却不建议这么做。因为这样会导致 JavaBean 与 Servlet 容器耦合,侵入性强,难以进行单元测试(如 Mock 测试)。最佳实践应当是传入注解后被解析好的数据类型,下面介绍这些常用的注解:

@PathVariable

@PathVariable 用于标注某个方法参数与某个 URI 模板变量(URI template variable) 的绑定关系,常用于 RESTful URL,例如 /hotels/{hotel}

@RequestParam

@RequestParam 用于标注某个方法参数与某个 HTTP 请求参数(HTTP request parameter) 的绑定关系。

Annotation which indicates that a method parameter should be bound to a web request parameter.

Supported for annotated handler methods in Spring MVC and Spring WebFlux as follows:

  • In Spring MVC, “request parameters” map to query parameters, form data, and parts in multipart requests. This is because the Servlet API combines query parameters and form data into a single map called “parameters”, and that includes automatic parsing of the request body.
  • In Spring WebFlux, “request parameters” map to query parameters only. To work with all 3, query, form data, and multipart data, you can use data binding to a command object annotated with ModelAttribute.

If the method parameter type is Map and a request parameter name is specified, then the request parameter value is converted to a Map assuming an appropriate conversion strategy is available.

If the method parameter is Map or MultiValueMap and a parameter name is not specified, then the map parameter is populated with all request parameter names and values.

例子 1:POST with form data

1
2
3
4
5
POST /test HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

data=123,234

例子 2:GET with query string parameters

1
2
GET /test?data=123,234 HTTP/1.1
Host: localhost:8080

代码:

1
2
3
4
@RequestMapping("/test")
public void test(@RequestParam("data") String data) {
log.info(data); // 123, 234
}

Type conversion is applied automatically if the target method parameter type is not String. See the section called “Method Parameters And Type Conversion”.

1
2
3
4
@RequestMapping("/test")
public void test(@RequestParam("data") ArrayList<String> data) {
log.info(data.toString()); // 123, 234
}

When an @RequestParam annotation is used on a Map<String, String> or MultiValueMap<String, String> argument, the map is populated with all request parameters.

1
2
3
4
@RequestMapping("/test")
public void test(@RequestParam("data") Map<String, String> data) {
log.info(data.get("data")); // 123, 234
}

使用时需要注意 required 这个属性:

  • 方法参数不写 @RequestParam,默认的 requiredfalse
  • 方法参数写了 @RequestParam,默认的 requiredtrue
  • 方法参数同时写了 @RequestParam + defaultValue,默认的 requiredfalse

@RequestBody

@RequestBody 用于标注某个方法参数与某个 HTTP 请求体(HTTP request body) 的绑定关系。 @RequestBody 会调用合适的 message convertersHTTP 请求体(HTTP request body) 写入指定对象,默认的 HttpMessageConverter 如下:

content-type Description
ByteArrayHttpMessageConverter converts byte arrays.
StringHttpMessageConverter converts strings.
application/x-www-form-urlencoded FormHttpMessageConverter converts form data to/from a MultiValueMap<String, String>.
application/json MappingJackson2HttpMessageConverter converts JSON.

例子:

1
2
3
4
5
POST /test HTTP/1.1
Host: localhost:8080
Content-Type: application/json

["123", "234"]
1
2
3
@RequestMapping("/test")
public void test(@RequestBody List<String> data) {
}

An @RequestBody method parameter can be annotated with @Valid, in which case it will be validated using the configured Validator instance. When using the MVC namespace or the MVC Java config, a JSR-303 validator is configured automatically assuming a JSR-303 implementation is available on the classpath.

@RequestHeader

@RequestHeader 用于标注某个方法参数与某个 HTTP 请求头(HTTP request header) 的绑定关系。

@CookieValue

@CookieValue 用于标注某个方法参数与某个 HTTP cookie 的绑定关系。方法参数可以是 javax.servlet.http.Cookie,也可以是具体的 Cookie 值(如字符串、数字类型等)。

举个例子:

1
2
3
4
5
6
@ResponseBody
@RequestMapping("/index")
public Employee getEmployeeBy(
@RequestParam("name") String name,
@RequestHeader("host") String host,
@RequestBody String body) {...}

方法参数验证

Spring MVC 可以快速整合 JSR 303 - Bean Validation 实现方法参数校验,用法如下:

1
2
3
4
5
6
7
8
9
@ResponseBody
@RequestMapping(value = "/test")
public String test(@Validated RequestVO request, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
String errMsg = bindingResult.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
...
}

用到:

org.springframework.validation.annotation.Validated 注解

org.springframework.validation.BindingResult 接口

参考:

Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC

方法返回类型

@RequestMapping 注解的方法,返回类型可以是下列任一常规类型:

HttpEntity<?>

org.springframework.http.HttpEntity

Represents an HTTP request or response entity, consisting of headers and body.

Typically used in combination with the RestTemplate, like so:

1
2
3
4
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
HttpEntity<String> entity = new HttpEntity<String>(helloWorld, headers);
URI location = template.postForLocation("http://example.com", entity);

or

1
2
3
HttpEntity<String> entity = template.getForEntity("http://example.com", String.class);
String body = entity.getBody();
MediaType contentType = entity.getHeaders().getContentType();

Can also be used in Spring MVC, as a return value from a @Controller method:

1
2
3
4
5
6
@RequestMapping("/handle")
public HttpEntity<String> handle() {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("MyResponseHeader", "MyValue");
return new HttpEntity<String>("Hello World", responseHeaders);
}

ResponseEntity<?>

org.springframework.http.ResponseEntity

Extension of HttpEntity that adds a HttpStatus status code. Used in RestTemplate as well @Controller methods.

In RestTemplate, this class is returned by getForEntity() and exchange():

1
2
3
4
ResponseEntity<String> entity = template.getForEntity("http://example.com", String.class);
String body = entity.getBody();
MediaType contentType = entity.getHeaders().getContentType();
HttpStatus statusCode = entity.getStatusCode();

Can also be used in Spring MVC, as the return value from a @Controller method:

1
2
3
4
5
6
7
8
@RequestMapping("/handle")
public ResponseEntity<String> handle() {
URI location = ...;
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setLocation(location);
responseHeaders.set("MyResponseHeader", "MyValue");
return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}

Or, by using a builder accessible via static methods:

1
2
3
4
5
@RequestMapping("/handle")
public ResponseEntity<String> handle() {
URI location = ...;
return ResponseEntity.created(location).header("MyResponseHeader", "MyValue").body("Hello World");
}

ModelAndView

org.springframework.web.servlet.ModelAndView

Holder for both Model and View in the web MVC framework.

String

表示直接返回视图名。

方法返回注解

@ResponseBody

用于标注某个方法返回值与 WEB 响应体(response body) 的绑定关系。 @ResponseBody 会跳过 ViewResolver 部分,调用合适的 message converters,将方法返回值作为 WEB 响应体(response body) 写入输出流。

1
2
3
4
5
@RequestMapping("/index")
@ResponseBody
public Date go() {
return new Date();
}

@ResponseStatus

@ResponseStatus 用于返回 HTTP 响应码,例如返回 404:

1
2
3
4
@RequestMapping("/index")
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "找不到网页")
public void go() {
}

视图处理

统一异常处理

Classes annotated with @ControllerAdvice can contain @ExceptionHandler, @InitBinder, and @ModelAttribute annotated methods, and these methods will apply to @RequestMapping methods across all controller hierarchies as opposed to the controller hierarchy within which they are declared.

@RestControllerAdvice is an alternative where @ExceptionHandler methods assume @ResponseBody semantics by default.

Both @ControllerAdvice and @RestControllerAdvice can target a subset of controllers:

1
2
3
4
5
6
7
8
9
10
11
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class AnnotationAdvice {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class BasePackageAdvice {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class AssignableTypesAdvice {}

例子:

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(Throwable.class)
public ResponseEntity<String> handleException(Throwable e){
log.error(e.getMessage(), e);
return new ResponseEntity<>(e.getMessage(), HttpStatus.valueOf(500));
}

}

CORS 支持

核心配置

参考

Serving Web Content with Spring MVC

Building a RESTful Web Service

Enabling Cross Origin Requests for a RESTful Web Service

告别混乱代码: Spring 后端接口规范

本文目的:

  • 能够理解事件绑定和事件委托两种机制的区别
  • 能够使用原生 API 和 jQuery API 两种方式进行事件委托

Native API

项目开发时遇到一个需求:修改页面中所有 A 链接的默认行为。

事件绑定

最开始想到了用“事件绑定”机制进行实现:

1
2
3
4
5
6
7
8
9
10
11
var showMessage = function(event) {
// 阻止 A 链接的默认行为(不进行跳转)
event.preventDefault();
// 仅弹窗显示链接的 href 属性
alert(event.currentTarget.href);
};

var links = document.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
links[i].addEventListener('click', showMessage, false);
}

这种做法的问题是:如果页面中绑定了大量的事件处理程序,将直接影响页面的整体运行性能,因为:

  1. 函数即对象,对象越多,越占用内存,性能就越差。
  2. 事件绑定前,必须先找到指定的 DOM 元素。而 DOM 元素查找次数越多,页面的交互就绪时间就越长。

更麻烦的是,如果页面加载完后再次插入新元素,需要再次绑定事件处理程序,灵活性差:

1
2
3
4
5
var newLink = document.createElement("a");
newLink.innerHTML = 'Click Me';
newLink.href = 'http://localhost';
newLink.addEventListener('click', showMessage, false);
document.body.appendChild(newLink);

事件委托

利用事件委托机制可以同时解决上述两个问题。只需在 DOM 树中尽量最高的层次上添加一个事件处理程序,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var showMessage = function(event, target) {
// 阻止 A 链接的默认行为(不进行跳转)
event.preventDefault();
// 仅弹窗显示链接的 href 属性
alert(target.href);
},
// 递归查询指定父元素
findTarget = function(target, tagName) {
while (target.tagName && target.tagName !== tagName.toUpperCase()) {
target = target.parentNode;
}
return (target.tagName && target.tagName === tagName.toUpperCase()) ? target : null;
};

document.body.addEventListener('click', function(event) {
// 间接判断 A 链接是否被点击
var target = findTarget(event.target, 'a');
if (target) {
showMessage(event, target);
}
}, false);

由于所有 A 链接都是 body 元素的子节点,并且它们的事件都会冒泡,因此点击事件最终会被 body 上添加的事件处理程序所处理。代码重构后在以下方面提升了页面性能:

  • 由于 document 对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待 DOMContentLoadedload 事件),因此只要可点击的元素呈现在页面上,就可以立即具备适当的功能。
  • 由于只添加一个事件处理程序,因此所需的 DOM 引用更少,整个页面占用的内存空间也更少。

此外,事件会关联到当前以及以后添加的子元素上面,可以避免反复为新元素绑定事件处理程序,可谓一劳永逸。

jQuery API

理解了两种机制的区别后,看看如何使用 jQuery 进行最快的实现:

on()

jQuery 1.7+ 推出了 on() 方法,其目的有两个:

  1. 统一接口

  2. 提高性能

用法如下:

  • 事件绑定:on(events,[data],fn) ,用于替换 bind()
  • 事件委托:on(events,[selector],[data],fn) ,用于替换 live()delegate() 。这里的 [selector] 参数很关键,起到了一个过滤器的效果,只有被选中元素的 子元素 才会触发事件。

on() 方法重构后的代码如下:

1
2
3
4
$('body').on('click', 'a', function(event) {
event.preventDefault();
alert(event.currentTarget.href);
});

可见,代码重构后非常简洁,推荐使用。

参考

本文目的:

  • 理解并能按需使用各种事件绑定 API
  • 理解事件对象
  • 理解事件流

事件绑定

事件是用户或浏览器自身执行的某种动作,例如 onclickonload ,都是事件的名字。而响应某个事件的函数就叫做 事件处理程序(Event Handlers)。为事件绑定处理程序的方式有以下几种:

HTML

做法:在 HTML 元素中直接编写事件处理程序:

1
2
3
4
5
6
7
8
<!-- 输出“Clicked” —— 事件处理程序中,可以直接编写 JavaScript 代码 -->
<input type="button" value="Click Me" onclick="alert('Clicked')" />

<!-- 输出“click” —— 事件处理程序中,可以直接访问事件对象 event -->
<input type="button" value="Click Me" onclick="alert('event.type')" />

<!-- 输出“Click Me” —— 事件处理程序中,this 指向事件的目标元素 -->
<input type="button" value="Click Me" onclick="alert('this.value')" />

其中除了可以编写 JavaScript 代码,还可以调用外部脚本:

1
2
3
4
5
6
7
8
<!-- 事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码 -->
<input type="button" value="Click Me" onclick="showMessage()" />

<script type="text/javascript">
function showMessage() {
alert("Hello world!");
}
</script>

特点:上述 onclick 事件将自动产生一个事件处理程序(函数),例如:

1
2
3
function onclick(event) {
alert('Clicked')
}

优点:简单、粗暴,浏览器兼容性好。

缺点:

  • 存在时差问题。用户可能会在 HTML 元素一出现在页面上时,就触发相应事件,但当时的事件处理程序有可能还未具备执行条件(例如事件处理程序所在的外部脚本文件还未加载或解析完毕),此时会引发 undefined 错误。
  • HTML 与 JavaScript 代码紧密耦合。如果要重命名事件处理程序,就要改动两个地方,容易改漏、改错。

DOM Level 0

做法:首先获取目标 HTML 元素的引用,然后将一个事件处理程序赋值给其指定的事件属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<input type="button" id='btn' value="Click Me" />

<script type="text/javascript">
var btn = document.getElementById('btn');

// 绑定事件处理程序
btn.onclick = function() {
alert('Clicked');
alert(this.id); // 输出“myDiv” —— this 指向事件的目标元素
}

// 删除事件处理程序
btn.onclick = null;
</script>

特点:本质上,DOM 0级事件处理程序 等于 HTML 事件处理程序,例如:

1
2
3
4
5
6
7
8
9
<input type="button" id='btn' value="Click Me" onclick="alert('Clicked')" />

<script type="text/javascript">
setTimeout(function() {
var btn = document.getElementById('btn');
alert(typeof btn.onclick); // 通过 HTML 的事件属性,访问其 HTML 事件处理程序,并输出其类型“function”
btn.onclick = null; // 几秒后,将会删除该按钮的事件处理程序
}, 3000);
</script>

优点:

  • 传统、常用、浏览器兼容性好。
  • 解决了 HTML 事件处理程序的两个缺点。

缺点:一个事件只能绑定唯一一个事件处理程序。

DOM Level 2

做法:目前最主流的写法,可以支持事件冒泡或捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<input type="button" id='btn' value="Click Me" />

<script type="text/javascript">
var btn = document.getElementById('btn'),
showMessage = function() {
alert('Clicked');
alert(this.id); // 输出“myDiv” —— this 指向事件的目标元素
};

// 绑定事件处理程序。false 表示在“冒泡阶段”和“目标阶段”触发
btn.addEventListener('click', showMessage, false);

// 删除事件处理程序。注意,匿名函数无法移除
btn.removeEventListener('click', showMessage, false);
</script>

优点:一个事件可以绑定多个事件处理程序,以绑定的顺序执行。

缺点:浏览器兼容性差,IE8 及以下版本不支持。

API:element.addEventListener(event, function, useCapture) 。其中 useCapture 可选,布尔值,指定事件是否在捕获或冒泡阶段执行:

  • true 捕获阶段执行
  • false 冒泡阶段执行(默认值)

IE

IE 实现了与 DOM 2 级类似的两个方法,只支持事件冒泡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<input type="button" id='btn' value="Click Me" />

<script type="text/javascript">
var btn = document.getElementById('btn'),
showMessage = function() {
alert('Clicked');
alert(this === window); // 输出“true” —— 注意 this 指向 window
};

// 绑定事件处理程序。仅在“冒泡阶段”和“目标阶段”触发
btn.attachEvent('onclick', showMessage);

// 删除事件处理程序。注意,匿名函数无法移除
btn.detachEvent('onclick', showMessage);
</script>

优点:一个事件可以绑定多个事件处理程序,以绑定的顺序 逆序 执行。

缺点:浏览器兼容性差,仅支持 IE 及 Opera。

Cross-Browser

鉴于上述几种方式的各有优劣,为了以跨浏览器的方式处理事件,可以定义自己的 EventUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var EventUtil = {

addHandler: function(element, type, handler){
if (element.addEventListener){
element.addEventListener(type, handler, false);
} else if (element.attachEvent){
element.attachEvent(“on” + type, handler);
} else {
element[“on” + type] = handler;
}
},

removeHandler: function(element, type, handler){
if (element.removeEventListener){
element.removeEventListener(type, handler, false);
} else if (element.detachEvent){
element.detachEvent(“on” + type, handler);
} else {
element[“on” + type] = null;
}
}

};

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event ,这个对象中包含着所有与事件有关的信息。尽管触发的事件类型不同,可用属性和方法也会不同,但是所有事件都会包含下列常用成员:

DOM Level 2 Type IE Type Description
type String type String 被触发的事件类型
eventPhase Integer - - 调用事件处理程序的所处阶段:1 表示捕获阶段,2 表示“处于目标”,3 表示冒泡阶段
target Element srcElement Element 事件的目标元素
currentTarget Element - - 当前正在处理事件的元素。如果事件处于目标元素,则 this === currentTarget === target
stopPropagation() Function cancelBubble Boolean 取消事件的进一步捕获或冒泡
preventDefault() Function returnValue Boolean 取消事件的默认行为。该方法将通知 Web 浏览器不要执行与事件关联的默认动作(如果存在这样的动作)。例如,如果 type 属性是 “submit”,可以阻止提交表单。

事件流

最后总结下与事件处理程序息息相关的“事件流”。事件流是指从页面中接收事件的顺序。但有意思的是,历史上 IE 和 Netscape 开发团队居然提出了 完全相反 的事件流概念 —— IE 使用“事件冒泡(Event Bubbling)”、Netscape 使用“事件捕获(Event Capturing)”。下图演示了这两种事件流的区别:

事件流(Event Flow)

下表列出了四种事件绑定所使用的事件流模型:

事件冒泡 or 事件捕获?
HTML 取决于 IE or Netscape
DOM Level 0 取决于 IE or Netscape
DOM Level 2 事件冒泡 + 事件捕获
IE 事件冒泡

下面重点讲解 DOM Level 2 事件处理程序所规定的事件流,其共包含三个阶段(其运行效果如上图从 1 到 10):

  1. 事件捕获阶段,可用于事件截获
  2. 处于目标阶段
  3. 事件冒泡阶段,可用于事件委托(Event Delegation)

下面这段代码演示了 DOM Level 2 的整个事件流:

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
<!DOCTYPE html>
<html>
<body>
<input type="button" id='btn' value="Click Me" />

<script type="text/javascript">

// 仅在“事件捕获阶段”和“处于目标阶段”触发
document.body.addEventListener('click', function(event){
alert(event.eventPhase + ' body');
}, true);

// 仅在“事件冒泡阶段”和“处于目标阶段”触发
document.getElementById('btn').addEventListener('click', function(event){
alert(event.eventPhase + ' input');
}, false);

// 仅在“事件冒泡阶段”和“处于目标阶段”触发
document.addEventListener('click', function(event){
alert(event.eventPhase + ' document');
}, false);

</script>
</body>
</html>

点击 input 按钮,将依次输出:

1
2
3
1 body
2 input
3 document

可见,DOM Level 2 是同时支持事件冒泡 + 事件捕获的。

参考

  • 《JavaScript 高级程序设计》

一年前,为了优化这个博客的访问速度,我将 Pages 服务迁移 到了 GitCafe,没想到一年后 GitCafe 竟被 codeing.net 收购了,其服务将在五月底全面停止,真是令人叹息。

幸好 Coding Pages 支持免费绑定自定义域名,其配置也非常简单。在完成配置之后,只需要到 DNSPod 切换下 cname ,等待 DNS 解析生效即可。整个过程对网站用户透明。

最后,这里 列举了不少知名的 Pages 服务可供选择。不过对于国内用户来说,还是使用国内服务最快、最稳定。

本文演示如何动态加载脚本。即脚本在页面加载时不存在,但将来的某一时刻通过修改 DOM 动态添加脚本,从而实现按需加载脚本。

加载脚本文件

1
2
3
4
5
6
7
8
function loadScriptFile(url) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;

// 在执行到这行代码将 <script> 元素添加到页面之前,不会下载指定外部文件
document.body.appendChild(script);
}

内联脚本代码

1
2
3
4
5
6
7
8
function loadScriptString(code) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.text = code;

// 在执行到这行代码将 <script> 元素添加到页面之前,不会下载指定外部文件
document.body.appendChild(script);
}

以这种方式加载的代码会在全局作用域中执行,而且当脚本执行后将立即可用。实际上,这样执行代码与在全局作用域中把相同的字符串传递给 eval() 是一样的。

有了一套成熟的分支模型以及配套的权限控制之后,接下来我们以一个例子来演示如何实践这套流程。

分支模型实践

创建版本分支

首先,项目管理员(Master)从 master 分支中创建出版本分支 release-* 进行新版本的开发,* 为发布日期:

1
2
3
4
5
$ git checkout -b release-20190101

do something and commit...

$ git push origin release-20190101

版本分支 release-* 一般是锁起来的,不允许随便提交代码。

创建特性分支

然后,开发人员(Developer)从版本分支中创建出特性分支,并在其上进行特性开发:

1
2
3
4
5
$ git checkout -b feature-test

do something and commit...

$ git push origin feature-test

由于特性分支可能会跨版本开发,因此需要定期维护:主要的工作就是定期将 master 分支或版本分支合并进来,保持同步,代码够新。使用命令:rebase

Merge Request

开发完毕后,开发人员(Developer)需要整理特性分支——例如从中挑选出能够发版的提交,剔除掉不能发版的提交。如果想要筛选出将要被合并的提交有哪些,可以参考这里

整理完毕后,给项目管理员(Master)发起一个 MR,请求合并到版本分支。

标记新版本

当版本分支发布完毕,Master 打 Tag 标记该新版本,以便后续回顾:

1
2
$ git tag tag-20190101 -m "XX 项目 v1.0 版本"
$ git push origin tag-20190101

注意,在默认情况下,git push 并不会把标签(tag)推送到远端仓库上,只有通过显式命令才能分享标签到远端仓库。其命令格式如同推送分支,运行 git push origin [tagname] 即可。如果要一次推送所有本地新增的标签上去,可以使用 --tags 选项。

清理分支

最后是一些清理工作,Master 需要删除已完成开发的版本分支、特性分支,避免分支越来越多导致不好管理。例如:

1
2
$ git branch -d release-20190101
$ git push --delete origin release-20190101

最后,列出所有远程和本地分支确认下:

1
$ git branch -a

总结

代码提交指南

  • 请不要在更新中提交多余的白字符(whitespace)。Git 有种检查此类问题的方法,在提交之前,先运行 git diff --check ,会把可能的多余白字符修正列出来。
  • 请将每次提交限定于完成一次逻辑功能。并且可能的话,适当地分解为多次小更新,以便每次小型提交都更易于理解。
  • 最后需要谨记的是提交说明的撰写。可以理解为第一行的简要描述将用作邮件标题,其余部分作为邮件正文。

分支管理指南

  • 主分支 master 一般不提交代码,只合并代码。
  • 各特性分支要定期将 master 分支合并进来,避免后续处理合并请求时产生冲突,以减轻项目管理员的工作负担。
  • 发版之后,项目管理员要记得打 tag 。

技巧

跟踪分支

查看本地分支和远程分支的跟踪关系:

1
2
3
4
5
$ git branch -vv

* feature-modify-remit-recon f898c3c [origin/feature-modify-remit-recon: ahead 2, behind 6] chore:xxx
feature-recon-history cfbf905 [origin/feature-recon-history] Merge branch master into feature-recon-history
master e1f5e67 [origin/master] chore:xxx

设置本地分支 master 跟踪远程分支 origin/<branch>

1
$ git branch --set-upstream-to=origin/<branch> master

删除本地远程分支

1
2
3
-r
--remotes
List or delete (if used with -d) the remote-tracking branches.

删除本地远程分支

1
$ git branch -r -d origin/branch-name

参考

除了 Git 命令,权限控制也是 Git 中极为重要的组成部分,本文主要介绍 GitLab 系统提供的最常用的权限控制功能。

分配成员角色

首先来了解下,Git 中的五种角色:

角色 描述
Owner Git 系统管理员
Master Git 项目管理员
Developer Git 项目开发人员
Reporter Git 项目测试人员
Guest 访客

每一种角色所拥有的权限都不同,如下图:

Git 权限控制

我们需要做的是,为项目成员分配恰当的角色,以限制其权限。

Protected Branches

在对 Git 不熟悉的时候,时常苦恼于各个分支不受约束,任何开发人员都可以向任何分支直接推送任何提交,各种未经审查的代码、花样百出的 Bug 就这样流窜在预发布分支上。

其实我们可以通过 GitLab 的受保护分支(Protected Branches)功能解决该问题,该功能可用于:

Keep stable branches secure and force developers to use merge requests.

By default, protected branches are designed to:

  • prevent their creation, if not already created, from everybody except Masters
  • prevent pushes from everybody except Masters
  • prevent anyone from force pushing to the branch
  • prevent anyone from deleting the branch

接下来我们就使用这项功能,锁定我们的受保护分支——主分支 master 和预发布分支 release-*,以阻止 Developer 直接向这两类分支中推送代码:

Git 受保护分支

锁定后,Developer 推送代码将会报错:

1
2
3
4
5
6
7
8
9
10
11
$ git push origin master
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 283 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 1 (delta 0)
remote: GitLab: You are not allowed to access master!
remote: error: hook declined to update refs/heads/master
To git@website:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@website:project.git'

Merge Requests

锁定受保护分支后,要么 Master 需要时刻、主动关注各特性分支的进度,要么 Developer 需要线下、口头向 Master 汇报其特性分支的进度,这两种做法都非常不便于 Master 管理每个预发布分支的合并,尤其在团队大、分支多的情况。

我们可以通过 GitLab 的发起合并请求(Merge Request)功能解决该问题,这样既可以让 Developer 更自如的掌控自己分支进度,在必要的时候才主动发起合并请求;又可以减轻 Master 的合并工作量和沟通成本,可谓一举两得。

新建 MR

第一步,按表单要求填写合并请求。注意,对于 Developer 而言:

  • Source branch 是你的特性分支 feature-*
  • Tagget branch 只可能是预发布分支 release-*
  • TitleDescription 要填写恰当的分支描述;
  • Assignee 是该项目的 Master。

新建合并请求

审查 MR

第二步,Master 收到合并请求后,进行代码审查。逐一查看 CommitsChanges 一栏提交的内容即可,对于需要改进的代码,可以直接在该行添加注释,非常方便。

如果对整个请求还有疑问的地方,还可以通过 Discussion 功能进行线上讨论。

处理 MR

第三步,针对审查结果进行相应处理:

关闭

对于完全不合格的垃圾代码、或者废弃的特性分支的合并请求,Master 点击右上角的 Close 按钮即可。合并请求将被关闭,相当于扔进回收站。

改进

对于分支内需改进的代码,Developer 直接修正并推送即可,合并请求将会自动包含最新的推送提交。

接受

Master 审查无误后,可以接受该次合并请求。点击 Accept Merge Request 按钮将自动合并分支,勾选 Remove source-branch 将同时删除该特性分支。

整个自动合并过程如果以命令形式手工执行的话,步骤如下:

1
2
3
4
5
6
7
8
#Step 1. Update the repo and checkout the branch we are going to merge 
git fetch origin
git checkout -b test origin/feature-test

#Step 2. Merge the branch and push the changes to GitLab
git checkout release-2016.4.7
git merge --no-ff feature-test
git push origin release-2016.4.7

非快进式合并完成后,祖先图谱(graph)的展现结果如下:

1
2
3
4
5
*   be512fa (HEAD, origin/release-2016.4.7, release-2016.4.7)  Merge branch 'test' into 'release-2016.4.7'
|\
| * 1f52adf 测试
|/
* a4febbb (tag: 1.0.0, origin/master) 格式化货币保留两位小数

最后需要注意的是,只有 Assignee 才能够接受合并请求,其它人只会被通知:

You don’t have permission to merge this MR

总结

GitLab 提供的上述功能非常实用,为项目的源码管理提供了有力的支持。

项目总归要协作开发,在此总结我在团队中推广使用的分支模型。

A successful Git branching model

分支模型

主分支(Main branches)

企业的项目开发不像开源的项目开发,通常只会有一个远程仓库。这种情况下,通常会有两个常驻分支:

Branch Name Is locked? Description
master YES 主干分支,仅用于发布新版本,平时不能在上面干活,只做代码合并、以及打标记(git tag)。
理论上,每当对 master 分支有一个合并提交操作,我们就可以使用 Git 钩子脚本来自动构建并且发布软件到生产服务器。
dev NO 开发分支,平时干活的地方。每当发版时,需要被合并到 master

对于简单的项目而言,这样的分支模型已经够用了。

辅助性分支(Supporting branches)

除了常驻分支,通常大的特性开发或生产缺陷修复还建议创建相应的临时分支。因为:

  1. 在分支上开发可以让你随意尝试,进退自如,比如碰上无法正常工作的特性或补丁,可以先搁在那边,直到有时间仔细核查修复为止。
  2. 团队中如果有代码审查流程,独立的分支还可以留给审查者抽空审查的时间和改进代码的余地,并将是否合并、是否发布的权利留给审查者,为代码质量设一道门槛。

每一类分支都有一个特定目的,如何命名每一类分支?建议用相关的主题关键字进行命名,并且建议将分支名称分置于不同命名空间(前缀)下,例如:

Branch Name May branch off from Must merge back into Is locked? Description
feature-* dev dev NO 特性分支,为了开发某种特定功能而建。开发完成并测试通过后,需发送 Merge Request 到 release-* 进行代码审查及合版。
release-* dev dev
master
YES 预发布分支,为了新版本的发布做准备,一般命名为 release-<版本号>。这是一个稳定分支,只接受审核通过的 Merge Request。
hotfix-* master dev
master
NO 补丁分支,为了修复生产缺陷而建,一般命名为 hotfix-<issue 编号>

与主分支不同,这些辅助性分支总是有一个有限的生命期,因为他们在被合并到主分支之后,就会被移除掉。

参考