基于 API 快速搭建前后分离的项目

以我们目前的开放平台为例。

Sandbox(沙盒)的在线文档可以在: https://stagingdoc.airdwing.com/ 找到。

Node.js版本SDK:https://github.com/AirDwing/node-airx-sdk

SDK 使用

后端接口采用@airx/sdk来完成AirX Open API的反向代理.

const SDK = require('@airx/sdk');

const sdk = new SDK({
  SecretId: 'xxxx',
  SecretKey: 'xxxx',
  // 访问的域名:
  Domain: 'staging.airdwing.com',
  // 是否启用HTTPS:
  Secure: true
});

(async () => {
  const result = await sdk.get('/user/check', {
    username: '13212341234'
  });
  console.log(result);
})();

前端请求参数

签名相关参数

签名相关参数不用通过前端传递,防止AK/SK的泄露,签名交给Node.js后端反代应用程序处理,所以以下几个签名相关参数不用传递:

  • Nonce
  • SecretId
  • Signature
  • SignatureMethod
  • Timestamp

登录Auth Token

由于反代应用程序支持 Cookie Session, 所以前端不用存储 Auth Token,也免去了 Auth Token被截获的风险.所以如果接口需要以下参数,不用传递:

  • auth

登录设备相关参数

每个会话都会生成一个随机唯一的模拟登录设备id, 如果接口需要以下参数, 不用传递:

  • guid
  • device

但需要注意的是, 你需要调用 /guid 接口来查询本次会话的 guid 并进行登录设备绑定.

返回结果如下:

{
  status: 1,
  data: {
    guid: 'xxxx'
  }
}

项目目录结构

  • server/ 服务器后端源码目录
  • app/ 前端应用源码目录
  • dist/ 前端应用通过webpack等工具进行打包压缩, 将静态文件存放的目录

建议的 dist 目录结构

  • index.html
  • 404.html 和 403,500... 等其他相关错误的html
  • static/ 文件目录,或分类目录存放 css/js/图片等静态资源

核心代码

实现前后端分离

位于:server/server.js

app.use(async (ctx, next) => {
  ctx.api = await swagger();
  const path = ctx.api.paths[ctx.path];
  // ! 仅供开发测试, 允许跨域操作很危险
  if (ENV === 'development') {
    ctx.set('Access-Control-Allow-Origin', '*');
  }
  if (path === undefined) {
    // 前后端分离, 处理前端相关静态文件
    try {
      await send(ctx, ctx.path, { root: `${__dirname}/../dist` });
    } catch (err) {
      ctx.status = 404;
      // 注意要添加 404.html 到 dist 目录
      // await send(ctx, '/404.html', { root: `${__dirname}/../dist` });
    }
    return;
  }
  await next();
});

处理接口反向代理

位于:server/server.js

app.use(async (ctx) => {
  // 提供 guid 查询接口
  if (ctx.path === '/guid') {
    let guid = ctx.session.guid;
    if (isEmpty(guid)) {
      guid = uuid();
      ctx.session.guid = guid;
    }
    ctx.status = 200;
    ctx.body = { status: 1, data: { guid } };
    return;
  }
  // 处理后端接口
  // 封装sdk请求
  if (ctx.path === '/upload') {
    // 处理上传
    await upload(ctx);
  } else {
    // 处理其他接口
    await others(ctx);
  }
});

从远程 Swagger 取得参数信息

位于:server/lib/swagger.js

const getSwagger = async () => {
  const result = await request({
    method: 'GET',
    url: apiOptions.doc,
    timeout: 5000
  });
  const paths = result.paths;
  const app = {
    host: result.host,
    paths: Object.keys(paths).reduce((p, x) => {
      /* eslint no-param-reassign:0 */
      const method = Object.keys(paths[x])[0];
      p[x] = paths[x][method].parameters.map(t => t.name);
      return p;
    }, {})
  };
  return app;
};

module.exports = async () => {
  let app = await redis.get('app');
  if (isEmpty(app)) {
    app = await getSwagger();
    await redis.set('app', app);
  }
  return app;
};

通过 Swagger 配置文件可以将接口列表,及各个接口需要的参数,以便于后边再接口调用的时候有针对性的处理参数。

处理普通接口请求

位于:server/handler/others.js

module.exports = async (ctx) => {
  const method = ctx.request.method.toLowerCase();
  const receivedParams = method === 'get' ? ctx.query : await parse(ctx.req);

  const sdk = new SDK({
    SecretId: apiOptions.ak,
    SecretKey: apiOptions.sk,
    Domain: ctx.api.host,
    Secure: apiOptions.scheme === 'https'
  });

  // 处理请求参数
  const params = ctx.api.paths[ctx.path];
  if (params.indexOf('auth') !== -1) {
    // 处理需要 登录 的接口
    const auth = ctx.session.auth;
    if (isEmpty(auth)) {
      ctx.status = 200;
      ctx.body = { status: 0, code: 401 };
      return;
    }
    const ttl = ~~ctx.session.ttl;
    // 处理登录超时(1小时),提前10分钟重新获取auth
    if (ttl - getTimestamp() < 600) {
      const tmpParams = JSON.parse(ctx.session.params);
      const login = await sdk.post('/user/login', tmpParams);
      const loginResult = doLogin(ctx, login, tmpParams);
      // 密码被修改等无法登录
      if (loginResult === -1) {
        ctx.status = 200;
        ctx.body = { status: 0, code: 401 };
        return;
      }
    }

    receivedParams.auth = auth;
  }

  if (params.indexOf('guid') !== -1) {
    // 处理需要 guid 的接口
    let guid = ctx.session.guid;
    if (isEmpty(guid)) {
      guid = uuid();
      ctx.session.guid = guid;
    }
    receivedParams.guid = guid;
  }

  if (params.indexOf('device') !== -1) {
    // 处理需要登录设备名称的接口
    receivedParams.device = 'AirX网页版';
  }

  if (params.indexOf('password') !== -1) {
    // 处理需要 authcode加密 的接口
    receivedParams.key = randStr(6);
    receivedParams.passwod = encode(receivedParams.passwod, receivedParams.key);
  }

  const result = await sdk[method](ctx.path, receivedParams);
  // 记录登录信息
  if (ctx.path === '/user/login') {
    doLogin(ctx, result, receivedParams);
  }
  ctx.status = 200;
  ctx.body = result;
};

该实例代码可以在: https://github.com/AirDwing/node-airx-webapp 找到。