CI CD

代码 -> 构建 -> 集成 -> 测试 -> 发布 -> 部署

|———持续集成——| CI

|———–持续发布————| CD

|—————持续部署—————| CD

CI CD

  • 持续集成(CI)

  • 持续交付和持续部署(CD)

  • 现代软件开发的需求加上部署到不同基础设施的复杂性使得创建应用程序成为一个繁琐的过程。当应用程序出现规模性增长,开发团队人员变得更分散时,快速且不断地生产和发布软件的流程将会变得更加困难。

  • 为了解决这些问题,开发团队开始探索新的策略来使他们的构建、测试和发布流程自动化,以帮助其更快地部署新的生产。这就是持续交付和持续集成发展的由来。

流程

自动构建

在软件开发过程中,构建流程会将开发人员生成的代码转换为可执行的可用软件。

对于Go或者C语言等编译语言,此阶段需要通过编译器运行源代码以生成独立的二进制文件。

对于JavaScript或PHP等解释性语言,没有编译的步骤,但是代码依旧需要在特定的时间内冻结、绑定依赖项、打包以便于分发。这些过程通常称为“构建”或“发布”的工件。

虽然开发人员可以手动构建,但这样操作有诸多不利。首先,从主动开发到创建构建的转变中引入了上下文转换,使得开发人员不得不停止生产效率更高的工作来专注于构建过程。其次,每个开发人员都在制作自己的工件,这可能导致构建过程不一致。

为了解决这些顾虑,许多开发团队配置了自动构建流水线。这些系统监视源代码存储库,并在检测到更改时自动启动预配置的构建过程。这一配置无需牵涉过多的人力在其中并且确保了每个构建过程一致。

进程,线程,协程

程序执行

  • 写在硬盘上的静态程序文件
  • cpu从磁盘上找到程序文件
  • .exe 文件中包含操作cpu的指令
  • 把指令放到内存中
  • 在内存中的指令就可以叫做cpu的进程

进程

对于cpu来讲,有一个时间线的概念,时间线上的每一个点对应着一个操作指令

操作系统把时间线分割为不同的时间片,时间片是执行程序的小单位

按照 1,2,1,2的顺序,来回切换任务的分配,可以让cpu调度进程,看起来在同时执行多个操作

进程比较重,每个任务分配一个进程,频繁切换的时候损耗比较大,所以有了线程的概念

进程之间内存相互独立,进程之间相互通信需要内核转发(系统调用)。也就是进程之间的通信需要经过操作系统,损耗比较大

进程主要占据的内存代码(进程的实现代码),数据(需要处理的数据),文件(和硬盘文件关联的文件句柄,需要通过系统内核,来完成文件的操作)

执行的时候需要写寄存器,在进程切换的时候需要保存寄存器中信息的状态,把寄存器中的数据写到内存中。

栈 函数调用栈

线程

线程比较轻,线程依附与进程,是多个线程对应一个进程的关系,

线程共享进程的内存(代码,数据,文件),但是有自己独立的寄存器和栈,所以线程比较轻

线程共享数据存在的问题,可能读写同时操作,需要线程锁

IO 密集型

输入输出的速度远小于CPU的速度。

多任务 -> 多线程 -> 时间驱动 -> 协程

CPU密集型

多进程 -> 多线程

网络配置问题

可以使用type +命令来查看命令所在目录

查看网络配置基本信息

centenOS 6 之前使用 ifconfg 命令, 之后使用ip 命令, 也可以安装 ifconfig命令包

重启网卡

如果可以直接操作服务其 可以使用 ifup ,ifdown

如果提示系统没有命令可以使用 apt install ifupdown安装

如果是远程不能使用这个两个命令重启网卡,使用ifdown后会断开网络连接

可以在确保网络配置修改成功之后,使用 systemctl restar network

如果命令不可用可以使用 service network-manager restart

如果是 Kali Linux(Debian),则需要service networking restart

如果是Centos 8 需要nmcli c reload

查找占用端口

