第三章 数据驱动与ORM

113 阅读6分钟

MySQL驱动程序

驱动程序是连接内存与其他存储介质的桥梁,MySQL驱动程序是连接内存数据和MySQL数据的桥梁

MySQL驱动程序通常使用:mysql、mysql2

本章选择使用mysql2

const mysql = require("mysql2");

// 创建一个普通的数据库连接
const connection = mysql.createConnection({
    host: "localhost",			// 数据库所在主机的主机号
    user: "root",				// 连接的数据库的账号
    password: "123123",			// 连接的数据库的密码
    database "...",				// 使用的数据库的名称
    multipleStatements: true	// 允许一次执行多条SQL语句
});

// 通过connection的query方法可以执行任何的SQL语句
connection.query(
	"select * from tablename",
    (err, res)=>{
        console.log(res);
    }
);

// 断开与数据库的连接
connection.end();

mysql2也提供了promise的版本

const mysql = require("mysql2/promise");

async function test(){
    const connection = await mysql.createConnection({
            host: "localhost",
            user: "root",
            password: "123123",
            database "..."
    });

    const [ res ] = await connection.query("select * from tablename");

    connection.end();
}

test();

预防SQL注入

SQL注入是指:用户通过注入SQL语句到代码中,导致整个SQL的行为与预期不符

query方法会直接执行传入进去的SQL字符串,这就可能导致SQL注入

function queryById(id){
    const mysql = require("mysql2");

    const connection = mysql.createConnection({
        host: "localhost",
        user: "root",
        password: "123123",
        database "...",
        multipleStatements: true
    });

    connection.query(
        `select * from tablename where id = ${id}`,
        (err, res)=>{
            console.log(res);
        }
    );

    connection.end();
}

queryById("''; delete from tabelname;");
// 相当于执行了一个查询语句和一个删除表内容语句

另一个能够执行SQL语句的方法叫做execute,该方法会在执行SQL前,将SQL进行编译,之后执行的是编译后SQL

execute所要执行的SQL语句,不应该包含任何拼接外部变量的内容,SQL语句中所有不确定的部分,应该使用?进行占位,并通过第二个参数数组来传递实际的数据

function queryById(id){
    const mysql = require("mysql2");

    const connection = mysql.createConnection({
        host: "localhost",
        user: "root",
        password: "123123",
        database "...",
        multipleStatements: true
    });

    connection.execute(
        "select * from tablename where id = ?",
        [id],
        (err, res)=>{
            console.log(res);
        }
    );

    connection.end();
}

queryById("''; delete from tabelname;");
// 只执行了一个查询语句,查询的内容是id为【''; delete from tablename;】的对象

占位符号?只能处在SQL语句的顶级位置,它不能被单引号包裹

connection.execute(
    "select * from tablename where name like '%?%'",
    ['张三'],
    (err, res)=>{
        console.log(res);
    }
);
// 这里的?并不是占位符,而是实际的查询条件的一部分

面对这种情况,需要使用concat函数

connection.execute(
    "select * from tablename where name like concat('%', ?, '%')",
    ['张三'],
    (err, res)=>{
        console.log(res);
    }
);

创建连接池

在实际应用中,服务器通常需要服务大量的用户,处理大量的用户请求,因此服务器与数据库通常需要建立大量的连接

开发者手动创建的数据库连接需要开发者手动进行管理,如果开发者忘记释放掉了某个数据库连接,就会导致服务器大量堆积的数据库连接,这会极大消耗服务器的资源

因此实际开发中通常并不会直接创建连接,而是使用连接池中的已经创建好的连接

数据库连接池中的连接是由mysql驱动程序自行创建并负责管理的,开发者可以直接使用这些连接与数据库建立联系

数据库连接池中的连接数量是有上限的,并且只有当前处于空闲状态的连接才能够被使用,此时如果有大量的请求需要使用到数据库连接时,那些拿不到空闲连接的请求就需要排队

const mysql = require("mysql2");

