Node.js+Express

292 阅读25分钟

「本文正在参与技术专题征文Node.js进阶之路,点击查看详情

01、初识nodejs

Nodejs在VSCode下代码智能提示

npm install --save-dev @types/node

🤔思考:为什么js可以在浏览器中被执行

image.png

不同的浏览器使用不同的js解析引擎

  • Chrome 浏览器 => v8
  • Firefox 浏览器 => OdinMonkey (奥丁猴)
  • Safri 浏览器 => JSCore
  • IE 浏览器 => Chakra (查克拉)
  • etc.... 其中,Chrome 浏览器的V8 解析引擎性能最好!所以我们前端基本使用Chrome进行调试开发

🤔思考:为什么js可以操作DOM和BOM

image.png

每个浏览器都内置了DOM、BOM 这样的API函数,因此,在浏览器中的js才可以调用它们。

🤔思考:浏览器中的 js 的运行环境

运行环境是指代码正常运行所需的必要环境(比如种子发芽:需要水、空气、土)

image.png

总结:

1、V8引擎负责解析和执行js代码

2、内置API是由运行环境(浏览器)提供的特殊接口,只能在所属的运行环境中被调用

01、Node.js 简介

🤔思考:什么是Node.js

Node.js 是基于Chrome V8引擎的 js运行环境 解析和执行js代码

一段简单的js代码即可以在浏览器中运行,也可以放在node里面执行,放在浏览器里执行就是前端开发,放在node环境下就是后端开发

Node.js 中的 js 运行环境

image.png

总结:

1、浏览器是js的前端运行环境

2、Node.js是js的后端运行环境

3、Node.js中无法调用DOM和BOM 等浏览器内置API

🤔思考:Node.js可以做什么

Node.js 作为一个js的运行环境,仅仅提供了基础的功能和API。然而,基于Node.js提供的这些基础功能,很多强大的工作和框架如雨后春笋,层出不穷,所以学会了Node.js,可以让前端程序员胜任更多的工作和岗位

image.png

🤔思考:Node.js好学吗???怎么学???

浏览器中的 js学习路径 js基础语法 + 浏览器内置API(DOM + BOM) + 第三方库(jq)

Node.js的学习路径

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

02、fs文件系统模块

🤔思考:什么是fs文件系统模块?

fs模块是Node.js官方文档提供的、用来操作文件的模块,提供了一系列方法和属性,用来满足用户对文件的操作需求

例如:

  • fs.readFile()方法,用来读取指定文件的内容
  • fs.writeFile()方法,用来向指定文件写入内容

在js代码中我们,使用fs模块操作文件,需要先引入它

 const fs = require('fs');

🤔思考:读取指定文件中的内容?

使用fs.readFile()方法,可以读取指定文件中的内容,方法如下

   fs.readFile(path[,options],callback)

其中被中括号包裹的是可选参数,其他的是必选参数

参数解读:

  • 参数1:必选参数,字符串,表示文件路径
  • 参数2:可选参数,表示以什么编码格式读取文件
  • 参数3:必选参数,文件读完后,通过回调函数拿到读取的结果

以utf8的编码格式读取,读取指定文件内容,并打印err和dataStr的值:

   const fs = require('fs');
   fs.readFile('./files/1.txt', 'utf8', (err, dataStr) => {
       console.log(err); // null
       console.log('-------');
       console.log(dataStr); // hello
   });

如果文件读取成功err的值为null, dataStr为文件内容

如果文件读取失败err的值为错误对象, dataStr为undefined

🤔思考:如何向指定文件中写入内容?

使用fs.writeFile()方法,可以向指定文件中写入内容,方法如下:

   fs.writeFile(file,data[,options],callback)

参数解读:

  • 参数1: file必选参数,需要指定一个文件路径的字符串,表示文件的存放路径
  • 参数2: data必选参数,表示要写的内容
  • 参数3: options可选参数,表示以什么编码格式写文件,默认utf8
  • 参数3: callback必选参数,文件写完后,写入完成后的回调函数
   const fs = require('fs');
   fs.writeFile('./1.txt', 'world', err => {
    console.log(err); // 成功则为null
   });

练习:考试成绩整理

使用fs文件系统模块,将素材目录下score.txt文件中的考试数据,整理到score-ok.txt文件中, 整理前,score.txt文件中的数据格式如下:

image.png

我们整理后的格式为

image.png

