nodeJS

注意事项

  • Web API 的大多数都不可用,比如windowdocument

    • 通用的只有==console==、==定时器==
  • 虽然没有window,但是有等同于windowglobal对象

    • ES 新特新中也可以使用globalThis,这两是相等的
  • 想要使用==ES6==模块语法,需要在package.json里添加"type":"module"配置项

    • 但是这样就无法使用require(path)方法,会报错
    • 并且没有__dirname全局变量了,需要根据控制台==当前==目录,使用import.meta.url作为替代
    • 通过"type"切换ES6和commonJS只能==二选一==,node ./文件时只能都用ES6或者commonJS
      • 通过==改后缀名==方式可以灵活切换
        • .mjs支持ES6语法
        • .cjs支持commonJS语法
  • 配置文件package.json中的"scripts"配置的运行指令是以package.json文件为根目录

    • 所以如果是多项目共用package.json,运行指令的==路径==一定是./项目/文件夹/文件的形式
  • 流程

    express开启服务、设置路由规则。决定如何返回静/动态资源、如何处理请求

    Mongodb/MySQL开启服务、在服务器负责存储数据

    Mongoose等工具包封装了操作数据库的命令、用代码形式便捷的操作数据库数据

    在Mongoose等工具包方法的回调中用express以接口需要的形式给页面返回数据

Buffer 缓冲器

  • 类似于==数组==的对象,是用于处理==二进制数据==的内存空间

  • ==大小固定==

  • 可直接对计算机==内存==进行==操作==

  • 每个元素大小为==1byte==(字节),而一个元素由==8==个==bit==组成

    • 其实是 8 个==二进制==转换成 1 个==16 进制==显示
    • 转换字符/数组时,是将每个==字符==或数组中==每个元素==在==码表==中的十进制转换成 16 进制
      • 对于数组中的元素是==对象==或==字符串==的会表示为 00
  • 创建

    • Buffer.alloc(num):表示创建==num 字节==的 Buffer

      • 每一个二进制位都会归零
    • Buffer.allocUnsafe(num):使用不安全的方式创建

      • 因为会使用已经被==使用过==的内存空间,且不会清空旧数据,就有可能==存在旧数据==

      • 性能更快,因为不用清零

    • Buffer.from(params):将==数组==、==字符串==转换成 Buffer

  • b.toString():将 Buffer 按照==utf-8==编码方式转换为==字符串==

  • 读写

    • 通过数组下标的方式读取,内容为==十进制==表示方式
    • 通过数组下标的方式修改,只能写十进制
      • 注意,==最大==只能写 255,因为 8 个二进制数最大只能表示 255,多的会溢出,遵循==高位数字舍弃==原则

fs 模块

  • 不能使用import方式导入模块

  • ==相对路径==参照的是==命令行工作目录==,不是JS 所在目录

    • 要解决这个问题使用__dirname变量,它会保存 JS==文件夹==位置的==绝对路径==(不包含文件),如__dirname + './index.js'
      • __filename是==文件==的==绝对路径==
  • writeFile:文件写入

    • fs.writeFile('写入路径','写入内容',回调函数)
      • 写入路径下没有对应文件会新建
      • writeFile 方法写入的内容会==覆盖==源文件内容
      • 回调函数内形参是==失败==后的==对象==,==成功==则返回==nul==l
      • 还有第三个配置项fs.writeFile('写入路径','写入内容',{flag:'a'},回调函数),配置 flag 值为a表示==追加==内容==而不是覆盖==
      • 写入内容只能是==字符串==!如果收到的是二进制文件,是不能写入文件的!
    • writeFileSync:文件同步写入
      • ==没有回调函数==
      • ==同步==执行,等文件写入完后才会执行后续的代码
    • 只能在==已有路径目录==下创建文件!
  • appendFile:文件追加写入

    • fs.appendFile('路径','内容',回调函数)
    • ==不会覆盖==文件内容,而是==追加==内容
    • appendFileSync
    • 在需要换行的字符串前使用\r\n==换行==
    • 写入路径下没有对应文件会新建
  • createWriteStream:文件流式写入

    let ws = fs.createWriteStream('文件路径')
    ws.write('内容')
    ws.write('内容2')
    ws.close() 关闭通道 可选 因为脚本执行完毕资源也会被回收 通道会自动关闭
    
    • 类似于 websocket==推流==的方式,即当前执行脚本和文件之间==连接不断开==,有需要就写人,适合==高频次==写入或者==大文件==写入

    • 同样会==覆盖==源文件内容

  • readFile:文件读取

    • fs.readFile('文件路径',(err,data)=>{})
      • 有两个参数,第一个依然是错误参数,第二个是读取==成功==的内容
      • 读取到的值是==Buffer==,转换成可读字符串需要toString
    • readFileSync:文件同步读取
      • let data = fs.readFileSync('路径')
      • 因为是同步,所以没有回调函数,而是直接==返回值==,data依然是==Buffer==
  • createReadStream:文件流式读取

 let rs = fs.createReadStream('路径') 可读取视频等文件
 rs.on('data',chunk => { 必须写 data 绑定的是 data事件 不是 自定义事件名
     事件在 读取完一块 后执行回调
     chunk是读取到的 Buffer数据 不是什么数据都能用toString转 比如读取视频 就无法转换
 })
 rs.on('end',()=>{ 没有参数 同 createWriteStream 可以不绑定end事件 脚本执行完会自动释放
     console.log('读取完成')
 })
  • 将文件一块一块读取,读取效率高

  • ==文件复制==

    第一种方法——读写最快、占用内存最小,因为每次读取64KB就写入,不需要将整个文件缓存进内存
    const rs = fs.createReadStream('xxx')
    const ws = fs.createWriteStream('xxx2')
    rs.on('data',chunk=>{
        ws.write(chunk)
    })
    rs.on('end',()=>{
        console.log('写入完成')
    })
    第二种方法——整体读取、写入
    let data = fs.readFileSync('xxx')
    fs.writeFileSync('xxx2',data)
    第三种方法——最简单、用的少
    rs.pipe(ws)
    
  • rename:文件重命名(也可根据路径移动文件)

    • fs.rename('源文件路径','重命名路径',回调函数)
    • renameSync:文件同步重命名
    • 只能移动文件到==存在的目录下==,不能同时创建文件夹并移动
  • rm:文件删除

    • fs.rm('路径',err=>{})
    • 同样有rmSync
    • 要删除多层级目录和文件,需要配置项fs.rm('路径',{recursive:true},err=>{}),==recursive==,即可递归删除==多级目录和文件==
  • mkdir:创建文件夹

    • fs.mkdir('路径',err=>{})
    • 当创建路径中存在多个层级不存在时,即需要递归创建路径中有不存在的文件夹,需要第二个参数fs.mkdir('./a/b/c',{recursive:true},err=>{}),==recursive==,意为递归,设为==true==即可
  • readdir:读取文件夹内文件目录

    • fs.readdir('路径',(err,data)=>{})
    • 同样有同步方法readdirSync
  • stat:获取文件状态信息(比如:创建时间、大小等)

    • fs.stat('路径'(err,data)=>{})
      • data 是文件状态==对象==
      • data 方法data.isFile(),返回 true/false,表示是否是文件,是文件夹的话会返回 false
      • data 方法data.isDirectory(),返回 true/false,表示是否是文件夹

path 模块

  • 用于==拼接==、规范路径格式,resolve(多个参数),从第一个参数往后拼接

    • ==URL 对象==里的==pathname==只有/index.css单斜线,会省略.././,因此 path 方法处理时会==重新定位到根目录==
    const path = require('path')
    path.resolve(__dirname,'index.js') => D:\node\index.js
    path.resolve(__dirname,'./index.js') => D:\node\index.js
    path.resolve(__dirname,'index.js','index2.js') => D:\node\index.js\index2.js
    path.resolve(__dirname,'/index.js') => D:\index.js 注意此处是从根目录开始
    path.resolve(__dirname, '../xxx/index.js') => D:\xxx\index.js 返回上一级下的xxx目录
    
  • ==地址拼接==

    • __dirname + '/../xxx' + pathname当前==执行 js 的文件夹为根目录==,/../表示返回根目录上一级,下的xxx文件夹下的pathname路径
      • 不能用 path 方法把这个路径整合,会变成D:\xxx\pathname

http 模块

const http = require('http')
// createServer接收一个函数作为参数 所以可以使用express()返回的函数对象作为参数
const server = http.createServer((request, response)=>{ 回调函数 在 *每次*收到请求后都会调用
    第一个参数 request => 浏览器发来的 请求报文 对象,包括 请求行、请求头、请求体
    第二个参数 response => 服务器的 响应报文 对象,可以设置 返回结果
})

