少湖说 | 科技自媒体

互联网,科技,数码,鸿蒙

概述

公司的GitLab中,有一个存放所有技术文档的Wiki仓库,按照目录分门别类,包括API文档,编码规范,技术专题文档等,通过与Gollum进行持续部署.

然而在GitLab中,每个项目都有自己的Wiki库, 所以在将项目文档合并更新到总Wiki仓库时,同步更新比较麻烦,通过充分使用GitLab的持续集成功能, 将项目Wiki与Wiki仓库集成, 从而实现了Wiki的自动部署,

同步时,自动同步的提交信息和提交人信息

步骤

配置SSH

  • GitLab中在使用SSH的时候, 会生成公钥和私钥对

  • 将公钥添加到gitlab上, 以便于该用于可以拉取代码

  • CI/CD Piplines中设置 Secret Variables, 这里名为 SSH_PRIVATE_KEY

SSH_PRIVATE_KEY 值为私钥.

编写 .gitlab-ci.yml 文件, 注入私钥, 通过ssh执行远程命令

创建一个分支, 如docs, 在该分支中添加 gitlab-ci.yml文件, 实现wiki自动提交, 内容形如以下内容:

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

image: zacksleo/docker-composer:develop

before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" > deploy.key
- chmod 0600 deploy.key
- ssh-add deploy.key
- rm -f deploy.key
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'

build-docs:
stage: deploy
variables:
GIT_STRATEGY: none
dependencies: []
script:
# 定义变量: 项目Wiki的Git地址,项目(目录)别名
- export WIKI_REPO=git@domain.com:project.wiki.git && export PROJECT_NAME=$CI_PROJECT_NAME
# 创建临时目录, 用于存放和合并git文档
- mkdir ~/tmp && cd ~/tmp
# 克隆项目wiki
- git --git-dir=~/tmp/$PROJECT_NAME.wiki.git clone --depth=1 $WIKI_REPO $PROJECT_NAME
# 删除.git 只保留纯文档, 获取最近的提交日志,用户邮箱和名称
- cd $PROJECT_NAME && export GIT_LOG=`git log -1 --pretty=%B` && export GIT_EMAIL=`git log -1 --pretty=%ae` && export GIT_USERNAME=`git log -1 --pretty=%an` && rm -rf .git && cd ..
# 注册Git账号
- git config --global user.email $GIT_EMAIL && git config --global user.name $GIT_USERNAME
# 克隆联络Wiki
- git clone git@domain.com:orgs/wiki.git
# 删除旧wiki, 增加新wiki
- rm -rf wiki/api/$PROJECT_NAME && mv -f $PROJECT_NAME wiki/api
# 增加提交日志并提交
- cd wiki && git add . && git commit -m "$PROJECT_NAME:$GIT_LOG" && git push origin master
# 删除临时目录
- rm -rf ~/tmp
only:
- docs

其中, 将WIKI_REPO后面的git@domain.com:project.wiki.git替换为项目wiki的git地址,
$CI_PROJECT_NAME替换为项目英文别名(如不改则使用当前GitLab的项目名), 用于在文档中心的api下面创建相关目录。
其他地方不需要修改。

注意: 项目wiki的git地址与项目的git地址不相同, 请在Wiki右侧中的Clone repository 找到

创建 Triggers Token

打开项目的 CI/CD Pipelines 选项, 找到 Triggers, 点击添加一个Token, 并从下方的 Use webhook 段落找到触发URL, 如

https://domain.com/api/v4/projects/74/ref/REF_NAME/trigger/pipeline?token=TOKEN

将TOKEN替换为上述Triggers中获取的Token, 将 REF_NAME 替换分分支名称 docs, 得到最终URL

配置 Webhooks

打开项目的 integrations 选项, 在URL中, 填写上一步中拿到的URL

相关文档

  • [[GitLab-CI使用Docker进行持续部署]]
  • [[使用Git和Gollum搭建Wiki系统]]

背景

在GitLab-CI中,使用artifacts可以确保所需要传递的文件可靠性,但由于生成的artifacts存在的GitLab上,每次需要远程下载,因此速度相对较慢。
所以,在一些对依赖的准确性要求不高的地方,可以考虑使用cache

cache 简介

cache 顾名思义为缓存,不同的任务之前,缓存可以进行共享。根据配置中的声明,在需要缓存时,GitLab-CI会自动下载缓存,以供当前任务使用。

cache一旦命中,意味着这部分文件不需要重新生成(编译,下载或构建),这样一来,便省去了不少功夫,从而加速了构建过程。

使用

生成cache

1
2
3
4
5
6
7
8
9
10
11
  