核心实现步骤

  • 导入fs文件模块
  • 使用fs.readFile(),读取score.txt中的内容
  • 判断文件是否读取失败
  • 文件读取成功后,处理数据
  • 将处理好的数据调用fs.writeFile()写入score-ok.txt中
   const fs = require('fs');

    fs.readFile('./score.txt', 'utf8', (err, dataStr) => {
        if (err) {
            console.log('文件读取失败');
            return
        }
        // 先把数据按照空格分割
        const dataStrArr = dataStr.split(' ');
        console.log('dataStrArr: ', dataStrArr); //  [ '小红=99', '小黄=100', '小绿=66' ]
        // 循环分割后的数组,对每一项数据进行字符串的替换操作
        const arrNew = [];
        dataStrArr.forEach(item => {
            arrNew.push(item.replace('=', ': '));
        });
        console.log(arrNew); // [ '小红: 99', '小黄: 100', '小绿: 66' ]
        // 将新数组的每一项进行字符串合并\r\n代表回车换行
        const newStr = arrNew.join('\r\n');
        console.log('newStr: ', newStr); 
        // 将处理好的数据写入心的文件里面
        fs.writeFile('./score-ok.txt', newStr, err => {
            if (err) {
                console.log('写入文件失败!!!');
                return
            }
            console.log('成功了');
        });
    });

🤔思考:fs模块路径动态拼接的问题?

在使用fs模块操作文件时,如果提供的操作路径是以./或者../开头的相对路径时,很容易出现路径动态拼接错误的问题

原因:代码在运行的时候,会以执行node命令时所处的目录,动态拼接出被操作文件的完整路径。

    const fs = require('fs');
    // 不要使用相对路径,绝对路径也尽量不要使用(移植性太低)
    // 采用__dirname,表示当前文件所处的目录,比较恒定的值
    fs.readFile(__dirname + '/1.txt', 'utf8', (err, dataStr) => {
        if (err) return console.log('文件读取失败!');
        console.log(dataStr);
    });

03、path路径模块

path路径模块是Node.js官方提供的、用来处理路径的模块,它提供了一系列的属性和方法,用来满足用户对路径处理的需求

  • path.jion()方法,用来将多个路径片段拼接成一个完整的路径字符串
  • path.basename()方法,用来从路径字符串中,将文件名解析出来

如果要在js代码中,使用path模块来处理路径,则需要使用如下的方法引入它

🤔思考:路径拼接?

使用path.join()方法,可以实现多个路径片段的拼接,方法如下:

   path.join([...paths])

参数解读:

  • 参数1: paths string 路径片段的序列
  • 返回值: 拼接好的路径
    const path = require('path');

    const pathStr = path.join('/a', '/b/c', '../', './d', 'e');
    console.log('pathStr: ', pathStr); // \a\b\d\e

    const pathStr2 = path.join(__dirname, './1.txt');
    console.log('pathStr2: ', pathStr2); // 当前文件所处目录

注意:今后凡是涉及到路径拼接的操作,都要使用path.jion(),不要直接使用+进行字符串的拼接 path.jion()方法可以写文件的相对路径.

🤔思考:jion和resolve的区别?

    const path = require('path'); 
    let myPath = path.join(__dirname,'/img/so'); 
    let myPath2 = path.join(__dirname,'./img/so'); 
    let myPath3 = path.resolve(__dirname,'/img/so'); 
    let myPath4 = path.resolve(__dirname,'./img/so'); 
    console.log(__dirname); //D:\myProgram\test 
    console.log(myPath); //D:\myProgram\test\img\so 
    console.log(myPath2); //D:\myProgram\test\img\so 
    console.log(myPath3); //D:\img\so<br> 
    console.log(myPath4); //D:\myProgram\test\img\so

🤔思考:获取路径中的文件名?

使用path.basename()方法,可以实现获取路径中的文件名,方法如下:

    const path = require('path');

    const fpath = '/a/b/c/index.html';
    const fullName = path.basename(fpath);
    console.log('fullName: ', fullName); // index.html

    // 第二个参数是为了去掉扩展名
    const nameWithoutExt = path.basename(fpath, '.html');
    console.log('nameWithoutExt: ', nameWithoutExt); // index

🤔思考:获取路径中的文件扩展名?

使用path.extname()方法,可以实现获取路径中的文件扩展名,方法如下:

path.extname(path)

