nodejs

目录

目录

Node.js

一、Node.js基础

1. 认识Node.js

01 nodejs的特性

02 浏览器环境vs node环境

2. 模块、包、commonJS

3. Npm&Yarn

01 npm的使用

02 package.json和package-lock.json

02 全局安装 nrm

03 yarn使用

5. 内置模块

01 http模块:作为服务器

(1)server对象

(2)server的事件

(3)server的request事件的参数req

(4)server的request事件的参数res

02 http模块:作为客户端

(1)http.request(options,callback)

(2)http.get(options,callback)

(3)http.ClientRequest

(4)http.ClientReponse

03 url模块

04 querystring模块,已被弃用,但仍然可用。

05 http模块补充

(1)接口:jsonp

(2) 跨域:CORS

(3) 模拟get

(4) 模拟post:服务器提交(攻击)

(5) 爬虫

05 event模块

06 fs文件操作模块

07 stream流模块

09 crypto

6. 路由

01 基础

02 获取参数

03 静态资源处理

二、Express

1.安装   npm install express --save

2.路由

(1)路由路径

(2)回调函数

4.中间件

(1)应用级中间件

(2)路由级中间件

(3)错误处理中间件

(4)内置的中间件

(5)第三方中间件

5. 获取请求参数

6.利用 Express 托管静态文件

7.服务端渲染(模板引擎)和服务端渲染

(1)服务端渲染模板

(2)客户端渲染

(3)node中间层

(4)服务端渲染(ssr)

(5)服务端渲染的实现方式

8.生成器

三、MongoDB

nodejs连接操作数据库

四、接口规范与业务分层

五、登录鉴权

1. Cookie&Session

2. JSON Web Token (JWT)

六、文件上传管理

七、APIDOC - API 文档生成工具

八、Koa2

九、Socket编程


Node.js

一、Node.js基础

1. 认识Node.js

Node.js是一个javascript运行环境。它让javascript可以开发后端程序,实现几乎其他后端语言实现的所有功能,可以与PHP、Java、Python、.NET、Ruby等后端语言平起平坐。

Nodejs是基于V8引擎,V8是Google发布的开源JavaScript引擎,本身就是用于Chrome浏览器的js解释部分,但是Ryan Dahl 这哥们,鬼才般的,把这个V8搬到了服务器上,用于做服务器的软件。

01 nodejs的特性

  • Nodejs语法完全是js语法,只要你懂js基础就可以学会Nodejs后端开发

  • NodeJs超强的高并发能力,实现高性能服务器

  • 开发周期短、开发成本低、学习成本低

02 浏览器环境vs node环境

nodejs_第1张图片

Node.js 可以解析JS代码(没有浏览器安全级别的限制)提供很多系统级别的API,如:

  • 文件的读写 (File System)

    const fs = require('fs')
    
    fs.readFile('./ajax.png', 'utf-8', (err, content) => {
      console.log(content)
    })
  • 进程的管理 (Process)

    function main(argv) {
      console.log(argv)
    }
    
    main(process.argv.slice(2))
    
  • 网络通信 (HTTP/HTTPS)

    const http = require("http")
    
    http.createServer((req,res) => {
      res.writeHead(200, {
        "content-type": "text/plain"
      })
      res.write("hello nodejs")
      res.end()
    }).listen(3000)
    

2. 模块、包、commonJS

模块化简介

在node中,一个js文件就是一个模块。

每一个js文件中的js代码都是独立运行在一个函数中,而不是在全局作用域,所以一个模块中的变量和函数在其他模块中是不能访问的。

如果要让外部可以访问模块里面的方法或者属性,就必须在模块里面通过 exports 或者 module.exports 暴露属性或者方法。使用require引入。

exports:对象,该对象用来将变量和函数暴露到外部
require:函数,用来引入外部的模块
module:模块,代表我们当前的模块

通过exports只能用.的方式向外暴露自身变量和函数
module.exports既可以通过.的方式,也可以通过直接赋值的方式
    module.exports.xxx = xxx
    module.exports = {}
注意点:exports就是module的一个属性,即module.exports==exports 为true,但是exports不能通过对象的方式向外暴露属性

// m1.js
const name = 'gp19'
const sayName = () => {
  console.log(name)
}
// 接口暴露方法一:使用exports.xxx一个一个向外暴露
exports.name = name
exports.sayName = sayName
// 接口暴露方法二:使用module.exports把要暴露的内容放到一个对象中,批量向外暴露
module.exports = {
  name:name
  sayName: sayName
}
// 错误方法,不能exports一个对象
exports = {
  name:name
  sayName: sayName
}

// 其他文件通过require引入即可
// m2.js
 const m1 = require('./m1')
m1.say()

如果需要使用es6的模块化规范,需要在package.json中添加"type"="module"

3. Npm&Yarn

01 npm的使用

npm   init        //构建项目说明,生成 package.json文件
npm install 包名 –g  //或者uninstall,update,表示全局安装,卸载或者更新
npm install 包名 --save-dev //uninstall,update 表示全局安装,卸载或者更新 
//而参数--save 的作用是在项目下的package.json文件记录安装过的依赖包名称,当复制项目到另外的电脑上,只需运行命令: npm i 就能自动安装项目用到的依赖包
npm list -g //(不加-g,列举当前目录下的安装包)
npm info 包名 //(详细信息) npm info 包名 version(获取最新版本)
npm install md5@1//(安装指定版本)
npm outdated  //( 检查包是否已经过时)

02 package.json和package-lock.json

package.json 是在运行 “ npm init ”时生成的,主要记录项目依赖,有以下结构

  • name:项目名,也就是在使用npm init 初始化时取的名字,但是如果使用的是npm init -y 快速初始化的话,那这里的名字就是默认存放这个文件的文件名;
  • version:版本号;
  • private:希不希望授权别人以任何形式使用私有包或未发布的;
  • scripts-serve:是vue的项目启动简写配置;
  • scripts-build:是vue的打包操作简写配置;
  • dependencies:指定了项目运行时所依赖的模块;
  • devDependencies:指定项目开发时所需要的模块,也就是在项目开发时才用得上,一旦项目打包上线了,就将移除这里的第三方模块;

package-lock.json是在运行“npm install”时生成的一个文件,用于记录当前状态下项目中实际安装的各个package的版本号、模块下载地址、及这个模块又依赖了哪些依赖。

区别:

为什么有了package.json,还需要package-lock.json文件呢?

当项目中已有 package-lock.json 文件,在安装项目依赖时,将以该文件为主进行解析安装指定版本依赖包,而不是使用 package.json 来解析和安装模块。因为 package 只是指定的版本不够具体,而package-lock 为每个模块及其每个依赖项指定了版本,位置和完整性哈希,所以它每次创建的安装都是相同的。无论你使用什么设备,或者将来安装它都无关紧要,每次都应该给你相同的结果。

dependencies字段的说明:

"dependencies": { "md5": "^2.1.0"  } ^ 表示 如果 直接npm install 将会安md5的2.*.* 的最新版本
"dependencies": { "md5": "~2.1.0"  } ~ 表示 如果 直接npm install 将会安装md5 2.1.* 的最新版本
"dependencies": {    "md5": "*"  }  * 表示 如果 直接npm install 将会安装 md5的最新版本

02 全局安装 nrm

NRM (npm registry manager)是npm的镜像源管理工具,有时候国外资源太慢,使用这个就可以快速地在 npm 源间切换。

手动切换方法: npm config set registry https://registry.npm.taobao.org

安装 nrm

        在命令行执行命令,npm install -g nrm,全局安装nrm。

使用 nrm

        执行命令 nrm ls 查看可选的源。 其中,带*的是当前使用的源,上面的输出表明当前源是官方源。

切换 nrm

        如果要切换到taobao源,执行命令nrm use taobao。

测试速度

        你还可以通过 nrm test 测试相应源的响应时间。

03 yarn使用

npm install -g yarn
对比npm:
	速度超快: Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。
    超级安全: 在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。

开始新项目
	yarn init 
添加依赖包
	yarn add [package] 
	yarn add [package]@[version] 
	yarn add [package] --dev 
升级依赖包
	 yarn upgrade [package]@[version] 
移除依赖包
	 yarn remove [package]
	 
安装项目的全部依赖
	 yarn install 

5. 内置模块

01 http模块:作为服务器

(1)server对象

// 引入http模块
const http = require('http');
// 创建本地服务器来从其接收数据
const server = http.createServer((req, res) => {
    //设置响应头信息
  res.writeHead(200, { 'Content-Type': 'application/json' });
    //设置响应体信息
  res.end(JSON.stringify({
    data: 'Hello World!'
  }));
});
//启动服务器,8000端口
server.listen(8000);
 
  
const http = require('http');
// 创建本地服务器来从其接收数据
const server = http.createServer();
// 监听请求事件,on方法为server绑定了request事件,只要客户端请求服务器的数据就会触发
server.on('request', (req, res) => {
    //req:请求对象,包含了与客户端相关的数据和属性
    //		req.url:客户端请求的 URL 地址
    //		req.method:客户端的 method 请求类型
    
    //设置 Content-Type 响应头,charset=utf-8防止中文乱码
    res.setHeader("Content-Type","text/html; charset=utf-8");
  	res.writeHead(200, { 'Content-Type': 'application/json' });
    //调用 res.end() 向客户端响应一些内容,调用后会关闭流。而res.write()也是响应一些内容,但不会关闭流
  	res.end(JSON.stringify({
    	data: 'Hello World!'
  	}));
});
//启动服务器
server.listen(8000);