build-package:
stage: prepare
script:
- composer install --prefer-dist --optimize-autoloader -n --no-interaction -v --no-suggest
- composer dump-autoload --optimize
cache:
key: "$CI_COMMIT_REF_NAME"
paths:
- vendor

如上,在build-package任务中,声明了cache,其目录为vendor, 当script执行完之后,vendor目录会生成,该任务最后,cache会自动生成(push)

使用cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

phpcs:
stage: testing
cache:
key: "$CI_COMMIT_REF_NAME"
policy: pull
paths:
- vendor
script:
- if [ ! -d "vendor" ]; then
- composer install --prefer-dist --optimize-autoloader -n --no-interaction -v --no-suggest && composer dump-autoload --optimize
- fi
- php vendor/bin/phpcs --config-set ignore_warnings_on_exit 1
- php vendor/bin/phpcs --standard=PSR2 -w --colors ./
except:
- docs

如上,该过程定义了所要使用的cache, 由于cache并不保证每次都命中(即拿到的cache可能为空),周时在script处进行判断,如果cache为空时,重新生成所需文件

对比

以doctor-online为例子,在未使用cache之前 ,使用的是artifacts, 每次构建时间在7分钟左右,

使用cache后,构建时间缩短到了1分钟

参考文档

引子

最近接触了一个抽奖的项目,由于用户量比较大,而且第三方提供的认证接口并发量有限,为了保证服务的高可用性,所以对高并限制发有一定的要求。经过一系列研究和讨论,做出了以下一些优化方案。

需求分析

  • 根据用户量和日活情况,估算出并发值在100左右,所以该项目的并发量就当在100上以,初期目标定为600-800

  • 特定页面的并发量不超过300,为了保证不对第三方服务造成访问压力,特将并发控制在150以内

  • 由于奖品数量有限,故得奖时,需要进行并发写入控制,防止奖品超发

实施步骤

前端方面

减少客户端访问次数。

  • 使用CDN对网站的静态资源进行优化,可以答复静态请求次数。

  • 抽奖结果一次性生成。

每个用户有有一次抽奖机会,但只能有一次中奖,其他两次随机弹出推荐产品。所以有一次抽奖中,只需要访问一次服务器的抽奖接口。

  • 使用本地缓存。

将抽奖次数,是否中奖等信息记录在本地,避免超次抽奖和多余的服务器请求。例如,一旦该用户中过将,就不需要再访问服务器抽奖接口。

服务端优化

使用缓存

  • 使用服务端缓存。

将页面和相关数据查询进行缓存,减少数据库访问次数。

部分业务逻辑异步处理

对于不需要实时处理的业务逻辑,压入队列,实现异步处理。例如优惠券的发放。

Nginx 优化

  • 提高 Nginx 处理性能

使用 worker_processesworker_cpu_affinityworker_rlimit_nofileworker_connectionsopen_file_cache等命令,提高nginx的处理性能。

  • Nginx 并发控制。

先看实例,如下:

1
2
3
4
5
6
limit_req_zone $binary_remote_addr zone=req_ip:10m rate=40r/s; // #每个IP平均处理的请求频率为每秒40次
limit_conn_zone $binary_remote_addr zone=conn_ip:10m;
limit_conn_zone $server_name zone=conn_server:10m;
limit_conn conn_ip 5; // #限制某个IP来源的连接并发数,此处为5个
limit_conn conn_server 600; //#限制某个虚拟服务器的总连接数,此处为600个
limit_req zone=req_ip burst=5; //小为5的缓冲区, 当有大量请求过来时,超过了访问频次限制的请求可以先放到这个缓冲区内

在这里,限制了每个IP的请求频率,限制了同一IP的并发连接数,限制了服务器的总连接数

  • 使用 Docker 实现负载均衡

配置 nginx, 使用负载均衡

1
2
3
4
5
6
7
upstream icontact_pool {
server web:9000 weight=5 max_fails=3 fail_timeout=10s;
server web2:9000 weight=5 max_fails=3 fail_timeout=10s;
server web3:9000 weight=5 max_fails=3 fail_timeout=10s;
...
}

如上,通过 Docker 启动多个处理请求的服务容器,在 nginx 中配置每个服务的地址,权重等信息,扩大请求的处理能力

  • 其他服务器环境优化

例如,增加服务器配置(CPU,内存,带宽),如果是PHP, 开启 opcache, 并使用较新版本(php7+), 各种依赖尽量使用最新版本。

1
2
3
4
zend_extension=opcache.so
opcache.enable=1
opcache.enable_cli=1

参考资料

问题

按照以往的做法,你会使用hexo来生成博客的静态文件,并通过git提交到github上,以此来写作和发布文章。

