前端笔记 — node.js

一. 简介

1.Node.js是js运行在服务器的一个平台
2.Node中,每一个js文件中的代码都是独立运行在一个闭包中的
3.Node.js的三个特点
  a. 单线程
  b. 非阻塞I/O
  c. 事件驱动
4.Node没有web容器,也没有根目录

二. exports与require

1.介绍

node在执行模块中的代码时,会将代码包含在一个函数中,这个函数有5个参数:

  1. exports:用来将模块内部的局部变量或局部函数暴露给外部
  2. require:用来引用外部的模块
  3. module:代表当前模块本身,exports就是module的属性,我们既可以用exports导出,也可以用module.exports导出
  4. __filename:当前模块的完整路径
  5. __dirname:当前模块所在文件夹的完整路径
    所以exports与require并不是全局变量,而是函数参数
function (exports, require, module, __filename, __dirname) {
    // js文件中的代码
}
2.使用方法

可以使用exports导出变量或函数

// common.js
exports.name = '张三'
exports.say = () => {
    console.log('hello')
}

使用require引入模块

let common = require('./common')
console.log(common.name) // 张三
common.say() // hello

也可以使用module.exports直接导出对象

// common.js
module.exports = {
    name: '张三',
    say: () => { console.log('hello') }
}

require引入的实际上是module.exports导出的内容,所以不能直接使用exports导出对象

// common.js
exports = {
    name: '张三',
    say: () => { console.log('hello') }
}

// other.js
let common = require('./common') // 这里的common是一个空的对象{},并没有name和say属性

经常有这样的写法:exports = module.exports = xxx,这种写法的作用就是module.exports指向新对象后,让exports重新指向这个新的对象

三. Buffer

Buffer是一个二进制容器,数据结构与数组类似,用于Node中的数据存放
Buffer是Node自带的,不需要引入,可以直接使用
Buffer常用方法:

  • Buffer.from(str):将一个字符串转换成buffer
  • Buffer.alloc(size):创建一个指定大小的buffer
  • Buffer.alloc(size[, fill[, encoding]]):size表示新建buffer的期望长度;fill用来填充buffer的值,默认为0;encoding为编码格式,默认utf-8

四. 文件系统(File System)

Node通过fs模块来和文件系统进行交互,该模块提供了一些标准的文件访问API
使用fs之前,需要先引入

const fs = require('fs')
1.open、openSync
open(path, flags[, mode], callback):使用异步的方式打开文件
let fd = fs.open('1.txt', 'w', (err, fd) => {
    // fd为打开的文件
}) // 异步打开文件用于写入

openSync(path, flags[, mode]):使用同步的方式打开文件

let fd = fs.openSync('1.txt', 'w') // 打开文件用于写入
2.write、writeSync、writeFile、writeFileSync

这四个方法用于向文件写入内容,其中前面两个方法只能传上面open方法获取的fd,后面的两个方法可以直接传路径

// write、writeSync
let fd = fs.openSync('hello.txt', 'w')
let content = 'hello world!'
fs.write(fd, content, err => {
    if (!err) console.log('写入成功')
})
fs.writeSync(fd, content) // 返回写入的字节数

// writeFile、writeFileSync
fs.writeFile('hello.txt', content, err => {
    if (!err) console.log('写入成功')
})
fs.writeFileSync('hello.txt', content) // 无返回值
3.使用文件流写入
let ws = fs.createWriteStream('test.txt')
ws.once('open', () => {
    console.log('通道打开')
})
ws.once('close', () => {
    console.log('通道关闭')
})
ws.write('你好啊')
ws.write('你好不好啊')
ws.write('我很好啊')
4.readFile、readFileSync
fs.readFile('1.txt', (err, data) => {
    if (!err) console.log(data) // data为读取到的文件内容
    else throw err
})

let data = fs.readFileSync('1.txt')  // 同步读取文件内容
5.使用文件流读写文件
let read = fs.createReadStream('1.txt')
let write = fs.createWriteStream('2.txt')
read.on('data', (data) => { // 监听读取事件,每读取到一段数据都会回调次方法
    write.write(data) // 将读入的文件内容写入2.txt中
})
// 也可以使用下面的方法直接建立读写通道
read.pipe(write)