listen方法 监听端口、创建服务
server.listen(端口号,()=>{ 回调函数在 服务启动成功 后被调用
    console.log('服务已启动')
})
  • ==重启==服务==修改==才能==生效==

  • request:创建服务传入函数的第一个参数,用于==解析请求报文==

    • request.method:属性。获取请求方法,如 post、get

    • request.url:属性。获取==端口号后==的内容,如/api?name=xxx

      • 但是使用不便,一般不会用这种方法提取参数,而是使用url 模块,详情见后
    • request.on('data', chunk =>{解析}):读取收到的==请求体==数据。request 是==流数据==,所以要绑定==data 事件==一片一片取数据

      • 注意此处只能提取到==post==方式发送的参数,无法获取拼接到地址栏的参数,如?xxx1=123&xxx2=456

      • chunk是==Buffer==类型数据

      • 请求体数据toString()后得到的结果

        [图片上传失败...(image-760b3b-1717731286603)]

    • request.on('end', () => {}):读取完成事件。在回调里执行返回页面结果

  • response:创建服务传入函数的第二个参数,用于设置==返回结果==

    • response.write(内容):设置==响应体==,可以==多次==调用,内容会==拼接==在一起

      • 如果response.end也有内容,则也会拼接在一起
      • 如果不写response.end设置的响应内容==不会返回==给页面,必须要有end
    • response.end(内容):设置==响应体==,==断开通信==,但是中文显示在页面上会是乱码,需要设置==字符集==

      • response.end()只能存在一个,不然会报错

      • 不写 end,会导致请求一直是==待处理==状态,从而占用资源,以致==其他请求==无法返回结果

      • ※end 执行完不会终止,会继续向后执行

    • endwrite均可以接收==Buffer==类型的数据!因此可以将fs 模块读取的==html 文件==数据传入,从而根据==路径名==返回对应的页面

      • 当页面解析返回的 html 文件,其内<img><link>等引入了外部文件,就又会发起请求,并且是==异步==请求获取外部文件
      • 通过解析==URL 对象.pathname==,可以区分是带路由的请求还是加载==外部资源==文件,如,获取./index.css文件时发送的请求是http.../index.css,而路由的请求是http.../login?name=xxx
    • response.setHeader(键,值):设置==响应头==

      • setHeader('content-type','text/html;charset=utf-8'),告诉浏览器==返回内容==是 HTML,内容对应的==字符集==是 UTF-8
      • setHeader('test',[1, 2, 3]),第二个参数传入==数组==可以创建==多个同名==请求头
    • response.statusCode = 404设置==响应状态码==,但是并不影响请求返回结果

    • response.statusMessage = 'xxx':设置==响应状态描述==,比如==Not Found==就是状态描述

  • listen(端口号,回调函数):监听端口、创建服务

    • ==80==是==http 协议==默认端口号,==https 协议==默认端口号是==443==,默认端口号不会在地址栏显示
  • 发送请求调用其他接口

    const http = require('http')
    // 或者https 与http方式相同
    const https = require('https')
    const options = {
        host: 'www.example.com',
        port: 80,
        path: '/api/getdata',
        method: 'GET',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer xxx'
        }
    }
    const req = http.request(options, res => {
        console.log('响应结果')
        res.on('data', chunk => {})
        res.on('end', () => {})
    })
    req.on('error', err => {
        console.log('请求遇到问题')
    })
    // 发送post请求时 设置请求体
    req.write('{name:'xx', password:'111'}')
    req.end() // 发送请求
    

发送请求调用其他接口

  • 有的服务、接口之间互相有数据关联,这就需要发送http请求从其他接口获取数据

  • 使用

    • 安装:npm i axios

    • 引入

      const axios = require('axios')
      // 使用方式详见网络请求笔记
      axios({
          method:'post',
          url:'',
          data:{}
      }).then(res=>{})
      
      

url 模块(旧版)

  • 用于解析地址栏 url,一般用于提取、解析==query==参数

    const url = require('url') 引入url内置模块
    ...
    const server = http.createServer((request,response)=>{
        let res = url.parse(request.url,true) 解析请求行url
        let keyword = res.query.xxx
    })
    
  • url.parse(url字符串,true/false):解析 url 字符串,返回==对象==

    • 第一个参数是请求行的 url,可以是完整形式http:...,也可以是端口号后内容request.url
    • 第二个参数是==是否将 query 属性解析为对象==,可选 true/false
      • 解析后的结果{ name:'xxx',password:'123' }

util模块

  • 用于将回调函数风格的方法转变成Promise形式

  • 只能传入==错误优先==风格的回调函数,即fn(params1, params2, ..., (err,data)=>{})

  • 应用

    const fs = require('fs')
    const util = require('util')
    // 封装成新的函数方法
    let newFsRead = util.promisify(fs.readFile)
    newFsRead('./src/text.txt').then(data=>{})
    
  • 内部实现逻辑

    // 传入一个函数作为参数 返回一个函数
    function promisify(fn) {
        // 返回的函数可接收多个参数 params是数组
        return (...params) => {
            return new Promise((success, fail) => {
                // 将参数数组展开 补全最后一个回调函数参数
                fn(...params, (err, data) => {
                    if (err) fail(err)
                    success('ok')
                })
            })
        }
    }
    // 实验函数
    function testFn(params1, params2, callback1, callback2) {
        console.log(params1)
        console.log(params2)
        // 关键点 如果有多个回调函数作为参数 必须 携带最后一个回调函数作为参数传入
        callback1(params1 + params2, callback2)
    }
    // 加工旧方法 返回一个新的函数
    let newTestFn = promisify(testFn)
    // 新方法传参 只传最后一个回调函数之前的
    newTestFn(5, 2, (result, callback) => {
        let t = null
        if (result > 5) {
            t = 'error'
        }
        // 关键点 执行最后一个回调函数 并按错误在前 数据在后顺序传入参数
        callback(t, result)
    }).then(res => {
        console.log(res)
    }).catch( res => {
        console.log(res)
    })
    

URL API

  • 与 HTML 页面的 URL 通用
  • 与旧版的区别
    • ==无需引入模块==
    • 默认将 url 携带参数整合成对象
    • 必须传入完整 url 地址,包括域名、协议、端口,或者在第二个参数中补全域名协议端口号
      • let url = new URL(request.url,'http://127.0.0.1')
      • 或者let url = new URL('http://127.0.0.1' + request.url)
      • 可以==省略端口号==
    • ==query 参数==不是对象,需要用==get 方法==获取里面的内容
      • url.searchParams.get('name')
  • URL 对象里的pathname属性会省略相对路径前的./../
  • url.hostname:IP 域名。不包括 http 协议及端口号

mime 资源类型

  • 结构[type]/[subType]

    • text/htmltext/cssimage/jpegapplication/json
    • 在响应头Content-Type表明类型,浏览器会根据类型决定如何处理资源
    • 对于未知类型,设置application/octet-stream类型,浏览器遇到该类型,会对响应体进行独立存储,即==下载==
  • ==获取对应类型==

    let mimes = {
      html: "text/html",
      css: "text/css",
      js: "text/javascript",
      png: "image/png",
      jpg: "image/jpeg",
      gif: "image/gif",
      mp4: "video/mp4",
      mp3: "audio/mpeg",
      json: "application/json",
    };
    http.createServer((request, respone) => {
      let { pathname } = new URL(request.url, "http://127.0.0.1");
      let file_path = __dirname + pathname;
      fs.readFile(file_path, (err, data) => {
          if (err) {
              response.end();
              return;
          }
          let t = pathname.split(".")[1];
          获取对应类型;
          let type = mimes[t];
          if (type) {
              response.setHeader("content-type", type);
          } else {
              没匹配到进行下载;
              response.setHeader("content-type", "application/octet-stream");
          }
          response.end(data);
      });
    });
    

