node.js基础教程之web开发

node.js从了解到入门之web开发

Posted by yishuifengxiao on 2019-03-25

本文章主要是讲述了 node.js 中 web 开发的基础知识,这里主要讲述如何使用 express 框架的基础使用


1 简单的 http 服务

  1. 引入 required 模块

使用 require 指令来载入 Node.js 模块。

var http = require('http');
  1. 创建服务器:服务器可以监听客户端的请求,

类似于 Apache 、Nginx 等 HTTP 服务器。具体的细节参加 API

// 使用 http.createServer() 方法创建一个 Web 服务器, 返回一个 Server 实例
var server = http.createServer()
  1. 接收请求与响应请求

服务器很容易创建,客户端可以使用浏览器或终端发送 HTTP 请求,服务器接收请求后返回响应数据。

//    注册 request 请求事件
// 当客户端请求过来,就会自动触发服务器的 request 请求事件,然后执行第二个参数:回调处理函数
server.on("request", function(request, response) {
console.log("收到客户端的请求了,请求的url为 " + request.url);
});
  1. 绑定端口号,启动服务器
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

因此,可以将步骤三中的代码更改为下面的示例,增加响应。

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 作为第二个参数。

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()。

// 返回 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 示例如下:

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 使用的是

http.createServer([options][, requestlistener])

这个接口,其中的 requestListener 是一个自动添加到 ‘request’ 事件的函数。

在上述响应中,如果有中文会出现乱码,可以增加响应头有进行控制,完整的 content-type 列表参见

response.setHeader("Content-Type", "text/html;charset=utf-8");

2 express 框架

2.1 安装 Express
cnpm install express --save

以上命令会将 Express 框架安装在当前目录的 node_modules 目录中, node_modules 目录下会自动创建 express 目录。以下几个重要的模块是需要与 express 框架一起安装的

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 使用的版本号

$ cnpm list express
/data/www/node
└── express@4.15.2 -> /Users/tianqixin/www/node/node_modules/.4.15.2@express
2.2 快速入门
//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” 字符串。

$ node express_demo.js
应用实例,访问地址为 http://0.0.0.0:8081
2.3 使用静态资源

image
项目的结构如上图所示,此时 public 为静态资源目录,无法直接访问,若需要将其当做静态资源访问,需要加上如下代码:

//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);
});

注意: 若使用路径出现异常时,访问路径里的资源会无法访问
出现问题的代码:

app.use(express.static("/public/"));

此时会出现如下问题:

image

完成正确的设置后,可以通过 http://localhost:8081/js/demo.js 访问静态资源问题。

image

express 官方文档参见 http://www.expressjs.com.cn/starter/static-files.html

注意

上面的配置方式中,放置资源的目录不在访问路径中。

另一种正确的配置方式如下:

app.use("/public/", express.static("./public/"));

此时,访问静态资源需要带上路径 public,访问路径为:

http://localhost:8081/public/js/demo.js

// 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

安装命令如下:

npm install art-template --sav
npm install express-art-template --sav

安装完成之后,即可在 node.js 里使用 art-template 了,
在 node 里增加以下代码

// 配置使用 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

如果想修改默认的模板文件夹路径,可以通过如下方法进行设置

app.set('views', __dirname + '/views');
app.set('view engine', 'jade');

上面两行是设置 views 文件夹,即模板文件夹,dirname 是 node.js 里面的全局变量,即取得执行的 js 所在的路径,另外dirname 是目前执行的 js 文件名。
所以,

app.set(‘views’, __dirname + ‘/views’);

是设置 views 的文件夹。

app.set(‘view engine,jade);

是设置 express.js 所使用的 render engine。除了 Jade 之外 express.js 还支持 EJS(embedded javascript)、Haml、CoffeScript 和 jQuerytemplate 等 js 模板

2.5 express 处理 post 请求
  1. 先安装 body-parser ,安装命令如下:
cnpm install body-parser --save
  1. 配置 body-parser
//引入 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

安装成功之后,就可以使用次插件了,使用的方法如下:

/**
* 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 的文件

文件的内容如下:

/**
* 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
// })
// })
// })

// }
  1. 将 router.js 挂在到服务中
/**
* 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

image

3 nodemon 自动重启服务

  1. 全局安装 nodemon 包,这样新创建的 Node.js 应用都能使用 Nodemon 运行起来了
npm install -g nodemon
  1. 安装完成之后,Nodemon 就可以启动 Express 应用了,先关闭当前正在执行的应用程序,然后再执行命令
nodemon index.js
3.1 nodemon 配置文件

Nodemon 默认会监听当前目录下(也就是执行 nodemon 命令所在的目录)的所有文件,不过有些情况下,虽然项目文件发生了改动,但是不需要 Nodemon 重启应用,那如何让文件不被 Nodemon 监听呢?不需要监听的文件,可以通过设置 Nodemon 的配置文件排除掉,新建文件 server/nodemon.json,添加代码

{
"ignore": [
"config.default.js"
]
}

Nodemon 配置文件是 JSON 文件,通过设置 ignore 属性值,一个由文件名组成的字符串数组,指定不需要监听的文件

3.2 nodemon 手动重启

有时候可能 Nodemon 还在运行的时候,需要手动重启它,在这种情况下不需要关闭正在运行的 Nodemon 进程然后再重启 Nodemon,只要在 Nodemon 命令运行的终端 窗口中输入 rs 两个字符,然后再按下回车键,就能重启 Nodemon 了

rs

4 session 处理

在 express 框架中,默认不支持 session,可以使用 express-session 来支持 session

4.1 安装 express-session
npm i express-session
4.2 配置 express-session
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 之后自动会生成这个值

//获取当前时间
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 数据

req.session.键=

访问 session 中的数据

req.session.键