S08-05 Node-Express
[TOC]
Express 初体验
认识 Web 框架
前面我们已经学习了使用 http 内置模块来搭建 Web 服务器,为什么还要使用框架?
- 原生 http 在进行很多处理时,会较为复杂;
- 有 URL 判断、Method 判断、参数处理、逻辑代码处理等,都需要我们自己来处理和封装;
- 并且所有的内容都放在一起,会非常的混乱;
目前在 Node 中比较流行的 Web 服务器框架是 express、koa;
- 我们先来学习 express,后面再学习 koa,并且对他们进行对比;
express 早于 koa 出现,并且在 Node 社区中迅速流行起来:
- 我们可以基于 express 快速、方便的开发自己的 Web 服务器;
- 并且可以通过一些实用工具和中间件来扩展自己功能;
express 的安装
express 的使用过程有两种方式:
- 方式一:通过 express 提供的脚手架,直接创建一个应用的骨架;
- 方式二:从零搭建自己的 express 应用结构;
方式一:安装 express-generator
npm install -g express-generator
创建项目:
express express-demo
项目目录如下:
├── app.js
├── bin
│ └── www
├── package-lock.json
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.jade
├── index.jade
└── layout.jade
我们可以安装依赖,将程序跑起来:
npm install
node bin/www
方式二:从零学习搭建
刚才创建的项目 express 项目,很多内容可能我们并不认识,所以刚开始我们最好从零来学习。
初始化一个新的项目
npm init -y
express 的安装:
- 目前最新的
release
版本是 4.17.1,我们使用该版本;
npm install express
express 初体验
我们来创建自己的第一个 express 程序:
const express = require('express')
// 创建服务器
const app = express()
// /home的get请求处理
app.get('/home', (req, res) => {
res.end('Hello Home')
})
// /login的post请求处理
app.post('/login', (req, res) => {
res.end('Hello Login')
})
// 开启监听
app.listen(8000, () => {
console.log('服务器启动成功~')
})
我们会发现,之后的开发过程中,可以方便的将请求进行分离:
无论是不同的 URL,还是 get、post 等请求方式;
这样的方式非常方便我们已经进行维护、扩展;
当然,这只是初体验,接下来我们来探索更多的用法;
请求和响应
请求的路径中如果有一些参数,可以这样表达:
/users/:userId
;- 在
request
对象中要获取可以通过req.params.userId
;
返回数据,我们可以方便的使用 json:
res.json(数据)
方式;- 可以支持其他的方式,可以自行查看文档;
- https://www.expressjs.com.cn/guide/routing.html
const express = require('express')
const app = express()
app.get('/users/:userId', (req, res, next) => {
console.log(req.params.userId)
res.json({ username: 'coderwhy', level: 99 })
})
app.listen(8000, () => {
console.log('静态服务器启动成功~')
})
Express 中间件
认识中间件
Express 是一个路由和中间件的 Web 框架,它本身的功能非常少:
- Express 应用程序本质上是一系列中间件函数的调用;
中间件是什么呢?
中间件的本质就是传递给 express 的一个回调函数;
这个回调函数接受三个参数:
- 请求对象(request 对象);
- 响应对象(response 对象);
- next 函数(在 express 中定义的用于执行下一个中间件的函数);
中间件中可以执行哪些任务呢?
- 执行任何代码;
- 更改请求(request)和响应(response)对象;
- 结束
请求-响应周期
(返回数据); - 调用栈中的下一个中间件;
如果当前中间件功能没有结束请求-响应周期
,则必须调用 next()将控制权传递给下一个中间件功能,否则,请求将被挂起。
中间件函数调用的元素:
应用中间件
那么,如何将一个中间件应用到我们的应用程序中呢?
- express 主要提供了两种方式:
app/router.use
和app/router.methods
; - 可以是 app,也可以是 router,router 我们后续再学习:
- methods 指的是常用的请求方式,比如:
app.get或app.post
等;
我们先来学习 use 的用法,因为 methods 的方式本质是 use 的特殊情况;
案例一:最普通的中间件
之所以称之为最普通的中间件,是因为无论是什么 path、methods 都会应用该中间件;
const express = require('express')
const app = express()
app.use((req, res, next) => {
console.log('common middleware 01')
next()
})
app.use((req, res, next) => {
console.log('common middleware 02')
res.end('Hello Common Middleware~')
})
app.listen(8000, () => {
console.log('中间件服务器启动成功~')
})
中间件的执行顺序:在匹配上的情况下,中间件按照注册的顺序执行;
案例二:path 匹配中间件
如果我们希望匹配一个明确的路径,也可以使用 use 方法:
// 案例二: 路径匹配中间件
app.use('/home', (req, res, next) => {
console.log('home middleware 01')
next()
})
app.use('/home', (req, res, next) => {
console.log('home middleware 02')
next()
res.end('Hello Home middleware')
})
app.use((req, res, next) => {
console.log('common middleware')
})
案例三:path 和 method 匹配中间件
// 案例三: method匹配中间件
app.get('/home', (req, res, next) => {
console.log('home get middleware')
next()
})
app.post('/login', (req, res, next) => {
console.log('login post middleware')
next()
})
app.use((req, res, next) => {
console.log('common middleware')
})
案例四:注册多个中间件
// 案例四: 注册多个中间件
const homeMiddleware1 = (req, res, next) => {
console.log('home middleware 01')
next()
}
const homeMiddleware2 = (req, res, next) => {
console.log('home middleware 02')
next()
}
const homeHandle = (req, res, next) => {
res.end('Hello Home~')
}
app.get('/home', homeMiddleware1, homeMiddleware2, homeHandle)
应用其他中间件
并非所有的中间件都需要我们从零去编写:
- express 有内置一些帮助我们完成对 request 解析的中间件;
- registry 仓库中也有很多可以辅助我们开发的中间件;
解析请求
- express.json():
- express.urlencoded():
在客户端发送 post 请求时,会将数据放到 body 中:
- 客户端可以通过 json 的方式传递;
- 也可以通过 form 表单的方式传递;
我们这里先使用 json 传递给服务器 body:
不进行解析时的操作:
app.post('/login', (req, res, next) => {
req.on('data', (data) => {
console.log(data.toString())
})
req.on('end', () => {
res.end('登录成功~')
})
})
我们也可以自己编写中间件来解析 JSON:
app.use((req, res, next) => {
if (req.headers['content-type'] === 'application/json') {
req.on('data', (data) => {
const userInfo = JSON.parse(data.toString())
req.body = userInfo
})
req.on('end', () => {
next()
})
} else {
next()
}
})
app.post('/login', (req, res, next) => {
console.log(req.body)
res.end('登录成功~')
})
但是,事实上我们可以使用 expres 内置的中间件或者使用body-parser
来完成:
app.use(express.json())
app.post('/login', (req, res, next) => {
console.log(req.body)
res.end('登录成功~')
})
如果我们解析的是 application/x-www-form-urlencoded
:
form 传递 body
我们可以使用 express 自带的 urlencoded
函数来作为中间件:
传入的 extended 用于表示使用哪一种解析方式:
- true:使用 qs 第三方模块;
- false:使用 querystring 内置模块;
- 备注:它们之间的区别这里不展开讲解;
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.post('/login', (req, res, next) => {
console.log(req.body)
res.end('登录成功~')
})
包:morgan
如果我们希望将请求日志记录下来,那么可以使用 express 官网开发的第三方库:morgan
安装 morgan:
npm install morgan
直接作为中间件使用即可:
const loggerWriter = fs.createWriteStream('./log/access.log', {
flags: 'a+'
})
app.use(morgan('combined', { stream: loggerWriter }))
包:multer
文件上传我们可以使用 express 官方开发的第三方库:multer
npm install multer
上传文件,并且默认文件名:
const upload = multer({
dest: 'uploads/'
})
app.post('/upload', upload.single('file'), (req, res, next) => {
console.log(req.file.buffer)
res.end('文件上传成功~')
})
添加文件名后缀:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/')
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname))
}
})
const upload = multer({
storage
})
app.post('/upload', upload.single('file'), (req, res, next) => {
console.log(req.file.buffer)
res.end('文件上传成功~')
})
我们也可以上传多张图片:
app.use('/upload', upload.array('files'), (req, res, next) => {
console.log(req.files)
})
如果我们希望借助于 multer 帮助我们解析一些 form-data 中的普通数据,那么我们可以使用 any:
- 请求如下:
app.use(upload.any())
app.use('/login', (req, res, next) => {
console.log(req.body)
})
请求和响应
客户端传递到服务器参数的方法常见的是 5 种:
- 方式一:通过 get 请求中的 URL 的 params;
- 方式二:通过 get 请求中的 URL 的 query;
- 方式三:通过 post 请求中的 body 的 json 格式(中间件中已经使用过);
- 方式四:通过 post 请求中的 body 的 x-www-form-urlencoded 格式(中间件使用过);
- 方式五:通过 post 请求中的 form-data 格式(中间件中使用过);
请求解析
方式一:params
请求地址:http://localhost:8000/login/abc/why
获取参数:req.params
app.use('/login/:id/:name', (req, res, next) => {
console.log(req.params)
res.json('请求成功~')
})
方式二:query
请求地址:http://localhost:8000/login?username=why&password=123
获取参数:req.query
app.use('/login', (req, res, next) => {
console.log(req.query)
res.json('请求成功~')
})
响应方式
end 方法
类似于 http 中的response.end
方法,用法是一致的。只能传入:string 、buffer、uint8array 类型
res.end('Hello World')
json 方法
json 方法中可以传入很多的类型:object、array、string、boolean、number、null 等,它们会被转换成 json 格式返回;
res.json({ name: 'why', age: 18 })
status 方法
用于设置状态码:
res.status(204)
其他支持补充
路由的使用
如果我们将所有的代码逻辑都写在 app 中,那么 app 会变得越来越复杂:
一方面完整的 Web 服务器包含非常多的处理逻辑;
另一方面有些处理逻辑其实是一个整体,我们应该将它们放在一起:比如对 users 相关的处理
- 获取用户列表;
- 获取某一个用户信息;
- 创建一个新的用户;
- 删除一个用户;
- 更新一个用户;
我们可以使用 express.Router()
来创建一个路由处理程序:
- 一个 Router 实例拥有完整的中间件和路由系统;
- 因此,它也被称为
迷你应用程序
(mini-app);
// 用户相关的处理
const userRouter = express.Router()
userRouter.get('/', (req, res, next) => {
res.end('用户列表')
})
userRouter.post('/', (req, res, next) => {
res.end('创建用户')
})
userRouter.delete('/', (req, res, next) => {
res.end('删除用户')
})
app.use('/users', userRouter)
当然,我们可以配置更多的路由,并且将所有的逻辑放到一个单独的文件中。
静态资源服务器
部署静态资源我们可以选择很多方式:
- Node 也可以作为静态资源服务器,并且 express 给我们提供了方便部署静态资源的方法;
const express = require('express')
const app = express()
;+app.use(express.static('./build'))
app.listen(8000, () => {
console.log('静态服务器启动成功~')
})
错误处理方式
app.use((req, res, next) => {
next(new Error('USER DOES NOT EXISTS'))
})
app.use((err, req, res, next) => {
const message = err.message
switch (message) {
case 'USER DOES NOT EXISTS':
res.status(400).json({ message })
}
res.status(500)
})
源码
express()
1、创建 app
2、express()
函数的本质其实是createApplication()
,返回一个 app 函数对象
app.listen()
1、调用app.listen()
2、在createApplication()
中通过mixin()
将 app 进行了混入
3、app.listen()
本质上是对http.createServer(this)
的封装,此处的 this 指向 app
app.use()
1、注册中间件
1、通过 use 来注册一个中间件
// 注册普通中间件
+++ app.use(
(req, res, next) => {
console.log('普通中间件1')
next()
},
(req, res, next) => {
console.log('普通中间件2')
next()
}
)
2、无论是 app.use 还是 app.methods 都会注册一个主路由, app 本质上会将所有的函数,交给这个主路由去处理
// application.js
// 2. 实现use()
app.use = function use(fn) {
// 初始化变量
var offset = 0;
var path = '/';
// default path to '/'
// disambiguate app.use([fn])
// 参数fn可以是function也可以是path + function
if (typeof fn !== 'function') {
var arg = fn;
// 取出参数列表中的第一个参数,此时它是path
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// first arg is the path
// 获取到path
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
var fns = flatten(slice.call(arguments, offset));
if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')
}
// setup router
// 路由器懒加载
this.lazyrouter();
var router = this._router;
// 遍历中间件函数
fns.forEach(function (fn) {
// non-express app
// 非Express应用中间件,直接使用router.use(path, fn)将其注册到指定的路径
if (!fn || !fn.handle || !fn.set) {
+++ return router.use(path, fn);
}
debug('.use app under %s', path);
fn.mountpath = path;
fn.parent = this;
// restore .app property on req and res
// 创建一个新的中间件函数
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});
// mounted an app
fn.emit('mount', this);
}, this);
return this;
};
3、在主路由router.use(path, fn)
中,一个函数 fn 会创建一个 layer,并被放入到 router.stack
中
proto.use = function use(fn) {
var offset = 0;
var path = '/';
// default path to '/'
// disambiguate router.use([fn])
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// first arg is the path
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
var callbacks = flatten(slice.call(arguments, offset));
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')
}
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
}
// add the middleware
debug('use %o %s', path, fn.name || '<anonymous>')
+++ var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
// this指向router,所以fns也是保存在router.stack中
+ this.stack.push(layer);
}
return this;
};
4、在 Layer 中,会将 fn 赋值给layer.handle
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug('new %o', path)
var opts = options || {};
+ this.handle = fn;
this.name = fn.name || '<anonymous>';
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts);
// set fast path flags
this.regexp.fast_star = path === '*'
this.regexp.fast_slash = path === '/' && opts.end === false
}
2、请求的处理过程
如果有一个请求过来,那么从哪里开始呢?
1、当请求过来时,会被app.listen
监听并执行http.createServer(this)
中的 this(app)
// 1、调用 app.listen
+++ app.listen(8000, () => {
console.log('express is running...')
})
// 2、调用 app.listen 的时候,本质上是调用 proto 中的 listen
app.listen = function listen() {
+++ var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
2、app 函数被调用开始的;
function createApplication() {
// 2.1 定义app变量,给变量赋值为一个中间件函数
var app = function(req, res, next) {
+++ app.handle(req, res, next);
};
// 省略
}
3、app.handle 本质上会去调用 router.handle
app.handle = function handle(req, res, callback) {
var router = this._router;
// final handler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
// no routes
if (!router) {
debug('no routes defined on app');
done();
return;
}
+++ router.handle(req, res, done);
};
4、router.handle 中做的事:
- 取出 fns(layer):
var stack = self.stack
- 执行 next:
next()
- 遍历 fns(layer),匹配 path
- 当匹配到时,执行 fn(layer)
proto.handle = function handle(req, res, out) {
var self = this;
debug('dispatching %s %s', req.method, req.url);
var idx = 0;
var protohost = getProtohost(req.url) || ''
var removed = '';
var slashAdded = false;
var sync = 0
var paramcalled = {};
// store options for OPTIONS request
// only used if OPTIONS request
var options = [];
// middleware and routes
++ var stack = self.stack;
// manage inter-router variables
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params');
// setup next layer
req.next = next;
// for options requests, respond with a default if nothing else responds
if (req.method === 'OPTIONS') {
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
}
// setup basic req values
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
++ next();
function next(err) {
var layerError = err === 'route'
? null
: err;
// remove added slash
if (slashAdded) {
req.url = req.url.slice(1)
slashAdded = false;
}
// restore altered req.url
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.slice(protohost.length)
removed = '';
}
// signal to exit router
if (layerError === 'router') {
setImmediate(done, null)
return
}
// no more matching layers
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
}
// max sync stack
if (++sync > 100) {
return setImmediate(next, err)
}
// get pathname of request
var path = getPathname(req);
if (path == null) {
return done(layerError);
}
// find next matching layer
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
if (typeof match !== 'boolean') {
// hold on to layerError
layerError = layerError || match;
}
if (match !== true) {
continue;
}
if (!route) {
// process non-route handlers normally
continue;
}
if (layerError) {
// routes do not match with a pending error
match = false;
continue;
}
var method = req.method;
var has_method = route._handles_method(method);
// build up automatic options response
if (!has_method && method === 'OPTIONS') {
appendMethods(options, route._options());
}
// don't even bother matching route
if (!has_method && method !== 'HEAD') {
match = false;
}
}
// no match
if (match !== true) {
return done(layerError);
}
// store route for dispatch on change
if (route) {
req.route = route;
}
// Capture one-time layer values
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path;
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
next(layerError || err)
} else if (route) {
++ layer.handle_request(req, res, next)
} else {
trim_prefix(layer, layerError, layerPath, path)
}
sync = 0
});
}
function trim_prefix(layer, layerError, layerPath, path) {
// 省略
}
};