五. mongoDB

mongoDB是非关系型数据库,没有表、行的概念,只有集合(Collection,类似于关系型数据库中的表)与文档(Document,类似于关系型数据库中的行),用json格式存储数据。

1.mongoDB常用命令

show dbs:显示当前所有的数据库
use 数据库名:进入到指定数据库,没有则创建
db:显示当前数据库
show tables:显示当前数据库中的所有集合
db.dropDatabase():删除当前数据库
db.<collecton>.drop():删除集合

2.常用的CRUD语句

db.<collection>.insert(doc):添加数据

db.student.insert({name: '张三', age: 20}) // 插入一条数据
db.student.insert([
    {name: '张三', age: 20},
    {name: '李四', age: 25}
]) // 插入多条数据

db.<collection>.find():查询集合中的所有数据

db.student.find() // 查询student集合中的所有数据
db.student.find({name: '张三'}) // 查询所有name为张三的数据
db.student.findOne({name: '张三'}) // 查询一条name为张三的数据
db.student.find({name: '张三'}, {age: 1, _id: 0}) // 查询所有name为张三的数据,只显示age字段,不显示_id字段
db.student.find().count() 或 db.student.find().length() // 返回数据总条数,可传入查询条件

配合操作符进行查询:$lt(小于),$lte(小于等于),$gt(大于),$gte(大于等于)

db.student.find({age: {$lt: 20}}) // 查询年龄小于20的数据
db.student.find({age: {$gt: 10, $lt: 20}}) // 查询年龄大于10且小于20的数据
db.student.find({$or: [{age: {$lt: 10}}, {age: {$gt: 20}}]}) // 查询年龄小于10或大于20的数据

skip(num),跳过前面num条数据,limit(size),查询size条数据,可用于分页

// 查询第三页的数据,每页10条
db.student.find().skip(20).limit(10) // 跳过前面20条,查询10条

sort():用于数据排序

db.student.find().sort({name: 1}) // 根据name升序排列,1表示升序,-1表示降序
db.student.find().sort({name: 1, age: -1}) // 先根据name升序排序,若name相同,则根据age降序排列

db.<collection>.update():更新数据

db.student.update({name: '张三'}, {$set: {age: 30}}) // 更新第一条张三的age为30
db.student.update({name: '张三'}, {$set: {age: 30}}, {multi: true}) // 更新所有张三的age为30
// updateMany() === update({}, {}, {multi: true})
// updateOne() === update({}, {})
db.student.update({name: '张三'}, {$unset: {age: 1}}) // 删除张三的age,age的值可以随便传
db.student.updateMany({name: '张三'}, {$inc: {age: 10}}) // 将张三的年龄加10
db.student.replaceOne({name: '张三'}, {name: '马六', age: 20}) // 将name为张三的数据替换为{name: '马六', age: 20}

更深层次的操作

db.student.update({name: '张三'}, {
    $set: {
        hobby: {
            common: ['上网', '看书'],
            special: ['胸口碎大石', '徒手劈榴莲']
        }
    }
})

// 查询普通爱好有上网的文档
db.student.find({'hobby.common': '上网'})
// 给张三添加一个新的普通爱好“跑步”
db.student.update({name: '张三'}, {$push: {'hobby.common': '跑步'}}) // 也可以将$push替换为$addToSet,区别就是,$addToSet会判断是否已经存在,如果存在,则不添加
// 删除张三“徒手劈榴莲”的特殊爱好
db.student.update({name:  '张三'}, {$pull: {'hobby.special': '徒手劈榴莲'}})
// 或
db.student.update({name: '张三'}, {$pop: {'hobby.common': 1}}) // 只能传1或者-1,1表示删除最后一个,-1表示删除第一个

db.<collection>.remove():删除数据

db.student.remove({}) // 删除全部数据,参数不能为空
db.student.remove({name: '张三'}) // 删除所有name为张三的数据
db.student.remove({name: '张三'}, true) // 删除一条name为张三的数据

六. mongoose

Mongoose是一个可以通过Node来操作MongoDB的模块,本质上是一个对象文档模型(ODM)库

1.基本使用

引入mongoose模块

