# Express 框架

Express + Node.js 手册 (opens new window)

  • express 是一个用于快速搭建网站服务器的nodejs框架,作者是TJ。
  • express 中提供了很多网站中的常用功能,比如路由、上传、静态资源、模版等。
  • express 的升级版是koa,koa的升级版是koa2,核心区别是express是大而全的框架,而koa是轻薄的框架,koa中用到某个功能还得单独引入模块。

# 1. generator生成器

我们不建议使用generator初始化express项目,因为使用它创建项目的时候,会安装很多不常用的功能,如果我们的项目中仅用到其中很小一部分功能时,就太浪费资源了。

如果你一定要安装使用generator初始化项目,那么可以这样做:

npm install express-generator -g
1

express和express-generator的区别:

  • express只是一个框架,是在被引入项目中使用的,而非工具。
  • express-generator则是一个工具,此工具的作用是生成express项目。

安装成功之后,可以通过express命令调用该工具,需要明确的是,在没有安装express-generator前,express命令是没法正确执行的

# 创建express项目,该项目的文件夹名字叫做myapp
express myapp


# 切换目录
cd myapp

# 安装依赖
npm install 

# 运行项目
npm run start 
1
2
3
4
5
6
7
8
9
10
11
12

浏览器访问 http://localhost:3000 就可以看到项目了

刚才说了,上述方法消耗资源太大,所以一般我们开发express项目时,都是自己一步一步单独配置的。

# 2. Express框架

创建一个名字叫做myapp的空白文件夹,命令行进入到myapp。

cd myapp
1

初始化项目,通过 init 命令创建 package.json 文件。

cnpm init -y
1

然后执行下面的命令,安装express模块。

cnpm i express -S
1

在myapp文件夹下,创建app.js页面,里面编写搭建服务器的代码。

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

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

