有讚美業店鋪裝修前端解決方案

1、背景介紹

作過電商項目的同窗都知道,店鋪裝修是電商系統必備的一個功能,在某些場景下,多是廣告頁製做、活動頁製做、微頁面製做,但基本功能都是相似的。所謂店鋪裝修,就是用戶能夠在 PC 端進行移動頁面的製做,只須要經過簡單的拖拽就能夠實現頁面的編輯,屬於用戶高度自定義的功能。最終編輯的結果,能夠在 H五、小程序進行展現推廣。vue

有讚美業是一套美業行業的 SaaS 系統,爲美業行業提供信息化和互聯網化解決方案。有讚美業自己提供了店鋪裝修的功能,方便用戶自定義網店展現內容,下面是有讚美業店鋪裝修功能的截圖:react

有讚美業店鋪裝修

上面的圖片是 PC 端的界面,下面兩張圖分別是 H5 和小程序的最終展現效果。能夠簡單地看到,PC 端主要作頁面的編輯和預覽功能,包括了豐富的業務組件和詳細的自定義選項;H5 和小程序則承載了最終的展現功能。npm

再看看有讚美業當前的技術基本面:目前咱們的 PC 端是基於 React 的技術棧,H5 端是基於 Vue 的技術棧,小程序是微信原生開發模式。小程序

在這個基礎上,若是要作技術設計,咱們能夠從如下幾個角度考慮:api

  • 三端的視圖層都是數據驅動類型,如何管理各端的數據流程?數組

  • 三個端三種不一樣技術棧,業務中卻存在相同的內容,是否存在代碼複用的可能?bash

  • PC 最終生成的數據,須要與 H五、小程序共享,三端共用一套數據,應該經過什麼形式來作三端數據的規範管理?微信

  • 在擴展性上,怎麼低成本地支持後續更多組件的業務加入?架構

2、方案設計

因此咱們針對有讚美業的技術基本面,設計了一個方案來解決以上幾個問題。dom

首先擺出一張架構圖:

店鋪裝修架構圖

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 編輯生成數據,最終會與 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: '居右',
}];
複製代碼

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);
    },
},
複製代碼

3、總結

到這裏,全部設計都完成了。總結一下就是:PC 端組件間主要經過 React context 來作數據的共享;H5 和 小程序則是經過數據映射對應的組件數組來實現展現;核心要點則是經過 iframe 來達到樣式邏輯的複用;另外能夠經過第三方npm包來作數據規範的統一。

固然除了基本架構之外,還會有不少技術細節須要處理,好比須要保證預覽組件不可點擊等,這些則須要在實際開發中具體處理。

相關文章
相關標籤/搜索