// 使用连接池后,所有的SQL语句应该都通过该连接池对象进行执行,因此连接池对象只需要在最初创建一次即可
// 连接池以及池中的连接都无需手动释放,一切均交给驱动程序即可
const pool = mysql.createPool({
    host: "localhost",
    user: "root",
    password: "123123",
    database "...",
    multipleStatements: true,
    connectionLimit: 10				// 连接池中连接的最大数量
    queueLimit: 0					// 排队队列的长度,0表示不限制长度
});

function queryById(id){
    pool.exectue(
        "select * from tablename where id = ?",
        [id],
        (err, res)=>{
            console.log(res);
        }
    );
}

function deleteById(id){
    pool.exectue(
        "delete from tablename where id = ?",
        [id],
        (err, res)=>{
            console.log(res);
        }
    );
}

ORM框架

ORM,Object Relational Mapping,对象关系映射

通过ORM框架,可以自动地把程序中的对象和数据库中的表记录进行关联

此外,ORM框架会隐藏具体的数据库底层细节,让开发者使用同样的操作接口,完成对不同数据库的操作

ORM提供的API接口无需使用SQL语句,它会根据具体的调用方式,自动生成合适的SQL语句操作数据

image.png

ORM的优势可以概括为:

  • 不需要关心数据库,仅需要关心对象
  • 可轻易地完成对数据库的迁移
  • 无需拼接复杂的sql语句即可完成精确查询

Node中的ORM框架:

  • Sequelize

    支持JS和TS

    后续章节均使用该框架进行讲解

  • TypeORM

    只支持TS

模型定义和同步

在使用sequelize之前,需要安装相应的数据库驱动程序

npm i sequelize
npm i mysql2

连接数据库

const { Sequelize } = require("sequelize");

// sequelize是一个连接池
const sequelize = new Sequelize("dbname", "username", "password", {
    host: "localhost",
    dialect: "mysql",				// 连接的数据库的类型
    logging: null					// 设置日志的显示方式
});

测试连接:

try {
    await sequelize.authenticate();
	console.log("数据库连接成功");
} catch(err) {
    console.err("数据库连接失败");
}

可以使用sequelize.close()来主动关闭数据库连接

定义模型

定义模型就是在定义一张数据表,模型的属性就是表中的字段

使用sequelize.define方法来定义模型

定义模型时需要传入模型的名称(作为第一个参数),通常采用大驼峰命名法定义名称

第二个参数是一个对象,对象中的属性就是模型的属性,属性中可以传递一些配置

define方法允许传递第三个参数(是一个对象),通过第三个参数可以控制生成的表的名称;默认情况下,模型对应的表名是第一个参数的复数形式,可以通过第三个参数的tableName属性指定生成的表的名称,或者将freezeTableName设置为true,这样表名就会和第一个参数保持一致

sequelize在生成表时,会自动往表中加入两个字段,分别是createdAt和updatedAt,如果不需要这两个字段,可以在定义模型时通过第三个参数进行设置;将这两个属性设置为布尔值可以控制是否生成字段,将这两个属性设置为字符串可以控制所生成的字段的名称

在实际开发中,往往不允许真正删除数据表中的记录,因为一旦将记录删除,就很难将其恢复回来,通常会在表中增加一个记录是否可用的字段来表示该记录是否被“删除”。通过将第三个参数的paranoid属性设置为true,这样表中的记录就不会被真正地删除,并且表中还会多出一个deletedAt字段,通过该字段就可以判断记录是否已经“删除”(字段值为null时就表示没有被删除),同时也能记录下“删除”本记录的时间(字段值为具体的时间时就表示记录已经被删除)

define会返回一个模型对象

const { DataTypes } = require("sequelize");

// sequelize会自动生成主键列id,无需在定义模型时手动设置
const Admin = sequelize.define("Admin", {
    name: {
        type: DataTypes.STRING(255),	// 属性的类型
        allowNull: false				// 是否允许为null
    },
    loginId: {
        type: DataTypes.STRING,
        allowNull: false
    },
    loginPwd: {
        type: DataTypes.STRING,
        allowNull: false
    }
}, {
	tableName: "admin",					// 指定生成的表的名称
    createdAt: false,					// 阻止生成createAt字段
    updatedAt: false,					// 阻止生成updateAt字段
    paranoid: true,						// 生成deletedAt字段
    deletedAt: "deleteTime"				// 设置deletedAt字段的名称
});

