1. Express简介
EXpress: 精简的、灵活的Node.js Web程序框架,为构建单页、多页及混合的Web程序提供了一系列健壮的功能特性。
- 精简: Express在你的想法和服务器之间充当薄薄的一层,尽量少干预你,在你充分表达自己思想的同时,提供一些有用的东西。
- 灵活: 可扩展。Express提供了一个非常精简的框架,可以根据自己的需求添加Express功能中的不同部分,替换掉不能满足需求的部分。
- Web程序框架: 网站是 Web 程序,网页也是 Web 程序。但 Web 程序的含义不止这些, 它还可以向其他 Web 程序提供功能(还有别的)。一般而言,“程序”是具有功能的, 它不止是内容的静态集合(尽管这也是非常简单的 Web 程序)。
- 单页Web程序: 之前的网站,用户每次访问不同的页面都要发起网络请求,单页Web程序把整个网站都下载到客户端浏览器上。经过初始下载后,用户访问不同页面的速度加快了,几乎不需要或只需要很少的服务端通信。单页程序的开发可以使用 Angular 或 Ember 等流行框架,Express 跟它们都 配合得很好。
- 多页和混合的Web程序: 多页Web程序是更传统的方式。网站上的每个页面都是通过向服务器发起单独的请求得到的。这种方式确实比较传统,但这并不意味着它没有优点,或者说单页程序更好。 只是现在有更多选择了,你可以决定哪些内容应该作为单页程序提供,哪些应该通过不 同的请求提供。“混合”说的就是同时使用这两种方式的网站
2. 先导知识
2.1 开发必备环境条件
- 本地有Node、npm包管理工具
- 有开发工具,例如VS code
2.2 使用Node实现简单的Web服务器
- 当访问localhost:3000端口时,页面会打印Hello world,终端会输出console结果
var http = require('http');
http.createServer(function(req,res){
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello world!');
}).listen(3000);
console.log('Server started on localhost:3000; press Ctrl-C to terminate....');
- 多路由时,页面书写
var http = require('http');
http.createServer(function(req,res){ // 规范化 url,去掉查询字符串、可选的反斜杠,并把它变成小写
var path = req.url.replace(/\/?(?:\?.*)?$/, '').toLowerCase();
switch(path) {
case '':
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Homepage');
break;
case '/about':
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('About');
break;
default:
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
break;
}
}).listen(3000);
console.log('Server started on localhost:3000; press Ctrl-C to terminate....');
- 静态资源服务
var http = require('http'),
fs = require('fs');
function serveStaticFile(res, path, contentType, responseCode) {
if(!responseCode) responseCode = 200;
fs.readFile(__dirname + path, function(err,data) {
if(err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('500 - Internal Error');
} else {
res.writeHead(responseCode, { 'Content-Type': contentType });
res.end(data);
}
});
}
http.createServer(function(req,res){ // 规范化 url,去掉查询字符串、可选的反斜杠,并把它变成小写
var path = req.url.replace(/\/?(?:\?.*)?$/, '') .toLowerCase();
switch(path) {
case '':
serveStaticFile(res, '/public/home.html', 'text/html');
break;
case '/about':
serveStaticFile(res, '/public/about.html', 'text/html');
break;
case '/img/logo.jpg':
serveStaticFile(res, '/public/img/logo.jpg', 'image/jpeg');
break;
default:
serveStaticFile(res, '/public/404.html', 'text/html', 404);
break;
}
}).listen(3000);
console.log('Server started on localhost:3000; press Ctrl-C to terminate....');
3. 省时省力的Express
3.1 安装环境
npm init
生成一个package.json文件npm install --save express
安装express框架
3.2 .gitignore
忽略无需添加到代码库的内容
# ignore packages installed by npm
node_modules
# put any other files you don't want to check in here,
# such as .DS_Store (OSX), *.bak, etc.
3.2 创建入口文件
可以使用index.js,也可以使用与项目相关的[有意义名称].js。
var express = require('express'); // 引入express框架
var app = express();
app.set('port', process.env.PORT || 3000); // 设置执行端口
// 定制 404 页面
app.use(function(req, res){
res.type('text/plain');
res.status(404);
res.send('404 - Not Found');
});
// 定制 500 页面
app.use(function(err, req, res, next){
console.error(err.stack);
res.type('text/plain');
res.status(500);
res.send('500 - Server Error');
});
// 监听端口,执行内容
app.listen(app.get('port'), function(){
console.log( 'Express started on http://localhost:' + app.get('port') + '; press Ctrl-C to terminate.' );
});
3.3 添加路由
- app.get(路径, 函数)
app.get('/', function(req, res){
res.type('text/plain');
res.send('Meadowlark Travel');
});
app.get('/about', function(req, res){
res.type('text/plain');
res.send('About Meadowlark Travel');
});
Node | Express |
---|---|
res.end | res.send |
res.writeHead | res.set、res.status、res.type |
Tip:
我们对定制的404页面和500页面的处理与对普通页面的处理有所区别,普通页面路由使用的是app.get()
,而404和500页面使用的是app.use()
。app.use()是Express添加中间件的一种方法,可以看做处理所有没有匹配路由路径的处理器。在Express中,路由和中间件的添加顺序至关重要。如果将404处理器放在所有路由的上面,则所有路由都不能访问了,访问得到的URL都是404.通配符匹配也要放在下面的位置,避免影响了其他子路由等。例如/about*就要放在/about/contact等下面。
3.4 视图和布局
3.4.1 引入模板框架
模板框架Handlebars,不会对HTML进行抽象,编写的是带特殊标签的HTML,Handlebars 可 以借此插入内容。
npm install --save express3-handlebars
安装视图模板框架
var app = express(); // 设置 handlebars 视图引擎
var handlebars = require('express3-handlebars')
.create({ defaultLayout:'main' });
app.engine('handlebars', handlebars.engine);
app.set('view engine', 'handlebars');
这段代码创建了一个视图引擎,并对 Express 进行了配置,将其作为默认的视图引擎。接 下来创建 views 目录,在其中创建一个子目录 layouts。如果你是一位经验丰富的 Web 开发 人员,可能已经熟悉布局的概念了(有时也被称为“母版页”)。在开发网站时,每个页面 上肯定有一定数量的 HTML 是相同的,或者非常相近。在每个页面上重复写这些代码不仅 非常繁琐,还会导致潜在的维护困境:如果你想在每个页面上做一些修改,那就要修改所 有文件。布局可以解决这个问题,它为网站上的所有页面提供了一个通用的框架。
3.4.2 创建views/layouts/main.handlebars文件
{{!-- 给网站创建模板 --}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<header>
<img class="logo" src="/img/moon.jpg" alt="图片加载失败">
</header>
{{{body}}}
</body>
</html>
入口js文件中.create({ 'defaultLayout': 'main' });意味着除非你特别指明,否则视图都用这个布局。之后不同页面的代码都会插入到body处。
3.4.3 创建views/home.handlebars页面
在layouts同级,可以创建自定义的home页面、about页面、404页面、500页面等
home.handlebars
<h1>Welcome to Meadowlark Travel</h1>
因为html相关内容在main.handlebars布局页面都已经存在了,所以我们需要要写body处想要的内容即可。同时,可以修改一下访问home的路由,从直接send内容改为引入某个具体的文件
// get 方法配置路由
app.get('/', function(req, res) {
// res.type('text/plain')
// res.send('Home Page')
res.render('home')
})
// 定制500页面
app.use(function(err, req, res, next) {
// console.log(err.stack)
// res.type('text/plain')
// res.status(500)
// res.send('500 - Server Error')
console.log(err.stack)
res.status(500)
res.render(500)
})
此时,我们普通页面已经不需要返回状态码和指定内容了,视图引擎默认会返回 text/html 的内容类型和 200 的状态码。在 catch-all 处理器(提供定制的 404 页面)以及 500 处理器中, 我们必须明确设定状态码。
不使用模板的配置:
app.get('/nolay', function(req, res) {
res.render('nolay', {layout: null})
})
3.4.4 视图和静态文件
Express 靠中间件处理静态文件和视图。static 中间件可以将一个或多个目录指派为包含静态资源的目录,其中的资源不经过任何 特殊处理直接发送到客户端。你可以在其中放图片、CSS 文件、客户端 JavaScript 文件之 类的资源。
存放静态文件,我们可以在根目录中创建public文件夹,文件夹内放置一张我们需要的图片信息。
在入口文件.js中修改
// 静态资源目录,应该放在所有路由之前,内部所有文件对外开放
app.use(express.static(__dirname + '/public'));
static 中间件相当于给你想要发送的所有静态文件创建了一个路由,渲染文件并发送给客 户端。接下来我们在 public 下面创建一个子目录 img,并把 logo.png 文件放在其中。 现在我们可以直接指向 /img/logo.png(注意:路径中没有 public,这个目录对客户端来说是 隐形的),static 中间件会返回这个文件,并正确设定内容类型。接下来我们修改一下布 局文件,以便让我们的 logo 出现在所有页面上
main.handlebars中引入图片
<img class="logo" src="/img/moon.jpg" alt="图片加载失败">
3.4.5 视图中动态内容
-
视图并不只是一种传递静态 HTML 的复杂方式(尽管它们当然能做到)。视图真正的强大 之处在于它可以包含动态信息。
-
我们可以创建一个用来保存模块的目录。名称任意,但一般被称为lib(library的缩写),与public、views同级,可以在该目录下创建一个fortune.js文件
lib/fortune.js
// 动态数据
var fortuneCookies = [
"Conquer your fears or they will conquer you.",
"Rivers need springs.",
"Do not fear what you don nott know.",
"You will have a pleasant surprise.",
"Whenever possible, keep it simple."
]
// 处理动态数据的方法,随机返回某个数据
exports.getFortune = function() {
const idx = Math.floor(Math.random() * fortuneCookies.length);
return fortuneCookies[idx]
}
上述的数据我们想要在about页面中进行使用,
- 第一步:需要在入口文件.js中引入这个文件信息
var fortunes = require('./lib/fortune.js')
- 第二步:在入口文件.js对于about页面的路由进行修改,将值传入进去
app.get('/about', function(req, res) {
// res.type('text/plain')
// res.send('About Page')
res.render('about', {fortune: fortunes.getFortune()})
})
- 第三步:修改about页面
<h1>About Meadowlark Travel</h1>
<p>Your fortune for the day:</p>
<blockquote>{{fortune}}</blockquote>
- 第四步: 访问about页面,或刷新页面时我们可以看到效果,每次都会随机展示fortune值
3.4.6 代码结构
上述路由、路由对应页面、布局组件最终形成一个简单的express项目,项目结构如下图
4. 请求和响应对象
- 协议: 协议确定如何传输请求。我们主要是处理http和https。其他常见的协议还有file和ftp。
- 主机名: 主机名标识服务器。运行在本地计算机(localhost)和本地网络的服务器可以简单地表 示,比如用一个单词,或一个数字 IP 地址。在 Internet 环境下,主机名通常以一个顶 级域名(TLD)结尾,比如 .com 或 .net。另外,也许还会有子域名作为主机名的前缀。子域名可以是任何形式的,其中 www 最为常见。子域名通常是可选的。
- 端口: 每一台服务器都有一系列端口号。一些端口号比较特殊,如80端口和443端口。如果省略端口值,那么默认80端口负责HTTP传输,443端口负责HTTPS传输。如果不使用这两个端口的话,那么就需要一个大于1023的端口号。
- 路径: 路径是应用中的页面或其他资源的唯一标识。
- 查询字符串: 查询字符串是一种键值对集合,是可选的。它以?开头,键值对则以&区分开。所有名称和值都必须是URL编码的。
- 信息片段: 信息片段(或散列)被严格限制在浏览器中使用,不会传递到服务器。用它控制单页应用或AJAX富应用越来越普遍。最初,信息片段只是用来让浏览器展现文档中通过锚点标记
<a id="chapter06">
指定的部分
4.1 请求报头
我们在浏览网页时,发送到服务器的并不只是URL。当你访问一个网站时,浏览器会发送很多“隐形”信息。服务器会因此得知优先响应哪种语言的页面,它也会发送“用户代理”信息(浏览器、操作系统和硬件设备)和一些其他信息。可以通过以下代码查看请求头信息
app.get('/headers', function(req,res){
res.set('Content-Type','text/plain');
var s = '';
for(var name in req.headers) s += name + ': ' + req.headers[name] + '\n';
res.send(s);
});
4.2 响应报头
- 正如浏览器以请求报头的形式发送隐藏信息到服务器,当服务器响应时,同样会回传一些 浏览器没必要渲染和显示的信息,通常是元数据和服务器信息。我们已经熟悉内容类型头 信息,它告诉浏览器正在被传输的内容类型(网页、图片、样式表、客户端脚本等)。特 别要注意的是,不管 URL 路径是什么,浏览器都根据内容类型报头处理信息。因此你可 以通过一个叫作 /image.jpg 的路径提供网页,也可以通过一个叫作 /text.html 的路径提供图 片。(这样做并不合情理,这里要讲的重点是路径是抽象的,浏览器只根据内容类型来决 定内容该如何渲染。)除了内容类型之外,报头还会指出响应信息是否被压缩,以及使用 的是哪种编码。响应报头还可以包含关于浏览器对资源缓存时长的提示。
- 响应报头还经常会包含一些关于服务器的 信息,一般会指出服务器的类型,有时甚至会包含操作系统的详细信息。返回服务器信息 存在一个问题,那就是它会给黑客一个可乘之机,从而使站点陷入危险。非常重视安全的 服务器经常忽略此信息,甚至提供虚假信息。
- 禁用 Express 的 X-Powered-By 头信息:
app.disable('x-powered-by');
4.3 互联网媒体类型
内容类型报头信息非常重要,没有它客户端很难判断如何渲染接收到的内容。内容类型报头就是一种互联网媒体类型,由一个类型、一个子类型及可选参数组成。
例如, text/html;charset=UTF-8 说明类型是 text,子类型是 html,字符编码是 UTF-8。互联网编号分配机构维护了一个官方的互联网媒体类型清单(http://www.iana.org/assignments/media- types/media-types.xhtml)。我们常见的 content type、Internet media type 和 MIME type 是可 以互换的。MIME(多用途互联网邮件扩展)是互联网媒体类型的前身,它们大部分是相同的。
4.4 请求体
除请求报头外,请求还有一个主体(就像作为实际内容返回的响应主体一样)。一般 GET 请求没有主体内容,但 POST 请求是有的。POST 请求体最常见的媒体类型是 application/ x-www-form-urlendcoded,是键值对集合的简单编码,用 & 分隔(基本上和查询字符串的 格式一样)。如果 POST 请求需要支持文件上传,则媒体类型是 multipart/form-data,它是 一种更为复杂的格式。最后是 AJAX 请求,它可以使用 application/json。
4.5 参数
对于任何一个请求,参数可以来自查询字符串、会话、请求体或指定的路由参数。在 Node 应用中,请求对象的参数方法会重写所有的参数。
4.6 请求对象
请求对象(通常传递到回调方法,这意味着你可以随意命名,通常命名为 req 或 request)的生命周期始于 Node 的一个核心对象 http.IncomingMessage 的实例。
- req.params:
一个数组,包含命名过的路由参数。 - req.param(name):
返回命名的路由参数,或者GET请求或POST请求参数。 - req.query:
一个对象,包含以键值对存放的查询字符串参数(通常称为GET请求参数)。 - req.body:
一个对象,包含post请求参数。这样命名是因为POST请求参数在REQUEST正文中传递,而不像查询字符串在URL中传递。要使req.body可用,需要中间件能够解析请求正文内容类型 - req.route:
关于当前匹配的路由信息。主要用于路由调试 - req.cookies/req.signedCookies:
一个对象。包含从客户端传递过来的cookie值 - req.headers:
从客户端接收到的请求报头 - req.accepts([types]):
一个简便的方法,用来确定客户端是否接受一个或一组指定的类型(可选类型可以是 单个的 MIME 类型,如 application/json、一个逗号分隔集合或是一个数组)。写公共 API 的人对该方法很感兴趣。假定浏览器默认始终接受 HTML。 - req.ip:
客户端的ip地址 - req.path:
请求路径(不包含协议、主机、端口号或查询字符串) - req.host:
一个简便的方法,用来返回客户端所报告的主机名。这些信息可以用来伪造,所以不应该用户安全目的 - req.xhr:
一个简便属性,如果请求由 Ajax 发起将会返回 true。 - req.protocol:
用于标识请求的协议(http或https) - req.secure:
一个简便属性。如果连接是安全的,则返回true。等同于 req.protocol===‘https’ - req.url/req.originalUrl:
有点用词不当,这些属性返回了路径和查询字符串(它们不包含协议、主机或端口)。 req.url 若是出于内部路由目的,则可以重写,但是 req.orginalUrl 旨在保留原始请求 和查询字符串。 - req.acceptedLanguages:
一个简便方法,用来返回客户端首选的一组(人类的)语言。这些信息是从请求报头中 解析而来的。
5. 响应对象
响应对象(通常传递到回调方法,这意味着你可以随意命名它,通常命名为 res、resp 或 response)的生命周期始于 Node 核心对象 http.ServerResponse 的实例。Express 添加了一 些附加功能。
- res.status(code):
设置HTTP状态码。Express默认为200(成功),所以你可以使用这个方法返回状态码404(页面不存在)或500(服务器内部错误),或任何其他一个状态码。对于重定向(状态码301、302、303、307),有一个更好的方法redirect - res.set(name,value):
设置响应头。这通常不需要手动设置 - res.cookie(name,value,[options]),res.clearCookie(name,[options]):
设置或清除客户端cookie值。需中间件支持 - res.redirect([status],url):
重定向浏览器。默认重定向代码是 302(建立)。通常,你应尽量减少重定向,除非永 久移动一个页面,这种情况应当使用代码 301(永久移动)。 - res.send(body),res.send(status,body):
向客户端发送响应及可选的状态码。Express 的默认内容类型是 text/html。如果你想改 为 text/plain,需要在 res.send 之前调用 res.set(‘Content-Type’,‘text/plain’)。如 果 body 是一个对象或一个数组,响应将会以 JSON 发送(内容类型需要被正确设置), 不过既然你想发送 JSON,我推荐你调用 res.json。 - res.json(json), res.json(status,json):
向客户端发送JSON以及可选的状态码。 - res.jsonp(json), res.jsonp(status,json):
向客户端发送jsonp及可选的状态码。 - res.type(type):
一个简便的方法,用于设置 Content-Type 头信息。基本上相当于 res.set(‘Content- Type’,‘type’),只是如果你提供了一个没有斜杠的字符串,它会试图把其当作文件的 扩展名映射为一个互联网媒体类型。比如,res.type(‘txt’) 会将 Content-Type 设为 text/plain。此功能在有些领域可能会有用(例如自动提供不同的多媒体文件),但是 通常应该避免使用它,以便明确设置正确的互联网媒体类型。 - res.format(object):
这个方法允许你根据接收请求报头发送不同的内容。这是它在 API 中的主要用途.这里有一个非常简单的例子:res.format({‘text/plain’:‘hi there’,‘text/html’:‘hi there’})。 - res.attachment([filename]),res.download(path,[filename],[callback]):
这两种方法会将响应报头 Content-Disposition 设为 attachment,这样浏览器就会选 择下载而不是展现内容。你可以指定 filename 给浏览器作为对用户的提示。用 res. download 可以指定要下载的文件,而 res.attachment 只是设置报头。另外,你还要将 内容发送到客户端。 - res.sendFile(path,[option],[callback]):
这个方法可根据路径读取指定文件并将内容发送到客户端。使用该方法很方便。使用静 态中间件,并将发送到客户端的文件放在公共目录下,这很容易。然而,如果你想根据 条件在相同的 URL 下提供不同的资源,这个方法可以派上用场。 - res.links(links)
设置链接响应报头。这是一个专用的报头,在大多数应用程序中几乎没有用处。 - res.locals,res.render(view,[locals],callback)
res.locals 是一个对象,包含用于渲染视图的默认上下文。res.render 使用配置的模板引擎渲染视图(不能把 res.render 的 locals 参数与 res.locals 混为一谈,上下文 在 res.locals 中会被重写,但在没有被重写的情况下仍然可用)。res.render 的默认响 应代码为 200,使用 res.status 可以指定一个不同的代码。
6. express源码路径说明
- lib/application.js Express 主接口。如果想了解中间件是如何接入的,或视图是如何被渲染的,可以看 这里。
- lib/express.js 这是一个相对较短的 shell,是 lib/application.js 中 Connect 的功能性扩展,它返回一个 函数,可以用 http.createServer 运行 Express 应用。
- lib/request.js 扩展了 Node 的 http.IncomingMessage 对象,提供了一个稳健的请求对象。关于请求对 象属性和方法的所有信息都在这个文件里。
- lib/response.js 扩展了 Node 的 http.ServerReponse 对象,提供响应对象。关于响应对象的所有属性和 方法都在这个文件里。
- lib/router/route.js 提供基础路由支持。尽管路由是应用的核心,但这个文件只有不到 200 行,你会发现它 非常地简单优雅。
7. 处理表单
7.1 简单表单处理
- 处理基础表单
// 必须引入中间件 body-parser
app.post('/process-contact', function(req, res){
console.log('Received contact from ' + req.body.name + ' <' + req.body.email + '>'); // 保存到数据库……
res.redirect(303, '/thank-you');
});
- 更强大的表单处理
// 必须引入中间件 body-parser
app.post('/process-contact', function(req, res){
console.log('Received contact from ' + req.body.name + ' <' + req.body.email + '>');
try { // 保存到数据库……
return res.xhr ?
res.render({ success: true })
:
res.redirect(303, '/thank-you');
} catch(ex) {
return res.xhr ?
res.json({ error: 'Database error.' })
:
res.redirect(303, '/database-error');
}
});
参考书籍-Node与Express开发
如果有用,点个赞呗~
总结用法,希望可以帮助到你,
我是Ably,你无须超越谁,只要超越昨天的自己就好~