第二段代码是通过直接创建一个Server对象,然后为其添加request事件监听。

而server.on()里面配置的写法和在http.createServer()里面直接配置的写法效果是一样的。

其实也就说createServer方法其实本质上也是为http.Server对象添加了一个request事件监听,这似乎更好理解了。

(2)server的事件

http.createServer创建的server对象是一个基于事件的服务器,它是继承自EventEmitter,事实上,nodejs中大部分模块都继承自EventEmitter,包括fs、net等模块,这也是为什么说nodejs基于事件驱动(关于EventEmitter的更多内容可以在官方api下的events模块找到),server提供的事件如下:

  • request:当客户端请求到来时,该事件被触发,提供两个参数req和res,表示请求和响应信息,是最常用的事件

  • connection:当TCP连接建立时,该事件被触发,提供一个参数socket,是net.Socket的实例

  • close:当服务器关闭时,触发事件(注意不是在用户断开连接时)

其中request事件是最常用的,而参数req和res分别是http.IncomingMessage和http.ServerResponse的实例

(3)server的request事件的参数req

req是http.IncomingMessage的实例,http.IncomingMessage是HTTP请求的信息,是后端开发者最关注的内容,一般由server的request事件发送,并作为第一个参数传递,其提供了3个事件,如下:

  • data:当请求体数据到来时,该事件被触发,该事件提供一个参数chunk,表示接受的数据,如果该事件没有被监听,则请求体会被抛弃,该事件可能会被调用多次(这与nodejs是异步的有关系)

  • end:当请求体数据传输完毕时,该事件会被触发,此后不会再有数据

  • close:用户当前请求结束时,该事件被触发,不同于end,如果用户强制终止了传输,也是用close

req实例的属性

nodejs_第2张图片

(4)server的request事件的参数res

res是http.ServerResponse的实例,http.ServerResponse是返回给客户端的信息,决定了用户最终看到的内容,一般也由server的request事件发送,并作为第二个参数传递,它有三个重要的成员函数,用于返回响应头、响应内容以及结束请求

  • res.writeHead(statusCode,[heasers]):向请求的客户端发送响应头,该函数在一个请求中最多调用一次,如果不调用,则会自动生成一个响应头

  • res.write(data,[encoding]):想请求的客户端发送相应内容,data是一个buffer或者字符串,如果data是字符串,则需要制定编码方式,默认为utf-8,在res.end调用之前可以多次调用

  • res.end([data],[encoding]):结束响应,告知客户端所有发送已经结束,当所有要返回的内容发送完毕时,该函数必需被调用一次,两个可选参数与res.write()相同。如果不调用这个函数,客户端将用于处于等待状态。

02 http模块:作为客户端

http模块提供了两个函数http.request和http.get,功能是作为客户端向http服务器发起请求。

(1)http.request(options,callback)

options是一个类似关联数组的对象,表示请求的参数。常用的参数有host、port(默认为80)、method(默认为GET)、path(请求的相对于根的路径,默认是“/”,其中querystring应该包含在其中,例如/search?query=byvoid)、headers(请求头内容)。

callback作为回调函数,需要传递一个参数,为http.ClientResponse的实例,http.request返回一个http.ClientRequest的实例,一般叫req。

需要记住的是,如果我们使用http.request方法时需要利用返回的http.ClientRequest的实例调用end方法,没有调用end方法,服务器将不会收到信息。

(2)http.get(options,callback)

这个方法是http.request方法的简化版,唯一的区别是http.get自动将请求方法设为了GET请求,同时不需要手动调用req.end()。

因为http.get和http.request方法都是返回一个http.ClientRequest对象,所以我们来看一下这两个对象。

(3)http.ClientRequest

http.ClientRequest是由http.request或者是http.get返回产生的对象,表示一个已经产生而且正在进行中的HTPP请求,提供一个response事件,也就是我们使用http.get和http.request方法中的回调函数所绑定的对象,我们可以显式地绑定这个事件的监听函数

var http=require("http");
var options={    
    hostname:"cn.bing.com",
    port:80
}
var req=http.request(options);
req.on("response",function(res){
    res.setEncoding("utf-8");
    res.on("data",function(chunk){
        console.log(chunk.toString())
    });
    console.log(res.statusCode);
})
req.on("error",function(err){
    console.log(err.message);
});

​​​​​​​req.end();

http.ClientRequest也提供了write和end函数,用于向服务器发送请求体,通常用于POST、PUT等操作,所有写操作都必须调用end函数来通知服务器,否则请求无效。此外,这个对象还提供了abort()、setTimeout()等方法,具体可以参考文档

(4)http.ClientReponse

与http.ServerRequest相似,提供了三个事件,data、end、close,分别在数据到达、传输结束和连接结束时触发,其中data事件传递一个参数chunk,表示接受到的数据。其属性如下

nodejs_第3张图片

此外,这个对象提供了几个特殊的函数

  • response。setEncoding([encoding]):设置默认的编码,当data事件被触发时,数据将会以encoding编码,默认值是null,也就是不编码,以buffer形式存储

  • response.pause():暂停结束数据和发送事件,方便实现下载功能

  • response.resume():从暂停的状态中恢复

03 url模块

1.   parse会解析url并转换成一个对象,方便我们对其进行操作。

url模块的以上方法已被弃用,替换为URL类,详见文档。

const url = require('url')
const urlString = 'https://www.baidu.com:443/ad/index.html?id=8&name=mouse#tag=110'
const parsedStr = url.parse(urlString)
//会生成一个对象
//Url {
//  protocol: null,
//  slashes: null,
//  auth: null,
//  host: null,
//  port: null,
//  hostname: null,
//  hash: null,
//  search: null,
//  query: null,
//  pathname: '/home',
//  path: '/home',
//  href: '/home'
//}
console.log(parsedStr)

2.   format可以将一个对象转换成url

const url = require('url')
const urlObject = {
  protocol: 'https:',
  slashes: true,
  auth: null,
  host: 'www.baidu.com:443',
  port: '443',
  hostname: 'www.baidu.com',
  hash: '#tag=110',
  search: '?id=8&name=mouse',
  query: { id: '8', name: 'mouse' },
  pathname: '/ad/index.html',
  path: '/ad/index.html?id=8&name=mouse'
}
const parsedObj = url.format(urlObject)
console.log(parsedObj)

02.3 resolve用于拼接url

const url = require('url')
var a = url.resolve('/one/two/three', 'four')  ( 注意最后加/ ,不加/的区别 )
//如果three后面加/那么就是在路径最后添加 	a='/one/two/three/four'   
//如果three后面不加/那么就是替换最后一个路径	a='/one/two/four'
var b = url.resolve('http://example.com/', '/one')
//b= 'http://example.com/one'
var c = url.resolve('http://example.com/one', '/two')
//c= 'http://example.com/two'

04 querystring模块,已被弃用,但仍然可用。

官方推荐URLSearchParams替代。或者使用querystringify插件

1. parse 把key=value&key=value形式的字符串转为对象

const querystring = require('querystring')
var qs = 'x=3&y=4'
var parsed = querystring.parse(qs)
console.log(parsed)
//{
//  x: '3',
//  y: '4'
//}

2. stringify 把对象转为key=value&key=value形式的字符串

const querystring = require('querystring')
var qo = {
  x: 3,
  y: 4
}
var parsed = querystring.stringify(qo)
console.log(parsed)
//x=3&y=4

3. escape/unescape

const querystring = require('querystring')
var str = 'id=3&city=北京&url=https://www.baidu.com'
var escaped = querystring.escape(str)
console.log(escaped)
//id%3D3%26city%3D%E5%8C%97%E4%BA%AC%26url%3Dhttps%3A%2F%2Fwww.baidu.com
const querystring = require('querystring')
var str = 'id%3D3%26city%3D%E5%8C%97%E4%BA%AC%26url%3Dhttps%3A%2F%2Fwww.baidu.com'
var unescaped = querystring.unescape(str)
console.log(unescaped)
//id=3&city=北京&url=https://www.baidu.com

05 http模块补充

(1)接口:jsonp

//后端代码
const http = require('http')
const url = require('url')

const app = http.createServer((req, res) => {
  let urlObj = url.parse(req.url, true)

  switch (urlObj.pathname) { 
    case '/api/user':
      res.end(`${urlObj.query.callback}({"name": "gp145"})`)
      break
    default:
      res.end('404.')
      break
  }
})
app.listen(8080, () => {
  console.log('localhost:8080')
})
前端的html文件中

(2) 跨域:CORS​​​​​​​

const http = require('http')
const url = require('url')
const querystring = require('querystring')

const app = http.createServer((req, res) => {
  let data = ''
  let urlObj = url.parse(req.url, true)

  res.writeHead(200, {
    'content-type': 'application/json;charset=utf-8',
      //只要在后端配置下面的语句就可以实现CORS解决跨域
    'Access-Control-Allow-Origin': '*'
  })
 req.on('data', (chunk) => {
    data += chunk
  })

  req.on('end', () => {
    responseResult(querystring.parse(data))
  })

  function responseResult(data) {
    switch (urlObj.pathname) {
      case '/api/login':
        res.end(JSON.stringify({
          message: data
        }))
        break
      default:
        res.end('404.')
        break
    }
  }
})

app.listen(8080, () => {
  console.log('localhost:8080')
})

(3) 模拟get

