Giter VIP home page Giter VIP logo

blogs's Introduction

对写作的定位

写一篇好的博客其实很费时间,不过换个角度想,这些费的时间可能就是在沉淀,还能有什么方法比「把事情和别人讲清楚」更能说明自己对某个事情的了解程度呢。

关于博客的主题

往往是我们自己限制了自己,还是觉得只要是觉得有用的知识就值得去学习,所以也博客不会有确定的主题,不过还是有一些优先的方向。

基础篇

计算机的世界真的很有趣,计算机的世界有很多通用的原理。算法,数据结构,设计模式,系统编程,各种协议等等都值得深入的去了解。

语言篇

目前的工作语言为JavaScript,平时也会写一些 python 代码,之后有机会还想再学学go。在语言篇中想要多写写基础知识不是那么容易写明白的事情。

工具篇

工欲善其事必先利其器

有很多工具本身就很有意思,用起来很酷,还能帮我们解决不少问题。熟练使用 Git 能让你在协作开发中应当自如,熟练的使用 Vim | Emac 能让你迎来众人羡慕的眼光,即使是熟练使用一个编辑器也能让你开发效率倍增,此篇将记录我对一些工具的理解:

实践篇

光说不练假把式

其实还是很容易陷入懂了很多,动手却啥也不会的境地,学过的东西还是得用起来,此篇将记录通过编程对自己想法的实现

翻译篇

翻译也是一种很好的学习过程,不把每句话看懂,肯定也翻译不好。

实在是遇到了很喜欢的外文,也会在此翻译。

读书笔记

后记

还真是越来越喜欢那种顿悟时刻,期待自己每个月能有一篇更新。

blogs's People

Contributors

val-zhang avatar zhu-ting avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blogs's Issues

Express4.x api 翻译(draft)

Express4.x api 翻译(draft)

用了一年多的Express了,其实会用的功能也能基本满足业务的需求,但是总是感觉自己对其的掌握还是缺少一种系统性。故翻译其api,以期自己在翻译过程中对其有更深的认识。

API 原文地址

翻译的内容还比较粗糙,欢迎提建议进行修改。

express()

用以生成一个Express应用,express()函数是从express模块导出的顶级函数。

var express = require('express');
var app = express();

方法

express.json([options])

此方法支持Express4.16.0及更新的版本,用于取代body-parser

这是Express提供的一个内置中间件函数,它基于body-parser用以解析传入的请求为JSON格式。

本函数返回只解析JSON的中间件,并且只作用于Content-Type请求头与type选项匹配的请求。此解析器可接收任何编码格式的body,支持自动解压gzip和压缩deflate编码。

在进过此中间件处理后,request对象中会添加body属性(如req.body),它是一个对象,其中包含解析而来的数据,如果请求中没有body可供解析或Content-Type不匹配抑或发生了错误,body则会是一个空对象({})

下表描述了可选的options对象的属性:

属性 描述 类型 默认值
inflate 是否允许处理压缩的请求体,当设置为false时,压缩请求体的请求会被拒绝 Boolean true
limit 控制请求体的大小,如果是一个数值,为指定的字节数,如果是一个字符串,该值会被传给bytes库进行解析 Mixed "100kb"
reviver reviver选项会直接传递给JSON.parse做为第二个参数,可以在mdn上查看更多信息 Function null
strict 是否只接收数组或对象,当设置为false时能接收任何JSON.parse可处理的类型 Boolean true
type 用于确定中间件将处理那种媒体类型,其值可以是一个字符串,字符串构成的数组或者一个函数,当不是一个函数时,该值会被传输给type-is库进行处理,该值还可以是一个拓展名(如json),mime type(如application/json)或者包含通配符的 mime type (如*/* 或者 */json),如果是一个函数,type类型将通过fn(req)调用,如果返回的是一个有效值,请求会被解析 Mixed "application/json"
verify 如果这个选项被支持,将以verify(req, res, buf, encoding)形式被调用,其中buf是由原始请求体构成的Buffer,encoding是请求的编码,解析可以通过抛出错误而放弃 Function undefined

express.static(root,[options])

这也是Express内置中间件之一,它基于serve-static构建,用于提供静态文件。

Note: 使用反向代理缓存可以提高静态文件服务器的效率

root参数指定提供静态文件的根目录。服务器将拼合req.url和所提供的根目录来查找静态文件。如果没有找到对应的文件,服务器不会返回404,而将调用next()以执行下一个中间件,允许堆叠和回退。

何谓倒退

下表描述了可选的options对象中可配置的属性:

属性 描述 类型 默认值
dotfiles 决定如何看待以点开头的文件 String "ignore"
etag 是否生成etag,express.static始终生成弱校验Etags Boolean true
extensions 设置文件的备选拓展名,如果一个文件没有找到则依据此处的设置寻找是否有其它后缀的文件并将发送第一个找到的文件,如['html','htm'] Mixed false
fallthrough 让客户端的错误作为未处理的请求,否则转发客户端错误,详见下文 Boolean true
immutable Cache-Control响应头中启用或禁用不可变的指令。如果启用,还应指定maxAge选项以启用缓存。不可变的指令将阻止受支持的客户端在maxAge有效期间发出检查文件是否已更改的条件请求。 Boolean false
index 发送指定的目录索引文件。设置为false以禁用目录索引 Mixed "index.html"
lastModified Last-Modified头部信息设置为文件在该系统上的最后修改日期 Boolean true
maxAge 以毫秒为单位设置Cache-Control头部信息的max-age属性,或以ms格式设置字符串 Number 0
redirect 当路径名是一个目录时,自动在后面加上“/” Boolean true
setHeaders 用于设置HTTP头文件的函数 Function

更多信息可查看Serving static files in ExpressUsing middleware - Built-in middleware.

ETag是HTTP协议提供的若干机制中的一种Web缓存验证机制,并且允许客户端进行缓存协商。这就使得缓存变得更加高效,而且节省带宽。如果资源的内容没有发生改变,Web服务器就不需要发送一个完整的响应。ETag也可用于乐观并发控制[1],作为一种防止资源同步更新而相互覆盖的方法。

强校验的ETag匹配要求两个资源内容的每个字节需完全相同,包括所有其他实体字段(如Content-Language)不发生变化。强ETag允许重新装配和缓存部分响应,以及字节范围请求。 弱校验的ETag匹配要求两个资源在语义上相等,这意味着在实际情况下它们可以互换,而且缓存副本也可以使用。不过这些资源不需要每个字节相同,因此弱ETag不适合字节范围请求。当Web服务器无法生成强ETag不切实际的时候,比如动态生成的内容,弱ETag就可能发挥作用了。

dotfiles

此选项的可选值有以下几个:

  • allow:不会特别对待以点开头的文件;
  • deny:拒绝返回以点开头的文件,会返回403错误并调用next()
  • ignore:忽略对以点开头的文件的请求,返回404错误并调用next()

Note: 使用默认值,不会忽略文件夹中的以点开头的文件

fallthrough

当此选项设置为true,诸如无效请求或请求不存在的文件时将引起中间件调用next(),使得下一个中间件位于栈中。设置为false时,这些错误将触发next(err)

将此选项设置为true,可以让你映射多个物理目录到相同的Web地址或者调用路由来充填不存在的文件。

如果你想让某路径严格限制在某文件系统中则可以使用false,通过404短路可以减小服务器压力,
如果您将此中间件安装在严格意义上为单个文件系统目录的路径上,则可以使用false,这样可以使404短路,从而减少开销,这个中间件对所有的请求方法生效。

setHeaders

此选项用于指定一个函数用以自定义相应头,必须使用同步方法修改头部内容。函数签名如下:

fn(res, path, stat)

各选项意义如下:

  • res,响应对象
  • path,发送的文件的路径
  • stat,发送的文件的stat对象
express.static使用示例
var options = {
  dotfiles: 'ignore',
  etag: false,
  extensions: ['htm', 'html'],
  index: false,
  maxAge: '1d',
  redirect: false,
  setHeaders: function (res, path, stat) {
    res.set('x-timestamp', Date.now())
  }
}

app.use(express.static('public', options))

express.Router([options])

创建一个新的router对象

var router = express.Router([options]);

可选参数option对象中的属性如下

属性 描述 默认值 兼容性
caseSensitive 是否启用大小写敏感 默认不启用,这意味着/Foofoo是一样的
mergeParams 是否保存父路由中的req.params值,如果相互冲突,取子路由中的值 false 4.5.0+
strict 是否启用严格匹配 默认禁止,/foo/foo/的响应一致

你可以像对待express应用一样,给router添加中间件和各种方法。

express.urlencoded([options])

此中间件适用于Express v4.16.0及更新的方法

这是Express提供的一个内置中间件,它基于body-parser解析传入的请求为urlencoded格式。

返回只解析urlencoded的中间件,而且只解析请求头的Content-Typetype匹配的请求。此解析器只接收 UTF-8编码格式的body,支持自动解压gzip和压缩编码。

进过此中间件处理后,会返回response对象中将包含body对象(如req.body)其中包含解析所得数据,如果没有body可供解析或Content-Type不匹配或发生错误则会返回一个空对象({})。对象的值可以是字符串或者数组(当extended为false时),或者其它任意类型(当extended为true时)。

下表描述了可选的options对象中可配置的属性:

属性 描述 类型 默认值
extended 此选项将决定会使用querystring库(false时)还是qs库(true时)来解析URL-encoded数据。“extended”语法允许将对象和数组编码为URL格式,从而达到使用URL编码的类似获得类似JSON的体验。查看qs了解更多信息 Boolean true
inflate 是否允许处理压缩的请求体,当设置为false时,压缩请求体的请求会被拒绝 Boolean true
limit 控制请求体的大小,如果是一个数值,为指定的字节数,如果是一个字符串,该值会被传给bytes库进行解析 Mixed "100kb"
parameterLimit 该选项控制URL编码数据中允许的最大参数数量。如果一个请求包含比这个值更多的参数,将会引发一个错误。 Bumber 1000
type 用于确定中间件将处理那种媒体类型,其值可以是一个字符串,字符串构成的数组或者一个函数,如果不是一个函数,该值会被直接传输给type-is库,值可以是一个拓展名(如urlencoded),mime type(如 "application/x-www-form-urlencoded")或者包含通配符的 mime type (如*/* 或者 */json),如果是一个函数,type类型将通过fn(req)调用并且如果返回一个真值请求会被解析 Mixed "application/x-www-form-urlencoded"
verify 如果这个选项被支持,将以verify(req, res, buf, encoding)形式被调用,其中buf是由原始请求体构成的Buffer,encoding是请求的编码,解析可以通过抛出错误而放弃 Function undefined

Application

app对象常被用来表示Express应用,它通过调用Express模块提供的顶级函数express()生成

var express = require('express');
var app = express();

app.get('/', function(req, res){
  res.send('hello world');
});

app.listen(3000);

app对象具备以下方法:

app对象还提供一些其它的影响应用行为的配置,可以查看Application settings了解更多信息。

Express appliaction 对象可以分别以 req.appres.app 指向 request对象response对象

属性

app.locals

app.locals是一个对象,其以app内部的各变量为属性

app.locals.title
// => 'My App'

app.locals.email
// => '[email protected]'

一旦设置,app.locals属性将在整个应用的生命周期内有效,相比而言res.locals的属性值则只在某请求的生命周期内有效。

你可以在app渲染的模板的过程中访问本地变量。这样就可以为模板提供辅助函数及app级别的数据,app本地变量在中间件中可以通过req.app.locals访问(详见req.app

app.locals.title = 'My App';
app.locals.strftime = require('strftime');
app.locals.email = '[email protected]';

app.mountpath

app.mountpath属性用以表示某sub-app所匹配的一个或多个路径模式。

sub-app指的是用于处理对路由的请求的express的实例。

var express = require('express');

var app = express(); // the main app
var admin = express(); // the sub app

admin.get('/', function (req, res) {
  console.log(admin.mountpath); // /admin
  res.send('Admin Homepage');
});

app.use('/admin', admin); // mount the sub app

它和req对象提供的baseUrl功能类似,不同之处在于req.baseUrl返回的是匹配的URL路径而非匹配模式。

如果一个sub-app有多种路径匹配模式,sub-app.mountpath将返回一个模式的列表

var admin = express();

admin.get('/', function (req, res) {
  console.log(admin.mountpath); // [ '/adm*n', '/manager' ]
  res.send('Admin Homepage');
});

var secret = express();
secret.get('/', function (req, res) {
  console.log(secret.mountpath); // /secr*t
  res.send('Admin Secret');
});

admin.use('/secr*t', secret); // load the 'secret' router on '/secr*t', on the 'admin' sub app
app.use(['/adm*n', '/manager'], admin); // load the 'admin' router on '/adm*n' and '/manager', on the parent app

次级app还可以再拥有次级app,如果如此,那次级approuter的区别在哪儿呢?
router其实只具备部分功能,sub-app具备全部功能

Events

app.on('mount',callback(parent))

mount事件在sub-app挂载(mount)到父app时触发,父app会当做参数传入回调函数中。

Note:
Sub-app将:

  • 不继承settings中的默认值,在sub-app中需要重新设置;
  • 将继承没有默认值的settings中的值
var admin = express();

admin.on('mount', function (parent) {
  console.log('Admin Mounted');
  console.log(parent); // refers to the parent app
});

admin.get('/', function (req, res) {
  res.send('Admin Homepage');
});

app.use('/admin', admin);

Methods

app.all(path,callback[,callback])

此方法类似标准的app.MEYHOD()方法,不同的地方在于它将匹配所有类型的http请求。

参数

参数1: path 默认值: /(root path)

描述:

中间件被触发的路径,可以是以下值中的一种:

  • 用字符串表达的路径
  • 匹配路径的正则表达式
  • 路径模式
  • 上述值组成的数组
    可以点击Path examples查看实际的例子

参数2: callback 默认值: None

描述:

回调函数可以是如下中的一种:

  • 一个中间件函数
  • 由逗号隔开的一系列中间件函数
  • 一个由中间件函数构成的数组
  • 上述情况的组合

您可以提供多个回调函数,其行为与中间件类似,只不过这些回调可以调用next('route')来绕过剩余的路由回调。你可以使用此机制来决定应该使用哪个路由,如果没有继续使用当前路由的理由,则可以调到下一个路由。

由于routerapp都实现了中间件接口,因此你也可以把它们当做中间件使用。

可在此处参考示例

示例

以下回调将响应GETPOSTPUTDELETE或任何其他HTTP请求方法对路由/secret的请求:

app.all('/secret', function (req, res, next) {
  console.log('Accessing the secret section ...')
  next() // pass control to the next handler
});

app.all()方法在处理对某特定的前缀或匹配的特殊路径的所有类型的请求时特别有用。比如说如果你把下述代码放在所有其它路径的定义之前,就会让从此代码之后的所有路由都需要身份验证,并自动加载一个user。这些回调也不必做为终点,loadUser可以用来执行某个任务,然后调用next()来继续匹配之后的路由。

app.all('*', requireAuthentication, loadUser);

上述代码也等同于

app.all('*', requireAuthentication);
app.all('*', loadUser);

下面还有另外一个非常有用的app.all使用示例,此例和上面的例子类似,但是严格限制路径以/api开头

app.all('/api/*', requireAuthentication);

app.delete(path, callback [, callback ...])

为某路径的HTTP DELETE请求绑定特定的回调函数。更多信息可查看路由指南

参数

参数1: path 默认值: /(root path)

描述:

路径模式可以是以下类型中的一种:

  • 路径字符串
  • 匹配路径的正则表达式
  • 通配符
  • 上述类型值组成的数组
    点击Path examples可查看更多实际的例子

参数2: callback 默认值: None

描述:

回调函数可以是如下类型中的一种:

  • 一个中间件函数
  • 由逗号隔开的一系列中间件函数
  • 一个由中间件函数构成的数组
  • 上述情况的组合

可以提供多个回调函数,多个回调函数的调用与多个中间件的调用类似,不同之处在于在回调函数中调用next('route')可绕过之后的回调函数。你可以基于此机制来觉得是否需要触发之后的回调函数

routerapp都实现了中间件接口,你可以像使用中间件一样使用它们。

可在此处参考示例

示例
app.delete('/', function (req, res) {
  res.send('DELETE request to homepage');
});

app.disable(name)

设置setting中的布尔值属性name的值为falsenameapp settings表中的值为布尔型的项。调用app.set('foo',false)和调用app.disable('foo')的效果一致:

如:

app.disable('trust proxy');
app.get('trust proxy');
// => false

app.disabled(name)

判断setting中的设置项name的值是否为false,如果setting中的设置项name的值为false则返回true,nameapp settings表中的值为布尔型的项。

app.disabled('trust proxy');
// => true

app.enable('trust proxy');
app.disabled('trust proxy');
// => false

app.enable(name)

设置setting中的布尔值设置项nametrue,调用app.enable('foo')和调用app.set('foo',true)效果相同。

app.enable('trust proxy');
app.get('trust proxy');
// => true

app.enabled(name)

判断setting中的设置项name的值是否为true,如果setting中的设置项name的值为true则返回true,nameapp settings表中的值为布尔型的项。

app.enabled('trust proxy');
// => false

app.enable('trust proxy');
app.enabled('trust proxy');
// => true

app.engine(ext,callback)

注册ext格式的模板的回调函数为callback

默认情况下,Express会基于拓展名require()引擎,比如说,如果你渲染文件foo.pug,Express将在内部触发以下代码,并会为接下来的请求缓存require()以提高性能。

app.engine('pug', require('pug').__express);

对不提供直接可用的.__express的引擎,或者你想把不同的后缀映射到当前引擎可以使用下述方法,

// 使用EJS引擎来渲染`.html`文件
app.engine('html', require('ejs').renderFile);

在上面的例子中,renderFile()方法提供了Express想要的相同的签名(path,options,callback),不过请注意这个方法会自动在内部调用ejx.__express)所以如果你想要渲染的文件的后缀是.ejx,则不需要调用做别的事情。