参数解读:

  • 参数1: 必选参数 path string 文件路径
  • 返回值: 文件扩展名
    const path = require('path');

    const fpath = '/a/c/index.html';
    const fext = path.extname(fpath);
    console.log('fext: ', fext); // .html

04、http模块

🤔思考:什么是http模块?

回顾:什么事客户端?什么是服务器?

在网络节点中,负责消耗资源的电脑,叫做客户端,负责提供网络资源的电脑,叫做服务器。

http模块是Node.js官方提供的、用来创建web服务器的模块,通过http模块提供的http.createSever()方法,就能把一台普通的电脑,变成web服务器,从而对外提供web资源服务。

如果想使用http模块创建web服务器,则需要先导入它:

    const http = require('http');

在Node.js中,我们不需要Apache等这些第三方web服务器软件,因为我们可以基于Node.js提供的http模块,通过几行简单的代码,就能轻松的手写一个服务器软件,从而对外部提供web服务。

🤔思考:服务相关的概念?

ip地址

ip地址就是互联网上每台计算机的唯一地址,因此ip地址具有唯一性,如果把个人电脑,比做一台电话,那么ip地址就相当于电话号码,只有对方在知道ip地址的情况下,才能与对应的电脑之间进行数据痛信。

ip地址的格式:通常用点分十进制表示成(a.b.c.d)的形式,其中a,b,c,d都是0-255之间的十进制整数,例如:127.0.0.1

域名和域名服务器

尽管ip地址能够唯一的标识网络上的计算机,但是ip地址是一串长数字,不直观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓得域名地址。

ip地址和域名是一一对应的关系,这份对应关系存在一种叫做域名服务器的电脑中,使用者只需通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。因此,域名服务器就是提供ip地址和域名之间的转换服务的服务器。

注意:

  • 单纯使用ip地址,互联网中的电脑也能正常工作,有了域名的加持,能让互联网的世界变得更加方便。
  • 在开发测试期间,127.0.0.1对应的域名是localhost,它们都代表我们自己的电脑,在使用上没有任何区别

端口号

计算机中的端口号,就好像生活中的门牌号一样,通过门牌号,外卖小哥可以在整栋大楼众多的房间中,准确把外卖送到你的手中。

同理,在一台电脑中,可以运行成百上千个web服务,每个web服务都对应一个端口号,客户端发送过来的网络请求,通过端口号,可以被准确地交给对应web服务进行处理。

注意:

  • 一个端口号只能对应一个web服务
  • 在实际应用中,url中的80端口号可以被省略

🤔思考:服务相关的概念?

创建web服务器的基本步骤

  • 导入http模块
  • 创建web服务器实例 调用http.createServer()方法
  • 为实例绑定request事件,监听客户端的请求
  • 启动服务器
    // http模块
    const http = require('http');

    // 服务器实例对象
    const server = http.createServer();

    // 使用on()方法,为服务器绑定一个request 事件
    server.on('request', (req,res) => {
        // 只要有客户端请求。就会触发 request 事件,从而调用这个事件的处理函数
        console.log('someone visit our server');
    })

    // 调用listen()方法 
    server.listen('80', _ => {
        console.log('server is running');
    })

🤔思考:req请求对象?

只要服务器接收到了客户端的请求,就会调用通过server.on()为服务器绑定的request事件处理函数。 如果想在事件处理函数中,访问客户端相关的数据或者属性,可以使用如下方式:

    server.on('request', (req,res) => {
    // 只要有客户端请求。就会触发 request 事件,从而调用这个事件的处理函数
    console.log('someone visit our server');
    // req是请求对象,它包含了与客户端想逛的数据和属性:
    // req.url 是客户端请求的url地址 端口号后面的就是请求地址
    // req.method 是客户端的 method请求类型
})

🤔思考:res响应对象?

在服务器的request事件处理函数中,如果想访问与服务器相关的属性和数据,可以使用如下方式:

    server.on('request', (req,res) => {
    // 只要有客户端请求。就会触发 request 事件,从而调用这个事件的处理函数
    console.log('someone visit our server');
    // res是响应对象,它包含了与服务器相关的属性和方法
    // res.end() 方法的作用
    // 想客户端发送指定的内容,并结束这次请求的处理过程
    res.end('hi')
})

🤔思考:解决中文乱码问题?

    // 防止中文乱码问题,需要设置响应头 Content-Type 的值为 text/html; charset=utf-8
    res.setHeader('Content-Type', 'text/html; charset=utf-8')
    res.end('你好')

