【知识体系 - Node系列】

139 阅读13分钟

Node.js

初识Node.js

What is Node.js

  • Node.js 是一个基于Chrome V8引擎的Javascript的运行环境
/*
    1. 浏览器是Javascript的前端运行环境
    2. Node.js是Javascript的后端运行环境
*/

Node.js运行环境 = V8引擎 + 内置API(fs/path/http/JS内置对象/...)
  • 学习路径
    • 浏览器中的Javascript学习路径
      • Javascript基础语法 + 浏览器内置APIDOM + BOM)+ 第三方库
    • Node.js学习路径
      • Javascript基础语法 + Node.js内置APIfs/path/http)+ 第三方API模块(expressmysql

Install Node.js

查看node版本号
node -v

Node.js中执行Javascript代码

node 要执行的js文件路径

fs文件系统模块

What is fs

  • fs模块是Node.js官方提供的,用来操作文件的模块。提供了一系列的方法和属性,用来满足用户对文件的操作需求。
    • fs.readFile() - 读取文件中的内容
    • fs.writeFile() - 指定文件写入内容
  • 使用时,先导入
    const fs = require('fs')
    
  • 例子
    // 导入fs模块
    const fs = require('fs');
    
    // 如果出现路径拼接问题,是因为使用了 ./ 或者 ../ 开头的相对路径
    /**
     * 解决方式:
     *      1. 提供完成的路径(移植性非常差,难以维护)
     *      2. 使用node提供的 __dirname - 表示当前文件所处的目录
     */
    console.log(__dirname); // C:\Users\one\Desktop\yunfan\代码\node
    
    // 读取文件
    /**
     * 参数1:读取文件的存放路径
     * 参数2:读取文件是采用的编码方式,一般默认指定utf8
     * 参数3:回调函数,获取读取失败和成功的结果 err dataStr
     */
    fs.readFile(`${__dirname}/data.json`, 'utf8', (err, dataStr) => {
        if (err) {
            console.log(' -- err -- ', err.message);
            return;
        }
        console.log(' -- dataStr -- ', dataStr);
    })
    
    // 写入文件
    /**
     * 参数1:读取文件的存放路径
     * 参数2:写入的内容
     * 参数3:回调函数,获取读取失败和成功的结果 err dataStr
     * 默认以utf8的格式写入
     */
    fs.writeFile(`${__dirname}/files/text.txt`, '写入的内容', (err) => {
        // 如果文件写入成功,err默认为null
        // 如果文件写入失败,err是一个错误对象
        if (err) {
            console.log(' -- err-- ', err);
            return;
        }
        console.log(' -- 文件写入成功 -- ');
    })
    

path路径模块

What is path

  • path模块是Node.js官方提供的,用来处理路径的模块。提供了一系列的方法和属性,用来满足用户对路径的处理需求。
    • path.join() - 将多个路径片段拼接成一个完整的路径字符串
    • path.basename() - 从路径字符串中,将文件名解析出来
  • 使用时,先导入
    const path = require('path')
    
  • 例子
    const path = require('path')
    
    let pathStr = path.join('/a', '/b/c')
    console.log(' -- pathStr -- ', pathStr); // \a\b\c
    
    /**
     *  ../ 会抵消前面一个路径
     *  . 会被处理掉
     */
    pathStr = path.join('/a', '/b/c', '../', './d', 'e')
    console.log(' -- pathStr -- ', pathStr); // \a\b\d\e
    
    const filePath = path.join(__dirname, './files/text/txt')
    console.log(' -- filePath -- ', filePath); //  C:\Users\one\Desktop\yunfan\代码\node\files\text\txt
    

HTTP模块

What is http

  • http模块是 Node.js官方提供的,用来创建web服务器的模块
    • http.createServer()可以将一台普通电脑,变成一台Web服务器
  • 消费资源的计算机 - 客户端;对外提供网络资源的计算机 - 服务器
  • 要使用,先导入
    const http = require('http')
    
  • 例子
    // 导入http模块
    const http = require('http');
    
    const port = 8080;
    
    // 创建服务器实例
    const server = http.createServer();
    
    /**
     * 为服务器实例绑定request事件
     * 使用服务器实例的 .on()方法,为服务器绑定一个 request 事件
     */
    server.on('request', (req, res) => {
        // 客户端请求的url地址,从端口号后面开始
        const url = req.url;
        // 客户端请求的类型
        const method = req.method;
        const str = `Your request url is ${url} and request method is ${method}`;
        console.log(str);
    
        // 发送中文时,会产生中文乱码问题。此时需要设置响应头的编码方式
        res.setHeader('Content-Type', 'text/html; charset=utf-8')
        res.end(`请求成功了!${str}`)
    })
    
    /**
     * 启动服务器
     * 调用 server.listen(端口号,cb回调)方法,即可启动web服务器
     */
    server.listen(port, () => {
        console.log(`http server running at http://127.0.0.1/${port}`);
    })
    

Node.js中的模块化

Node.js模块分类

  • Node.js中根据模块来源不同,将模块分类3大类:
    • 内置模块(由Node.js官方提供的,例如:fs/path/http等)
    • 自定义模块(用户创建的每个.js文件)
    • 第三方模块(由第三方开发,使用前需要先下载)
      • 例如 moment
        npm install moment
        
        const moment = require('moment')
        

Express相关

使用自定义路由模块化

apiRouter.js

const express = require('express');

const router = express.Router();

// http://localhost/api/get?name=elio&age=26
router.get('/get', (req, res) => {
  const data = req.query;
  res.send({
    status: 200,
    meg: '请求成功!',
    data
  })
})

module.exports = router

expressServer.js

// 导入express
const express = require('express');

const apiRouter = require('./apiRouter.js')

// 创建web服务器
const app = express();

// 中间件 - 路由中间件
app.use('/api', apiRouter);


// 启动web服务器
app.listen(80, () => {
  console.log('express server running at http://127.0.0.1');
})

跨域

CORS相关的请求头

Access-Control-Allow-Origin

1. 指定允许访问该外部资源的外域URL
res.setHeader('Access-Control-Allow-Origin', 'https://www.baidu.com')

2. 允许来自任何域的访问
res.setHeader('Access-Control-Allow-Origin', '*')

Access-Control-Allow-Headers

  • 默认情况下,CORS仅支持 客户端向服务器 发送9种请求头
    • Accept
    • Accept-Language
    • Content-Language
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
    • Content-Type
      • text/plain
      • multipart/form-date
      • application/x-www-form-urlencode
  • 如果客户端向服务器发送额外的请求头信息,需要在服务器通过Access-Control-Allow-Headers对额外的请求头进行声明
// 多个请求头用英文逗号分割
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Customer-Header')

Access-Control-Allow-Methods

  • 默认情况下,CORS 仅支持 GET/POST/HEAD请求
  • 客户端希望进行 PUT/DELETE 等方式进行请求,需要在服务器通过 Access-Control-Allow-Methods 来指明实际请求所允许使用的 HTTP 方法
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE')
res.setHeader('Access-Control-Allow-Methods', '*')

CORS请求分类

  • 简单请求

    • 请求方式:GET/POST/HEAD
    • HTTP头部信息不超过以下几种字段:无自定义头部信息、客户端向服务器 发送9种请求头。
    • 客户端与服务器之间只会发生一次请求
  • 预检请求(以下三者满足其一)

    • 请求方式:除了GET/POST/HEAD之外的请求方式
    • 请求头种包含自定义头部字段
    • 向服务器发送了application/json格式的数据
    • 客户端与服务器之间会发生两次请求
      在客户端和服务器正式通信之前,客户端会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求。这一次的OPTION请求称为“预检请求”。
      服务器成功响应预检请求后,才会发送真正的请求,并携带真实的数据
      

编写JSONP接口

  • 概念:浏览器通过<script>标签的src属性,请求服务器上的数据,服务器返回一个函数的调用。这种请求数据的方式叫做JSONP
  • 特点
    • 不属于真正的ajax请求,未使用XMLHttpRequest对象
    • 仅支持GET请求
  • 注意事项
    • 如果项目中已经配置了CORS跨域资源共享,为了防止冲突,必须在配置CORS中间件之前声明JSONP接口,否则JSONP接口会被处理成开启了CORS的接口。
      const express = require('express');
      const cors = require('cors');
      
      const app = express();
      
      // 先创建JSONP接口
      app.get('/jsonp', (req, res) => {})
      
      // 再配置CORS中间件
      app.use(cors());
      
      app.listen(80, () => {
          console.log('web server is running!');
      })
      
  • 步骤
    • 获取客户发送过来的回调函数名称

    • 得到要通过JSONP形式发送给客户端的数据

    • 根据前两步得到的数据,拼接出一个函数调用的字符串

    • 把上一步拼接得到的字符串,响应给客户端的<script>标签进行解析执行

      app.get('/jsonp', (req, res) => {
          const funcName = req.query.callback;
          const data = {
              name: 'elio',
              age: 26,
              message: 'get request'
          };
          const scriptString = `${funcName}(${JSON.stringify(data)})`
          res.send(scriptString)
      })
      

      <script>src方式

      <body>
        <script>
          function funTest(data) {
            console.log(' -- success -- ', data);
          }
        </script>
        <script src="http://127.0.0.1:3008/jsonp?callback=funTest"></script>
      </body>
      

      Snipaste_2023-04-30_22-09-48.jpg

      JQueryJSONP 【有误,待修正】

      <button onclick="jsonpClick()">JSONP请求</button>
      
      function jsonpClick() {
          $.ajax({
              method: 'get',
              url: 'http://127.0.0.1/jsonp',
              dataType: 'jsonp', // 表示发起JSONP请求
              success: (res) => {
                  console.log(' == res == ', res);
              }
          })
      }
      

Mysql模块

安装与配置mysql模块

安装

本地安装mysql即可

配置
// 导入mysql模块
const mysql = require('mysql')

// 建立与mysql数据库的连接
const db = mysql.createPool({
    host: '127.0.0.1', // 数据库的ip
    user: 'root', // 登录数据库的用户名
    password: 'hello', // 登录数据库的密码
    database: 'elio_system' // 指定操作的数据库
})

// 检测 mysql 模块是否能正常工作
db.query('select 1', (err, results) => {
    if (err) return console.log(err.message)
    console.log(results)
})

查看数据库的ip - select SUBSTRING_INDEX(host,':',1) as ip , count from information_schema.processlist group by ip;

操作数据库表

查询表数据
// 查询表数据
const sqlStr = 'select * from eduinf'
db.query(sqlStr, (err, results) => {
  if (err) return console.log('-- err -- ')
  console.log(results) // 结果时个数组
})
插入表数据
// 1. 要插入到admin表中的数据对象
const admin = {name: 'test', pwd: 'test'}

// 2. 待执行的 SQL 语句,其中英文的 ? 表示占位符
const sqlInsertStr = 'insert into admin(admin_name, admin_pwd) values(?, ?)'

db.query(sqlInsertStr, [admin.name, admin.pwd], (err, results) => {
  if (err) return console.log(' -- err -- ', err)
  console.log(results)
  if (results.affectedRows) console.log('插入数据成功!')
})

/**
OkPacket {
  fieldCount: 0,
  affectedRows: 1,
  insertId: 2,
  serverStatus: 2,
  warningCount: 0,
  message: '',
  protocol41: true,
  changedRows: 0
}
*/
更新数据
// 更新数据
const admin = {id: 2, name: 'test', pwd: 'test123'}
const sqlStr = 'update admin set admin_pwd=? where admin_id=?'
db.query(sqlStr, [admin.pwd, admin.id], (err, results) => {
  if (err) return console.log(' -- err -- ', err)
  console.log(results)
  if (results.affectedRows) console.log('数据更新成功!')
})
删除数据
// 删除数据
const sqlStr = 'delete from admin where admin_id=?'
db.query(sqlStr, 2, (err, results) => {
  if (err) console.log(' -- err -- ', err.message)
  if (results.affectedRows) console.log('删除数据成功!')
})

前后端的身份认证

Web开发模式

  • 服务端渲染

    • 优点
      • 前端耗时少
      • 有利于SEO(页面由后端渲染,爬虫容易爬取页面内容)
    • 缺点
      • 占用服务器资源
      • 不利于前后端分离,开发效率低
  • 前后端分离

    依赖于ajax技术广泛应用
    后端只提供API接口
    前端使用ajax调用接口
    
    • 优点
      • 开发体验好 - 前后端分离,业务逻辑清晰
      • 用户体验好 - ajax局部刷新
      • 减轻服务器端压力
【如何取舍】-【根据实际业务】
- 没有api请求的可以使用服务端渲染
- 前端业务逻辑复杂的,选用前后端分离

身份认证

  • 服务端渲染 - session认证机制
  • 前后端分离 - JWT认证机制
Session认证机制
HTTP协议的无状态性
客户端的每次HTTP请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次HTTP请求的状态
如何突破HTTP无状态协议
  • Cookie
    • Cookie是存储在用户浏览器中的一段不超过4KB的字符串。

      • 由一个 名称(name)、值(value)和其他几个用于控制Cookie有效期、安全性、使用范围的可选属性组成。
      • 不同域名下的Cookie各自独立。每当用户端发起请求时,会自动把当前域名下所有未过期的Cookie一同发送到服务器。
    • 特性

      • 自动发送、域名独立、过期时限、4KB限制
    • Cookie不具有安全性

      • 不要使用Cookie存储重要且隐私的数据
Session的工作原理
Client】提交账号/密码
【Server】
    验证账号/密码。
    将登录成功后的用户信息存储在服务器的内存中,同时生成对应的Cookie字符串。通过响应头将Cookie发送给客户端
【Client】自动把Cookie存储在当前域名下
​
【Client】再一次发送请求时,请求头携带当前域名下所有未过期的Cookie发送给服务器
【Server】
    服务器根据请求头携带的Cookie,从内存中查找出对应的信息。
    用户的身份认证成功后,服务器针对当前用户生成特定的响应内容返回给浏览器
Express中使用Session认证
  • 安装express-session依赖包

    npm install express-session
    
  • 配置express-session中间件

    const express = require('express')
    
    // 创建express的服务器实例
    const app = express();
    
    //导入session中间件
    const session = require('session')
    
    // 配置session中间件
    app.use(session({
        secret: 'keyboard cat', // secret属性值可以为任意字符串
        resave: false, // 固定写法
        saveUninitalized: true // 固定写法
    }))
    
  • Session中存数据

    • express-session中间件配置成功后,即可通过req.session来访问和使用session对象,从而存储相关信息
    app.post('/api/login',(req, res) => {
        const data = req.query
        req.session.user = req.query
        req.session.isLogin = true
        
        res.send({
            status: 200,
            msg: '登录成功!'
        })
    })
    
  • session中取数据

    app.get('/api/getUser',(req, res) => {
        if (!req.session.isLogin) {
            return res.send({
                status: 2001,
                msg: 'fail'
            })
        }
        
        res.send({
            status: 200,
            msg: 'success',
            data: req.session.user
        })
    })
    
  • 清空Session - 只会清空当前用户的Session,不是所有

    • 业务场景:用户退出系统
    app.post('api/logout', (req,res) => {
        req.session.destory();
        res.send({
            status: 200,
            msg: '退出成功'
        })
    })
    
JWT认证机制
Session认证的局限性
  • Session认证机制需要配合Cookie才能实现。Cookie不支持跨域访问,当前后端进行跨域请求时,需要很多额外的配置。
what is JWT
  • JWT(JSON Web Token)是目前最流行的跨域认证解决方案
JWT工作原理
Client】提交账号密码
【Serve】
    验证账号密码
    验证通过后,将用户的信息对象经过加密生成Token字符串 【加密】
    发送给客户端
【Client】将Token存储到WebStorage中(LocalStorage/SessionStorage)

【Client】再一次发送请求时,通过请求头的 Authorization 字段,将Token发送给服务器
【Server】
    服务器把Token还原成用户的信息对象 【解密】
    用户身份认证成功后,服务器针对当前用户生成特定的响应内容返回给浏览器

【总结】

  • 用户的信息通过Token的形式,保存在客户端浏览器中。服务器通过还原Token来认证用户身份
JWT组成部分

三部分组成:Header(头部)、Payload(有效荷载)、Signature(签名)。三者之间用 . 分割

  • Payload: 真正的用户信息,是用户信息经过加密后生成的字符串
  • Header/Signature:安全性相关的部分,为了保证Token的安全
JWT的使用方式
  • 客户端接收到服务器返回的JWT后,存储在WebStorage
  • 此后每次通信客户端把JWT放在HTTP请求头的Authorization字段中
    Authorization: Bearer <token>
    
Express中使用JWT
安装JWT相关的包
npm install jsonwebtoken express-jwt
  • jsonwebtoken - 用于生成JWT字符串
  • express-jwt - 用于将JWT字符串解析还原成JSON对象
导入JWT相关的包
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
定义secret密钥
  • 为了保证JWT字符串的安全性,防止JWT在网络传输过程中被破解,需要专门定义一个用于加密解密secret密钥

    • 生成JWT之前,使用secret密钥进行加密,得到一个JWT字符串
    • JWT还原JSON时,需要使用同一个secret密钥进行解密
  • 定义一个普通字符串即可,越复杂越好

    // secret密钥的本质:就是一个字符串
    const secretKey = 'jtsecret*@*^&^'
    
登录成功后生成JWT字符串
  • 调用jsonwebtoken包提供的sign()方法,将用户信息加密成JWT字符串,响应给客户端
    //登录接口
    app.post('/spi/login', (req, res) => {
        res.send({
            status: 200,
            message: '登录成功',
            // 三个参数:用户信息对象,加密密钥,配置对象
            token: jwt.sgin({username: username}, secretKey, {expiresIn: '30s'})
        })
    })
    
JWT还原成JSON对象
  • 客户端每次访问有权限的接口时,都需要主动通过请求头中的Authorization字段,将Token发送到服务器进行身份验证

  • 服务器通过express-jwt解析tokenJSON对象

    // 使用app.use()来注册中间件
    // expressJWT({secret: secretKey})解析Token
    // .unless({path:[/^/api//]})用来指定哪些接口不需要访问权限
    
    // 【注意】:只要配置成功了express-jwt这个中间件,可以把解析的用户信息挂载到req.user属性上
    
捕获解析JWT失败后产生的错误
  • express-jwt解析Token时。客户端发送的Token不合法期,或产生一个解析失败的错误。通过Express的错误中间件捕获这个错误并进行相关的处理
    app.use((err, req, res, next) => {
        // token解析失败导致的错误
        if (err.name === 'UnauthorizedError') {
            return res.send({
                status: 401,
                message: '无效的token'
            })
        }
        
        // 其他的错误
        res.send({
            status: 500,
            message: '未知错误'
        })
    })
    

【完整代码】

const express = require('express')

const app = express();

// 1. 导入 JWT 相关的包
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

// 解析 post 表单数据的中间件
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))

