Express
是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能。
- 可以方便的处理HTTP请求和响应
- 更加方便的路由功能
- 可以更加方便的链接数据库
- 提供了HTML模板
我们可以通过一个简单的例子来对比一下:
//index-express.js
const express = require('express');
const app = express();
const port = 1234;
app.get('/', (req, res) => res.send('Hello World!'));
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
我们先用一个用Express
建立一个最简单的服务器。上面的代码很简单,我们开启了一个可以访问根路由的服务,并且我们会响应一串字符串 Hello World!
。
如果同样的功能我们用Nodejs需要怎么写呢?
//index-node.js
const http = require('http');
const server = http.createServer();
const port = 1234;
server.on('request', (request, response) => {
if(request.url === '/'){
response.end('Hello World!')
}
});
server.listen(port, () => console.log(`Example app listening on port ${port}!`));
看到上面的代码你可能会觉得只是多了几行代码而已,如果这样想那你就too young to simple了。
我们可以看一下http请求
这个是index-express.js运行时返回的请求头信息
其中我们可以发现Express
会自动帮我们在返回的请求头中加上
"Content-Type":"text/html; charset=utf-8"
这说明其实Express
帮我们做了很多我们在代码中看不到的事情。这也是我们使用类似web框架的原因。
除此之外,Express
还为我们提供了一个中间件的功能,这个功能能让我们更加方便灵活的处理请求和响应。
Express
为我们提供了很多便捷的功能,帮我们节约了很多开发的时间。所以我们有理由去具体研究一下它给我提供的API。
当我们打开ExpressAPI文档的时候我们可以发现,其实Express
的API真的不多。
接下来我们就挨个去查看一下对应的api的作用。
官方文档对express()的定义如下
express函数是用来创建Express程序,是express模块导出的顶级方法。
所以我们一般都是先向下面那样创建express工程。
const express = require('express');
const app = express();
Express
为我们提供了很多内置的中间件(middleware),这也是它的核心特色之一。
Express
内置中间件,基于server-static
用来托管静态服务资源
这个中间件有两个参数:
为了更好的理解这些属性,我们可以来实际使用一下。
创建一个public目录,并且在里面新建一个index.html
mkdir public && cd public && touch index.html
接下来我们编辑一下index.html文件
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<p>这里是public/index.htmlp>
body>
html>
接下来我们来编辑一下逻辑文件。
为了方便管理我们将这次的文件放在一个名为API-express
的文件夹中,并且新建一个名为express-static.ts的文件。
如果你不熟悉typescript语法可以将后缀改为js
// /API-express/express-static.ts
import * as express from "express";
const app = express();
app.use(express.static('public'));
app.listen(1234, () => {
console.log("server up");
});
至此我们需要写的文件就全部完成了。接下来我们来运行一下。
ts-node-dev /API-express/express-static.ts
接下来我们就打开浏览器 输入 http://localhost:1234/index.html
就可以看到下面这样子的页面。
这里重点说两点
针对第二个注意点出现的问题,我们现在来完善一下代码。其实原理很简单就是利用node中的path模块将相对路径变成决定路径!
import * as express from "express";
import * as path from "path";
const app = express();
app.use(express.static(path.resolve(__dirname,'../public')));
app.listen(1234, () => {
console.log("server up");
});
这个时候我们会发现上面的第二个问题就不会出现了。下面我们来看看第三个问题。其实不加public这种行为有点反直觉的。我开始的时候其实也是加的。后来发现不行,就翻阅了文档。才明白为啥。
如果我们仔细翻阅文档后我们还会发现这样子的操作
app.use('/static', express.static('public'))
这个其实可以让我们自己设定一个虚拟目录。这个目录可能不存在,但是express会帮给我们自定映射到实际存在的那个目录。那么我们接着修改代码
import * as express from "express";
import * as path from "path";
const app = express();
app.use('/public',express.static(path.resolve(__dirname,'../public')));
app.listen(1234, () => {
console.log("server up");
});
这时候,我们就会发现加上public就可以访问了~
OK,现在我们应该对express.static()有了一个基本的认识了!接下来让我们深入了解一下他的options选项
作用: 用来指定如何处理以点开头的文件和目录的
有三个可选值
废话不多说,我们实操一把~
在public目录下我们新建一个文件.gitignore,并在里面写上下面这些文字。
../node_modules/
我们在浏览器中访问http://localhost:1234/public/.gitignore
如果不出意外我们会得到下面这样子
emm 出现这样子的界面其实是对的。因为文档上写了dotfiles默认就是ignore
现在我们修改之前的文件
import * as express from "express";
import * as path from "path";
const app = express();
app.use('/public',express.static(path.resolve(__dirname,'../public'),{
dotfiles:'allow',
}));
app.listen(1234, () => {
console.log("server up");
});
当我们再次刷新页面的时候,就会发现浏览器帮我们自动下载了那个.gitignore文件!
emm 同样的,这样子应该也是没啥大毛病!
接下来,我们们改成deny。运行之后我们发现一个意外的情况!
纳尼(ÒωÓױ)!说好的403呢??????
这时候我感觉好想发现了express的bug了!那么真相真的是这样子吗?广告之后。。。(开玩笑 狗头保命)
这个时候作为老司机的我就怀疑会不会这个值需要配合其他的属性才能生效呢?于是乎,我就开始继续查看文档中其他的属性。果然让我发现了一个叫做
fallthrough
的老铁!来来来,让我们揭开她的真面目!
当这个选项的值是true的时候,错误的请求或者404错误会被忽略,直接调用next()函数进入下一个中间件!如果是false的话,会跳到next(err)。
接下来我们来验证一下!
import * as express from "express";
import * as path from "path";
const app = express();
app.use('/public', express.static(path.resolve(__dirname, '../public'), {
dotfiles: 'deny',
}));
app.use((res, req, next) => {
req.send('我是下一个中间件');
});
app.listen(1234, () => {
console.log("server up");
});
由于这个选项默认就是true,所以我们看到express自动进入了下一个中间件!这是对的~
我们得到了这样子的结果。看来这可不是什么bug哦~
当文件后缀缺失的时候,将设置的值中第一个有效的作为其后缀。我们来测试一下!
import * as express from "express";
import * as path from "path";
const app = express();
app.use('/public', express.static(path.resolve(__dirname, '../public'), {
dotfiles: 'deny',
fallthrough: false,
extensions: ['html', 'htm']
}));
app.use((res, req, next) => {
req.send('我是下一个中间件');
});
app.listen(1234, () => {
console.log("server up");
});
这时候我们在浏览器中输入下面的地址
http://localhost:1234/public/index1111
通过上面的图我们可以发现。当我们没有后缀的时候express帮我们自动加上了后缀。可惜的是我们服务器上确实也没有这个文件。
如果我们变一变,将地址换成http://localhost:1234/public/index是不是就可以访问了呢。试试吧~
yes!成功了!好啦我们又了解了一个选项!
当我们访问的是一个文件夹,会查找是否有设定的值对应的文件。
由于其默认值为’index.html’,为了验证,我们将其值设置为false来试一下吧~
import * as express from "express";
import * as path from "path";
const app = express();
app.use('/public', express.static(path.resolve(__dirname, '../public'), {
dotfiles: 'deny',
fallthrough: false,
extensions: ['html', 'htm'],
index:false,
}));
app.use((res, req, next) => {
req.send('我是下一个中间件');
});
app.listen(1234, () => {
console.log("server up");
});
在浏览器中输入 http://localhost:1234/
这时候我们会发现浏览器上出现了下面提示
当pathname为路径的时候会重定向到根目录~
emm 这个就不试了。。字面意思。。
设置响应头的etag值
express总是发送弱类型的ETAG
如果这个值被设为true,那么在max-age对应的时间内,对应的资源被永久缓存。这是一个比较新的属性。尽量不用吧。。主要是兼容性问题。
是否设置响应头的Last-Modified
设置响应头的Cache-Control的max-age
etag|immutable|lastModified|maxAge这几个选项都是和http缓存有关系。
设置响应头。
有三个参数
这个方法是基于body-parser解析JSON类型的
incoming requests
数
据。
他只解析JSON数据,并且只关注那些Content-Type
符合options
中type
设定的请求头。
如果Content-Type
不符合JSON格式的或者发生错的适合,这个中间件会将数据处理为({})空对象。
基于上面的描述我们可以得到这样子的结论:
Content-Type
的字段。下面我们来验证一下。
// express-json.ts
import * as express from "express";
const app = express();
// app.use(express.json());
app.post('/users', (res, rep) => {
console.log(res.body);
res.on('data', (chunk) => {
console.log(chunk.toString());
});
rep.end();
});
app.listen(1234, () => {
console.log("server up");
});
这一次我们使用另外一个工具postman。我们会发送一个post请求。我们先不使用中间件。我们打印一下数据试试。
我们发现当监听request.data事件的时候,我们发现数据正确的传输的过来了。
但是request.body里面没有数据。
那么我们使用一下中间件试试
// express-json.ts
import * as express from "express";
const app = express();
app.use(express.json());
app.post('/users', (res, rep) => {
console.log(res.body);
res.on('data', (chunk) => {
console.log(chunk.toString());
});
rep.end();
});
app.listen(1234, () => {
console.log("server up");
});
我们惊喜的发现貌似有数据了,但是我们发现只有一次输出?这是什么鬼,这个到底是谁输出的呢?
我们把data的监听去掉试试。
// express-json.ts
import * as express from "express";
const app = express();
app.use(express.json());
app.post('/users', (res, rep) => {
console.log(res.body);
// res.on('data', (chunk) => {
// console.log(chunk.toString());
// });
rep.end();
});
app.listen(1234, () => {
console.log("server up");
});
那至少可以证明中间件是正常工作的!
类似的Express
内置中间件还有其他的。比如express.raw/express.text/express.urlencoded。他们功能和express.json类似。都是帮我们预处理请求的数据,极大的提高了我们开发的速度。
创建一个router对象
emm 这个我们待会在学习API-Router的时候再详细了解吧。。。溜了溜了~
app对象表示Express应用程序。通过调用express模块导出的顶级express()函数来创建:
app对象拥有如下几种方法:
当然它还可以通过设置影响应用程序的行为。
local对象的属性是应用程序中的局部变量。
一旦设置好,app.locals属性的值将在应用程序的整个生命周期内持续存在,而res.locals属性仅在请求的生命周期内有效。
这些局部变量可以在应用程序中渲染的模板中被访问到。除此之外,你还可以在request请求中的app.locals属性中访问到这些局部变量。
emm 废话不多说。我们赶快来试试吧!
//API-Application/app-locals.ts
import * as express from "express";
const app = express();
app.locals.title = "My app";
console.log(app.locals.title);
app.use((res, rep) => {
console.log(res.app.locals.title);
rep.end();
});
app.listen(1234, () => {
console.log('server up');
});
这一次我们换一个方式去测试。让我们打开终端,然后输入下面这段命令
curl -v http://localhost:1234
出现两次"My app"。这符合我们的预期!那么我们就继续学习下一个属性吧!
mountpath属性包含子应用程序挂载在其上的一个或多个路径匹配模式。
// API-Application/app-mountpath.ts
import * as express from "express";
const app = express();
const admin = express();
admin.get('/', (req, res) => {
console.log(req.baseUrl);
console.log(admin.mountpath);
res.send('Admin Homepage');
});
app.use('/admin', admin);
app.listen(1234, () => {
console.log('server up');
});
终端中输入
curl -v http://localhost:1234/admin
这给我们的感觉就好像admin对应的app实例对象可以作为app实例对象的子路由。emm,事实上好像也确实如此!
A sub-app is an instance of express that may be used for handling the request to a route.
接下来我们要开始对比一下req.baseUrl和app.mountpath的区别了!文档上是这么说的。
It is similar to the baseUrl property of the req object, except req.baseUrl returns the matched URL path, instead of the matched patterns.
它与req对象的baseUrl属性类似,不同之处在于req.baseUrl返回匹配的URL路径,而不是匹配的模式。
import * as express from "express";
const app = express();
const admin = express();
const secret = express();
secret.get('/', (req, res) => {
console.log(`baseUrl:${req.baseUrl}`);
console.log(`mountpath:${secret.mountpath}`);
res.send("Admin Secret");
});
admin.get('/', (req, res) => {
console.log(`baseUrl:${req.baseUrl}`);
console.log(`mountpath:${admin.mountpath}`);
res.send('Admin Homepage');
});
admin.use('/secr*t', secret);
app.use(['/adm*n', '/manager'], admin);
app.listen(1234, () => {
console.log('server up');
});
终端输入
curl http://localhost:1234/admin/secret
果然如同介绍所说。baseUrl会返回匹配到的路径信息,而mountpath则有点类似于路径的正则!
我们再试着看看多正则的情况
终端输入
curl http://localhost:1234/admin
终端输入
curl http://localhost:1234/manager
虽然他们的路径不一样,但是他们被指向了同一个路由。
If a sub-app is mounted on multiple path patterns, app.mountpath returns the list of patterns it is mounted on, as shown in the following example.
如果将子应用程序安装在多个路径模式上,则app.mountpath返回其安装的模式列表,如以下示例所示。
当一个子应用被挂载到父级应用上的时候,mount事件会被触发。父应用会被作为回到函数的参数传递给子应用。
//API-Aplication/app-mount.ts
import * as express from "express";
const app = express();
const admin = express();
app.locals.title = "parent Title";
admin.on('mount', (parent) => {
console.log("sub Mounted");
console.log(parent.locals.title);
});
app.use('/admin', admin);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wHAFBQcU-1596683551153)(https://s1.ax1x.com/2020/08/06/acQgBj.jpg)]
我们成功的拿到了父应用的局部变量。这样子应该就可以在子应用中使用父应用的局部变量。
emm 具体还有啥作用。。等实际使用中再开发。。
在指定的路径上安装指定的一个或多个中间件函数:当所请求路径的基数与path匹配时,将执行中间件函数。
emm 这个方法我们已经用了很多次了!说他是expres最常用的方法也不为过。express中间件的概念有点像一个队列。请求来的时候会经过很多中间件组成的队列,挨个处理完后返回响应。
将设置名分配给值。您可以存储任何您想要的值,但是某些名称可以用于配置服务器的行为。这些特殊名称列在Setting table中。
当属性值为布尔值得时候,app.set(‘foo’, true)与调用app.enable(‘foo’)相同。类似地,为布尔属性调用app.set(‘foo’, false)与调用app.disable(‘foo’)是一样的。
获取app.set设置的对应的name值
匹配HTTP GET 请求对应的路由路径,并引入一些回调或者中间件。
和这个类似的还有 app.post() | app.put() | app.delete() 等方法。他们的用法大致相同。小伙伴们可以去文档中对应的介绍。
匹配HTTP请求方法的小写形式
和app.METHOD()很类似,不同的是他匹配所有的HTTP动词
一般用来处理一些全局的逻辑,如判断是否登陆等
设置某些文件类型的模板渲染引擎。
设置服务器的监听端口和主机地址。
此方法还有一个回调函数。
给一个值设置为false
判断某个值是否是false
给一个值设置为true
判断某个值是否是true
如果请求的参数中有name值的回调。
这个只会被触发一下。
返回当前应用的路径
在已安装应用程序的复杂情况下,此方法的行为会变得非常复杂:通常最好使用req.baseUrl来获取应用程序的规范路径。
通过回调函数返回视图呈现的HTML。它接受一个可选参数,该参数是一个包含视图局部变量的对象。它类似于res.render(),只是它不能自己将呈现的视图发送给客户端。
返回单个路由的实例,然后可以使用该实例使用可选中间件处理HTTP谓词。使用app.route()可以避免重复的路由名称(从而避免输入错误)
request对象代表的是HTTP请求,并且觉有请求查询字符串、参数、正文、HTTP请求头等属性。在文档中,按照惯例,这个对象被称为req(同样的,HTTP 响应被称为res)
下面我们来了解一下req对象一些属性和方法
当被在中间件中,这个属性保存了一个Express应用程序实例的一个引用。
我们来试一下
// API-request/req-app.ts
import * as express from "express";
const app = express();
app.locals.title = "app`s title";
app.use('/', (req, res) => {
console.log(req.app.locals.title);
res.end();
});
app.listen(1234, () => {
console.log('server up');
});
终端输入
curl http://localhost:1234/
我们发现确实可以访问到app.locals.title。
被挂载的路由实例URL路径
req.baseUrl有点类似于application.mouuntpath,他们的区别是,app.mountpath返回的是匹配的正则。
这个我们之前在学习mountpath已经对比过了。
包含在请求正文中的键值对。默认情况下他是undefined,通常当我们使用正文解析类的中间件(例如 express.json() 或者 express.urlencoded())
emm 这个其实我们上面也举过例子,我们就不重复了,next one !
这个属性能够正确工作需要 cookie-parser中间件。
// API-request/app-cookie.ts
import * as express from "express";
import * as cookieParser from "cookie-parser";
const app = express();
app.use(cookieParser());
app.get('/', (req, res) => {
console.log(req.cookies.name);
res.end();
});
app.listen(1234, () => {
console.log('server up');
});
判断文件是否过期。
通常会结合http缓存使用。
返回HTTP 头部HOST 包含的hostname
// Host: "example.com:3000"
console.dir(req.hostname)
// => 'example.com'
返回请求的IP 地址
当信任代理设置的计算值未为false时,此属性包含在x - forward - for请求头中指定的IP地址数组。否则,它包含一个空数组。这个头可以由客户端或代理设置。
包含与请求的HTTP方法相对应的字符串:GET、POST、PUT等
在中间件功能中,req.originalUrl是req.baseUrl和req.path的组合,如以下示例所示。
app.use('/admin', function (req, res, next) { // GET 'http://www.example.com/admin/new'
console.dir(req.originalUrl) // '/admin/new'
console.dir(req.baseUrl) // '/admin'
console.dir(req.path) // '/new'
next()
})
一般用作捕获路由的参数
请求URL的路径部分。
// example.com/users?sort=desc
console.dir(req.path)
// => '/users'
请求的协议字符串(http/https)
此属性是一个对象,包含路由中每个查询字符串参数的属性。如果没有查询字符串,则为空对象{}。
// GET /search?q=tobi+ferret
console.dir(req.query.q)
// => 'tobi ferret'
// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
console.dir(req.query.order)
// => 'desc'
console.dir(req.query.shoe.color)
// => 'blue'
console.dir(req.query.shoe.type)
// => 'converse'
// GET /shoes?color[]=blue&color[]=black&color[]=red
console.dir(req.query.color)
// => ['blue', 'black', 'red']
返回匹配的路由
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 } }
判断是否建立了tls链接,等价于下面的代码
console.dir(req.protocol === 'https')
// => true
当使用 cooies-parser 中间件的时候,此属性包含请求发送的已签名的、未签名的、准备使用的cookie。
// Cookie: user=tobi.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3
console.dir(req.signedCookies.user)
// => 'tobi'
这个和req.false是相反的。
返回一个包含请求中域名中的子域名的名称的数组
// Host: "tobi.ferrets.example.com"
console.dir(req.subdomains)
// => ['ferrets', 'tobi']
如果请求的X-Requested-With头字段是XMLHttpRequest,则一个布尔属性为true,表示请求是由jQuery等客户端库发出的。
根据请求头中Accept 字段的值检查请求体是否符合匹配。如果不匹配会返回false,出现这种情况一般需要返回406 NOT Acceptable
根据请求的Accept-Charset HTTP报头字段,返回指定字符集的第一个被接受的字符集。如果指定的字符集都不被接受,则返回false。
返回指定编码的第一个被接受的编码,基于请求的接受编码HTTP头字段。如果不接受指定的编码,则返回false。
返回指定语言的第一种被接受的语言,基于请求的接受语言HTTP头字段。如果没有指定的语言被接受,返回false
返回指定的HTTP请求头字段(大小写不敏感匹配)
如果传入请求的内容类型HTTP头字段匹配类型参数指定的MIME类型,则返回匹配的内容类型。如果请求没有主体,则返回null。否则返回false。
弃用
头部解析的范围
配合数据流使用可是实现断点续传。
res对象表示Express应用程序在收到HTTP请求时发送的HTTP响应
emm 我已经带你看完了前三个api了。你应该可以自己动手去查看官方文档了!
这一次我就挑出几个我认为重点的几个api讲讲。
设置响应头中的cook
直接以附件的形式传输文件,通常会在浏览上显示下载文件。
结束响应过程
如果你想快速结束响应就用这个,尽量不要用这个传输大量的数据。会有性能问题
发送http响应。
主体参数可以是缓冲区对象、字符串、对象或数组。
将响应的数据JSON序列化
一般用作后端接口
路由器对象是中间件和路由的隔离实例。 您可以将其视为“微型应用程序”,仅能执行中间件和路由功能。 每个Express应用程序都有一个内置的应用路由器。
emm…里面的几个api都和app对应的很像。所以我就不说了。。等我实际用了之后再回来补坑。。
我去,没想到我也会有耐心花整整2天的时间啃文档!给自己点个赞~
当然,作为一个前端小菜鸟,我知道自己肯定有很多不足。欢迎各位指正~
–end–