Node

375 阅读22分钟

初识Node

定义

node.js 是一个基于ChromeV8引擎的 JavaScript运行环境,在浏览器中运行js为前端,在node中运行JS代码为后端。(V8引擎解析效率最高)

node的运行环境核心部分

  1. V8引擎
  2. 内置API 注:node中无法调用Dom和Bom等浏览器API

Node.js 可以做什么

  1. 基于express框架,快速web构建web应用
  2. 基于Electron框架 ,可以构建跨平台的桌面应用
  3. 基于restify框架,可以快速构建API接口项目
  4. 文件读写和操作数据库、创建实用的命令行工具辅助前端开发。

学习路径

JS基础语法 + Node.js内置模块 + 第三方API模块(express、 mysql)

读写文件(require('fs'))

  1. 读取指定文件中的内容 fs.readFile(path[, options], callback) 参数,被中括号包裹的为 可选参数项。
path 表示文件的路径;
options 表示以什么编码格式来读取文件,默认为utf-8;
callback 文件读取完成后通过回调函数拿到读取的结果;
导入读写模块
const fs = require('fs');
fs.readFile('../../node_base/千峰.txt', 'utf8',(err, dataStr) =>{
    console.log(err);   // 输出null,没有文件输出错误对象
    console.log('--------');
    console.log(dataStr);  // 输出文件内容,没文件输出undefined
} ) 
  1. 向指定的文件中写入内容 fs.writeFile(path,data[, options], callback)
  path:为想要写入的文件的地址。
     (1)若没有这个文件则先创建此文件;
     (2)如果有这个文件,先把原来的内容清空,再把想要写入的内容写入;
     (3)只能创建文件不能创建路径(文件的上一层)
  data : 想要写入的内容
fs.writeFile('./write.txt', '44', (err) =>{
    console.log(err);  // 写入成功为null,否则输出错误对象
}) 

注: 以相对路径为路径时,容易出现路径的动态拼接错误问题。 原因: 代码在运行时,会以执行node命令时所处的目录,动态拼接处被操作文件的完整路径。

解决:

  • (1)写绝对路径从盘符开始,斜线需要双斜线,否则代表转义。(移植性、维护性较差)
  • (2) _ _dirname: 表示当前文件所处的目录(以 + 拼接)

path模块(const path = require('path');)

定义:path模块是Node官方提供的用来处理路径的模块,它提供了一系列方法和属性,用来满足用户对路径的处理请求。

  1. path.join([...path]): 可以将多个路径片段拼接为完整的路径,返回路径的字符串。
const pathStr = path.join(__dirname, 'fs_read_write.js')
console.log(pathStr); //C:\Users\a\Desktop\前端之路\node.js\node\node_base\fs_read_write.js

const pathStr1 = path.join('/a', '/b/c', '../', '/c'); // "../"会抵消前面的路径
console.log(pathStr1); // 输出\a\b\c
  1. path.basename(path[,ext]): 可以获取路径中的最后一部分,可以用这个方法获取文件名

path: 要截取的路径; ext:可以去掉获取的文件的扩展名

const fPath = path.basename('C:/Users/a/Desktop/node/node_base/fs_read_write.js');
console.log(fPath); // fs_read_write.js
  1. path.extname(path): 获取文件的扩展名,path: 路径。
 const pathExt =  path.extname('C:/Users/a/Desktop/node/node_base/fs_read_write.js');
 console.log(pathExt); // .js

☆http

定义:

客户端: 在网络节点中,负责消费资源的叫客户端; 服务器: 负责对外提供网络资源的叫服务。 http模块是Node官方提供的一个用来创建web服务器的模块,通过http提供的http.createServer()方法,就方便把一台普通的电脑变成服务器,从而向外部提供web网络资源,不需要第三方服务器软件。 ip地址: 身份证号; 域名: 姓名; 域名服务器(DNS): 将域名转换为IP使能够访问 ;

端口号:一个电脑可以有上百个web服务器,但是每个端口号都对应唯一一个服务器(门牌号),在实际应用中,URL中的80端口可以被省略;

创建HTTP服务器

创建的基本步骤

// 1. 导入http模块
const http = require('http');

// 2.创建web服务器实例
const server =  http.createServer();