// 2. secret密钥
const secretKey = '^jwt&secret@elio^'

// 4. 注册将 JWT 还原成 JSON 对象的中间件
// 【注意】只要配置成功 express-jwt 这个中间件,就可以把解析出来的用户信息挂载到 req.user 这个属性上
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^/api//]}))

// 登录接口
app.post('/api/login', (req, res) => {
  const userInfo = req.body;

  if (userInfo.username !== 'admin' && userInfo.password !== 'admin000') {
    return res.send({
      status: 2000,
      message: '登录失败'
    })
  }

  const tokenStr = jwt.sign({ username: userInfo.username }, secretKey, {expiresIn: '30s'})
  res.send({
    status: 200,
    message: '登录成功',
    token: tokenStr
  })
})

// 获取用户信息
app.get('/getUserInfo', (req, res) => {
  // 5. 使用 req.auth.user 获取用户信息,并响应给客户端
  console.log(req.user)
  res.send({
    status: 200,
    message: '获取用户信息成功',
    data: req.user
  })
})


// 6. 声明一个全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res, next) => {
  // token解析失败导致的错误
  if (err.name === 'UnauthorizedError') {
      return res.send({
          status: 401,
          message: '无效的token'
      })
  }
  
  // 其他的错误
  res.send({
      status: 500,
      message: '未知错误'
  })
})

app.listen(8888, () => {
  console.log('express server running at http://127.0.0.1:8888')
})
补充说明
express-jwt版本太高引发的问题

node.js 使用 express-jwt 报错:expressJWT is not a function_expressjwt设置中间件没有生效_潮汐未见潮落的博客-CSDN博客

Node-Server-Project

GitHub - github.com/elio1900/no…