也有一些模板引擎不遵循这个约定,consolidate.js库可以映射 Node 模板引擎为准守这种规律,所以他们可以和Express无缝链接使用。

var engines = require('consolidate');
app.engine('haml', engines.haml);
app.engine('html', engines.hogan);

app.get(name)

返回app setting 中相关属性name的值,如:

app.get('title');
// => undefined

app.set('title', 'My Site');
app.get('title');
// => "My Site"

app.get(path,callback[,callback])

使用特定的回调函数处理特定路径的HTTP GET请求

参数

参数1: path 默认值: /(root path)

描述:

中间件被触发的路径,可以是以下值中的一种:

  • 路径字符串
  • 匹配路径的正则表达式
  • 路径模式
  • 上述值组成的数组
    可以点击Path examples查看实际的例子

参数2: callback 默认值: None

描述:

回调函数可以是如下中的一种:

  • 中间件函数
  • 由逗号隔开的一系列中间件函数
  • 一个由中间件函数构成的数组
  • 上述情况的组合

可以提供多个回调函数,多个回调函数的调用与多个中间件的调用类似,不同之处在于在回调函数中调用next('route')可绕过之后的回调函数。你可以基于此机制来觉得是否需要触发之后的回调函数

routerapp都实现了中间件接口,你可以像使用其他中间件功能一样使用它们。

可在此处参考示例

更多信息可参考routing 指南

app.listen(path,[callback])

启动UNIX套接字并侦听指定路径上的连接。此方法等同于Node的http.Server.listen()方法.

var express = require('express');
var app = express();
app.listen('/tmp/sock');

app.listen(port,[hostname],[backlog],[callback])

绑定并监听对指定的host和端口的连接。此方法和Node的http.Server.listen()方法一致。

var express = require('express');
var app = express();
app.listen(3000);

express()方法返回的app实际上是一个JavaScript Function,它实际上被设计为传递给Node的HTTP servers作为回调函数来处理请求。由于 app 并没有什么继承,这使得可以非常方便使用同一套代码提供httphttps版本的app。

var express = require('express');
var https = require('https');
var http = require('http');
var app = express();

http.createServer(app).listen(80);
https.createServer(options, app).listen(443);

app.listen()方法返回一个http.Server对象,对于http来说,它可以像下面这样使用

app.listen = function() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

app.METHOD(path,callback[,callback])

依据请求的类型处理http请求,请求类型可以是GET,PUT,POST等等的小写模式。因此,实际的方法是app.get(),app.post(),app.put()等等。点击这里可以查看详细的路由方法清单。

参数

参数1: path 默认值: /(root path)

描述:

路径模式可以是以下类型中的一种:

  • 路径字符串
  • 匹配路径的正则表达式
  • 通配符
  • 上述类型值组成的数组
    点击Path examples可查看更多实际的例子

参数2: callback 默认值: None

描述:

回调函数可以是如下类型中的一种:

  • 一个中间件函数
  • 由逗号隔开的一系列中间件函数
  • 一个由中间件函数构成的数组
  • 上述情况的组合

可以提供多个回调函数,多个回调函数的调用与多个中间件的调用类似,不同之处在于在回调函数中调用next('route')可绕过之后的回调函数。你可以基于此机制来觉得是否需要触发之后的回调函数

routerapp都实现了中间件接口,你可以像使用中间件一样使用它们。

可在此处参考示例

路由方法

Express下述路由方法,它们和对应的HTTP方法具有相同的名称

  • checkout
  • copy
  • delete
  • get
  • head
  • lock
  • merge
  • mkactivity
  • mkcol
  • move
  • m-search
  • notify
  • options
  • patch
  • post
  • purge
  • put
  • report
  • search
  • subscribe
  • trace
  • unlock
  • unsubscribe

API文档中只对常用的HTTP方法进行了描述,如app.get(),app.post(),app.put()以及app.delete()。不过上面列出的其它方法使用方法也是类似的

对于无效的JavaScript变量名类型,可以使用中括号来调用,比如app['m-search']('/', function ....

如果没有在app.get()前指定HTTP HEAD对应的方法,将会调用app.get()来响应HEAD请求。

app.all会响应针对某个特定路径的所有请求,详细可参看

更多信息可参考routing 指南

app.param([name],callback)

为路由的参数添加回调函数,其中name是参数名或由参数组成的数组,callback是回调函数。回调函数的参数依次是请求对象(request),响应对象(response),下一个中间件,参数值及参数名。

如果name是一个数组,回调函数会按照它们声明的顺序,依次注册到回调函数,此时除了此数据中的最后一项,回调函数中的next将会触发下一个注册参数的回调函数,而对于最后一个参数,next则会调用处理当前路由的下一个中间件,此时的处理逻辑和name只是一个字符串一样。

下面的例子实现了当:user存在于路由的路径中时,在req对象中添加了req.user以供后期路由使用:

app.param('user', function(req, res, next, id) {

  // try to get the user details from the User model and attach it to the request object
  User.find(id, function(err, user) {
    if (err) {
      next(err);
    } else if (user) {
      req.user = user;
      next();
    } else {
      next(new Error('failed to load user'));
    }
  });
});

处理Param的回调函数对于包含它们的路由来说是本地的。因此不会被app或者其它的路由继承。

Param回调函数会在在任何匹配了该路由的处理函数前触发,并且一个请求响应周期内只会被触发一次,即使参数匹配了多个路由也是如此。

app.param('id', function (req, res, next, id) {
  console.log('CALLED ONLY ONCE');
  next();
});

app.get('/user/:id', function (req, res, next) {
  console.log('although this matches');
  next();
});

app.get('/user/:id', function (req, res) {
  console.log('and this matches too');
  res.end();
});

对于请求GET /user/42将打印以下语句:

CALLED ONLY ONCE
although this matches
and this matches too
app.param(['id', 'page'], function (req, res, next, value) {
  console.log('CALLED ONLY ONCE with', value);
  next();
});

app.get('/user/:id/:page', function (req, res, next) {
  console.log('although this matches');
  next();
});

app.get('/user/:id/:page', function (req, res) {
  console.log('and this matches too');
  res.end();
});

对于请求 GET /user/42/3,下面语句将被打印

CALLED ONLY ONCE with 42
CALLED ONLY ONCE with 3
although this matches
and this matches too

源文档中此处有一段已经自Express4.10弃用,此处不再做翻译

app.path

返回应用程序的规范路径其是一个字符串。

var app = express()
  , blog = express()
  , blogAdmin = express();

app.use('/blog', blog);
blog.use('/admin', blogAdmin);

console.log(app.path()); // ''
console.log(blog.path()); // '/blog'
console.log(blogAdmin.path()); // '/blog/admin'

对于那些特别复杂加载了特别多app的程序,app.path的行为会变得很复杂,这种情况下使用req.baseUrl来获取路径更好。

app.post(path,callback[,callback])

绑定针对某特定路径的HTTP POST请求到特定的回调函数上。更多信息可查看路由指南

参数

参数1: path 默认值: /(root path)

描述:

路径模式可以是以下类型中的一种:

  • 路径字符串
  • 匹配路径的正则表达式
  • 通配符
  • 上述类型值组成的数组
    点击Path examples可查看更多实际的例子

参数2: callback 默认值: None

描述:

回调函数可以是如下类型中的一种:

  • 一个中间件函数
  • 由逗号隔开的一系列中间件函数
  • 一个由中间件函数构成的数组
  • 上述情况的组合

可以提供多个回调函数,多个回调函数的调用与多个中间件的调用类似,不同之处在于在回调函数中调用next('route')可绕过之后的回调函数。你可以基于此机制来觉得是否需要触发之后的回调函数

routerapp都实现了中间件接口,你可以像使用中间件一样使用它们。

可在此处参考示例

示例
app.post('/', function (req, res) {
  res.send('POST request to homepage');
});

app.put(path,callback[,callback])

绑定针对某特定路径的HTTP POST请求到特定的回调函数上。更多信息可查看路由指南

参数

参数1: path 默认值: /(root path)

描述:

路径模式可以是以下类型中的一种:

  • 路径字符串
  • 匹配路径的正则表达式
  • 通配符
  • 上述类型值组成的数组
    点击Path examples可查看更多实际的例子

参数2: callback 默认值: None

描述:

回调函数可以是如下类型中的一种:

  • 一个中间件函数
  • 由逗号隔开的一系列中间件函数
  • 一个由中间件函数构成的数组
  • 上述情况的组合

可以提供多个回调函数,多个回调函数的调用与多个中间件的调用类似,不同之处在于在回调函数中调用next('route')可绕过之后的回调函数。你可以基于此机制来觉得是否需要触发之后的回调函数

routerapp都实现了中间件接口,你可以像使用中间件一样使用它们。

可在此处参考示例

示例
app.put('/', function (req, res) {
  res.send('PUT request to homepage');
});

app.render(view,[locals],callback)

通过回调函数返回某个视图对应渲染出的HTML,它接收一个可选的参数,这个参数是一个对象用以像视图传送本地变量。app.render()很像res.render()区别在于它本身不能发送渲染后的视图给客户端。

可以把app.render()看做用于生成视图字符串的实用函数。事实上,res.render()在内部会使用app.render()来渲染视图。

本地变量cache被用来设置启用视图缓存,如果你想要在开发过程中启用,你需要将其设置为true,视图缓存在生产环境中默认被启用。

app.render('email', function(err, html){
  // ...
});

app.render('email', { name: 'Tobi' }, function(err, html){
  // ...
});

app.route(path)

返回单一路由的实例,可以链式的为不同的请求绑定不同的中间件处理函数。使用app.route()可以避免重复的写路由名及由此造成的输入错误。

var app = express();

app.route('/events')
.all(function(req, res, next) {
  // runs for all HTTP verbs first
  // think of it as route specific middleware!
})
.get(function(req, res, next) {
  res.json(...);
})
.post(function(req, res, next) {
  // maybe add a new event...
});

app.set(name,value)

设置setting中的属性name的值为value

前面已经提到过,调用app.set('foo',true)设置布尔值为true与使用app.enable('foo')相同,类似的,调用app.set('foo',false)app.disable('foo')相同。

使用app.get可以获取设定的值。

app.set('title', 'My Site');
app.get('title'); // "My Site"
应用设定

下表列出了app setting的可选项

注意sub-app具有以下特征:

  • 不会继承具有默认值的settings的值,其值必须在sub-app中设置;
  • 会继承没有默认值的值,具体细节可见下表。

例外:

Sub-apps将继承trust proxy的值,尽管它有默认值,这样做是出于向后兼容的目的;Sub-apps 在生产环境中不会继承view cache的值(当 NODE_ENV 设置为 production)。

属性 类型 描述 默认值
case sensitive routing Boolean 启用大小写敏感,当启用时,/Foo/foo是不同的路由,当禁用时,/Foo/foo将被看做一样的,**注意:**Sub-app将继承此值 N/A(undefined)
env String 设置环境模式,请务必在生产环境中设置为production;详见 Production best practices: performance and reliability. process.env.NODE_ENV(Node_ENV环境变量)或如果NODE_ENV没有设置则为development
etag Varied 设置Etag响应头。可选值可参考options table,更多关于Etag可以参考 维基百科--Etag weak
jsonp callback name String 指定默认的JSONP的回调名称 "callback"
json escape Boolean 对来自res.josn,res.josnp以及res.send的JSON响应启用转义,会转义JSON中的<,>,&为Unicode。此设置的目的在于当响应来自HTML的响应时协助缓解某些类型的持续XSS攻击。**注意:**sub-app将继承此值的设置 N/A(undefined)
josn replacer Varied 指定JSON.stringly使用的replacer参数 **注意:**Sub-app将继承此值在setting中的设置 N/A(undefined)
json spaces Varied 指定JSON.stringly使用的space参数,此值被用来设置用于美化缩进的空格数量,注意:Sub-app将继承此值 N/A(undefined)
query parser Varied 设置该值为false将禁用query解析,也可以设置其值为simpleextended 或者一个自定义的查询字符串解析函数。 最简单的query parser是基于Node的原生query parserquerystring ,拓展的query parser基于qs。 自定义的查询字符串解析函数将接收完整的查询字符串,并且必须返回一个有查询名和它们的值组成的对象 "extended"
strict routing Boolean 启用严格路由模式,当启用时,路由将视/foo/foo/为不同的路由。否则设为相同的路由 注意: Sub-app将继承此设置 N/A (undefined)
subdomain offset Number 为了获取子域名需要移除的由点隔开的部分 2
trust proxy Varied 只是应用程序位于前置代理之后,使用X-Forwarded-*请求头来确定客户端的IP地址及连接,X-Forwarded- *标头容易伪造,检测到的IP地址不可靠。 启用后,Express会尝试确定通过前置代理或一系列代理连接的客户端的IP地址,req.ips属性将包含连接的客户端的IP地址组成的数组。要启用它,可以查看trust proxy options table;trust proxy的设置使用了proxy-addr包,可以查看其文档了解更多内容。 注: 尽管包含默认值,sub-apps会继承其值 false(disabled)
views String/Array 供程序视图使用的一个或一组文件夹,如果是一个文件夹,将按照数组值的顺序查找 process.ced() + '/views'
view cache Boolean 启用视图模板汇编缓存,**注:**Sub-apps不会继承此值在生产环境中的设置(当NODE_ENV设置为producetion时为生产环节)。 生产环境上默认为true,否则为undefined
view engine String 默认的视图处理引擎 **注:**Sub-app将继承此值的设置 N/A(undefined)
x-powered-by Boolean 启用X-Powered-By:Express HTTP 头部 true
trust proxy的可用设置值

参考Express behind proxies可获取更多的信息。

类型
Boolean 如果设置为true,客户端的IP地址将被认为是X-Forwarded- *头中最左边的条目,如果设置为false,后端应用被认为直接与互联网连接,并入客户端的IP地址可以从req.connection.remoteAddress中获取,这也是默认的设置。
字符串/逗号分隔的字符串/字符串构成的数组 一个IP地址,subnet或者一组IP地址和一组可信任的子网的组合,下面展示了预配置的子网名称: loopback - 127.0.0.1/8, ::1/128,linklocal - 169.254.0.0/16, fe80::/10,uniquelocal - 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7,你可以用下列方法设置IP地址,指定单一的子网 app.set('trust proxy', 'loopback') ,指定一个子网及地址app.set('trust proxy', 'loopback, 123.123.123.123') ,指定多个子网为CSV,app.set('trust proxy', 'loopback, linklocal, uniquelocal') ,通过数组格式指定多个子网app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']),指定时,将从地址确定过程中排除IP地址或子网,并将离应用程序服务器最近的不可信IP地址确定为客户端的IP地址。
Number 信任从前置代理服务器作为客户端的第nth hop。
Function 自定义代理实现,只有当你知道你在做啥的时候才应该做这一步,实例如下
app.set('trust proxy', function (ip) {
  if (ip === '127.0.0.1' || ip === '123.123.123.123') return true; // trusted IPs
  else return false;
});

etag选项的配置

注意: 这些设置只适用于动态生成的文件而不适用于静态文件,express.static中间件将忽略这些设置。

ETag功能是使用etag库实现的。有关更多信息,可参阅其文档

类型
Boolean 设置为true将允许weak Etag,这是默认的设置,设置为false将禁用Etag
String 设置为strong,将允许strong Etag,设置为weak,将允许weak Etag
Function 自定义代理实现,只有当你知道你在做啥的时候才应该做这一步,实例如下
app.set('etag', function (body, encoding) {
  return generateHash(body, encoding); // consider the function is defined
});

app.use([path],callback[,callback])

为指定的路径指定中间件函数,当请求的路径与之匹配时,中间件函数将会被执行。

参数

参数1: path 默认值: /(root path)

描述:

路径模式可以是以下类型中的一种:

  • 路径字符串
  • 匹配路径的正则表达式
  • 通配符
  • 上述类型值组成的数组
    点击Path examples可查看更多实际的例子

参数2: callback 默认值: None

描述:

回调函数可以是如下类型中的一种:

  • 一个中间件函数
  • 由逗号隔开的一系列中间件函数
  • 一个由中间件函数构成的数组
  • 上述情况的组合

可以提供多个回调函数,多个回调函数的调用与多个中间件的调用类似,不同之处在于在回调函数中调用next('route')可绕过之后的回调函数。你可以基于此机制来觉得是否需要触发之后的回调函数

routerapp都实现了中间件接口,你可以像使用中间件一样使用它们。

可在此处参考示例

描述

将会匹配任何当前路径的子路径,如app.use('/apple',...)将匹配/apple,/apple/images,/apple/images/news等等。

path默认的值是/,如果不设置路径,所用中间件将响应每一个请求。

比如说下述中间件函数将响应每一个请求

app.use(function (req, res, next) {
  console.log('Time: %d', Date.now());
  next();
});

请注意sub-app具有以下特征:

  • 不会继承具有默认值的settings的值,其值必须在sub-app中设置;
  • 会继承没有默认值的值,这些会在下表中明确提到。

中间件函数将会按照顺序执行,因此中间件的顺序非常重要。

// 请求不会超出下面的中间件
app.use(function(req, res, next) {
  res.send('Hello World');
});

// 请求永远不会到达下面的路由
app.get('/', function (req, res) {
  res.send('Welcome');
});

错误处理中间件

错误处理中间件需要接受四个参数,使用时必须传入四个参数以证明当前中间件时错误处理中间件。这四个参数中包含next,即使你用不上next,也需要在参数中包含它,这样才能满足错误处理中间件的函数签名。签名不对当前中间件会被当做普通的中间件使用而失去处理错误的能力。关于错误处理中间件的详细信息可以参考这里

除了必须接受四个参数,错误处理中间件的定义和普通中间件一样,其函数签名固定为(err,req,res,next)

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

路径写法示例

下表是一些有效的路径示例

type: path:
示例:

将匹配以/abcd开头的路径:

app.use('/abcd', function (req, res, next) {
  next();
});

type: 路径通配符

// 下述将匹配以'/abcd'和'/abd'开头的路径
app.use('/abc?d', function (req, res, next) {
  next();
});

// 下例将匹配以'/abcd','/abbcd','/abbbbbcd'等开头的路径
app.use('/ab+cd', function (req, res, next) {
  next();
});

// 下例将匹配以 '/abcd','/abxcd','/abFOOcd','/abbArcd'等开头的路径
app.use('/ab\*cd', function (req, res, next) {
  next();
});

// 下例将匹配 '/ab' 或 '/abcd'开头的路径

app.use('/a(bc)?d', function (req, res, next) {
  next();
});

type: 正则表达式

// 下例将匹配以'/abc','/xyz'开头的路径
app.use(/\/abc|\/xyz/, function (req, res, next) {
  next();
});

type: 数组

// 下例将匹配以'/abcd','/xyza','/lmn','/pqr'开头的路径
app.use(['/abcd', '/xyza', /\/lmn|\/pqr/], function (req, res, next) {
  next();
});

中间件回调函数示例

下面的示例展示了app.use(),app.METHOD(),app.all()中中间件函数的使用方法。

单个中间件

// 可用直接写中间件函数
app.use(function (req, res, next) {
  next();
});

// router也是一个有效的中间件
var router = express.Router();
router.get('/', function (req, res, next) {
  next();
});
app.use(router);

// Express app也是一个有效的中间件
var subApp = express();
subApp.get('/', function (req, res, next) {
  next();
});
app.use(subApp);

一系列的中间件

// 针对同一个路径可用指定多个中间件
var r1 = express.Router();
r1.get('/', function (req, res, next) {
  next();
});

var r2 = express.Router();
r2.get('/', function (req, res, next) {
  next();
});

app.use(r1, r2);

数组

// 可传入一个中间件数组,如果中间件数组是第一个或者唯一的一个参数,则你必须指定中间件匹配的路径

var r1 = express.Router();
r1.get('/', function (req, res, next) {
  next();
});

var r2 = express.Router();
r2.get('/', function (req, res, next) {
  next();
});

app.use('/', [r1, r2]);

组合

// 你可以组合使用上述所有的中间件
function mw1(req, res, next) { next(); }
function mw2(req, res, next) { next(); }

var r1 = express.Router();
r1.get('/', function (req, res, next) { next(); });

var r2 = express.Router();
r2.get('/', function (req, res, next) { next(); });

var subApp = express();
subApp.get('/', function (req, res, next) { next(); });

app.use(mw1, [mw2, r1, r2], subApp);

下面是一些在Express App中使用express.static中间件的示例。

// 把应用目录下的`public`文件夹中的内容作为静态内容的方法
// GET /style.css etc
app.use(express.static(__dirname + '/public'));

// 匹配 以/static开头的路径以提供静态内容
app.use('/static', express.static(__dirname + '/public'));

// 通过把logger中间件放在静态中间件之后使得请求静态内容时不logging
app.use(express.static(__dirname + '/public'));
app.use(logger());

// 从多个目录中提供静态文件,不过优先使用`./public`中的内容
app.use(express.static(__dirname + '/public'));
app.use(express.static(__dirname + '/files'));
app.use(express.static(__dirname + '/uploads'));

Request

req对象代表的是http请求,该对象中包含由请求而来的query,参数,body,HTTP headers等解析而来的属性。按照惯例(此文档也是如此)请求对象会记为req(HTTP响应对象记为res),不过这个名字具体是什么还是依据回调函数中的定义。

比如:

app.get('/user/:id', function(req, res) {
  res.send('user ' + req.params.id);
});

同样,你也可以按照下面这样做:

app.get('/user/:id', function(request, response) {
  response.send('user ' + request.params.id);
});

req对象是node本身的请求对象的增强版,并且支持所有的内置字段和方法.

属性

在Express4中,req.files默认不再存在于req对象中,你需要使用类似busboy,multer,formidable,multiparty,connect-multiparty,pez这样的多部件处理中间件来在通过req.files获取到上传文件的信息。

req.app

此属性指向使用当前中间件的Express application。

如果你遵照以下模式,在一个模块中导出一个中间件然后在主文件中require()这个中间件,则可以在中间件中通过req.app获取到当前的Express实例。

比如:

//index.js
app.get('/viewdirectory', require('./mymiddleware.js'))

//mymiddleware.js
module.exports = function (req, res) {
  res.send('The views directory is ' + req.app.get('views'));
});

