网易蜂巢(云计算基础服务)项目框架迁移指北(一)

此文已由做者张磊受权网易云社区发布。html

欢迎访问网易云社区,了解更多网易技术产品运营经验。前端

前言
在对蜂巢项目从 nej + regularjs 迁移到 vue 的过程当中,遇到的问题,以及在此过程当中所使用的解决方案。vue

遇到的问题
父子页面通讯
项目分为待重构的模块和已重构的模块,待重构的模块是使用 nej 和 regular ,重构的模块是 vue。页面是经过 iframe 引用重构的模块。node

这里会涉及到几个问题。iframe 和父窗口数据交换问题、模态框、以及路由同步的问题。 这三个问题的解决方案都是使用了一个通讯机制。这个通讯机制对数据进行了序列化(在 ie 下,不序列化会遇到暗坑),因此函数是没法进行传递的,只能够传递值,而后经过 JSON.stringify 序列化成字符串,传递过去后再反序列化成相应类型。采用这种形式,在项目后期,对代码进行批量修改的时候查找也很方便,甚至能够将通讯机制二次改造,而不须要改业务代码。webpack

问题须要特殊说明的有:web

  1. 模态框

因为 iframe 并非铺满整个页面,在 iframe 内部实现模态框的时候,致使导航栏不会被覆盖掉。因而就能够看到页面的一部分变灰,但导航栏仍是能够点击的。同时模态框的居中是相对于 iframe 的,因此看起来也不是特别居中。暂时解决方案,是使用通讯机制传参调用父页面的模态框的逻辑。2. 路由同步vue-router

nej 和 vue 都有一套路由方案,可是路由的格式是不一致的,同时模块的命名方案也会不一致,再者 iframe 和 父页面路由的变动都须要通知到对方。
接下来说通讯机制如何解决问题的。json

API后端

/**api

* 发送信息
* @param {string} receiver 收件人
* @param {string} action 描述
* @param {*} [msg] 内容
* @param {function} [cb] 回执函数
* @param {boolean} [isTemp] 若是收件人不存在,经过设置这个参数来指定该消息是丢弃仍是保存,当将来某时刻收件人存在的时候,会依次读取保存的信息,通常不须要指定。
*/

send(receiver, action, msg, cb, isTemp) {

},
// Bridge.send('parent', 'urlchange', '/module/list');
// Bridge.send('parent', 'error', '网络出错');
action 能够在父子页面的 handles 对象里注册。eg:

// 父页面const handles = {

show() {
    toggle(true);
},
hide() {
    toggle(false);
},
hideModal: _u._$hideModal,
alert(options) {
    _u._$alert(options);
},
error(msg) {
    CloudUI.Toast.error(msg);
},
confirm(options, cb) {
    _u._$confirm(Object.assign({
        onok() {
            cb('sub', {
                msg: true,
            });
        },
        oncancel() {
            cb('sub', {
                msg: false,
            });
        },
    }, options));
},

}
// 子页面handles.urlchange = function urlchange(path) {

router.replace(path);

};
再来一些复杂的例子

同步调用

子页面向父页面传递数据

Bridge.send('parent', 'urlchange', '/module/list');// 这里注意一点,为了之后方便,在 vue 模块内部使用的路由均是 vue 的,vue 路由向 nej 路由的转换在 /src/html/module/vue/map.js 进行配置,配置信息以下:// {// '/m/module/': '/module/list',// }
父页面向子页面传递数据

Bridge.send('sub', 'urlchange', '/module/list');// 而在 nej 模块,写路由就能够随意点,能够直接写 vue 的路由,也可让其进行转换,nej 的模块在后面均会丢弃,因此容许随意一点
异步调用

