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 方法来遍历。

那么这里为什么不用 forEachmap 呢?纯粹是个人喜好,for...ofES6 标准带来的新的遍历方法,可以支持 continuebreak 中断。

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

如果用 forEachmap 函数,是不能直接使用 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 则表示其他异常返回。

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

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