let mongoose = require('mongoose')

通过connect(url(s), [options], [callback])方法创建数据库连接

mongoose.connect('mongodb://数据库ip:端口号/数据库名称')
// 指定用户名连接
mongoose.connect('mongodb://用户名:密码@数据库ip:端口号/数据库名称') // 默认端口27017
// 如果要连接多个数据库,只需要设置多个url以逗号隔开,同时设置mongos为true
mongoose.connect('urlA, urlB, ...', {mongos: true})

监听各种状态

let db = mongoose.connection
db.on('error', () => { console.log('连接失败') })
db.on('open', () => { console.log('连接成功') })
db.on('close', () => { console.log('连接断开') })

使用disconnect()断开连接

mongoose.disconnect()
2.Schema

用于定义MongoDB中集合里文档的结构,可以理解为mongoose对表结构的定义,每个schema会映射到MongoDB中的一个Collection,它不具备操作数据库的能力

定义schema时,指定字段名和类型即可,支持的类型包括:String、Number、Data、Buffer、Boolean、Mixed、ObjectId及Array。通过mongoose.Schema来调用schema,然后使用new方法创建schema

let Schema = mongoose.Schema
let personSchema = new Schema({
    name: String,
    age: Number,
    sex: {
        type: 'string',
        default: '男'
    }
})
// 创建schema对象时,声明字段类型有两种方法,一种是首字母大写的字段类型,另一种是引号包含的小写字段类型

mongoose会将集合名称设置为模型名称的小写版。如果名称的最后一个字符是字母,则会变成复数,如果最后一个字符是数字,则不变。例如模型名称为“person”,则映射的集合名称为“people”,也可以指定集合名称

let personSchema = new Schema({
    // ...
}, {collection: 'user_info'}) // 之后的增删改查操作都是针对user_info这个集合

如果创建后还需要添加其他的字段,可以使用add()方法

personSchema.add({chat: String})
3.Model

Model是由Schema编译而成的假想构造器,具有抽象属性和行为。Model的每一个实例就是一个Document,简单说model就是由schema生成的模型,可以对数据库操作

使用model()方法,将schema编译为model,第一个参数为模型名称

let personModel = mongoose.model('person', personSchema)

也可以在编译model时指定集合名称

let personModel = mongoose.model('user', personSchema, 'user_info')
4.插入数据

使用create()插入数据

personModel.create({
    name: '张三',
    age: 25,
    chat: '15345632345'
}, err => {
    if (!err) console.log('插入成功')
    else throw err
})

插入多条数据

personModel.create([
    {name: '李四', age: 27, chat: 'lisi'},
    {name: '静静', age: 30, sex: '女', chat: 'jj'}
])

使用save()方法添加数据

let person = new personModel({
    name: '王五',
    age: 32
})
person.save((err, result) => { console.log(result) }) // result为插入的数据
5.查找、修改及删除

查询

personModel.find({name: '张三'}, (err, docs) => {
    console.log(docs) // docs为查询结果
})
// 查询所有
personModel.find({}, (err, docs) => { 
    console.log(docs)
})
// 分页查询,不显示_id,只显示name、age
personModel.find({}, '-_id name age', {skip: 2, limit: 2}, (err, docs) => {})
或 personModel.find({}, {_id: 0, name: 1, age: 1}, {skip: 2, limit: 2}, (err, docs) => {})
// 统计数据总条数
personModel.count({}, (err, total) => {
    console.log(total) // total为总条数
})

修改

// 将所有名为张三的人,年龄改为28
personModel.update({name: '张三'}, {$set: {age: 28}}, {multi: true}, err => {})
// 也可使用updateOne()更新一条或updateMany()更新所有

删除

// 删除名为王五的数据
personModel.remove({name: '王五'}, err => {})

七. HTTP

1.基本使用

引入模块,使用createServer()方法创建服务,该方法接受一个回调函数作为参数,回调函数有两个参数,第一个为请求对象,第二个为响应对象。然后使用创建的服务进行监听

let http = require('http')
http.createServer((req, res) => {
    // 处理代码
    res.end() // 最后必须调用该方法结束请求
}).listen(port, ip) // port为端口号,ip地址