node也可以当作客户端,向服务器发信息,然后将信息传给前端。相当于做一个中间人。

var http = require('http')
var https = require('https')

// 1、接口 2、跨域
const server = http.createServer((request, response) => {
  var url = request.url.substr(1)

  var data = ''

  response.writeHeader(200, {
    'content-type': 'application/json;charset=utf-8',
    'Access-Control-Allow-Origin': '*'
  })
// http.get方法不需要在最后调用req.end()
  https.get(`https://m.lagou.com/listmore.json${url}`, (res) => {
 	//data事件,当服务端接收到数据时触发
    res.on('data', (chunk) => {
      data += chunk
    })
	//end事件数据接收完时触发
    res.on('end', () => {
      response.end(JSON.stringify({
        ret: true,
        data
      }))
    })
  })

})
server.listen(8080, () => {
  console.log('localhost:8080')
})

(4) 模拟post:服务器提交(攻击)

const https = require('https')
const querystring = require('querystring')

const postData = querystring.stringify({
  province: '上海',
  city: '上海',
  district: '宝山区',
  address: '同济支路199号智慧七立方3号楼2-4层',
  latitude: 43.0,
  longitude: 160.0,
  message: '求购一条小鱼',
  contact: '13666666',
  type: 'sell',
  time: 1571217561
})
const options = {
  protocol: 'https:',
  hostname: 'ik9hkddr.qcloud.la',
  method: 'POST',
  port: 443,
  path: '/index.php/trade/add_item',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': Buffer.byteLength(postData)
  }
}

function doPost() {
  let data

  let req = https.request(options, (res) => {
    res.on('data', chunk => data += chunk)
    res.on('end', () => {
      console.log(data)
    })
  })
  req.write(postData)
// http.request方法却需要在最后调用end()方法
  req.end()
}

// setInterval(() => {
//   doPost()
// }, 1000)

​​​​​​​(5) 爬虫

const https = require('https')
const http = require('http')
//cheerio模块用于解析数据
const cheerio = require('cheerio')

http.createServer((request, response) => {
  response.writeHead(200, {
    'content-type': 'application/json;charset=utf-8'
  })

  const options = {
    // protocol: 'https:',
    hostname: 'i.maoyan.com',
    port: 443,
    path: '/',
    method: 'GET'
  }
 const req = https.request(options, (res) => {
    let data = ''
    res.on('data', (chunk) => {
      data += chunk
    })

    res.on('end', () => {
      filterData(data)
    })
  })
  function filterData(data) {
    //   console.log(data)
    let $ = cheerio.load(data)
    let $movieList = $('.column.content')
    console.log($movieList)
    let movies = []
    $movieList.each((index, value) => {
      movies.push({
        title: $(value).find('.movie-title .title').text(),
        detail: $(value).find('.detail .actor').text(),
      })
    })
//传给前端需要转为JSON形式
    response.end(JSON.stringify(movies))
  }

  req.end()
}).listen(3000)

05 event模块

const EventEmitter = require('events')

class MyEventEmitter extends EventEmitter {}

const event = new MyEventEmitter()
//每次on都是挂载新方法,不会覆盖原挂载的方法,所以,需要每次要重新new MyEventEmitter()
event.on('play', (movie) => {
  console.log(movie)
})

event.emit('play', '我和我的祖国')
event.emit('play', '中国机长')

06 fs文件操作模块

const fs = require('fs')
// 可以写相对路径,也可以绝对路径
// 第二个参数回调函数的参数err,当报错时err为一个对象,否则为null
// 创建文件夹
fs.mkdir('./logs', (err) => {
  console.log('done.')
})

// 文件夹改名
fs.rename('./logs', './log', () => {
  console.log('done')
})

// 删除文件夹,如果文件夹下有内容,该文件夹是删不掉的,要先unlink删除文件夹下的文件
fs.rmdir('./log', () => {
  console.log('done.')
})

// 写内容到文件里
//第一个参数:路径  第二个参数:写入文件的内容(会覆盖原内容)
fs.writeFile('./logs/log1.txt','hello',
  // 错误优先的回调函数
  (err) => {
    if (err) {
      console.log(err.message)
    } else {
      console.log('文件创建成功')
    }
  }
)

// 给文件追加内容
fs.appendFile('./logs/log1.txt', '\nworld', () => {
  console.log('done.')
})

// 读取文件内容
fs.readFile('./logs/log1.txt', 'utf-8', (err, data) => {
  console.log(data)
})

// 删除文件
fs.unlink('./logs/log1.txt', (err) => {
  console.log('done.')
})

// 批量写文件
for (var i = 0; i < 10; i++) {
  fs.writeFile(`./logs/log-${i}.txt`, `log-${i}`, (err) => {
    console.log('done.')
  })
}

//读取文件目录信息,data有b.txt文件的一些信息
fs.stat("./avatar/b.txt",(err,data)=>{
    //isFile表示是否是一个文件
    console.log(data.isFile())
    //isDirectory表示是否是一个目录
    console.log(data.isDirectory())
})

// 读取文件/目录信息
fs.readdir('./', (err, data) => {
  data.forEach((value, index) => {
    fs.stat(`./${value}`, (err, stats) => {
      // console.log(value + ':' + stats.size)
      console.log(value + ' is ' + (stats.isDirectory() ? 'directory' : 'file'))
    })
  })
})

//删除文件夹,readdir只能删除空文件夹。
fs.readdir("./avatar",(err,data)=>{
    // console.log(data)
    //所以,这里要把文件夹下的文件都删掉
    data.forEach(item=>{
        fs.unlink(`./avatar/${item}`,(err)=>{})
        //fs.unlinkSync(`./avatar/${item}`,(err)=>{})
    })
	//删除文件夹。
    fs.rmdir("./avatar",(err)=>{
        console.log(err)
    })
})
//这里会有问题,当avatar下文件多的时候,可能avatar下的文件还没删完,就执行rmdir了,然后avatar删不了
//因为fs下的这些方法,第二个参数都是回调函数的形式,当执行fs.unlink的时候,需要里面的回调函数(err)=>{}执行完以后才表示删完了。但是因为不会阻塞后面的代码执行,执行fs.rmdir的时候可能avatar下的文件还没删完,就报错了。
// 可以通过使用readFileSync,mkdirSync等方法来替代原方法,不过需要包一个try-catch,因为如果创建一个已经存在的文件,系统会报错,并且又会阻塞代码运行,代码就会卡住。

// 同步读取文件
try {
  const content = fs.readFileSync('./logs/log-1.txt', 'utf-8')
  console.log(content)
  console.log(0)
} catch (e) {
  console.log(e.message)
}

// 异步读取文件:方法一,会导致回调地狱,不推荐
fs.readFile('./logs/log-0.txt', 'utf-8', (err, content) => {
  console.log(content)
  console.log(0)
})
console.log(1)

// 异步读取文件:方法二
const fs = require("fs").promises
fs.readFile('./logs/log-0.txt', 'utf-8').then(result => {
  console.log(result)
})

fs模块中,提供同步方法是为了方便使用。那我们到底是应该用异步方法还是同步方法呢?

由于Node环境执行的JavaScript代码是服务器端代码,所以,绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,否则,同步代码在执行时期,服务器将停止响应,因为JavaScript只有一个执行线程。

服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。

07 stream流模块

stream是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。

什么是流?流是一种抽象的数据结构。想象水流,当在水管中流动时,就可以从某个地方(例如自来水厂)源源不断地到达另一个地方(比如你家的洗手池)。我们也可以把数据看成是数据流,比如你敲键盘的时候,就可以把每个字符依次连起来,看成字符流。这个流是从键盘输入到应用程序,实际上它还对应着一个名字:标准输入流(stdin)。

如果应用程序把字符一个一个输出到显示器上,这也可以看成是一个流,这个流也有名字:标准输出流(stdout)。流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。

有些流用来读取数据,比如从文件读取数据时,可以打开一个文件流,然后从文件流中不断地读取数据。有些流用来写入数据,比如向文件写入数据时,只需要把数据不断地往文件流中写进去就可以了。

在Node.js中,流也是一个对象,我们只需要响应流的事件就可以了:data事件表示流的数据已经可以读取了,end事件表示这个流已经到末尾了,没有数据可以读取了,error事件表示出错了。

var fs = require('fs');

// 打开一个流:
var rs = fs.createReadStream('sample.txt', 'utf-8');

rs.on('data', function (chunk) {
    console.log('DATA:')
    console.log(chunk);
});

rs.on('end', function () {
    console.log('END');
});

rs.on('error', function (err) {
    console.log('ERROR: ' + err);
});

要注意,data事件可能会有多次,每次传递的chunk是流的一部分数据。

要以流的形式写入文件,只需要不断调用write()方法,最后以end()结束:

var fs = require('fs');

var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream写入文本数据...\n');
ws1.write('END.');
ws1.end();

pipe 就像可以把两个水管串成一个更长的水管一样,两个流也可以串起来。一个Readable流和一个Writable流串起来后,所有的数据自动从Readable流进入Writable流,这种操作叫pipe

在Node.js中,Readable流有一个pipe()方法,就是用来干这件事的。

让我们用pipe()把一个文件流和另 一个文件流串起来,这样源文件的所有数据就自动写入到目标文件里了,所以,这实际上是一个复制文件的程序:

const fs = require('fs')

const readstream = fs.createReadStream('./1.txt')
const writestream = fs.createWriteStream('./2.txt')

readstream.pipe(writestream)
//这样1.txt的内容就写到2.txt里面了