可以使用 ssnetstat 命令 后面加 -anp

如果没有 netstat 命令,可以通过 apt install net-tools 安装

最后使用 kill 命令杀掉端口

工程化常用命令

ps

查看进程

1
ps aux

kill pkill

1
kill -9 pid

pkill 后面可以直接写进程的名字

如果是一个服务使用 systemctl stop

w who

看谁正在连接系统

lsof 查看端口占用

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
#列出所有打开的文件:
lsof
备注: 如果不加任何参数,就会打开所有被打开的文件,建议加上一下参数来具体定位

# 查看谁正在使用某个文件
lsof /filepath/file

#递归查看某个目录的文件信息
lsof +D /filepath/filepath2/
备注: 使用了+D,对应目录下的所有子目录和文件都会被列出

# 比使用+D选项,遍历查看某个目录的所有文件信息 的方法
lsof | grep ‘/filepath/filepath2/’

# 列出某个用户打开的文件信息
lsof -u username
备注: -u 选项,u其实是user的缩写

# 列出某个程序所打开的文件信息
lsof -c mysql
备注: -c 选项将会列出所有以mysql开头的程序的文件,其实你也可以写成lsof | grep mysql,但是第一种方法明显比第二种方法要少打几个字符了

# 列出多个程序多打开的文件信息
lsof -c mysql -c apache

# 列出某个用户以及某个程序所打开的文件信息
lsof -u test -c mysql

# 列出除了某个用户外的被打开的文件信息
lsof -u ^root
备注:^这个符号在用户名之前,将会把是root用户打开的进程不让显示

# 通过某个进程号显示该进行打开的文件
lsof -p 1

# 列出多个进程号对应的文件信息
lsof -p 123,456,789

# 列出除了某个进程号,其他进程号所打开的文件信息
lsof -p ^1

# 列出所有的网络连接
lsof -i

# 列出所有tcp 网络连接信息
lsof -i tcp

# 列出所有udp网络连接信息
lsof -i udp

# 列出谁在使用某个端口
lsof -i :3306

# 列出谁在使用某个特定的udp端口
lsof -i udp:55

# 特定的tcp端口
lsof -i tcp:80

# 列出某个用户的所有活跃的网络端口
lsof -a -u test -i

# 列出所有网络文件系统
lsof -N

#域名socket文件
lsof -u

#某个用户组所打开的文件信息
lsof -g 5555

# 根据文件描述列出对应的文件信息
lsof -d description(like 2)

# 根据文件描述范围列出文件信息
lsof -d 2-3

免密登陆

  • 生成密钥对

    -t 指定密钥类型,默认是 rsa ,可以省略。
    -C 设置注释文字,比如邮箱。
    -f 指定密钥文件存储文件名。

    如果想免密登录,密钥不要填写密码,否则必须验证密钥密码才能登录

    1
    2
    3
    4
    ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

    # 或者创建使用 ed25519 加密算法的密钥
    ssh-keygen -t ed25519 -C "your_email@example.com"
  • 上传共钥到服务器对应账号的 home 目录下.ssh 文件夹下面,公钥的权限为 600

    linux 执行以下命令

    1
    ssh-copy-id -i mykey_rsa.pub you_user_name@xxx.xxx.xxx.xxx

    window 需要手动在服务器上创建文件,

    1
    2
    3
    4
    5
    6
    7
    touch  ~/.ssh/authorized_keys

    # 或直接写入文件
    echo "your_public_key_here" >> ~/.ssh/authorized_keys

    # 修改权限
    chmod 600 authorized_keys
  • 指定私钥登陆,私钥的权限为 600

    1
    ssh -i 私钥 user@xxx.xxx.xxx.xxx
  • 通过配置文件免密登陆,在本地服务器把私钥复制到 home 下的.ssh 文件夹下面

    创建名称为 config 的文件,配置单一服务器免密登陆

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Host tencent
    HostName 124.222.139.87
    User ubuntu
    IdentityFile ./.ssh/id_ed25519
    IdentitiesOnly yes


    # Host 别名
    #  HostName IP
    #  Port 端口
    #  User 用户名
    #  IdentitiesOnly yes
    # IdentityFile ~/.ssh/user_rsa (私钥路径)
    # Protocal 2 (协议版本号)
    # Compression yes
    # ServerAliveInterval 60 (防止被踢配置,长时间没有操作会被踢掉,每隔60秒发一个信号)
    # ServerAliveCountMax 20 (最大连接数)
    # LogLevel INFO

    通配符配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Host app-produce
    HostName 192.168.1.10
    Port 22
    User appuser
    IdentityFile ~/.ssh/id_ed25519

    Host \*\_produce
    User commonuser
    IdentityFile ~/.ssh/id_ed25519
    Port 22