模块化

  • ==暴露==数据的方式

    • module.exports是==固定==写法

    • exportsmodule.exports有隐性的关系,exports = module.exports = {},所以exports.name = xxx暴露出来的是{name:xxx},且不能用export = xxx的方式去暴露

      第一种方式——直接赋值变量
      function fn(){
          console.log('hhhh')
      }
      module.exports = fn
      
      第二种方式——用对象整体暴露
      function fn(){}
      let a = '123'
      module.exports = {fn,a} 简写形式 等同于 {fn:fn,a:a}
      
      第三种方式——exports.name
      function fn(){}
      let a = '455'
      exports.fn = fn 等同于 module.exports={fn,a}
      exports.a = a
      
  • ==导入==模块的方式

    • require返回的是module.exports的==值==

    • require本身就会运行一遍文件

      // 如果需要的是文件执行后得到的对象
      // index.js文件
      let dataType = new mg.Schema({...});
      let model = mg.model('test2', dataType);
      module.exports = model;
      // index2.js文件
      const model = require('./index.js')
      // 在某事件回调中
      model.create(...).then(...)
      
      // 如果需要的是函数,选择时机执行
      // index.js文件
      module.exports = function (success, error) {
          mg.connect(...);
          mg.connection.once('open', () => {
              console.log('连接成功');
              success();
          });
          mg.connection.on('error', () => {
              if (typeof error !== 'function') {
                  error = () => {
                      console.log('连接失败');
                  };
              }
              error();
          });
          mg.connection.on('close', () => {
              console.log('连接关闭');
          });
      };
      // index2.js文件
      const connect = require('./index.js')
      connect(()=>{...})
      
      
    • 对于自己创建的模块,不能省略==相对路径==

      • jsjson文件可以省略后缀
      • 当导入没写后缀的模块时,会==优先搜索js==后缀的文件,如:同一目录下有module.jsmodule文件夹,导入require('./module')会优先导入module.js
    • 导入==文件夹==时,会搜索文件夹下package.json里的属性"main":"./xxx.js"所对应的文件

      • 如果==main 属性==不存在,则会尝试导入index.jsindex.json
    • 导入==内置==模块,无需路径或相对路径,直接写模块名称

      第一种方式——导入文件
      const f = require('模块文件路径')
      f()
      
      第二种方式——导入文件夹
      const f = require('./文件夹')
      f()
      
      第三种方式——导入内置模块
      const fs = require('fs')
      
    • ==require==原理

      • require 实际在内部执行了一次fs 模块文件读取,再转字符串
      伪代码
      function require(file_path){
          拼接绝对路径
          let absolute_path = path.resolve(__dirname,file_path)
          检测缓存
          if(catchs[absolute_path]){
              return catchs[absolute_path]
          }
          如果没有缓存 读取文件代码 从Buffer转换到字符串
          let code = fs.readFileSync(absolute_path).toString()
          自执行读取的代码
          let module = {}
          let exports = module.exports = {}
          (function (module, exports, __dirname, __filename, require){
              code 内部对传入的 module.exports进行了赋值
          })(module, exports, __dirname, __filename, require)
          将module.exports的 值 写入缓存
          return catchs[absolute_path] = module.exports
      }
      
    • require只能导入==JSON==和==JS==文件

      • 导入 txt 等格式文件会以 JS 格式去运行
      • 导入==JSON==文件,返回的就是 JSON 文件保存的对象,可以直接进行取值操作
      • 导入==JS==文件,会先运行一遍,再返回module.exports的值。如果 JS 文件没有给module.exports赋值,则返回{}

npm

  • ==初始化==

    • 以一个文件夹目录启动命令行,执行npm init
    • 本质是将当前文件夹==初始化==为,在目标文件夹下生成package.json文件
    • 每个都必须要有package.json
    • 名默认是==文件夹名==,不能是==中文==!所以文件夹最好不要用中文
    • 意义:创建==项目==必须的操作,因为直接 npm 安装,package.json里只有dependencies属性
  • 搜索工具包

  • ==导入==工具包

    • require函数:const xx = require('包名')
    • 导入的包会作为自己所创建的包的依赖包
    • 导入包的流程
      • 会在==当前==文件夹下的node_modules寻找同名==文件夹==
      • 没找到则,往==上级目录==下的node_modules中寻找,直到磁盘==根目录==
  • 生产环境和开发环境

    • 开发依赖:只在==开发阶段==使用的依赖包
      • 命令:npm i 包名 -D
        • D必须大写!
      • 包信息存在package.json中==devDependencies==属性
    • 生产依赖:==开发阶段==和最终==上线==都用得到的依赖包
      • 命令:npm i 包名 -S
        • S必须大写!
      • -S是默认选项
      • 包信息存在package.json中==dependencies==属性
    • 普通(==局部==)安装
      • 命令:npm i 包名
      • 会一层层向上级目录查找node_modules文件夹,因此不必在根目录下执行安装命令,在子文件夹下安装即可
      • 执行安装的工具包
        • 第一种方法:path\node_modules\.bin\指令 -v
        • 第二种方法:
          • 配置 package.json 文件中script属性别名,"name":"nodemon -v"
          • 再执行npm run name
    • ==全局==安装
      • 命令:npm i -g 包名
      • 任何工作目录下均可执行安装
      • 无需require函数,而是通过命令行指令去调用,如:nodemon,安装完后自动运行,需要调用指令时nodemon xxx去执行
      • 查看全局安装位置:npm root -g
      • 执行安装的工具包:指令 -v
    • 切换安装版本:npm i 包名@版本号
    • 删除依赖
      • 局部:npm remove 包名npm r 包名
      • 全局:npm remove -g 包名
    • 配置别名
      • package.json文件中有个scripts属性,在其中添加==自定义名==,如"server": "node 执行路径及额外参数",即可使用npm run server运行,省去了冗余的命令参数
      • 也可以在script属性中配置start: "node 执行路径",同样可以使用npm run start运行,不过 start 可以省略 runnpm start
      • npm run会自动向上级目录查找package.json
    • NodeJs==版本管理==软件:nvm
      • 显示可下载Node.js版本:nvm list available
      • 已安装版本:nvm list
      • 安装某一指定版本:nvm install 16.11.12
      • 安装最新版本:nvm install latest
      • ==删除==某版本:nvm uninstall 16.11.12
      • ==切换==版本:nvm use 16.11.12
  • npx

    • node_modules下的.bin==临时==添加为环境变量,即==无需引入直接在命令行中使用==
    • npx webpack ./main.js --mode=development
  • Tips

    • ==nodemon==工具的使用,用nodemon ./xx.js代替node ./xx.js
    • 开发环境中的node_modules文件夹,是不会上传服务器的,主要是为了去除冗余的安装包文件。所以协同开发时,都是下载package.jsonpackage-lock.json文件,根据里面的dependence属性重新在本地安装依赖包,而npm i命令可以快捷安装 json 里所有依赖

express 框架

  • 也是一个==工具包==,封装了多个功能,便于开发 HTTP 服务

    • 导入的工具包是函数,使用前需创建==函数对象==,const app = express()app其实是一个函数,所以可以传入http或https.createServer(..., app)
  • 与 node 原生 http 模块区别

    // http模块
    const http = require("http");
    const server = http.createServer((req, res) => {
      let url = new URL(req.url, "http://127.0.0.1");
      switch (url.pathname) {
          case "/":
              res.end(页面);
              break;
          case "/login":
              req.on("data", (chunk) => {
                  解析;
              });
              req.on("end", () => {
                  res.end("数据");
              });
              break;
      }
    });
    server.listen(30, () => {
      console.log("服务已启动");
    });
    
    // express
    const express = require("express");
    const app = express();
    app.use(express.json());
    app.use(express.urlencoded({ extended: false }));
    app.get("/", (req, res) => {
      res.send(页面);
    });
    app.post("/login", (req, res) => {
      res.send("收到数据" + req.body + "对象");
    });
    
    // ※http模块也可以使用express创建的应用对象
    const server = http.createServer(app)
    

express==路由==

  • 替代了 http 模块开启的服务里,对pathname进行的复杂分类跳转。例:app.get('/home',(req,res)=>{...}),表示:接收请求类型为 get、且pathname为/home 的请求

  • 格式:app.<method>(path,callback)

    • <method>

      • postget:表示匹配 post、get 请求
      • all:表示匹配==任意==请求类型
    • path

      • 形如'/home'形式
      • '*':表示匹配==任意路径==
    • callback

      • 回调函数,有两个参数(request,result)=>{...}request表示请求报文对象,result表示响应报文对象
  • Tips

    • 一般登陆首页是没写路径的,因此服务端收到的路径是/,所以要监听app.get('/',callback),跳转到主页
    • 匹配是按照代码中从上至下的书写顺序,如app.all('*',callback)写在前,那之后所有的匹配规则都不会触发,全被all('*')接收,所以要注意匹配的优先顺序

获取==请求报文==参数

  • 兼容==http 模块==的属性方法
  • express 操作
    • req.path:等同于new URL里的pathname属性,会省略==query 参数==,只返回路径
    • req.query:返回对象。以键值对的形式保存 query 参数
    • req.ip:获取 ip
    • req.get('key'):获取==请求头==。
      • 只能通过 get 获取,因为请求头存在==Symbol==值为键名的对象里

获取==路由==参数

  • 在==路径==中使用占位符来匹配获取对应参数值

    • :命名:占位符格式

    • 除了占位符以外的其他字符会做==格式==匹配,不匹配的字符串集合到最后一个占位符

      页面请求: //www.jd.com/abc.7.8.html
      
      http: 匹配示例;
      app.get("/:a.html", callback); //得到{a:'abc.7.8'}
      app.get("/:a.:b"); //得到{a:'abc', b:'7.8.html'}
      app.get("/:a.:b.html"); //得到{a:'abc', b:'7.8'}
      
  • request.params属性集合了所有占位符存储的==路由参数==,没有占位符,params返回值为空

