NodeJs 初识 (三)

HTTPS 协议 && node_EventLoop && others

HTTPS 原理

保证数据在传输过程中不被窃取和篡改,从而保证 传输 安全。默认端口 443

加密

  1. 对称加密: 产生一个密钥,可以用它加密,也可以用它解密。

    常用的对称加密算法有: DES / 3DES / AES /Blowfish 等。

  2. 非对称加密:产生一对密钥,一个用于加密,一个用于解密(公钥 ←→ 私钥)

CA (Certificate Authority) - 证书颁发机构

流程:

  • 服务器申请证书(CA 通过私钥加密生成)
  • 服务器将证书给予客户端 (客户端能拿到CA机构的公钥,证书签名生成算法等便于进行后面的验证)
  • 客户端 CA公钥解密拿到服务器公钥 key1
  • 通过公开算法进行证书签名验证
  • 中间者就无法篡改服务器给予客户端的公钥 key1,从而也就拿不到客户端生成的通过公钥 key1 加密的对称密钥 key2 了。也就无法参与中间信息的篡改…
  • 这样就保证了传输过程的安全性(因为一旦证书签名验证不通过,两边都会知道传输过程中出了问题)

证书签名:由域名、CA 公钥 KEY 和服务器公钥 key 通过公开的算法得出,用于验证签名是否被篡改。

图示(可能证书生成并不像本人画的那样,但主要想表明 服务器公钥是通过CA私钥加密的,且生成唯一的证书签名):

在这里插入图片描述

https 模块

服务器结构

一般是采用 nginx 搭建的基于 https 的代理服务器去与用户进行数据传输,node 在服务器内部搭建基于 http 的与 nginx 服务器交互的服务器结构。

https使用

借用上一篇的例子更改

const https = require('https'); // https 模块
const URL = require('url');
const path = require("path");
const fs = require('fs');

async function getStat(filename) {
    try {
        return await fs.promises.stat(filename);
    } catch {
        return null;
    }
}

async function getFileContent(url) {
    const urlObj = URL.parse(url);
    let filename = path.resolve(__dirname, "public", urlObj.pathname.substr(1)),
        stat = await getStat(filename);
    if (!stat) {
        return null;
    } else if (stat.isDirectory()) {
        filename = path.resolve(__dirname, "public", urlObj.pathname.substr(1), "index.html");
        stat = await getStat(filename);
        if (!stat) {
            return null;
        } else {
            return await fs.promises.readFile(filename);
        }
    } else {
        return await fs.promises.readFile(filename);
    }
}

async function handler(req, res) {
    const info = await getFileContent(req.url);
    if (info) {
        res.write(info);
    } else {
        res.statusCode = 404;
        res.write("Response is no exists...");
    }
    res.end();
}

const server = https.createServer({
    // 在创建服务器时配置私钥与证书 即可
    key: fs.readFileSync(path.resolve(__dirname, "/xxxx.pem")), // 私钥
    cert: fs.readFileSync(path.resolve(__dirname, "/xxxx.cert")) // 证书
}, handler);

server.on('listening', () => {
    console.log("server listing 9999...");
});
server.listen(9999);

node 生命周期 (或事件循环)

node_event_loop