wget

wget 是一个从网络上自动下载文件的自由工具,支持通过 HTTP、HTTPS、FTP 三个最常见的 TCP/IP 协议 下载,并可以使用 HTTP 代理。”wget” 这个名称来源于 “World Wide Web” 与 “get” 的结合。

wget 可以在用户退出系统的之后在后台执行。这意味这你可以登录系统,启动一个 wget 下载任务,然后退出系统,wget 将在后台执行直到任务完成,相对于其它大部分浏览器在下载大量数据时需要用户一直的参与.

1
wget (选项) (参数)

其中选项如下:

-a<日志文件>:在指定的日志文件中记录资料的执行过程;

-A<后缀名>:指定要下载文件的后缀名,多个后缀名之间使用逗号进行分隔;

-b:进行后台的方式运行 wget;

-B<连接地址>:设置参考的连接地址的基地地址;

-c:继续执行上次终端的任务;

-C<标志>:设置服务器数据块功能标志 on 为激活,off 为关闭,默认值为 on;

-d:调试模式运行指令;

-D<域名列表>:设置顺着的域名列表,域名之间用“,”分隔;

-e<指令>:作为文件“.wgetrc”中的一部分执行指定的指令;

-h:显示指令帮助信息;

-i<文件>:从指定文件获取要下载的 URL 地址;

-l<目录列表>:设置顺着的目录列表,多个目录用“,”分隔;

-L:仅顺着关联的连接;

-r:递归下载方式;

-nc:文件存在时,下载文件不覆盖原有文件;

-nv:下载时只显示更新和出错信息,不显示指令的详细执行过程;

-q:不显示指令执行过程;

-nh:不查询主机名称;

-v:显示详细执行过程;

-V:显示版本信息;

–passive-ftp:使用被动模式 PASV 连接 FTP 服务器;

–follow-ftp:从 HTML 文件中下载 FTP 连接文件。

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
wget http://test.com/testfile.zip ->下载指定文件到当前文件夹
wget -O wordpress.zip http://test.com/download ->指定保存名字
wget --limit-rate=300k http://www.linuxde.net/testfile.zip ->限制下载速度
wget -c http://www.linuxde.net/testfile.zip ->断点续传
wget -b http://www.linuxde.net/testfile.zip ->后台下载

# 设置使用指定浏览器下载(伪装下载)
wget --user-agent="Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16" http://www.linuxde.net/testfile.zip

wget --spider url ->测试下载
wget --tries=40 URL ->设置重试次数为40
wget -i filelist.txt ->从filelist.txt获取下载地址

# 镜像网站
# --miror开户镜像下载。
# -p下载所有为了html页面显示正常的文件。
# --convert-links下载后,转换成本地的链接。
# -P ./LOCAL保存所有文件和目录到本地指定目录
wget --mirror -p --convert-links -P ./LOCAL URL

wget --reject=gif ur ->下载一个网站,但你不希望下载图片,可以使用这条命令
wget -o download.log URL ->把下载信息存入日志文件
wget -Q5m -i filelist.txt ->限制总下载文件大小
wget -r -A.pdf url ->下载指定格式文件

# FTP下载
wget ftp-url
wget --ftp-user=USERNAME --ftp-password=PASSWORD url