req.baseUrl

获取一个路由器实例所匹配的路径。

req.baseUrl属性和app对象的mountpath属性类似,不同的地方在于app. mountpath返回的是匹配的路径模式。

比如:

var greet = express.Router();

greet.get('/jp', function (req, res) {
  console.log(req.baseUrl); // /greet
  res.send('Konichiwa!');
});

app.use('/greet', greet); // load the router on '/greet'

即使你使用的是路径通配符或者一组路径模式来匹配路由,baseUrl属性返回的也是匹配的字符串而非模式本身,如:

app.use(['/gre+t', '/hel{2}o'], greet); // load the router on '/gre+t' and '/hel{2}o'

当请求的路径为/greet/ip时,req.baseUrl的值为/greet,当请求的路径为/hello/jp时,req.baseUrl/hello

req.body

包含从request body中提交而来的键值对形式的数据。默认情况下,req.body的值为undefined,你需要使用如body-parsermulter这类body解析中间件来为其填充内容。

下例展示了如何使用body解析中间件来扩充req.body中的内容:

var app = require('express')();
var bodyParser = require('body-parser');
var multer = require('multer'); // v1.0.5
var upload = multer(); // for parsing multipart/form-data

app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded

app.post('/profile', upload.array(), function (req, res, next) {
  console.log(req.body);
  res.json(req.body);
});

req.cookies

当使用cookie-parser中间件时,此属性是一个由请求中的cookie信息构建的对象。如果请求中没有cookie,其值为{}

// Cookie: name=tj
req.cookies.name
// => "tj"

如果cookie有签名,则需要使用req.signedCookies.

可参照cookie-parser查看更多信息。

req.fresh

用以表征当前请求是否“新鲜”,与req.stale相反。

如果cache-control请求头不是no-cache并且下面的每一项的值都不是true,则它的值为true.

  • if-modified-since请求头是指定的,并且last-modified请求头等于或者早于modified响应头
  • if-none-match请求头为*;
  • if-none-match请求头在被解析为指令后,不匹配etag响应头
req.fresh
// => true

更多信息可查看fresh

req.hostname

用以表征从HTTP header派生出来的主机名。

trust proxy 不等于false时,此属性将使用X-Forwarded-Hostheader中的值,此值可以通过客户端或者代理设置。

// Host: "example.com:3000"
req.hostname
// => "example.com"

req.ip

用以表征请求的远程ip

trust proxy不为false时,此值将取自X-Forwarded-For header.最左侧,此请求头可以被客户端或者代理设置。

req.ip
// => "127.0.0.1"

req.ips

trust proxy 不等于false时,此属性将使用X-Forwarded-Host header中指定的一组IP地址。或者将包含一个空数组,此请求头可以被客户端或者代理设置。

比如说,如果X-Forwarded-Forclient, proxy1, proxy2,req.ips将会是["client", "proxy1", "proxy2"],而proxy2是最下游的。

req.method

包含一个对应于当前请求方法的字符串,如GET,POST,PUT等等。

req.originalUrl

req.url并非原生的Express属性,其继承自Node的http模块。

此属性非常类似于req.url,不同之处在于,它保留了原始请求URL,允许你为内部路由重写req.url。比如说,可以使用app.use()mounting功能来重写req.url以去除挂载点。

// GET /search?q=something
req.originalUrl
// => "/search?q=something"

在中间件函数中,req.originalUrlreq.baseUrlreq.path的组合,如下所示:

app.use('/admin', function(req, res, next) {  // GET 'http://www.example.com/admin/new'
  console.log(req.originalUrl); // '/admin/new'
  console.log(req.baseUrl); // '/admin'
  console.log(req.path); // '/new'
  next();
});

req.params

此属性是一个映射到命名路由参数的对象。比如你的路由为/user/:name,那么可以通过req.params.name获取到name属性的值,此对象默认值为{}

// GET /user/tj
req.params.name
// => "tj"