nodejs 事件循环中会有六个队列,一个队列到达后,看其中有没有任务,若有,则拿出来执行到清空队列,再进入下一个队列。

  1. timers 队列: 存放计时器的回调函数(循环查看计时器线程按个检查,再运算计时器事件的时间到了没有)

  2. poll 队列: 轮询队列(除了 timers、check),绝大部分回调都会放入此队列,比如文件的读取,监听用户请求等

    运作方式:

    • 如果poll中有回调,依次执行回调,直到清空队列
    • 如果poll中没有回调
      则等待其他队列中出现回调,结束该阶段,进入下一阶段;
      如果其他队列也没有回调,则持续等待,直到出现回调为止。
  3. check 队列: setImmediate() 进入的队列(类似于单纯地 push 进数组再依次执行)

    事件循环中,每打算执行一个回调之前,必须要先清空 nextTick 和 Promise 队列

    node事件循环面试题

    // 面试题 1
    setImmediate(() => {
        console.log(1);
    });
    process.nextTick(() => {
        console.log(2);
        process.nextTick(() => {
            console.log(6);
        });
    });
    console.log(3);
    Promise.resolve().then(() => {
        console.log(4);
        process.nextTick(() => {
            console.log(5);
        });
    });
    
    // 结果: 3 2 6 4 5 1
    
    // 面试题 2
    async function async1() {
        console.log("async1 start");
        await async2();
        console.log("async1 end");
    }
    async function async2() {
        console.log("async2");
    }
    console.log("script start");
    setTimeout(() => {
        console.log("setTimeout0");
    }, 0);
    setTimeout(() => {
        console.log("setTimeout3");
    }, 3);
    setImmediate(() => console.log("setImmediate"));
    process.nextTick(() => console.log("nextTick"));
    async1();
    new Promise(resolve => {
        console.log("promise1");
        resolve();
        console.log("promise2");
    }).then(() => console.log("promise3"));
    console.log("script end");
    // 这里在我做的时候没有注意的点是:
    // 1. async1();  => 是同步代码执行
    // 2. new Promise 函数里 resolve() 之后的打印代码也是同步代码
    
    // 结果(前9位可确定,后三位不确定):
    
    // script start, async1 start, async2, promise1, promise2,
    // script end, nextTick, async1 end, promise3,
    
    // 最后三个仅 setTimeout0 在 setTimeout3 之前可以确定。
    // setImmediate 所在位置不能确定,所以有三种答案,如下:
    
    // (1) setTimeout0, setTimeout3, setImmediate
    // (2) setTimeout0, setImmediate, setTimeout3
    // (3) setImmediate, setTimeout0, setTimeout3
    

EventEmitter

事件处理

const { EventEmitter } = require('events'); // events 模块的事件
// 创建事件处理对象
// 可以注册事件 / 触发事件

const emitter = new EventEmitter();

emitter.on("test", () => {  // 注册 test 事件
    console.log("test event");
});

emitter.emit("test"); // 手动触发

// 传递数据
emitter.on("transfer", (data1, data2) => {
    console.log("传递的数据: ", data1, data2);  // 打印: 传递的数据: 123 456
});
emitter.emit("transfer", 123, 456);

应用:

// request.js
const http = require("http");
const { EventEmitter } = require('events');

module.exports = class extends EventEmitter {
    constructor(url, options){
        super();
        this.url = url;
        this.options = options;
    }
    send(body = ""){
        const request = http.request(this.url, this.options, res => {
            const headers = res.headers;
            let result = "";
            res.on('data', chunk => {
                result += chunk.toString('utf-8');
            });
            res.on('end', () => {
                this.emit('res', {
                    headers,
                    result
                });
            })
        });
        request.write(body);
        request.end();
    }
}

// index.js
const ownRequest = require('./request.js');
const url = "http://www.baidu.com";
const request = new ownRequest(url);

request.send();
request.on("res", res => {
    console.log(res);
});

重温 MySQL

曾经在学校也学习和使用过 MySQL, SQL Server, SQLite 等关系型数据库。
作为 关系型数据库,特点就是以表和表的关联构成数据结构,可以表达复杂的数据关系,SQL 语句作为强大的结构化查询语句,能通过条件设置精确地查找想要查询的数据。
但同时,它的结构也就比较死板,读写的性能就差了一些。所以,它一般用作存储结构复杂的数据。它的代表有:(甲骨文的) Oracle 和 MySQL 以及 (微软的) SQL Server等。

与之相对的 非关系型数据库,本人也曾使用过 MongoDB 。特点是可以以极其简单的结构存储数据,比如文档型、键值对型。格式就比较灵活,针对海量数据的读写效率很高。但它对复杂的数据结构就有些吃力了,难以表示复杂的数据结构,而且对于复杂的数据查询也比较差。所以一般用来存储结构简单的数据。它的代表有: MongoDB, Redis, Membase 等。

相关术语:

  • DB: database 数据库
  • DBA: database administrator 数据库管理员
  • DBS: database system 数据库系统 (DBS 包含 DB, DBA, DBMS)
  • DBMS: database management system 数据库管理系统

关于忘记 MySQL root 密码之重新安装 MySQL

18年课设用过 MySQL,后来就没怎么用过,然后 root 密码就忘了。后来重装过系统(对啊,我重装系统的时候C盘全清了啊…),再下就要密码,没搞定也没急着用(就搁置了)。

解决办法:

  • 控制面板卸载之前装过的 MySQL(与之相关的全卸掉)

  • 正如网上一些博客说的,打开注册表编辑器: win + R 输入 regedit 删除注册表信息:

HKEY_LOCAL_MACHINE/SYSTEM/ControlSet001/Services/MySQL
HKEY_LOCAL_MACHINE/SYSTEM/ControlSet001/Services/Eventlog/MySQL
好像是,之前我是删了的 ( 没做下面第三步依旧需要验证密码 )

  • 关键是这一步: 打开 C:\ProgramData 文件夹, 删掉 MySQL 文件夹, 这里面可能就是记录存储了 root 的密码(重装系统我全格式化了啊???! 疑问)。

  • 之后再去官网或者腾讯给予的 MySQL msi 镜像文件下载,安装就可以 重新设置 root 密码 了…(这次得记住了,免得又忘了…)

MySQL 安装好之后使用

  1. 环境变量 path 里加上 MySQL 安装目录的 bin 目录路径;
  2. 在 cmd 窗口输入 mysql -uroot -p 进入 mysql 命令交互 (password 就是你安装时设置的 root 密码);
  3. show variables like 'character\_set\_%'; 命令查看数据库字符编码;
  4. 修改 C:\ProgramData\MySQL\MySQL Server 8.0(我这里装的是 MySQL 8.0.19版所以可能文件夹名字不一样)目录下的 my.ini 配置文件:

    default-character-set=utf8mb4
    character-set-server=utf8mb4
    并将修改后的此文件复制到 MySQL 的安装目录下(与bin目录同级即可);

  5. 重启 MySQL 服务,运行 show variables like 'character\_set\_%'; 命令即可看到修改后的字符编码。
  6. 然后下载 Navicat数据库管理工具 (至于怎么永久使用,此处不予说明哈), 用它可进行交互式数据库操作 (命令行顺手的就不用下),可能图形化操作会更顺手,也更简单,但是会让人变 "傻"

数据库设计

SQL (Structured Query Language 结构化查询语言),可以访问和处理数据库。

这就干脆在用 Navicat 工具时,看看 SQL 预览 ,重新熟悉熟悉SQL语句。

DDL (数据定义语言) 操作数据库、表、视图跟存储视图
DML (数据操控语言) 操作数据库中的记录
DCL (数据控制语句) 操作用户权限

数据库表中字段一些类型:

  • bit ----- (1位,0或1,false或true)
  • int ----- (32位,整数类型)
  • decimal(M, N) ----- (能精确计算的实数,M是总的数字位数,N为小数的位数,超出部分会被截去)
  • char(n) ----- (固定长度位 n 的字符)
  • varchar(n) ----- (长度不固定,最大长度位 n 的字符)
  • text ----- (大量的字符)
  • date ----- 仅日期
  • datetime ----- 日期和时间
  • time ----- 仅时间

主键: 根据关系型数据库设计规则,每张表都要有 主键( 单一主键或组合主键 ),唯一且不可更改,而且不能有业务含义
外键: 用于关联其他表的键

表关系:

  • 一对一: 一个 A 对应一个 B,一个 B 对应一个 A
  • 一对多: 一个 A 对应多个 B,一个 B 对应一个 A,A 和 B 是一对多,B 和 A 是多对一 (如一个人和他写的博客)
  • 多对多: 一个 A 对应多个 B,一个 B 对应多个 A

三大设计范式:

每一列都是不可分割的原子数据项
非主键列必须依赖于主键列
非主键列必须直接依赖于主键列

数据的基本操作方式: 增删改查

  • 增: INSERT INTO <表名> [(字段1, 字段2, ...)] VALUES ('value1', 'value2', ...) 有默认值 — 对应位置 value 置为 default,或者不设置此字段; [] 中可不写(按默认列一次插值)

  • 改: UPDATE <表名> SET 字段名=value WHERE 条件

  • 删: DELETE FROM <表名> WHERE 条件

  • 查:

    -- 基本结构
    select ...
    from ...
    where ...
    order by ...    -- 排序 asc (默认升序), - desc 降序
    limit m, n  -- 跳过 m 条数据,取 n 条数据
    
    -- case用法  对male列进行单独处理 --- 表中存储 0 / 1,希望拿到 男 / 女
    -- as 可省略
    select id, `name`,
    case male
    when 1 then '男'
    else '女'
    end as sex,
    birthDay
    from students;
    
    select DISTINCT `location` FROM student; -- DISTINCT 被查询项去除重复 (注意查询多列也得是相应对应重复项)
    
    -- 联表查询 employee 表的外键 departId 关联 department 表的主键 id
    select * from department as d left join employee as e
    on d.id = e.departId
    -- 左连接如果在另一张表找不到相应的对应数据,则左表占一行,连接数据全为 Null
    -- 右连接(right join)以右表为基准
    
    -- 内连
    select * from department as d inner join employee as e
    on d.id = e.departId
    
    -- sql语句运行顺序 -----------------------------------------------------
    from... join...on...wheregroup byselecthavingorder bylimit
    