underscore框架设计

立即执行函数

使用立即执行函数,创建局部作用域,隔离环境并初始化代码

1
2
3
(function (global, factory) {

}(this, function () {}))

判断采用那种模块化规范导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// commomjs 规范
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
// amd 规范
typeof define === 'function' && define.amd ? define('underscore', factory) :
// 如果都不是则 直接挂在this上
(global = global || self, (function () {
var current = global._;
var exports = global._ = factory();
//防止多次引入冲突
exports.noConflict = function () {
global._ = current;
return exports;
};
}()));

创建根节点

1
2
3
4
5
// 建立根节点对象,self 在浏览器端, global 在服务端, this 在一些虚拟机中,使用self 代替 window 提供对 Webworker 的支持
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
Function('return this')() ||
{};

each方法

each 依赖的函数

一个内部函数,根据参数返回不同的回调函数的封装,一个复用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function optimizeCb(func, context, argCount) {

// 没有传入执行上下文, 直接返回函数
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1:
return function (value) {
return func.call(context, value);
};
// The 2-argument case is omitted because we’re not using it.
case 3:
return function (value, index, collection) {
return func.call(context, value, index, collection);
};
case 4:
return function (accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function () {
return func.apply(context, arguments);
};
}

isArrayLike

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;


// 简单获取属性值
function shallowProperty(key) {
return function (obj) {
return obj == null ? void 0 : obj[key];
};
}

var getLength = shallowProperty('length');

// 属性检查 数字格式,且不能超过数组最大值
function createSizePropertyCheck(getSizeProperty) {
return function (collection) {
var sizeProperty = getSizeProperty(collection);
return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
}
}

var isArrayLike = createSizePropertyCheck(getLength);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function each(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
if (isArrayLike(obj)) {
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj);
}
} else {
var _keys = keys(obj);
for (i = 0, length = _keys.length; i < length; i++) {
iteratee(obj[_keys[i]], _keys[i], obj);
}
}
return obj;
}

挂载方法 混合模式

依赖的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//内部方法,创建一个toString 基础测试器
function tagTester(name) {
var tag = '[object ' + name + ']';
return function (obj) {
return toString.call(obj) === tag;
};
}
var isFunction = tagTester('Function');
var isFunction$1 = isFunction;

//返回一个排序的 所有工具函数名的数组
function functions(obj) {
var names = [];
for (var key in obj) {
if (isFunction$1(obj[key])) names.push(key);
}
return names.sort();
}

定义underscore方法

  • 如果是underscore实例直接返回
  • 如果不是通过new操作符执行函数
  • 再次进入第二行的判断,这时已经是underscore的实例会继续往下执行
  • 在实例上挂在一个变量指向传入的对象
1
2
3
4
5
function _(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
}

核心mixin 方法

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
function mixin(obj) {

// 循环所有导出方法的名称
each(functions(obj), function (name) {
// 每个名称对应的方法
var func = _[name] = obj[name];
// 在原型上挂载同样的方法
_.prototype[name] = function () {
//拿到上面挂载的 传入的对象
var args = [this._wrapped];
// 拼接为整个数组
Array.prototype.push.apply(args, arguments);
//直接用定义的内置方法执行
return chainResult(this, func.apply(_, args));
};
});
return _;
}

var allExports = {
each:each
}

var _$1 = mixin(allExports);
// Legacy Node.js API.
_$1._ = _$1;

return _$1;

所以可以用下面的方法执行方法

1
_([1,2,3]).each(function(item){console.log(item)});

测试环境karma

初始化

1
yarn init -y

实现第一个测试用例

index.js

1
2
3
var add = function(a){
return 1 + a
}

index.spec.js

1
2
3
4
5
describe('测试基本api',function(){
it('add',function(){
except(add(1)).toBe(2)
})
})

如何让上面的代码运行

安装karma测试框架

1
yarn add -D karma

配置package.json可以执行karma命令,如果不想配置可以安装 karma-cli

