Skip to content

上一章接口服务器,我们实现了一个异常简单的接口服务器。

可能很多人会感觉有点不真实的感觉,接口这么简单吗?没错,就这么简单。

我们在真实项目的前后端对接中,调用接口,拿到数据,就是如此而已。

只不过,在接口的 判断,数据的 处理 和 获取,都加入了更为复杂和完善的 逻辑 而已。

本章呢,我们就来把接口服务器进一步完善一下,不然写那么多的 if判断,也着实麻烦。

问题来了,如何改造呢?首先,接口的业务逻辑肯定不能直接放在我们的 server.js 文件中,不然这文件写到后面,不得几千上万行啊,不行。

所以,我们可以将不同的接口,用不同的文件来表示,请求什么接口,就执行该文件导出的函数就好了。

js
// /apis/getUserScore.js 文件

export default () => {
    return {
        code: 0,
        msg: '查询数据成功',
        data: {
            id: 1,
            name: 'chensuiyi',
            score: 100
        }
    };
};

这是我们将 获取用户分数接口 从 server.js 文件中分离后的代码。

bash
CoolApi
├── apis
│   └── getUserScore.js
├── public
│   ├── index.html
│   ├── index.css
│   └── favicon.ico
├── package.json
└── server.js

此时的项目结构如上。

js
// server.js 文件
import { createServer } from 'node:http';
import { readFileSync, existsSync } from 'node:fs';
import { lookup } from 'mime-types';
import { default as getUserScore } from './apis/getUserScore.js';

const server = createServer((req, res) => {
    if (req.url.startsWith('/api/') === true) {
        if (req.url === '/api/getUserScore') {
            res.setHeader('Content-Type', 'application/json');
            const apiResult = getUserScore();
            res.end(JSON.stringify(apiResult));
        }
    } else {
        if (req.url === '/') {
            req.url = '/index.html';
        }
        if (existsSync(`./public/${req.url}`) === true) {
            res.setHeader('Content-Type', lookup(req.url));
            res.end(readFileSync(`./public/${req.url}`));
        } else {
            res.writeHead(404, { 'Content-Type': 'text/plain' });
            res.end('404 Not Found');
        }
    }
});

server.listen(3000, '127.0.0.1', () => {
    console.log('服务已启动,监听端口为:3000');
});

那么将接口分离后到 /apis 目录中后,在 server.js 文件中把接口函数导出,再执行,最后再返回,效果也是一样的。

这样,不同的接口可以用不同的文件来表达,代码管理起来方便了不少。

但是...,不知道有没有人发现,这样还是很麻烦,if 判断还是没少啊。

所以我们下一步呢,就是要把 if判断 也 干掉。

我们可以这样设想,当请求到达的时候,我们根据请求的接口名称,自动去获取同名的文件,执行该文件内的函数,获得结果后赋值给 apiResult 变量,最后再通过 res.send() 返回给浏览器。

要获取文件名,这里就少不了读取文件和目录的内置模块 fs,要自动匹配接口名称,这里就要用到哈希结构,也就是对象了。

说起来比较抽象,直接看下面的写法。

js
import { readdirSync } from 'node:fs';
const files = readdirSync('./apis', { recursive: true });
console.log('🔥[ files ]-3', files);

我们创建一个新文件 routes.js,含义就是 接口路由。

此时的项目结构如下:

bash
CoolApi
├── apis
│   └── getUserScore.js
├── public
│   ├── index.html
│   ├── index.css
│   └── favicon.ico
├── package.json
├── routes.js
└── server.js

前端有 页面路由,后端有 接口路由。

每个页面路由对应着一个个页面,每个接口路由对应着一个个接口。

如果没有把握一次性把功能搞定,那就每实现一步就打印出来看看,毕竟眼见为实,我们才好知道下一步怎么做。

bash
D:\codes\chensuiyi\nodejs-full-stack>node routes.js
🔥[ files ]-3 [ 'getUserScore.js' ]

打印的结果如上,获得的是一个接口文件名称数组。