// 3. 为服务器实例绑定request方法, 即可监听客户端发过来的网络请求
server.on('request', (req, res) => { 
// 只要有客户端来请求我们的服务器,就会触发request方法,从而触发回调
console.log("表哥,我出来了哦");
})

// 4.启动web 服务器 
server.listen(80, () => {
console.log('启动server');
})

req请求对象

req是客户端的请求对象,存放了客户端的相关数据或属性,例如:

req.url 是客户端请求的url地址; req.method是客户端的method请求类型

server.on('request', (req, res) => { 
// 只要有客户端来请求我们的服务器,就会触发request方法,从而触发回调
console.log("someone visit me");
console.log('客户端地址:' + req.url ); // 客户端地址:/
console.log("请求方式:" + req.method); //请求方式:GET
})

res响应对象

在request中可以访问server的相关数据或属性,例如:

  1. res.end(): 想客户端发送指定的内容,并结束这次请求的处理过程;
  2. res.setHeader():设置请求头, = 两边不能留空格;
server.on('request', (req, res) => { 
//   为了防止中文显示乱码的问题,需要设置响应头, 
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.end('返回的数据')
})

根据不同的url相应不同的html页面

注意不能给content赋值为常量

server.on('request', (req, res) => { 

  const url = req.url;
  let content = 'Not found 404!!!';
  if(url === '/' || url === '/index.html') content = '<h1>首页</h1>';
  if(url === '/about.html')   content = '<h1>关于</h1>';

  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.end(content)
})

案例:

  1. 把每个资源实际的存放路径,作为每个资源的请求url地址
  2. link 标签导入的文件会自动去拼接客户端请求文件的地址
server.on('request', (req, res) => { 

  let url = req.url;
  // 帮用户补全地址
  if(url === '/')  url = '/index.html'
  //把请求的路径映射为本地路径
  const fPath = path.join(__dirname, url);
  
 //根据地址读取文件
  fs.readFile(fPath,(err,dataStr) =>{
      //响应并返回客户端   
      if(err) return res.end('<h1>Not Found 404</h1>');
      res.end(dataStr);
  })  
 
  
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.end(content)
})

模块化

Node中的模块化

  1. require加载模块
1.加载内置模块
const http = require('http');

2.加载自定义模块(需要路径)
const http = require('./index.js');

3.加载第三方模块
const http = require('express');

注: 当使用require加载模块时,会执行被加载模块中的代码。 2. 模块作用域

虽然js可以被引入,但是不能在引入的模块被使用,因为变量具有作用域; 优势:

  • 防止变量的全局污染
  1. 向外共享模块作用域中的成员
  • module对象存储了当前模块的信息
  • module中的export属性可以向外暴露作用域中的变量,被引入模块使用; 注:外界使用require引入自定义模块时,得到的就是module.export中的内容。
  1. module.export的使用 导出的输出为:{ name: '赵本山', school: 'F4' };
module.exports.name = '赵本山';
module.exports.school = 'F4';

导出的输出为:{ F1: '刘能', F2: '赵四', F3: '宋小宝', F4: '小沈阳' };

module.exports.name = '赵本山';
module.exports.school = 'F4';

module.exports = {
    F1: '刘能',
    F2: '赵四',
    F3: '宋小宝',
    F4: '小沈阳'
}

注: 导出的结果永远以module.exports指向的对象为准。 5. export对象 定义: 由于module.exports单词写起来比较复杂,所以Node提供了exports对象,通常来说exports和module.exports指向同一个对象,最终共享结果还是以module.exports为准。

Node的模块化规范

定义:Node遵循了commonJs的模块化规范,commonJs规定了模块的特性和各模块之间的相互依赖。

CommonJs规定:

  1. module: 每个模块内部,module变量代表当前模块。
  2. module.exports: module的exports属性为对外的接口。
  3. require(): 加载依赖模块的module.exports 属性。

包(第三方模块)与npm

npm 的使用

  1. npm i 包名: 装包。
  2. npm uninstall 包名:卸载包且自动把包从dependencies节点删除。
  3. npm install: 安装的包匹配dependencies节点中的包。
  4. npm i 包名 -D(npm i 包名 --save-dev):把包记录在devDependencies节点中。
  5. npm config get registry: 查看当前包的镜像源。
  6. npm config set 镜像源: 修改镜像源。 nrm: 方便切换镜像源的工具
  7. nrm is: 查看所有可用镜像源。
  8. nrm use 镜像源:将镜像源切换为。