🤔思考:根据不通的url响应不同的html?

核心步骤:

  • 获取请求的url地址
  • 设置默认的响应内容为 404 Not Found
  • 判断用户请求是否为 / 或者 /index.html首页
  • 判断用户请求是否为 /about.html
  • 设置Content-Type响应头,防止中文乱码
  • 使用res.end()结束响应内容
    const http = require('http')

    const server = http.createServer();

    server.on('request', (req,res) => {
    const url = req.url;
    let content = '404 Not found';
    if (url === '/' || url === '/index.html'){
        content = '<h1>首页</h1>';
    } else if (url === '/about.html'){
        content = '<h1>关于</h1>';
    }
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    res.end(content);
    })

    server.listen('90', ()=>{
        console.log('server is running');
    })

返回一个html文件

    const path = require('path');
    const fs = require('fs');
    const http = require('http');

    const server = http.createServer();
    server.on('request', (req, res) => {
        const url = req.url;
        let fpath = '';
        if (url === '/') {
            fpath = path.join(__dirname, './page/index.html');
        } else {
            fpath = path.join(__dirname, '/page', url);
        }
        fs.readFile(fpath, 'utf8', (err, dataStr) => {
            if (err) return res.end('404');
            res.end(dataStr);
        })

    })

    server.listen(90, ()=>{
        console.log('server is running');
    })

05、模块化

  • 模块化的好处
  • 能够知道CommonJs规定了那些内容
  • 能够说出Node.js中三大分类各自是什么
  • 能够使用npm管理包
  • 能够了解什么是规范的包结构
  • 能够了解模块的加载机制

🤔思考:什么是模块化?

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分为若干模块的过程。对整个系统来说,模块是可组合、分解和更换的单元。

编程领域中的模块化,就是遵守固定的规则,把一个大文件拆成独立并相互依赖的多个小模块。

把代码进行模块化的好处:

  • 提高了代码的复用性
  • 提高了代码的可维护性
  • 可以实现按需加载

模块化规范:

模块化规范就是对代码进行模块化的拆分与组合时,需要遵守哪些规则:

  • 使用什么语法格式引入其他模块
  • 在模块中使用什么样的语法格式向外暴露成员

模块化规范的的好处:

大家都遵守同样的模块化规范写代码,降低了沟通成本,极大方便了各个模块之间的相互调用,约定大于配置,利人利己。

🤔思考:node.js中的模块分类?

Node.js 中根据模块的来源不同,将模块分为了3大类,分别是:

  • 内置模块(如fs模块、http模块等)
  • 自定义模块(用户创建的每个js模块,都是自定义模块)
  • 第三方模块(由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载)

🤔思考:加载模块?

使用强大的require()方法,可以加载需要的内置模块,用户自定义模块,第三方模块进行使用

🤔思考:模块作用域?

和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域。

🤔思考:模块作用域的好处?

防止全局变量污染的问题。

在一个自定义模块中,默认情况下,module.exports = {},在外界使用require导入一个自定义模块的时候,得到的成员就是那个模块中,通过module.exports指向的那个对象。

exports对象

由于module.exports单词写起来比较复杂,为了简化向外共享成员的代码,Node提供了exports对象,默认情况下,module.exports和exports指向同一个对象,最终共享的结果还是以module.exports指向的对象为准。

注意

时刻谨记,require()模块时,得到的永远都是module.exports指向的对象

为了防止混乱,建议大家不要在一个模块里同时使用module.exports和exports

🤔思考:Node.js中的模块化规范?

Node.js遵循了CommonJs模块化规范,CommonJs规定了模块的特性和各模块之间如何相互依赖。

CommonJs规定

  • 每个模块内部,module变量代表当前模块
  • module变量是一个对象,它的exports属性(module.exports)是对外的接口
  • 加载某个模块,其实是加载的module.exports

06、NPM与包

🤔思考:什么是包?

Node.js中的第三方模块又叫做包

🤔思考:包的来源?

不同于内置模块和自定义模块,包是由第三方个人或者团队开发的,免费供所有人使用

🤔思考:为什么需要包?

由于Node.js只提供了一些底层的API,导致基于内置模块进行项目开发时,效率很低。包是基于内置模块封装出来的,提供了更高级、更方便的API,提高了开发效率。

🤔思考:初次装包多了哪些文件?