// 开启服务器 
var server = app.listen(8080, function () {
	var host = server.address().address
	var port = server.address().port
	console.log("应用实例,访问地址为 http://%s:%s", host, port)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

代码编写完毕后,在命令行执行命令,挂起服务。

nodemon app.js
1

然后打开浏览器,访问 http://localhost:8080 就可以看到项目了

# 3. 静态资源托管 static

NodeJS有一个特点,无论用户访问哪个页面时,都会被app.js文件拦截下来,所以得通过路由来解决该问题。

一个网站中,一定会有很多的图片、css文件、js文件的,如果给每一个文件都单独配置路由,会变得特别麻烦。

所以express中内置了中间件 express.static 允许设置静态文件,只需要将文件夹配置出来,然后用户每次访问页面时,都先在该文件夹中查找匹配,如果没有匹配的文件,才向下在app.js的路由中查找。

比如项目根目录下有一个public文件夹,该文件夹下有1.jpg文件。

project
    node_modules
    public
        1.jpg
    app.js
1
2
3
4
5

app.use是配置中间件的方法,express.static是中间件。

app.use(express.static('public'));
1

运行程序后,浏览器访问 http://localhost/1.jpg,就能够看到图片了。

  • 访问文件时不需要写public
  • 允许设置多个静态目录
app.use(express.static('abc'));
app.use(express.static('xyz'));
1
2

中间件的概念是什么?

当用户每次访问一个页面时,先被该中间件处理一下,然后在正常的向下执行程序。

中间件就是一个函数,中间件具体的作用,取决于中间件里面的代码是怎么写的,比如刚才的 static 中间件,起到的作用就是每次用户访问页面时,先在所配置的文件夹中查找文件,如果没有找到文件,则找 app.js 中的路由。

  • 使用:app.use
    • 请求体解析:使用bodyParse中间件,给req添加body对象
    • 静态资源中间件:app.use("/static", express.static(__dirname + '/public')),自带
    • cookie处理:cookie-parser中间件,给req和res添加cookie对象
    • 路由中间件:自带,app.use('/user', (req, res, next) => {})
      • 以/user开头
      • next表示执行下一个中间件
const express = require('express')
const app = express()
const cookieParser = require('cookie-parser')

//2、中间件
app.use("/static", express.static(__dirname + '/public'));
// 解析 application/json
app.use(bodyParser.json()); 
// 解析 application/x-www-form-urlencoded
app.use(bodyParser.urlencoded());
app.use(cookieParser());
// ⽐如访问/user 开头的⻚⾯需要鉴权
app.use('/user', (req, res, next) => {
 console.log('todo 访问/user⻚⾯需要验证身份')
  if(req.query.id){
    next() //继续执行下一个中间件
  }else{
    next(new Error('鉴权失败'))
  }
})
//错误处理中间件
app.use('/user', (err, req, res, next) => {
 console.log('todo 访问/user⻚⾯需要验证身份')
 res.send(err.message)
})

//1、路由
app.get('/', (req, res) => {
 res.send('⾸⻚')
})

app.get('/user/:userId', (req, res) => {
 console.log(req.query, req.params)
 res.send('⽤户⻚')
})

app.post('/user', (req, res) => {
 console.log(req.body)
 console.log('cookie: ', req.headers.cookie, req.cookies)
 res.cookie('age', '5')
 res.send('⽤户⻚post\n')
})

app.listen(3001, () => {
 console.log(`express-server started at 3001`)
})

app.on('error', function(err){
  consolg.log('error: ', err)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  • 中间件执行流程和原理
    • 洋葱模型
    • 异步模式:即使下一个中间件有异步处理,当前中间件不会等待下一个中间件处理完成

# 4. 配置路由

# get和post路由

app.get('/a', function (req, res) {
	res.send('这是get时的a');
})

app.get('/b', function (req, res) {
	res.send('这是get时的b'); 
})

app.post('/a', function (req, res) {
	res.send('这是post时的a');
})
1
2
3
4
5
6
7
8
9
10
11

路由配置完毕后,记得通过浏览器访问一下,看看效果。

# 子路由

使用:require('express').Router()

app.use('/api', apiHandler) //api开头前缀

//apiHandler.js
const router = require('express').Router()
router.post('/news', (req, res) => {
 		res.send(req.path) //这里的path为/news
})
router.get('/list', (req, res) => {
 		res.send(req.path)
})
module.exports = router
1
2
3
4
5
6
7
8
9
10
11

# 通配符

支持通配符匹配

app.get('/b/*.html', function (req, res) {
	res.send('b目录下,任意名称的html文件');
})

app.get('/a*c.html', function (req, res) {
	res.send('符合a开头c结尾的名称的html文件');
})
1
2
3
4
5
6
7

# 正则

支持正则表达式匹配

app.get(/^\/(a|b)\.html$/, function (req, res) {	
	res.end("根目录下的a.html或b.html");
}) 
1
2
3

# next

猜一猜下面的程序输出什么?

app.get('/a', function (req, res) {
	console.log("你好");
})

app.get('/a', function (req, res) {
	console.log("中文");
})
1
2
3
4
5
6
7

浏览器访问/a时,按照代码的书写顺序,仅执行了一个。

如果想让第二段代码也能够执行,需要调用next方法。

app.get('/a', function (req, res, next) {
	console.log("1");
	next();
	console.log("3");
})

app.get('/a', function (req, res) {
	console.log("2");
})
1
2
3
4
5
6
7
8
9

# send与sendFile

关于输出

app.get('/a', function (req, res, next) {
    // express框架写法
    //res.setHeader("Content-Type", "text/html");
    //res.setHeader('Access-Control-Allow-Origin','*');
	res.send("中文不会出现乱码,但只能发送一次");
	//res.json({err:0, text:'输出json'})
    //res.sendFile(__dirname+"/form.html"); // 将这个文件的内容作为响应到页面上
    //res.redirect('/b'); // 页面跳转
})
1
2
3
4
5
6
7
8
9

api接口规范中,返回的内容应该是json,要有属性描述状态码。

状态码属性叫什么名都可以,通常叫 err、code、state、status、type 等具有语义的名字。

// 示例
res.json({
    err : 0,
    text : '成功啦'
})
1
2
3
4
5

# get数据(重点)

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

app.get('/', function (req, res) {
	res.sendFile( __dirname+"/form.html" );
})

app.get('/chk', function (req, res) {
	console.log(req.query);
}) 

app.listen(80)
1
2
3
4
5
6
7
8
9
10
11
12

# post数据(重点)

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

// cnpm i body-parser -S
var bodyParser = require('body-parser');
app.use(bodyParser());

app.get('/', function (req, res) {
	res.sendFile( __dirname+"/form.html" );
})

app.post('/chk', function (req, res) {
	console.log(req.body)
}) 

app.listen(80)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# params数据

app.get('/news/:id', (req, res)=>{
    console.log( req.params );
    res.send('结束');
})
1
2
3
4

# 5. 错误处理

错误处理是在⼀个特殊签名的中间件中完成的,⼀般被添加到链的最后

  • 可以通过next传递错误
  • 可以是代码运⾏错误
  • 使用最后的中间件捕获
// 可以通过next传递错误
app.get('/test', (req, res, next) => {
   console.log('test')
   next(new Error('500 can only greet "world"'))
   // res.send('end')
})
// 可以是代码运⾏错误
app.use((req, res) => {
   if (req.query.greet !== 'world') {
      throw new Error('can only greet "world"')
   }
   res.status(200)
   res.send(`Hello ${req.query.greet} from Express`)
})
// 会被最后的中间件捕获,4个参数即可不必放置在最后
app.use((err, req, res, next) => {
   if (!err) {
      next()
      return
   }
   console.log('Error handler:', err.message)
   res.status(500)
   res.send('Uh-oh: ' + err.message)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 6. 模版引擎EJS

现在讲究前后端彻底分离,所以不推荐使用模版EJS或Jade

EJS 是一个 JavaScript 模板库,用来从 JSON 数据中生成 HTML 字符串。

# 基础用法

目录结构

node_modules/
views/
    index.ejs
app.js
1
2
3
4

views/index.ejs

<p>Hi<%=name%></p>
1

app.js

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

// 模版引擎设置为ejs
// npm install ejs
app.set('view engine', 'ejs');  // 规定必须要这么写

// 路由
app.get('/',function (req, res) {
	// 渲染
	res.render('index',{
		'name' : '张三'
	})
});
app.listen(80);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

当用户在浏览器中访问根目录时,渲染的是 views/index.ejs和{name:'张三'}替换合并后的内容。

# 语法

if 语句

<%
if ( 条件 ){
    
}else{
    
}
%>
1
2
3
4
5
6
7

关于循环

<ul>
	<%
		for ( var i=0; i<arr.length; i++ ) {
	%>
		<li>
		<%=arr[i]%>
		</li>
	<%	
		}
	%>
	
	<%
	for( var i in arr ){
		%>
		<%-'<li> -表示转义 '+arr[i]+'</li>'%>
		<li><%=arr[i]%></li>
		<%
	}
	%>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

<%-以html的形式输出%>

include 引入其他模版

b.ejs

<%= data %>
1

a.ejs

1
<% include b.ejs %>
2
1
2
3

在渲染 a.ejs 时,在其内容1与2之间,会把 b.ejs 的内容渲染出来。

不能写成<%= include%>,但可以写成<%- include%>