==响应==设置

  • 兼容 http 模块方法
  • express 响应设置可以==链式==调用。例:res.status(500).set('aaa','123').send(xxx)
  • res.status(500):设置==状态码==。等同于 http 模块的res.status = 500
  • res.set('aaa','123'):设置==响应头==。等于 http 模块res.setHeader
  • res.send('中文xxx'):设置==响应体==。等同于 http 模块res.end,会==覆盖页面==
    • 但是send 方法会自动添加text/html;charset=utf-8响应头,因此不需要设置响应头也可正常显示中文
    • res.end一样,只能有一个
    • send后不能写res.download等方法
    • 同 http 模块的res.end,会继续执行后续代码
  • res.redirect(url,状态码):==重定向==
    • 将==当前请求==转向新的 url,并不会修改当前页面
    • url可以是./path/path,也可以是完整的 urlhttp...
    • 只能有一个redirect,同res.end一样
    • 会继续执行后续代码
  • res.download(path,重命名,失败回调):==下载==响应。后面两个参数可以省略
    • path只能是本地/服务器资源路径,==不能是 url==!
    • res.send放在一起执行会==失效==,因为res.download执行完会断开请求连接
    • 只有新开页签发送的==get 请求才会触发下载==,如果是页面内元素、事件触发则不会自动下载
      • 方式1:前端页面header中携带密匙,后端返回一个新的url,前端接收到后在新页签中打开访问,后端对访问该url的请求使用res.download响应下载
      • 方式2:前端页面在新页签中将token放在地址栏发送请求,后端对该请求使用res.download响应下载
  • res.json(任何类型数据)
    • ==接口==响应,返回 JSON 数据,不覆盖原页面,由==发起请求==的回调接收响应结果
    • 同样执行完会断开连接
    • 会自动设置==响应头==类型为application/json
    • 如果传读取的文件内容,依然是 Buffer 类型数据,即使toString转换成字符串,也会有\n等字符,因此不适合传文件
    • 会继续执行后续代码
  • res.sendFile(绝对路径):响应==文件==内容
    • 只能是绝对路径!因此需要用path.resolve或者__dirname + '/path'
    • 响应的文件内容是==原格式==,如果在地址栏发送请求,响应 HTML 文件会==直接运行==!
    • 响应的文件视浏览器是否支持在线预览,不同的浏览器可能会直接打开或者跳转下载
      • 因为浏览器默认自动解析添加Content-Disposition:video/mp4等响应头,识别不了的会添加Content-Disposition:attachment,从而进行下载
      • 如果手动添加响应头res.set('Content-Disposition','attachment'),则所有响应文件均会下载
      • 哪怕是res.send('字符串')也会当做文件下载
  • Tips
    • 当访问http(s)://ip:port
      • 一种是可以res.redirect('./index.html')==重定向==,让app.use(express.static(__diranme))静态资源获取服务器上的首页
      • 另一种是res.sendFile(__dirname + '/index.html')直接==返回==首页==文件==
      • 如果首页文件名为index.html,则可以省略上述方法,因为没有 path 路径时默认访问http(s)://ip:port/index.html,静态资源中间件会直接返回文件

==中间件==函数

function middleware(req,res,next){
    接收三个参数,分别是 请求报文对象、响应报文对象、next函数
    next函数执行后会运行后续的 路由回调函数 或 中间件回调函数
}
  • 是个回调函数,用于过滤、预处理请求数据

  • ==全局==中间件:在所有路由之前执行

    • 必须要将app.use方法写在所有路由规则前,写在app.<method>('path')后会先执行路由匹配。由此可见执行顺序是从上到下,谁写在前面先执行谁。
    • 并且app.use写到路由后也不会执行!除非app.use前的路由一个都没匹配上才会执行==中间件函数==
    • app.use(中间件函数):专门解析中间件函数的方法
      • use方法设置的是==全局==中间件
      • 会将 http 请求交给中间件
      • 可以传两个参数app.use('前缀', 路由对象),第一个参数是路由匹配规则前缀,第二个是路由对象
  • ==路由==中间件:在路由匹配规则触发后执行,并先于callback回调前执行

    • 使用方法app.<method>(path, 中间件函数, callback)
    • 同样会传入上述三个参数
  • ==静态资源==中间件

    http模块读取静态资源文件
    const http = require('http')
    const fs = require('fs')
    const server = http.createServer((req,res)=>{
        let {pathname} = new URL(`http://127.0.0.1${req.url}`)
        if(pathname === '/xxx'){
            ...
        } else if(pathname === 'path'){
    
        } else {
            需要一大串ifelse解析路由和静态资源读取
            fs.readFile(`.${pathname}`,(err,data)=>{
                if(err){
                    res.end()
                    return
                }
                res.end(data)
            })
        }
    })
    
    express框架读取静态资源
    const express = require('express')
    const app = express()
    app.use(express.static(__dirname + '/存放静态资源的文件夹路径')) //如 __dirname+'/img'
    app.<method>('path',callback)
    
    • 不过一般而言 HTML 页面上的静态资源都是相对路径,以当前 html 文件所在文件夹为==**dirname==,路径一般为./img/xxx.png等,因此请求 url 为http://127.0.0.1:port/img/xxx.png,这种情况下静态资源路径应写为express.static(**dirname)即可,因为静态资源路径写为express.static(\_\_dirname+'/img'),请求url必须为http://127.0.0.1:port/xxx.png,不能加/img,相当于express.static方法已经帮你把存放静态资源的文件夹路径拼上了
    • use方法都是==全局中间件==,因此要写在路由前
    • express.static方法会自动为静态资源加上对应响应头类型和字符集
    • Tips
      • ==小技巧==:当处理类似https://www.baidu.com这种无路径、端口的请求时,可以用res.redirect('./index.html')重定向到导航页的静态文件,就会触发express.static方法

获取==请求体==数据

  • express自身也有解析请求体的方法

    • app.use(express.json()/urlencoded()/text()/raw()):同body-parser一样有四钟方法解析请求体
    • app.use(express.urlencoded({ extended:false })):必写方法。同body-parser一样必须要有这个配置项才能解析请求体
  • body-parser将请求体数据解析成==对象==形式

    • body-parser只有JSON(application/json)表单(application/x-www-form)字符串(text/plain)二进制数据(application/octet-stream)准备了解析模块。分别为json()urlencoded()text()raw(),对==文件==类型的数据没有方法处理
    const parser = require('body-parser')
    // 解析 JSON 格式请求体的中间件
    const jsonp = parser.json()
    // 解析 查询字符串 格式请求体的中间件 如 name=123&password=111
    const queryp = parser.urlencoder({extended:false}) // 固定写法
    // 建议请求体中间件用作路由中间件
    app.<method>('path', queryp或jsonp, (req,res)=>{
        // 因为是同一个请求对象 请求体中间件执行完后会往 req对象 身上添加 body 属性
        res.send(req.body) // {name:123, password:111}
    })
    
  • body-parser不能处理文件的==二进制文件流==,因此需要引入formidable工具

    • formidable函数中uploadDir配置项不能写不存在的文件夹!只能选已有的文件夹下保存
    • 形参files是对象,存储着文件的详细信息,因为上传的文件会重新命名,所以有newFilenameoldFilename属性
    const formidable = require("formidable");
    app.post("/test", (req, res) => {
      // 创建formidable对象
      let form = formidable({
          mutiples: true,
          uploadDir: __dirname + "/src", //配置项 上传文件保存目录
          keepExtensions: true, //配置项 是否保留文件后缀
      });
      form.parse(req, (err, fields, files) => {
          // fields对象内存储着 除了文件之外 字段
          // files对象内存储着 文件数据 没传文件则为 空对象
          if (err) {
              next(err);
              return;
          }
      });
    });
    

防盗链

  • ==请求头==中的referer字段会显示==发送请求的网站==,可以根据这个字段决定是否返回内容,这即是防盗链

  • 注意==第一次==访问页面没有referer字段,之后页面中的静态资源以及触发的事件请求才有

    app.use((req, res, next) => {
      let url = req.get("referer");
      if (url) {
          // 第一种方法 正则
          let reg = /^https?:\/\/(?<a>[0-9\.a-zA-Z]+)(:[0-9]+)?\//g;
          req.cus_params = reg.exec(url).groups.a;
          // 第二种方法 URL API
          let obj = new URL(url);
          req.cus_params = obj.hostname;
          if (req.cus_params !== "127.0.0.1") {
              res.status(404).send("<h1>404 NOT FOUND</h1>");
              return;
          }
      }
      next();
    });
    app.use(express.static(__dirname));
    app.post("/login", (req, res) => {
      res.send(req.cus_params);
    });
    

路由模块化

  • 使用前需创建==路由对象==,const router = express.Router()

  • ==路由==对象跟==应用==对象app一样,使用同样的方法,只不过需要模块化暴露出去,并在其他地方==导入==app.use(router)使用

  • app.use('/user',router)use 可以设置==路由前缀==,即引用的路由都会加上/user等前缀,在路由文件的匹配规则中就不用写/user

  • 示例

    模块1.js
    const express = require('express')
    const router = express.Router()
    router.get('/home',(req,res)=>{...}) // 因为有前缀 相当于匹配get('/excel/home',callback)
    module.exports = router
    
    模块2.js
    const express = require('express')
    const router = express.Router()
    // 不同模块间也可以互相导入
    const router2 = require('./模块1.js')
    router.use('/excel', router2) // 加前缀
    router.post('/login',(req,res)=>{...})
    module.exports = router
    