初次装完包后,在项目文件夹下多一个叫做node_modules的文件夹和package-lock.json的配置文件。

其中:

node_modules文件夹用来存放所有已安装到项目中的包。require()导入第三方包时,就是从这个目录中查找并安装包。

package-lock.json 配置文件用来记录node_modules目录下的每一个包的下载信息,例如包的名字,版本号和下载地址等。

注意:程序猿千万不要手动修改node_modules或者package-lock.json文件中的任何代码,npm包管理工具会自动维护它们。

🤔思考:安装指定版本的包?

npm i lodash@4.17.21

🤔思考:包的语义化版本?

包的版本号是以“点分十进制”形式定义的,总共有3位数字,例如2.24.4

其中每一位数字含义如下:

第一位数字:大版本

第二位数字:功能版本

第三位数字:bug修复版本

🤔思考:开发属于自己的包?

初始化包的基本结构

需要新建一个文件夹作为包的根目录,其中包含:

  • index.js 入口文件
  • package.json 包管理配置
  • README.md 说明文档
    {
        "name": "zy-tools",  // 包的名称
        "version": "1.0.0",  // 版本号
        "main": "index.js",  // 入口
        "description": "提供了格式化时间、htmlEscape相关的功能",  // 检索描述
        "keywords": ["dataFormat","escape"], // 关键词
        "license": "ISC" // 开源许可协议
    }

🤔思考:登录npm && 发包?

  • nrm list
  • nrm use npm
  • 切换npm官网服务不要用镜像服务
  • 终端输入npm login
  • 发包:npm publish
  • 删包:npm unpublish 包名 --force

07、Express

🤔思考:什么是Express?

官方:Express是基于node.js平台,快速、开放、极简的web开发框架 通俗理解:Express的作用和node.js内置的http模块类似,是专门用来创建web服务器的。 本质:一个npm上的第三方的包,提供了快速创建web服务器的便捷方法

🤔思考:常见的服务器?

  • web网站服务器:专门对外提供web网页资源的服务器
  • API接口服务器:专门对外提供api接口的服务器

🤔思考:express的使用?

   // 导入express
    const express = require('express');
    // 创建web服务器 express()返回值是一个实例
    const app = express();
    // 启动web服务器
    app.listen(80, _=>{
        console.log('server is running!');
    });

    // 监听客户端的get请求,并向客户端响应具体的内容
    app.get('/user', (req,res)=>{
        res.send({name:'zy',age:18});
    });

    // 监听客户端的post请求,并向客户端响应具体的内容
    app.post('/user', (req,res)=>{
        res.send('请求成功');
    });

    // 获取url中携带的查询参数
    // 通过req.query对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数
    app.get('/', (req,res)=>{
        // req.query 默认是一个对象
        // 客户端使用?name=zy&age=20 这种字符串形式,发送到服务器的参数
        // 可以通过req.query 对象访问到,例如:
        // req.query.age req.query.name
        console.log(req.query); // { age: '10', name: 'zy' }
    });

    // express.static()
    // express提供了一个非常好用的函数,叫做express.static(),通过它,我们可以非常便捷地        创建一个静态资源服务器,
    // 例如:通过下面代码就可以将public目录下的图片、css文件、js文件对外开放访问了
    // app.use(express.static('public'));
    // 注意:express在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文        件的目录不会出现在url中。

    // 挂载路径前缀
    // app.use('/public', express.static('public'));


    // 托管多个静态资源目录,多次调用express.static()函数
    // 访问静态资源时,express.static()函数会根据目录的添加顺序查找所需的文件
    app.use(express.static('./static'));

08、Express中的路由

🤔思考:什么是路由?

广义:路由就是映射关系

🤔思考:Express中的路由?

在Express中,路由是指客户端的请求与服务器处理函数之间的映射关系。

Express中的路由由三部分组成:请求类型、请求的url地址,处理函数。格式如下:

    app.METHOD(PATH,HANDLER)

🤔思考:模块化路由?

为了方便对路由进行模块化管理,Express不建议将路由直接挂在app上,而是推荐将路由抽离为单独的模块。

步骤如下:

  • 创建路由模块对应的.js文件
  • 调用express.Router()函数创建路由对象
  • 向路由对象上挂载具体的路由
  • 使用module.exports向外共享路由对象
  • 使用app.use()函数注册路由模块
   // 导入express
    const express = require('express');

    // 创建路由对象
    const router = express.Router();

    // 挂载具体的路由
    router.get('/user/list', (req,res)=>{
        res.send('success');
    });

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

    // 向外导出路由
    module.exports = router;

