如何搭建一个node的后端接口项目(持续更新中...)

1,859 阅读4分钟

从零创建第一个node后端接口项目

本node项目作者是用于开源项目的纯接口后端,所以没有考虑静态页面相关内容

1、搭建egg脚手架

官方使用npm来搭建脚手架

mkdir egg-example && cd egg-example
npm init egg --type=simple
npm i

我一般比较喜欢用yarn

mkdir egg-example && cd egg-example
yarn create egg --registry https://registry.npm.taobao.org/
// --registry 用于切换淘宝镜像源
yarn

2、安装插件

egg-mysql

数据库使用mysql时需要安装插件egg-mysql来连接数据库

yarn add egg-mysql

egg-validate

对前端请求参数数据进行校验,如果满足则会执行后续代码,反之则报错并给出错误信息

yarn add egg-mysql

3、连接数据库

前提条件是已经安装egg-mysql插件

(1) 添加配置

在conifg目录下新增不同环境下不同配置文件,例如config.local或者config.prod,如果存在则新增(相关问题查看官方网站)

也有人建议写在config.default.js中,具体放置在何处根据个人习惯存放

文件中添加如下内容:

如果项目中存在访问多个不同数据库则使用下面的方法

exports.mysql = {
    clients: {
        // clientId, 获取client实例,需要通过 app.mysql.get('clientId') 获取
        db1: {
        // host
        host: 'mysql1.com',
        // 端口号
        port: '3307',
        // 用户名
        user: 'test_user',
        // 密码
        password: 'test_password',
        // 数据库名
        database: 'test',
        },
        db2: {
        // host
        host: 'mysql2.com',
        // 端口号
        port: '3307',
        // 用户名
        user: 'test_user',
        // 密码
        password: 'test_password',
        // 数据库名
        database: 'test',
        },
        // ...
    },
    // 所有数据库配置的默认值
    default: {
        timezone: '+8:00',
    },

    // 是否加载到 app 上,默认开启
    app: true,
    // 是否加载到 agent 上,默认关闭
    agent: false,
};

如果项目中仅使用了一个数据库则可以简单写为

exports.mysql = {
  // 单数据库信息配置
  client: {
    // host
    host: 'mysql.com',
    // 端口号
    port: '3306',
    // 用户名
    user: 'test_user',
    // 密码
    password: 'test_password',
    // 数据库名
    database: 'test',
  },
  // 是否加载到 app 上,默认开启
  app: true,
  // 是否加载到 agent 上,默认关闭
  agent: false,
};

(2) 操作数据库

如果是多数据源的设置,那么需要使用下面的代码进行访问数据库以及操作数据库

    // 获取指定数据库
    let client1 = this.app.mysql.get('db1');
    // 查找数据库
    let user = await client1.select('test', {
        where: { user_age: age }
    });

如果是单数据源则可以使用下面代码访问数据库以及操作数据库 未尝试过但大体应该没有问题

// 获取指定数据库
    let client1 = this.app.mysql.select('test', {
        where: { user_age: age }
    });

4、接口校验

前提条件是已经安装egg-validate插件

在control目录下的相关control文件中,对某个接口入参进行校验

// 表示age参数需要使用number类型
const apiRule = {
    age: {
        type: 'number',
    }
};

可以配置的属性

  • required: 是否为必须
  • type: 字段属性的类型
  • converType: 使参数转换输入参数的具体类型,支持intnumberstringboolean 以及自定义函数
  • default: 字段的默认值,如果该属性为非必须的且入参时不存在,则使用默认值
  • widelyUndefined: 覆盖options.widelyUndefined,用于将NaN和NULL转换成undefined

type类型包括

  • int
  • nuber
  • date
  • dateTime
  • id
  • boolean
  • bool
  • string
  • email
  • password
  • url
  • enum
  • object
  • array
  • abbr

在接收到接口传入的参数后进行校验,如果校验通过则会执行后续操作,否则将会报错弹出

5、中间件

中间件的文件命名必须与内部函数名称一致,否会无法挂载中间件

中间件文件放在app目录下的middleware文件夹中,如果没有则新增一个

需要在config.default.js的文件中挂载中间件

// 统一错误处理
    config.middleware = ['errorHandler'];

(1)统一错误处理中间件

对所有的错误进行统一处理