当你的路由定义使用的是正则表达式时,可以使用req.params[n]来获取捕获组的值,其中n是第n个捕获组,此规则也适用于未命名的通配符与字符串路由(如/file/*)的匹配:

// GET /file/javascripts/jquery.js
req.params[0]
// => "javascripts/jquery.js"

如果你需要对req.params中的键做改变,可以使用app.param处理器,更改仅适用于已经在路径中定义的参数。

在中间件或路由处理函数中对req.params对象所做的任何更改都将被重置。

注: Express会自动依据(decodeURIComponent)解码req.params中的值。

req.path

表示请求URL的路径部分。

// example.com/users?sort=desc
req.path
// => "/users"

从中间件调用时,挂载点不包含在req.path中。有关更多详细信息,请参阅app.use()

req.protocol

表征请求协议的字符串,可能是httphttps

trust proxy 不等于false时,此属性将使用X-Forwarded-Hostheader中的值,此值可以通过客户端或者代理设置。

req.protocol
// => "http"

req.query

此属性通过解析查询字符串而生产的对象。如果没有查询字符串,则为空对象{}

// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"

// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc"

req.query.shoe.color
// => "blue"

req.query.shoe.type
// => "converse"

req.route

返回一个对象,表示当前匹配的路由,比如:

app.get('/user/:id?', function userIdHandler(req, res) {
  console.log(req.route);
  res.send('GET');
});

上述代码片段的输出结果如下:

{ path: '/user/:id?',
  stack:
   [ { handle: [Function: userIdHandler],
       name: 'userIdHandler',
       params: undefined,
       path: undefined,
       keys: [],
       regexp: /^\/?$/i,
       method: 'get' } ],
  methods: { get: true } }

req.secure

表征TLS连接是否建立的布尔值,等同于:

'https' == req.protocol;

req.signedCookies

当使用了cookie-parser中间件时,此属性包含请求带来的签名cookies,普通的cookie可通过req.cookie访问,但是容易被伪造存在恶意攻击的风险,签名cookie实际上并不会使cookie被加密或者隐藏,但是会使得它难以被篡改(用于签名的secret是私密的)。

如果没有签名cookie,此属性的值为{}

// Cookie: user=tobi.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3
req.signedCookies.user
// => "tobi"

更多信息可查看cookie-parser中间件。

req.stale

表征此请求是否是过时,此属性是req.fresh的对立面。更多信息可查看req.fresh.

req.stale
// => true

req.subdomains

表征请求域名的子域名构成的数组。

// Host: "tobi.ferrets.example.com"
req.subdomains
// => ["ferrets", "tobi"]

app settings 中 subdomain offset 的默认值为2,此值可以用来确定子域名的起始位置。可通过app.set()来改变默认值。

req.xhr

是一个布尔值,如果请求头的X-Requested-WithXMLHttpRequest,则为true,表明该请求由一个类似jQuery的客户端库发起。

req.xhr
// => true

Methods

req.accepts(types)

检测指定的内容类型是否被接受,结果基于HTTP中的Accept请求头。此方法返回最佳匹配值,如果都不匹配则返回false,这种情况下,应用的状态码应该为406 Not Acceptable

type的值可以是单个的MIME类型字符串(比如说application/json),可以是拓展名如json,可以是由逗号分隔的列表,或者一个数组。如果是列表或者数组,则返回最佳的匹配值。

// Accept: text/html
req.accepts('html');
// => "html"

// Accept: text/*, application/json
req.accepts('html');
// => "html"
req.accepts('text/html');
// => "text/html"
req.accepts(['json', 'text']);
// => "json"
req.accepts('application/json');
// => "application/json"

// Accept: text/*, application/json
req.accepts('image/png');
req.accepts('png');
// => undefined

// Accept: text/*;q=.5, application/json
req.accepts(['html', 'json']);
// => "json"

查看accepts可了解更多信息。

req.acceptsCharsets(charset[,...])

返回指定的字符集中第一个匹配的字符集,此结果基于Accept-Charset请求头,如果指定的字符集都不被认可则返回false.

查看accepts可了解更多信息。

req.acceptsEncodings(encoding [, ...])

返回指定的编码集中的第一个匹配的编码,结果基于Accept-Encoding请求头,如果都不匹配则返回false.

查看accepts可了解更多信息。

req.acceptsLanguages(lang [, ...])

返回匹配到的第一种语言,结果技术Accept-Language请求头。如果都不匹配则返回false.

查看accepts可了解更多信息。

req.get(field)

获取请求头中对应项的值(大小写不敏感),ReferrerReferer是通用的。

req.get('Content-Type');
// => "text/plain"

req.get('content-type');
// => "text/plain"

req.get('Something');
// => undefined

结果和req.header(filed)一致。

req.is(type)

如果传入请求的“Content-Type”HTTP头字段与type参数指定的MIME类型匹配,则返回匹配的内容类型。否则返回false。

// With Content-Type: text/html; charset=utf-8
req.is('html');       // => 'html'
req.is('text/html');  // => 'text/html'
req.is('text/*');     // => 'text/*'

// When Content-Type is application/json
req.is('json');              // => 'json'
req.is('application/json');  // => 'application/json'
req.is('application/*');     // => 'application/*'

req.is('html');
// => false

可参看type-is了解更多信息。

req.param(name [, defaultValue])

已弃用,请使用req.params,req.body.req.query

req.range(size[, options])

api还不算理解

规范 头解析器。

size参数表示资源的最大值。

options是一个可包含如下值得对象:

属性 类型 描述
combine 布尔值 指示重叠或者相邻的域是否该合并,默认为false,当设置为true时,域将被合并返回就类似本身他们在header中是这样表示的一样

此方法会返回一个数组代表成功或者一个负数表示错误的解析

  • -2 表示格式错误的头部字符串
  • -1 表示不满足范围
// parse header from request
var range = req.range(1000)

// the type of the range
if (range.type === 'bytes') {
  // the ranges
  range.forEach(function (r) {
    // do something with r.start and r.end
  })
}

Response

res对象代表的是当Express app 接收 HTTP 请求时 发送的 HTTP 响应。一般说来此对象会被命名为res(相应请求对象是req),不过其命名实际上是由回调函数中的参数确定的。

比如说你可以这样做:

app.get('/user/:id', function(req, res){
  res.send('user ' + req.params.id);
});

也可以这样做:

app.get('/user/:id', function(request, response){
  response.send('user ' + request.params.id);
});

res对象是Node内置的response对象的加强版并且支持其所有的内置方法

属性

res.app

指向使用该中间件的express实例,在请求对象中 req.appres.app一样。

res.headersSent

是一个布尔值,指示app是否为响应发送了HTTP headers

app.get('/', function (req, res) {
  console.log(res.headersSent); // false
  res.send('OK');
  console.log(res.headersSent); // true
});

res.locals

表示包含在请求生命周期内的本地变量。除了在请求/响应过程中视图渲染时可用,其余和 app.locals 功能一样。

这个属性在暴露请求层面的信息时非常有用,比如 路径名, 授权用户, 用户设置等等。

app.use(function(req, res, next){
  res.locals.user = req.user;
  res.locals.authenticated = ! req.user.anonymous;
  next();
});

方法

res.append(field[,value])

在Express4.11.0以上版本中被支持。

添加指定值到HTTP响应头中,如果header不存在,则依据指定的值创建该头,值可以是字符串或者数组。

注意:在res.append()后面调用res.set()将覆盖前面设置的值。

res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
res.append('Warning', '199 Miscellaneous warning');

res.attachment([filename])

设置HTTP响应头Content-Dispositionattachment,如果指定了filename,则会依据filename的后缀通过res.type()设置Content-Type,同时会设置Content-Disposition “filename=”部分:

res.attachment();
// Content-Disposition: attachment

res.attachment('path/to/logo.png');
// Content-Disposition: attachment; filename="logo.png"
// Content-Type: image/png

res.cookie(name, value [, options])

设置cookie name的值为value。 value的值可以是一个字符串或者转换为json的对象。

options 参数是一个可以拥有以下属性的参数。

Property Type Description
domain String 设置 cookie 的域名,默认为 app 的域名
encode Function 用于编码 cookie 的函数,默认为encodeURIComponent
expires Date GMT 格式的时间,用以表示cookie的过期时间. 如果没有指定,则生成一个session cookie
httpOnly Boolean 标记该cookie只能在服务器端可用
maxAge Number 已ms格式设置的cookie过期时间
path String cookie的路径,默认为/
secure Boolean 指示该cookie只有在https情况下可用
signed Boolean 指示该cookie是否应该被签名
sameSite Boolean or String “SameSite” Set-Cookie 熟悉. 更多信息可参考这里

res.cookie()做的事情其实就是设置了Set-Cookie头中对应的信息。

使用示例如下:

res.cookie('name', 'tobi', { domain: '.example.com', path: '/admin', secure: true });
res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });

encode值是一个函数,用于指定cookie的编码格式,不支持异步函数。

示例如下:

//Default encoding
res.cookie('some_cross_domain_cookie', 'http://mysubdomain.example.com',{domain:'example.com'});
// Result: 'some_cross_domain_cookie=http%3A%2F%2Fmysubdomain.example.com; Domain=example.com; Path=/'

//Custom encoding
res.cookie('some_cross_domain_cookie', 'http://mysubdomain.example.com',{domain:'example.com', encode: String});
// Result: 'some_cross_domain_cookie=http://mysubdomain.example.com; Domain=example.com; Path=/;'

maxAge是一种更为方便的设置过期时间的方法,如下:

res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true });

cookie的值也可以是一个对象,其之后会被bodyParser()序列化为JSON。

res.cookie('cart', { items: [1,2,3] });
res.cookie('cart', { items: [1,2,3] }, { maxAge: 900000 });

当使用cookie-parser中间件时,此方法同样支持签名cookie,只需要设置signedtrue,res.cookie()就会利用传输给cookieParser(secret)的secret对该值进行签名。

res.cookie('name', 'tobi', { signed: true });

之后你可以通过req.signedCookie读取签名的cookie值。

res.clearCookie(name[,options])

清除名称为name的cookie。

浏览器等客户端只会清除到达过期时间的cookie。

res.cookie('name', 'tobi', { path: '/admin' });
res.clearCookie('name', { path: '/admin' });

res.download(path [, filename] [, options] [, fn])

此方法中的参数options只在v4.16.0之后的版本中可用。

已附件在路径中传输文件,一般说来,浏览器会提示用户下载文件,默认情况下 Content-Disposition header 中的 filename= 参数就是路径(此值一般会出现在浏览器的对话框中)。使用filename参数可用覆盖此值。传输出错或者下载完成会调用回调函数fn。此方法使用res.sendFile()来传送文件。

可选的options参数传递给底层的res.sendFile()调用,并采用与其完全相同的参数。

res.download('/report-12345.pdf');

res.download('/report-12345.pdf', 'report.pdf');

res.download('/report-12345.pdf', 'report.pdf', function(err){
  if (err) {
    // Handle error, but keep in mind the response may be partially-sent
    // so check res.headersSent
  } else {
    // decrement a download credit, etc.
  }
});

res.end([data] [, encoding])

用于结束响应过程,此方法来自node核心,http.ServerResponse 模块中的 response.end().用于不传输任何数据快速结束响应,如果你想要传输数据,请使用res.send()或者res.json()

res.end();
res.status(404).end();

res.format(object)

如果请求对象中存在的Accept HTTP头,可触发内容协商,将使用req.accepts()值的权重为选择请求对应的处理器,如果请求的Accept请求头没有被指定,将触发第一个回调函数,当没有匹配值时,服务器会返回406 “Not Acceptable”,或者触发默认的回调函数。

当回调函数被选定时,响应头的Content-Type将会被自动设定,当然在回调函数中你也可以使用res.set() 或者 res.type()来更改此请求头的值。

下例中,当Accept头设置为“application/json” 或 “*/json” 时响应为{ "message": "hey" },(如果Accept头设置为*/*,响应为hey)。

res.format({
  'text/plain': function(){
    res.send('hey');
  },

  'text/html': function(){
    res.send('<p>hey</p>');
  },

  'application/json': function(){
    res.send({ message: 'hey' });
  },

  'default': function() {
    // log the request and respond with 406
    res.status(406).send('Not Acceptable');
  }
});

除了指定规范化的 MIME 类型,还可以使用拓展名来映射,来简化上述语句:

res.format({
  text: function(){
    res.send('hey');
  },

  html: function(){
    res.send('<p>hey</p>');
  },

  json: function(){
    res.send({ message: 'hey' });
  }
});

res.get(field)

依据指定的field,返回指定的HTTP 响应头对应的值,field大小写不敏感。

res.get('Content-Type');
// => "text/plain"

res.json([body])

发送一个JSON响应,此方法将使用正常的内容类型发送响应,参数将通过JSON.stringify()转换为JSON字符串。

参数可以是任何JSON类型,包括对象,数组,字符串,布尔值,数值等等,你也可以使用它转换其它值为JSON,比如说null,undefined(虽然这些类型从技术上来讲不是有效的JSON)。

res.json(null);
res.json({ user: 'tobi' });
res.status(500).json({ error: 'message' });

res.jsonp([body])

使用JSONP发送JSON响应,除了支持JSONP回调,此方法与res.json()相同。

res.jsonp(null);
// => callback(null)

res.jsonp({ user: 'tobi' });
// => callback({ "user": "tobi" })

res.status(500).jsonp({ error: 'message' });
// => callback({ "error": "message" })

默认情况下,JSONP的回调函数名称为callback,可以通过设置jsonp callback name来更换。

以下是JSONP的一些使用示例:

// ?callback=foo
res.jsonp({ user: 'tobi' });
// => foo({ "user": "tobi" })

app.set('jsonp callback name', 'cb');

// ?cb=foo
res.status(500).jsonp({ error: 'message' });
// => foo({ "error": "message" })

res.links(links)

把参数添加到HTTP 响应头 Link 中。

如下例:

res.links({
  next: 'http://api.example.com/users?page=2',
  last: 'http://api.example.com/users?page=5'
});

将得到以下结果;

Link: <http://api.example.com/users?page=2>; rel="next",
      <http://api.example.com/users?page=5>; rel="last"

res.location(path)

设置响应头Location为指定的值:

res.location('/foo/bar');
res.location('http://example.com');
res.location('back');

back具有特殊的含义,它指向请求头中的Referer头的值,如果请求头中Referer没有被指定,响应头中的Location将指向/

如果没有编码,在进行编码后,Express将通过头Location传递指定的URL到浏览器中,这个过程不会有任何验证。
浏览器负责从当前URL或引用UR以及location中指定的URL获取预期的URL,并相应的进行重定向。

res.redirect([status,] path)

依据指定的路径和状态(一个对应于HTTP状态码的正整数)重定向URL,如果没有指定,默认值为302 Found

res.redirect('/foo/bar');
res.redirect('http://example.com');
res.redirect(301, 'http://example.com');
res.redirect('../login');

可以传入一个完整的站点信息重定向到其它的网站

res.redirect('http://google.com');

重定向也可以相对域名所有的root发生,比如说,如果当前位置位于http://example.com/admin/post/new,下述代码将重定向至http://example.com/admin

res.redirect('/admin');

重定向也可以相对于当前的URL,比如下面的例子中将从http://example.com/blog/admin/重定向至http://example.com/blog/admin/post/new

res.redirect('post/new');

如果从http://example.com/blog/admin重定向至post/new将重定向至http://example.com/blog/post/new.

可以把/这个过程理解,这样能让你的思路更为清晰。

如果传入的值为back,将重定向至referer请求头,如果referer不存在则默认为/

res.redirect('back');

res.render(view [, locals] [, callback])

渲染视图并发送渲染得到的html字符串到客户端。可选参数如下:

  • locals:一个定义了视图函数中可用的本地变量组成的对象;
  • callback:一个回调函数。如果提供,该方法返回可能的错误和呈现的字符串,但不会执行自动响应。发生错误时,该方法会在内部调用next(err)

view参数是一个字符串,指向视图文件的位置。此值可以是决定路径也可以是相对views setting的相对路径,如果该路径不包含拓展名则view engine的设置会决定其拓展名,如果包含拓展名将以指定的模板引擎渲染模块(使用require),并将触发对应模块的__express方法来进行渲染。更多信息可见在Express中使用模板引擎

view参数执行文件系统操作,如从磁盘读取文件和评估Node.js模块,因此出于安全考虑,不应包含来自最终用户的输入。

// send the rendered view to the client
res.render('index');

// if a callback is specified, the rendered HTML string has to be sent explicitly
res.render('index', function(err, html) {
  res.send(html);
});

// pass a local variable to the view
res.render('user', { name: 'Tobi' }, function(err, html) {
  // ...
});

res.send([body])

发送Http响应,

body参数可以是Buffer object, a String, an object, or an Array.

如:

res.send(new Buffer('whoop'));
res.send({ some: 'json' });
res.send('<p>some html</p>');
res.status(404).send('Sorry, we cannot find that!');
res.status(500).send({ error: 'something blew up' });

此方法为非流式响应提供了一些自动操作,如自动添加Content-Length响应头,并且自动添加HEAD以及HTTP缓存。

当参数为Buffer对象时,此方法设置Content-Type响应头为application/octet-stream,除非像下面这样预先定义:

res.set('Content-Type', 'text/html');
res.send(new Buffer('<p>some html</p>'));

当参数为String时,会自动设置Content-Type为“text/html”

res.send('<p>some html</p>');

当响应为对象或者数组时,响应值为JSON表示:

res.send({ user: 'tobi' });
res.send([1,2,3]);

res.sendFile(path [, options] [, fn])

res.sendFile(path [, options] [, fn])Express4.8.0之后的版本中被支持。

基于给定的路径传输文件,并依据文件的拓展名设置响应头的Content-Type.除非在options对象中设置了root,否者路径必须为绝对路径。

下表提供了options参数的详细信息

Property Description Default Availability
maxAge 以毫秒格式设置Cache-Control响应头中的max-age值,可以是数值或者数值格式的字符串 0
root 相对文件的Root路径
lastModified 设置Last-Modified响应头的值为该文件在系统中最后被修改的日期,设置为false 可以禁用它 Enabled 4.9.0+
headers 包含HTTP头文件的对象。
dotfiles 是否提供点开头的文件. 可选值有 “allow”, “deny”, “ignore”. “ignore”
acceptRanges 启用或禁用接受范围的请求。 true 4.14+
cacheControl 启用或者禁用设置 Cache-Control 响应头 true 4.14+
immutable 启用或者禁用响应头中的Cache-Controlimmutable指示,如果启用,maxAge应该指示为可用。此指示会在maxAge生命周期内组织客户端进行额外的请求 false 4.16+

当传输完成或者出现错误时会触发回调函数fn(err),如果指定了回调函数并且确实发生了错误,则回调函数必须处理响应过程,可中断响应也可传入控制到下一个route

示例如下:

app.get('/file/:name', function (req, res, next) {

  var options = {
    root: __dirname + '/public/',
    dotfiles: 'deny',
    headers: {
        'x-timestamp': Date.now(),
        'x-sent': true
    }
  };

  var fileName = req.params.name;
  res.sendFile(fileName, options, function (err) {
    if (err) {
      next(err);
    } else {
      console.log('Sent:', fileName);
    }
  });

});

下面的例子展示了使用res.sendFile为服务文件提供精细的控制:

app.get('/user/:uid/photos/:file', function(req, res){
  var uid = req.params.uid
    , file = req.params.file;

  req.user.mayViewFilesFrom(uid, function(yes){
    if (yes) {
      res.sendFile('/uploads/' + uid + '/' + file);
    } else {
      res.status(403).send("Sorry! You can't see that.");
    }
  });
});

更多信息可以查看send

res.sendStatus(statusCode)

设置HTTP响应的状态码为statusCode,并且在响应body中添加它的字符串表示。

res.sendStatus(200); // equivalent to res.status(200).send('OK')
res.sendStatus(403); // equivalent to res.status(403).send('Forbidden')
res.sendStatus(404); // equivalent to res.status(404).send('Not Found')
res.sendStatus(500); // equivalent to res.status(500).send('Internal Server Error')

如果指定了不受支持的状态码,该状态码依旧会被指定,响应信息会是该状态码的字符串表示。

res.sendStatus(2000); // equivalent to res.status(2000).send('2000')

关于状态码的更多信息

res.set(field [, value])

设置响应头对应的fieldvalue,此方法也支持同时设置多个field为对应的值。

res.set('Content-Type', 'text/plain');

res.set({
  'Content-Type': 'text/plain',
  'Content-Length': '123',
  'ETag': '12345'
});

此方法和res.header(field [, value]).功能一致。

res.status(code)

设置响应的HTTP状态码,它可以看做一个可链式调用的response.statusCode

res.status(403).end();
res.status(400).send('Bad Request');
res.status(404).sendFile('/absolute/path/to/404.png');

res.type(type)

设置Content-Type为对应的MIME类型,如果type中包含/,将会设置Content-Type为传入的type

res.type('.html');              // => 'text/html'
res.type('html');               // => 'text/html'
res.type('json');               // => 'application/json'
res.type('application/json');   // => 'application/json'
res.type('png');                // => image/png:

res.vary(field)

如果不存在添加该字段到Vary响应头中。

res.vary('User-Agent').render('docs');

Router

router对象是一个中间件或者路由的独立实例,你可以把它当做迷你程序,只能执行中间件和路由函数。每一个Express程序都有一个内置的app路由。

路由器的行为就像中间件本身一样,所以你可以用它作为app.use()的参数,或者作为另一个路由器的use()方法的参数。

顶层的express对象拥有一个Router()方法可以用于创建一个router对象。

一旦创建了一个路由器对象,就可以像应用程序一样向其添加中间件和HTTP方法路由(例如getputpost等)比如:

// invoked for any requests passed to this router
router.use(function(req, res, next) {
  // .. some logic here .. like any other middleware
  next();
});

// will handle any request that ends in /events
// depends on where the router is "use()'d"
router.get('/events', function(req, res, next) {
  // ..
});

你可以为特定的路径指定路由,从而把对不同路由的处理分隔到不同的文件中。

// only requests to /calendar/* will be sent to our "router"
app.use('/calendar', router);

方法

router.all(path, [callback, ...] callback)

此方法类似标准的router.MEYHOD()方法,不同的地方在于它将匹配所有的http请求。

app.all()方法在处理对某特定的前缀或匹配的特殊路径的所有类型的请求时特别有用。比如说如果你把下述代码放在所有其它路径的定义之前,就会让从此代码之后的所有路由都需要身份验证,并自动加载一个user。这些回调也不必做为终点,loadUser可以用来执行某个任务,然后调用next()来继续匹配之后的路由。

router.all('*', requireAuthentication, loadUser);

上述代码也等同于

router.all('*', requireAuthentication);
router.all('*', loadUser);

下面还有另外一个非常好用的全局函数示例,这个例子和上面那个类似,但是严格限制路径以/api开头

router.all('/api/*', requireAuthentication);

router.METHOD(path, [callback, ...] callback)

依据请求的类型处理http请求,请求类型可以是GET,PUT,POST等等的小写模式。因此,实际的方法是app.get(),app.post(),app.put()等等。点击这里可以查看详细的路由方法清单。

如果没有在router.get()前指定HTTP HEAD对应的方法,将会调用router.get()响应HEAD请求。

你也可以提供多个回调函数,他们会被同等的对待,并且像中间件一样行为,只不过这些回调可能会调用next('route')来绕过剩余的路由回调。您可以使用此机制在路由上执行预处理,并且在不再需要在继续处理时把控制权移交给下一个路由。

下面的例子展示了最简单的路由定义,Express会将路径字符串转换为正则表达式,用以匹配接收到的请求,匹配时不会考虑查询字符串,比如GET /将会匹配GET /?name=tobi,如下所示:

router.get('/', function(req, res){
  res.send('hello world');
});

你同样可以使用正则表达式来进行匹配,这在你有特殊匹配时非常有用,比如说下面的路由将匹配GET /commits/71dbb9cGET /commits/71dbb9c..4c084f9

router.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res){
  var from = req.params[0];
  var to = req.params[1] || 'HEAD';
  res.send('commit range ' + from + '..' + to);
});

router.param(name, callback)

为路由参数添加回调函数,其中name是参数名或参数组成的数组,callback是回调函数。回调函数的参数依次是请求对象(request),响应对象(response),下一个中间件,参数值及参数名。

如果name是一个数组,回调函数会按照它们声明的顺序被注册到其中的每个值。此外除了最后一个声明的参数,回调函数中的next将会触发下一个注册参数的回调函数,而对于最后一个参数,next则会调用处理当前路由的下一个中间件,此时的处理就像name只是一个字符串一样。

比如说当:user存在于路由的路径中时,你可能想映射用户加载逻辑以自动提供req.user给路由或者对参数输入执行验证。

app.param('user', function(req, res, next, id) {

  // try to get the user details from the User model and attach it to the request object
  User.find(id, function(err, user) {
    if (err) {
      next(err);
    } else if (user) {
      req.user = user;
      next();
    } else {
      next(new Error('failed to load user'));
    }
  });
});

Param 回调函数对于它们定义的路由来说是本地的。它们不会被载入的app及router继承。因此,定义在app上的参数回调只会被定义在app路由上的路由参数触发。

所有的参数回调将会在任何匹配该路由的处理函数前触发,并且在一个请求响应周期内只会被触发一次,即使参数匹配了多个路由也是如此。

app.param('id', function (req, res, next, id) {
  console.log('CALLED ONLY ONCE');
  next();
});

app.get('/user/:id', function (req, res, next) {
  console.log('although this matches');
  next();
});

app.get('/user/:id', function (req, res) {
  console.log('and this matches too');
  res.end();
});

对于请求GET /user/42将打印以下语句:

CALLED ONLY ONCE
although this matches
and this matches too
app.param(['id', 'page'], function (req, res, next, value) {
  console.log('CALLED ONLY ONCE with', value);
  next();
});

app.get('/user/:id/:page', function (req, res, next) {
  console.log('although this matches');
  next();
});

app.get('/user/:id/:page', function (req, res) {
  console.log('and this matches too');
  res.end();
});

对于请求 GET /user/42/3,下面语句将被打印

CALLED ONLY ONCE with 42
CALLED ONLY ONCE with 3
although this matches
and this matches too

router.route(path)

返回单一路由的实例,你可以使用不同的可选中间件来处理不同类型的请求。使用route.route()可以避免重复的写路由名及由此造成的输入错误。

var router = express.Router();

router.param('user_id', function(req, res, next, id) {
  // sample user, would actually fetch from DB, etc...
  req.user = {
    id: id,
    name: 'TJ'
  };
  next();
});

router.route('/users/:user_id')
.all(function(req, res, next) {
  // runs for all HTTP verbs first
  // think of it as route specific middleware!
  next();
})
.get(function(req, res, next) {
  res.json(req.user);
})
.put(function(req, res, next) {
  // just an example of maybe updating the user
  req.user.name = req.params.name;
  // save user ... etc
  res.json(req.user);
})
.post(function(req, res, next) {
  next(new Error('not implemented'));
})
.delete(function(req, res, next) {
  next(new Error('not implemented'));
});

这种方法为单一的路径/users/:user_id添加了不同的 HTTP 方法。

router.use([path], [function, ...] function)

为可选的path添加一系列的处理函数,path默认值为/

此方法类似于app.use(),下面是一个简单的例子,可以查看app.use查看更多信息。

中间件就像水暖管一样,有请求时会从第一个匹配的中间件开始逐步往下到所有匹配到的中间件。

var express = require('express');
var app = express();
var router = express.Router();

// simple logger for this router's requests
// all requests to this router will first hit this middleware
router.use(function(req, res, next) {
  console.log('%s %s %s', req.method, req.url, req.path);
  next();
});

// this will only be invoked if the path starts with /bar from the mount point
router.use('/bar', function(req, res, next) {
  // ... maybe some additional /bar logging ...
  next();
});

// always invoked
router.use(function(req, res, next) {
  res.send('Hello World');
});

app.use('/foo', router);

app.listen(3000);

匹配的路径被剥离并且对中间件函数不可见,此特性的意义在于一个匹配的中间件函数可以独立于路径执行。

使用router.use()定义的中间件函数非常重要,它们会依次被触发,比如说第一个中间件函数常常是logger,这样所有的请求都会被log.

var logger = require('morgan');

router.use(logger());
router.use(express.static(__dirname + '/public'));
router.use(function(req, res){
  res.send('Hello');
});

假如现在你想忽略静态文件的log,但是对其它的请求还是想要有log,你只需把express.static()移动到logger中间件上面即可。

router.use(express.static(__dirname + '/public'));
router.use(logger());
router.use(function(req, res){
  res.send('Hello');
});

静态服务器是另一个很好的例子,假如你想给/public更高的权重,你可以按照下面这样进行:

app.use(express.static(__dirname + '/public'));
app.use(express.static(__dirname + '/files'));
app.use(express.static(__dirname + '/uploads'));

router.use()方法还支持命名参数,以便其他路由器的挂载点可以使用命名参数进行预加载。

注意:虽然这些中间件功能是通过一个特定的路由器添加的,但是当它们运行的​​时候是由它们所匹配的路径(而不是路由器)来定义的。因此,如果路由匹配,通过一个路由器添加的中间件可以运行其他路由器。例如,这段代码显示了两个不同的路由器匹配到了同一个路径

var authRouter = express.Router();
var openRouter = express.Router();

authRouter.use(require('./authenticate').basic(usersdb));

authRouter.get('/:user_id/edit', function(req, res, next) { 
  // ... Edit user UI ...  
});
openRouter.get('/', function(req, res, next) { 
  // ... List users ... 
})
openRouter.get('/:user_id', function(req, res, next) { 
  // ... View user ... 
})

app.use('/users', authRouter);
app.use('/users', openRouter);

上面的例子中,虽然authenticate定义在了authRouter上,但是也会执行openRouter路由器关联的中间件,为了避免这种行为,最好不同的路由器不要匹配相同的路径。

运行出错请问怎么回事?

运行下面三个怎么都出错啊?这是怎么回事

E:\nn>npm run build:dev

[email protected] build:dev E:\nn
webpack --progress --colors --watch --config webpack.dev.js

E:\nn\webpack.dev.js:1
(function (exports, require, module, __filename, __dirname) { let webpack = requ
ire("webpack");
^^^

SyntaxError: Block-scoped declarations (let, const, function, class) not yet sup
ported outside strict mode
at exports.runInThisContext (vm.js:53:16)
at Module._compile (module.js:373:25)
at Object.Module._extensions..js (module.js:416:10)
at Module.load (module.js:343:32)
at Function.Module._load (module.js:300:12)
at Module.require (module.js:353:17)
at require (internal/module.js:12:17)
at requireConfig (E:\nn\node_modules\webpack\bin\convert-argv.js:97:18)
at E:\nn\node_modules\webpack\bin\convert-argv.js:104:17
at Array.forEach (native)
npm ERR! Windows_NT 6.1.7601
npm ERR! argv "C:\Program Files\nodejs\node.exe" "C:\Program Files\nodejs\
node_modules\npm\bin\npm-cli.js" "run" "build:dev"
npm ERR! node v4.5.0
npm ERR! npm v2.15.9
npm ERR! code ELIFECYCLE
npm ERR! [email protected] build:dev: webpack --progress --colors --watch --config webpack.dev.js
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] build:dev script 'webpack --progress --col
ors --watch --config webpack.dev.js'.
npm ERR! This is most likely a problem with the rock_cms package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR! webpack --progress --colors --watch --config webpack.dev.js
npm ERR! You can get information on how to open an issue for this project with:
npm ERR! npm bugs rock_cms
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!
npm ERR! npm owner ls rock_cms
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR! E:\nn\npm-debug.log

# Git-深入一点点

本文来自半个月前我在我司内部进行的分享。一直觉得 Git 是一个非常值得深入学习的工具,准备这次内部分享用了很久的时间,不过关于Git的讲解还是有很多不足之处,大家有什么建议,欢迎来本文的githug地址讨论,我们一起把 Git 学得更深一点。

Git是一个CLI(Common line interface),我们与其的交互常常发生在命令行,(当然有时候也会使用GUI,如sourcetree,Github等等),由于我们的使用方式,我们常常会忽略git仓库本身是一个没那么复杂的文件系统,我们输入git命令时其实就是对这个文件系统进行操作。

对 Git 文件系统的定义有一种更专业的说法,「从根本上来讲 Git 是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面」。
…from Git - 关于版本控制

Git做为文件系统长什么样子

找一个空文件夹,执行git init后我们会发现其中会多出一个隐藏文件夹.git,其文件结构如下:

➜ mkdir gitDemo && cd gitDemo && git init && tree -a
Initialized empty Git repository in /Users/zhangwang/Documents/personal/Test/gitDemo/.git/
.
└── .git
    ├── HEAD
    ├── branches
    ├── config
    ├── description
    ├── hooks
    │   ├── applypatch-msg.sample
    │   ├── commit-msg.sample
    │   ├── post-update.sample
    │   ├── pre-applypatch.sample
    │   ├── pre-commit.sample
    │   ├── pre-push.sample
    │   ├── pre-rebase.sample
    │   ├── pre-receive.sample
    │   ├── prepare-commit-msg.sample
    │   └── update.sample
    ├── info
    │   └── exclude
    ├── objects
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags

10 directories, 14 files

几乎 Git 相关的所有操作都和这个文件夹相关,如果你是第一次见到这个文件系统,觉得陌生也很正常,不过读完本文,每一项都会变得清晰了。

我们先想另外一个问题,做为版本控制系统的 Git ,究竟会存储那些内容在上述文件系统中,这些内容又是如何被存储的呢?

「分支」,「commit」,「原始的文件」,「diff」…
…from 热烈的讨论中

好吧,不卖关子,实际上在上述文件系统中 Git 为我们存储了五种对象,这些对象存储在/objects/refs文件夹中。

Git 中存储的五种对象

  • Blobs, which are the most basic data type in Git. Essentially, a blob is just a bunch of bytes; usually a binary representation of a file.

Blobs是Git中最基础的数据类型,一个blob对象就是一堆字节,通常是一个文件的二进制表示

  • Tree objects, which are a bit like directories. Tree objects can contain pointers to blobs and other tree objects.

tree,有点类似于目录,其内容由对其它treeblobs的指向构成;

  • Commit objects, which point to a single tree object, and contain some metadata including the commit author and any parent commits.

commit,指向一个树对象,并包含一些表明作者及父 commit 的元数据

  • Tag objects, which point to a single commit object, and contain some metadata.

Tag,指向一个commit对象,并包含一些元数据

  • References, which are pointers to a single object (usually a commit or tag object).

References,指向一个commit或者tag对象

blobs , tree , commit ,以及声明式的 tag 这四种对象会存储在 .git/object 文件夹中。这些对象的名称是一段40位的哈希值,此名称由其内容依据sha-1算法生成,具体到.git/object文件夹下,会取该hash值的前 2 位为子文件夹名称,剩余 38 位为文件名,这四类对象都是二进制文件,其内容格式依据类型有所不同。下面我们一项项来看:

Blobs

我们都常用git add这个命令,也都听说过,此命令会把文件添加到缓存区(index)。但是有没有想过「把文件添加到缓存区」是一种很奇怪的说法,如果说这个文件我们曾经add过,为什么我们需要在修改过后再次添加到缓存区?

我们确实需要把文件重新添加到缓存区,其实每次修改后的文件,对 git 来说都是一个新文件,每次 add 一个文件,就会添加一个 Blob 对象。

blobs是二进制文件,我们不能直接查看,不过通过 Git 提供的一些更底层的命令如 git show [hash] 或者 git cat-file -p [hash] 我们就可以查看 .git/object 文件夹下任一文件的内容。

➜ git cat-file -p 47ca
abc
456

从上面的内容中就可以看出,blob 对象中仅仅存储了文件的内容,如果我们想要完整还原工作区的内容,我们还需要把这些文件有序组合起来,这就涉及到 Git 中存储的另外一个重要的对象:tree

Tree objects

tree 对象记录了我们的文件结构,更形象的说法是,某个 tree 对象记录了某个文件夹的结构,包含文件以及子文件夹。tree 对象的名称也是一个40位的哈希值,文件名依据内容生成,因此如果一个文件夹中的结构有所改变,在 .git/object/ 中就会出现一个新的 tree object, 一个典型的 tree object 的内容如下:

➜ git ls-tree bb4a8638f1431e9832cfe149d7f32f31ebaa77ef
100644 blob 4be9cb419da86f9cbdc6d2ad4db763999a0b86f2	.gitignore
040000 tree dccea6a66df035ac506ab8ca6d2735f9b64f66c1	01_introduction_to_algorithms
040000 tree 363813a5406b072ec65867c6189e6894b152a7e5	02_selection_sort
040000 tree 5efc07910021b8a2de0291218cb1ec2555d06589	03_recursion
040000 tree cc15fd67f464c29495437aa81868be67cd9688b2	04_quicksort
040000 tree 9f09206e367567bf3fe0f9b96f3609eb929840f1	05_hash_tables
040000 tree c8b7b793b0318d13b25098548effde96fc9f1377	06_breadth-first_search
040000 tree 7f111006c8a37eab06a3d8931e83b00463ae0518	07_dijkstras_algorithm
040000 tree 9f6d831e5880716e0eda2d9312ea2689a8cc1439	08_greedy_algorithms
040000 tree 692a9b39721744730ad1b29c052e288aeb89c2ac	09_dynamic_programming
100644 blob 290689b29c24d3406a1ed863077a01393ae2aff3	LICENSE
100644 blob 9017b1121945799e97825f996bc0cefe3422cbaf	README.md
040000 tree ce710aa0b6c23b7f81dbd582aad6f9435988a8b4	images

我们可以看过,tree 中包含两种类型的文件,treeblob,这就把文件有序的组合起来了,如果我们知道了根 tree(可以理解为root文件夹对应的tree),我们就有能力依据此tree还原整个工作区。

可能我们很早就听说过 Git 中的每一个 commit 存储的都是一个「快照」。理解了tree对象,我们就可以较容易的理解「快照」这个词了 ,接下来我们看看 commit object

commit object

我们知道,commit记录了我们的提交历史,存储着提交时的 message,Git 分支中的一个个的节点也是由 commit 构成。一个典型的 commit object 内容如下:

➜ git cat-file -p e655
tree 73aff116086bc78a29fd31ab3fbd7d73913cf958
parent 8da64ce1d90be7e40d6bad5dd1cb1a3c135806a2
author zhangwang <[email protected]> 1521620446 +0800
committer zhangwang <[email protected]> 1521620446 +0800

bc

我们来看看其中每一项的意义:

  • tree:告诉我们当前 commit 对应的根 tree,依据此值我们还原此 commit 对应的工作区;
  • parent:父 commit 的 hash 值,依据此值,我们可以记录提交历史;
  • author:记录着此commit的修改内容由谁修改;
  • committer:记录着当前 commit 由谁提交;
  • ...bc: commit message;

commit 常常位于 Git 分支上,分支往往也是由我们主动添加的,Git 提供了一种名为 References 的对象供我们存储「类分支」资源。

References

References 对象存储在/git/refs/文件夹下,该文件夹结构如下:

➜ tree .git/refs
.git/refs
├── heads
│   ├── master
│   ├── meta-school-za
│   └── ...
├── remotes
│   ├── origin
│   │   ├── ANDROIDBUG-4845
│   │   ├── ActivityCard-za
│   │   ├── ...
├── stash
└── tags

其中 heads 文件夹中的每一个文件其实就对应着一条本地分支,已我们最熟悉的 master 分支为例,我们看看其中的内容:

➜ cat .git/refs/heads/master
603bdb03d7134bbcaf3f84b21c9dbe902cce0e79

有没有发现,文件 master 中的内容看起来好眼熟,它其实是就是一个指针,指向当前分支最新的 commit 对象。所以说 Git 中的分支是非常轻量级的,弄清分支在 Git 内部是这样存储之后,也许我们可以更容易理解类似下面这种图了。

我们再看看 .git/refs 文件夹中其它的内容:

  • .git/refs/remotes 中记录着远程仓库分支的本地映射,其内容只读;
  • .git/refs/stashgit stash 命令相关,后文会详细讲解;
  • .git/refs/tag, 轻量级的tag,与 git tag 命令相关,它也是一个指向某个commit 对象的指针;

tag是一种辅助 Git 做版本控制的对象,上面这种 tag 只是「轻量级tag」 ,此外还存在另一种「声明式tag」,声明式 tag 对象可以存储更多的信息,其存在于 .git/object/下。

Tag objects

上文已经说过 Git 中存在两种 tag

  • lightweight tags,轻量标签很像一个不会改变的分支,其内容是对一个特定提交的引用,这种 tag 存储在.git/refs/tag/文件夹下;
  • annotated tags: 声明式的标签会在object下添加tag object,此种 tag 能记录更多的信息;

两种 tag 的内容差别较大:

# lightweight tags
$ git tag 0.1
# 指向添加tag时的commit hash值
➜ cat 0.1 
e9f249828f3b6d31b895f7bc3588df7abe5cfeee

# annotated tags
$ git tag -a -m 'Tagged1.0' 1.0
➜ git cat-file -p 52c2
object e9f249828f3b6d31b895f7bc3588df7abe5cfeee
type commit
tag 1.0
tagger zhangwang <[email protected]> 1521625083 +0800

Tagged1.0

对比可以发现,声明式的 tag 不仅记录了对应的 commit ,标签号,额外还记录了打标签的人,而且还可以额外添加 tag message(上面的-m 'Tagged1.0')。

值得额外说明的是,默认情况下,git push 命令并不会推送标签到远程仓库服务器上。 想要传送,必须显式地推送标签到共享服务器上。 推送方法为 git push origin [tagname],如果要推送所有的标签,可以使用 git push origin --tags

另外我们也可以在后期给某次 commit 打上标签,如:git tag -a v1.2 9fceb02

至此,我们已经理解了 Git 中的这几类资源,接下来我们看看 Git 命令是如何操作这些资源的。

常见git命令与上述资源间的映射

依据场景,我们可以粗略按照操作的是本地仓库还是远程仓库,把 Git 命令分为本地命令和远程命令,我们先看本地命令,我们本地可供操作的 Git 仓库往往是通过 git clone 或者 git init 生成。我们先看git init做了些什么。

本地命令

git init && git init --bare

git init:在当前文件夹下新建一个本地仓库,在文件系统上表现为在当前文件夹中新增一个 .git 的隐藏文件夹
如:

gitDemo on  master 
➜ ls -a
.     ..    .git  a.txt data

Git 中还存在一种被称为裸仓库的特殊仓库,使用命令 git init --bare 可以初始化一个裸仓库

其目录结构如下:

➜ mkdir gitDemoBear && cd gitDemoBear && git init --bare && tree
Initialized empty Git repository in some/path/gitDemoBear/
.
├── branches
├── hooks
├── info
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

9 directories, 14 files

和普通仓库相比,裸仓库没有工作区,所以并不会存在在裸仓库上直接提交变更的情况,这种仓库会直接把 .git 文件夹中的内容置于初始化的文件夹下。此外

在 config 文件下我们会看到 bare = true 这表明当前仓库是一个裸仓库:

# normal
	bare = false
	logallrefupdates = true

# bare
	bare = true

普通的方法是不能修改裸仓库中的内容的。裸仓库只允许贡献者clone,push,pull

git add

我们都知道 git add [file] 会把文件添加到缓存区。那缓存区本质上是什么呢?

为了理清这个问题,我们先看下图:

image.png

很多地方会说,git 命令操作的是三棵树。三棵树对应的就是上图中的工作区( working directory )、缓存区( Index )、以及 HEAD。

工作区比较好理解,就是可供我们直接修改的区域,HEAD 其实是一个指针,指向最近的一次 commit 对象,这个我们之后会详述。Index 就是我们说的缓存区了,它是下次 commit 涉及到的所有文件的列表。

回到git add [file],这个命令会依次做下面两件事情:

  1. .git/object/ 文件夹中添加修改或者新增文件对应的 blob 对象;
  2. .git/index 文件夹中写入该文件的名称及对应的 blob 对象名称;

通过命令 git ls-files -s 可以查看所有位于.git/index中的文件,如下:

➜ git ls-files -s
100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0	a.txt
100644 aceb8a25000b1c680a1a83c032daff4d800c8b95 0	b.txt
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0	c.txt
100644 0932cc0d381ab943f3618e6125995f643cad4425 0	data/d.txt

其中各项的含义如下:

  • 100644100代表regular file,644代表文件权限
  • 8baef1b4abc478178b004d62031cf7fe6db6f903:blob对象的名称;
  • 0:当前文件的版本,如果出现冲突,我们会看到12
  • data/d.txt: 该文件的完整路径
    Git 还额外提供了一个命令来帮我我们查看文件在这三棵树中的状态,git status

git status

git status有三个作用:

  1. 查看当前所在分支;
  2. 列出已经缓存,未缓存,未追踪的文件(依据上文中的三棵树生成);
  3. 给下一步的操作一定的提示;

一般来说 .git/HEAD 文件中存储着 Git 仓库当前位于的分支:

➜ cat .git/HEAD
ref: refs/heads/mate-school--encodeUri

当我们 git add 某个文件后,git 下一步往往会提示我们commit它。我们接下来看看,commit过程发生了什么。

git commit

对应到文件层面,git commit做了如下几件事情:

  1. 新增tree对象,有多少个修改过的文件夹,就会添加多少个tree对象;
  2. 新增commit对象,其中的的tree指向最顶端的tree,此外还包含一些其它的元信息,commit对象中的内容,上文已经见到过, tree对象中会包含一级目录下的子tree对象及blob对象,由此可构建当前commit的文档快照;;

通过git cat-file -p hash可查看某个对象中的内容
通过git cat-file -t hash可查看某个对象的类型

当我们 git add 某个文件后,下一步我们往往需要执行 git commit 。接下来我们看看,commit过程发生了什么。

git branch

前文我们提到过,分支在本质上仅仅是「指向提交对象的可变指针」,其内容为所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件(一个commit对象),所以分支的创建和销毁都异常高效,创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符),足见 Git 的分支多么轻量级。
此外上文中提到的 HEAD 也可以看做一个指向当前所在的本地分支的特殊指针。
在开发过程中我们会创建很多分支,所有的分支都存在于.git/refs文件夹中。

➜ tree .git/refs
.git/refs
├── heads
│   ├── master
│   ├── meta-school-za
│   └── ...
├── remotes
│   ├── origin
│   │   ├── ANDROIDBUG-4845
│   │   ├── ActivityCard-za
│   │   ├── ...
├── stash
└── tags

➜ cat heads/feature
0cdc9f42882f032c5a556d32ed4d8f9f5af182ed

存在两种分支,本地分支远程分支
本地分支:

对应存储在.git/refs/heads中;
还存在一种叫做「跟踪分支」(也叫「上游分支」)的本地分支,此类分支从一个远程跟踪分支检出,是与远程分支有直接关系的本地分支。 如果在一个跟踪分支上输入 git pull,Git 能自动地识别去那个远程仓库上的那个分支抓取并合并代码。

远程分支:

对应存储在.git/refs/remotes中,可以看做远程仓库的分支在本地的备份,其内容在本地是只读的。

.git/config文件中信息进一步指明了远程分支与本地分支之间的关系:

➜ cat .git/config
...
[remote "origin"]
	url = [email protected]:zhangwang/xxx-lite.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
	remote = origin
	merge = refs/heads/master
[remote "wxa"]
	url = https://git.in.xxx.com/wxa/xxx-lite.git
	fetch = +refs/heads/*:refs/remotes/wxa/*

使用 git branch [newBranchName] 可以创建新分支 newBranchName。不过一个更常见的用法是git checkout -b [newBranchName],此命令在本地创建了分支 newBranchName,并切换到了分支 newBranchName。我们看看git checkout 究竟做了些什么

git checkout

还记得前面我们提到过的HEAD吗?git checkout 实际上就是在操作HEAD
前文中我们提到过一般情况下 .git/HEAD 指向本地仓库当前操作的分支。那只是一般情况,更准确的说法是 .git/HEAD 直接或者间接指向某个 commit 对象。
我们知道每一个 commit 对象都对应着一个快照。可依据其恢复本地的工作目录。 HEAD 指向的 commit 是判断工作区有何更改的基础。
Git 中有一个比较难理解的概念叫做「HEAD分离」,映射到文件层面,其实指的是 .git/HEAD 直接指向某个commit对象。
我们来看git checkout的具体用法

  1. git checkout <file>:
    此命令可以用来清除未缓存的更改,它可以看做是 git checkout HEAD <file> 的简写,
    映射到文件层面,其操作为恢复文件<file>的内容为,HEAD对应的快照时的内容。其不会影响已经缓存的更改的原因在于,其实缓存过的文件就是另外一个文件啦。
    相应的命令还有 git checkout <commit> <file> 可以用来恢复某文件为某个提交时的状态。

  2. git checkout <branch>
    切换分支到 其实际上是修改 .git/HEAD 中的内容为 <branch>,更新工作区内容为 <branch> 所指向的 commit 对象的内容。

➜ cat .git/HEAD
ref: refs/heads/master
  1. git checkout <hash|tag>
    HEAD直接指向一个commit对象,更新工作区内容为该commit对象对应的快照,此时为HEAD分离状态,切换到其它分支或者新建分支git branch -b new-branch|| git checkout branch可以使得HEAD不再分离。
➜ cat .git/HEAD
8e1dbd367283a34a57cb226d23417b95122e5754

在分支上进行了一些操作后,下一步我们要做的就是合并不同分支上的代码了,接下来我们看看git merge 是如何工作的。

git merge

Git 中分支合并有两种算法,快速向前合并三路合并

快速向前合并:

此种情况下,主分支没有改动,因此在基于主分支生成的分支上做的更改,一定不会和主分支上的代码冲突,可以直接合并,在底层相当于修改.refs/heads/ 下主分支的内容为最新的 commit 对象。

image.png

三路合并:

新的feature分支在开发过程中,主分支上的代码也做了修改并添加了新的 commit ,此时合并,需要对比 feature 分支上最新的 commit,feature 分支的 base commit 以及 master 分支上最新的 commit 这三个commit的快照。如果一切顺利,这种合并会生成新的合并 commit ,格式如下:

➜ git cat-file -p 43cfbd24b7812b7cde0ca2799b5e3305bd66a9b3
tree 78f3bc25445be087a08c75ca62ca1708a9d2e33a
parent 51b45f5892f640b8e9b1fec2f91a99e0d855c077
parent 96e66a5b587b074d834f50d6f6b526395b1598e5
author zhangwang <[email protected]> 1521714339 +0800
committer zhangwang <[email protected]> 1521714339 +0800

Merge branch 'feature'

和普通的 commit 对象的区别在于其有两个parent,分别指向被合并的两个commit

不过三路合并往往没有那么顺利,往往会有冲突,此时需要我们解决完冲突后,再合并,三路合并的详细过程如下(为了叙述便利,假设合并发生在 master 分支与 feature 分支之间):

  1. Git 将接收 commit 的哈希值写入文件 .git/MERGE_HEAD。此文件的存在说明 Git 正在做合并操作。(记录合并提交的状态)
  2. Git 查找 base commit:被合并的两个分支的第一个共有祖先 commit
  3. Git 基于 base commitmaster commitfeature commit 创建索引;
  4. Git 基于 base commit — master commitbase commit — feature commit 分别生成 diff,diff 是一个包含文件路径的列表,其中包含添加、移除、修改或冲突等变化;
  5. Git 将 diff 应用到工作区;
  6. Git 将 diff 应用到 index,如果某文件有冲突,其在index中将存在三份;
  7. 如果存在冲突,需要手动解决冲突
  8. git add 以更新 index 被提交, git commit基于此 index 生成新的commit;
  9. 将主分支.git/refs/heads/master中的内容指向第8步中新生成的 commit,至此三路合并完成;

git cherry-pick(待进一步补充)

Git 中的一些命令是以引入的变更即提交这样的概念为中心的,这样一系列的提交,就是一系列的补丁。 这些命令以这样的方式来管理你的分支。
git cherry-pick做的事情是将一个或者多个commit应用到当前commit的顶部,复制commit,会保留对应的二进制文件,但是会修改parent信息。

image.png

在D commit上执行,git cherry-pick F 会将F复制一份到D上,复制的原因在于,F的父commit变了,但是内容又需要保持不可变。

一个常见的工作流如下:

$ git checkout master
$ git checkout -b foo-tmp
$ git cherry-pick C D
# 将foo指向foo-tmp,reset将HEAD指向了某个特殊的commit
$ git checkout foo
$ git reset --hard foo-tmp
$ git branch -D foo-tmp

git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中,本质上就是撤销或者倒转。

有时候我们会想要撤销一些commit,这时候我们就会用到git reset

git reset

git reset 具有以下常见用法:

  1. git reset <file>:从缓存区移除特定文件,但是不会改变工作区的内容
  2. git reset : 重设缓存区,会取消所有文件的缓存
  3. git reset --hard : 重置缓存区和工作区,修改其内容对最新的一次 commit 对应的内容
  4. git reset <commit> : 移动当前分支的末端到指定的commit
  5. git reset --hard <commit>: 重置缓存区和工作区,修改其内容为指定 commit 对应的内容
    相对而言,git reset是一个相对危险的操作,其危险之处在于可能会让本地的修改丢失,可能会让分支历史难以寻找。

我们看看git reset的原理

  1. 移动HEAD所指向的分支的指向:如果你正在 master 分支上工作,执行 git reset 9e5e64a 将会修改 master 让指向 哈希值为 9e5e64acommit object
  • 无论你是怎么使用的git reset,上述过程都会发生,不同用法的区别在于会如何修改工作区及缓存区的内容,如果你用的是 git reset --soft,将仅仅执行上述过程;
  • git reset本质上是撤销了上一次的 git commit 命令。

执行 git commit ,Git 会创建一个新的 commit 对象,并移动 HEAD 所指向的分支指向该commit。 而执行git reset会修改 HEAD 所指向的分支指向 HEAD~(HEAD 的父提交),也就是把该分支的指向修改为原来的指向,此过程不会改变index和工作目录的内容。

  1. 加上 —mixed 会更新索引:git reset --mixedgit reset 效果一致,这是git reset的默认选项,此命令除了会撤销一上次提交外,还会重置index,相当于我们回滚到了 git addgit commit 前的状态。

  2. 添加—hard会修改工作目录中的内容:除了发生上述过程外,还会恢复工作区为 上一个 commit对应的快照的内容,换句话说,是会清空工作区所做的任何更改。

—hard 可以算是 reset 命令唯一的危险用法,使用它会真的销毁数据。

如果你给 git reset 指定了一个路径,git reset 将会跳过第 1 步,将它的作用范围限定为指定的文件或文件夹。 此时分支指向不会移动,不过索引和工作目录的内容则可以完成局部的更改,会只针对这些内容执行上述的第 2、3 步。

git reset file.txt 其实是 git reset --mixed HEAD file.txt 的简写形式,他会修改当前index看起来像 HEAD 对应的commit所依据的索引,因此可以达到取消文件缓存的作用。

git stash

有时候,我们在新分支上的feature开发到一半的时候接到通知需要去修复一个线上的紧急bug🐛,这时候新feature还达不到该提交的程度,命令git stash就派上了用场。

git stash被用来保存当前分支的工作状态,便于再次切换回本分支时恢复。其具体用法如下:

  1. feature分支上执行git stash 或 git stash save,保存当前分支的工作状态;
  2. 切换到其它分支,修复bug,并提交
  3. 切换回feature分支,执行git stash list,列出保存的所有stash,执行 git stash apply,恢复最新的stash到工作区;

也可以覆盖老一些的stash, 用法如git stash apply stash@{2};

关于git stash还有其它一些值得关注的点:

  1. 直接执行git stash会恢复所有之前的文件到工作区,也就是说之前添加到缓存区的文件不会再存在于缓存区,使用 git stash apply --index 命令,则可以恢复工作区和缓存区与之前一样;
  2. 默认情况下,git stash 只会储藏已经在索引中的文件。 使用 git stash —include-untrackedgit stash -u 命令,Git 才会将任何未跟踪的文件添加到stash;
  3. 使用命令git stash pop 命令可以用来应用最新的stash,并立即从stash栈上扔掉它;
  4. 使用命令 git stash —patch ,可触发交互式stash会提示哪些改动想要储藏、哪些改动需要保存在工作目录中。
➜ git stash --patch
diff --git a/src/pages/index/index.mina b/src/pages/index/index.mina
index 6e11ce3..038163c 100644
--- a/src/pages/index/index.mina
+++ b/src/pages/index/index.mina
@@ -326,6 +326,7 @@ Page<Props, Data, {}>({
   },

   onPageScroll({scrollTop}) {
+    // abc
 //    TODO: cover-view 的 fixed top 样式和 pullDownRefresh 有严重冲突。
 //    当 bug 解决时,可以在 TabNav 内使用 <cover-view> 配合滚动实现 iOS 的磁铁效果

Stash this hunk [y,n,q,a,d,/,e,?]?
  1. 使用命令git stash branch <new branch>:构建一个名为new branch的新分支,并将stash中的内容写入该分支

说完了git stash的基本用法,我们来看看,其在底层的实现原理:

上文中我们提到过,Git 操作的是 工作区,缓存区及 HEAD 三棵文件树,我们也知道,commit 中包含的根 tree 对象指向,可以看做文档树的快照。

当我们执行git stash时,实际上我们就是依据工作区,缓存区及HEAD这三棵文件树分别生成commit对象,之后以这三个commit 为 parent 生成新的 commit对象,代表此次stash,并把这个 commit 的 hash值存到.git/refs/stash中。

当我们执行git stash apply时,就可以依据存在 .git/refs/stash 文件中的 commit 对象找到 stash 时工作区,缓存区及HEAD这三棵文件树的状态,进而可以恢复其内容。

gitDemo on  master [$]
➜ cat .git/refs/stash
68e5413895acd479daad0c96815cdb69a3c61bef

gitDemo on  master [$]
➜ git cat-file -p 68e5
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent aade8236c7c291f927f0be3f51ae57f5388eafcc
parent 408ef43aacaf7c255a0c3ea4f82196626a28a39b
parent 6bacdafcddf0685d8e4a0b364ea346ff209a87be
author zhangwang <[email protected]> 1522397172 +0800
committer zhangwang <[email protected]> 1522397172 +0800

WIP on master: aade823 first commit

暂留的疑问?
.git/refs/stash文件中只存有最新的stash commit值,git stash list是如何生效的。

git clean

使用git clean命令可以去除冗余文件或者清理工作目录。 使用git clean -f -d命令可以用来移除工作目录中所有未追踪的文件以及空的子目录。

此命令真的会从工作目录中移除未被追踪的文件。 因此如果你改变主意了,不一定能找回来那些文件的内容。 一个更安全的命令是运行 git stash --all 来移除每一项更新,但是可以从stash栈中找到并恢复它们。。

git clean -n 命令可以告诉我们git clean的结果是什么,如下:

$ git clean -d -n
Would remove test.o
Would remove tmp/

所有在不知道 git clean 命令的后果是什么的时候,不要使用-f,推荐先使用 -n 来看看会有什么后果。

讲到这里,常用的操作本地仓库的命令就基本上说完了,下面我们看看 Git 提供的一些操作远程仓库的命令。

远程命令

如果我们是中途加入某个项目,往往我们的开发会建立在已有的仓库之上。如果使用github或者gitlab,像已有仓库提交代码的常见工作流是

  1. fork一份主仓库的代码到自己的远程仓库;
  2. clone 自己远程仓库代码到本地;
  3. 添加主仓库为本地仓库的远程仓库,git remote add ...,便于之后保持本地仓库与主仓库同步git pull
  4. 在本地分支上完成开发,推送本地分支到个人远程仓库某分支git push
  5. 基于个人远程仓库的分支向主仓库对应分支提交MR,待review通过合并代码到主仓库;

这期间涉及很多远程命令,我们接触到的第一个命令很可能是git clone,我们先看这个命令做了些什么

git clone

git clone的一般用法为git clone <url>
<url>部分支持四种协议:本地协议(Local),HTTP 协议,SSH(Secure Shell)协议及 Git 协议。典型的用法如下:

$ git clone git://github.com/schacon/ticgit.git
Cloning into 'ticgit'...
remote: Reusing existing pack: 1857, done.
remote: Total 1857 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (1857/1857), 374.35 KiB | 193.00 KiB/s, done.
Resolving deltas: 100% (772/772), done.
Checking connectivity... done.

git clone做了以下三件事情

  1. 复制远程仓库objects/文件夹中的内容到本地仓库; (对应Receiving objects);
  2. 为所接收到的文件创建索引(对应Resolving deltas);
  3. 为所有的远程分支创建本地的跟踪分支,存储在.git/refs/remote/xxx/下;
  4. 检测远程分支上当前的活跃分支(.git/HEAD文件中存储的内容);
  5. 在当前分支上执行git pull,保证当前分支和工作区与远程分支一致;

参考 What is git actually doing when it says it is “resolving deltas”? - Stack Overflow

除此之外,git会自动在.git/config文件中写入部分内容,

[remote "origin"]
        url = [email protected]:zhangwang/xxx-lite.git
        fetch = +refs/heads/*:refs/remotes/origin/*

默认情况下会把clone的源仓库取名origin,在.git/config中存储其对应的地址,本地分支与远程分支的对应规则等。

除了git clone另一个与远程仓库建立连接的命令为git remote

git remote

git remote 为我们提供了管理远程仓库的途径。
对远程仓库的管理包括,查看,添加,移除,对远程分支的管理等等。

  1. 查看远程仓库 git remote
$ git remote
origin

# 添加 -v,可查看对应的链接
$ git remote -v
origin	https://github.com/schacon/ticgit (fetch)
origin	https://github.com/schacon/ticgit (push)

# git remote show [remote-name] 可查看更加详细的信息
$ git remote show origin
* remote origin
  Fetch URL: https://github.com/schacon/ticgit
  Push  URL: https://github.com/schacon/ticgit
  HEAD branch: master
  Remote branches:
    master                               tracked
    dev-branch                           tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)
  1. 添加远程仓库 git remote add <shortname> <url>
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin	https://github.com/schacon/ticgit (fetch)
origin	https://github.com/schacon/ticgit (push)
pb	https://github.com/paulboone/ticgit (fetch)
pb	https://github.com/paulboone/ticgit (push)
  1. 远程仓库重命名 git remote rename
$ git remote rename pb paul
$ git remote
origin
paul
  1. 远程仓库的移除 git remote rm <name>
$ git remote rm paul
$ git remote
origin

上述示例代码参照 Git - 远程仓库的使用

本地对远程仓库的记录存在于.git/config文件中,在.git/config中我们可以看到如下格式的内容:

# .git/config
[remote "github"]
	url = https://github.com/zhangwang1990/weixincrawler.git
	fetch = +refs/heads/*:refs/remotes/github/*
[remote "zhangwang"]
	url = https://github.com/zhangwang1990/weixincrawler.git
	fetch = +refs/heads/*:refs/remotes/zhangwang/*
  • [remote] "github":代表远程仓库的名称;
  • url:代表远程仓库的地址
  • fetch:代表远程仓库与本地仓库的对应规则,这里涉及到另外一个 Git 命令,git fetch

git fetch

我们先看git fetch的作用:

  1. git fetch <some remote branch> :同步某个远程分支的改变到本地,会下载本地没有的数据,更新本地数据库,并移动本地对应分支的指向。
  2. git fetch --all会拉取所有的远程分支的更改到本地

我们继续看看git fetch是如何工作的:

# config中的配置
[remote "origin"]
    url = /home/demo/bare-repo/
    fetch = +refs/heads/*:refs/remotes/origin/* #<remote-refs>:<local-refs> 远程的对应本地的存储位置

fetch的格式为fetch = +<src>:<dst>,其中

  • +号是可选的,用来告诉 Git 即使在不能采用「快速向前合并」也要(强制)更新引用;
  • <src>代表远程仓库中分支的位置;
  • <dst> 远程分支对应的本地位置。

我们来看一个git fetch的实例,看看此命令是怎么作用于本地仓库的:

git fetch origin

  1. 会在本地仓库中创建.git/refs/remotes/origin文件夹;
  2. 会创建一个名为.git/FETCH_HEAD的特殊文件,其中记录着远程分支所指向的commit 对象;
  3. 如果我们执行 git fetch origin feature-branch,Git并不会为我们创建一个对应远程分支的本地分支,但是会更新本地对应的远程分支的指向;
  4. 如果我们再执行git checkout feature-branch,git 会基于记录在.git/FETCH_HEA中的内容新建本地分支,并在.git/config中添加如下内容,用以保证本地分支与远程分支future-branch的一致
[branch "feature-branch"]
    remote = origin
    merge = refs/heads/feature-branch

git 每次执行git fetch都会重写.git/FETCH_HEA

上述fetch的格式也能帮我们理解git push的一些用法

git push

我们在本地某分支开发完成之后,会需要推送到远程仓库,这时候我们会执行如下代码:

git push origin featureBranch:featureBranch
此命令会帮我们在远程建立分支featureBranch,之所以要这样做的原因也在于上面定义的fetch模式。
因为引用规格(的格式)是 <src>:<dst>,所以其实会在远程仓库建立分支featureBranch,从这里我们也可以看出,分支确实是非常轻量级的。

此外,如果我们执行 git push origin :topic:,这里我们把 <src>留空,这意味着把远程版本库的 topic 分支定义为空值,也就说会删除对应的远程分支。

回到git push,我们从资源的角度看看发生了什么?

  1. 从本地仓库的.git/objects/目录,上传到远程仓库的/objects/下;
  2. 更新远程仓库的refs/heads/master内容,指向本地最新的commit;
  3. 更新文件.git/refs/remotes/delta/master内容,指向最新的commit;

说完git push,我们再来看看 git pull

git pull

此命令的通用格式为 git pull <remote> <branch>
它做了以下几件事情:

  1. git fetch <remote>:下载最新的内容
  2. 查询.git/FETCH_HEAD找到应该合并到的本地分支;
  3. 如果满足要求,没有冲突,执行git merge

git pull 在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。

至此,常用的git命令原理我们都基本讲解完了。如果大家有一些其它想要了解的命令,我们可以再一起探讨,补充。

一些推荐的 git 资料

Home · geeeeeeeeek/git-recipes Wiki · GitHub
gitlet.js
git-from-the-inside-out
A Hacker’s Guide to Git | Wildly Inaccurate
githug

在截取entry的时候获取fileName为undefined

pages.forEach(function (pathname) {
const fileName = pathname.split('\')[1]; //这里应该转译一下 反斜杠取不到 取到为undefined
const conf = {
filename: fileName + '.html',
template: 'src' + pathname + '.html',
inject: 'body',
chunks: ['vendors', 'manifest', fileName]
};
plugins.push(new HtmlWebpackPlugin(conf));
entry[fileName] = ./src/${pathname}.js;
devServer.historyApiFallback.rewrites.push(
{from: ${fileName}, to: /dist/${fileName}.html}
)
});

读《算法图解》— 对算法的一些基本理解

「算法」二字听来高深,常常让人望而却步,而《算法图解》是一本对算法初学者友好的书,此书图文并茂,循序渐进的帮我们理清算法中一些基础概念,还介绍了一些有意思的算法及其用途,以提升读者的兴趣,帮助我们步入算法的大门。本书也许不仅仅是一本讲述概念的书,作者还在潜移默化中培养我们的算法思维,从计算机的角度来看待问题。

原书中的示例使用 Python 编写,不过考虑到目前我日常工作中使用最多的还是JavaScript,为了加深自己对相关概念的理解,笔记中我又用 JS 重写了一些示例。

何为算法

算法其实没有那么高深,它只是一组完成任务的指令。任何代码片段都可以视为算法。
算法又没有那么简单,虽然写出来的完成任务的代码都是算法,但是算法也有好坏之分。

算法为何重要

选用合适的算法能大大缩短运算时间。

关于这点,我们来看一个实际的例子。

问题:
从 1 ~ 100 个数中猜出某个我现在想到的数字,我只会告诉你「大了」,「小了」,「对了」,最快需要几次猜到?

解决这个问题有多种方案,如果你运气好,也许1次就猜对了,运气差也许会需要100次。有一种叫做「二分查找」的算法非常适合解决这类问题。

二分查找

定义:
二分查找是一种算法,其输入是一个有序的元素列表,如果元素包含在列表中,二分查找返回其位置,否则返回 null.

分析:
简单查找模式:从1开始往上猜,又称「傻找」,最多会需要99次猜测。
二分查找:每次猜最大,最小数字的中间值,由此每次可排除一般的可能性,最多只需要7次就可猜对。

一般而言,对于包含n个元素的列表,用二分查找最多需要 ㏒2n步,而简单查找最多需要n步。

代码实现

/* 二分查找 */
function binarySearch(arr = [], item) {
  let low = 0,
    high = arr.length - 1;
  while (low <= high) {
    const mid = parseInt((low + high) / 2);
    const guess = arr[mid];
    if (guess === item) {
      return mid;
    }
    if (guess > item) {
      high = mid - 1;
    }
    if (guess < item) {
      low = mid + 1;
    }
  }
  return null;
}

const myList = [1, 3, 5, 7, 9];
console.log(binarySearch(myList, 3));
console.log(binarySearch(myList, 10));

上面提到简单查找模式最多需要 99 次猜测, 而用二分查找最多需要 ㏒ n步,这其实引出了另外一个算法初学者还不算熟悉但很重要的概念 — 运行时间

算法速度的表征方式 —— 大O表示法

我们都想选用效率最高的算法,以最大限度的减少运行时间或占用空间,换句话说我们想要选用时间复杂度低的算法,大O表示法是一种指明算法的速度有多快的特殊表示法,常常用它来表示时间复杂度。我们继续以上面的例子来说明
比如说我们 1ms 能查看一个元素,下表表现了不同元素个数简单查找和二分查找找到某个元素的时间

image.png

不要误解,大O表示法并非指的是以秒为单位的速度,而是告诉我们运行时间会以何种方式随着运算量的增加而增长,换句话说它指出了算法运行时间的的增速。

例如,假设列表包含 n 个元素。简单查找需要检查每个元素,因此需要执行 n 次操作。使用大O表示法, 这个运行时间为O(n)。O 是 Operation 的简写。

还需要注意的是大O表示法指出的是最糟糕的情况下的运行时间,这种运行时间是一个保证。

一些常见的时间复杂度

算法的时间复杂度往往是以下几种中的一种或者组合,虽然还没有介绍到具体的算法,不过我们可以先就各种时间复杂度留下一个印象。

  • O(log n):也叫对数时间,这样的算法包括二分查找;
  • O(n):也叫线性时间,这样的算法包括简单查找;
  • O(n * log n): 这种算法包括快速排序 —— 一种速度较快的排序算法;
  • O(n²):这样的算法包括选择排序 —— 一种速度较慢的排序算法;
  • O(n!): 这样的算法包括难以解决的旅行商问题 —— 一种非常慢的算法。

算法常常和数据结构挂钩。在介绍数据结构之前,我们需要先理解内存的工作原理,这样有助于我们理解数据结构。

常见的数据结构 — 数组和链表

内存的工作原理

我们可以把计算机的内存想象成有很多抽屉的柜子,每个柜子都有其编号。

image.png

当需要将数据存储到内存时,我们会请求计算机提供存储空间,计算机会分配一个柜子给我们(存储地址),一个柜子只能存放一样东西,当需要存储多项连续的数据时,我们需要以一种特殊的方法来请求存储空间,这就涉及到两种基本的数据结构—数组和链表。

数组和链表的区别

数组

「数组」这个概念我们很熟悉,在我们的平日的开发过程中会经常用到,不过暂时忘记 JavaScript 中的 Array 吧。我们来看看数组的本质。

使用数组意味着所有的存储事项在内存中都是相连的
这样做的优点是,如果我们知道某个存储事项的索引,我们只需要一步就能找到其对应的内容。
但是缺点也显而易见,如果计算机开始为我们的数组分配的是一块只能容纳三项的空间,当需要存储第四项的时候,会需要把所有内容整体移动到新的分配区域。如果新的区域再满了,则需要再整体移动到另外一个更大的空区域。因此有时候在数组中添加新元素是很麻烦的一件事情。针对这个问题常见的解决方案时预留空间,不过预留空间也有两个问题:
* 可能浪费空间;
* 预留的空间可能依旧不够用;

链表

链表中的元素可以存储在任何地方,链表的每个元素都存储了下一个元素的地址,从而使得一系列的内存地址串在一起。

我们可以通过一个更形象的比如来理解链表,可以把链表比作一个寻宝游戏,前往某个地点后,会打开一个宝箱,其中有一张纸条上写着下个地点的位置。

考虑到链表的这些性质,链表在「插入元素」方便颇具优势,存储后只需要修改相关元素的指向内存地址即可。当然伴随而来也有劣势,那就是在需要读取链表的最后一个元素时,必须从第一个元素开始查找直至找到最后一个元素。

链表的劣势恰恰是数组的优势,当需要随机读取元素时,数组的效率很高。(数组中的元素存储的内存地址相邻,可容易计算出任意位置的元素的存储位置)。

链表和数组各种操作的时间复杂度对比

操作 数组 链表
读取 O(1) O(n)
插入 O(n) O(1)
删除 O(n) O(1)

删除只考虑删除,不考虑读取,由于数组支持随机访问,而数组支持随机访问,因此数组用得多。

前面介绍的「二分查找」的前提是查找的内容是有序的,在熟悉了数组和链表后,我们来学习第一种排序算法—选择排序。

选择排序

定义
遍历一个列表,每次找出其中最大的那个值,存入一个新的列表,并从原来的列表中去除该值,直到排完。

时间复杂度
每次遍历的时间复杂度为O(n),需要执行n次,因此总时间为O(n²)。

虽然需要遍历的个数越来越少,平均检查元素数为½*n,但是大O表示法常常忽略常数。

示例代码

function findSmallest(arr) {
  let smallest = arr[0];
  let smallest_index = 0;
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < smallest) {
      smallest = arr[i];
      smallest_index = i;
    }
  }
  return smallest_index;
}