模型同步

模型同步就是根据模型对象创建出真实的数据库表

使用模型对象的sync方法可以同步模型

sync方法在调用时可以传递一个对象参数,传递的参数不同sync方法的行为也将不同:

  • 如果表不存在,则创建该表,否则什么也不做

    await Admin.sync();
    
  • 如果表不存在,则创建该表,如果表存在,则删除重建

    await Admin.sync({
        force: true
    });
    
  • 如果表不存在,则创建该表,如果表存在,则对比其与模型的差异,并根据差异更新表以匹配模型

    await Admin.sync({
        alter: true
    });
    

也可以通过sequelize.sync方法同时同步所有模型:

await sequelize.sync({
    alter: true
});

建立关联

A和B都是模型对象

  • A.hasOne(B)

    一个A拥有一个B,此时,sequelize会将外键建立在B表中

  • A.belongsTo(B)

    一个A属于一个B,此时,sequelize会将外键建立在A表中

  • A.hasMany(B)

    一个A拥有多个B,此时,sequelize会将外键建立在B表中

  • A.belongsToMany(B, { through: "C" })

    建立中间表C,C用于管理A与B的多对多关系

模型的增删改

三层架构

image.png

增加

利用模型对象的build方法,可以创建出一个待插入的表记录的模型实例,创建实例时需要传入一个对象参数,对象中的相应属性会成为新增记录的内容数据

仅仅创建出模型实例还不会向表中增加新记录,只有调用了实例的save方法后才能真正增加一条新的记录

const ins = Admin.build({
    name: "张三",
    loginId: "zhangsan",
    loginPwd: "123123"
});

// 在save之前还可以直接通过ins修改要增加的记录中的数据
ins.name = "李四";

ins.save().then(()=>{
    console.log("数据添加成功");
});

sequelize还提供了更简单的增加数据的方法 —— create,该方法实际上是上面两个方法的结合

create方法会返回一个Promise,Promise完成之后的数据就是插入记录的模型实例

Admin.create({
    name: "张三",
    loginId: "zhangsan",
    loginPwd: "123123"
}).then((ins)=>{
    console.log("数据添加成功", ins);
});

create一次只能增加一条记录,要想增加多条记录,需要使用bulkCreate方法,该方法需要传入一个数组,数组的元素就是插入的记录的映射对象

Admin.bulkCreate([{
    name: "张三",
    loginId: "zhangsan",
    loginPwd: "123123"
}]).then(()=>{
    console.log("数据全部添加成功");
});

使用模型实例的toJSON()方法,可以去除掉实例对象中的多余属性,并返回一个简洁的普通对象

删除

删除记录的方式有两个,一个是通过待删除记录的模型实例进行删除,另一个是通过模型对象进行删除

第一种删除记录的方式需要先获取到要删除记录的模型实例

通过模型对象的findByPk即可根据主键查找出相应的记录并得到相应的模型实例,之后通过实例的destroy方法即可删除该记录

如果表中存在相应的记录(即没有被删除,或deletedAt为null),则实例对象就不为null,如果不存在,则实例为null

const ins = await Admin.findByPk(id);

if(ins !== null){
    ins.destroy().then(()=>{
		console.log("删除成功");
    });
}

这种方式运行时需要执行两条SQL,第一条是获取待删除记录的模型实例时执行的查询语句,第二条是在调用destroy方法时执行的删除语句

第二种删除记录的方式是直接通过模型对象的destroy方法进行删除,如果不想一次性删除所有记录,则需要给定查询的条件

// 删除id为【id】的记录
Admin.destroy({
    where: {
        id: id
    }
}).then((res)=>{
    console.log("删除成功", res);
});

这种方式运行时只会执行一条SQL,效率更高

修改