package.json文件

  1. npm init -y : 初始化package.json文件,只能在英文的目录下运行且不能有空格。
  2. dependencies节点: 记录npm install安装了哪些包。
  3. devDependencies节点: 包只在开发的时候用,在上线之后就不在用了,则把包记录在此节点中。

包的分类

项目包:安装到node_modules中的包都可以叫项目包。

  1. 开发依赖包: 被记录到devDependencies节点中。
  2. 核心依赖包:被记录到dependencies节点中。 全局包: 默认安装到C:\Users\a\AppData\Roaming\npm\node_modules路径下
  3. npm i 包名 -g: 全局安装指定的包。
  4. npm uninstall 包名 -g: 卸载全局安装的包。 注:只有工具性的包才有全局安装的必要。

包的结构

写包:

  1. 必须以单独的目录存在。
  2. 必须有package.json配置文件。
  3. package.json配置文件中必须包含name、version、main三个属性,分别代表包的名字、版本号、入口。 登录npm:
  4. 注册 npm 账号。,
  5. 在终端登录npm:在终端输入npm login命令,依次输入用户名、密码、邮箱后即可登录成功。(镜像源必须是Npm官方的镜像) 发布包:
  6. 在终端切换到包的根目录上,运行 npm publish 即可发布。 删除包:
  7. npm unpublish 包名 --force 即可删除发布的包
  8. 只能删除 72 小时的包,超过不可再删除。
  9. 24小时内不可重复发布相同名字的包。

模块的加载机制

  1. 模块优先从缓存中加载。
  2. 内置模块的加载优先级最高。
  3. 在加载自定义模块时,require没有用路径符 '/' Node会把其当做内置或第三方模块加载,找不到则加载失败。
  4. 在Node加载自定义模块时,如果省略了文件的扩展名,则Node会以以下的顺序进行补全。
  1. 按照确切的文件名进行加载。
  2. 补全 JS 进行加载。
  3. 补全 JSON 进行加载。
  4. 补全node扩展名进行加载。
  5. 加载失败,终端报错。
  1. Node加载第三方模块的加载顺序,先从当前文件的父目录开始,尝试从node_modules 中加载文件,如果找不到,则去上一层目录的 node_modules 中寻找,直到电脑的根目录。
  2. 如果把目录(文件夹)作为模块表示符,传递给require加载的时候有三种加载方式:
1. 在被加载的目录下查找package.json文件中的main属性,作为加载的入口。
2. 如果目录不存在package.json 文件,或main属性不存在或无法解析,则Node会加载目录下的index.js文件。
3. 如果上两步都失败了,则报错。

express框架

定义: 本质是 npm 的一个第三方包,是专门用来创建web服务器(网站服务器或接口服务器)的。

创建基本的服务器

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

// 创建服务器

const app = express();

// 启动服务器

app.listen('8090', () =>{
    console.log('不写localhost就默认127.0.0.1');
})

监听GET 和 POST 请求并处理参数

app.use(): 注册全局中间件