使用响应对象的writeHead()方法,写入状态码及响应头信息,write()或end()方法向客户端写入内容

res.writeHead(200, {'Content-Type': 'text/html;charset=UTF-8'})
res.write('Hello World!')
res.end('你好')

Node.js没有web容器,不能直接通过URL访问资源,需要配置路由,通过请求对象的url属性可以获取请求的资源路径

if (req.url === '/page1.html') {
    fs.readFile('./html/page1.html', (err, data) => {
        if (!err) {
            res.writeHead(200, {'Content-Type': 'text/html;charset=UTF-8'})
            res.end(data)
        }
    })
} else if (req.url === '/page2.html') {
    // ...
} else {
    res.writeHead(404, {'Content-Type': 'text/plain;charset=UTF-8'})
    res.end('访问的页面不存在')
}
2.处理get请求

使用url模块的parse()方法,可以将请求url转换成对象,对象的query属性则为get请求的参数

// http://127.0.0.1/?name=tom&age=28&sex=1
let url = require('url')
http.createServer((req, res) => {
    let urlObj = url.parse(req.url)
    console.log(urlObj.query) // name=tom&age=28&sex=1
    /*
        如果给parse()方法传入第二个参数true,则query会被转换成对象
        let urlObj = url.parse(req.url, true)
        console.log(urlObj.query) // {name: 'tom', age: 28, sex: 1}
    */
    res.end()
}).listen(...)
3.处理post请求

Node接受post请求传递的数据时,是分段接受的,并不是一次性全部接受,需要监听data事件

let qs = require('queryString')
http.createServer((req, res) => {
    let allData = ''
   // 将每次接受到的数据拼接起来
   req.on('data', data => { 
       allData += data
   })
   // 监听数据传输完毕
   req.once('end', () => {
       let d = qs.parse(allData) // 使用queryString模块的parse()方法将数据转成对象
       console.log(d)
   })
   res.end('ok')
})
4.formidable

formidable是一个第三方库,可以用来处理post请求,并保存上传的文件

// 安装后引入
let formidable = require('formidable')
let uuidv1 = require('uuid/v1') // 一个第三方库,可以生成UUID
let path = require('path')
http.createServer((req, res) => {
    // 创建
    let form = new formidable.IncomingForm()
    // 设置上传文件保存的路径
    form.uploadDir = './uploads'
    // 处理post请求上传的数据,第一个参数为请求对象,第二个参数是一个回调函数
    // err为错误信息,fields为请求的参数,files为上传的文件
    form.parse(req, (err, fields, files) => {
        // 这里面可以做一些处理,比如给上传的文件重命名
        let name = uuidv1() // 生成新的名称
        let extName = path.extName(files.photo.name) // 获取后缀名,假设上传的文件字段名为photo
        let oldPath = `${__dirname}/files.photo.path`
        let newPath = `${__dirname}/uploads/${name}${extName}`
        fs.rename(oldPath, newPath, err => { // 文件重命名
            if (!err) {
                res.writeHeader(200, ....)
                res.end()
            } else throw err
        })
    })
})
5.静态资源匹配

根据请求资源路径,自动去查找对应的资源,并返回给客户端,不用每个路径都配置路由

http.createServer((req, res) => {
    let pathUrl = url.parse(req.url) // 将请求url转成对象
    let pathName = pathUrl.pathName // 获取请求资源路径
    if (!pthName.includes('.')) { // 如果请求路径不包含.,则请求的是文件夹,返回index.html
        pathName += '/index.html'
    }
    /*
        path.normalize()方法能将路径格式化成正确的格式
        从static文件夹中寻找资源
    */
    let fileUrl = './' + path.normalize('static/' + pathName)
    let extName = path.extName(pathName) // 获取后缀名
    // 根据fileUrl去查找对应的资源
    fs.readFile(fileUrl, (err, data) => {
        if (err) { // 访问的资源不存在
            res.write(404, {'Content-Type': 'text/html;charset=UTF-8'})
            res.end('<h1>404,页面不存在</h1>')
        }
        let contentType = getContentType(extName) // 自定义方法,根据后缀名取对应的ContentType
        res.writeHeader(200, {'Content-Type': contentType})
        res.end(data) // 将读取到的资源返回给客户端
    })
}).listen(80, '127.0.0.1')
6.模板引擎(EJS)

