后端接口迁移(从 webapi 到 openapi)前端经验总结
此文已由作者张磊授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
前情提要
以前用的是 webapi 现在统一切成 openapi,字段结构统统都变了
接入接口 20+,涉及模块的创建等主要流程。
页面基本无改,仅有一个新需求,创建时新增一个字段
其他依赖接口需要接入模块
预想解决方案
20+ 接口,如果根据返回值去更改页面,由于返回值整个结构都变掉了,修改起来这个工程量吃不消,再加上回测,基本上不可能在一个迭代内完成,所以需要一个新的方案。想一下变的是数据结构,不变的是什么,不变的是业务,那么方案就出来了,对接口的发送和数据的接收阶段对数据进行转换不就完美解决了。比如:
// 原请求service.xxx = function (ops) { };// 新请求// 重点都在 transform 这个函数上service.xxx = function (ops) { const newOps = transform.xxx.pre(ops); return request.xxxNew(newOps).then((res) => { return transform.xxx.post(res); }); };
采用这样的方案,修改的范围会大大的缩减,基本集中在对接口的修改,对数据的转换上。
实施
页面的接口有些是原子性的,比如 删除 等,仅仅传递少量参数,转换即可。即使是创建,字段比较多,但仅仅对 发送数据阶段进行转换 即可。 问题是出在 返回值 上。以前返回的字段,现在没有返回的,就需要找到该字段,尽量复原有结构。这一阶段需要涉及到和 openapi 开发的反复沟通,确认每一个值怎么取,确认哪些字段是没有用的,总之尽量把数据结构给复原回来。 这里需要特殊说明
列表页的数据结构不完整,可以对部分数据用异步添加的方式,将数据结构补完,这样用户可以尽早的看到列表。 但是详情页,必须是完整的数据结构,因为之前的实现方案,统统没考虑数据延迟拿到时页面的渲染处理,如果不一次性返回所有的数据,出问题的几率就很高。举例
// 列表的接口,需要特别传递 refresh 函数 service.list = function (refresh) { return request.xx().then((list) => { list.forEach((item) => { // 异步添加 request.xx2().then((detail) => { Object.assign(item, detail); refresh(); }); }); // 首先返回主体内容 return list; }); };// 详情的接口 service.detail = function () { return Promise.all( request.xx(), request.xx2(), ).then((detail, help1) => { Object.assgin(detail, help1); return request.xx3().then((help2) => { Object.assgin(detail, help2); return detail; // 注意这里 return 的 detail }); }); };
后端的文档,存在部分无实时更新,部分是错误的情况。 由于是 20+ 的接口,并没有采用手动复制接口的方案,因为手动复制,可能都要占用不少的时间,而且无法保证正确性,采用的是使用 jQuery 在 console 调用对文档进行格式化。脚本例子如下(脚本本身不具备通用性,因为后端写文档的风格不一样)
var result = {};var eles = $('h2').splice(1); eles.forEach((item, index) => { let cur = $(item); const desc = cur.text(); const info = { desc, }; let target; while((cur = cur.next())[0] !== eles[index + 1] && (index !== eles.length - 1)) { if (cur.is('table')) { if (!info[target]) { info[target] = {}; } const map = []; [...cur.find('tr:nth-child(1) th')].forEach((item, index) => { const th = $(item); if (th.text().includes('名称')) { // map.push('name'); } if (th.text().includes('类型')) { map.push('type'); } if (th.text().includes('是否必须')) { map.push('require'); } if (th.text().includes('描述')) { map.push('desc'); } }); [...cur.find('tr td:nth-child(1)')].forEach((item, index) => { const td = $(item); const mapClone = [...map]; let next = td.next(); const key = td.text().trim(); while (next.length) { info[target][key] = { [mapClone.shift()]: next.text().trim(), }; next = next.next(); } if (key === 'Action') { result[info[target][key].desc.match(/[a-zA-Z]+/)[0]] = info; info[target][key].value = info[target][key].desc.match(/[a-zA-Z]+/)[0]; } if (key === 'Version') { info[target][key].value = info[target][key].desc.match(/[\d-]+/)[0]; } }); } else if (cur.is('p')) { const subDesc = cur.text(); const attr = subDesc.match(/[a-zA-Z]+/)[0].trim(); target = target + '_' + attr; info[target] = { desc: subDesc, }; } else { const subDesc = cur.text().replace(/[\r\n]/g, ''); let pass = false; if (subDesc.includes('url')) { target = 'url'; } if (subDesc.includes('body')) { target = 'body'; } if (subDesc.includes('返回参数')) { target = 'response'; } if (subDesc.includes('请求示例')) { target = 'reqDemo'; pass = true; } if (subDesc.includes('返回示例')) { target = 'resDemo'; pass = true; } if (subDesc.includes('方法')) { info.method = subDesc.match(/get|post/i)[0].toUpperCase(); } if (target) { if ((target === 'reqDemo' || target === 'resDemo') && !pass) { info[target].value = subDesc; } else { if (!info[target]) { info[target] = { desc: subDesc, }; } } } } } });console.log(result);var map = Object.values(result).map((item) => ({query:{Action: item.url.Action.value,Version: item.url.Version.value,},method:item.method})).reduce((a,b) => Object.assign(a, {[b.query.Action]: {url: {query: b.query,method: b.method,path: 'path'}}}), {}); JSON.stringify(map, null, '\t').replace(/"(.*?)":/g, '$1:').replace(/: "path",{0,1}/g, ',');
经验
离数据核心越近,则需要写的转换越少,离数据核心远的话,可能要在多个地方写同样的转换,所以建议不要把转换写在各个地方,另外前端建设 service 层的必要性,不要把接口写的到处都是,很难找的。
其他
顺便简单写一下实践,由实践的例子可以看到对接口的定义,采用了新的方案,这种方案的说明,会在后续的文章进行介绍。
// service 页面define([ 'pro/service/base', './apis/module.js', './transform.js', ], (base, api, transform) => { const service = base.transformOAI(apis, transform); return service; });// apis/module.jsdefine([], function () { const path = '/module'; return { Create: { url: { query: { Action: "Create", Version: "2017-12-14" }, method: "GET", path, } }, Delete: { url: { query: { Action: "Delete", Version: "2017-12-14" }, method: "GET", path, } }, DescribeList: { url: { query: { Action: "DescribeList", Version: "2017-12-14" }, method: "POST", path, } }, }; });// transform.jsdefine([ ], () => { const formatRes = function (res) { return { code: res.Code, msg: res.Message, requestId: res.RequestId, }; }; return { Create: { pre(params) { return { query: { InstanceId: params.data.instanceId, Name: params.data.name, Description: base64._$str2b64(params.data.description), }, config: { noAlert: params.noAlert, }, }; }, post: formatRes, }, Delete: { pre(params) { return { query: { Id: params.data.id, }, }; }, post: formatRes, }, }; });
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 21分钟学会写编译器
【推荐】 “网易有钱”sketch使用分享