08 zlib

服务器传数据给客户端的时候,可以将数据压缩,这样可以减少流量的消耗和响应时间。

const fs = require('fs')
const zlib = require('zlib')

const gzip = zlib.createGzip()

const readstream = fs.createReadStream('./note.txt')
const writestream = fs.createWriteStream('./note2.txt')

readstream.pipe(gzip).pipe(writestream)

09 crypto

crypto模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能不是不可能,但速度会非常慢。Nodejs用C/C++实现这些算法后,通过cypto这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。

MD5是一种常用的哈希算法,用于给任意数据一个“签名”。这个签名通常用一个十六进制的字符串表示:

const crypto = require('crypto');

const hash = crypto.createHash('md5');

// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');

console.log(hash.digest('hex')); 

update()方法默认字符串编码为UTF-8,也可以传入Buffer。

如果要计算SHA1,只需要把'md5'改成'sha1',就可以得到SHA1的结果1f32b9c9932c02227819a4151feed43e131aca40

Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥:

const crypto = require('crypto');

const hmac = crypto.createHmac('sha256', 'secret-key');

hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');

console.log(hmac.digest('hex')); // 80f7e22570...

只要密钥发生了变化,那么同样的输入数据也会得到不同的签名,因此,可以把Hmac理解为用随机数“增强”的哈希算法。

AES是一种常用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,但是需要自己封装好函数,便于使用:

const crypto = require("crypto");

function encrypt (key, iv, data) {
    let decipher = crypto.createCipheriv('aes-128-cbc', key, iv);
    // decipher.setAutoPadding(true);
    return decipher.update(data, 'binary', 'hex') + decipher.final('hex');
}

function decrypt (key, iv, crypted) {
     crypted = Buffer.from(crypted, 'hex').toString('binary');
     let decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
     return decipher.update(crypted, 'binary', 'utf8') + decipher.final('utf8');
}
key,iv必须是16个字节

可以看出,加密后的字符串通过解密又得到了原始内容。

6. 路由

01 基础

var fs = require("fs")
var path = require("path")

function render(res, path) {
    res.writeHead(200, { "Content-Type": "text/html;charset=utf8" })
    res.write(fs.readFileSync(path, "utf8"))
    res.end()
}


const route = {
    "/login": (req, res) => {
        render(res, "./static/login.html")
    },

    "/home": (req, res) => {
        render(res, "./static/home.html")
    },
    "/404": (req, res) => {
        res.writeHead(404, { "Content-Type": "text/html;charset=utf8" })
        res.write(fs.readFileSync("./static/404.html", "utf8"))
    }
}

02 获取参数

get请求

    "/api/login":(req,res)=>{
        const myURL = new URL(req.url, 'http://127.0.0.1:3000');
        console.log(myURL.searchParams.get("username"))   
        render(res,`{ok:1}`)
    }

post请求

"/api/login": (req, res) => {
        var post = '';
        // 通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中
        req.on('data', function (chunk) {
            post += chunk;
        });

        // 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
        req.on('end', function () {
            post = JSON.parse(post);
            render(res, `{ok:1}`)
        });
    }

03 静态资源处理

function readStaticFile(req, res) {
    const myURL = new URL(req.url, 'http://127.0.0.1:3000')
    var filePathname = path.join(__dirname, "/static", myURL.pathname);

    if (fs.existsSync(filePathname)) {
        // console.log(1111)
        res.writeHead(200, { "Content-Type": `${mime.getType(myURL.pathname.split(".")[1])};charset=utf8` })
        res.write(fs.readFileSync(filePathname, "utf8"))
        res.end()
        return true
    } else {
        return false
    }
}

二、Express

基于 Node.js 平台,快速、开放、极简的 web 开发框架。

1.安装   npm install express --save

2.路由

路由是指如何定义应用的端点(URIs)以及如何响应客户端的请求。

路由是由一个 URI、HTTP 请求(GET、POST等)和若干个句柄组成,它的结构如下: app.METHOD(path, [callback...], callback)

        app 是 express 对象的一个实例

         METHOD 是一个 HTTP 请求方法

        path 是服务器上的路径

        callback 是当路由匹配时要执行的函数。

下面是一个基本的路由示例:

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

// respond with "hello world" when a GET request is made to the homepage
app.get('/', function(req, res) {
  res.send('hello world');
});

(1)路由路径

可以是字符串、字符串模式或者正则表达式。

路由路径是字符串:

// 匹配根路径的请求
app.get('/', function (req, res) {
  res.send('root');
});

// 匹配 /about 路径的请求
app.get('/about', function (req, res) {
  res.send('about');
});

// 匹配 /random.text 路径的请求
app.get('/random.text', function (req, res) {
  res.send('random.text');
});

路由路径是字符串模式:

// 匹配 acd 和 abcd
app.get('/ab?cd', function(req, res) {
  res.send('ab?cd');
});

// /ab/:id 表示匹配 /ab/xxx 
// /ab/:id/:id2 表示匹配 /ab/xxx/xxx
app.get('/ab/:id', function(req, res) {
  res.send('aaaaaaa');
});

// 匹配 abcd、abbcd、abbbcd等,其中b+的+表示b可以重复多次
app.get('/ab+cd', function(req, res) {
  res.send('ab+cd');
});

// 匹配 abcd、abxcd、abRABDOMcd、ab123cd等,*表示匹配任意字符
app.get('/ab*cd', function(req, res) {
  res.send('ab*cd');
});

// 匹配 /abe 和 /abcde,括号里的内容可以有也可以有
app.get('/ab(cd)?e', function(req, res) {
 res.send('ab(cd)?e');
});

路由路径是正则表达式:

// 匹配任何路径中含有 a 的路径:
app.get(/a/, function(req, res) {
  res.send('/a/');
});

// 匹配 butterfly、dragonfly,不匹配 butterflyman、dragonfly man等
app.get(/.*fly$/, function(req, res) {
  res.send('/.*fly$/');
});

(2)回调函数

在定义回调函数的时候都会将第三个参数定义为next,next函数主要负责将控制权交给下一个中间件。如果当前中间件没有终结请求(如send),并且next没有被调用,那么请求将被挂起,后边定义的中间件将得不到被执行的机会。

app.get('/example/a', function (req, res) {
  res.send('Hello from A!');
});

写法一:将多个回调函数作为参数

app.get('/example/b', function (req, res, next) {
  console.log('response will be sent by the next function ...');
  next();
}, function (req, res) {
  res.send('Hello from B!');
});

写法二:将多个回调函数放到数组中

var cb0 = function (req, res, next) {
  console.log('CB0')
  next()
}

var cb1 = function (req, res, next) {
  console.log('CB1')
  next()
}

var cb2 = function (req, res) {
  res.send('Hello from C!')
}

app.get('/example/c', [cb0, cb1, cb2])

写法三:混合使用函数和函数数组

var cb0 = function (req, res, next) {
  console.log('CB0')
  next()
}

var cb1 = function (req, res, next) {
  console.log('CB1')
  next()
}

app.get('/example/d', [cb0, cb1], function (req, res, next) {
  console.log('response will be sent by the next function ...')
  next()
}, function (req, res) {
  res.send('Hello from D!')
})

4.中间件

Express 是一个自身功能极简,完全是由路由和中间件构成一个的 web 开发框架:从本质上来说,一个 Express 应用就是在调用各种中间件。

中间件是一个函数,它有三个参数,请求对象req, 响应对象res, 和 web 应用中处于请求-响应循环流程中的中间件,一般被命名为 next 的变量。

中间件的功能包括:

  • 执行任何代码。

  • 修改请求和响应对象。

  • 终结请求-响应循环。

  • 调用堆栈中的下一个中间件。

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

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

  • 应用级中间件

  • 路由级中间件

  • 错误处理中间件

  • 内置中间件

  • 第三方中间件

可在应用级别中间件或路由级别中间件中装载中间件。

另外,你还可以同时装在一系列中间件函数,从而在一个挂载点上创建一个子中间件栈。写法和上面的回调函数一样。

(1)应用级中间件

应用级中间件绑定到 app 对象 使用 app.use() ,有两个参数,第一个是匹配的路径,第二个是回调函数,可以相应所有的请求,例如 GET, PUT, POST 等等,全部小写。例如:

var app = express()
// 如果写在其他中间件的前面,那么就在其他中间件每次执行前执行
// 如果写在其他中间件的后面,那么就在其他中间件每次执行后执行
// 如果没写第一个参数,那么就是匹配所有路径
// 如果第一个参数是 / ,那么就是匹配当前路径
app.use(function (req, res, next) {
  console.log('Time:', Date.now())
  next()
})

(2)路由级中间件

路由级中间件和应用级中间件一样,只是它绑定的对象为 express.Router()。

var router = express.Router()
var app = express()
var router = express.Router()

// 没有挂载路径的中间件,通过该路由的每个请求都会执行该中间件
router.use(function (req, res, next) {
  console.log('Time:', Date.now())
  next()
})

// 一个中间件栈,显示任何指向 /user/:id 的 HTTP 请求的信息
router.use('/user/:id', function(req, res, next) {
  console.log('Request URL:', req.originalUrl)
  next()
}, function (req, res, next) {
  console.log('Request Type:', req.method)
  next()
})

// 一个中间件栈,处理指向 /user/:id 的 GET 请求
router.get('/user/:id', function (req, res, next) {
  // 如果 user id 为 0, 跳到下一个路由
  if (req.params.id == 0) next('route')
  // 负责将控制权交给栈中下一个中间件
  else next() //
}, function (req, res, next) {
  // 渲染常规页面
  res.render('regular')
})