修改记录的方式有两个,一个是通过待修改记录的模型实例进行修改,另一个是通过模型对象进行修改

第一种修改记录的方式需要先获取到要修改记录的模型实例,之后通过更改实例对象的属性来达到修改记录中某些字段的内容的效果,最后调用save方法即可将修改后的数据更新到原来的记录上

const ins = Admin.findByPk(id);

// 直接通过修改模型实例的属性来实现对字段内容的修改
ins.name = "王五";

ins.save().then((res)=>{
    console.log("修改成功", res);
});

第二种删除记录的方式是直接通过模型对象的update方法进行修改,update方法的第一个参数传入一个对象,该对象中的属性会更新到记录的相应字段中,第二个参数是一个配置对象,通过该对象可以设置要修改记录的查询条件

Admin.update({
	name: "赵六"
}, {
    where: {
        id: id
    }
});

数据查询

  • findOne

    查询单个数据

    该方法实际上是在匹配的多条记录中取出第一条(limit 0, 1),并会返回这条记录对应的模型实例

    如果没有匹配的记录,则返回null

    const ins = await Admin.findOne({
        where: {
            loginId,
            loginPwd
        }
    });
    
  • findByPk

    按照主键查询单个数据

    const ins = await Admin.findByPk(id);
    
  • findAll

    查询多个数据

    该方法会查询出所有匹配的记录,将这些记录的模型实例作为数组元素,并将数组返回

    如果没有满足条件的记录则返回空数组

    const insArr = await Admin.findAll({
        where: {
            name: name
        }
    });
    

    如果不书写条件,将会把表中的所有记录都查询出来

    findAll也支持分页查询,利用offset和limit配置可以实现分页查询

    const insArr = await Admin.findAll({
        offset: (page - 1) * limit,
        limit: limit
    });
    
  • count

    查询匹配的记录的数量

    const total = await Admin.count({
        where: { ... }
    });
    
  • findAndCountAll

    findAll和count方法的结合

    该方法在执行count方法时会忽略传入的offset和limit配置

    const res = await Admin.findAndCountAll({
        where: { ... },
    	offset: (page - 1) * limit,
        limit: limit
    });
    
  • 字段过滤

    默认情况下,sequelize会将记录中的所有字段都查询出来,可以利用attributes配置实现只查询某些字段

    const insArr = await Admin.findAll({
        attributes: ["id", "name", "sex"]
    });
    
  • 更多的条件查询

    sequelize提供了Op API,利用该API可以实现更多的条件查询

    const { Op } = require("sequelize");
    
    // 模糊查询
    const insArr = await Admin.findAll({
        where: {
            name: {
                [Op.like]: "%张三%"
            }
        }
    });
    
  • 查询关联信息

    在设置完两张表之间的关系后,只需要在配置中加入include,即可轻松地查询到当前记录所关联的另一张表的记录

    设置关联:

    const Student = require("./models/Student");
    const Class = require("./models/Class");
    
    Student.belonesTo(Class);
    Class.hasMany(Student);
    

    查询关联关系:

    const Student = require("./models/Student");
    const Class = require("./models/Class");
    
    const insArr = await Student.findAll({
        include: [Class]
    });
    

MD5加密

MD5加密是hash加密算法的其中一种,它能够将任意长度的字符串转换为固定长度的加密字符串,对于待加密的字符串,经过MD5加密后,只会得到固定且唯一的结果

MD5加密只能实现单向加密,无法对加密字符串进行解密

在nodejs中,可以使用第三方库md5来实现md5加密

const md5 = require("md5");

const res = md5("...");

moment

utc时间又称为世界协调时,它是以英国格林威治时间为标准

utc时间和北京时间相差8个小时,北京时间比utc时间多8个小时

时间戳:从utc时间的1970年1月1号0时0分0秒到另一个utc时间所经过的毫秒数

服务器可能会部署在世界的任何位置,因此服务器(包括数据库)内部应该使用utc时间或者时间戳来操作和记录时间