EJS是一套简单的模板语言,利用普通的JavaScript代码生成HTML页面。
EJS文件以.ejs为后缀,可以在HTML中插入js语句及变量或表达式。js语句使用<%%>插入,变量或表达式使用<%=%>插入
例:有如下模板及json数据

将数据绑定到模板中并返回给客户端

let ejs = require('ejs')
http.createServer((req, res) => {
    fs.readFile('./topNes.ejs', async (err, data) => {
        if (!err) {
            let newsList = await getNewsList()
            let tmp = data.toString()
            let newsEjs = ejs.render(tmp, newsList)
            res.writeHeader(200, {'Content-Type': 'text/html;charset=UTF-8'})
            res.end(newsEjs)
        }
    })
}).listen(80, '127.0.0.1')

let getNewsList = () => {
    return new Promise((resolve, reject)=> {
        fs.readFile('./topNews.json', (err, data) => {
            if (!err) resolve(JSON.parse(data))
            else reject(err)
        })
    })
}
7.request()发送请求

可以使用request(options[, callback])方法发送请求
options常用配置如下

  • protocol:协议类型。默认值:'http:'
  • hostname:请求服务器的域名或ip。默认值:'localhost'
  • port:请求的端口。默认值:'80'
  • path:请求的路径,get请求时包含查询字符串。例如 '/index.html?page=2'。默认值:'/'
  • method:请求方法。默认值:'get'
  • headers:请求头对象

使用该方法时,必须始终调用req.end()来表示请求结束。如果请求期间发生了错误,可以监听 'error' 事件,如果没有监听,则抛出错误

发送get请求

let http = require('http')
let req = http.request({
    hostname: '39.98.51.29',
    port: '6604',
    path: 'v1/api/device?deviceNo=200', // 请求参数拼在路径后面
    headers: {
        ACCESS_TOKEN: 'token' // token放在请求头中
        // get请求时可以不设置Content-Type
    }
}, res => { // 响应的回调函数
    /*
        保存接受到的数据。也可以将数据保存到数组中,最终得到的是二进制数组,
        可以通过toString()方法转为字符串,也可以直接使用JSON.parse()方法转为对象
    */
    let str = '' 
    res.on('data', data => { // 数据是分段传输的,需要监听data事件
        str += data
    })
    res.on('end', () => { // 表示数据传输完毕
        console.log(str)
    })
})
req.end() // 必须调用

发送post请求

let http = require('http')
let qs = require('querystring')

let data = qs.stringify({ // 请求参数为 form-data 时,使用qs.stringify()将对象转为 form-data 格式
    username: 'ykx',
    password: '123456'
})
let req = http.request({
    hostname: '39.98.51.29',
    port: '6604',
    path: '/login',
    method: 'post', // 非get请求时,必须设置method
    headers: {
        // 如果请求参数为 form-data,则 Content-Type 为 application/x-www-form-urlencoded
        // 如果请求参数为 json,则 Content-Type 为 application/json;charset=UTF-8
        'Content-Type': 'application/x-www-form-urlencoded'
    }
}, res => {
    let arr= []
    res.on('data', data => {
        arr.push(data)
    })
    res.on('end', () => {
        console.log(arr.toString()) // 或者直接JSON.parse(arr)转成对象
    })
})
req.write(data) // 将请求参数写入请求体
req.end()

如果请求头中包含Accept-Encoding: gzip, deflate,表示请求的结果需要gzip压缩,需要使用zlib解压,否则乱码

let zlib = require('zlib')

let req = http.request({
    ...
}, res => {
    let allData = ''
    let gunzip = zlib.createGunzip()
    res.pipe(gunzip)
    gunzip.on('data', data => {
          allData += data
     }).on('end', () => {
        // 这里的allData就是解压后的数据
     })
})
req.end()

八. Express

Express是基于Node.js平台,快速、开放、极简的web开发框架。不对Node.js已有的特性进行二次抽象,只是在它之上扩展了Web应用所需的基本功能

