此文已由做者張磊受權網易雲社區發佈。html
歡迎訪問網易雲社區,瞭解更多網易技術產品運營經驗。前端
前言
在對蜂巢項目從 nej + regularjs 遷移到 vue 的過程當中,遇到的問題,以及在此過程當中所使用的解決方案。vue
遇到的問題
父子頁面通訊
項目分爲待重構的模塊和已重構的模塊,待重構的模塊是使用 nej 和 regular ,重構的模塊是 vue。頁面是經過 iframe 引用重構的模塊。node
這裏會涉及到幾個問題。iframe 和父窗口數據交換問題、模態框、以及路由同步的問題。 這三個問題的解決方案都是使用了一個通訊機制。這個通訊機制對數據進行了序列化(在 ie 下,不序列化會遇到暗坑),因此函數是沒法進行傳遞的,只能夠傳遞值,而後經過 JSON.stringify 序列化成字符串,傳遞過去後再反序列化成相應類型。採用這種形式,在項目後期,對代碼進行批量修改的時候查找也很方便,甚至能夠將通訊機制二次改造,而不須要改業務代碼。webpack
問題須要特殊說明的有:web
因爲 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、架構及運維等問題,使企業更聚焦於業務,是新一代的雲計算平臺,點擊可免費試用。
文章來源: 網易雲社區