Building a Vue‑Based Visual Editor: Architecture, JSON Schema & Drag‑Drop
This article outlines the design and implementation of a Vue‑powered visual editor, covering its three‑part architecture (editor, component library, backend), JSON schema hierarchy, component creation and configuration, state management with Vuex, drag‑and‑drop mechanics, and server‑side rendering.
Background
Frontend visual editors are implemented in various ways in large companies, with different businesses assigning different functionalities; see https://github.com/taowen/awesome-lowcode for many Chinese low‑code platform implementations. Although there are many AI‑driven page slicing and generation cases, achieving rapid output requires substantial manpower and effort, so it is not recommended for typical teams.
Purpose
Designing such a visual editor aims to achieve the following goals:
Visual editing with drag‑and‑drop on the canvas.
Rich style configuration.
Extract common business logic into reusable components with configurable parameters.
Design Approach
The project is divided into three parts:
Editor – provides project management, element dragging, parameter configuration, preview and publishing capabilities.
Component Library – encapsulates business logic into a maintainable component library.
Backend – stores project data and parses the JSON produced by the editor to generate pages.
Tech Stack
The team is familiar with Vue, so both the editor and component library are built with Vue.
The backend uses egg.js, a mature framework that avoids building from scratch, though other frameworks are also possible.
Technology choices can be adjusted according to personal or team preferences.
Design Overview
1. JSON Schema
The JSON schema is the foundation of the project and is generated by the editor. It must keep a consistent format so the backend can parse and render the page.
1. Project Level
Project‑level JSON describes basic project information.
<code>{
id: '', // project unique identifier
name: '', // project name
label: [], // project tags
description: '', // project description
author: '', // author
pages: [] // pages
}</code>2. Page Level
Page‑level JSON corresponds to an HTML page, including scripts, styles, and elements.
<code>{
id: '', // page unique identifier
type: '', // page type
route: '', // page route
title: '', // page title
style: {
width: '',
height: '',
...
},
elements: [], // components
plugins: [] // page plugins
}</code>3. Component Level
Components are the building blocks of a page.
<code>{
id: '', // component unique identifier
elementName: '', // component name
style: {
position: '',
width: '',
height: '',
border: ''
},
props: {}, // component props
children: [], // child components
...
}</code>The
propsconcept mirrors Vue’s props, allowing components to render according to supplied parameters.
2. Component Library
Components are essential page bricks. Besides basic components (text, image, video), business‑specific components (carousel, navigation) must be abstracted into reusable modules. Building a component library requires two code bases: the component itself and a configuration panel for its props.
1. Component
A simple video component example:
<code><template>
<video :src="src" :autoplay="autoplay" />
</template>
<script>
export default {
name: 'aicc-video',
props: {
src: { type: String, default: '' },
autoplay: { type: String, default: '' }
}
}
</script></code>Importing the component in the editor:
<code>import AICCVideo from '@/components/AICCVideo'</code>Importing the component on the server (as a script):
<code><script src="https://.../aiccvideo.min.js"></script></code>2. Configuration Panel
The panel generates custom JSON data for the editor. Using the video component as an example:
<code><template>
<div class="props-video">
<input v-model="propsValue.src" />
<input v-model="propsValue.autoplay" />
</div>
</template>
<script>
export default {
name: 'props-video',
props: {
value: { type: Object, default: () => ({}) }
},
data() { return { propsValue: {} } },
watch: {
value(val) { this.propsValue = val },
propsValue(val) { this.$emit('input', val) }
}
}
</script></code>The panel is registered in the editor just like a component, with
v-modelbinding the component’s props.
3. Editor
The editor’s core task is to modify the JSON schema. Vuex is used for state management because it fits medium‑to‑large Vue projects.
Vuex is a state‑management pattern and library for Vue.js applications.
Key store snippets:
<code>const state = {
projectData: {}, // project JSON data
activePageUUID: '', // currently edited page
activeElementUUID: '' // currently selected component
}
export function initProject({ commit, state }, data) {
let projectData = data
if (!data) projectData = initProject() // generate default JSON for empty project
dispatch('setActivePageUUID', projectData.pages[0].uuid)
}
export function setActivePageUUID({ commit }, uuid) { state.activePageUUID = uuid }
export function setActiveElementUUID({ commit }, uuid) { state.activeElementUUID = uuid }
export function activePage(state) {
const idx = state.projectData.pages.findIndex(p => state.activePageUUID === p.uuid)
return state.projectData.pages[idx]
}
export function activeElement(state) {
const pIdx = state.projectData.pages.findIndex(p => state.activePageUUID === p.uuid)
const activePage = state.projectData.pages[pIdx]
const eIdx = activePage.elements.findIndex(e => state.activeElementUUID === e.uuid)
// return the active element (omitted for brevity)
}
</code>Components bind to the store via
mapGettersand
mapStateto obtain the current page, element, and project data.
<code><template>
<div>
<component :is="activeElement.elementName" v-model="activeElement.props" />
</div>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
export default {
computed: {
...mapGetters({ activeElement: 'editor/activeElement' }),
...mapState({
pages: state => state.editor.projectData.pages,
activePageUUID: state => state.editor.activePageUUID,
activeElementUUID: state => state.editor.activeElementUUID
})
}
}
</script></code>1. Drag‑and‑Drop Mechanics
To enable element dragging on the canvas:
Listen for
mousedownon the target element.
Record the element’s offsetTop/offsetLeft and the mouse coordinates.
On
mousemove, calculate the distance moved and compute new coordinates.
On
mouseup, remove listeners and optionally apply the final position.
<code>function mousedown(e) {
let newTop = null
let newLeft = null
const cTop = e.currentTarget.offsetTop
const cLeft = e.currentTarget.offsetLeft
const mouseX = e.clientX
const mouseY = e.clientY
const move = mEvent => {
mEvent.stopPropagation()
mEvent.preventDefault()
const cX = mEvent.clientX
const cY = mEvent.clientY
const distanceX = cX - mouseX
const distanceY = cY - mouseY
newTop = distanceX + cTop
newLeft = distanceY + cLeft
}
const up = () => {
document.removeEventListener('mousemove', move, true)
document.removeEventListener('mouseup', up, true)
}
document.addEventListener('mousemove', move, true)
document.addEventListener('mouseup', up, true)
}
</code>For performance, use
transformduring
mousemoveand apply the final
top/lefton
mouseup.
4. Server‑Side Rendering
The JSON produced by the editor is parsed on the server to generate a structured page using Vue’s render function.
Generate page title/SEO info from page data and create a
${name}.htmlfile based on the route.
Include the bundled component library via a script tag.
Since the render function does not need a compiler, load
vue.runtime.jsonly.
Use a template engine to replace placeholders with page info and component arrays.
Iterate over
elementsand call
createElement('ComponentName', { props: {...elementJSON} }).
Convert style JSON into CSS rules like
selector { key: value }.
Write the final HTML string to disk with
fs.writeFileSync(filePath, htmlString).
Conclusion
This article provides a high‑level overview of how to assemble a Vue‑based visual editor, covering architecture, JSON schema design, component creation, state management, drag‑and‑drop implementation, and server‑side rendering. Detailed implementation of each module can be expanded into separate topics such as component packaging, monorepo management with Lerna, real‑time resizing, data validation, and plugin systems like WeChat sharing.
37 Mobile Game Tech Team
37 Mobile Game Tech Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.