function selectionSort(arr) {
  const newArr = [];
  const length = arr.length;
  for (let i = 0; i < length; i++) {
    const smallestIndex = findSmallest(arr);
    debugger
    newArr.push(arr.splice(smallestIndex, 1)[0]);
    debugger;
  }
  return newArr;
}

console.log(selectionSort([5, 7, 3, 6, 1, 8]));  /* [ 1, 3, 5, 6, 7, 8 ]*/

附:我们可以在VisuAlgo - Sorting (Bubble, Selection, Insertion, Merge, Quick, Counting, Radix)查看选择排序的动态过程。

上面的选择排序用到了循环,递归是很多算法都使用的一种编程方法,甚至不夸张的讲,递归是一种计算机思维。

递归

递归是一种优雅的问题解决方法。其优雅体现在它让解决方案更为清晰,虽然在性能上,有些情况下递归不如循环。

如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。 —- Leigh Caldwell

递归的组成

递归由两部分组成:

  • 基线条件(base case):函数不再调用自己的条件
  • 递归条件(recursive case): 函数调用自己的条件

上面提到,递归可能存在性能问题,想要理解这一点,需要先理解什么是「调用栈」。

「栈」是一种先入后出(FILO)简单的数据结构。「调用栈」是计算机在内部使用的栈。当调用一个函数时,计算机会把函数调用所涉及到的所有变量都存在内存中。如果函数中继续调用函数,计算机会为第二个函数页分配内存并存在第一个函数上方。当第二个函数执行完时,会再回到第一个函数的内存处。

