Browse Source

Gitbook Auto Published

willin 6 years ago
parent
commit
e9b06cda07
2 changed files with 253 additions and 0 deletions
  1. 1 0
      _sidebar.md
  2. 252 0
      experience/advanced/webapp-proxy.md

+ 1 - 0
_sidebar.md

@@ -77,6 +77,7 @@
     - [元编程构造优雅解决方案](experience/advanced/meta.md)
     - [Electron桌面应用](experience/advanced/desktop-app.md)
     - [混合Web应用实践](experience/advanced/mixed-project.md)
+    - [基于接口快速搭建前后分离项目](experience/advanced/webapp-proxy.md)
 - 思想篇
   - 能力
     - [好员工的定义](mind/capability/define-good-employee.md)

+ 252 - 0
experience/advanced/webapp-proxy.md

@@ -0,0 +1,252 @@
+# 基于 API 快速搭建前后分离的项目
+
+以我们目前的开放平台为例。
+
+Sandbox(沙盒)的在线文档可以在: <https://stagingdoc.airdwing.com/> 找到。
+
+Node.js版本SDK:<https://github.com/AirDwing/node-airx-sdk>
+
+## SDK 使用
+
+后端接口采用`@airx/sdk`来完成`AirX Open API`的反向代理.
+
+```js
+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` 并进行登录设备绑定.
+
+返回结果如下:
+
+```js
+{
+  status: 1,
+  data: {
+    guid: 'xxxx'
+  }
+}
+```
+
+## 项目目录结构
+
+- server/ 服务器后端源码目录
+- app/ 前端应用源码目录
+- dist/ 前端应用通过`webpack`等工具进行打包压缩, 将静态文件存放的目录
+
+建议的 `dist` 目录结构
+
+- index.html
+- 404.html 和 403,500... 等其他相关错误的html
+- static/ 文件目录,或分类目录存放 css/js/图片等静态资源
+
+## 核心代码
+
+### 实现前后端分离
+
+位于:`server/server.js`
+
+```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`
+
+
+```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`
+
+```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`
+
+```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> 找到。