本文章主要是讲述了 node.js 中 web 开发的基础知识,这里主要讲述如何使用 express 框架的基础使用
1 简单的 http 服务
引入 required 模块
使用 require 指令来载入 Node.js 模块。
1 var http = require('http');
创建服务器:服务器可以监听客户端的请求,
类似于 Apache 、Nginx 等 HTTP 服务器。具体的细节参加 API
1 2 // 使用 http.createServer() 方法创建一个 Web 服务器, 返回一个 Server 实例 var server = http.createServer()
接收请求与响应请求
服务器很容易创建,客户端可以使用浏览器或终端发送 HTTP 请求,服务器接收请求后返回响应数据。
1 2 3 4 5 // 注册 request 请求事件 // 当客户端请求过来,就会自动触发服务器的 request 请求事件,然后执行第二个参数:回调处理函数 server.on("request", function(request, response) { console.log("收到客户端的请求了,请求的url为 " + request.url); });
绑定端口号,启动服务器
1 2 3 server.listen(8888, function () { console.log('服务器启动成功了,可以通过 http://127.0.0.1:8888/ 来进行访问') })
注意 : 当且仅当在第一次调用 server.listen() 或调用 server.close() 期间出现错误时,才能再次调用 server.listen() 方法。 否则,将抛出 ERR_SERVER_ALREADY_LISTEN 错误。参见
此时,若用户访问http://127.0.0.1:8888/则浏览器会一直在响应中,因为服务器没有返回信息。显示
该网页无法正常运作 127.0.0.1 未发送任何数据。 ERR_EMPTY_RESPONSE
因此,可以将步骤三中的代码更改为下面的示例,增加响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 server.on('request', function (request, response) { console.log('收到客户端的请求了,请求路径是:' + request.url) // 发送 HTTP 头部 ,可选操作 // HTTP 状态值: 200 : OK // 内容类型: text/plain response.writeHead(200, {'Content-Type': 'text/plain'}); // response 对象有一个方法:write 可以用来给客户端发送响应数据 // write 可以使用多次,但是最后一定要使用 end 来结束响应,否则客户端会一直等待 response.write('hello') response.write(' nodejs') // 告诉客户端,我的话说完了,你可以呈递给用户了 response.end() })
向请求发送响应头。 状态码是一个 3 位的 HTTP 状态码,如 404。 最后一个参数 headers 是响应头。 可以可选地将用户可读的 statusMessage 作为第二个参数。
1 2 3 4 const body = 'hello world'; response.writeHead(200, { 'Content-Length': Buffer.byteLength(body), 'Content-Type': 'text/plain' });
此方法只能在消息上调用一次,并且必须在调用 response.end() 之前调用。
如果在调用此方法之前调用了 response.write() 或 response.end(),则将计算隐式或可变的响应头并调用此函数。
当使用 response.setHeader() 设置响应头时,则与传给 response.writeHead() 的任何响应头合并,且 response.writeHead() 的优先。
如果调用此方法并且尚未调用 response.setHeader(),则直接将提供的响应头值写入网络通道而不在内部进行缓存,响应头上的 response.getHeader() 将不会产生预期的结果。 如果需要渐进的响应头填充以及将来可能的检索和修改,则改用 response.setHeader()。
1 2 3 4 5 6 7 // 返回 content-type = text/plain const server = http.createServer((req, res) => { res.setHeader('Content-Type', 'text/html'); res.setHeader('X-Foo', 'bar'); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('ok'); });
注意,Content-Length 以字节而非字符为单位。 上面的示例可行是因为字符串 ‘hello world’ 仅包含单字节字符。 如果主体包含更高编码的字符,则应使用 Buffer.byteLength() 来判断给定编码中的字节数。 Node.js 不检查 Content-Length 和已传输的主体的长度是否相等。
尝试设置包含无效字符的响应头字段名称或值将导致抛出 TypeError。
另一个完整的 demo 示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var http = require('http'); http.createServer(function (request, response) { // 发送 HTTP 头部 // HTTP 状态值: 200 : OK // 内容类型: text/plain response.writeHead(200, {'Content-Type': 'text/plain'}); // 发送响应数据 "Hello World" response.end('Hello World\n'); }).listen(8888); // 终端打印如下信息 console.log('Server running at http://127.0.0.1:8888/');
上述 demo 使用的是
1 http.createServer([options][, requestlistener])
这个接口 ,其中的 requestListener 是一个自动添加到 ‘request’ 事件的函数。
在上述响应中,如果有中文会出现乱码,可以增加响应头有进行控制,完整的 content-type 列表参见
1 response.setHeader("Content-Type", "text/html;charset=utf-8");
2 express 框架 2.1 安装 Express 1 cnpm install express --save
以上命令会将 Express 框架安装在当前目录的 node_modules 目录中, node_modules 目录下会自动创建 express 目录。以下几个重要的模块是需要与 express 框架一起安装的
1 2 3 cnpm install body-parser --save cnpm install cookie-parser --save cnpm install multer --save
这些附带安装的插件的作用如下:
body-parser - node.js 中间件,用于处理 JSON, Raw, Text 和 URL 编码的数据。
cookie-parser - 这就是一个解析 Cookie 的工具。通过 req.cookies 可以取到传过来的 cookie,并把它们转成对象。
multer - node.js 中间件,用于处理 enctype=”multipart/form-data”(设置表单的 MIME 编码)的表单数据。
安装完后,我们可以查看下 express 使用的版本号
1 2 3 $ cnpm list express /data/www/node └── express@4.15.2 -> /Users/tianqixin/www/node/node_modules/.4.15.2@express
2.2 快速入门 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //express_demo.js 文件 var express = require('express'); var app = express(); app.get('/', function (req, res) { res.send('Hello World'); }) var server = app.listen(8081, function () { var host = server.address().address var port = server.address().port console.log("应用实例,访问地址为 http://%s:%s", host, port) })
上述实例中我们引入了 express 模块,并在客户端发起请求后,响应 “Hello World” 字符串。
1 2 $ node express_demo.js 应用实例,访问地址为 http://0.0.0.0:8081
2.3 使用静态资源 项目的结构如上图所示,此时 public 为静态资源目录,无法直接访问,若需要将其当做静态资源访问,需要加上如下代码:
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 //express_demo.js 文件 var express = require("express"); const path = require("path"); var app = express(); //path.join([path1][, path2][, ...]) //用于连接路径。该方法的主要用途在于,会正确使用当前系统的路径分隔符,Unix系统是"/",Windows系统是"\"。 app.use(express.static(path.join(__dirname, "public"))); app.get("/", function(req, res) { res.send("Hello World"); }); app.get("/hello", function(req, res) { res.send("Hello World index"); }); //这里也可以链式调用 //app.get("/aa",function(){}).post("/bb",function(){}); var server = app.listen(8081, function() { var host = server.address().address; var port = server.address().port; console.log("应用实例,访问地址为 http://%s:%s", host, port); });
注意 : 若使用路径出现异常时,访问路径里的资源会无法访问 出现问题的代码:
1 app.use(express.static("/public/"));
此时会出现如下问题:
完成正确的设置后,可以通过 http://localhost:8081/js/demo.js 访问静态资源问题。
express 官方文档参见 http://www.expressjs.com.cn/starter/static-files.html
注意
上面的配置方式中,放置资源的目录不在访问路径中。
另一种正确的配置方式如下:
1 app.use("/public/", express.static("./public/"));
此时,访问静态资源需要带上路径 public ,访问路径为:
http://localhost:8081/public/js/demo.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // public 资源 app.use(express.static("public")); // static 资源 app.use(express.static("static")); // /public/xx app.use('/public',express.static("public")); // /static/xx app.use('/static',express.static("public")); // /static/xx app.use('/static',express.static(path.join(__dirname, "public")));
2.4 使用 art-template 安装命令如下:
1 2 npm install art-template --sav npm install express-art-template --sav
安装完成之后,即可在 node.js 里使用 art-template 了, 在 node 里增加以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 配置使用 art-template 模板引擎 // 第一个参数,表示,当渲染以 .art 结尾的文件的时候,使用 art-template 模板引擎 // express-art-template 是专门用来在 Express 中把 art-template 整合到 Express 中 // 虽然外面这里不需要记载 art-template 但是也必须安装 // 原因就在于 express-art-template 依赖了 art-template app.engine('art', require('express-art-template')) // Express 为 Response 相应对象提供了一个方法:render // render 方法默认是不可以使用,但是如果配置了模板引擎就可以使用了 // res.render('html模板名', {模板数据}) // 第一个参数不能写路径,默认会去项目中的 views 目录查找该模板文件 // 也就是说 Express 有一个约定:开发人员把所有的视图文件都放到 views 目录中 //若上面配置的为art,则这里后缀名不能为 html,否则会报错 app.get('/post', function (req, res) { res.render('post.html') })
具体的 art-template 用法参见官方文档
注意:
默认的模版文件的目录为 views
如果想修改默认的模板文件夹路径,可以通过如下方法进行设置
1 2 app.set('views', __dirname + '/views'); app.set('view engine', 'jade');
上面两行是设置 views 文件夹,即模板文件夹,dirname 是 node.js 里面的全局变量,即取得执行的 js 所在的路径,另外 dirname 是目前执行的 js 文件名。 所以,
1 app.set(‘views’, __dirname + ‘/views’);
是设置 views 的文件夹。 而
1 app.set(‘view engine’, ‘jade’);
是设置 express.js 所使用的 render engine。除了 Jade 之外 express.js 还支持 EJS(embedded javascript)、Haml、CoffeScript 和 jQuerytemplate 等 js 模板
2.5 express 处理 post 请求
先安装 body-parser ,安装命令如下:
1 cnpm install body-parser --save
配置 body-parser
1 2 3 4 5 6 7 8 9 //引入 body-parser var bodyParser = require('body-parser') // 配置 body-parser 中间件(插件,专门用来解析表单 POST 请求体) // parse application/x-www-form-urlencoded app.use(bodyParser.urlencoded({ extended: false })) // parse application/json app.use(bodyParser.json())
只要配置了这个属性,在 req 请求对象上就会增加一个属性 body
安装成功之后,就可以使用次插件了,使用的方法如下:
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 /** * app.js 入门模块 * 职责: * 创建服务 * 做一些服务相关配置 * 模板引擎 * body-parser 解析表单 post 请求体 * 提供静态资源服务 * 挂载路由 * 监听端口启动服务 */ var express = require('express') var router = require('./router') var bodyParser = require('body-parser') var app = express() app.use('/node_modules/', express.static('./node_modules/')) app.use('/public/', express.static('./public/')) // 配置使用 art-template 模板引擎 // 第一个参数,表示,当渲染以 .art 结尾的文件的时候,使用 art-template 模板引擎 // express-art-template 是专门用来在 Express 中把 art-template 整合到 Express 中 // 虽然外面这里不需要记载 art-template 但是也必须安装 // 原因就在于 express-art-template 依赖了 art-template app.engine('html', require('express-art-template')) // parse application/x-www-form-urlencoded // Express 为 Response 相应对象提供了一个方法:render // render 方法默认是不可以使用,但是如果配置了模板引擎就可以使用了 // res.render('html模板名', {模板数据}) // 第一个参数不能写路径,默认会去项目中的 views 目录查找该模板文件 // 也就是说 Express 有一个约定:开发人员把所有的视图文件都放到 views 目录中 // 如果想要修改默认的 views 目录,则可以 // app.set('views', render函数的默认路径) // 配置 body-parser 中间件(插件,专门用来解析表单 POST 请求体) // parse application/x-www-form-urlencoded app.use(bodyParser.urlencoded({ extended: false })) // parse application/json app.use(bodyParser.json()) // 当以 POST 请求 /post 的时候,执行指定的处理函数 // 这样的话我们就可以利用不同的请求方法让一个请求路径使用多次 app.post('/post', function (req, res) { // 1. 获取表单 POST 请求体数据 // 2. 处理 // 3. 发送响应 // req.query 只能拿 get 请求参数 // console.log(req.query) var comment = req.body comment.dateTime = '2017-11-5 10:58:51' comments.unshift(comment) // res.send // res.redirect // 这些方法 Express 会自动结束响应 res.redirect('/') // res.statusCode = 302 // res.setHeader('Location', '/') }) // 配置模板引擎和 body-parser 一定要在 app.use(router) 挂载路由之前 // 把路由容器挂载到 app 服务中 app.use(router) app.listen(3000, function () { console.log('running 3000...') }) module.exports = app
2.6 node.js 路由设置 假设项目的路由的路径作用如下:
请求方法
请求路径
get 参数
post 参数
备注
GET
/studens
渲染首页
GET
/students/new
渲染添加学生页面
POST
/studens/new
name、age、gender、hobbies
处理添加学生请求
GET
/students/edit
id
渲染编辑页面
POST
/studens/edit
id、name、age、gender、hobbies
处理编辑请求
GET
/students/delete
id
处理删除请求
1.在项目中创建一个名为 router.js 的文件
文件的内容如下:
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 /** * router.js 路由模块 * 职责: * 处理路由 * 根据不同的请求方法+请求路径设置具体的请求处理函数 * 模块职责要单一,不要乱写 * 我们划分模块的目的就是为了增强项目代码的可维护性 * 提升开发效率 */ var fs = require('fs') var Student = require('./student') // Express 提供了一种更好的方式 // 专门用来包装路由的 var express = require('express') // 1. 创建一个路由容器 var router = express.Router() // 2. 把路由都挂载到 router 路由容器中 /* * 渲染学生列表页面 */ router.get('/students', function (req, res) { Student.find(function (err, students) { if (err) { return res.status(500).send('Server error.') } res.render('index.html', { fruits: [ '苹果', '香蕉', '橘子' ], students: students }) }) }) /* * 渲染添加学生页面 */ router.get('/students/new', function (req, res) { res.render('new.html') }) /* * 处理添加学生 */ router.post('/students/new', function (req, res) { // 1. 获取表单数据 // 2. 处理 // 将数据保存到 db.json 文件中用以持久化 // 3. 发送响应 Student.save(req.body, function (err) { if (err) { return res.status(500).send('Server error.') } res.redirect('/students') }) }) /* * 渲染编辑学生页面 */ router.get('/students/edit', function (req, res) { // 1. 在客户端的列表页中处理链接问题(需要有 id 参数) // 2. 获取要编辑的学生 id // // 3. 渲染编辑页面 // 根据 id 把学生信息查出来 // 使用模板引擎渲染页面 Student.findById(parseInt(req.query.id), function (err, student) { if (err) { return res.status(500).send('Server error.') } res.render('edit.html', { student: student }) }) }) /* * 处理编辑学生 */ router.post('/students/edit', function (req, res) { // 1. 获取表单数据 // req.body // 2. 更新 // Student.updateById() // 3. 发送响应 Student.updateById(req.body, function (err) { if (err) { return res.status(500).send('Server error.') } res.redirect('/students') }) }) /* * 处理删除学生 */ router.get('/students/delete', function (req, res) { // 1. 获取要删除的 id // 2. 根据 id 执行删除操作 // 3. 根据操作结果发送响应数据 Student.deleteById(req.query.id, function (err) { if (err) { return res.status(500).send('Server error.') } res.redirect('/students') }) }) // 3. 把 router 导出 module.exports = router // 这样也不方便 // module.exports = function (app) { // app.get('/students', function (req, res) { // // readFile 的第二个参数是可选的,传入 utf8 就是告诉它把读取到的文件直接按照 utf8 编码转成我们能认识的字符 // // 除了这样来转换之外,也可以通过 data.toString() 的方式 // fs.readFile('./db.json', 'utf8', function (err, data) { // if (err) { // return res.status(500).send('Server error.') // } // // 从文件中读取到的数据一定是字符串 // // 所以这里一定要手动转成对象 // var students = JSON.parse(data).students // res.render('index.html', { // fruits: [ // '苹果', // '香蕉', // '橘子' // ], // students: students // }) // }) // }) // }
将 router.js 挂在到服务中
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 /** * app.js 入门模块 * 职责: * 创建服务 * 做一些服务相关配置 * 模板引擎 * body-parser 解析表单 post 请求体 * 提供静态资源服务 * 挂载路由 * 监听端口启动服务 */ var express = require('express') var router = require('./router') var bodyParser = require('body-parser') var app = express() app.use('/node_modules/', express.static('./node_modules/')) app.use('/public/', express.static('./public/')) app.engine('html', require('express-art-template')) // 配置模板引擎和 body-parser 一定要在 app.use(router) 挂载路由之前 // parse application/x-www-form-urlencoded app.use(bodyParser.urlencoded({ extended: false })) // parse application/json app.use(bodyParser.json()) // 把路由容器挂载到 app 服务中 app.use(router) app.listen(3000, function () { console.log('running 3000...') }) module.exports = app
express 具体的路由使用方法参见 http://www.expressjs.com.cn/4x/api.html#router
3 nodemon 自动重启服务
全局安装 nodemon 包,这样新创建的 Node.js 应用都能使用 Nodemon 运行起来了
安装完成之后,Nodemon 就可以启动 Express 应用了,先关闭当前正在执行的应用程序,然后再执行命令
3.1 nodemon 配置文件 Nodemon 默认会监听当前目录下(也就是执行 nodemon 命令所在的目录)的所有文件,不过有些情况下,虽然项目文件发生了改动,但是不需要 Nodemon 重启应用,那如何让文件不被 Nodemon 监听呢?不需要监听的文件,可以通过设置 Nodemon 的配置文件排除掉,新建文件 server/nodemon.json,添加代码
1 2 3 4 5 { "ignore": [ "config.default.js" ] }
Nodemon 配置文件是 JSON 文件,通过设置 ignore 属性值,一个由文件名组成的字符串数组,指定不需要监听的文件
3.2 nodemon 手动重启 有时候可能 Nodemon 还在运行的时候,需要手动重启它,在这种情况下不需要关闭正在运行的 Nodemon 进程然后再重启 Nodemon,只要在 Nodemon 命令运行的终端 窗口中输入 rs 两个字符,然后再按下回车键,就能重启 Nodemon 了
4 session 处理 在 express 框架中,默认不支持 session,可以使用 express-session 来支持 session
4.1 安装 express-session
4.2 配置 express-session 1 2 3 4 5 6 7 8 9 10 var session = require('express-session') var app = express() app.set('trust proxy', 1) // trust first proxy app.use(session({ secret: 'keyboard cat', //加密的key,提高安全性 resave: false, saveUninitialized: true, cookie: { secure: true } }))
express-session 一定要在路由之前配置
express-session 是针对 nodejs express 框架提供的一套 session 扩展,主要参数有 secret,sesave,saveUninitialized,cookie
cookie 主要属性如下
{ path: ‘/‘, httpOnly: true, secure: false, maxAge: null }
domain:设置 cookie 可以设置的域名,如果没有设置则 cookie 默认在当前域可以使用
expires:cookie 失效时间,可以设置时间,不建议给固定时间,设置 maxAge 之后自动会生成这个值
1 2 3 4 5 6 7 //获取当前时间 var date=new Date(); var expireDays=10; //将date设置为10天以后的时间 date.setTime(date.getTime()+expireDays*24*3600*1000); //将userId和userName两个Cookie设置为10天后过期 expires:date.toGMTString();
httpOnly:属性禁止客户端 JavaScript 的访问,禁止后不能使用 document.cookie
maxAge:单位毫秒,从设置 cookie 开始多少毫秒失效 ,如果 maxAge 和 expires 都设置了,最后设置的属性生效.
path:路径,默认值为域名的根路径.
sameSite: SameSite-cookies 是一种机制,用于定义 cookie 如何跨域发送。这是谷歌开发的一种安全机制,未来的一种 cookie 跨域授权处理方式,不明白的就不用设置了
(Strict 是最严格的防护,有能力阻止所有 CSRF 攻击。然而,它的用户友好性太差,因为它可能会将所有 GET 请求进行 CSRF 防护处理。
例如:一个用户在 reddit.com 点击了一个链接(GET 请求),这个链接是到 facebook.com 的,而假如 facebook.com 使用了 Samesite-cookies 并且将值设置为了 Strict,那么用户将不能登陆 Facebook.com,因为在 Strict 情况下,浏览器不允许将 cookie 从 A 域发送到B域。
Lax(relax 的缩写?)属性只会在使用危险 HTTP 方法发送跨域 cookie 的时候进行阻止,例如 POST 方式。
例 1:一个用户在 reddit.com 点击了一个链接(GET 请求),这个链接是到 facebook.com 的,而假如 facebook.com 使用了 Samesite-cookies 并且将值设置为了 Lax,那么用户可以正常登录 facebok.com,因为浏览器允许将 cookie 从 A 域发送到 B 域。
例 2:一个用户在 reddit.com 提交了一个表单(POST 请求),这个表单是提交到 facebook.com 的,而假如 facebook.com 使用了 Samesite-cookies 并且将值设置为了 Lax,那么用户将不能正常登陆 Facebook.com,因为浏览器不允许使用 POST 方式将 cookie 从 A 域发送到B域。
)
值 true:sameSite 使用 strict 模式
值 false:不设置 sameSite 属性
值 lax:sameSite 使用 lax 模式
值 strict: sameSite 使用 strict 模式
secure:设置 cookie 的 secure 值,默认是不设置任何值
setSecure(true); 的情况下,只有 https 才传递到服务器端。http 是不会传递的。
genid:设置创建 session id 的自定义函数,默认使用 uid-safe 扩展来生成 id, 自定义 genid 创建函数时一定要保证创建的 id 不要重复。
name :用来设置在 response 中范围 session id 是属性值,reuqest 中可以用默认的 request.session.id 访问。默认值为 connect.sid
proxy:代理,通过设置这个值可以设置 X-Forwarded-Proto 头,
proxy 的值有 true (X-Forwarded-Proto 使用),false (所有头信息忽略,只有 tls/ssl 可以安全连接),undefined(使用 trust proxy 设置) 具体大家研究,因为没有整代码大家继续努力实践
resave:是否允许 session 重新设置,要保证 session 有操作的时候必须设置这个属性为 true
rolling:是否按照原设定的 maxAge 值重设 session 同步到 cookie 中,要保证 session 有操作的时候必须设置这个属性为 true
saveUninitialized:是否设置 session 在存储容器中可以给修改(true 表示无论是否使用 session 都默认分配一个钥匙 )
session 过期 30 分钟,没有人操作时候 session 30 分钟后过期,如果有人操作,每次以当前时间为起点,使用原设定的 maxAge 重设 session 过期时间到 30 分钟只有这种业务场景必须同行设置 resave rolling 为 true.同时 saveUninitialized 要设置为 false 允许修改。
secret:用来注册 session id 到 cookie 中,相当与一个密钥。
store:session 存储的实例子,一般可以用 redis 和 mangodb 来实现
unset:设置 req.session 在什么时候可以设置
值:destory:请求结束时候 session 摧毁,值:keep session 在存储中的值不变,在请求之间的修改将会忽略,而不保存
更多具体的使用参见https://www.npmjs.com/package/express-session
4.3 session 的使用 添加 session 数据
访问 session 中的数据