我们再看看「递归调用栈」:

使用递归很方便,但是也要̶出代价:存储详尽的信息需要占用大量的内存。递归中函数会嵌套执行多层函数,每个函数调用都要占用一定的内存,如果栈很高,计算机就需要存储大量函数调用的信息,这就是为什么有的语言会限制递归最多的层数。

如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。

附:栈和堆的区别
尾调用优化

上面提到的 「选择排序」还是太慢,接下来我们基于递归介绍一种业界通用的排序方法 —— 快速排序。

在正式讲解「快速排序」之前,我们先介绍一下「分而治之」这种方法。
「分而治之 」(divide and conquer D&C )是一种著名的递归式问题解决方法。只能解决一种问题的算法毕竟作用有限,D&C 可能帮我们找到解决问题的思路。

分而治之

运用 D & C 解决问题的过程包括两个步骤:

  1. 找出基线条件,这种条件必须尽可能简单;
  2. 不断将问题分解,直到符合基线条件;(每次递归调用都必须缩小问题的规模)

image.png

我们以快速排序为例来看看,如何运用分而治之的思维。

快速排序

  1. 选择基准值
  2. 将数组分为两个子数组,小于基准值的元素和大于基准值的元素
  3. 对两个子数组再次运用快速排序

代码实现

const quickSort = (array) => {
  if (array.length < 2) {
    return array;
  }
  const pivot = array[0];
  const keysAreLessPivot = array.slice(1).filter(key => key <= pivot);
  const keysAreMorePivot = array.slice(1).filter(key => key > pivot);
  return [...quickSort(keysAreLessPivot), pivot, ...quickSort(keysAreMorePivot)];
};