视图 属于 DDL

CREATE VIEW databaseName <表名> AS SELECT ...

数据驱动和 ORM

mysql 驱动程序

  1. 驱动程序: 连接内存和其他存储介质的桥梁

    mysql 驱动程序是连接内存和 mysql 数据的桥梁

  2. 驱动 - mysql2(同mysql) 的使用

    • 基本使用

      // 回调模式
      const mysql = require('mysql2');
      
      // 创建数据库连接
      const connection = mysql.createConnection({
          host: 'localhost',
          user: 'root',
          password: '******', // 你的数据库 root 密码
          database: 'localdb'
      });
      // simple query
      connection.query(
          'SELECT * FROM `users` WHERE `name` LIKE "%s%" AND `age` > 22',
          (err, results) => {
              console.log(results); // 得到查询数据
          }
      );
      connection.end(); // 断开连接
      
      // ----------------- 利用 Promise 创建异步连接 ---------------
      const mysql = require('mysql2/promise');
      
      async function test(){
          // 创建数据库连接
          const connection = await mysql.createConnection({
              host: 'localhost',
              user: 'root',
              password: '******',
              database: 'localdb'
          });
          const [result] = await connection.query('SELECT * FROM `users` WHERE `name` LIKE "%s%" AND `age` > 22;'); // 解构语法只拿 result
          console.log(result);
          connection.end();
      }
      test();
      
    • 上面的 query API 及 sql 语句字符串拼接写法容易造成 SQL 注入攻击
      故而提供 预编译 方式处理 sql 语句

      const mysql = require('mysql2/promise');
      
      async function test(id, name){
          // 创建数据库连接
          const connection = await mysql.createConnection({
              host: 'localhost',
              user: 'root',
              password: '******',
              database: 'localdb'
          });
          const sql = 'SELECT * FROM `users` WHERE id=? or name=?;'; // 固定 sql 语句格式,不可再通过 sql 注入攻击
          const [result] = await connection.excute(sql, [id, name]); // 参数作为数组每一项对应 ? 位置依次传入
          console.log(result);
          connection.end();
      }
      test('', `''; delete from ...`);
      
    • connection pools (连接池)

      const mysql = require('mysql2/promise');
      
      async function test(id, name){
          // 创建一个连接池
          const pool = mysql.createPool({
              host: 'localhost',
              user: 'root',
              password: '********',
              database: 'localdb',
              waitForConnections: true, // 等待创建新连接
              connectionLimit: 10, // 连接池最大连接数量
              queueLimit: 0   // 排队 0 - 不限制长度
          });
          const sql = 'SELECT * FROM `users` WHERE id=? or `name`=?;'; // 固定 sql 语句格式,不可再通过 sql 注入攻击
          // const sql = `SELECT * FROM \`users\` WHERE \`name\` LIKE concat(%, ?, %);`;  // 模糊查询 (两层模板字符串 所以用到转义)
          const [result] = await pool.excute(sql, [id, name]); // 参数作为数组每一项对应 ? 位置依次传入
          console.log(result);
          connection.end();
      }
      test('', `''; delete from ...`);
      

ORM 框架

ORM (Object Relational Mapping 对象关系映射)
通过 ORM 框架,可以自动地把程序中的对象和数据库中的表进行关联
ORM 框架会隐藏具体的数据库底层细节,让开发者使用统一的数据操作接口,完成对不同数据库的操作。

ORM 示意图

如上图所示,让我们不用关心数据库具体是哪个,只需关心对象的操作等,也可以轻易地完成数据库的移植,无须拼接复杂的 sql 语句即可完成精确查询功能