09、Express中间件

🤔思考:什么是中间件?

中间件:特指业务流程的中间处理环节

现实生活中的例子:

在处理污水的时候,一般要经过3个处理环节,从而保证处理过后的废水,达到排放指标。

🤔思考:中间件的调用流程?

当一个请求到达Express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理。

🤔思考:next函数的作用?

next函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。

    // 定义一个简单的中间件函数
    const mv = (req,res,next)=>{
    console.log('这是一个简单的中间函数');
    // 把流转关系,转交给下一个中间件或路由
    next();
    }

🤔思考:全局生效的中间件?

客户端发起的任何请求,到达服务器之后,都会触发中间件,叫做全局生效的中间件。通过调用app.use(中间件函数),即可定义一个全局生效的中间件

    app.use(mv)

🤔思考:中间件在实际开发中的作用?

多个中间件之间,共享一份req和res,基于这样的特性,我们可以在上游的中间件中,统一为req和res对象添加自定义属性和方法,供下游的中间件或者路由进行使用。简化代码的书写。

    // 中间件的作用

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

    // 全局生效的中间件简化写法
    app.use((req,res,next)=>{
            // 获取到请求到达服务器的时间
            const time = new Date();
            // 为req对象,挂载自定义属性,从而把时间共享给后面的所有路由
            req.startTime = time;
            next();
    });

    app.get('/',(req,res)=>{
        // const time = new Date();
        res.send('home page' + req.startTime)
    })

    app.get('/user',(req,res)=>{
        // const time = new Date();
        res.send('user page'+ req.startTime)
    })


    app.listen(80,_=>{
        console.log('server is running');
    })

🤔思考:定义多个全局中间件?

可以使用app.use()连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用。

    // 定义多个全局中间件
    const express = require('express');
    const app = express();

    // 定义第一个全局中间件
    app.use((req,res,next)=>{
        console.log('调用了第一个');
        next();
    });

    // 定义第二个全局中间件
    app.use((req,res,next)=>{
        console.log('调用了第二个');
        next();
    });

    app.get('/',(req,res)=>{
        res.send('home page')
    })

    app.get('/user',(req,res)=>{
        res.send('user page')
    })


    app.listen(80,_=>{
        console.log('server is running');
    })

🤔思考:局部生效的中间件?

不使用app.use()定义的中间件,叫做局部生效的中间件

    // 局部生效的中间件
    const express = require('express');
    const app = express();

    const mv1 = (req,res,next)=>{
        console.log('这是一个局部的中间函数1');
        next();
    }
    
    const mv2 = (req,res,next)=>{
        console.log('这是一个局部的中间函数2');
        next();
    }

    // 中间件只在这个函数里面生效
    app.get('/', mv1, mv2, (req,res)=>{
        res.send('home page')
    })

    app.get('/user',(req,res)=>{
        res.send('user page')
    })


    app.listen(80,_=>{
        console.log('server is running');
    })

🤔思考:使用中间件的注意事项?

  • 一定要在路由之前定义中间件
  • 客户端发送来的请求,可以连续调用多个中间件进行处理
  • 执行完中间件的业务代码之后,不要忘记调用next()函数
  • 为了防止代码逻辑混乱,调用next()之后不要再写额外的代码
  • 连续调用多个中间件时,多个中间件之间,共享req和res对象

🤔思考:中间件的分类?

  • 应用级别的中间件:通过app.use()或app.get()、app.post(),绑定到app实例上的。
  • 路由级别的中间件:绑定在express.Router()实例上的中间件
  • 错误级别的中间件:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的情况。格式(err,req,res,next)
    const express = require('express');
    const app = express();


    app.get('/', (req,res)=>{
        throw new Error('服务器异常')
        res.send('home page')
    })

    // 错误级别中间件要写在路由下面进行捕获
    app.use((err,req,res,next)=>{
        console.log('发生了错误'+ err.message);
        res.send(err.message)
    });

    app.listen(80,_=>{
        console.log('server is running');
})
  • 内置的中间件:Express4.16.0版本后,内置了3个常用的中间件,极大的提高了Express项目的开发效率和体验: 1、express.static 快速托管静态资源的内置中间件。

