likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 20

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

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

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

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

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

过去的流程大概是这样:

  1. 在 AirCode 写接口并调试上线
  2. 在前端封装 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
评论
请登录