Node 中的 ORM: SequelizeTypeORM (本文选用 sequelize)

  • Sequelize 起步

    const mysql = require('mysql2/promise');
    const { Sequelize } = require('sequelize');
    // 创建数据库连接
    const sequelize = new Sequelize('localdb', 'root', 'your root password', {
        host: 'localhost',
        dialect: 'mysql'    // 指定数据库类型
    });
    
    // 测试连接是否成功 --- 若一段时间内没有操作,它会自动关闭连接
    sequelize
        .authenticate()
        .then(() => {
            console.log('Connection has been established successfully.');
        })
        .catch(err => {
            console.error('Unable to connect to the database:', err);
        });
    
    // sequelize.close();  // 手动关闭连接(一般不会去手动关闭)
    
  • 模型定义 – 模型同步 - 创建表

    const { Sequelize, DataTypes } = require('sequelize');
    const sequelize = new Sequelize('database', 'user', 'password', {
        host: 'localhost',
        dialect: 'mysql',
        logging: null // 日志记录禁用(或置为false),默认是控制台输出(console.log)
    });
    
    const Admin = sequelize.define('Admin', {
        // 定义模型属性 -- 字段定义
        loginId: {
            type: DataTypes.STRING,
            allowNull: false
        },
        loginPwd: {
            type: DataTypes.STRING,
            allowNull: false
        },
        name: {
            type: DataTypes.STRING,
            allowNull: false
        }
    }, {
        paranoid: true, // 使用此配置,该表中的数据不会真的删除,而是给予一个 deleteAt 标记
        createAt: false, // 取消创建 创建时间字段(默认为 true)
        updateAt: false, // 取消创建 更新时间字段(默认为 true)
    });
    
    const Student = sequelize.define('Student', {
        name: {
            type: DataTypes.STRING,
            allowNull: false
        },
        birthDay: {
            type: DataTypes.DATE,
            allowNull: false,
            get() { // 访问器 -- this 指向模型实例
                // 若使用 this.birthDay 将会无限循环调用,所以提供了 getDataValue('birthDay') 方法
                // 在查询数据时,就需要拿到 birthDay ,否则虚拟属性 age 无法计算,age 取它的值时会是 undefined 而报错
                return this.getDataValue('birthDay').getTime(); // 得到时间戳
            },
            set(val){
                // 设置值  如此处使用 moment 模块设置 utc 时间
                this.setDataValue('birthDay', moment.utc());
            }
        },
        // 虚拟属性 不会存储到数据库 (如同 vue 的 computed 属性 -- 计算得来)
        age: {
            type: DataTypes.VIRTUAL,  // 虚拟属性类型
            get() {
                const now = moment.utc(),
                    birth = moment.utc(this.birthDay);
                return now.diff(birth, 'y'); // 得到两个日期的年份差异(舍位存整)
            }
        },
        sex: {
            type: DataTypes.BOOLEAN,
            allowNull: false
        },
        mobile: {
            type: DataTypes.STRING(11),
            allowNull: false
        },
        location: {
            type: DataTypes.STRING,
            allowNull: false
        }
    }, {
        paranoid: true,
        createdAt: false,
        updatedAt: false
    });
    
    // 创建模块关联 - 在进行关联表查询时,每次都需要运行关联语句
    // 否则就会报错 --- 提示两张表未进行关联而无法查询
    const Student = require('./Student');
    const Class = require('./Class');
    // 相当于创建外键 - 此处是通过在 students 表中生成 ClassId 字段关联 classes 表的 id
    
    // Class.hasMany(Student);
    // Student.belongsTo(Class);
    
    // 通过本人测试得知: 上面两句设置模型关联语句每次在关联查询时都需要使用
    // 但是会运行一次就创建一个外键关联(待我发现时,查看外键已经有150+了)
    // 可以改用下面这句,单独放到设置关联的js文件 setRelation.js 中(这里直接 require 导入就可以了)
    Student.belongsTo(Class, {
    	foreignKey: 'ClassId',
    	// targetKey: 'id'		// 这条配置好像可写可不写(没写我也正常建了外键)
    });
    await sequelize.sync({ alter: true }); // 一次同步所有模型
    
    // 在下面插入模拟数据时,通过同步模型的回调函数中运行插入模拟数据的代码即可
    // 也可少运行一次设置关联的代码,减少一次新增外键
    

模拟表格数据 - Mock.js

// mockClass.js	-> 模拟 classes 表的数据
const Mock = require('mockjs');
const result = Mock.mock({
    "datas|16": [{
        "id|+1": 1,
        "name": " @id 班",
        openDate: "@date"
    }]
}).datas;
const Class = require("../models/Class");
Class.bulkCreate(result);