js
import { readdirSync } from 'node:fs';
const files = readdirSync('./apis', { recursive: true });
// 接口映射对象
const apiMaps = {};
for (let file of files) {
    if (file.endsWith('.js')) {
        apiMaps[file.replace('.js', '').replace(/\\+/, '/')] = await import(
            `./apis/${file}`
        );
    }
}

export { apiMaps };

导出结果如下:

js
{
  getUserScore: [Module: null prototype] { default: [Function: default] }
}

files 获取到的值是一个 文件名称数组,我们可以使用 for...of 方法来遍历。

那么这里为什么不用 forEach,map 呢?纯粹是个人喜好,for...of 是 ES6 标准带来的新的遍历方法,可以支持 continue 和 break 中断。

自 Node.js v14.8.0 开始,已经支持顶层 await。也就是说,我们不需要写 async,就能直接使用 await。

如果用 forEach 或 map 函数,是不能直接使用 await 的,因为它们需要在函数中处理逻辑,已经不处于 顶层 了,这就是使用 for...of 的好处了。

我们用一个 Object 类型的变量 apiMaps 用来集合我们所有的接口。

同时呢,把接口文件的方法,通过 import() 动态导入,复制给对应的文件名属性。

这样,我们就可以从 apiMaps 中获取跟接口请求路径同名的函数,执行这个函数,获得对应的接口返回数据了。

js
import { createServer } from 'node:http';
import { readFileSync, existsSync } from 'node:fs';
import { lookup } from 'mime-types';
import { apiMaps } from './routes.js';

const server = createServer((req, res) => {
    if (req.url.startsWith('/api/') === true) {
        const apiResult = apiMaps[req.url.replace('/api/', '')].default();
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(apiResult));
    } else {
        if (req.url === '/') {
            req.url = '/index.html';
        }
        if (existsSync(`./public/${req.url}`) === true) {
            res.setHeader('Content-Type', lookup(req.url));
            res.end(readFileSync(`./public/${req.url}`));
        } else {
            res.writeHead(404, { 'Content-Type': 'text/plain' });
            res.end('404 Not Found');
        }
    }
});

server.listen(3000, '127.0.0.1', () => {
    console.log('服务已启动,监听端口为:3000');
});

经过我们改造后的 server.js 文件,就变成这样了。

需要注意的是,我们的接口函数是一个 default 默认导出,所以我们在执行函数的时候,也需要通过 default 来执行函数。

请求成功

可以看到,请求也能拿到数据,跟我们之前的功能完全一样。

这么一来,不管我们写多少接口,都不用写麻烦的 if判断 了,偷懒 成功!

最后一步呢,我们再来处理一下,当请求的接口不存在的情况。

js
import { createServer } from 'node:http';
import { readFileSync, existsSync } from 'node:fs';
import { lookup } from 'mime-types';
import { apiMaps } from './routes.js';

const server = createServer((req, res) => {
    if (req.url.startsWith('/api/') === true) {
        const apiName = req.url.replace('/api/', '');
        res.setHeader('Content-Type', 'application/json');
        if (apiMaps[apiName]) {
            const apiResult = apiMaps[apiName].default();
            res.end(JSON.stringify(apiResult));
        } else {
            res.end(JSON.stringify({ code: 1, msg: '接口不存在' }));
        }
    } else {
        if (req.url === '/') {
            req.url = '/index.html';
        }
        if (existsSync(`./public/${req.url}`) === true) {
            res.setHeader('Content-Type', lookup(req.url));
            res.end(readFileSync(`./public/${req.url}`));
        } else {
            res.writeHead(404, { 'Content-Type': 'text/plain' });
            res.end('404 Not Found');
        }
    }
});

server.listen(3000, '127.0.0.1', () => {
    console.log('服务已启动,监听端口为:3000');
});

很简单,判断 apiMaps 中,是否有该接口名称即可。

接口不存在

当我们请求一个不存在的接口的时候,就能返回 接口不存在 的提示信息了。

在我们这个接口返回结构中,我们约定,code === 0 表示正常返回,code !== 0 则表示其他异常返回。

好了,接口完善后,后面的章节,我们就要开始学习如何操作数据库,如何发送邮件,如何实现登录注册,如何实现文章的增删改查了。

何以解忧,唯有代码。不忘初心,方得始终。