然而,经过一段时间,你会发现,这种写作方式存在几个问题:

  • 文章源和相关编译、部署程序都存储在本地,如果在其他设备上写作,没有同步措施。这样书写多有不便。
  • 文章源没有备份,也没有充分使用Git来管理。
  • 每次书写完文章,需要手动编译和部署。

鉴于以上几个问题,结合Travis-CI的使用经验,我决定对往常的写作方式进行改进:

步骤

新建分支

除了原来博客中的master分支, 再新建两个分支:docs分支用于存放文章源:hexo分支用于存放一些配置文件,我们将在该分支上进行持续部署。

docs 分支

该分支下存放文章源,各篇均以markdow格式书写。除此之外,有 .travis.yml 配置文件和一个触发部署的脚本 deploy.sh

每当有文章提交上来,就会自动触发一次构建,其内容是通过调用Travis-CI的API,自动执行一次 hexo 分支上的自动构建。

hexo 分支

该分支存放一些hexo的配置文件,当构建被触发时,会自动拉取docs中的文章内容,安装一个material皮肤,并进行hexo的自动网站编译。
最后编译完的静态Html,强制推送到master分支,从而博客自动更新。

简介

Code Climate 是一个代码测试工具, 它可以帮助你进行代码冗余检测、质量评估,同时支持多种语言,如PHP, Ruby, JavaScript, CSS, Golang, Python 等。

使用

配置GitLab Runner

1
2
3
4
5
6
7
8
9
10
11
[[runners]]
....
executor = "docker"
[runners.docker]
tls_verify = false
image = "docker:latest"
privileged = true
disable_cache = false
cache_dir = "cache"
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock", "/tmp/builds:/builds"]
shm_size = 0

注意, 需要增加一个 /tmp/builds:/builds , 这里用于映射放代码。否则根据官方文档中的描述,无法正常实现

为了能使用宿主机的docker 缓存, 加快构建速度, 这里使用 sock 绑定的方式使用docker, 不使用 docker in docker

配置 .gitlab-ci.yml 文件

1
2
3
4
5
6
7
codeclimate:
image: docker:latest
script:
- docker pull codeclimate/codeclimate
- VOLUME_PATH=/tmp/builds"$(echo $PWD | sed 's|^/[^/]*||')"
- docker run -v /tmp/cc:/tmp/cc -v $VOLUME_PATH:/code -v /var/run/docker.sock:/var/run/docker.sock codeclimate/codeclimate validate-config
- docker run --env CODECLIMATE_CODE="$VOLUME_PATH" -v /tmp/cc:/tmp/cc -v $VOLUME_PATH:/code -v /var/run/docker.sock:/var/run/docker.sock codeclimate/codeclimate analyze -f text

配置 .codeclimate.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
engines:
duplication:
enabled: true
config:
languages:
- javascript
- php
csslint:
enabled: true
eslint:
enabled: true
fixme:
enabled: true
phpmd:
enabled: true
ratings:
paths:
- "**.js"
- "**.css"
- "**.php"
exclude_paths:
- tests/
- vendor/

相关配置请参考官方文档

参考资料

简介

rsync命令是一个远程数据同步工具

主要参数

  • -r 递归目录

  • -t 保留修改时间

  • -v 详细日志

  • -h 输出数字以人类可读的格式

  • -z 在传输过程中压缩文件数据

  • -e 指定要使用的远程shell, 注意该过程需要注入SSH

配置参考

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

before_script:
- 'which ssh-agent || ( apk update && apk add openssh-client)'
- apk add rsync
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" > ~/deploy.key
- chmod 0600 ~/deploy.key
- ssh-add ~/deploy.key
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- export APP_ENV=testing


testing-server:
stage: deploy
image: alpine
variables:
DEPLOY_SERVER: "server-host"
script:
- cd deploy
- rsync -rtvhze ssh . root@$DEPLOY_SERVER:/data/$CI_PROJECT_NAME --stats

注意

远程服务器需要安装rsync, 否则会出现 bash: rsync: command not found 错误

参考资料

简介

LFTP是一款FTP客户端软件, 支持 FTP 、 FTPS 、 HTTP 、 HTTPS 、 SFTP 、 FXP 等多种文件传输协议。

本文介绍如何使用 LFTP 将文件同步到远程FTP服务器上, 从而实现自动部署