1
2
3
"scripts":{
"karma-init":"karma init"
}

执行命令初始化

1
yarn karma-init
  • Which testing framework do you want to use?

想要使用的单元测试框架 jasmine

  • Do you want to use Require.js

是否想要用Requirejs, 不需要

  • Do you want to capture any browsers automatically?

想要自动调用的浏览器 PhantomJS

PhantomJS是一个可编程的无头浏览器:一个完整的浏览器内核,包括js解析引擎,渲染引擎,请求处理等,但是不包括显示和用户交互页面的浏览器。

hantomJS的适用范围就是无头浏览器的适用范围。通常无头浏览器可以用于页面自动化,网页监控,网络爬虫等:

页面自动化测试:希望自动的登陆网站并做一些操作然后检查结果是否正常。
网页监控:希望定期打开页面,检查网站是否能正常加载,加载结果是否符合预期。加载速度如何等。
网络爬虫:获取页面中使用js来下载和渲染信息,或者是获取链接处使用js来跳转后的真实地址

  • What is the location of your source and test files

想要测试的文件位置,暂时留空

  • Should any of the files included by the previous patterns be excluded?

想要排除哪些文件暂时留空

  • Do you want Karma to watch all the files and run the tests on change?

是否需要监听文件改变, 暂时选no,不监听

下一步

打开生成的karma.conf.js文件

1
2
3
4
5
6
7
8
9
10
11
12
{
// 测试框架名称
frameworks: ['jasmine'],

//测试文件目录
files: [
"./test/unit/**/*.js",
"./test/unit/**/*.spec.js"
],

singleRun:true
}

在package.json中添加运行命令

1
2
3
4
 "scripts": {
"karma-init": "karma init",
"karma-start": "karma start"
}

安装无头浏览器和jasmine适配器

1
yarn add -D karma-jasmine karma-phantomjs-launcher phantomjs

编写测试用例

index.js

1
2
3
4
5
6
7
function add(a) {
if (a == 1) {
return a + 1;
} else {
return a + 2;
}
}

index.spec.js

1
2
3
4
5
describe('单元测试',function(){
it("contains spec with an expectation", function() {
expect(add(1)).toBe(2);
})
})

执行 karma-start 进行单元测试

覆盖率测试

安装 karma-coverage

1
yarn add -D karma-coverage

修改配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
preprocessors: {
"./test/unit/**/*.js":"coverage"
},

reporters: ['progress','coverage'],

coverageReporter: {
dir: 'doc/coverage',
reporters: [
// reporters not supporting the `file` property
{ type: 'html', subdir: 'report-html' },
]
}
}

执行再次执行 karma-start

UI测试

安装 backstop

1
yarn add -D backstop

添加 package.json 命令行

1
2
3
4
5
6
7
{
"scripts": {
"backstop-init": "backstop init",
"backstop-start": "backstop test"
}
}

