作過電商項目的同窗都知道,店鋪裝修是電商系統必備的一個功能,在某些場景下,多是廣告頁製做、活動頁製做、微頁面製做,但基本功能都是相似的。所謂店鋪裝修,就是用戶能夠在 PC 端進行移動頁面的製做,只須要經過簡單的拖拽就能夠實現頁面的編輯,屬於用戶高度自定義的功能。最終編輯的結果,能夠在 H五、小程序進行展現推廣。vue
有讚美業是一套美業行業的 SaaS 系統,爲美業行業提供信息化和互聯網化解決方案。有讚美業自己提供了店鋪裝修的功能,方便用戶自定義網店展現內容,下面是有讚美業店鋪裝修功能的截圖:react
上面的圖片是 PC 端的界面,下面兩張圖分別是 H5 和小程序的最終展現效果。能夠簡單地看到,PC 端主要作頁面的編輯和預覽功能,包括了豐富的業務組件和詳細的自定義選項;H5 和小程序則承載了最終的展現功能。npm
再看看有讚美業當前的技術基本面:目前咱們的 PC 端是基於 React 的技術棧,H5 端是基於 Vue 的技術棧,小程序是微信原生開發模式。小程序
在這個基礎上,若是要作技術設計,咱們能夠從如下幾個角度考慮:api
三端的視圖層都是數據驅動類型,如何管理各端的數據流程?數組
三個端三種不一樣技術棧,業務中卻存在相同的內容,是否存在代碼複用的可能?bash
PC 最終生成的數據,須要與 H五、小程序共享,三端共用一套數據,應該經過什麼形式來作三端數據的規範管理?微信
在擴展性上,怎麼低成本地支持後續更多組件的業務加入?架構
因此咱們針對有讚美業的技術基本面,設計了一個方案來解決以上幾個問題。dom
首先擺出一張架構圖:
首先關注 CustomPage 組件,這是整個店鋪裝修的總控制檯,內部維護三個主要組件 PageLeft、 PageView 和 PageRight,分別對應上面提到的 PC 端3個模塊。
爲了使數據共享,CustomPage 經過 React context 維護了一個」做用域「,提供了內部三個組件共享的「數據源」。 PageLeft 、 PageRight 分別是左側組件和右側編輯組件,共享 context.page
數據,數據變動則經過 context.pageChange
傳遞。整個過程大體用代碼表示以下:
// CustomerPage
class CustomerPage extends React.Component {
static childContextTypes = {
page: PropTypes.object.isRequired,
pageChange: PropTypes.func.isRequired,
activeIndex: PropTypes.number.isRequired,
};
getChildContext() {
const { pageInfo, pageLayout } = this.state;
return {
page: { pageInfo, pageLayout },
pageChange: this.pageChange || (() => void 0),
activeIndex: pageLayout.findIndex(block => block.active),
};
}
render() {
return (
<div>
<PageLeft />
<PageView />
<PageRight />
</div>
);
}
}
// PageLeft
class PageLeft extends Component {
static contextTypes = {
page: PropTypes.object.isRequired,
pageChange: PropTypes.func.isRequired,
activeIndex: PropTypes.number.isRequired,
};
render() {...}
}
// PageRight
class PageRight extends Component {
static contextTypes = {
page: PropTypes.object.isRequired,
pageChange: PropTypes.func.isRequired,
activeIndex: PropTypes.number.isRequired,
};
render() {...}
}
複製代碼
至於 H5 端,能夠利用 Vue 的動態組件完成業務組件的動態化,這種異步組件的方式提供了極大的靈活性,很是適合店鋪裝修的場景。
<div v-for="item in components">
<component :is="item.component" :options="convertOptions(item.options)" :isEdit="true">
</component>
</div>
複製代碼
小程序由於沒有動態組件的概念,因此只能經過 if else
的麪條代碼來實現這個功能。更深刻的考慮複用的話,目前社區有開源的工具實現 Vue 和小程序之間的轉換,可能能夠幫助咱們作的更多,但這裏就不展開討論了。
PC 編輯生成數據,最終會與 H五、小程序共享,因此協商好數據格式和字段含義很重要。爲了解決這個問題,咱們抽取了一個npm包,專門管理3端數據統一的問題。這個包描述了每一個組件的字段格式和含義,各端在實現中,只須要根據字段描述進行對應的樣式開發就能夠了,這樣也就解決了咱們說的擴展性的問題。後續若是須要增長新的業務組件,只須要協商好並升級新的npm包,就能作到3端的數據統一。
/**
* 顯示位置
*/
export const position = {
LEFT: 0,
CENTER: 1,
RIGHT: 2,
};
export const positionMap = [{
value: position.LEFT,
name: '居左',
}, {
value: position.CENTER,
name: '居中',
}, {
value: position.RIGHT,
name: '居右',
}];
複製代碼
PageView 是預覽組件,是這個設計的核心。按照最直接的思路,咱們可能會用 React 把全部業務組件都實現一遍,而後把數據排列展現的邏輯實現一遍;再在 H5 和小程序把全部組件實現一遍,數據排列展現的邏輯也實現一遍。可是考慮到代碼複用性,咱們是否是能夠作一些「偷懶」?
若是不考慮小程序的話,咱們知道 PC 和 H5 都是基於 dom 的樣式實現,邏輯也都是 js 代碼,兩端都實現一遍的話確定作了不少重複的工做。因此爲了達到樣式和邏輯複用的能力,咱們想了一個方法,就是經過 iframe 嵌套 H5 的頁面,經過 postmessage 來作數據交互,這樣就實現了用 H5 來充當預覽組件,那麼 PC 和 H5 的代碼就只有一套了。按照這個實現思路,PageView 組件能夠實現成下面這樣:
class PageView extends Component {
render() {
const { page = {} } = this.props;
const { pageInfo = {}, pageLayout = [] } = page;
const { loading } = this.state;
return (
<div className={style}>
<iframe
title={pageInfo.title}
src={this.previewUrl}
frameBorder="0"
allowFullScreen="true"
width="100%"
height={601}
ref={(elem) => { this.iframeElem = elem; }}
/>
</div>);
}
}
複製代碼
PageView 代碼很簡單,就是內嵌 iframe,其他的工做都交給 H5。H5 將拿到的數據,按照規範轉換成對應的組件數組展現:
<template>
<div>
<component
v-for="(item, index) in components"
:is="item.component"
:options="item.options"
:isEdit="false">
</component>
</div>
</template>
<script>
computed: {
components() {
return mapToComponents(this.list);
},
},
</script>
複製代碼
由於有了 iframe ,還須要利用 postmessage 進行跨源通訊,爲了方便使用,咱們作了一層封裝(代碼參考自有贊餐飲):
export default class Messager {
constructor(win, targetOrigin) {
this.win = win;
this.targetOrigin = targetOrigin;
this.actions = {};
window.addEventListener('message', this.handleMessageListener, false);
}
handleMessageListener = (event) => {
// 咱們能相信信息的發送者嗎? (也許這個發送者和咱們最初打開的不是同一個頁面).
if (event.origin !== this.targetOrigin) {
console.warn(`${event.origin}不對應源${this.targetOrigin}`);
return;
}
if (!event.data || !event.data.type) {
return;
}
const { type } = event.data;
if (!this.actions[type]) {
console.warn(`${type}: missing listener`);
return;
}
this.actions[type](event.data.value);
};
on = (type, cb) => {
this.actions[type] = cb;
return this;
};
emit = (type, value) => {
this.win.postMessage({
type, value,
}, this.targetOrigin);
return this;
};
destroy() {
window.removeEventListener('message', this.handleMessageListener);
}
}
複製代碼
在此基礎上,業務方就只須要關注消息的處理,例如 H5 組件接收來自 PC 的數據更新能夠這樣用:
this.messager = new Messager(window.parent, `${window.location.protocol}//mei.youzan.com`);
this.messager.on('pageChangeFromReact', (data) => {
...
});
複製代碼
這樣經過兩端協商的事件,各自進行業務邏輯處理就能夠了。
這裏有個細節須要處理,由於預覽視圖高度會動態變化,PC 須要控制外部視圖高度,因此也須要有動態獲取預覽視圖高度的機制。
// vue script
updated() {
this.$nextTick(() => {
const list = document.querySelectorAll('.preview .drag-box');
let total = 0;
list.forEach((item) => {
total += item.clientHeight;
});
this.messager.emit('vueStyleChange', { height: total });
}
}
// react script
this.messsager.on('vueStyleChange', (value) => {
const { height } = value;
height && (this.iframeElem.style.height = `${height}px`);
});
複製代碼
拖拽功能是經過 HTML5 drag & drop api 實現的,在此次需求中,主要是爲了實現拖動過程當中組件可以動態排序的效果。這裏有幾個關鍵點,實現起來可能會花費一些功夫:
目前社區有不少成熟的拖拽相關的庫,咱們選用了vuedraggable。緣由也很簡單,一方面是避免重複造輪子,另外一方面就是它很好的解決了咱們上面提到的幾個問題。
vuedraggable 封裝的很好,使用起來就很簡單了,把咱們前面提到的動態組件再封裝一層 draggable 組件:
<draggable
v-model="list"
:options="sortOptions"
@start="onDragStart"
@end="onDragEnd"
class="preview"
:class="{dragging: dragging}">
<div>
<component
v-for="(item, index) in components"
:is="item.component"
:options="item.options"
:isEdit="false">
</component>
</div>
</draggable>
const sortOptions = {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
};
// vue script
computed: {
list: {
get() {
return get(this.designData, 'pageLayout') || [];
},
set(value) {
this.designData.pageLayout = value;
this.notifyReact();
},
},
components() {
return mapToComponents(this.list);
},
},
複製代碼
到這裏,全部設計都完成了。總結一下就是:PC 端組件間主要經過 React context 來作數據的共享;H5 和 小程序則是經過數據映射對應的組件數組來實現展現;核心要點則是經過 iframe 來達到樣式邏輯的複用;另外能夠經過第三方npm包來作數據規範的統一。
固然除了基本架構之外,還會有不少技術細節須要處理,好比須要保證預覽組件不可點擊等,這些則須要在實際開發中具體處理。