神奇的 http-modular 魔法,让前端不用封装接口

最近突发奇想,研究出了一个神奇的"编程魔法",我把这个思想封装成了一个 Node.js 库,叫做 http-modular

这个想法的核心是,将服务端的 HTTP 接口转换成符合 ESM 规范的 JavaScript 代码,然后将它直接通过浏览器 import 进来,调用其中的函数,得到返回结果。

有经验的同学应该大致上明白,这个其实类似于传统的 RPC,但是它的区别是,因为浏览器原生支持 ESM import,因此我们根本不用写任何前端侧的桥接代码,节省了前端的工作。

光说可能你还体会不深,我们通过一个项目来理解。

通过 http-modular 实现服务端KV存储

假设我们要在码上掘金写一个应用,但是想将数据存储在数据库,我们可以通过 AirCode 写一个存储接口然后部署上线,再通过前端封装成 storage 的 API。

过去的流程大概是这样:

  1. 在 AirCode 写接口并调试上线
  2. 在前端封装 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 账号哦。

这个思路虽然简单,是不是有趣且有用呢?你可以自己动手试一试。

有任何问题欢迎在下方留言评论❤️

相关推荐
她似晚风般温柔7892 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
Jiaberrr3 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy3 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
Ylucius3 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
200不是二百4 小时前
Vuex详解
前端·javascript·vue.js
LvManBa4 小时前
Vue学习记录之三(ref全家桶)
javascript·vue.js·学习
深情废杨杨4 小时前
前端vue-父传子
前端·javascript·vue.js
司篂篂6 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客6 小时前
pinia在vue3中的使用
前端·javascript·vue.js
Jiaberrr8 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选