mirror 命令及主要参数

  • -R 反向传输, 因为是上传(put)到远程服务器, 所以使用该参数 (默认是从远程服务器下载)

  • -L 下载符号链接作为文件, 主要处理文件软链接的问题

  • -v 详细输出日志

  • -n 只传输新文件 (相同的旧文件不会传输, 大大提升了传输效率)

  • –transfer-all 传输所有文件, 不论新旧

  • –parallel 同时传输的文件数

  • –file 本地文件

  • –target-directory 目标目录

配置参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
deploy:
stage: deploy
dependencies:
- installing-dependencies
script:
- apk add lftp
# 只上传新文件
- lftp -c "set ftp:ssl-allow no; open -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST; cd /wwwroot; mirror -RLnv ./ /wwwroot --ignore-time --parallel=50 --exclude-glob .git* --exclude .git/"
# 指定目录覆盖上传 (强制更新)
- lftp -c "set ftp:ssl-allow no; open -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST;mirror -RLv ./vendor/composer /wwwroot/vendor/composer --ignore-time --transfer-all --parallel=50 --exclude-glob .git* --exclude .git/"
# 单独上传autoload文件(强制更新)
- lftp -c "set ftp:ssl-allow no; open -u $FTP_USERNAME,$FTP_PASSWORD $FTP_HOST;mirror -Rv --file=vendor/autoload.php --target-directory=/wwwroot/vendor/ --transfer-all"
only:
- master

参考资料

简介

PHPMD是与PMD类似的静态代码分析工具, 通过分析可以找出潜在的Bug或设计问题, 从而进一步提高代码质量

使用

  • 首先通过composer安装phpmd库
1
2
composer require phpmd/phpmd --dev --prefer-dist 

  • 运行phpmd命令
1
2
vendor/bin/phpmd ./ text phpmd.xml --suffixes php

phpmd.xml配置如下:

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
<?xml version="1.0"?>
<ruleset name="PHPMD rule set for Yii 2" xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>Custom PHPMD settings for naming, cleancode and controversial rulesets</description>

<rule ref="rulesets/naming.xml/ConstructorWithNameAsEnclosingClass" />
<rule ref="rulesets/naming.xml/ConstantNamingConventions" />
<!-- Long variable names can help with better understanding so we increase the limit a bit -->
<rule ref="rulesets/naming.xml/LongVariable">
<properties>
<property name="maximum" value="25" />
</properties>
</rule>
<!-- method names like up(), gc(), ... are okay. -->
<rule ref="rulesets/naming.xml/ShortMethodName">
<properties>
<property name="minimum" value="2" />
</properties>
</rule>

<rule ref="rulesets/cleancode.xml">
<!-- else is not always bad. Disabling this as there is no way to differentiate between early return and normal else cases. -->
<exclude name="ElseExpression" />
<!-- Static access on Yii::$app is normal in Yii -->
<exclude name="StaticAccess" />
</rule>

<rule ref="rulesets/controversial.xml/Superglobals" />
<rule ref="rulesets/controversial.xml/CamelCaseClassName" />
<rule ref="rulesets/controversial.xml/CamelCaseMethodName" />
<rule ref="rulesets/controversial.xml/CamelCaseParameterName" />
<rule ref="rulesets/controversial.xml/CamelCaseVariableName" />
<!-- allow private properties to start with $_ -->
<rule ref="rulesets/controversial.xml/CamelCasePropertyName">
<properties>
<property name="allow-underscore" value="true" />
</properties>
</rule>
</ruleset>

GitLab-CI 集成

在.gitlab-ci.yml中添加一个任务, 用于执行静态分析, 一个典型的例子:

1
2
3
4
5
6
phpmd:
stage: testing
dependencies:
- installing-dependencies
script:
- vendor/bin/phpmd api,backend,common,frontend,console text phpmd.xml --exclude console/migrations/ --suffixes php

参考资料

Github

简介

Fixtures 是测试中非常重要的一部分。主要目的是建立一个固定/已知的环境状态以确保 测试可重复并且按照预期方式运行。

简答说就是Fixtures提供一种预填充数据的方式,即在测试前需要准备好哪些数据,以便测试可以正常展开,不受其他测试的影响。

一个 Fixture 可能依赖于其他的 Fixtures ,所定义的依赖会自动加载。

该方法相比于dump.sql的填充方法更加灵活, 且不会出去填充的冲突问题.

配置

定义一个Fixtures

通过继成yii\test\ActiveFixture, 并声明 modelClass 来定义一个Fixtures , depends为要依赖的Fixtures, 可选。

Fixtures通常放置于tests目录中的fixtures目录下.

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace tests\fixtures;

use yii\test\ActiveFixture;

class UserFixture extends ActiveFixture
{
public $modelClass = 'app\models\User';
public $depends = ['app\tests\fixtures\UserFixture'];
}

设置填充数据