Express自身功能极简,完全由路由和中间件构成,从本质上说,一个express应用就是在调用各种中间件

1.基本使用

npm安装后在文件中引入,然后创建express()实例,监听端口

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

// 配置路由
app.get('/', (req, res) => {
    // 监听根路由的访问 http://127.0.0.1:3000
    res.send('Hello world!')
})
app.get('/news', (req, res) => { ... }) // http://127.0.0.1:3000/news

// 配置二级路由,多级路由也是如此配置
app.get('/news/topTen', (req, res) => { ... }) // http://127.0.0.1:3000/news/topTen

// 可以传递路径参数
app.get('/about/:name', (req, res) => {
    console.log(req.params.name) // 通过请求对象的params属性可以获取参数
    res.send('关于')
})

app.listen(3000) // 相当于 http.createServer(app).listen(3000)
/*
    app.listen底层实现如下

     app.listen = function () {
         var server = http.createServer(this)
         return server.listen.apply(server, arguments)
     }
*/
2.搭建静态资源库

利用express托管静态文件,可以提供诸如图像、CSS文件和JavaScript文件之类的静态文件

app.use(express.static('./static')) // 配置static为静态文件库,可以直接通过路径访问,可以同时配置多个静态文件库
// 用上面的方法配置静态文件库后,下面这个回调不会被执行,当访问根路径时,实际上是访问的static文件夹中的index.html
app.get('/', (req, res) => {
    res.send('Hello world!')
})

也可以给静态文件配置指定的访问路径

app.use('/test', express.static('./static')) // 访问静态文件时,路径前面必须加上test -> 127.0.0.1/test/img/a.jpg
// 由于访问静态文件必须加上test,所以访问根路径时下面的回调会被执行
app.get('/', (req, res) => {
    /* 
        发送HTTP响应,其body参数可以是一个Buffer对象,一个String,Object或者Array。
        send()方法只能使用一次,多次使用会报错,如果需要返回多个数据,可以使用write()和end()
    */
    res.send('Hellow world')
})
3.模板引擎

使用app.engine(ext, callback)方法创建自己的模板引擎。ext是指文件扩展名,callback是模板引擎函数,它接受以下参数:

  • filePath:模板文件的位置
  • options:选项对象,即渲染模板时传入的参数
  • callback:回调函数
// 模板文件index.ntl内容如下
<h1>#title#</h1>
<p>#message#</p>

// 映射模板引擎
let fs = require('fs')
app.engine('ntl', (filePath, options, callback) => {
    fs.readFile(filePath, (err, data) => {
        if (err) return callback(err)
        let rendered = data.toString().replace('#title#', options.title).replace('#message#', options.message)
        return callback(null, rendered)
    })
})
app.set('views', './views') // 设置模板文件目录,会自动在这个目录中寻找对应名称的模板文件
app.set('view engine', 'ntl') // 注册模板引擎,第二个参数为模板文件夹的后缀名

app.get('/', (req, res) => {
    res.render('index', { title: 'Hey', message: 'Hello world!' })
})

// 渲染结果
<h1>Hey</h1>
<p>Hello world!</p>

使用ejs模板时,不需要映射,内部已实现

app.set('views', './views')
app.set('view engine', 'ejs')
app.get('/', (req, res) => {
    res.render('topNews', newsList) // newsList是要绑定的数据
})
4.应用生成器

通过应用生成工具express-generator 可以快速创建一个应用骨架

  1. 使用 npm install express-generator -g 全局安装
  2. 通过 express --view=ejs myapp 快速创建应用(ejs为使用的模板,myapp为应用名称)
  3. 启动应用,DEBUG=myapp:* npm start(MacOS或Linux),set DEBUG=myapp:* & npm start(Windows)

生成的应用目录如下

app.js中代码解释

// 引用包文件
let createError = require('http-errors')
...
let logger = require('morgan')

// 引用路由
let indexRouter = require('./routers/index')
let usersRouter = require('./routers/users')

// 生成express实例
let app = express()

// 设置模板引擎为ejs
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'ejs')

// 使用中间件
app.use(logger('dev'))
/*
    下面两个中间件用来处理post请求的参数
    第一个处理json格式的,第二个处理form-data格式的
    如果不经过处理,通过req.body取不到请求参数
*/
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
...
app.use(express.static(path.join(__dirname, 'public')))

