神奇的 http-modular 魔法,让前端不用封装接口
最近突发奇想,研究出了一个神奇的“编程魔法”,我把这个思想封装成了一个 Node.js 库,叫做 http-modular。
这个想法的核心是,将服务端的 HTTP 接口转换成符合 ESM 规范的 JavaScript 代码,然后将它直接通过浏览器 import 进来,调用其中的函数,得到返回结果。
有经验的同学应该大致上明白,这个其实类似于传统的 RPC,但是它的区别是,因为浏览器原生支持 ESM import,因此我们根本不用写任何前端侧的桥接代码,节省了前端的工作。
光说可能你还体会不深,我们通过一个项目来理解。
通过 http-modular 实现服务端KV存储
过去的流程大概是这样:
- 在 AirCode 写接口并调试上线
- 在前端封装 Storage API,然后再 Storage API 中通过 fetch 或者 axios 调用服务端接口。
但是,现在有了 http-modular,我们只需要步骤 1, 不需要步骤 2。
具体怎么做呢?请看——
首先我们在 AirCode 上创建一个项目,在项目中创建一个云函数 index.mjs
。
我们在 index.mjs
中添加如下代码:
const table = aircode.db.table('storage');
async function setItem(key, value) {
return await table.where({key}).set({value}).upsert(true).save();
}
async function getItem(key) {
const res = await table.where({key}).findOne();
return res?.value;
}
async function removeItem(key) {
return await table.where({key}).delete();
}
async function clear() {
return await table.drop();
}
这样我们就用 AirCode 自带的数据库实现了 kv 存储的基本功能。
接下来,我们安装依赖包 http-modular
,然后将这个云函数的模块导出函数写成:
export default modular({
setItem,
getItem,
removeItem,
clear,
}, config.aircode);
完整代码如下:
import aircode from 'aircode';
import {config, modular} from 'http-modular';
const table = aircode.db.table('storage');
async function setItem(key, value) {
return await table.where({key}).set({value}).upsert(true).save();
}
async function getItem(key) {
const res = await table.where({key}).findOne();
return res?.value;
}
async function removeItem(key) {
return await table.where({key}).delete();
}
async function clear() {
return await table.drop();
}
export default modular({
setItem,
getItem,
removeItem,
clear,
}, config.aircode);
然后我们点 Deploy 按钮发布这个云函数,这样我们得到了一个接口: z55f1hze2s.us.aircode.run/storage
前端不需要任何封装了,直接这么写:
打开控制台,你就能看到正确的输入输出。
你看,这样我们就得到了一个非常方便的,只写服务端,不用封装前端的API。
实现鉴权
有同学说,你这么写的确是方便,但是如果我要添加复杂一些的功能,比如鉴权逻辑,那么该怎么办呢?
这个其实也简单,只需要一点函数式编程的技巧,我们修改一下上面的代码:
import aircode from 'aircode';
import {config, context, modular} from 'http-modular';
import {sha256 as hash} from 'crypto-hash';
const auth = context(async (ctx) => {
const origin = ctx.headers.origin;
const referer = ctx.headers.referer;
const xBucketId = ctx.headers['x-bucket-id'];
if(origin !== 'https://code.devrank.cn') {
throw new Error(JSON.stringify({error: {reason: '非法访问'}}));
}
if((!xBucketId && (!referer || !referer.includes('?projectId')))) {
throw new Error(JSON.stringify({error: {reason: '缺少projectId,需要在HTML中添加<meta name="referrer" content="no-referrer-when-downgrade"/>,Safari浏览器请取“消阻止跨站跟踪选项”。'}}));
}
const bucket = await hash(xBucketId || (referer && referer.split('?projectId=')[1]));
return aircode.db.table(`storage-${bucket}`)
});
const setItem = context(auth, async (table, key, value) => {
return await table.where({key}).set({value}).upsert(true).save();
});
const getItem = context(auth, async (table, key) => {
const res = await table.where({key}).findOne();
return res?.value;
});
const removeItem = context(auth, async (table, key) => {
return await table.where({key}).delete();
});
const clear = context(auth, async (table) => {
return await table.drop();
});
export default modular({
setItem,
getItem,
removeItem,
clear,
}, config.aircode);
只有当鉴权满足时,auth 才会返回数据表。
我们引入 http-modular
的一个辅助函数 context
,它是一个高阶函数,第一个参数是一个函数,负责处理请求的 context 并将处理结果作为第一个参数传给后面的云函数,客户端返回的其他参数则作为后续参数也传递给云函数。
在使用 context 方法时,我们也可以缺省 context 处理函数,只传一个云函数,那样的话,context 将作为第一个参数传给云函数,比如:
const echo = context(ctx => ctx.request.body);
<meta name="referrer" content="no-referrer-when-downgrade"/>
http-modular 是如何工作的?
实际上,http-modular 的实现原理非常简单,就是将 HTTP 接口封装成 ES Module 规范的 JavaScript 代码。
我们用浏览器直接请求,看一下发布的 AirCode 云函数的内容: z55f1hze2s.us.aircode.run/storage
你会看到,这就是一段 JS 代码:
function makeRpc(url, func) {
return async(...args) => {
const ret = await fetch(url, {
method: 'POST',
body: JSON.stringify({func, args}),
headers: {
'content-type': 'application/json'
}
});
const type = ret.headers.get('content-type');
if(type && type.startsWith('application/json')) {
return await ret.json();
} else if(type && type.startsWith('text/')) {
return await ret.text();
}
return await ret.arrayBuffer();
}
}
export const setItem = makeRpc('https://z55f1hze2s.us.aircode.run/storage', 'setItem');
export const getItem = makeRpc('https://z55f1hze2s.us.aircode.run/storage', 'getItem');
export const removeItem = makeRpc('https://z55f1hze2s.us.aircode.run/storage', 'removeItem');
export const clear = makeRpc('https://z55f1hze2s.us.aircode.run/storage', 'clear');
这段代码非常好理解,它就是把各个方法封装成 RPC 调用。
在服务端, http-modular 做的事情也很简单,主体代码也就60多行:
const sourePrefix = `
function makeRpc(url, func) {
return async(...args) => {
const ret = await fetch(url, {
method: 'POST',
body: JSON.stringify({func, args}),
headers: {
'content-type': 'application/json'
}
});
const type = ret.headers.get('content-type');
if(type && type.startsWith('application/json')) {
return await ret.json();
} else if(type && type.startsWith('text/')) {
return await ret.text();
}
return await ret.arrayBuffer();
}
}
`;
function buildModule(rpcs, url) {
let source = [sourePrefix];
for(const key of Object.keys(rpcs)) {
source.push(`export const ${key} = makeRpc('${url}', '${key}');`);
}
return source.join('\n');
}
const _ctx = Symbol('ctx');
export function context(checker, func) {
if(!func) {
func = checker;
checker = ctx => ctx;
}
const ret = async (context, ...rest) => {
const ctx = await checker(context);
return await func(ctx, ...rest);
};
ret[_ctx] = true;
return ret;
}
export function modular(rpcs, {getParams, getUrl, getContext, setContentType, setBody}) {
return async function (...rest) {
const ctx = getContext(...rest);
const method = ctx.request?.method || ctx.req?.method;
if(method === 'GET') {
setContentType(...rest);
return setBody(buildModule(rpcs, getUrl(...rest)), ...rest);
} else {
const {func, args = []} = await getParams(...rest);
const f = rpcs[func];
if(f?.[_ctx]) {
return setBody(await f(ctx, ...args), ...rest);
}
return setBody(await f(...args), ...rest);
}
};
}
export default modular;
也就是当 HTTP 请求的 method 为 GET
的时候,生成 JavaScript 代码发回给客户端,而当 HTTP 请求的 method 为其他类型的时候,根据 func 和 args 参数执行对应的函数。
http-modular 针对主流的 Node.js 框架做了适配,它默认的 config 支持以下各个框架或云函数平台:
每个框架或平台具体如何使用,详见 GitHub 仓库 中的文档。
这个思路虽然简单,是不是有趣且有用呢?你可以自己动手试一试。
有任何问题欢迎在下方留言评论❤️
转载自:https://juejin.cn/post/7265625144764334121