客户端在发送时间给服务器时,应该发送的是utc时间或者时间戳,而不应该是本地时间;客户端在收到服务器响应的utc时间或者时间戳后,应该将其转换为本地时间以展示给用户

image.png

使用第三方库moment就可以很好地对时间进行转换

const moment = require("moment");

console.log(moment());					// 得到当前本地时间的moment时间对象
console.log(moment().toString());		// 得到当前本地时间的时间字符串
console.log(moment.utc().toString());	// 得到utc时间的时间字符串
console.log(moment().valueOf());		// 得到时间戳
console.log(+moment());					// 得到时间戳

调用moment函数时可以传入一个数字,此时moment会将其视为时间戳

console.log(moment(0).toString());		// Thu Jan 01 1970 08:00:00 GMT+0800
console.log(moment.utc(0).toString());	// Thu Jan 01 1970 00:00:00 GMT+0000

也可以传入一个字符串,此时moment会将其视为本地时间

const time = "1970-01-01 00:00:00";

console.log(moment(time).toString());			// Thu Jan 01 1970 00:00:00 GMT+0800
console.log(moment.utc(time).toString());		// Thu Jan 01 1970 00:00:00 GMT+0000

moment可以将时间字符串转换为对应的moment时间对象,但在转换时需要手动指定时间字符串的格式,第三个参数传入一个布尔值,当第三个参数为true时表示仅根据传入的格式字符串进行转换,若为false,则moment还会尝试使用其他的字符串格式进行转换

如果传入的时间字符串与格式字符串格式无法匹配,则moment会返回一个无效的时间对象,无效的时间对象的时间戳固定为NaN

const time = "1970-01-01 00:00:00";
const format = "YYYY-MM-DD HH:mm:ss";

console.log(moment(time, format, true));		// time字符串对应的moment时间对象

第二个参数除了可以是一个格式字符串,还可以是一个格式字符串数组,此时moment会依次在数组中查找匹配的格式字符串然后对时间字符串进行转换

const time = "1970-01-01 00:00:00";
const formats = ["YYYY-MM-DD HH:mm:ss", "YYYY/M/D H:m:s", "x"];

console.log(moment(time, formats, true));		// time字符串对应的moment时间对象

小写的"x"表示毫秒时间戳,大写的"X"表示秒时间戳

如果被转换的的时间字符串不满足格式字符串中的格式,则将会得到一个错误的moment时间对象

const time = "Thu Jan 01 1970 00:00:00 GMT+0800";
const formats = ["YYYY-MM-DD HH:mm:ss", "YYYY/M/D H:m:s", "x"];

console.log(+moment(time, formats, true));				// NaN
console.log(moment(time, formats, true).toString());	// "Invalid date"
console.log(moment.isValid());							// 该方法可以判断时间对象是否有效

如果需要将时间格式化为字符串,可以利用moment对象的format方法

const m1 = moment.utc("1970-01-01 00:00:00", "YYYY-MM-DD HH:mm:ss", true);

const format = "YYYY年MM月DD日 HH时mm分ss秒";

console.log(m1.isUTC());					// 判断是否是utc时间
console.log(m1.format(format));				// "1970年01月01日 00时00分00秒"
console.log(m1.local().format(format));		// 转换为本地时间的时间对象后再进行格式化

const m2 = moment("1970-01-01 00:00:00", "YYYY-MM-DD HH:mm:ss", true);

console.log(m2.utc().format(format));		// 转换为uct时间的时间对象后再进行格式化

使用moment时间对象的fromNow方法可以得到该时间与当前时间的距离

const m = moment.utc("1970-01-01 00:00:00", "YYYY-MM-DD HH:mm:ss", true);

console.log(m.utc().fromNow());					// "55 years ago"

可以通过设置moment的语言来控制moment的一些输出结果

moment.locale("zh-cn");						// 设置语言为“中文”

const m = moment.utc("1970-01-01 00:00:00", "YYYY-MM-DD HH:mm:ss", true);

console.log(m.utc().fromNow());					// "55 年前"

数据验证