// 路由配置
app.use('/', indexRouter)
app.use('/users', usersRouter)

// 使用中间件处理404错误
app.use(function(req, res, next) {
    next(createError(404))
})

// 使用中间件处理错误
app.use(function(err, req, res, next) {
    res.locals.message = err.message
    ...
    res.render('error')
})

// 把app.js暴露出去
module.exports = app
5.中间件

中间件(Middleware)是一个函数,它可以访问请求对象,响应对象和web应用中处于请求-响应循环流程中的中间件,一般被命名为next的变量

中间件功能包括:

  • 执行任何代码
  • 修改请求和响应对象
  • 终结请求-响应循环
  • 调用堆栈中的下一个中间件

如果中间件没有终结请求-响应循环,则必须调用next()方法将控制权交个下一个中间件,否则请求会被挂起

Express 应用可以使用如下几种中间件:

  • 应用级中间件
  • 路由级中间件
  • 错误处理中间件
  • 内置中间件
  • 第三方中间件

应用级中间件绑定到app对象,使用 app.use() 和 app.METHOD(),其中METHOD是需要处理的HTTP请求方法,包括 GET、PUT、POST等

let app = express()
// 没有挂载路径的中间件,每个请求都会执行该中间件
app.use((req, res, next) => {
    console.log(Data.now())
    next()
})
// 挂载至 /user/:id 的中间件,任何指向 /user/:id 的请求都会执行它
app.use('/user/:id', (req, res, next) => {
    console.log(req.method)
    next()
})
// 路由及其处理函数(中间件系统),处理指向 /user/:id 的 GET 请求
app.get('/user/:id', (req, res, next) => {
    res.send('USER')
})

如果需要在中间件栈中跳过剩余中间件,调用 next('route') 方法将控制权交给下一个路由,next('route') 只对使用 app.METHOD() 或 router.METHOD() 加载的中间件有效

// 可以在一个挂载点上挂载一组中间件,从而创建一个子中间件栈
app.get('/users/:id', (req, res, next) => {
    // 如果 id 为 0,跳到下一个路由
    if (req.params.id === 0) next('route')
    else next()
}, (req, res, next) => {
    res.send('regular')
})
app.get('/users/:id', (req, res, next) => {
    res.send('special')
})

路由级中间件与应用级中间件一样,只是它绑定的对象为 express.Router()
使用 router.use() 和 router.METHOD() 函数加载路由级中间件

let router = express.Router()
router.get('/user/:id', (req, res, next) => {
    // 如果 id 为 0,跳到下一个路由
    if (req.params.id === 0) next('route')
    else next()
}, (req, res, next) => {
    res.send('regular')
})
router.get('/users/:id', (req, res, next) => {
    res.send('special')
})

错误处理中间件和其他中间件定义类似,只是要使用4个参数,即使参数用不到,也必须声明

app.use((err, req, res, next) => {
    console.error(err.stack)
    res.status(500).send('Something broke!')
})

内置中间件主要有以下三种:

  1. express.static 提供静态资源
  2. express.json 解析JSON格式的post请求参数
  3. express.urlencoded 解析Form-Data格式的post请求参数

第三方中间件可以为express应用程序添加功能

let cookieParser = require('cookie-parser')
app.use(cookieParser())
6.路由

路由是指应用程序的端点(URL)如何响应客户端的请求
可以使用Express app对象的方法定义路由。例如,app.get()处理get请求,app.post()处理post请求。也可以使用app.all()来处理所有HTTP请求,并使用app.use()将中间件指定为回调函数

let express = require('express')
let app = express()

app.get('/', (req, res) => {
    res.send('GET request to the homepage')
})
app.post('/', (req, res) => {
    res.send('POST request to the homepage')
})
// 对路由 '/secret' 的请求执行处理程序,无论请求方法是什么
app.all('/secret', (req, res, next) => {
    console.log('Accessing the secret section ...')
    next()
})

可以使用一组回调函数来处理路由

let cb1 = (req, res, next) => {
    console.log('111')
    next()
}
let cb2 = (req, res, next) => {
    console.log('222')
    next()
}
app.get('/', [cb1, cb2])

