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

此文已由作者张磊授权网易云社区发布。

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

前言

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

遇到的问题

父子页面通信

项目分为待重构的模块和已重构的模块,待重构的模块是使用 nej 和 regular ,重构的模块是 vue。页面是通过 iframe 引用重构的模块。

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

问题需要特殊说明的有:

1. 模态框

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

nej 和 vue 都有一套路由方案,但是路由的格式是不一致的,同时模块的命名方案也会不一致,再者 iframe 和 父页面路由的变更都需要通知到对方。复制代码

接下来讲通信机制如何解决问题的。

  1. 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);
};复制代码

再来一些复杂的例子

  1. 同步调用

  • 子页面向父页面传递数据

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 的模块在后面均会丢弃,所以允许随意一点复制代码
  1. 异步调用

// 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`。复制代码

路由

  1. 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);复制代码
  1. 路由写在每个模块的下面,只存在一个文件。

eg:

// 建议- modules
    - moduleA
        - routes.js复制代码

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

// 不建议- modules
    - moduleA
        - detail
            - routes.js
        - routes.js复制代码

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

const routesA = [];const routesB = [];export default [
    ...routesA,
    ...routesB,
]复制代码
  1. 路由的划分问题

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、架构及运维等问题,使企业更聚焦于业务,是新一代的云计算平台,点击可免费试用

相关文章:
【推荐】 网易云社区高手问答第一期:315之业务反作弊反欺诈
【推荐】 Android 标题栏(2)
【推荐】 wireshark抓包分析——TCP/IP协议


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值