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

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

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

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

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

问题需要特殊说明的有:

  1. 模态框

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

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

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

文章来源: 网易云社区


网易数帆
391 声望550 粉丝

网易数智旗下全链路大数据生产力平台,聚焦全链路数据开发、治理及分析,为企业量身打造稳定、可控、创新的数据生产力平台,服务“看数”、“管数”、“用数”等业务场景,盘活数据资产,释放数据价值。