2、express.json 解析JSON格式的请求数据。

   // 内置中间件
    const express = require('express');
    const app = express();

    // 解析客户端的json
    app.use(express.json());

    app.post('/', (req,res)=>{
        // 默认情况下,如果不配置解析表单的中间req.bodyundefined
        console.log(req.body);
        res.send('home page')
    })

    app.listen(80,_=>{
        console.log('server is running');
    })

3、express.urlencoded 解析URL-encoded格式的请求数据。

  • 第三方的中间件:如body-parser

10、Express编写接口

    // 接口编写
    const express = require('express');
    const app = express();
    const router = require('./10');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    
    // jsonp要在cors前进行注册
    app.get('/api/jsonp', (req,res)=>{
        // 得到函数的名称
        const funcName = req.query.callback
        // 定义要发送给客户端的数据对象
        const data = {name:'zs',age:18}
        // 拼接出一个函数的调用
        const scriptStr = `${funcName}(${JSON.stringify(data)})`
        // 把拼接的字符串返回给客户端
        res.send(scriptStr)
    })
    app.use(cors);
    app.use(bodyParser.urlencoded({extended:false}));
    app.use('/api',router)


    app.listen(80,_=>{
        console.log('server is running');
    })
    
    
    // api 路由模块
    const express = require('express');
    const apiRouter = express.Router();

    apiRouter.get('/',(req,res)=>{
        // 获取客户端参数
        const query = req.query;
        res.send({
            status: 0,
            msg: '成功',
            data: query
        });
    })

    apiRouter.post('/add',(req,res)=>{
        // 获取客户端通过请求体,发送到服务器的url-encoded 数据
        const body = req.body;
        res.send({
            status: 0,
            msg: '成功',
            data: body
        });
    })

    module.exports = apiRouter;

🤔思考:解决跨域?

  • 使用cors中间件为主流方案
  • JSONP 只能get请求

🤔思考:什么是cors?

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

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

🤔思考:cors的注意事项?

  • cors主要在服务端进行配置,客户端无须做任何配置
  • cors在浏览器中有兼容性问题,ie10+

🤔思考:jsonp接口?

  • 获取客户端发送过来的回调函数的名字
  • 得到要通过JSONP形式发送给客户端的数据
  • 拼出一个函数调用的字符串
  • 响应给客户端的

11、Web开发模式

1、服务端渲染:服务器发送给客户端的HTML页面,服务端通过字符串的拼接,动态生成的。因此不需要Ajax这样的技术进行请求。

服务端渲染优点:

  • 前端耗时少
  • 有利于seo

服务端渲染缺点:

  • 占用大量的服务端资源
  • 不利于前后端分离,开发效率低

2、前后端分离:依赖于Ajax这样的技术的广泛应用。后端只需要提供接口,前端进行接口的请求。

前后端分离优点:

  • 开发体验好
  • 用户体验好
  • 减轻服务端渲染压力

前后端分离缺点:

  • 不利于seo 可通过ssr进行解决

3、如何选择开发模式

比如企业级网站,主要是展示、交互少、需要良好的seo,那就采用服务渲染

管理后台交互多的,不考虑seo,可以使用前后端分离的模式

首屏服务端渲染、其它页面前后端分离

12、身份认证

通过一定的手段对用户进行身份的验证

  • 服务端渲染使用Session认证机制
  • 前后端分离使用JWT认证机制

13、Session

HTTP协议的无状态性

客户端每次HTTP请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次HTTP请求的状态。

🤔思考:cookie?

cookie是存储在用户浏览器中的一段不超过4kB的字符串,它是由name,value和其他几个用于控制Cookie有效期、安全性、使用范围的可选属性。

不同域名下的cookie各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的cookie一同发送给服务器。

cookie的特性:

  • 自动发送
  • 域名独立
  • 过期时限
  • 4KB限制

cookie在身份认证中的作用:

客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的cookie,客户端会将cookie存在浏览器中。

随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的cookie,通过请求头的形式发送给服务器,服务器即可验证客户端的身份。

image.png

cookie不具有安全性:

由于cookie是存储在浏览器中的,而且浏览器提供了读写cookie的API,因此很容易被伪造,不建议将重要的隐私数据,通过cookie发送给服务器。

image.png

session的工作原理

image.png

session的局限性

session认证需要配合cookie才能实现,由于cookie默认不支持跨域, 当跨域请求的时候,需要很多的额外配置。非跨域是推荐使用