最近突发奇想,研究出了一个神奇的"编程魔法",我把这个思想封装成了一个 Node.js 库,叫做 http-modular。
这个想法的核心是,将服务端的 HTTP 接口转换成符合 ESM 规范的 JavaScript 代码,然后将它直接通过浏览器 import 进来,调用其中的函数,得到返回结果。
有经验的同学应该大致上明白,这个其实类似于传统的 RPC,但是它的区别是,因为浏览器原生支持 ESM import,因此我们根本不用写任何前端侧的桥接代码,节省了前端的工作。
光说可能你还体会不深,我们通过一个项目来理解。
通过 http-modular 实现服务端KV存储
假设我们要在码上掘金写一个应用,但是想将数据存储在数据库,我们可以通过 AirCode 写一个存储接口然后部署上线,再通过前端封装成 storage 的 API。
过去的流程大概是这样:
- 在 AirCode 写接口并调试上线
- 在前端封装 Storage API,然后再 Storage API 中通过 fetch 或者 axios 调用服务端接口。
但是,现在有了 http-modular,我们只需要步骤 1, 不需要步骤 2。
具体怎么做呢?请看------
首先我们在 AirCode 上创建一个项目,在项目中创建一个云函数 index.mjs
。
我们在 index.mjs
中添加如下代码:
js
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
,然后将这个云函数的模块导出函数写成:
js
export default modular({
setItem,
getItem,
removeItem,
clear,
}, config.aircode);
完整代码如下:
js
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
我们在码上掘金里创建一个项目 code.juejin.cn/pen/7265857...
前端不需要任何封装了,直接这么写:
js
import * as storage from 'https://z55f1hze2s.us.aircode.run/storage';
await storage.setItem('url', 'https://juejin.cn');
console.log(await storage.getItem('url'));
await storage.removeItem('url');
console.log(await storage.getItem('url'));
打开控制台,你就能看到正确的输入输出。
你看,这样我们就得到了一个非常方便的,只写服务端,不用封装前端的API。
实现鉴权
有同学说,你这么写的确是方便,但是如果我要添加复杂一些的功能,比如鉴权逻辑,那么该怎么办呢?
这个其实也简单,我们用一点点函数式编程的思想,修改一下上面的代码:
js
import aircode from 'aircode';
import {config, modular} from 'http-modular';
import {sha256 as hash} from 'crypto-hash';
function auth(context) {
const origin = context.headers.origin;
const referer = context.headers.referer;
const xBucketId = context.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 = hash(xBucketId || (referer && referer.split('?projectId=')[1]));
return aircode.db.table(`storage-${bucket}`)
}
async function setItem(key, value, context) {
return await auth(context).where({key}).set({value}).upsert(true).save();
}
async function getItem(key, context) {
const res = await auth(context).where({key}).findOne();
return res?.value;
}
async function removeItem(key, context) {
return await auth(context).where({key}).delete();
}
async function clear(context) {
return await auth(context).drop();
}
export default modular({
setItem,
getItem,
removeItem,
clear,
}, config.aircode);
在上面的代码里,我们添加了一个负责校验授权的 auth 函数,它有两个作用,一个保证调用的 origin 是来自马上掘金,二是根据 referer 来生成唯一的 hash,这样保证每个马上掘金项目一个独立的存储空间,不会混起来。
只有当鉴权满足时,auth 才会返回数据表。
为了跟踪 referer,我们在刚才的码上掘金项目中,HTML 的 head 标签里添加一个设置:
html
<meta name="referrer" content="no-referrer-when-downgrade"/>
这样的话,我们就可以安全地在码上掘金项目里存取数据了。
http-modular 是如何工作的?
实际上,http-modular 的实现原理非常简单,就是将 HTTP 接口封装成 ES Module 规范的 JavaScript 代码。
我们用浏览器直接请求,看一下发布的 AirCode 云函数的内容: z55f1hze2s.us.aircode.run/storage
你会看到,这就是一段 JS 代码:
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 做的事情也很简单,主体代码也就40多行:
js
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');
}
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') {
// eslint-disable-next-line no-unsafe-optional-chaining
setContentType(...rest);
return setBody(buildModule(rpcs, getUrl(...rest)), ...rest);
} else {
const {func, args} = await getParams(...rest);
return setBody(await rpcs[func](...(args||[]), ctx), ...rest);
}
};
}
export default modular;
也就是当 HTTP 请求的 method 为 GET
的时候,生成 JavaScript 代码发回给客户端,而当 HTTP 请求的 method 为其他类型的时候,根据 func 和 args 参数执行对应的函数。
http-modular 针对主流的 Node.js 框架做了适配,它默认的 config 支持以下各个框架或云函数平台:
每个框架或平台具体如何使用,详见 GitHub 仓库 中的文档。
我把上面例子的服务端代码也放在了 GitHub 仓库,有兴趣的同学也可以一试,点击项目 README 文件里的一键发布,就可以把代码发布到你自己的 AirCode 账号哦。
这个思路虽然简单,是不是有趣且有用呢?你可以自己动手试一试。
有任何问题欢迎在下方留言评论❤️