数据验证的位置:

  • 前端:为了用户体验
  • 路由层:验证接口格式是否正常
  • 业务逻辑层:保证业务完整性
  • 数据库验证:保证数据完整性

支持数据验证的第三方库有:validator、validate.js

validator只能对字符串进行验证,它能够检验某个字符串是否满足规则

validate.js则能够对更多类型的数据进行验证,其中一个就是对对象的验证,它能够对对象的属性进行检验,检验它们是否满足规则

本小节仅对validate.js进行介绍

validate.js会导出一个对象,利用该对象的validate方法即可对对象的属性进行验证

验证时,第一个参数传入的是待检验的对象,第二个参数是验证的规则

通过设置验证规则中的属性名称,即可实现对待验证对象的同名属性进行检验

验证规则中的每一个属性都是一个对象,对象中的每一个属性都是一个验证器

当验证成功后,validate方法会返回undefined,否则将返回验证不通过的提示对象

const validate = require("validate.js");

const obj = {
    name: "张三",
    birthday: "1970-01-01",
    mobile: "12345678901"
};

const res = validate.validate(obj, {
    name: {								// 对obj的name属性进行验证
        presence: true,					// 不允许为null或undefined
        presence: {						// 不允许为空值,包括空对象、空数组、空串、空格字符串
       		allowEmpty: false
        },
        type: "string",					// 属性值的类型必须为string
        length: {						// 字符串的长度范围
            minimum: 1, 
            maximum: 10
        }
    },
    birthday: {
        datetime: {
			dateOnly: true,				// 只需要日期,不需要时间
        	earliest: ...,				// 最早不能早于该时间
            latest: ...					// 最晚不能晚于该时间
        }
    },
    mobile: {
        presence: {
            allowEmpty: false
        },
        type: "string",
        formats: /^1\d{10}$/			// 字符串的格式
    }
});

如果验证规则中存在异步执行的验证器,则在触发验证时应该使用async方法

该方法会返回一个Promise,当验证通过时,该方法完成,否则将失败

validate.async(obj, rule).then(()=>{
    console.log("验证通过");
}).catch((info)=>{
    console.log("验证失败");
});

validate.js在验证时间或日期时,只能是通过时间戳进行验证,而JS对日期的表示方式多种多样,因此在验证日期时间之前,需要将其统一转换为时间戳的形式

使用extend方法即可完成该操作,extend会在验证器即将开始进行验证时,将待验证的值或验证器中的日期时间配置经过parse函数进行转换,之后参与验证的就是转换后的内容

验证结束后,会将错误提示信息中的被转换部分替换为format的返回结果

// 对datetime这个验证器进行扩展
validate.extend(validate.validators.datetime, {
    parse(value, options) {
        // value为待转换的值(可以是待验证的值或者验证器中的日期时间配置),options是验证器的配置
        let formats = ["YYYY-MM-DD HH:mm:ss", "YYYY-M-D H:m:s", "x"];
        if (options.dateOnly) {
            formats = ["YYYY-MM-DD", "YYYY-M-D", "x"];
        }
        return +moment.utc(value, formats, true);
    },
    format(value, options) {
        // value是时间戳
        let format = "YYYY-MM-DD";
        if (!options.dateOnly) {
            format += " HH:mm:ss";
        }
        return moment.utc(value).format(format);
    }
});

通过为validators增加属性可以自定义验证器

验证器的具体处理过程需要封装为一个函数,若验证成功则函数应该返回undefined,否则应该返回错误的提示字符串:

validate.validators.test = function(value){
    // value为待验证的值
    if(...){
		// 验证成功
        return;
    }else{
        // 验证失败
        return "err";
    }
}

使用自定义验证器时,只需要将在待验证的属性值中将对应验证器的值设置为true即可

validate.validate(obj, {
    name: {
        test: true
    }
});

访问器和虚拟字段

访问器

在定义模型时,可以为模型设置访问器属性,通过访问器,可以实现对获取或者设置的数据进行格式转换

例如,数据库中存储的日期格式为时间戳,但获取时希望得到的是字符串,此时就可以使用访问器属性get