监听请求并返回数据

  1. 监听GET或POST请求: app.get('请求地址', (req, res) => {// 回调函数}。post请求把get换成post就可以。
  2. 向客户端发送数据: res.send()
// 监听客户端的get和post请求
app.get('/user', (req, res) =>{
    // 调用express的res.send方法向客户端响应一个json对象
    res.send({name:'赵本山', school: 'F4'})
})
app.post('/user', (req, res) =>{
    // 普通文本
    res.send('I am F4')
})

处理参数

  1. 获取URL中携带的参数(以?续参): req.query对象中包含以字符串传过来的数据,默认为空。
请求格式:http://127.0.0.1:8090/user?name=赵本山&school=F4

app.get('/user', (req, res) =>{
    //  获取通过URL传过来的参数。 
    console.log(req.query);
})
  1. 获取动态匹配:req.params对象来匹配到通过':'或 params形式传的参。
请求格式:http://127.0.0.1:8090/user/001

app.get('/user/:id', (req, res) =>{
    res.send(req.params);  //  req.params是动态匹配到的url参数
})

输出: {id: '001'}

注: / 不是分隔,:才是分隔,有无 / 都可,但是必须得有:多个变量的话用 / 做分隔。

静态资源处理

  1. express.statics(): 通过它我们可以创建一个静态资源服务器,使静态资源对外开放。
public目录下的文件对外开放:
app.use(express.static('./public'))

访问:
http://127.0.0.1:8090/index.css

注:

 访问的时候省略开放的文件夹,不用写。
 托管多个静态资源文件时,多次写app.use(express.static('./public'))托管语句就行,资源的查找顺序,按托管的先后。

2. 挂载路径前缀:上面的方法请求资源时省略了路径前缀,当引进多个相同名字的静态资源文件夹时也做不到精准匹配。

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

访问:
http://127.0.0.1:8090/public/index.css

前面的public也可以起别名。

nodemon工具:自动重启服务器

使用:由原本的 node app.js 启动服务器改为 nodemon app.js

路由

概念

定义: 指的是客户端请求和服务端处理函数之间的映射关系。

路由组成:请求的类型、 请求的URL地址、 处理函数

格式: app.METHOND(PATH, CALLBACK)

执行匹配: 按组成的顺序进行匹配,请求的类型和请求的URL必须同时满足才能去调用回调。

使用

  1. 最简单的用法:app.get('请求地址', (req, res) => {// 回调函数})

路由模块化

  1. 创建路由模块对应的JS文件。
  2. 调用express.Router()函数创建路由对象。
  3. 向路由对象上挂载具体的路由。
  4. 使用module.exports向外共享路由对象。
  5. 使用app.use()函数注册路由模块。 路由页面
const  express = require('express');
const  router = express.Router();

router.get('/user', (req, res) =>{ 
    res.send('getUser')
})

router.post('/user', (req, res) =>{ 
    res.send('postUser')
})

module.exports = router;

服务器页面

const express = require('express');
const router = require('./router/user');

const app = express();

app.use(router);


app.listen('8090', () =>{
    console.log('不写localhost就默认127.0.0.1');
})

注: 注册路由模前添加前缀,可用来区分不同的模块,不用再在模块中的每个api都加上自己的模块名做区分

app.use( '/api', router);

请求命令:
http://127.0.0.1:8090/api/user

中间件

定义:对请求进行预处理,express中间件本质上是function函数, req和res是共享的(包括api回调中的那个)。 注: 中间件的函数中必须包含next迭代器。

const mw = function(req, res, next){
    console.log("最简单的中间件");
    next()
}

全局生效的中间件

定义: 客户端发起的请求到达服务器后,都会触发的中间件叫做全局中间件。 注册: 需要在注册路由之前,否则res.send结束请求,不能执行中间件

app.use(mw)

作用:在上游的中间件中为 res 和 req 中添加方法或属性,方便在api中使用。

局部中间件

定义: 只在局部起作用,不一定非要执行。 使用:不要用app.use注册

只在该接口调用的时候才执行mw中间件
app.get('/user', 'mw', (req, res) =>{ 
    res.send('getUser')
})

注册多个局部中间件
方法1:
app.get('/user', 'mw', 'mw1', (req, res) =>{ 
    res.send('getUser')
})
方法2:
app.get('/user', ['mw', 'mw1'], (req, res) =>{ 
    res.send('getUser')
})

注意

  1. 一定要在路由之前注册中间件。
  2. 客户端发送过来的请求,可以调用连续的中间件做处理。
  3. 自定义中间件中一定调用next。
  4. 为防止逻辑混乱,在next之后就不要写代码了。
  5. res 和 req 是全部的中间件共享的,包括路由。

☆中间件的分类

  1. 应用级别的中间件 定义: 通过app.use、app.get、app.post等,绑定到app实例上的中间件,叫做应用级别的中间件。
  2. 路由级别的中间件 定义: 绑定到router实例上的中间件。
const express = require('express');
const router = require('./router/user');

const app = express();

const mw = function(req, res, next){
    console.log("最简单的中间件");
    next()
}

router.use(mw);
app.use(router);
  1. 错误级别的中间件 定义: 专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。

格式: function(err, req, res, next){}

app.use((err, req, res, next) => {
    res.send(err)
})

注: 要放在所有的中间件后面,做承接作用。

  1. express的内置中间件(必须全局注册)
  • express.static:托管静态资源
  • express.json:解析json 格式的请求体数据,仅在4.16.0版本后可用。
app.use(express.json())
  • express.urlencoded:解析 URL-encoded 格式的请求体数据,仅在4.16.0版本后可用。
app.use(express.urlencoded({extend: false}))

注: 如果不配置解析表单数据的中间件,req.body对象默认为undefined

  1. 第三方中间件 安装, 引入, 注册中间件

自定义中间件

步骤:

  1. 定义中间件
  2. 监听req的data事件(必须是post等携带数据的请求类型,否则不触发req的事件)。
  3. 监听req的end事件。
  4. 使用querystring模块解析请求体数据。
  5. 将解析出来的数据对象挂载到 req.body 上。
  6. 将自定义中间件封装为模块。
const mw = function(req, res, next){
    let str = ''
    req.on('data', (chunk) =>{
        // 当数据过多时会分批传送,不断地触发data事件。
        str += chunk;
    })
    req.on('end', () => {
        // 将字符串解析为对象
        req.body = qs.parse(str)
        next()
    })
}


app.use(mw);

注:

  • 请求的方式为post等携带body参的。
  • next的结束位置要在req的end事件中。

编写接口

GET接口

路由:
const express = require('express');
const router = express.Router();

router.get('/get', (req, res) =>{
     res.send({
         status: 0,
         msg: 'GET 请求成功',
         data: req.query
     })
})

module.exports =  router;


服务器:
const express = require('express');
const goodRouter = require('./router/good');
const app = express();

app.use('/api', goodRouter);

app.listen('8060', () =>{

})

post接口

路由接口:
router.post('/post', (req, res) =>{
    res.send({
        status: 200,
        msg: 'post 请求成功',
        data: req.body
    })
})

中间件将数据格式转换:调用内置中间件

app.use(express.urlencoded({extended: false}))

注: 必须全局注册,否则不生效。

跨域

jQuery写ajax做例子

 <button id="get">
        GET
    </button>
    
    <button id="post">
        POST
    </button>
    <script>
        $('#get').on('click', function () {
            $.ajax({
                type: 'GET',
                url: 'http://127.0.0.1:8060/api/get',
                data: {
                    name: "赵本山",
                    school: 'F4'
                },
                success: function(res){
                    console.log(res);
                }
            })
        })
        $('#post').on('click', function () {
            $.ajax({
                type: 'POST',
                url: 'http://127.0.0.1:8060/api/post',
                data: {
                    name: "赵本山",
                    school: 'F4'
                },
                success: function(res){
                    console.log(res);
                }
            })
        })
    </script>

CORS解决跨域问题

定义: CORS是第三方插件,可以解决第三方跨域问题

使用:

  1. npm install cors 安装中间件。
  2. const cors = require('cors') 导入
  3. app.use(cors()) 注册配置 注:要在路由之前

原理:

cors由一系列HTTP响应头组成,这些http响应头决定了浏览器是否阻止前端js代码进行跨域获取资源。

浏览器的同源安全策略会默认阻止网页跨域获取资源。但如果接口服务器配置CORS相关的htp响应头,就可以解除浏览器端的跨域限制

注意:

CORS只有在服务端才可配置,客户端无需做任何事情。 CORS具有兼容性,只有支持 XMLHttpRequest level2 的浏览器才兼容

☆CORS响应头

  1. Access-Control-Allow-Origin: 允许哪些网页来访问服务器
只允许百度网页请求
res.setHeader('Access-Control-Allow-Origin', 'https://www.baidu.com/')
  1. Access-Control-Allow-Headers:声明请求头

image.png 使用:

res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
  1. Access-Control-Allow-Headers:声明请求方式(get、post、HEAD除外)

image.png 使用:

res.setHeader('Access-Control-Allow-Methods', 'PUT')

CORS 请求的分类

定义:根据请求方式和请求头可以将请求分为简单请求和预检请求。

  1. 简单请求:

image.png 2. 预检请求

image.png 3. 简单请求和预检请求的区别

在项目中操作数据库

安装配置mysql模块

在根目录下建立config文件夹,建立index.js文件进行配置

npm i mysql: 安装模块;

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

通过mysql模块连接到mysql数据库

//  建立与MySQL数据库的联系
const db = mysql.createPool({
    host: '127.0.0.1', // 数据库的ip
    user: 'root',  // 登录数据库的用户名
    password: '123',  // 登录数据库的密码
    database: 'bipowernode'  // 指定要操作的数据库名字
})

通过mysql模块执行sql语句

  1. 查询
// 查询dept表的所有数据
db.query('select * from dept',(err, result) =>{
    if(err) console.log(err);
    console.log(result);
})

注: select查询语句输出的结果为数组。 2.插入

  • 普通插入
// 要插入的数据
const deptDate = {DEPTNO: 99, DNAME: '摸鱼部', LOC: '济南'};
// 插入的语句, ? 为占位符
// sql不区分大小写,包括表中的字段名,但是sql尽量大写,字段名根据真实的字段名来写
const insert = 'INSERT INTO dept(DEPTNO, DNAME, LOC)values(?, ?, ?)'
// 使用数组的形式依次为 ? 占位符指定具体的值。
db.query(insert, [deptDate.DEPTNO, deptDate.DNAME, deptDate.LOC],(err, result) =>{
    if(err) console.log(err);                             
    if(result.affectedRows === 1)  console.log("插入成功"); // 当result.affectedRows === 1表示插入成功
})
  • 插入数据对象和字段为一一对应的关系时
// 要插入的数据
const deptDate = {DEPTNO: 99, DNAME: '摸鱼部', LOC: '济南'};
// 
const insert = 'INSERT INTO dept SET ?'
// 直接将数据对象顶替占位符。
db.query(insert, deptDate,(err, result) =>{
    if(err) console.log(err);                             
    if(result.affectedRows === 1)  console.log('成功'); 
})

注: result 结果为对象 3. 更新

  • 普通方式
// 要修改的数据对象
const deptDate = {DEPTNO: 99, DNAME: '小沈阳', LOC: '沈阳'};
// 用问号来占位,和限制条件
const upDept = 'UPDATE dept SET DNAME = ?, LOC = ? WHERE DEPTNO = ?'
// 用数组来顶替占位符。要按占位符顺序赋值。
db.query(upDept, [deptDate.DNAME, deptDate.LOC, deptDate.DEPTNO],(err, result) =>{
    if(err) console.log(err.message);                             
    if(result.affectedRows === 1)  console.log('更新成功'); 
})
  • 简便写法
const deptDate = {DEPTNO: 99, DNAME: '小沈阳', LOC: '沈阳'};
const upDept = 'UPDATE dept SET ? WHERE DEPTNO = ?'
// 条件要单独出来
db.query(upDept, [deptDate, deptDate.DEPTNO],(err, result) =>{
    if(err) console.log(err.message);                             
    if(result.affectedRows === 1)  console.log('更新成功'); 
})
  1. 删除(要用标记删除)
  • 普通删除
const upDept = 'DELETE FROM dept WHERE DEPTNO = ?'
// 当where条件为一个时,可以省略数组
db.query(upDept, [99],(err, result) =>{
    if(err) console.log(err.message);                             
    if(result.affectedRows === 1)  console.log('删除成功'); 
})
  • 标记删除: 设置一个status字段,如果为1,则是使用状态, 为0,则标记为删除

const upDept = 'UPDATE dept SET status = ? WHERE DEPTNO = ?'
// 条件要单独出来
db.query(upDept, [0, 99],(err, result) =>{
    if(err) console.log(err.message);                             
    if(result.affectedRows === 1)  console.log('标记删除成功'); 
})

web端开发模式

服务器端渲染开发(不用ajax)

常用: 企业级网站展示,没有复杂的交互,并且需要良好的SEO;

优点:

前端耗时少:服务器负责生成HTML,浏览器只需要渲染页面就可以(不用ajax去请求页面了),尤其是移动端,更省电。 有利于SEO: 服务器端响应的是完整的HTML页面,更利于爬虫。 缺点: 占用服务器端的资源:页面都是在服务器端拼接,如果请求较多,会对服务器造成压力。
不利于前后端分离,开发效率低。

前后端分离的开发模式(用ajax)

常用:类似于后台管理,逻辑较多,交互性强。

优点:

开发体验好:前端专注于UI的开发,后端专注于接口的开发 用户体验好:ajax的广泛使用,实现了局部的刷新 减轻了服务端的渲染压力: 最终页面在浏览器直接生成 缺点: 不利于SEO:html 在客户端拼接完成,不利于爬虫。(利用vue,react的SSR技术可以解决)

身份认证(cookie)

定义: 因为HTTP协议具有无状态性,所以服务器不知道请求的人的信息,需要身份认证进行识别

服务器端渲染模式: session身份认证;
前后端分离渲染: jwt身份认证

  1. cookie 概念: image.png 身份认证中的作用:

image.png 不安全性:

image.png 提高身份认证的安全性: 服务器分配cookie,客户端请求时与存储在服务器的信息进行对比

session认证机制

原理:在客户端存储用户登录成功的cookie,在服务器端设置变量存储用户的信息。服务器根据客户端传过来的cookie查看是否有对应的信息。

使用:

安装: npm install express-session

引入配置:
const session = require('express-session')
app.use(
  session({
    secret: 'session_cookie',   //任意字符串
    resave: false,
    saveUninitialized: true,
  })
)

向session中存用户数据:

// 登录的 API 接口
app.post('/api/login', (req, res) => {
  // 判断用户提交的登录信息是否正确
  if (req.body.username !== 'admin' || req.body.password !== '000000') {
    return res.send({ status: 1, msg: '登录失败' })
  }

  // TODO_02:请将登录成功后的用户信息,保存到 Session 中
  // 注意:只有成功配置了 express-session 这个中间件之后,才能够通过 req 点出来 session 这个属性
  req.session.user = req.body // 用户的信息
  req.session.islogin = true // 用户的登录状态

  res.send({ status: 0, msg: '登录成功' })
})

向session中取数据:

// 获取用户姓名的接口
app.get('/api/username', (req, res) => {
  // TODO_03:请从 Session 中获取用户的名称,响应给客户端
  if (!req.session.islogin) {
    return res.send({ status: 1, msg: 'fail' })
  }
  res.send({
    status: 0,
    msg: 'success',
    username: req.session.user.username,
  })
})

清空session:

// 退出登录的接口
app.post('/api/logout', (req, res) => {
  // TODO_04:清空 Session 信息
  req.session.destroy()
  res.send({
    status: 0,
    msg: '退出登录成功',
  })
})

局限性: session认证机制需要配合cookie才能实现,由于cookie默认不支持跨域访问,所以当涉及到前端跨域请求后端接口时,需要做很多额外的配置,才能实现跨域session认证

JWT认证(json web token)

原理:客户端提交账户与密码,服务器生成一个token,把用户的信息保存到token中,再把token发送给客户端,客户端收到后把token存储到storage中,放在每次请求的http请求头Authorization字段中。

image.png 组成:

image.png

image.png 使用:

安装:npm install jsonwebtoken express-jwt

jsonwebtoken: 生成jwt(token)字符串
express-jwt: 将JWT字符串解析还原成JSON对象


const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

定义secret秘钥

// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'linyunre No1 ^_^'

生成

// 登录接口
app.post('/api/login', function (req, res) {
  // 将 req.body 请求体中的数据,转存为 userinfo 常量
  const userinfo = req.body
  // 登录失败
  if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
    return res.send({
      status: 400,
      message: '登录失败!',
    })
  }
  // 登录成功
  // TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
  // 参数1:用户的信息对象
  // 参数2:加密的秘钥
  // 参数3:配置对象,可以配置当前 token 的有效期
  // 记住:千万不要把密码加密到 token 字符中
  const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
  res.send({
    status: 200,
    message: '登录成功!',
    token: tokenStr, // 要发送给客户端的 token 字符串
  })
})

将JWT还原成JSON字符串,挂载到req.user上

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

捕获解析JWT失败的错误(过期,token错误)注意中间件的位置

// TODO_06:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res, next) => {
  // 这次错误是由 token 解析失败导致的
  if (err.name === 'UnauthorizedError') {
    return res.send({
      status: 401,
      message: '无效的token',
    })
  }
  res.send({
    status: 500,
    message: '未知的错误',
  })
})

项目过程(www.escook.cn:8088/#/)

初始化

  1. npm init -y : 初始化项目;
  2. npm i express: 安装express;
  3. 创建app.js, 创建web服务器;
  4. npm i cors: 配置跨域;
  5. 配置解析表单数据的中间件;
  6. 创建文件,配置路由模块;
  7. 抽离路由处理函数。

数据库

  1. 建库建表
  2. npm i mysql: 安装mysql;
  3. 连接数据库,暴露;

api处理函数

  1. 检查提交的数据表单是否合法
  2. 处理数据(对密码进行加密becrypt)
  3. 判断成功或失败(封装res.cc中间件),然后res.send