下表中响应对象(res)的方法向客户端返回响应,终结请求-响应循环

方法 描述
res.download() 提示下载文件
res.end 终结响应处理流程
res.json() 发送一个JSON格式的响应
res.jsonp() 发送一个支持JSONP的JSON格式响应
res.redirect() 重定向请求
res.render() 渲染视图模板
res.send() 发送各种类型的响应
res.sendFile() 以八位字节流的形式发送文件
res.sendStatus() 设置响应状态代码,并将其以字符串的形式作为响应体的一部分发送

可以使用app.route()创建链式路由处理程序

app.route('/book')
    .get((req, res) => {
        res.send('Get a random book')
    })
    .post((req, res) => {
        res.send('Add a book')
    })
    .put((req, res) => {
        res.send('Update the book')
    })

可使用express.Router 类创建模块化、可挂载的路由处理程序。Router 实例是一个完整的中间件和路由系统,因此被称为“mini-app”

下面的实例创建了一个路由模块,并加载了一个中间件,定义了一些路由,并且将它们挂载至应用的路径上

// birds.js
let express = require('express')
let router = express.Router()
// 该路由使用的中间件
router.use((req, res, next) => {
    console.log('Time: ', Date.now())
    next()
})
// 定义网站主页的路由,匹配 /birds
router.get('/', (req, res) => {
    res.send('Birds home page')
})
// 定义 about 页面的路由,匹配 /birds/about
router.get('/about', (req, res) => {
    res.send('About birds')
})
module.exports = router

// 在应用中加载路由模块
let birds = require('./birds')
...
app.use('/birds', birds)
7.使用session

在express中使用session,需要安装并引入express-session,然后在入口文件(app.js)使用中间件

const session = require(‘express-session’)

app.use(session({
    name: '', // cookie的name
    secret: '', //通过设置的secret字符串,来计算hash值并放在cookie中
    resave: true, // 强制保存session,即使它没有变化
    rolling: false, // 在每次请求时设置cookie,将重置cookie过期时间
    saveUninitialized: true, // 强制将未初始化的session存储,默认为true
    cookie: { maxAge: 1000 * 86400 }, // 设置cookie的过期时间,单位为ms
    store: {} // session的存储方式,默认存储在内存中,可以设置持久化存储在数据库中
}))

// 可通过req.headers.cookie获取到请求的cookie

持久化存储session到mongodb中,需要安装并引入connect-mongo

let MongoStore = require(‘connect-mongo’)(session)
app.use(session({
    …,
    store: new MongoStore({
        url: 'mongodb://localhost:27017/dbname',
        touchAfter: 24 * 3600 // 多长时间往数据库中更新一次存储,除了在会话上更改某些数据外
    })
}))

九. Socket.io

Socket.io将Websocket和轮询(Polling)机制以及其他的实时通信方式封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码

1.服务端的使用
let socketIO = require('socket.io') // 引入socket.io
let io = socketIO(server) // 基于server(http.createServer创建的对象)生成socketIO实例对象,
/*
    当有客户端向服务器建立连接的时候,‘connection’事件就会被激发。
    对应的回调函数就会执行。回调函数的参数socket就是客户端与服务器的连接对象
*/
io.on('connection', socket => { 
    socket.emit('msg', '连接成功') // 建立连接时向客户端发送消息
    // 监听客户端发送的消息
    socket.on('msg', data => {
        socket.emit('msg', '收到!')
    })
    // 监听连接关闭
    socket.on('disconnect', () => {
        console.log('连接关闭')
    })
})

io.sockets.emit('String', msg) // 给所有客户端广播消息
io.sockets.socket(socketId).emit('String', msg) // 给指定客户端广播消息
2.客户端的使用
<script src='/socket.io/socket.io.js'></script>
let socket = io.connect('http://127.0.0.1:3000') // 建立连接
socket.on('connect', () => {
    console.log('已建立连接')
    socket.emit('open') // 打开通道
})
// 监听服务器发送的消息
socket.on('msg', msg => {
    console.log(msg)
})
// socket.disconnect() // 调用disconnect()方法断开连接
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342