sequelize.define("Admin", {
    birthday: {
        type: DataTypes.INTEGER,
        get(){
            const timestamp = this.getDataValue("birthday");
            return moment(timestamp).format("YYYY-MM-DD");
        }
    }
});

虚拟字段

在定义模型时,可以为模型设置虚拟字段,虚拟字段并不会作为表的字段存在,虚拟字段仅已经在获取到表的记录时,对记录中的某些字段进行计算后形成新的字段并加入到模型实例中

sequelize.define("Admin", {
    birthday: {
        type: DataTypes.INTEGER,
        get(){
            const timestamp = this.getDataValue("birthday");
            return moment(timestamp).format("YYYY-MM-DD");
        }
    },
    age: {
        type: DataTypes.VIRTUAL,	// 表中并不会包含age字段,因为它是根据真实字段计算得来的
        get(){
            const now = moment.utc();
            const birth = moment.utc(this.birthday);
            return now.diff(birth, "y");
        }
    }
});

日志记录

使用第三方库log4js可以很方便地进行日志记录

日志分类(level):log4js将日志划分出了以下几个等级:

image.png

具有更低的日志等级的日志会将大于等于它的日志等级的日志信息记录下来

日志分类(category):log4js允许开发者可以对日志的类型进行分类,例如:sql日志、请求日志等

日志出口(appender):规定将日志内容输出到哪里,将日志书写为何种格式

具体使用

通过导入log4js库得到log4js对象,调用log4js对象的getLogger方法可以获得一个日志记录对象logger

调用getLogger方法时可以传入日志的分类(字符串),不传入则表示日志分类为默认日志

logger对象的日志等级默认为off,通过logger.level属性可以设置logger的日志等级

logger对象中包含需对方法,例如all、info、off等,通过调用这些方法来书写对应等级的日志

const log4js = require("log4js");

const logger = log4js.getLogger();			// default分类的日志记录对象

logger.level = "all";

logger.info("info log content");
logger.off("off log content");

默认情况下,书写的日志内容会输出到控制台之中,即日志出口为控制台

通过log4js对象的configuer方法可以设置日志的分类与出口

log4js.configure({
    appenders: {							// 设置日志内容的出口
        sql: {								// sql日志的出口
            type: "file",					// 写入到文件之中
            filename: "./logging.log",		// 写入的文件的位置
            layout: {						// 设置日志的格式
                type: "pattern",			// 设置日志内容的格式为pattern配置项中的格式
				// 自定义日志内容的格式
                // %d日期与时间,后跟{}可进一步设置日期的格式、%p日志的级别、%c日志的分类、%m日志的内容、%n换行
                pattern: "[%d{yyyy-MM-dd hh:mm:ss}] %p %c %m%n"
            },
            maxLogSize: 1024 * 1024,		// 设置日志文件的最大容量(单位:字节),内容超出时会自动将旧内容备份到新文件中
            keepFileExt: true,				// 设置备份的日志文件的后缀名为主日志文件的后缀名
            daysToKeep: 1					// 保留最近几天内的日志文件,过早的日志文件会被删除,值为0时表示全部保留
        },
        default: {							// 定义默认出口
            type: "dataFile"				// 写入到文件之中,但相比"file",日志文件的文件名中会自动加入日志被记录的日期
        }
    },
    categories: {							// 定义日志分类
        sql: {								// 定义sql日志
            appenders: ["sql"],				// 使用appenders中的sql配置作为出口配置
            level: "all"					// 设置日志等级
        },
        default: {							// 定义默认日志
			appenders: ["sql"],
            level: "all"
        }
    }
});

定义完分类后,就可以通过log4js对象创建出相应分类的logger对象来书写日志

const sqlLogger = log4js.getLogger("sql");	// 创建sql日志记录对象

sqlLogger.info("hello");

为避免程序的意外退出导致日志内容没有被完整记录,建议在程序退出之前强制将日志完整地写入到出口之中,使用log4js.shutdown方法即可强制写完日志内容

process.on("exit", ()=>{
    log4js.shutdown();
});