I. Background introduction
Students who have done e-commerce projects know that shop decoration is a necessary function of e-commerce system. In some scenarios, it may be advertising page production, activity page production, micro page production, but the basic functions are similar. The so-called shop decoration is that users can make mobile pages on the PC side, and only need to realize page editing by simple drag and drop. It is a highly customized function for users. The final editing results can be displayed and promoted in H5 and small programs.
Youchengye is a SaaS system for the beauty industry, providing information and Internet solutions for the beauty industry. You Praise industry itself provides the store decoration function, which is convenient for users to customize the display content of the online store. The following is a screenshot of the store decoration function of Praise Industry:
The top image is the PC interface, and the bottom two images are the final display of H5 and applets respectively. As you can easily see, the PC side mainly does the page editing and preview function, including rich business components and detailed customization options; H5 and applets carry the ultimate presentation function.
Take a look at the current technical fundamentals of praise industry: at present, our PC terminal is based on React technology stack, H5 terminal is based on Vue technology stack, and small program is wechat native development mode.
On this basis, if we want to do technical design, we can consider from the following perspectives:
-
The view layer of the three ends is data-driven. How to manage the data flow of each end?
-
Three ends and three different technology stacks, but there is the same content in the business, is there the possibility of code reuse?
-
PC finally generated data, need to share with H5, small program, three end to share a set of data, should be through what form to do three end data standard management?
-
In terms of expansibility, how to support the subsequent addition of more components at a low cost?
Second, program design
Therefore, we designed a solution to solve the above problems according to the technical fundamentals of the praise industry.
Start with an architecture diagram:
2.1 Data Driven
First of all, pay attention to the CustomPage component, which is the general console for the decoration of the whole store. It maintains three main components PageLeft, PageView and PageRight, respectively corresponding to the three modules mentioned above on the PC side.
To enable data sharing, CustomPage maintains a “scope” through React Context that provides a “data source” shared by the three internal components. PageLeft and PageRight are the left and right editing components, respectively, that share context.page data, and data changes are passed through context.pageChange. The whole process is roughly expressed in code as follows:
// 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() {...}
}
Copy the code
As for H5, dynamic components of Vue can be used to achieve dynamic business components. This asynchronous component approach provides great flexibility and is very suitable for the scene of store decoration.
<div v-for="item in components">
<component :is="item.component" :options="convertOptions(item.options)" :isEdit="true">
</component>
</div>
Copy the code
Applets have no concept of dynamic components, so they can only do this through if else noodle code. In terms of further reuse, there are currently open source tools in the community for converting Vue and applets that may help us do more, but we won’t go into that here.
PC editors generate data that will eventually be shared with H5 and applets, so it’s important to negotiate the format and field meanings of the data. To solve this problem, we extracted an NPM package that manages the unification of data on the three ends. This package describes the field format and meaning of each component. In the implementation of each end, it is only necessary to develop the corresponding style according to the field description, thus solving the problem of scalability we said. If you need to add new service components, you only need to negotiate and upgrade the new NPM package to achieve data unification on the three ends.
/** * display position */export const position = {
LEFT: 0,
CENTER: 1,
RIGHT: 2,
};
export const positionMap = [{
value: position.LEFT,
name: 'the left',
}, {
value: position.CENTER,
name: 'center',
}, {
value: position.RIGHT,
name: 'the right',}];Copy the code
2.2 Cross-end multiplexing
PageView is the preview component that is at the heart of this design. In the most straightforward way, React might be used to implement all business components, and then the logic for arranging and displaying data. Again in H5 and small procedures to achieve all the components again, the logic of data arrangement display is also achieved again. But for code reuse, can we do some lazy work?
If we do not consider the small program, we know that PC and H5 are based on the DOM style implementation, logic is also JS code, both ends of the implementation of a time must do a lot of repetitive work. Therefore, in order to achieve the ability of style and logic reuse, we came up with a method that is to use IFrame to nest H5 pages and postMessage to do data interaction, so as to realize the use of H5 as a preview component, so that PC and H5 code is only one set. The PageView component can be implemented as follows:
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>); }}Copy the code
The PageView code is simple, just embed the iframe, and leave the rest to H5. H5 will get the data, according to the specification into the corresponding component array display:
<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>
Copy the code
Because of iframe, we also need to use PostMessage to communicate across sources. For ease of use, we have made a layer of encapsulation (code reference:
export default class Messager {
constructor(win, targetOrigin) {
this.win = win;
this.targetOrigin = targetOrigin;
this.actions = {};
window.addEventListener('message', this.handleMessageListener, false); } handleMessageListener = (event) => {// Can we trust the sender of the message? (Maybe this sender is not the same page we originally opened).if(event.origin ! == this.targetOrigin) { console.warn(`${event.origin}Does not correspond to the source${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); }}Copy the code
On this basis, the business side only needs to focus on message processing. For example, H5 component receiving data updates from PC can be used as follows:
this.messager = new Messager(window.parent, `${window.location.protocol}//mei.youzan.com`);
this.messager.on('pageChangeFromReact', (data) => {
...
});
Copy the code
In this way, the events negotiated by both ends can be processed by business logic respectively.
There is one detail that needs to be handled here, because the preview height changes dynamically, and the PC needs to control the external view height, so it also needs to have a mechanism for dynamically obtaining the preview height.
// 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`);
});
Copy the code
2.3 Drag and drop implementation
The drag function is implemented through HTML5 Drag & Drop API. In this requirement, the main purpose is to achieve dynamic sorting of components during the drag process. Here are a few key points that may take some work to implement:
- View automatically scrolls while dragging up and down
- Drag and drop results to synchronize data changes
- Appropriate animation effects
There are many mature drag-related libraries in the community, and we chose VueDraggable. The reason is very simple. On the one hand, it avoids repeating the wheel. On the other hand, it solves the problems we mentioned above.
Vuedraggable encapsulates the dynamic component we mentioned earlier and then encapsulates the draggable component:
<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() {
returnmapToComponents(this.list); }},Copy the code
Third, summary
At this point, all the design is done. React Context is used to share data between PC components. H5 and applets are displayed through the corresponding component array of data mapping. The core point is to reuse style logic through IFrame. In addition, third-party NPM packages can be used to unify data specifications.
Of course, beyond the basic architecture, there are many technical details that need to be addressed, such as making the preview components unclickable, which need to be addressed in the actual development.