// mockStudent.js	-> 模拟 students 表的数据
const Mock = require('mockjs');
const result = Mock.mock({
    "datas|500-700": [{		// 500-700 条记录
        "id|+1": 1,			// id 自增
        "name": "@cname",	// 随机中文名
        birthDay: "@date",	// 随机日期
        "sex|1-2": true,	// 性别: true - false 随机
        mobile: /1(3|5|7|8|9)\d{9}/,	// 手机号 或者用它 → /^[1]([3-9])\d{9}$/
        location: "@county(true)",		// 随机 省 市 区 地址
        "ClassId|1-16": 1
    }]
}).datas;
const Student = require("../models/Student");
Student.bulkCreate(result);

ClassId 的数据插入,都需要两个模块建立关联,否则将无法插入数据

数据查询

  1. 单条查询 findOne():

    // const Admin = sequelize.define(...);
    async function getInfo(loginId, loginPwd) {
        const res = await Admin.findOne({ where: { loginId, loginPwd } });
        if (res && res.loginId === loginId && res.loginPwd === loginPwd) {
            return res.toJSON();
        }
        return null;
    }
    // sql 语句中本不区分大小写,因而查询返回结果进行比较判定再返回
    getInfo('username', 'password').then(res => console.log(res));
    
  2. 按主键查找单条数据 findByPK(): 传入主键即可(创建表的时候主键默认是自增 id 值)

  3. 查询多个数据 findAll() , 返回一个 数组:

    // const Student = sequelize.define(...);
    async function getStudents() {
        const res = await Student.findAll();
        return JSON.stringify(res);
    }
    getStudents().then(res => console.log(res));
    
    // 分页查询 --- 使用 limit 和 offset 参数 -------------------
    async function getStudents(page = 1, limit = 10) {
        const res = await Student.findAll({
            offset: (page - 1)*limit
            limit: +limit
        });
        const total = await Student.count(); // 查询数量
        const datas = JSON.parse(JSON.stringify(res));
        return {
            total,
            datas
        }
    }
    getStudents().then(res => console.log(res));
    
    • 提供 findAndCountAll() 方法 – 结合 findAll 和 count 的便捷方法
    const { Op } = require('sequelize');
    async function getStudents(page = 1, limit = 10, name = '') {
        const res = await Student.findAndCountAll({
           where: {
               [Op.like]: `%${name}%`   // 模糊查询
           },
           offset: (page - 1)*limit
           limit: +limit
        });
        return {
            total: res.count
            datas: JSON.parse(JSON.stringify(res.rows))
        }
    }
    getStudents().then(res => console.log(res));
    
    // 限制查询内容(不想全部查询出来) -- 设置 attributes 属性
    findAndCountAll({
        attributes: ['id', 'sex', 'name'], // 只查'id', 'sex', 'name'
    });
    
  4. 包含关系 include() - 联表查询:

    const Class = require('./Class');
    const Student = require('./Student');
    // require('../models/relation.js');
    /** -------------------------------------------------------------------------
    * 每次使用关联查询时,必须运行下面进行外键关联的语句
    * 否则将无法认为这两张表进行了关联操作,从而导致查询失败
    * Class.hasMany(Student);
    * Student.belongsTo(Class);
    * 或者更改为一句 ==================== :
    * Student.belongsTo(Class, {
    *    foreignKey: "ClassId"
    * });
    * 但同时,每次一运行到这两句设置关联的语句时(如关联查询时),都会新增外键;我在测试查询接口时,
    * 就已经加了 150 多个外键了。(这个问题我目前还没有找到良好的解决办法)
    * 但后面这句设置关联貌似比前面两句要好,新增外键数量不会每次查询都加一个外键(我不确定是否正确,但测试过几次是这样)
    * ----------------------------------------------------------------------------
    */
    async function getInfo(page = 1, limit = 10){
        const res = await Student.findAndCountAll({
        	offset: (page - 1) * limit,
        	limit: +limit,
            include: Class
        });
    }
    