console.log(quickSort([10, 5, 2, 3])); // [2, 3, 5, 10]

在最糟情况下,栈长为 O(n),而在最佳情况下,栈长为O(log n)
快速排序的平均时间复杂度为 nlog(n),比选择排序快多了O(n²)

常见的数据结构 —— 散列表

在编程语言中,存在另外一种和数组不同的复杂数据结构,比如JavaScript中的对象,或 Python 中的 字典。对应到计算机的存储上,它们可能可以对应为 散列表。

要理解散列表,我们需要先看看散列函数。

散列函数

散列函数是一种将输入映射到数字,且满足以下条件的函数:

  • 相同的输入会得到相同的输出;
  • 不同的输入会映射到不同的数字

散列函数准确的指出了数据的存储位置,能做到这个是因为:

  • 散列函数总是将同样的输入映射到相同的索引;
  • 散列函数将不同的输入映射到不同的索引;
  • 散列函数知道数组有多大,只返回有效的索引;

我们可以把散列表定义为::使用散列函数和数组创建了一种数据结构。::

散列表也被称为 「散列映射」,「映射」,「字典」和「关联数组」。

但是就像上面提到的,需要连续内存位置的数组存储空间毕竟有限,散列表中如果两个键映射到了同一个位置,一般做法是在这个位置上存储一个链表。

