一、背景介紹
做過電商專案的同學都知道,店鋪裝修是電商系統必備的一個功能,在某些場景下,可能是廣告頁製作、活動頁製作、微頁面製作,但基本功能都是類似的。所謂店鋪裝修,就是使用者可以在 PC 端進行移動頁面的製作,只需要通過簡單的拖拽就可以實現頁面的編輯,屬於使用者高度自定義的功能。最終編輯的結果,可以在 H5、小程式進行展示推廣。
有讚美業是一套美業行業的 SaaS 系統,為美業行業提供資訊化和網際網路化解決方案。有讚美業本身提供了店鋪裝修的功能,方便使用者自定義網店展示內容,下面是有讚美業店鋪裝修功能的截圖:
上面的圖片是 PC 端的介面,下面兩張圖分別是 H5 和小程式的最終展示效果。可以簡單地看到,PC 端主要做頁面的編輯和預覽功能,包括了豐富的業務元件和詳細的自定義選項;H5 和小程式則承載了最終的展示功能。
再看看有讚美業當前的技術基本面:目前我們的 PC 端是基於 React 的技術棧,H5 端是基於 Vue 的技術棧,小程式是微信原生開發模式。
在這個基礎上,如果要做技術設計,我們可以從以下幾個角度考慮:
-
三端的檢視層都是資料驅動型別,如何管理各端的資料流程?
-
三個端三種不同技術棧,業務中卻存在相同的內容,是否存在程式碼複用的可能?
-
PC 最終生成的資料,需要與 H5、小程式共享,三端共用一套資料,應該通過什麼形式來做三端資料的規範管理?
-
在擴充套件性上,怎麼低成本地支援後續更多元件的業務加入?
二、方案設計
所以我們針對有讚美業的技術基本面,設計了一個方案來解決以上幾個問題。
首先擺出一張架構圖:
2.1 資料驅動
首先關注 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 編輯生成資料,最終會與 H5、小程式共享,所以協商好資料格式和欄位含義很重要。為了解決這個問題,我們抽取了一個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: '居右',
}];
複製程式碼
2.2 跨端複用
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`);
});
複製程式碼
2.3 拖拽實現
拖拽功能是通過 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包來做資料規範的統一。
當然除了基本架構以外,還會有很多技術細節需要處理,比如需要保證預覽元件不可點選等,這些則需要在實際開發中具體處理。