数据抓取

  • axios 库cheerio 库 配合使用, 抓取数据存储于数据库 books 表中

    cheerio 库 是 jQuery 的核心库,但它不涉及 DOM 操作

    以抓取 豆瓣 书籍数据为例:

    const axios = require('axios').default;
    const cheerio = require('cheerio');
    const Books = require("../modules/Book");
    
    async function getBooksHTML(url) {
        const res = await axios.get(url);
        return res.data;
    }
    
    // 拿到所有详情页链接
    async function getBookLnks() {
        const html = await getBooksHTML("https://book.douban.com/latest");
        const $ = cheerio.load(html);
        const aEle = $("#content .grid-12-12 li a.cover");
        const links = aEle.map((i, ele) => {
            const href = ele.attribs["href"];
            return href;
        }).get();
        return links;
    }
    
    /**
    * 根据详情页地址,得到书籍详情页的书籍详细信息
    * @param {*} detailUrl 详情页地址
    */
    async function getBookDetail(detailUrl) {
        const res = await axios.get(detailUrl);
        const $ = cheerio.load(res.data);
        const name = $('h1').text().trim(); // 书籍名
        const imgUrl = $("#mainpic .nbg img").attr("src"); // 图片地址
        const spans = $('#info span.pl');
        const authorSpan = spans.filter((i, ele) => {
            return $(ele).text().includes("作者");
        });
        const author = authorSpan.next("a").text(); // 作者
        const publishSpan = spans.filter((i, ele) => {
            return $(ele).text().includes("出版年");
        });
        const publishDate = publishSpan[0].nextSibling.nodeValue.trim(); // 出版时间
        return {
            name,
            imgUrl,
            author,
            publishDate
        }
    }
    
    async function fetchAll() {
        const links = await getBookLnks();
        const pro = links.map(link => {
            return getBookDetail(link);
        });
        return Promise.all(pro);
    }
    
    async function savaToDB() {
        const books = await fetchAll();
        await Books.bulkCreate(books);
        console.log("抓取数据并成功保存到数据库...");
    }
    
    savaToDB();
    

MD5 加密

hash 算法之一 — 单向加密规则(只可加密,不可解密)
将任何一个字符串加密成任何一个 固定长度 的字符串
同样的原字符串加密结果是固定的

md5 库 --> 提供函数 md5()

let str = md5('jhagfg23jhhih.awfh'); // => edc1cgdcd21ce225d5fb42bfxc74h0dc (这是乱改的 >=.=< )

moment库

进行时间转换以适应不同需求

  1. UTC: 世界协调时(英国格林尼治天文台旧址本初子午线时间) 它与东八区相差八个小时

  2. 时间戳: 一个 UTC 时间到 UTC 1970-1-1 凌晨 0:00 的时间差(默认毫秒)

  3. 服务器可能会部署到世界的任何位置,所以服务器内部英国统一使用 UTC 时间或时间戳,数据库也一样

  4. 对于客户端,就需要根据 UTC 时间或时间戳转换为当地时间

    const moment = require('moment');
    console.log(moment().toString()); // 无参 -> 当前时间(本地)
    console.log(moment.utc().toString()); // 一样的结果
    
    // 得到当前时间戳
    console.log(moment().valueOf());    // 毫秒数
    console.log(moment.utc().valueOf());
    
    // 根据指定的日期格式得到时间
    console.log(moment.utc("1970-01-01 00:00:00").toString());
    
  5. 令牌: 格式化字符串 如: "YYYY-MM-DD HH:mm:ss"

    const moment = require('moment');
    // moment.locale("zh-cn"); // 设置中文
    const token = ['YYYY-MM-DD HH:mm:ss', 'YY-M-D H:m:s', 'YYYY/MM/DD HH:mm:ss']; // 日期格式
    /** 参数
     * 1. 日期
     * 2. 令牌 - 日期格式数组
     * 3. 严格检查(true)
     */
    const m = moment.utc("1970-01-01 00:00:00", token, true);
    
    // 调用 moment 对象的 isValid() 方法可以判断是否是合乎规则的日期格式
    const flag = m.isValid(); // false
    
    // 显示
    // 拿到 moment 对象
    m.format("YYYY年MM月DD日 HH时mm分ss秒"); // 1970年01月01日 00时00分00秒  --  显示的是 utc 时间
    // 转换成本地时间 调用 local()
    m.local().format("YYYY年MM月DD日 HH时mm分ss秒");
    // 转换成utc时间 调用 utc()
    const n = moment("2020-05-23 22:43:37");
    const toServerTime = n.utc().format('YYYY-MM-DD HH:mm:ss');
    
    // 相较于当前时间的时间差
    const last = moment.utc("2020-05-23 22:23:37");
    console.log(last.local().fromNow()); // 20 minutes ago
    

