有赞美业店铺装修前端解决方案

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包来作数据规范的统一。

固然除了基本架构之外,还会有不少技术细节须要处理,好比须要保证预览组件不可点击等,这些则须要在实际开发中具体处理。

相关文章
相关标签/搜索