express 中使用 ejs 模板

  • 使用 ejs 模块渲染 html 结构有两种方式

    • 读取.html文件生成字符串,放入 ejs 函数渲染

      const express = require("express");
      const app = express();
      const ejs = require("ejs");
      let content = "内容";
      let title = "标题";
      const fs = require("fs");
      let fileString = fs.readFileSync("./index.html").toString();
      let result = ejs(fileString, { title, content });
      app.get("/", (req, res) => {
          res.send(result); // 注意 此处用的是express的send
      });
      app.listen("80", () => {});
      
    • .ejs文件内写 html 结构,并用 ejs 在响应设置添加的方法渲染

      const express = require("express");
      const app = express();
      const ejs = require("ejs");
      app.set("view engine", "ejs");
      app.set("views", __dirname);
      app.get("/", (req, res) => {
          let text = "hahah";
          res.render("ejsPageName", { title: text }); //注意,此处使用render,且不需要写文件后缀
      });
      
  • app.set(key,value):express==应用对象==的 set 方法,用于设置==系统配置==

    • app.set('view engine', 'ejs')view engineviews是固定写法
    • app.set('views', path模块.resolve(__dirname, '/HTML模板存储文件夹')):指定res.render()渲染页面文件存放位置
  • 用例

    • js 页面

      const express = require("express");
      const app = express();
      app.set("view engine", "ejs");
      app.set("views", `${__dirname}`);
      app.get("/", (req, res) => {
          let text = "hahahah";
          res.render("ejsPageName", { title: text });
      });
      
    • ejsPageName.ejs 页面

      <!-- html结构 -->
      <h2><%= title %></h2>
      
  • Tips

    • 模板中写的变量==必须==传值进去

    • .ejs需要换行的地方==必须用==<% %>==包裹==

      <% array.forEach(item => { %> <!-- 注意此处只有半个括号 -->
          <li><%= item %></li>
      <% }) %>
      
    • 模板中可以写 js 语句,如:<%= title || 'title为false' %>

Mongodb

  • 三个重要概念

    • ==数据库(database)==:数据仓库,数据库==服务==下可以创建多个数据库,用以存放多个==集合==
      • 一个项目使用一个数据库
      • 一台电脑安装一个 mongodb,这台电脑就是一个==节点==
    • ==集合(collection)==:类似==数组==,集合中存放多个==文档==
      • 一个集合存储==同一类型==的数据
    • ==文档(document)==:文档是数据库中最小单位,类似==对象==。文档中的属性称之为==字段==
  • 安装数据库注意事项

    • 通过安装程序安装的mongodb不需要命令行启动服务,而是在window系统下的服务功能中启动
    • 以下展示.zip压缩包形式启动服务
      • 在安装或解压目录下的bin目录打开 cmd,执行mongod --dbpath=..\data\test指定数据库路径为..\data\test(以 bin 文件目录为起点同级的 data 下的 test)目录,并启动==mongodb 服务==,注意不是==HTTP 服务==,在浏览器端无法直接访问,因为是 HTTP==协议==向 mongodb 服务发起请求

        • 为了能连接 Mongodb==服务==,还需要启动 Mongodb==客户端==,以客户端连接服务

          • 客户端启动命令mongo注意不是mongod,有两个.exe文件
        • ==服务==开启后不能关闭,否则==客户端==访问不到

        • Mongodb 默认存放数据位置C:\data\db,如果用默认目录可以执行mongod直接启动服务,但使用自定义目录,就需要每次执行mongod --dbpath=..\data\test来启动服务

      • 命令行mongod指令其实是执行bin目录下的mongod.exe,可能会提示缺少xxx.dll文件,下载后放在c:\Windows\System32下即可

      • 配置环境变量,便于命令启动

        • 复制 mongodb 的 bin 文件夹目录,在系统——高级系统设置——环境变量——Path 中新建粘贴文件夹目录
        • 以后启动客户端就可以直接mongo,无需在bin目录下
          • 但是因为是自定义数据存放目录,因此启动服务还需要在bin目录下执行
      • 不要在服务窗口选中文本,会导致服务暂停,客户端操作无法返回结果

        • 解决方法:服务端窗口按下回车
  • 命令行交互

    • 数据库命令

      • 显示==所有==数据库:show dbs
      • 切换到指定数据库:use 数据库名
        • 如果不存在,则自动创建
      • 显示当前所在的数据库:db
      • 删除当前数据库:use 库名——db.dropDatabase()
        • 删除当前库后,再使用db查看当前库,会发现还是当前库,并且依然可以通过创建集合等方法填入内容,再次show dbs就又可以看到被删的库
    • 集合命令

      • 创建集合:db.createCollection('集合名')
      • 显示==当前==数据库中所有集合:show collections
      • 删除==某个==集合:db.集合名.drop()
      • 重命名集合:db.集合名.renameCollection('newName')
    • 文档命令

      • 插入文档:db.集合名.insert(文档对象)

      • 查询文档:db.集合名.find(查询条件)

        • _id是 mongodb 自动生成的唯一编号,用来唯一标识文档
        • 查询条件可以是文档中某一字段,但必须是对象的形式,如db.test1.find({age:20})
          • 字段必须完全匹配,不支持模糊搜索
          • 可以多字段同时查询
      • 更新文档:

        • 用新内容全部替换:db.集合名.update(查询条件, 新的文档)
        • 修改对应字段:db.集合名.update({name:'xxx'}, {$set:{age:19}})
        • 当有多个查询结果符合条件时,修改更新失败
      • 删除文档:db.集合名.remove(查询条件)

        • 当有多个查询结果符合条件,一起删除
  • 事务

    • 操作要么全部成功,要么全部失败。例如转账,银行从A扣钱,B加钱,有一环异常都会回滚操作

    • 基本操作

      • 开始事务:调用startTransaction()开启一个事务

      • 执行操作:事务中需要执行的操作,例如插入、删除文档

      • 提交事务:调用commitTransaction()提交事务,这会将执行操作持久化到磁盘(数据库)

      • 回滚事务:如果事务执行失败,调用abortTransaction()来回滚操作

    • 示例

      const client = await MongoClient.connect(url, {useNewUrlParser: true})
      // 创建事务示例
      const session = client.startSession()
      
      try{
          session.startTransaction()
          // 执行操作 中间有一步出错直接抛出异常
          await 修改文档
          await 更新文档
          
          await session.commitTransaction()
      }catch(err){
          // 捕获异常后回滚操作
          await session.abortTransaction()
      }finally{
          // 全部操作执行完毕并提交事务后 释放内存
          session.endSession()
      }
      
      
  • Tips

    • MySQL的区别
      • Mongodb属于==非关系型==数据库,MySQL属于==关系型==数据库
        • ==关系型==数据库特点是以的方式存储数据,类似 Excel,一系列行和列组成,一组组成数据库,每一列的==字段名==都是==固定==的
        • ==非关系型==数据库的特点是类 JSON,没有行和列,以文档为最小单元,每个文档中==字段名==可以==不固定==
        • ==关系型==数据库中对应==非关系型==数据库中集合一组表或集合组成一个数据库
      • MySQL必须用==SQL 语句==操作,而Mongodb不只用 SQL 语句,还可以用其他语言操作
      • MySQL适合读写操作==密集==的场景,Mongodb适合灵活更改==数据类型==场景