// 处理 /user/:id, 渲染一个特殊页面
router.get('/user/:id', function (req, res, next) {
  console.log(req.params.id)
  res.render('special')
})

// 将路由挂载至应用
app.use('/', router)
 
  

(3)错误处理中间件

错误处理中间件和其他中间件定义类似,只是要使用 4 个参数,而不是 3 个,其签名如下: (err, req, res, next)。

// 要获取err参数需要在之前的回调函数里面通过next(数据)返回数据
app.use(function(err, req, res, next) {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

(4)内置的中间件

express.static 是 Express 唯一内置的中间件。它基于 serve-static,负责在 Express 应用中提托管静态资源。每个应用可有多个静态目录。

app.use(express.static('public'))
app.use(express.static('uploads'))
app.use(express.static('files'))

(5)第三方中间件

安装所需功能的 node 模块,并在应用中加载,可以在应用级加载,也可以在路由级加载。

下面的例子安装并加载了一个解析 cookie 的中间件: cookie-parser

// 下载 npm install cookie-parser
var express = require('express')
var app = express()
var cookieParser = require('cookie-parser')

// 加载用于解析 cookie 的中间件
app.use(cookieParser())

5. 获取请求参数

get:获取get请求的方式是req.query
req.query

post:获取post请求的方式是req.body
// 不过在获取之前需要执行以下两行代码,才能在回调函数里面头盖骨req.body获取参数
app.use(express.urlencoded({extended:false}))
app.use(express.json())

req.body

6.利用 Express 托管静态文件

通过 Express 内置的 express.static 可以方便地托管静态文件,例如图片、CSS、JavaScript 文件等。

将静态资源文件所在的目录作为参数传递给 express.static 中间件就可以提供静态资源文件的访问了。例如,假设在 public 目录放置了图片、CSS 和 JavaScript 文件,你就可以:

app.use(express.static('public'))
// 在浏览器可以访问public文件夹里面的资源了
// 如:http://localhost:3000/index.html
//     http://localhost:3000/images/kitten.jpg
//     http://localhost:3000/css/style.css
//     http://localhost:3000/js/app.js
//     http://localhost:3000/images/bg.png
存放静态文件的目录名public不会出现在 URL 中。

如果你的静态资源存放在多个目录下面,你可以多次调用 express.static 中间件:

app.use(express.static('public'))
app.use(express.static('files'))

访问静态资源文件时,express.static 中间件会根据目录添加的顺序查找所需的文件。

如果你希望所有通过 express.static 访问的文件都存放在一个“虚拟(virtual)”目录(即目录根本不存在)下面,可以通过为静态资源目录指定一个挂载路径的方式来实现,如下所示:

app.use('/static', express.static('public'))

现在,你就可以通过带有 “/static” 前缀的地址来访问 public 目录下面的文件了。
// http://localhost:3000/static/images/kitten.jpg
// http://localhost:3000/static/css/style.css
// http://localhost:3000/static/js/app.js
// http://localhost:3000/static/images/bg.png
// http://localhost:3000/static/hello.html

7.服务端渲染(模板引擎)和服务端渲染

(1)服务端渲染模板

前端与后端最初的渲染方式是后端模板渲染,后端直接通过模板引擎,将数据与模板结合,直接生成html文件,返回给客户端进行解析。客户端只负责解析html,动态的数据直接在后端与模板结合了,不需要前端进行请求获取,但是一些后期的交互,还是需要在客户端执行,像点击删除某一个商品信息,发送ajax请求,然后 js 去操作 dom 或者渲染其他动态的部分。

这个过程大致分成以下几个步骤:

nodejs_第4张图片

在这个过程中,前端的 html 代码需要嵌入到后端代码中(如 javaphp),并且在很多情况下,前端源代码和后端源代码是在一个工程里的。

所以,不难看出,这种方式的有这样的几个不足:

  1. 前后端杂揉在一起,不方便本地开发、本地模拟调试,也不方便自动化测试

  2. 前端被约束在后端开发的模式中,不能充分使用前端的构建生态,开发效率低下

  3. 项目难以管理和维护,也可能会有前后端职责不清的问题

尽管如此,但因为这种方式是最早出现的方式,并且这种渲染方式有一个好处:

就是前端能够快速呈现服务器端渲染好的页面,而不用等客户端渲染,这能够提供很好的用户体验与 SEO 友好,所以当下很多比较早的网站或者需要快速响应的展示性网站仍然是使用这种方式。

(2)客户端渲染

随着前端工程化与前后端分离的发展,以及前端组件化技术的出现,如 react、vue 等,客户端渲染已经慢慢变成了主要的开发方式了。

与后端模板渲染刚好相反,客户端渲染的页面渲染都是在客户端进行,后端不负责任何的渲染,只管数据交互。

这个过程大致分成以下几个步骤:

nodejs_第5张图片

在客户端生成最终的html

第一次请求: 请求页面输入网址,服务端返回html静态文件,浏览器再进行解析渲染

第二次请求:请求动态数据,页面的数据需要请求服务器获得,得到相应数据后,浏览器根据html文件的js,操作DOM,生成最终的页面

后期的动态交互与服务器渲染就是相同的了

这样一来,前端与后端将完全解耦,数据使用全 ajax 的方式进行交互,如此便可前后端分离了。

其实,不难看出,客户端渲染与前后端分离有很大的好处:

  1. 前端独立出来,可以充分使用前端生态的强大功能

  2. 更好的管理代码,更有效率的开发、调试、测试

  3. 前后端代码解耦之后,能更好的扩展、重构

所以,客户端渲染与前后端分离现在已经是主流的开发方式了。

但这种方式也有一些不足:

  1. 首屏加载缓慢,因为要等 js 加载完毕后,才能进行渲染

  2. SEO 不友好,因为 html 中几乎没有可用的信息

(3)node中间层

为了解决客户端渲染的不足,便出现了 node 中间层的理念。

传统的 B/S 架构中,是 浏览器 -> 后端服务器 -> 浏览器,上文所讲的都是这种架构。

而加入了 node 中间层之后,就变成 浏览器 -> node -> 后端服务器 -> node -> 浏览器

这个过程大致分成以下几个步骤:

nodejs_第6张图片

  1. 前端请求一个地址 url

  2. node 层接收到这个请求,然后根据请求信息,向后端服务器发起请求,获取数据

  3. 后端服务器接收到请求,然后根据请求信息,从数据库或者其他地方获取相应的数据,返回给 node 层

  4. node 层根据这些数据渲染好首屏 html

  5. node 层将 html 文本返回给前端

一个典型的 node 中间层应用就是后端提供数据、node 层渲染模板、前端动态渲染。

这个过程中,node 层由前端开发人员掌控,页面中哪些页面在服务器上就渲染好,哪些页面在客户端渲染,由前端开发人员决定。

这样做,达到了以下的目的:

  1. 保留后端模板渲染、首屏快速响应、SEO 友好

  2. 保留前端后分离、客户端渲染的功能(首屏服务器端渲染、其他客户端渲染)

但这种方式也有一些不足:

  1. 增加了一个中间层,应用性能有所降低

  2. 增加了架构的复杂度、不稳定性,降低应用的安全性

  3. 对开发人员要求高了很多

(4)服务端渲染(ssr)

大部分情况下,服务器端渲染(ssr)与 node 中间层是同一个概念。

服务器端渲染(ssr)一般特指,在上文讲到的 node 中间层基础上,加上前端组件化技术在服务器上的渲染,特别是 react 和 vue。

react、vue、angular 等框架的出现,让前端组件化技术深入人心,但在一些需要首屏快速加载与 SEO 友好的页面就陷入了两难的境地了。

因为前端组件化技术天生就是给客户端渲染用的,而在服务器端需要被渲染成 html 文本,这确实不是一件很容易的事,所以服务器端渲染(ssr)就是为了解决这个问题。

好在社区一直在不断的探索中,让前端组件化能够在服务器端渲染,比如 next.js、nuxt.js、razzle、react-server、beidou 等。

一般这些框架都会有一些目录结构、书写方式、组件集成、项目构建的要求,自定义属性可能不是很强。

以 next.js 为例,整个应用中是没有 html 文件的,所有的响应 html 都是 node 动态渲染的,包括里面的元信息、css, js 路径等。渲染过程中,next.js 会根据路由,将首页所有的组件渲染成 html,余下的页面保留原生组件的格式,在客户端渲染。

(5)服务端渲染的实现方式

1.下载 npm i ejs

2. 需要在应用中进行如下设置才能让 Express 渲染模板文件:

views, 放模板文件的目录,比如: app.set('views', './views')

view engine, 模板引擎,比如: app.set('view engine', 'ejs')

3.跳转

// 通过res.render可以跳转到app.set中指定的文件夹下的文件login,第二个参数是传给login文件的数据
res.render('login',{err: '用户名或密码错误'})

数据通过<% %>接收,在login文件中通过<% err %>可以拿到传过来的err参数

<% %>里面包着的会被当作js表达式

nodejs_第7张图片

8.生成器

用于帮助我们快速创建一个nodejs的代码骨架。

(1)安装生成器:npm i -g express-generator

(2)创建模板,在命令行输入:

express 文件名 默认创建的是jade模板的文件

express 文件名 --view=ejs 这样创建的就是ejs模板的文件

(3)然后进入到创建的文件夹下 npm i 下载相应依赖

三、MongoDB

mongodb的下载安装,以及可视化工具,命令行指令都不在此介绍了。这里主要对nodejs中操作mongodb进行增删改查进的方法做介绍。

nodejs连接操作数据库

(1)安装mongoose模块:npm i mongoose

(2)连接数据库,可以在入口app.js中引入并连接数据库,也可以重新创建一个js文件引入并连接数据库。

一般是创建一个config文件夹,在index.js文件里面编写以下代码,然后在bin/www中引入这个index文件。

const mongoose = require("mongoose") 
// company-system是数据库名,如果没有这个数据库,那么在进行数据库操作时会创建这个数据库
mongoose.connect("mongodb://127.0.0.1:27017/company-system")

 创建模型,然后在需要使用的文件中引入UserModel,通过UserModel即可实现对数据库的操作

const mongoose = require("mongoose")

const Schema = mongoose.Schema
// 这是限定对数据库的数据的操作,只能操作这里定义的数据
const UserType = {
    username:String,
    password:String,
    gender:Number,
    introduction:String,
    avatar:String,
    role:Number
}
const UserModel = mongoose.model("user",new Schema(UserType))
module.exports  = UserModel 

增加数据

UserModel.create({
    introduction,username,gender,avatar,password,role
})

查询数据

UserModel.find({username:"kerwin"},["username","role","introduction","password"]).sort({createTime:-1}).skip(10).limit(10)

更新数据

UserModel.updateOne({
    _id
},{
    introduction,username,gender,avatar
})

删除数据

UserModel.deleteOne({_id})

四、接口规范与业务分层

app.js:作为入口,进行相关资源的引入和配置,并匹配路由交给 路由中间件 处理

routes文件夹:进行相关路由的匹配,拿到前端传来的数据,并返回相应数据。

controllers文件夹:将routes中的逻辑操作抽离到这一层

service文件夹:将routes对数据库的操作抽离到这一层。

五、登录鉴权

1. Cookie&Session

「HTTP 无状态」我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的。「标记」那解决办法是什么呢?

nodejs_第8张图片

(1)下载: npm i express-session,然后引入

const express = require("express");
const session = require("express-session");
// 这个用于将session保存到数据库
const MongoStore = require("connect-mongo");
const app = express();

(2)配置 ,需要写在其他中间件之前

app.use(
  session({
    secret: "this is session", // 服务器生成 session 的签名
    resave: true,  // 重新设置session之后,过期时间重新记时
    saveUninitialized: true, //强制将为初始化的 session 存储
    cookie: {
      maxAge: 1000 * 60 * 10,// 过期时间
      secure: false, // 为 true 时候表示只有 https 协议才能访问cookie
    },
    rolling: true, //为 true 表示 超时前刷新,cookie 会重新计时; 为 false 表示在超时前刷新多少次,都是按照第一次刷新开始计时。
    store: MongoStore.create({
      mongoUrl: 'mongodb://127.0.0.1:27017/kerwin_session',
      ttl: 1000 * 60 * 10 // 过期时间
  }),

  })
);
(3)然后在登录成功时,通过给req.session.user设置一个值,之后进入某一页面前(如进入个人中心)就可以判断有没有user这个值,有了才能进入。

(4)但是,如果我进入了个人中心,我挂着页面,很久之后cookie过期了,我还是能操作。
     对于这种情况,可以在每一个页面进入之前都判断有没有user这个值,如果一个一个设置很麻烦。
     可以在app里面配置一个中间件,代码如下:

当然,具体的代码需要结合你的页面的功能需求来写

// 进入页面之前进行判断
app.use((req,res,next)=>{
  console.log('外面',req.url)
  // next()
  if(req.url==="/users"){
    console.log('进入users')
    next()
    return;
  }

  if(req.session.user){
      // 每次进入都让session过期时间重新计时
      req.session.garbage = Date();
      console.log('有cookie')
      next();
  }else{
      console.log('其他')
      req.url.includes('login') ? res.status(401).send({ok:0}) : res.redirect('/users')
  }
})

2. JSON Web Token (JWT)

首先,需要下载引入,然后封装一下

const jwt = require("jsonwebtoken")
// 设置密钥
const secret = "george-secret"
const JWT = {
    // 根据密钥将value值进行加密,生成token,expires是过期时间
    generate(value,expires){
        return jwt.sign(value,secret,{expiresIn:expires})
    },
    // verify解密token
    verify(token){
        try {
            // 如果正常会返回一个解密出来的对象,如果过期了会报错,所以放在try-catch里面
            return jwt.verify(token,secret)
        } catch (error) {
            return false
        }
    }
}
  
module.exports = JWT

使用:

首先,后端在登录验证成功时,在响应头返回token

const token = JWT.generate({
          _id:data[0]._id,
          username:data[0].username
},"1h")  
//将token设置在header中返回
res.header("authorization",token)

然后前端在响应拦截器中接收token并存在localStorage中

axios.interceptors.response.use(function (response) {
            const {authorization } = response.headers
            authorization && localStorage.setItem("token",authorization)
            return response;
}, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
            return Promise.reject(error);
});

对于个人中心等登录后才能访问的页面中,每次发请求前都在请求拦截器中从localStorage中获取token,通过请求头的authorization字段发送

axios.interceptors.request.use(function (config) {
          // 如果这里没拿到token那token就是null
          const token = localStorage.getItem("token")
          //如果token是null 那么authorization = 'Bearer null'
          config.headers.authorization = `Bearer ${token}`
          return config;
}, function (error) {
          // Do something with request error
          return Promise.reject(error);
});

如果响应头中有authorization字段,就将它存到localStorage中

axios.interceptors.response.use(function (response) {
          console.log("login响应拦截器:",response.headers)
          const {
            authorization
          } = response.headers
          authorization && localStorage.setItem("token", authorization)
          return response;
}, function (error) {
          // console.log(error.response.status)
          if(error.response.status===401){
            localStorage.removeItem("token")
            location.href = "/users"
          }
          return Promise.reject(error);
});

每次进入页面之前进行判断:

        是login直接放行

        否则在请求头通过Authorization字段获取token

                如果有token

                解析token,如果正确,让token过期时间刷新,在响应头返回token,放行

否则,返回401

否则放行

//node中间件校验
app.use((req,res,next)=>{
// 每次跳转路由时进行判断,如果是login界面,就放行
  if(req.url==="/login"){
    next()
    return;
  }
// 否则从请求头获取token(因为个人中心等需要登录才能进入的页面,都在请求拦截器中设置了authorization字段,并且不管有没有从本地获取到token,if判断时token的值都为true,因为'null'也转换为true)
  const token = req.headers["authorization"].split(" ")[1]
  if(token){
    var payload = JWT.verify(token)
    // 如果token验证正确,那么就重新设置token,让它过期时间刷新
    if(payload){
      const newToken = JWT.generate({
        _id:payload._id,
        username:payload.username
      },"1h")
      res.header("authorization",newToken)
      next()
    // 如果验证失败,将状态码设置为401,并返回错误信息
    }else{
      res.status(401).send({errCode:"-1",errorInfo:"token过期"})
    }
// 对于不需要登录的页面直接放行
  }else{ 
    next()
  } 
})
 
  

六、文件上传管理

form表单里面的数据

        当发送get请求时,数据是以key=value&key=value的形式放在请求地址中发送的

        当发送post请求时,数据是在请求体里面发送的

如果里面带有文件,就要设置enctype="multipart/form-data",但是后端是无法接收这种格式的数据。因此需要引入multer中间件来处理。

Multer 是一个 node.js 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。

注意: Multer 不会处理任何非 multipart/form-data 类型的表单数据。

前端文件的方式:通过form

用户名:
密码:
年龄:
头像:

后端接收前端传过来的文件 

// (1)下载npm install --save multer
// (2)在app.js中引入multer
const multer  = require('multer')
// 这里会在public文件夹下创建一个uploads文件夹用来存放multipart/form-data类型的表单数据
const upload = multer({ dest: 'public/uploads/' })
// (3)在对应路由的中间件中,调用upload.single("avatar"),用于拿到form表单传过来的multipart/form-data类型的文件并存入uploads文件夹里面
router.post("/user",upload.single("avatar"),UserController.addUser)

// (4)req.file就能拿到一个对象,对象里面是本次存入uploads里面的文件的信息,包括文件名、文件路径等,将文件的路径保存到数据库中,然后通过操作数据库获取文件路径就可以拿到文件
前端发文件也可以
// html
用户名:
密码:
年龄:
头像:
// script // 通过id拿到标签对象 var username = document.querySelector("#username") var password = document.querySelector("#password") var age = document.querySelector("#age") var avatar = document.querySelector("#avatar") // 把要上传的内容通过标签对象获取,创建formData,并把要上传的内容append进去 const formsdata = new FormData() formsdata.append("username", username.value) formsdata.append("password", password.value) formsdata.append("age", age.value) formsdata.append("avatar", avatar.files[0]) // 发送请求时带上formsdata数据 axios.post("/api/user",formsdata,{ headers:{ "Content-Type":"multipart/form-data" } }) .then(res => { console.log(res.data) })

七、APIDOC - API 文档生成工具

apidoc 是一个简单的 RESTful API 文档生成工具,它从代码注释中提取特定格式的内容生成文档。支持诸如 Go、Java、C++、Rust 等大部分开发语言,具体可使用 apidoc lang 命令行查看所有的支持列表。

apidoc 拥有以下特点:

  1. 跨平台,linux、windows、macOS 等都支持;

  2. 支持语言广泛,即使是不支持,也很方便扩展;

  3. 支持多个不同语言的多个项目生成一份文档;

  4. 输出模板可自定义;

  5. 根据文档生成 mock 数据;

安装:npm i -g apidoc

在vscode里面下载一个叫ApiDoc Snippets的插件,用于帮助你快速生成代码。

然后,配置后某个接口的说明

/**
 * 请求的方法 路径
 * @api {method} /path name
    接口的名字,随便起一个就行
 * @apiName addUser
    分组,组名相同的会放到一个文件夹下面
 * @apiGroup userGroups
    版本号:     如
 * @apiVersion  1.0.0.minor.patch
 * 
 * 
   前端调用接口需要传的参数,有几个就写几条
    如: 
 * @apiParam  {String} username 用户名
 * @apiParam  {String} password 密码
 * @apiParam  {String} sex 性别
 * 
    前端调用接口返回的数据
 * @apiSuccess (200) {Number} ok 返回成功
 * 
    前端传数据的示例: 其中{json}表示数据的形式
 * @apiParamExample  {json} Request-Example:
 * {
 *     username : 'george',
       password : '123;
       sex : '男'
 * }
 * 
 * 
    返回成功字段的示例
 * @apiSuccessExample {type} Success-Response:
 * {
 *     ok : 1
 * }
 * 
 * 
 */

在命令行输入以下代码就会生成一个doc文件夹,打开里面的html文件就能看到接口说明

// 会根据.\route\下的接口说明,生成一个doc文件夹
api -i .\route\ -o .\doc

注意:

需要在根目录下新建 apidoc.json文件

{
	"name": "****接口文档",
	"version": "1.0.0",
	"description": "关于****的接口文档描述",
	"title": "****"
}

八、Koa2

1.简介

koa 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。

2. 快速开始

2.1 安装koa2

# 初始化package.json
npm init

# 安装koa2 
npm install koa

2.2 hello world 代码

const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
  ctx.body = 'hello koa2' //json数据
})