执行 backstop-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
{
"id": "baidu",

// 设计图分辨率设置
"viewports": [
{
"label": "phone",
"width": 375,
"height": 667
},
{
"label": "tablet",
"width": 1024,
"height": 768
}
],

//调用 puppeteer 库的脚本
"onBeforeScript": "puppet/onBefore.js",
"onReadyScript": "puppet/onReady.js",
"scenarios": [
{
"label": "baidu home",
"cookiePath": "backstop_data/engine_scripts/cookies.json",
"url": "https://garris.github.io/BackstopJS/",
"referenceUrl": "",
"readyEvent": "",
"readySelector": "",
"delay": 0,
"hideSelectors": [],
"removeSelectors": [],
"hoverSelector": "",
"clickSelector": "",
"postInteractionWait": 0,
"selectors": [],
"selectorExpansion": true,
"expect": 0,
"misMatchThreshold" : 0.1,
"requireSameDimensions": true
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"engine_scripts": "backstop_data/engine_scripts",
"html_report": "doc/backstop_data/html_report",
"ci_report": "backstop_data/ci_report"
},
"report": ["browser"],
"engine": "puppeteer",
"engineOptions": {
"args": ["--no-sandbox"]
},
"asyncCaptureLimit": 5,
"asyncCompareLimit": 50,
"debug": false,
"debugWindow": false
}

单元测试理论

TDD 测试驱动开发

  • 首先,开发者在码业务前写一些测试用例
  • 运行这些测试用例。结果肯定是运行失败,因为测试用例中的业务逻辑还没实现嘛
  • 开发者实现测试用例中的业务逻辑
  • 再运行测试用例, 如果开发者代码能力不错,这些测试用例应该可以跑通了(pass)
  • 对业务代码及时重构,包括增加注释,清理重复等。因为没人比开发者自己更了解哪些代码会对哪些部分造成影响从而导致测试失败(fail)

我们通过举例来了解一下如何实践TDD。例子中的代码可以从github上获取tdd-vs-bdd。将代码clone下来,执行命令npm install && grunt

假设我们想写一个计算阶乘的函数(这是一个很刻意的例子,但是这个例子对我们指出TDD和BDD的区别很有帮助)。TDD的常用方式是运行某函数,然后断言结果满足某个值。

在阶乘的例子中,我们使用的javascript测试框架是Mocha。废话不说,上代码:

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
var assert = require('assert'),
factorial = require('../index');

suite('Test', function (){
setup(function (){
// Create any objects that we might need
});

suite('#factorial()', function (){
test('equals 1 for sets of zero length', function (){
assert.equal(1, factorial(0));
});

test('equals 1 for sets of length one', function (){
assert.equal(1, factorial(1));
});

test('equals 2 for sets of length two', function (){
assert.equal(2, factorial(2));
});

test('equals 6 for sets of length three', function (){
assert.equal(6, factorial(3));
});
});
});

显然上述测试会失败,因为我们尚未实现函数功能。所以接下来我们需要实现满足上述测试用例的阶乘函数。代码如下

1
2
3
4
5
6
module.exports = function (n) {
if (n < 0) return NaN;
if (n === 0) return 1;

return n * factorial(n - 1);

现在我们再次运行测试用例,所有的case都跑通了! 这就是TDD的使用方式。

BDD 行为驱动开发

BDD旨在消除TDD过程中可能造成的问题。

与TDD相比,BDD是通过编写行为和规范来驱动软件开发。 行为和规范可能看起来与测试非常相似,但是它们之间却有着微妙但重要的区别。

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
var assert = require('assert'),
factorial = require('../index');

describe('Test', function (){
before(function(){
// Stuff to do before the tests, like imports, what not
});

describe('#factorial()', function (){
it('should return 1 when given 0', function (){
factorial(0).should.equal(1);
});

it('should return 1 when given 1', function (){
factorial(1).should.equal(1);
});

it('should return 2 when given 2', function (){
factorial(2).should.equal(2);
});

it('should return 6 when given 3', function (){
factorial(3).should.equal(6);
});
});

after(function () {
// Anything after the tests have finished
});
});

敏捷开发

敏捷开发以用户的需求进化为核心,采用迭代、循序渐进的方法进行软件开发。在敏捷开发中,软件项目在构建初期被切分成多个子项目,各个子项目的成果都经过测试,具备可视、可集成和可运行使用的特征。换言之,就是把一个大项目分为多个相互联系,但也可独立运行的小项目,并分别完成,在此过程中软件一直处于可使用状态。

偏函数和函数柯里化

偏函数 (Partial application)

In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.

在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。(什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。)

没有上下文的偏函数
1
2
3
4
5
6
7
const partial = (fn, ...args) => {
return (...args2) => fn.call(this, ...args, ...args2)
}

console.log(partial(function (a, b, c, d) {
return a + b + c + d
}, 1, 2)(3, 4))
bind 实现
  • 类型判断,错误处理
  • 缓存一级参数
  • 定义返回的新函数
  • 处理原型链
  • 绑定新函数的执行上下文,判断是否通过new调用
1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.bind = function (ctx) {
if (typeof this !== 'function') throw new Error();
var args = Array.prototype.slice.call(arguments, 1);
var toBind = this;
var fn = function _fn() {
args = args.concat(Array.prototype.slice.call(arguments, 0))
toBind.apply(this instanceof _fn ? this : ctx, args)
}
if (toBind.prototype) {
fn.prototype = Object.create(toBind.prototype)
}
return fn;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.bind = function (ctx) {
if (typeof this !== 'function') throw new Error();
var args = Array.prototype.slice.call(arguments, 1);
var toBind = this;
var noop = function(){}
var fn = function _fn() {
args = args.concat(Array.prototype.slice.call(arguments, 0))
toBind.apply(noop.prototype.isPrototypeOf(this) ? this : ctx, args)
}
if (toBind.prototype) {
noop.prototype = toBind.prototype;
}
fn.prototype = new noop()
return fn;
}

柯里化 (Currying)

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

ES6实现
1
2
const curry = (fn, args = []) =>
fn.length === args.length ? fn(...args) : (...args2) => curry(fn, [...args, ...args2])
反柯理化

使用箭头函数不能绑定函数的this

1
const uncurry = (fn) => (...args) => fn.apply(this, args)
1
2
3
4
5
6
Function.prototype.uncurring = function () {
var self = this;
return function () {
return self.apply(this, arguments);
};
};

偏函数与柯里化区别

柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。

局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

.bashr 和 .profile的区别

要搞清bashrc与profile的区别,首先要弄明白什么是交互式shell和非交互式shell,什么是login shell 和non-login shell。

交互式模式

shell等待你的输入,并且执行你提交的命令。这种模式被称作交互式是因为shell与用户进行交互。

这种模式也是大多数用户非常熟悉的:登录、执行一些命令、签退。当你签退后,shell也终止了

非交互式模式

shell不与你进行交互,而是读取存放在文件中的命令,并且执行它们。当它读到文件的结尾,shell也就终止了。

bashrc与profile都用于保存用户的环境信息,bashrc用于非交互式non-loginshell,而profile用于交互式login shell。

系统中存在许多bashrc和profile文件:

  • /etc/profile 此文件为系统的每个用户设置环境信息,当第一个用户登录时,该文件被执行.并从/etc/profile.d目录的配置文件中搜集shell的设置.

    每个用户都可使用该文件输入专用于自己使用的shell信息,当用户登录时,该文件仅仅执行一次!默认情况下,它设置一些环境变量,然后执行用户的.bashrc文件.

  • /etc/bashrc:为每一个运行bash shell的用户执行此文件.当bash shell被打开时,该文件被读取。有些linux版本中的/etc目录下已经没有了bashrc文件。

    该文件包含专用于某个用户的bash shell的bash信息,当该用户登录时以及每次打开新的shell时,该文件被读取.另外,/etc/profile中设定的变量(全局)的可以作用于任何用户,而~/.bashrc等中设定的变量(局部)只能继承/etc/profile中的变量,他们是”父子”关系.

/etc/profile,/etc/bashrc 是系统全局环境变量设定

/.profile,/.bashrc用户家目录下的私有环境变量设定

当登入系统时候获得一个shell进程时,其读取环境设定档有三步

  1. 首先读入的是全局环境变量设定档/etc/profile,然后根据其内容读取额外的设定的文档,如/etc/profile.d和/etc/inputrc

  2. 然后根据不同使用者帐号,去其家目录读取/.bash_profile,如果这读取不了就读取/.bash_login,这个也读取不了才会读取~/.profile,这三个文档设定基本上是一样的,读取有优先关系

  3. 然后在根据用户帐号读取/.bashrc至于/.profile与~/.bashrc的不区别都具有个性化定制功能

~/.profile可以设定本用户专有的路径,环境变量,等,它只能登入的时候执行一次
~/.bashrc也是某用户专有设定文档,可以设定路径,命令别名,每次shell script的执行都会使用它一次

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信