@tests/fixtures/data目录中,每个Fixtures添加一个数据文档

在位置 @tests/fixtures/data/user.php 中, 设置以下数据, 为要被插入用户表中的数据文件, user1user2为别名, 方便调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
return [
'user1' => [
'username' => 'lmayert',
'email' => 'strosin.vernice@jerde.com',
'auth_key' => 'K3nF70it7tzNsHddEiq0BZ0i-OU8S3xV',
'password' => '$2y$13$WSyE5hHsG1rWN2jV8LRHzubilrCLI5Ev/iK0r3jRuwQEs2ldRu.a2',
],
'user2' => [
'username' => 'napoleon69',
'email' => 'aileen.barton@heaneyschumm.com',
'auth_key' => 'dZlXsVnIDgIzFgX4EduAqkEPuphhOh9q',
'password' => '$2y$13$kkgpvJ8lnjKo8RuoR30ay.RjDf15bMcHIF7Vz1zz/6viYG5xJExU6',
],
];

使用

在测试用例中, 通过定义_fixtures方法, 声明需要使用的Fixtures及填充数据文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

/**
* @var \UnitTester
*/
protected $tester;

public function _fixtures()
{
return [
'users' => [
'class' => UserFixture::className(),
'dataFile' => '@tests/fixtures/data/user.php'
],
'profiles' => [
'class' => UserProfileFixture::className(),
'dataFile' => '@tests/fixtures/data/user_profile.php'
],
];
}

通过如下方法, 可以获取插入的记录, 返回值为该Fixture类中对应的modelClass的一个实例

1
2
$user = $this->tester->grabFixture('users', 'default');

参考资料

Fixtures
Fixtures

概述

如果多个项目中存在使用相同类库、模块的情况,此时可以考虑将类库或者模块单独抽取出来,形成独立类库,通过composer
来进行依赖管理,这样可以更方便维护,大大提升开发效率。

优势

  • 可以对特定模块进行统一维护和升级
  • 特定的类库可由专人进行维护,保证稳定性和可靠性
  • 避免了重复开发的情况

步骤

本地开发

为了方便调试,可先在本地现有项目中开发类库,等到开发完成后,再将相关代码单独抽取出来。

  • 首先在项目中创建一个存放类库的目录,如packages/zacksleo/my-libs,

其中packages是类库总目录, zacksleo是用户名,相当于命名空间的第一级,my-libs是类库存放目录。

  • 在目录中创建composer.json 文件,并添加形如以下的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "zacksleo/my-libs",
"description": "my libs",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "zacksleo",
"email": "zacksleo@gmail.com"
}
],
"minimum-stability": "stable",
"autoload": {
"psr-4": {
"zacksleo\\my\\libs\\": "src"
}
}
}

其中,name是类库名称,descrption是详细说明,type是类别,license是使用的协议,authers是作者信息,

minimum-stability 用来声明最小依赖,通常有devstable可选,autoload中的psr-4声明了

命名空间和对应的目录,注意命名空间就当使用双反斜杠,目录使用相对路径,此外声明了目录为`src``目录

  • src目录中添加相关代码,其中的类使用命名空间zacksleo\\my\\libs

  • 在项目的composer.json中,通过path方式引入本地类库,如可在repositories中添加如下信息:

1
2
3
4
5
6
7
"repositories": {
"my-libs": {
"type": "path",
"url": "packages/zacksleo/my-libs"
}
}

其中my-libs是别名,可任意填写,type设置成path, url为类库所在的相对路径(与composer.json文件相对)

  • 通过composer require命令或者在composer.json中的require部分添加声音,来实现依赖加载,如

composer require zacksleo/my-libs

在Github上创建库并上传代码

当在本地开发完成后,可将类库独立抽取出来(此处的my-libs目录下的内容),并提交到Github上新建的仓库中

配置packagist并发布

  1. 先在packagist.org中注册好账号,以便发布包。
  2. 在Github的仓库中,点击settings,找到 Intergrations & services, 点击Add servies, 选择Packagist,

填写在packagist.org注册的用户名和Token(在Profile中找到Your API Token)

点击确定添加,这样,每次Github的变动,都会自动更新到packagist上,免去了手动更新的麻烦

本地依赖改成线上版本, 并清除开发代码

类库一经发布到packagist上后,就可将本地项目composer.json添加的repositories移除,重新运行composer install

来安装packagist上的版本,同时packages 目录亦可删除。

版本问题说明

composer使用语义化的版本进行依赖管理,因此类库在更新和发布时,所标记的版本号,也就当遵循语义化的版本规范

基主要有以下几个内容:

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。
  4. 先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

参考资料

0%