app.listen(3000)

2.3 启动demo

node index.js

3. koa vs express

通常都会说 Koa 是洋葱模型,这重点在于中间件的设计。但是按照上面的分析,会发现 Express 也是类似的,不同的是Express 中间件机制使用了 Callback 实现,这样如果出现异步则可能会使你在执行顺序上感到困惑,因此如果我们想做接口耗时统计、错误处理 Koa 的这种中间件模式处理起来更方便些。最后一点响应机制也很重要,Koa 不是立即响应,是整个中间件处理完成在最外层进行了响应,而 Express 则是立即响应。

3.1更轻量

  • koa 不提供内置的中间件;

  • koa 不提供路由,而是把路由这个库分离出来了(koa/router)

3.2 Context对象

koa增加了一个Context的对象,作为这次请求的上下文对象(在koa2中作为中间件的第一个参数传入)。同时Context上也挂载了Request和Response两个对象。和Express类似,这两个对象都提供了大量的便捷方法辅助开发, 这样的话对于在保存一些公有的参数的话变得更加合情合理

3.3 异步流程控制

express采用callback来处理异步, koa v1采用generator,koa v2 采用async/await。

generator和async/await使用同步的写法来处理异步,明显好于callback和promise,

3.4 中间件模型

express基于connect中间件,线性模型;