// send(receiver, action, msg, cb, isTemp)Bridge.send('parent', 'confirm', { content: '所选快照正在维护中,建立可能须要等待较长时间,建议稍后再试。', okButton: '继续建立', cancelButton: '稍后再试', primaryButton: 'cancelButton',
}, (err, status) => { if (status) { this.create();

} else {        this.submitting = false;
}

});// 其中第四个参数是回调函数 callback,模仿 nodejs 的实现,err 存在的时候就是失败,第二个参数是调用返回的 message。能够参见上面 handles.confirm
路由
vue 以及 vue-router 支持的异步加载仅仅是组件级别的,而不是路由级别的,因此实现路由级别的异步加载就会绕一些。eg:

// 通常实现const Create = () => import('./create.vue');const List = () => import('./list.vue');
这种方案下,webpack 会对每个路由进行打包,致使一个路由一个 chunk 的模式,前端加载负担过大。实际上,咱们须要的粒度可能没有这么细,在这里使用一种 vue-router 官方的方案(滑到页面底部)。

// 优化实现const Create = () => import(/ webpackChunkName: "a" / './create.vue');const List = () => import(/ webpackChunkName: "a" / './list.vue');
这里是使用注释 / webpackChunkName: "a" / 来标明打入同一个 chunk a 中。惟一的坑点是使用注释。固然还有一种方案进行处理。eg:

// index.jsexport { default as create } from './create.vue';export { default as list } from './list.vue';// route.jsconst Create = () => import('./index.js').then((modules) => modules.create);
路由写在每一个模块的下面,只存在一个文件。

eg:

// 建议- modules

- moduleA
    - routes.js

不建议在子目录放置路由,不清晰,完整的路由,可能须要打开多个文件,才能看到

// 不建议- modules

- moduleA
    - detail
        - routes.js
    - routes.js

建议方案会产生 routes.js 文件变的很庞大的问题,不方便查看。可参考以下写法,能够缓解此问题:

const routesA = [];const routesB = [];export default [

...routesA,
...routesB,

]
路由的划分问题

eg:

// 不建议const router = [

{        path: '/',        component: () => import(/* webpackChunkName: "module" */ './list.vue'),        children: [
        {                path: 'tab1',                name: 'module.list.tab1',                // ...
        },
        {                path: 'tab2',                name: 'module.list.tab2',                // ...
        },
        {                path: 'tab3',                name: 'module.list.tab3',                // ...
        },
    ],
},
{        path: 'tab1/edit',        name: 'module.edit.tab1',        // ...
}

];
module 模块页,有三个 tab。三个 tab 头是一致的。 list.vue 的代码仅仅是实现了 3个 tab 一致的部分即头部。观察 tab1/edit 和 tab1,在逻辑层面上它们应该被放到一块儿,可是 path: '/' 所在的组件的 dom 中含有 3个 tab 的头,致使没有办法写在一块儿(写在一块儿的话同时会继承头部),权限控制更显麻烦。更好的作法是 path: '/' 这一层级不作任何 dom 相关的东西,写到每一个 tab 内部。

// 建议const tab1 = { path: 'tab1', // 权限控制 tab1 的准入 children: [

{            path: 'list',            name: 'module.tab1.list',            // ...
    },
    {            path: 'edit',            name: 'module.tab1.edit',            // ...
    },
],

};
const router = [

{        path: '/',        // 权限控制 module 的准入        children: [
        tab1,
        {                path: 'tab2',                name: 'module.tab1.list',                // ...
        },
        {                path: 'tab3',                name: 'module.tab3.list',                // ...
        },
    ],
}

];
固然能够根据权限控制进行调整,写法不是很固定。交互可能不太喜欢定义 path: 'list', 可是第一种写法,至关于污染了整个路由的顶层,那后面必须定义多个顶层进行覆盖,由模块单入口路由变成了模块多入口路由。

openapi 和 webapi 数据转换
举例说明:在模块从 webapi 迁移到 openapi 的时候使用了一种方案,在数据获取层面对数据进行转换。即:

// openapiconst result1 = { Id: 1, Name: 2,
};// webapiconst result2 = { id: 1, name: 2,
};// transform(result1) 后的数据结构包含 result2 中有用的数据结构// 这个 transform 函数会将 openapi 的数据转换成 webapi 的,这样只须要改数据结构,让新老保持一致,再修改少许的业务逻辑便可完成接口迁移工做。
这个问题自己属于后端接口变动,与框架迁移属于并行任务,单独拿来看并没有关联,问题放在一块儿的时候,就变得棘手了。

在对 win 模块进行迁移的时候,在使用 vue 的时候但愿接口方面使用 opeanpi 的数据,不进行数据转换。可是在使用老模块的时候,为了尽可能少的动业务代码,对 opeanpi 的数据进行了转换,那就意味着二者的数据的并不一致,在使用上面提到的通讯机制(调用父页面的模态框,须要传递数据)的时候,这就很致命了,意味着一方须要再作一次数据转换。目前代码是 vue 模块手工硬编码转换的,后面能够把这部分放到 nej ,能够借用其已有的接口的数据转换函数,对 vue 传递的数据进行二次转换。

另外还有一个问题,若是模块先进行框架迁移,后进行接口迁移,此时就面临两个方案。一个是使用 transform 函数对数据进行转换,另外一种,推到重写。此时确定更倾向于第一种方案,那么就须要对 transform 函数进行设计,让其更方便使用。这里简单设计了一种。

const source = { standard: { bandwidth: 1, ipChargeType: 2

},    instanceId: 6,

};
const rules = { standard: 'NewStandard', instanceId: 'InstanceId', 'standard.bandwidth': ['InternetMaxBandwidth', 'BizParam.InternetMaxBandwidth'], 'standard.ipChargeType': 'BizParam.NetworkChargeType',
};
const out = { NewStandard: { bandwidth: 1, ipChargeType: 2

},    InstanceId: 6,    BizParam: {        InternetMaxBandwidth: 1, 
    NetworkChargeType: 2, 
},    InternetMaxBandwidth: 1,

};
it(transform(source, rules) is correct, () => { assert.deepEqual(transform(source, rules), Object.assign({}, source, out));
});
it(transform(source, rules, true) is correct, () => { assert.deepEqual(transform(out, rules, true), Object.assign({}, out, source));
});
静态资源
这里主要是 js 文件内引用的静态资源,该静态资源的路径须要用此语法进行设置

default: { logo: require(@/assets/images/logos/logo.png), // @是项目根路径},
这样这个静态文件就能够享受到 webpack 的处理,算出正确的路径,否则有可能出现显示不出来的状况。 另外静态资源不推荐写相对路径。eg: ../../../assets/images/logos/logo.png

接口
nej + regular 在代码里写了接口,在 vue 须要再次找到接口,从新写一遍,基本不可复用。但若是以前放置接口的地方稍显混乱,那么在找接口的时候,就须要一个个业务逻辑的看过去。试想可不能够

将接口按照模块放置在一块儿,称为 api 层,同时再划分出来 service 层。api 层经过 json 来描述一个接口的方方面面, service 层是从 api 层生成出来的,外加上对接口进行二次处理和对多个接口拼接。
而后就很好的抽象出一个独立的 api 层,和业务逻辑无关,仅和后端文档输出有关,同时 service 层又很好的保持必定的业务相关性。那么在换框架的时候,api 直接拿走便可,service 层仅须要稍许改动。另有文章介绍具体的实现。

这里可能会有接口数据缓存以及必定时间内只发一次请求的需求,那么想一下,须要在该层实现吗?仍是须要更高的一层对数据处理的抽象,而不受限于仅对接口数据?又或者对 service 层的定义进行扩充,包含对数据处理的抽象?

网易云计算基础服务深度整合了 IaaS、PaaS 及容器技术,提供弹性计算、DevOps 工具链及微服务基础设施等服务,帮助企业解决 IT、架构及运维等问题,使企业更聚焦于业务,是新一代的云计算平台,点击可免费试用。

文章来源: 网易云社区

相关文章
相关标签/搜索