数据验证

  1. 前端 – 为了用户体验
  2. 路由层 – 验证格式是否正常
  3. 业务逻辑层 – 保证业务完整性(含数据)
  4. 数据库验证 – 保证数据完整性

validator

用于验证某个 字符串 是否满足某个规则

validate.js

用于验证单个数据或某个对象的属性是否满足某些规则

const validate = require('validate.js');
const classServ = require('./classService');
/** 例如:
 * stuObj = {
 *    name: 'xxx',
 *    sex: 'x',
 *    mobile:  'xxxxxxxxxxxx'
 *    ...
 * }
*/
async function addStudent(stuObj){
    // 定义验证规则
    const rule = {
        name: {
            // 验证 name 属性为 null, undefined 都不通过, 但"" 能通过
            presence: true,
            // presence: {
            // // 验证 name 属性,除上面之外,为 "", "   "也不通过
            //    allowEmpty: false
            // },
            type: "string", // 验证类型
            length: {   // 验证 name 长度为 1-10 个字符
                minmum: 1,
                maximum: 10
            }
        }
    }
    const reslut = validate.validate(stuObj, rule); // 同步验证
    // 通过验证返回 undefined
    // 不通过,默认返回 {name: ["Name can't be blank"]}
}
async function addStudent(stuObj){
    // 扩展验证规则
    validate.validators.classExists = async value => {
        const res = await classServ.getClassById(value);
        if(res){
            return; // 验证通过
        }
        return "is not exist"; // 前面会自动拼接 value 值
    }
    const rule = {
        classId: {
            presence: true,
            classExists: true // 置为 true 会去运行上面定义的函数,并将 classId 的值做为参数传递
        }
    }
    await validate.async(stuObj, rule); // 异步验证 => 通过 - 无返回值; 不通过 - 报错
    // 通过就执行下面语句进行添加学生操作;不通过就报错阻塞
    const ins = await Student.create(stuObj);
    return ins.toJSON();
}

日志记录

  • 日志:调试日志、信息日志、错误日志等

  • 等级:

    all ---------- 低
    trance
    debug
    info
    warn
    error
    fatal
    mark
    off ---------- 高

  • 日志分类: categroy 如 sql 日志、请求日志等

  • 日志出口: appender,写到哪儿,写的格式

log4js库

// start -------
const log4js = require('log4js');
const logger = log4js.getLogger(); // 默认类别 - default
logger.level = 'all'; // 设置等级  -- 默认 off 级别
logger.info('abc');

// ----------- 配置
const log4js = require('log4js');
const path = require('path');
log4js.configure({
    // 出口
    appenders: {
        sqlOut: {
            type: 'file', // 备份文件会直接以 .1 .2 .3 的格式接在备份文件名之后  -- 改用 "dateFile" 即可以日期格式记录
            filename: path.resolve(__dirname, 'logs', 'sql', 'logging.log'),
            maxLogSize: 1024, // 配置文件的最大字节数(超过此值即分割备份一个文件) -- 1024 -> 1kb
            keepFileExt: true,  // 保持备份文件后缀名不变
            daysToKeep: 0,
            layout: { // 输出配置
                type: "pattern",
                pattern: "%c [%d{yyyy-MM-dd hh:mm:ss}] [&p] : %m" // 自定义日志记录格式
            }
        },
        // 自己去配置时,必须 配置 default 项,否则会报错
        default: {
            type: 'file',
            filename: path.resolve(__dirname, 'logs', 'default', 'logging.log'),
        }
    },
    // 分类
    categories: {
        sql: {
            appenders: ['sqlOut'], // 该分类使用出口 sqlOut 的配置写入日志
            level: 'all'
        },
        default: {
            appenders: ['default'],
            level: 'all'
        }
    }
});

process.on('exit', () => {
    log4js.shutdown();  // 程序退出时,让日志记录完成记录工作
});

const logger = log4js.getlogger('sql');
logger.info('info to exit');

针对上述配置,可以专门新建一个 log.js 文件用于管理日志记录处理,导出需要的 logger 对象。在需要日志记录的地方,直接导入使用即可。

总结

在本文中,总结了一下 https 协议安全的原理, 重点了解 node 的事件循环机制,重温了一下 MySQL 及 sequelize ORM 框架的涉水,另外涉及了几个库的使用…就这么多吧,记录一下自己的学习进度

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值