koa中间件采用洋葱模型(对于每个中间件,在完成了一些事情后,可以非常优雅的将控制权传递给下一个中间件,并能够等待它完成,当后续的中间件完成处理后,控制权又回到了自己)

//同步
var express = require("express")
var app = express()

app.use((req,res,next)=>{
    console.log(1)
    next()
    console.log(4)
    res.send("hello")
})
app.use(()=>{
    console.log(3)
})

app.listen(3000)
//异步
var express = require("express")
var app = express()

app.use(async (req,res,next)=>{
    console.log(1)
    await next()
    console.log(4)
    res.send("hello")
})
app.use(async ()=>{
    console.log(2)
    await delay(1)
    console.log(3)
})

function delay(time){
 return new Promise((resolve,reject)=>{
    setTimeout(resolve,1000)
 })
}
//同步
var koa = require("koa")
var app = new koa()

app.use((ctx,next)=>{
    console.log(1)
    next()
    console.log(4)
    ctx.body="hello"
})
app.use(()=>{
    console.log(3)
})

app.listen(3000)

//异步
var koa = require("koa")
var app = new koa()

app.use(async (ctx,next)=>{
    console.log(1)
    await next()
    console.log(4)
    ctx.body="hello"
}) 
app.use(async ()=>{
    console.log(2)
    await delay(1)
    console.log(3)
})

function delay(time){
 return new Promise((resolve,reject)=>{
    setTimeout(resolve,1000)
 })
}

app.listen(3000)

4. 路由

4.1基本用发

var Koa = require("koa")
var Router = require("koa-router")

var app = new Koa()
var router = new Router()

router.post("/list",(ctx)=>{
    ctx.body=["111","222","333"]
})
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)

4.2 router.allowedMethods作用

4.3 请求方式

Koa-router 请求方式: getputpostpatchdeletedel ,而使用方法就是 router.方式() ,比如 router.get()router.post() 。而 router.all() 会匹配所有的请求方法。

var Koa = require("koa")
var Router = require("koa-router")

var app = new Koa()
var router = new Router()

router.get("/user",(ctx)=>{
    ctx.body=["aaa","bbb","ccc"]
})
.put("/user/:id",(ctx)=>{
    ctx.body={ok:1,info:"user update"}
})
.post("/user",(ctx)=>{
    ctx.body={ok:1,info:"user post"}
})
.del("/user/:id",(ctx)=>{
    ctx.body={ok:1,info:"user del"}
})


app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)

4.4 拆分路由

list.js

var Router = require("koa-router")
var router = new Router()
router.get("/",(ctx)=>{
    ctx.body=["111","222","333"]
})
.put("/:id",(ctx)=>{
    ctx.body={ok:1,info:"list update"}
})
.post("/",(ctx)=>{
    ctx.body={ok:1,info:"list post"}
})
.del("/:id",(ctx)=>{
    ctx.body={ok:1,info:"list del"}
})
module.exports = router

index.js

var Router = require("koa-router")
var router = new Router()
var user = require("./user")
var list = require("./list")
router.use('/user', user.routes(), user.allowedMethods())
router.use('/list', list.routes(), list.allowedMethods())

module.exports = router

entry入口

var Koa = require("koa")
var router = require("./router/index")

var app = new Koa()
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)

4.5 路由前缀

router.prefix('/api')

4.6 路由重定向

router.get("/home",(ctx)=>{
    ctx.body="home页面"
})
//写法1 
router.redirect('/', '/home');
//写法2
router.get("/",(ctx)=>{
    ctx.redirect("/home")
})

5. 静态资源

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

app.use(static(
  path.join( __dirname,  "public")
))


app.use( async ( ctx ) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('[demo] static-use-middleware is starting at port 3000')
})

6. 获取请求参数

6.1get参数

在koa中,获取GET请求数据源头是koa中request对象中的query方法或querystring方法,query返回是格式化好的参数对象,querystring返回的是请求字符串,由于ctx对request的API有直接引用的方式,所以获取GET请求数据有两个途径。

  • 是从上下文中直接获取 请求对象ctx.query,返回如 { a:1, b:2 } 请求字符串 ctx.querystring,返回如 a=1&b=2

  • 是从上下文的request对象中获取 请求对象ctx.request.query,返回如 { a:1, b:2 } 请求字符串 ctx.request.querystring,返回如 a=1&b=2

6.2post参数

对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中

const bodyParser = require('koa-bodyparser')

// 使用ctx.body解析中间件
app.use(bodyParser())

7. ejs模板

7.1 安装模块

# 安装koa模板使用中间件
npm install --save koa-views

# 安装ejs模板引擎
npm install --save ejs

7.2 使用模板引擎

文件目录

├── package.json
├── index.js
└── view
    └── index.ejs

./index.js文件

const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()

// 加载模板引擎
app.use(views(path.join(__dirname, './view'), {
  extension: 'ejs'
}))

app.use( async ( ctx ) => {
  let title = 'hello koa2'
  await ctx.render('index', {
    title,
  })
})

app.listen(3000)

./view/index.ejs 模板




    <%= title %>


    

<%= title %>

EJS Welcome to <%= title %>

8. cookie&session

8.1 cookie

koa提供了从上下文直接读取、写入cookie的方法

  • ctx.cookies.get(name, [options]) 读取上下文请求中的cookie

  • ctx.cookies.set(name, value, [options]) 在上下文中写入cookie

8.2 session

  • koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。

    const session = require('koa-session-minimal')
    app.use(session({
        key: 'SESSION_ID',
        cookie: {
            maxAge:1000*60
        }
    }))

    app.use(async (ctx, next) => {
        //排除login相关的路由和接口
        if (ctx.url.includes("login")) {
            await next()
            return
        }
    
        if (ctx.session.user) {
            //重新设置以下sesssion
            ctx.session.mydate = Date.now()
            await next()
        } else {
    
            ctx.redirect("/login")
        }
    })

9. JWT

