# 基于 API 快速搭建前后分离的项目 以我们目前的开放平台为例。 Sandbox(沙盒)的在线文档可以在: 找到。 Node.js版本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; }; ``` 该实例代码可以在: 找到。