module.exports = () => {
    return async function errorHandler(ctx, next) {
        try {
            await next();
        } catch (error) {
            // 控制台输出
            console.error(`发生错误: ${error}`);
            // 所有异常在app上触发一个error事件,框架会记录一条错误日志
            ctx.app.emit('error', error, ctx);
            // status 500 來表示服务器内部错误
            let status = error.status || 500;
            // 如果是500错误,且是生产环境,则统一显示服务器内部错误
            let errorInfo = status === 500 && ctx.app.config.env === 'prod'? '服务器内部错误': error;
            // 改变上下文状态
            ctx.status = status;
            // 从 error 对象独处各个属性,设置到响应中,下面两种中需要根据是否做框架扩展选择其中一个使用
            // 未做框架扩展
            ctx.body = {
                errorInfo,
            };
            // 做框架扩展后
            ctx.response.error(ctx, {
                code: status,
                msg: errorInfo.message,
                data: errorInfo.errors,
            })
        };
    };
}

6、框架扩展

Response

由于需要对返回数据做统一格式所以对框架中Response对象做扩展

app目录下创建extend文件夹并创建response.js文件来对Response文件做扩展

module.exports = {
    /**
     * @desc: 接口调用正常情况的返回数据封装
     * @param {Object} ctx - context上下文
     * @param {string} msg - message 相关信息
     * @param {*} data - 返回的数据
     * @return {*}
     */
    success(ctx, msg, data) {
        ctx.body = {
            code: 0,
            message: msg,
            data,
        };
        ctx.status = 200;
    },
    /**
     * @desc: 处理失败,处理传入的失败原因
     * @param {Object} ctx - contentx上下文
     * @param {Object} res - 返回的状态数据
     * @return {*}
     */
    error(ctx, res) {
        ctx.body = {
            code: res.code,
            message: res.msg,
            data: res.data
        };
        ctx.status = 200
    }
}

然后修改control文件夹下文件中对返回值的处理

    // 原先
    ctx.body = userInfo;
    ctx.status = 200;
    // 扩展后
    ctx.response.success(ctx, "ok", userInfo);

7、安全

CSRF

CSRF(Cross-site request forgery跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。 CSRF 攻击会对网站发起恶意伪造的请求,严重影响网站的安全。

鉴于安全性,未关闭框架内置的防范方案,由于安全性需要前后端均设置相关内容

后台

由于防止在query或者body中需要每次调用是都去写语句获取csrfToken比较麻烦且前端使用的是统一封装的请求接口,所以防止在header中更方便

// config.default.js
// 配置接口通过header传递csrfToken
const userConfig = {
    security: {
        csrf: {
            headerName: 'x-csrf-token',
            // useSession: true, // 默认为 false,当设置为 true 时,将会把 csrf token 保存到 Session 中
            // cookieName: 'csrfToken', // Cookie 中的字段名,默认为 csrfToken
            // sessionName: 'csrfToken',
        },
    },
};

前端

前端使用的是axios的封装接口,且仅需要在post接口需要进行csrf鉴权操作,所以针对性的对post类型的接口做处理

instance.post(realUrl, params, { headers: { 'x-csrf-token': cookie }}).then(response => resolve(response)).catch(error => reject(error));

8、其他

(1) VSCode调试

在vscode调试文件中配置launch.json

{
    "name": "Launch Egg",
    "type": "node",
    "request": "launch",
    "cwd": "${workspaceRoot}",
    "runtimeExecutable": "npm",
    "windows": { "runtimeExecutable": "npm.cmd" },
    "runtimeArgs": [ "run", "debug" ],
    "console": "integratedTerminal",
    "protocol": "auto",
    "restart": true,
    "port": 7001,
    "autoAttachChildProcesses": true
}

(2)配置独立入口地址与端口

config.default.js文件中添加

listen: {
    // 端口
    port: '',
    // IP地址
    hostname: '',
},

(3) ESlint校验

使用脚手架生成的代码框架中的ESlint使用的是2字符空格缩进,我不是很喜欢所以修改项目中eslint对缩进的校验方式,修改为4字符的tab缩进

9、遇到的问题

(1)前后端分离项目第一个接口为post请求时报message: 'invalid csrf token'

这是因为Egg.js默认开启了CSRF的防护,由于前后端分离导致node项目仅用于接口,会导致第一个接口使用post时无法从请求中获取存放在cookie中的csrf的值。

解决办法

  • 关闭csrf的防护
    //config.default.js
    security: {
        csrf: {
            enable: false,
        }
    }
  • 在前端加载的首页中加入一个测试服务器通讯的get接口用于获取csrf的值