app.use(async(ctx, next) => {
    //排除login相关的路由和接口
    if (ctx.url.includes("login")) {
        await next()
        return
    }
    const token = ctx.headers["authorization"]?.split(" ")[1]
    // console.log(req.headers["authorization"])
    if(token){
        const payload=  JWT.verify(token)
        if(payload){
            //重新计算token过期时间
            const newToken = JWT.generate({
                _id:payload._id,
                username:payload.username
            },"10s")

            ctx.set("Authorization",newToken)
            await next()
        }else{
            ctx.status = 401
            ctx.body = {errCode:-1,errInfo:"token过期"}
        }
    }else{
        await next()
    }
})

10.上传文件

@koa/multer - npm

npm install --save @koa/multer multer
const multer = require('@koa/multer');
const upload = multer({ dest: 'public/uploads/' })

router.post("/",upload.single('avatar'),
(ctx,next)=>{
    console.log(ctx.request.body,ctx.file)
    ctx.body={
        ok:1,
        info:"add user success"
    }
})

11.操作MongoDB

const mongoose = require("mongoose")

mongoose.connect("mongodb://127.0.0.1:27017/kerwin_project")
//插入集合和数据,数据库kerwin_project会自动创建

const mongoose = require("mongoose")
const Schema = mongoose.Schema
const UserType = {
    username:String,
    password:String,
    age:Number,
    avatar:String
}

const UserModel = mongoose.model("user",new Schema(UserType))
// 模型user 将会对应 users 集合, 
module.exports = UserModel

九、Socket编程

1.websocket介绍

应用场景:

  • 弹幕

  • 媒体聊天

  • 协同编辑

  • 基于位置的应用

  • 体育实况更新

  • 股票基金报价实时更新

WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。我们来看看WebSocket连接是如何创建的。

首先,WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求,格式如下:

GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13

该请求和普通的HTTP请求有几点不同:

  1. GET请求的地址不是类似/path/,而是以ws://开头的地址;

  2. 请求头Upgrade: websocketConnection: Upgrade表示这个连接将要被转换为WebSocket连接;

  3. Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;

  4. Sec-WebSocket-Version指定了WebSocket的协议版本。

随后,服务器如果接受该请求,就会返回如下响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string

该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。

版本号和子协议规定了双方能理解的数据格式,以及是否支持压缩等等。如果仅使用WebSocket的API,就不需要关心这些。

现在,一个WebSocket连接就建立成功,浏览器和服务器就可以随时主动发送消息给对方。消息有两种,一种是文本,一种是二进制数据。通常,我们可以发送JSON格式的文本,这样,在浏览器处理起来就十分容易。

为什么WebSocket连接可以实现全双工通信而HTTP连接不行呢?实际上HTTP协议是建立在TCP协议之上的,TCP协议本身就实现了全双工通信,但是HTTP协议的请求-应答机制限制了全双工通信。WebSocket连接建立以后,其实只是简单规定了一下:接下来,咱们通信就不使用HTTP协议了,直接互相发数据吧。

安全的WebSocket连接机制和HTTPS类似。首先,浏览器用wss://xxx创建WebSocket连接时,会先通过HTTPS创建安全的连接,然后,该HTTPS连接升级为WebSocket连接,底层通信走的仍然是安全的SSL/TLS协议。

浏览器支持

很显然,要支持WebSocket通信,浏览器得支持这个协议,这样才能发出ws://xxx的请求。目前,支持WebSocket的主流浏览器如下:

  • Chrome

  • Firefox

  • IE >= 10

  • Sarafi >= 6

  • Android >= 4.4

  • iOS >= 8

服务器支持

由于WebSocket是一个协议,服务器具体怎么实现,取决于所用编程语言和框架本身。Node.js本身支持的协议包括TCP协议和HTTP协议,要支持WebSocket协议,需要对Node.js提供的HTTPServer做额外的开发。已经有若干基于Node.js的稳定可靠的WebSocket实现,我们直接用npm安装使用即可。

2.ws模块

服务器:

const  WebSocket = require("ws")
WebSocketServer = WebSocket.WebSocketServer
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
    ws.on('message', function message(data, isBinary) {
        wss.clients.forEach(function each(client) {
            if (client !== ws && client.readyState === WebSocket.OPEN) {
                client.send(data, { binary: isBinary });
            }
        });

    });

    ws.send('欢迎加入聊天室');
});

客户端:

var ws = new WebSocket("ws://localhost:8080")
ws.onopen = ()=>{
    console.log("open")
}
ws.onmessage = (evt)=>{
    console.log(evt.data)
}

授权验证:

//前端
var ws = new WebSocket(`ws://localhost:8080?token=${localStorage.getItem("token")}`)
ws.onopen = () => {
      console.log("open")
      ws.send(JSON.stringify({
        type: WebSocketType.GroupList
      }))
    }
ws.onmessage = (evt) => {
    console.log(evt.data)
}
//后端
const WebSocket = require("ws");
const JWT = require('../util/JWT');
WebSocketServer = WebSocket.WebSocketServer
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws, req) {
  const myURL = new URL(req.url, 'http://127.0.0.1:3000');
  const payload = JWT.verify(myURL.searchParams.get("token"))
  if (payload) {
    ws.user = payload
    ws.send(createMessage(WebSocketType.GroupChat, ws.user, "欢迎来到聊天室"))

    sendBroadList() //发送好友列表
  } else {
    ws.send(createMessage(WebSocketType.Error, null, "token过期"))
  }
  // console.log(3333,url)
  ws.on('message', function message(data, isBinary) {
    const messageObj = JSON.parse(data)
    switch (messageObj.type) {
      case WebSocketType.GroupList:
        ws.send(createMessage(WebSocketType.GroupList, ws.user, JSON.stringify(Array.from(wss.clients).map(item => item.user))))
        break;
      case WebSocketType.GroupChat:
        wss.clients.forEach(function each(client) {
          if (client !== ws && client.readyState === WebSocket.OPEN) {
            client.send(createMessage(WebSocketType.GroupChat, ws.user, messageObj.data));
          }
        });
        break;
      case WebSocketType.SingleChat:
        wss.clients.forEach(function each(client) {
          if (client.user.username === messageObj.to && client.readyState === WebSocket.OPEN) {
            client.send(createMessage(WebSocketType.SingleChat, ws.user, messageObj.data));
          }
        });
        break;
    }

    ws.on("close",function(){
      //删除当前用户
      wss.clients.delete(ws.user)
      sendBroadList() //发送好用列表
    })
  });

});
const WebSocketType = {
  Error: 0, //错误
  GroupList: 1,//群列表
  GroupChat: 2,//群聊
  SingleChat: 3//私聊
}
function createMessage(type, user, data) {
  return JSON.stringify({
    type: type,
    user: user,
    data: data
  });
}

function sendBroadList(){
  wss.clients.forEach(function each(client) {
    if (client.readyState === WebSocket.OPEN) {
      client.send(createMessage(WebSocketType.GroupList, client.user, JSON.stringify(Array.from(wss.clients).map(item => item.user))))
    }
  });
}

3.socket.io模块

服务端:

const io = require('socket.io')(server);
io.on('connection', (socket) => {

  const payload = JWT.verify(socket.handshake.query.token)
  if (payload) {
    socket.user = payload
    socket.emit(WebSocketType.GroupChat, createMessage(socket.user, "欢迎来到聊天室"))
    sendBroadList() //发送好友列表
  } else {
    socket.emit(WebSocketType.Error, createMessage(null, "token过期"))
  }


  socket.on(WebSocketType.GroupList, () => {
    socket.emit(WebSocketType.GroupList, createMessage(null, Array.from(io.sockets.sockets).map(item => item[1].user).filter(item=>item)));
  })

  socket.on(WebSocketType.GroupChat, (messageObj) => {
    socket.broadcast.emit(WebSocketType.GroupChat, createMessage(socket.user, messageObj.data));
  })

  socket.on(WebSocketType.SingleChat, (messageObj) => {
    Array.from(io.sockets.sockets).forEach(function (socket) {
      if (socket[1].user.username === messageObj.to) {
        socket[1].emit(WebSocketType.SingleChat, createMessage(socket[1].user, messageObj.data));
      }
    })
  })

  socket.on('disconnect', reason => {
     
     sendBroadList() //发送好用列表
  });

});

function sendBroadList() {
  io.sockets.emit(WebSocketType.GroupList, createMessage(null, Array.from(io.sockets.sockets).map(item => item[1].user).filter(item=>item)))
}
//最后filter,是因为 有可能存在null的值

客户端:

const WebSocketType = {
    Error: 0, //错误
    GroupList: 1, //群列表
    GroupChat: 2, //群聊
    SingleChat: 3 //私聊
}


const socket = io(`ws://localhost:3000?token=${localStorage.getItem("token")}`);
socket.on("connect",()=>{
	socket.emit(WebSocketType.GroupList)
})
socket.on(WebSocketType.GroupList, (messageObj) => {
    select.innerHTML = ""
    select.innerHTML = `` + messageObj.data.map(item => `
    `).join("")
})

socket.on(WebSocketType.GroupChat, (msg) => {
	console.log(msg)
})

socket.on(WebSocketType.SingleChat, (msg) => {
	console.log(msg)
})

socket.on(WebSocketType.Error, (msg) => {
    localStorage.removeItem("token")
    location.href = "/login"
})

send.onclick = () => {
    if (select.value === "all") {
        socket.emit(WebSocketType.GroupChat,{
            data: text.value
        })
    } else {
        socket.emit(WebSocketType.SingleChat,{
            data: text.value,
            to:select.value
        })
    }
}

你可能感兴趣的:(Node.js,javascript,前端,开发语言)