Mongoose

  • 是一个工具包使用npm i mongoose安装,作用是便于用==代码操作==Mongodb 数据库

    • 绕过mongo客户端命令行操作数据库,而是通过mongoose直连mongodb服务用代码进行操作
  • 导入 mongoose:const mongoose = require('mongoose')

  • 连接 mongodb 服务:mongoose.connect('协议名://IP地址:端口号/数据库名')

    • 协议名mongodb
    • 端口号默认27017
    • 数据库名不存在会自动创建
    • 注意本地连接时,IP 地址不能用localhost只能用127.0.0.1
  • 设置事件回调

    • 连接成功回调:mongoose.connection.on('open',()=>{})
      • 连接成功会执行一系列内置操作,使用on监听会导致掉线重连时,重复触发回调及内置操作,官方推荐使用mongoose.connection.once('open',callback),只会触发一次
    • 连接失败回调:mongoose.connection.on('error',()=>{})
      • 连接失败有一个==超时==的过程,不会立即返回结果
    • 连接关闭回调:mongoose.connection.on('close',()=>{})
      • 关闭连接:mongoose.disconnect()
  • 连接数据库后在集合中==添加==文档

    const mongoose = require("mongoose");
    
    // 连接Mongodb服务中指定数据库
    mongoose.connect("mongodb://127.0.0.1:27017/customdb");
    
    mongoose.connection.once("open", () => {
      // 首先创建文档结构对象 其实就是定义数据类型
      let dataType = new mongoose.Schema({
          name: String,
          price: String,
          num: Number,
      });
    
      // 然后创建对象模型 其实就是所连接数据库下指定要操作哪个集合,并限制文档字段数据类型
      // ※没有对应集合则会新建
      let obj = mongoose.model("集合名", dataType);
    
      // ※注意Mongoose@6.0以后的版本不支持create中传入回调函数 而是返回一个Promise对象
      obj
          .create({
              name: "菠萝",
              price: "30",
              num: 2,
          })
          .then(
              (data) => {
                  console.log("创建成功" + data);
              },
              (err) => {
                  console.log("创建失败" + err);
              }
          );
        
        obj.create({
            name: '可以只传入部分字段,创建出来的文档就只有name'
        }).then(data => {}, err => {})
        // 批量创建
        let users = [
            {name: 'xxx', age: 10},
            {name: 'xxx2', age: 30},
        ]
        obj.create(users).then()
    });
    mongoose.connection.on("error", () => {
      console.log("连接失败");
    });
    mongoose.connection.on("close", () => {
      console.log("连接关闭");
    });
    
    • 也可以通过save()创建==单个==文档
      // 可以使用mongoose.model实例对象创建文档对象
      let doc = new obj({
          name: "菠萝",
          price: "30",
          num: 2,
      })
      await doc.save()
      
      
  • ==删除==文档

    mongoose.connection.once("open", () => {
      let dataType = new mongoose.Schema({
          name: String,
          price: String,
          num: Number,
      });
      let obj = mongoose.model("集合名", dataType);
      // ※注意Mongoose@6.0以后的版本不支持delete中传入回调函数 而是返回一个Promise对象
      // 删除一条 可以有多个符合查询条件的 但只会删除第一条匹配到的
      obj.deleteOne({ name: "菠萝" }).then(
          (data) => {
              console.log("删除成功" + data);
          },
          (err) => {
              console.log("删除失败" + err);
          }
      );
      // 删除多条
      obj.deleteMany({ price: "30" }).then(
          (data) => {
              console.log("删除成功" + data);
          },
          (err) => {
              console.log("删除失败" + err);
          }
      );
    });
    
  • ==更新==文档

    • 更新文档的语法是model.update(查询条件, {操作符: {key: newValue}}),如果没写操作符,一律当$set设置值
    mongoose.connection.once('open',()=>{
        let dataType = new mongoose.Schema({
            name: String,
          price: String,
          num: Number,
        })
        let obj = mongoose.model('集合名', dataType)
        // ※注意Mongoose@6.0以后的版本不支持update中传入回调函数 而是返回一个Promise对象
        // 更新一条 传入两个参数 第一个参数是查询条件 第二个参数是更新内容
        obj.updateOne({name:'菠萝'},{price:'10'}).then( data =>{
            console.log('更新成功' + data)
        }, err =>{
            console.log('更新失败' + err)
        })
        // 更新多条
        obj.updateMany({price:'10'},{name:'荔枝'})then( data =>{
            console.log('更新成功' + data)
        }, err =>{
            console.log('更新失败' + err)
        })
    })
    
    • 更新文档中==数组==字段,使用$push等操作符

      // 数组末尾添加单个元素
      obj.updateOne({name:'test'}, {$push: {list: newElement}})
      // 数组末尾添加多个元素
      obj.updateOne({name:'test'}, {$push: {list: {$each: [newElement1, ...]}}})
      // 数组任意位置前插入元素 下例表示往数组中第2个位置前插入一个或多个元素
      obj.updateOne({name:'test'}, {$push: {list: {$each: [newElement], $position: 1}}})
      // 删除数组中指定值的元素
      obj.updateOne({name:'test'}, {$pull: {list: elementValue}})
      // 删除数组头部元素
      obj.updateOne({name:'test'}, {$pop: {list: -1}})
      // 删除数组末尾元素
      obj.updateOne({name:'test'}, {$pop: {list: 1}})
      // 删除指定索引的元素 下例表示删除数组第4个元素 1表示删除 0保留
      obj.updateOne({name:'test'}, {$unset: {'list.3': 1}})
      
    • 更新文档中==对象==字段

      // 更新对应字段值
      obj.updateOne({name:'test'}, {$set: {'obj1.obj2': newValue}})
      // 替换整个对象
      obj.updateOne({name:'test'}, {obj1: newObject})
      
    • (推荐)save()更新文档

      • 优点是非常自由,功能全,无需记忆操作符
      obj.find({name:'test'}).then(async (data) => {
          // 注:find查询结果是数组
          data[0].key = newValue
          // save()新版是Promise风格
          // save()只能操作 单个 文档
          await data[0].save()
      })
      // save是文档身上的方法 所以不能对多个文档查询结果使用
      let doc = await obj.findOne({name:'test'})
      doc.key = newValue
      await doc.save()
      
      
  • ==读取==文档

    • 注意:读取结果都是==数组==
    mg.connection.once("open", () => {
      let datatype = new mg.Schema({
          name: String,
          price: String,
          num: Number,
      });
      let obj = mg.model("test2", datatype);
      // ※读取不到也不会读取失败 只会返回空数组[] 旧版本返回null
      // 读取单条
      obj.findOne({ name: "茄子2" }).then(
          (data) => {
              mg.disconnect();
              console.log("读取成功", data);
          },
          (err) => {
              mg.disconnect();
              console.log("读取失败");
          }
      );
      // 通过id读取
      obj.findById("646dddcc94cf30cef7076975").then((data) => {
          mg.disconnect();
          console.log("读取成功" + data);
      });
      // 批量查询
      obj.find({ name: "test2" }).then((data) => {
          mg.disconnect();
          console.log("读取成功" + data);
      });
      // 读取所有
      obj.find().then((data) => {
          mg.disconnect();
          console.log("读取成功" + data);
      });
      // 读取筛选 读取到符合条件的记录只显示需要的字段内容
      // select 接收对象作为参数 对象内要读取的字段值为1
      // 省略或值设置为0 表示不显示
      obj
          .find()
          .select({ price: 1, name: 1 })
          .then((data) => {
              console.log("旧版本需要exec执行回调,将find里的回调方法放到exec中执行,新版返回的是Promise对象,因此不需要回调");
          });
      // 读取数据排序
      // select等方法可以链式调用
      obj
          .find()
          .select({ price: 1 })
          .sort({ price: 1 })
          .then((data) => {
              console.log("升序1 降序-1");
          });
      // 数据截取
      // skip跳过num条 limit取前num条 数据
      obj
          .find()
          .sort({ price: -1 })
          .skip(3)
          .limit(3)
          .then((data) => {
              console.log("price从降序排列,取4~6位置(skip跳过3条,limit只取前3条)的记录");
              console.log("常用于分页");
          });
    });
    
  • ==条件==控制

    • 运算符:mongodb不能使用<>=!==等运算符,而是用替代符号
      • >$gt
      • <$lt
      • >=$gte
      • <=$lte
      • !==$ne
      • 使用示例:db.test.find({ price: { $gt: 10 } }),表示价格比 10 大的记录
    • 逻辑运算
      • 逻辑==或==:$or
        • 示例:db.test.find({ $or:[{price:10}, {price:30}] }),价格 10 或 30 的
      • 逻辑==与==:$and
        • 示例:db.test.find({ $and:[{price:{$gt:10}, {price:{$lt:30}}] }),价格==大于==10==小于==30 的
    • 正则匹配
      • 示例:db.test.find({name:/[A-Z0-9]+/})
  • ==文档==结构可选字段类型

    • 字符串:String

    • 数字:Number

    • 布尔值:Boolean

    • 数组:Array,也可以用[]

      const mongoose = require('mongoose');
      let datatype = new mongoose.Schema({
          // 普通数组
          list: [Number]
      });
      // 对象数组
      let child = new mongoose.Schema({
          // 定义对象内字段
          name: String,
          age: Number
      });
      let parent = new mongoose.Schema({
          // 可以多个类型嵌套使用
          list: [child]
      });
      // 定义形式很自由 不需要刻板的使用类型对象嵌套
      let datatype = new mongoose.Schema({
          list: [{name: String, age: Number}]
      });
      
    • 对象:Object,也可以用{}

      const mongoose = require('mongoose');
      let datatype = new mongoose.Schema({
          // 多层级对象
          obj: {
              name: String,
              member: {
                  father: String,
                  mother: String
              },
              hobby: [String]
          }
      })
      
      
    • 日期:Date

      • 存储在数据库中是以==时间戳==的形式,毫秒为单位。但是在可视化工具上看是Thu Aug 17 2023 20:08:45 GMT+0800 (中国标准时间)形式

        mongooseModel.create({time:new Date()})
        
        
    • Buffer 对象:Buffer

    • 任意类型:mongoose.Schema.Types.Mixed

    • 对象 ID:mongoose.Schema.Types.ObjectId

      • 主要用于==关联表==,也就是==外键==,通过文档中这个字段来==查找其他表==,以联合搜索内容
    • 高精度数字:mongoose.Schema.Types.Decimal128

    • Tips

      • 写入文档的字段名和文档结构中==不符==,则会==忽视==
      • 写入文档==字段类型==与文档结构==不符==,则会==报错==
  • ==字段值==验证,mongoose 内置功能

    name:{
        type: String, // 指定类型
        required: true, // 设置为必填
    },
    price:{
        type: String,
        default: '10', // 设置默认值 此处default值类型和type不一致会被忽略
    },
    options:{
        type: String,
        // 设置的值 必须 是数组中的 比如options:'test2'
        // enum中值类型和type 必须 一致
        enum: ['test1','test2']
    },
    name:{
        type: String,
        // 设置为独一无二 重复会报错
        // 注意 要使用此项必须一开始就用 不能在旧集合中使用此项 会失效
        unique: true,
    }
    
  • ==条件==控制

    • mongodb 不能使用<>=!=等运算符,需要使用符号代替
    • 运算符:例{price:{$gt:10}}price 大于 10 的记录
      • >$gt
      • <$lt
      • >=$gte
      • <=$lte
      • !==$ne
    • 逻辑运算
      • $or逻辑或:{$or:[{price:{$lt:10}}, {price:{$gt:30}}]}price 小于 10或大于 30 的记录
      • $and逻辑与:{$and:[{price:{$gt:10}}, {price:{$lt:30}}]}price 大于 10小于 30 的记录
    • 正则匹配:{price:/[1-3]{1}[0-9]{1}/}
  • Tips

    • mongoose 除了_id字段会自动往文档里添加id字段

    • 可以用==点表示法==表示嵌套字段

      • 但是更新文档时不能用这种方式,会更新无效
      mg.connection.once("open", () => {
          let datatype = new mg.Schema({
              name: String,
              price: String,
              num: Number,
          });
          let obj = mg.model("test2", datatype);
          obj.find({'cusKey1.cusKey2': {$gt:10}}).then((data) => {});
      });
      
      

接口

  • RESTful API

    • 一种特殊风格的接口

    • URL 的路径表示==资源==,路径中不能有==动词==,例如createdeleteupdate等这些都不能有

    • 操作资源要与HTTP 请求方法对应

    • 操作结果要与HTTP 响应状态码对应

    • 请求类型

      • 其实是语义化的规范,里面逻辑还是自己写
      请求类型 返回
      GET 资源信息
      POST 新增并返回新的资源信息
      PUT 更新并返回新的资源信息(完整更新替换)
      DELETE 删除资源返回空文档
      PATCH 更新并返回新的资源信息(局部更新同名属性值)
  • json-server工具包

    • 用于快速搭建RESTful API服务
    • 安装:npm i -g json-server
  • 接口测试工具apipost

    • none表示没有请求体
    • form-data表示表单形式的数据
    • x-www-form-urlencoded表示query查询字符串
    • raw表示json等格式的原生请求体
    • 放入同一文件夹的接口可以设置通用header

日期对象

  • 存到数据库里的==时间==得是==日期对象==,因为后端内部调用接口时处理字符串形式的日期很麻烦,因此存成一个对象使用更方便
  • 有两种方式
    • 一种new Date()传入日期时间字符串
    • 另一种使用moment工具包转换成Date对象
      • moment('2023-10-1').toDate()

权限控制(用户身份识别)

  • HTTP 协议无法区分请求来自哪里,即==无法区分用户==

  • 常见的权限(会话)控制技术

    • cookie

      • HTTP 服务器发送到==浏览器保存==的一小块数据,主要是用作==载体==,用于将服务端生成的session传递给客户端,或是由客户端通过cookie传递数据给服务端

      • 按==域名==划分保存

      • key=value;形式的键值对

      • 特性

        • 浏览器向服务器发送请求时,会==自动==将==当前域名下==可用的 cookie 设置在==请求头==中,发送给==服务器==,再由服务器发送set-cookie: name=xxx;password=111响应头,浏览器识别到这一响应头会自动将 cookie 存储到当前域名下
        • 没有 cookie 不会有cookie: name=xxx请求头
        • 当前域名指同一==IP==、==端口号==,都会携带 cookie
        • 对于没有设置生命时长的 cookie,==浏览器==关闭时会自动清除;设置了时长的,时间一到也会自动清除
        • 注意:==响应报文==中的Max-Age=120单位是==秒==
      • 应用

        // express文件
        app.get("/login", (req, res) => {
          // 设置cookie
          res.cookie("name", "xxx");
        
          // 设置生命时长 单位毫秒
          res.cookie("name", "xxx", { maxAge: 2 * 60 * 1000 }); // 2分钟
        
          // 清除cookie
          // 只能一条条删
          res.clearCookie("name");
        });
        // 读取cookie
        // 需要安装cookie-parser工具包
        let cookieparser = require("cookie-parser");
        app.use(cookieparser());
        app.get("/read", (req, res) => {
          res.json(req.cookies);
        });
        
    • session

      • 诞生并保存在==服务器端==的数据,由服务器端主导一切

      • 运行流程

        1. 填写身份验证信息,校验通过后创建==session 对象==,将session_id的值通过set-cookie返回给浏览器
        2. 浏览器下次请求就会携带cookie,服务器通过cookie中的session_id确定身份
      • 应用

        // 需要安装express-session、connect-mongo工具包
        const session = require("express-session");
        const cm = require("connect-mongo");
        
        const app = express();
        // 设置session中间件 传入配置对象 返回一个函数
        app.use(
          session({
              name: "sid", //设置cookie的name,默认值:connect.sid
              secret: "key", //参与加密的字符串(签名/密钥)
              saveUninitialized: false, //是否每次请求设置一个cookie存储session的id,为true则不用session也会创建空对象
              resave: true, //是否每次请求重新保存session(延续生命周期),如20分钟过期,只要操作间隔不大于20分钟就一直不会过期
              store: cm.create({
                  mongoUrl: "mongodb://127.0.0.1:27017/customdb", //数据库连接配置
              }),
              cookie: {
                  httpOnly: true, //前端是否可通过JS操作cookie
                  maxAge: 60 * 1000, //控制 sessionID 的过期时间(包括浏览器cookie、数据库session过期时间)
              },
          })
        );
        // 路由
        app.get("/login", (req, res) => {
          if (req.query.name === "admin" && req.query.password === "admin") {
              // 设置 session 信息
              req.session.username = "admin";
              req.session.password = "admin";
        
              res.send("登陆成功");
          } else {
              res.send("登陆失败");
          }
        });
        // 读取 session
        app.get("/home", (req, res) => {
          // 中间件已经根据浏览器请求中携带的cookie获取session id
          // 并在数据库中查询将查询结果放入req对象中
          if (req.session.username) {
              res.send(`欢迎 ${req.session.username} 登录`);
          } else {
              res.send("你还没有登录");
          }
        });
        // 销毁 session
        app.get("/loginout", (req, res) => {
          req.session.destroy(() => {
              res.send("退出登录");
          });
        });
        
    • token

      • 诞生自==服务端==,保存在客户端,由客户端主导。返回给客户端的加密字符串,保存着==用户信息==

      • cookie不同的是,token是由客户端发送请求时手动添加到请求报文中,一般放在请求头

      • 特点

        • 数据存储在客户端,服务端压力更小
        • 更安全,数据加密避免伪造请求攻击
          • 即使token泄露,但==服务端==掌握的==加密字符串(钥匙)==不会泄露,就无法解析token获得加密信息
        • 拓展性强,可服务间共享、增加服务节点更简单
      • Tips

        • 记录token中携带的用户信息,便于取用

          let checklogin = (req, res, next)=>{
              let token = req.get("token");
              if (token && token !== "undefined") {
                  // 不要把空token传入校验会报错
                  jwt.verify(token, "myKey", (err, data) => {
                      if (err) {
                          console.log(err);
                          return res.json(json(null, "登陆已过期!"));
                      }
                      req.userInfo = data;//将用户信息保存起来
                      next();
                  });
              } else {
                  res.json(json(null, "登陆已过期!"));
              }
          }
          app.use(checklogin)
          // 路由规则
          
        • 因为token不存在服务端,因此无法刷新延长过期时效,想要像session一样时效内刷新token过期时间,只能把数据存储在服务端验证

          • 思路:生成不设过期时间的token,并存在数据库,添加字段表示过期时间。同一用户拿这个token请求时,服务端先从数据库中查找对应token,查看是否过期。如果在过期时间内,则重置过期时间字段。如果已经过期,则删除数据库对应token数据,并给前端返回登陆过期
        • ※为什么要将token放在headers:{Authorization: Bearer token}

          • 因为Authorization是专门用来做认证的请求头
          • ==OAuth2.0规则==要求使用Bearer token的形式
      • 应用

        // 需要安装 jsonwebtoken 工具包
        const jwt = require("jsonwebtoken");
        
        // 创建 token
        // jwt.sign(数据, 加密字符串, 配置项)
        let token = jwt.sign(
          {
              username: "xxx",
          },
          "secretkey",
          {
              expiresIn: 60, //单位 秒
          }
        );
        
        // 解析 token
        jwt.verify(token, "secretkey", (err, data) => {
          if (err) {
              // 使用加密字符串解析token不正确时触发
              console.log(err);
              return;
          }
          //解析得到得数据 {username:'xxx', iat: 注册时间戳, exp: 过期时间戳}
          console.log(data);
        });
        
  • sessioncookie的区别

    • 存储位置
      • cookie:浏览器端
      • session:服务器端
    • 安全性
      • cookie是明文,安全性较低
      • session数据存于服务器相对安全,即时暴露也只有 cookie 中的session_id
    • 网络传输量
      • cookie设置内容过多会影响传输效率
      • session存在服务器,只通过cookieid,不影响效率
    • 存储限制
      • 浏览器限制单个 cookie 保存数据不能超过==4K==,且==同域名==下存储条数也有限制

服务器部署运行(项目上线)

  • 环境配置

    • 首先在服务器中安装nodeJSMongodbgit环境
    • 然后npm i安装package.json下的依赖包
    • 最后在命令行使用node ./path/server.js启动服务
  • 在==云服务器本地==访问服务使用127.0.0.1:port/page访问页面,而==外网==使用云服务器公网IP:port/page访问页面

  • 域名注册(以阿里云为例)

    • 产品中的域名注册,搜索像注册使用的域名看是否可以购买
    • 进入域名-控制台,如果是国内使用,需要在ICP备案按流程登记等待审核
    • ==审核过==的域名点击解析,将域名和==云服务器公网IP==做一个映射
      • tips:像www.baidu.com这类都是域名,不是IP地址,浏览器其实不知道把请求发给谁,所以会去DNS服务器去查询当前域名对应的IP地址
    • 点击添加记录记录类型选择指向一个IPV4地址主机记录设置www等前缀,记录值填写服务器公网IP
  • 配置HTTPS证书

    • 只能在==服务器==端操作,本地服务无法操作

    • https可以==加密HTTP报文==,响应报文由客户端解码,请求报文由服务器解码

    • 工具官网

    • 操作流程

      1. 下载工具安装

      2. 管理员运行命令certbot certonly --standalone

        • 证书获取会占用80端口,注意不要跟服务器启的服务冲突
        • 获取证书需要输入邮箱、==域名==
          • 域名必须是==指向当前服务器IP==的
        • 证书会存储在本地
      3. 导入并使用https模块创建服务

        const https = require('https')
        const fs = require('fs')
        // 导入express
        const express = require('express')
        const app = express()
        // 创建https服务 应用express对象
        https.createServer({
            // 证书存放路径 注意因为是反斜线需要转义\
            key: fs.readFileSync('C:\\Certbot\\live\\www.custom.com\\privkey.pem','utf8'),
            cert: fs.readFileSync('C:\\Certbot\\live\\www.custom.com\\cert.pem','utf8'),
            ca: fs.readFileSync('C:\\Certbot\\live\\www.custom.com\\chain.pem','utf8'),
        },   // 在http或https.createServer中配置express对象
            app).listen(443,()=>{
            // https默认端口号443
            console.log('443端口监听中')
        })
        // 设置路由规则
        app.use(中间件)
        const router = require('./api/router.js')
        app.use('/api',router)
        app.get('/', callback)
        
      4. 证书更新

        • 有效期==三个月==
        • 一般更新:certbot renew
        • 强制更新:certbot --force-renewal

微服务

  • 演化过程

    1. 单体架构

      [图片上传失败...(image-4ed798-1717731286604)]

      <center>启1个服务,所有应用接口都集中在一起,共同操作维护一个数据库</center>

    2. 负载均衡

      [图片上传失败...(image-f979eb-1717731286604)]

      <center>同一套应用接口部署在多台设备上,由nginx转发请求到不同设备,共同操作维护一个数据库</center>

    3. 负载均衡+主从数据库

      [图片上传失败...(image-73f871-1717731286604)]

      <center>在负载均衡基础上,每台设备维护单独的数据库,数据库之间同步数据</center>

    4. 微服务

      [图片上传失败...(image-24c709-1717731286604)]

      <center>将单个服务的应用接口拆成分散的服务搭载独立的接口部署在多台服务器</center>

  • GRPC

    • 概念:通过远程调用其他服务上的接口称为RPC,基于RPC技术思想开发的微服务框架有谷歌的GRPC框架、阿里的dubbo框架、Facebook的Thrift、腾讯的Tars等。可以跨语言调用接口方法,如Java中写了个微服务,在NodeJS中可以调用

    • 通俗理解微服务

      • 一整个服务被拆分成多个小服务,每个小服务单独维护自己的接口路由和数据库,那每个被拆分的服务怎么知道其他服务中有哪些接口可以调用呢?答案就是在上层用一个统一的协议记录每个服务的接口信息。而前端在请求接口时则是先经过==微服务网关==,通过微服务网关分发到各个小服务器下。其中微服务网关到微服务的请求方式依据用的框架而定,前端到微服务网关的请求则是前端自己的请求方式
    • 示例

      • 所在项目如果没有package.json,需要npm init生成

      • 安装==生产==依赖:cnpm i @grpc/grpc-js @grpc/proto-loader

        • npm没有这个包,必须安装cnpm
      • 配置.proto文件

        // proto文件 语句结尾需要加;号
        syntax = 'proto3'; // 1.设置proto版本
        
        package test1; // 包名
        
        // 2.定义服务
        service Greeter{
          // 3.定义远程调用方法 rpc、returns是固定写法
          rpc fn1(fn1Req) returns (fn1Res) {}
        }
        
        // 4.定义形参和返回值数据结构
        // message在不同语言中生成不同的结果 在js中得到的是对象 在go语言中得到的是结构体 诸如此类
        message fn1Req{
          // 定义对象中接收的参数类型 int32、bool、double、string
          // 类型后的字段表示 形参名
          // 参数后的数字表示 传参顺序
          string name = 1;
          int64 age = 2;
          string sex = 3;
          bool flag = 4;
        }
        message fn1Res{
          int32 code = 1;
          string message = 2;
          string result = 3;
        }
        
      • 配置微服务文件server.js

        // 1.引入proto文件
        let grpc = require('@grpc/grpc-js');
        let protoloader = require('@grpc/proto-loader');
        const path = require('path');
        let proto_path = path.resolve(__dirname + '/protos/greeter.proto');
        
        // 2.解析proto文件 固定写法
        let package_definition = protoloader.loadSync(proto_path, {
          keepCase: true,
          longs: String,
          enums: String,
          defaults: true,
          oneofs: true,
        });
        // test1是在.proto文件中定义的包名
        let proto_obj = grpc.loadPackageDefinition(package_definition).test1;
        
        // 3.定义远程调用方法 函数名必须跟.proto文件中 rpc定义的方法名 统一
        function fn1(req, res) {
          console.log(req);
          res(null, { code: 200, message: '测试成功', result: 'test server' });
        }
        
        // 4.启动服务
        // 创建实例对象
        let server = new grpc.Server();
        // 注册服务 service是固定写法 第二个参数是调用方法 可以传入多个
        server.addService(proto_obj.Greeter.service, { fn1 });
        // 监听端口
        // 参数:地址及端口 通信凭证 回调函数
        // 地址可以填'0.0.0.0'表示任意地址
        server.bindAsync('127.0.0.1:3001', grpc.ServerCredentials.createInsecure(), () => {
          server.start();
          console.log('监听3001端口...');
        });
        
      • 配置客户端文件client.js

        // 1.引入proto文件
        let grpc = require('@grpc/grpc-js');
        let protoloader = require('@grpc/proto-loader');
        const path = require('path');
        let proto_path = path.resolve(__dirname + '/protos/greeter.proto');
        
        // 2.解析proto文件 固定写法
        let package_definition = protoloader.loadSync(proto_path, {
          keepCase: true,
          longs: String,
          enums: String,
          defaults: true,
          oneofs: true,
        });
        // test1是在.proto文件中定义的包名
        let proto_obj = grpc.loadPackageDefinition(package_definition).test1;
        
        // 3.建立连接
        // 参数:地址及端口 通信凭证
        // 注:通信凭证与服务端不同
        let client = new proto_obj.Greeter('127.0.0.1:3001', grpc.credentials.createInsecure());
        // 调用方法 参数必须对应.proto的message生成结果 对象中属性可以不写 但不能写错 会接收不到
        // 参数:错误信息 响应体数据
        client.fn1({ name: '张三', age: 22, sex: '男', flag: true }, (err, res) => {
          console.log(res);
        });
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容