散列表的用途

由于散列函数的存在,散列表中数据的查找变得非常快,散列函数值唯一,本身就是一种映射,基于这些,散列表可用作以下用途:

  • 模拟映射关系
  • 防止重复
  • 缓存/记住数据

使用散列表页存在一些注意事项:

  1. 散列函数很重要,理想的情况下,散列函数将键均匀地映射到散列表的不同位置;
  2. 如果散列表存储的链表很长,散列表的速度将急剧下降;

散列表的时间复杂度

理想情况下 散列表的操作为O(1),但是最糟情况下,所有操作的运行时间为O(n)

操作 散列表(平均情况) 散列表最差情况 数组 链表
查找 O(1) O(n) O(1) O(n)
插入 O(1) O(n) O(n) O(1)
删除 O(1) O(n) O(n) O(1)

在平均情况下,散列表的查找速度和数组一样快,而插入和删除速度和链表一样快,因此它兼具二者的优点。但是在最糟的情况下,散列表的各项操作速度都很慢。

要想让散列表尽可能的快,需要避免冲突。
想要避免冲突,需要满足以下两个条件:

  • 较低的填充因子;
  • 良好的散列函数;

填装因子 = 散列表包含的元素总数 / 位置总数

如果填装因子大于1 意味着商品数量超过数组的位置数。一旦填装因子开始增大,就需要在散列表中添加位置,这被称为调整长度。

一个不错的经验是,当填装因子大于 0.7 的时候,就调整散列表的长度。

良好的散列函数

良好的散列函数让数组中的值呈均匀分布,不过我们不用担心该如何才能构造好的散列函数,著名的SHA 函数,就可用作散列函数。

在大多数编程语言中,散列表都有内置的实现。下面我们看看散列表的用途。

广度优先搜索

广度优先算法是图算法的一种,可用来找出两样东西之间的最短距离。在介绍这种算法之前,我们先看看什么叫图。

什么是图

图由节点(node)和边(edge)组成,它模拟一组连接。一个节点可能与众多节点直接相连,这些节点被称为邻居。有向图指的是节点之间单向连接,无向图指的是节点直接双向连接。

在编程语言中,我们可以用散列表来抽象表示图

image.png

广度优先搜索解决的问题

广度优先搜索用以回答两类问题:

  • 从节点A出发,有前往节点B的路径吗?
  • 从节点A出发,到节点B的哪条路径最短?

我们还是举例来说明该如何运用 广度优先搜索。

芒果销售商问题
如果你想从你的朋友或者有朋友的朋友中找到一位芒果销售商,涉及关系最短的路径是什么呢?

分析:查看自己的朋友中,是否有芒果销售商,如果没有则检查朋友的朋友,在最近的那一层检查完之前不去检查下一层。

编码实现

const graph = {};
graph.you = ['alice', 'bob', 'claire'];
graph.bob = ['anuj', 'peggy'];
graph.alice = ['peggy'];
graph.claire = ['thom', 'jonny'];
graph.anuj = [];
graph.peggy = [];
graph.thom = [];

const isSeller = name => name[name.length - 1] === 'm';

const search = (name, graph) => {
  const iter = (waited, visited) => {
    if (waited.length === 0) {
      return false;
    }
    const [current, ...rest] = waited;
    if (visited.has(current)) {
      return iter(rest, visited);
    }
    if (isSeller(current)) {
      console.log(`${current} is a mango seller!`);
      return true;
    }
    visited.add(current);
    const personFriends = graph[current];
    return iter([...rest, ...personFriends], visited);
  };
  return iter(graph[name], new Set());
};

search('you');

上面的编码中涉及到了一种新的数据结构 —— 队列。

队列
队列的工作原理和现实生活中的队列完全相同,可类比为在公交车前排队,队列只支持两种操作:入队 和 出队。
队列是一种先进先出的(FIFO)数据结构。

散列表模拟图
散列表是一种用来模拟图的数据结构

广度优先算法的时间复杂度

广度优先算法的时间复杂度为 O(V+E) 其中V为顶点数,E为边数。

提到图就不得不说一种特殊的图 —— 树。

image.png

树其实可以看做是所有的边都只能往下指的图。树的用途非常广,还有很多种分支,更多信息可参考

狄克斯特拉算法

广度优先搜索只能找出最短的路径,但是它却并不一定是最快的路径,想要得到最快的路径需要用到狄克斯特拉算法(Dijkstra’s algorithm)

在狄克斯特拉算法算法中,每段路径存在权重,狄克斯特拉算法算法的作用是找出的是总权重最小的路径。

平时我们使用地图软件导航到某个我们不熟悉的地点时,地图软件往往会给我们指出多条路线。直达但是绕圈的公交并不一定会比需要换乘的地铁快。

狄克斯特拉算法的使用步骤如下:

  1. 画一张表,列出所有节点,并标注出从当前出发可到达节点的值,找出其中最便宜(可最快到达)的节点供第二步使用;
  2. 更新该节点的到达其所有邻居节点的时间,依据结构修改上面列出的表,检查是否有前往它们的更短路径,如果有,更新其开销,并更新其父节点;
  3. 重复这个过程,直到对图中的节点都这么做了;
  4. 统计最终的表格,找出最短的路径;

狄克斯特拉算法涉及到的这种拥有权重的图被称为 「加权图」
不存在权限的图被称为「非加权图」。
image.png

狄克斯特拉算法算法只适用于有向无环图。
不能将狄克斯特拉算法算法用于负权边的情况。

编码实现

// the graph
const graph = {};
graph.start = {};
graph.start.a = 6;
graph.start.b = 2;

graph.a = {};
graph.a.fin = 1;

graph.b = {};
graph.b.a = 3;
graph.b.fin = 5;

graph.fin = {};

// The costs table
const costs = {};
costs.a = 6;
costs.b = 2;
costs.fin = Infinity;

// the parents table
const parents = {};
parents.a = 'start';
parents.b = 'start';
parents.fin = null;

let processed = [];


const findLowestCostNode = (itCosts) => {
  let lowestCost = Infinity;
  let lowestCostNode = null;

  Object.keys(itCosts).forEach((node) => {
    const cost = itCosts[node];
    // If it's the lowest cost so far and hasn't been processed yet...
    if (cost < lowestCost && (processed.indexOf(node) === -1)) {
      // ... set it as the new lowest-cost node.
      lowestCost = cost;
      lowestCostNode = node;
    }
  });
  return lowestCostNode;
};

let node = findLowestCostNode(costs);

while (node !== null) {
  const cost = costs[node];
  // Go through all the neighbors of this node
  const neighbors = graph[node];
  Object.keys(neighbors).forEach((n) => {
    const newCost = cost + neighbors[n];
    // If it's cheaper to get to this neighbor by going through this node
    if (costs[n] > newCost) {
      // ... update the cost for this node
      costs[n] = newCost;
      // This node becomes the new parent for this neighbor.
      parents[n] = node;
    }
  });

  // Mark the node as processed
  processed = processed.concat(node);

  // Find the next node to process, and loop
  node = findLowestCostNode(costs);
}

console.log('Cost from the start to each node:');
console.log(costs); // { a: 5, b: 2, fin: 6 }

并非所有问题都存在最优解

有些时候,我们很难找出最优解,这时候我们可以采用另外一种计算机思维来解决问题 —— 贪婪算法。
贪婪算法指的是每步都采取最优的做法,每步都选择局部最优解,最终的结果一定不会太差。

完美是优秀的敌人。有时候,你只需要找一个能够大致解决问题的算法,此时贪婪算法正好可派上用场,它们的实现很容易,得到的结果又与正确结果接近。这时候采用的算法又被称作近似算法。

判断近似算法优劣的标准如下:

  • 速度有多快;
  • 得到的近似解和最优解的接近程度;

有的问题也许不存在所谓的最优解决方案,这类问题被称为「NP完全问题」。这类问题以难解著称,如旅行商问题集合覆盖问题。很多非常聪明的人都认为,根本不可能编写出可快速解决这些问题的算法。对这类问题采用近似算法是很好的方案,能合理的识别NP完全问题,可以帮我们不再无结果的问题上浪费太多时间。

如何识别NP完全问题

以下是NP完成问题的一些特点,可以帮我我们识别NP完全问题:

  • 元素较少时,算法的运行速度非常快,但是随着元素的增加,速度会变得非常慢;
  • 涉及 所有组合 的问题通常是NP完成问题;
  • 不能将问题分解为小问题,必须考虑各种可能的情况的问题,可能是NP完全问题;
  • 如果问题涉及到序列且难以解决(旅行商问题中的城市序列),则可能是NP完全问题;
  • 如果问题涉及到集合(如广播台集合)且难以解决,可能是NP完全问题;
  • 如果问题可转换我集合覆盖问题或者旅行商问题,一定就是NP完全问题;

动态规划

还有一种被称作「动态规划」的思维方式可以帮我们解决问题
这种思维方式的核心在于,先解决子问题,再逐步解决大问题。这也导致「动态规划」**适用于子问题都是离散的,即不依赖其他子问题的问题。

动态规划使用小贴士:

  • 每种动态规划解决方案都设计网格;
  • 单元格中的值通常是要优化的值;
  • 每个单元格都是一个子问题

附:什么是动态规划?动态规划的意义是什么?—知乎讨论

K最近邻算法

本书对 KNN 也做了简单的介绍,KNN的合并观点如下

  • KNN 用于分类和回归,需要考虑最近的邻居。
  • 分类就是编组
  • 回归就是预测结果
  • 特征抽离意味着将物品转换为一系列可比较的数字。
  • 能否挑选合适的特征事关KNN算法的成败

进一步的学习建议

读完本书,对算法总算有了一个入门的理解,当然算法还有很多值得深入学习的地方,以下是作者推荐的一些方向。

  • 反向索引:搜索引擎的原理
  • 傅里叶变换:傅里叶变换非常适合用于处理信号,可使用它来压缩音乐;
  • 并行算法:速度提升并非线性的,并行性管理开销,负载均衡
  • MapReduce:是一种流行的分布式算法,可通过流行的开源工具 Apache Hadoop 来使用;
  • 布隆过滤器和 HyperLogLog:面对海量数据,找到键对于的值是一个挑战性的事情,布隆过滤器是一种概率性的数据结构,答案可能不对也可能是正确的;其优点在于占用的存储空间很小
  • SHA 算法(secure hash algorithm)安全散列函数,可用于对比文件,检查密码
  • 局部敏感的散列算法,让攻击者无法通过比较散列值是否类似来破解密码
  • Diffie-Hellman 密钥交换
  • 线性规划:用于在给定约束条件下最大限度的改善制定的指标

书读完了

《算法图解》确实是一本比较好的算法入门书,不枯燥,又能让人有收获就能激励出人的学习欲望,学习算法单阅读本书肯定还是不够的,

Coursera | Online Courses From Top Universities. Join for Free是一门非常好的算法课,如果感兴趣可以一起学学。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.