May 30, 2021 Article blog
Author: muwoo
Original: https://github.com/muwoo/rose
Activity page builder for quickly building activity pages
If you're in regular contact with some of the company's activity pages, you may often have the following headaches: these projects have short cycles, frequent demands, fast iterations, low technical requirements, and little room for growth. B ut we're still rushing to get the products coming up one by one, and as the size of the company grows, we can't keep repeating these activities without limits. Here I will not specifically introduce some of the concepts that some do not have, because there are too many concepts to introduce, as a front-end of us, just go straight to the code!!!!
Our goal is to implement a page production background, where we can make component selections for pages
组件选择 --> 布局样式调整 --> 发布上线 --> 编辑修改
such process operations.
The first is to be able to provide components for the user to choose from, then we need a
组件库
and then we need to lay out the selected components, so we need a
页面编辑后台
Then we need to render the data from the edit output as real pages, so we need a node
node服务
and a
template 模板
for padding. R
elease online, this direct docking of the various companies within the release system is good, here we do not do much elaboration. T
he final edit modification function is also for configuration modifications, so we need a database, here I chose to use
mysql.
O
f course you can also do rights management by the way, page management.... A
nd so on and stuff like that.
After so long, let's draw a picture and understand the approximate process:
First we implement this part of the component, because the component is associated with the preview of background editing and the use of the last release. C omponent design We should try to maintain the external consistency of the component, so that when rendering, we can provide a unified external data interface. Our technical selection here is Vue-based, so the following part of the code is also mainly Vue-based, but it's never out of place, and other languages are similar.
Based on the image above, our components are split one by one and published separately to the
npm
repository. I
n fact, we have also considered designing a component library before, all components are included in a component library, so you only need to publish a component library package, when used to load on demand. L
ater in the process of practice found that this is not suitable for collaborative development, other front end if you want to contribute components, access to the transformation costs are also very large. F
or example, Xiaoming wrote a
Button
component in his business that is often reused by other projects, and he wants to contribute this component to our system, be used by templates, and, if it is a component library, he first pulls the code from our component library and submits it in the canonical format of the component library. I
n this way, lazy Xiaoming may not be willing to do so, the best way is of course to build an npm library locally, the development of the choice of
TypeScript
or other we do not care, the choice of Css preprocessors we do not care, even coding
ESLint
we do not care. F
inally, just go through the compiled file. T
his avoids the constraints of a component library.
Relying on NPM's perfect release/pull, as well as version control mechanism, we can do less extra work, but also can quickly set up the platform.
Having said that, what about the code?
Let's take
Button
as an example, and we offer this form of component to the outside world:
<template>
<div :style="data.style.container" class="w_button_container">
<button :style="data.style.btn"> {{data.context}}</button>
</div>
</template>
<script>
export default {
name: 'WButton',
props: {
data: {
type: Object,
default: () => {}
}
}
}
</script>
You can see that we have only exposed one
props
to the outside world, and the advantage of doing so is that we can unify the data exposed to the outside of the component, and how the component's internal love plays.
Note that here we can also introduce some third-party component libraries, such as
mint-ui
Before we write code, let's consider what we need to do:
props
inside a component
In order, we first implement the property editing capabilities of the components. W e need to consider what configurable information a component exposes. How this configurable information syncs to the background editing area for the consumer to edit, and the configurable information for a button might be like this:
If all of these configurations are written in the background library, loading different configurations according to the components currently selected can be quite cumbersome to maintain, and as the number of components increases, it can become bloated, so we can store these configurations on the server side, the background only needs to be parsed according to the rules of storage, for example, we can actually store such editing configurations:
[
{
"blockName": "按钮布局设置",
"settings": {
"src": {
"type": "input",
"require": true,
"label": "按钮文案"
}
}
}
]
We're in the editing background, and by requesting these configurations through the interface, we can render the rules:
/**
* 根据类型,选择创建对应的组件
* @param {VNode} vm
* @returns {any}
*/
createEditorElement (vm: VNode) {
let dom = null
switch (vm.config.type) {
case 'align':
dom = this.createAlignElement(vm)
break;
case 'select':
dom = this.createSelectElement(vm)
break;
case 'actions':
dom = this.createActionElement(vm)
break;
case 'vue-editor':
dom = this.createVueEditor(vm)
break;
default:
dom = this.createBasicElement(vm)
}
return dom
}
The first thing we need to consider is, how do components register? Because we need to render a component when it is selected by the user, we can provide a node script to traverse the required component and register the installation of the component:
// 定义渲染模板和路径
var OUTPUT_PATH = path.join(__dirname, '../packages/index.js');
console.log(chalk.yellow('正在生成包引用文件...'))
var INSTALL_COMPONENT_TEMPLATE = ' {{name}}';
var IMPORT_TEMPLATE = 'import {{componentName}} from \\\\'{{name}}\\\\'';
var MAIN_TEMPLATE = `/* Automatic generated by './compiler/build-entry.js' */
{{include}}
const components = [
{{install}}
]
const install = function(Vue) {
components.map((component) => {
Vue.component(component.name, component)
})
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export {
install,
{{list}}
}
`;
// 渲染引用文件
var template = render(MAIN_TEMPLATE, {
include: includeComponentTemplate.join(endOfLine),
install: installTemplate.join(`,${endOfLine}`),
version: process.env.VERSION || require('../package.json').version,
list: listTemplate.join(`,${endOfLine}`)
});
// 写入引用
fs.writeFileSync(OUTPUT_PATH, template);
The final rendered file is probably like this:
import WButton from 'w-button'
const components = [
WButton
]
const install = function(Vue) {
components.map((component) => {
Vue.component(component.name, component)
})
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export {
install,
WButton
}
This is also a common way to write component libraries, so the idea here is to publish components to
npm
aggregate them, aggregate them into a component package reference, and we need to introduce them in full when we edit them in the background:
import * as W_UI from '../../packages'
Vue.use(W_UI)
In this way, our components are registered, the component selection area, mainly to provide the component options, we can traverse the components, provide a List for the user to choose, of course, if each of our components only provides a component name, the user may not know what the component looks like, so we would do well to provide a thumbnail of what the component length is. H ere we can do this as well as through the node script when the component is released. There's more code to implement here, and I'll talk about the process, because it's not the core logic, but there's nothing left, it's better to have an experience:
puppeteer
to adjust the page to the phone side mode for the current dev-server screenshot.
This allows you to attach a thumbnail to a component when it loads the component selection area.
When a user selects a component in the selection area and we need to show it in the preview area, how do we know which components the user has selected? Y
ou can't write all the components to the rendering area in advance, can you tell the choice by
v-if
Of course not so stupid, Vue already offers the functionality of dynamic components:
<div
:class="[index===currentEditor ? 'active' : '']"
:is="select.name"
:data="select.data">
</div>
Why don't we use thumbnails instead of real components? On the one hand, there is a problem with the thumbnail size generated, on the other hand, we need the linkage of editing, that is, the editing of the editing area needs timely feedback to the user.
Having said that, everything seems to be going well, but in practice, it's clear that the preview area between us is actually trying to simulate the mobile page effect as much as possible. B
ut if we add components that contain something like
position: fixed
style, you'll see obvious problems with the style. T
ypical
Dialog Loading
are Dialog Loading. S
o we looked at the design of the
m-ui
component library and presented the intermediate preview operation container as an
iframe
R
esize
iframe
to
375 * 667
which simulates the phone side of the iPhone 6. T
his way, there will be no style problems. B
ut another difficulty arises: how does the editing data on the left react to
iframe
in a timely manner?
That's right, it's
postMessgae
and here's the general idea:
Use
vuex
as a data storage pool, and all changes are synchronized via
postMessgae
so that we can map changes to the render layer simply by ensuring that the data in the data pool changes.
For example, if we've made component selection and drag sorting in the preview area, we can simply synchronize the information via
vuex
// action.ts
const action = {
setCurrentPage ({commit, state}, page: number) {
// 更新当前store
commit('setCurrentPage',page)
// 对应postMessage
helper.postMsgToChild({type: 'syncState', value: state})
},
// ...
}
The design implementation of the template, I refer to
Vue-cli 2.x
version of the idea, put the template here, there is a corresponding
git
repository. W
hen the user needs to build the page, pull the corresponding template directly from the git repository. O
f course, after pulling, will also cache a copy on-premises, later rendering, directly from the local cache can be read. W
e now put the center on the format and specification of the template. I
t doesn't matter what syntax we use for templates, I'm using the same
Handlerbars
engine as
Vue-cli
Here's a direct look at the design of our template:
<template>
<div class="pg-index" :style="{backgroundColor: '{{bgColor}}'}">
<div class="main-container" :style="{
backgroundColor: '{{bgColor}}',
backgroundImage: '{{bgImage}}' ? 'url({{bgImage}})' : null,
backgroundSize: '{{bgSize}}',
backgroundRepeat: 'no-repeat'
}">
{{#components}}
<div class="cp-module-editor {{className}} {{data.className}}">
<{{name}} class="temp-component" :data="{{tostring data}}" data-type="{{upcasefirst name}}"></{{name}}>
</div>
{{/components}}
</div>
</div>
</template>
<script>
{{#noRepeatCpsName}}
import {{upcasefirst this}} from '{{this}}'
{{/noRepeatCpsName}}
export default {
name: '{{upcasefirst repoName}}',
components: {
{{#noRepeatCpsName}}
{{upcasefirst this}},
{{/noRepeatCpsName}}
}
}
</script>
To simplify the logic, we designed the templates to be streamlined, with all components stacked down in order. T
his file is
App.vue
in our
vue-webpack-simple
template. W
e've rewritten it. T
his allows a Vue single file to be rendered when the data is populated.
Here I only give one example, we can also implement multi-page templates and other complex templates, depending on the need to pull different templates can be.
When rendering requests are submitted in the background, the main work done by our node service is:
Pulling is to pull the template through the
download-git-repo
in the specified repository. C
ompilation is actually the use of
metalsmith
static template generators as input, data as padding, and rule rendering according to
handlebars
syntax. T
he final output
build
builds a well-built catalog. I
n this step, the components we needed earlier will be rendered into the
package.json
file.
Let's take a look at the core code:
// 这里就像一个管道,以数据入口为生成源,通过renderTemplateFiles编译产出到目标目录
function build(data, temp_dest, source, dest, cb) {
let metalsmith = Metalsmith(temp_dest)
.use(renderTemplateFiles(data))
.source(source)
.destination(dest)
.clean(false)
return metalsmith.build((error, files) => {
if (error) console.log(error);
let f = Object.keys(files)
.filter(o => fs.existsSync(path.join(dest, o)))
.map(o => path.join(dest, o))
cb(error, f)
})
}
function renderTemplateFiles(data) {
return function (files) {
Object.keys(files).forEach((fileName) => {
let file = files[fileName]
// 渲染方法
file.contents = Handlebars.compile(file.contents.toString())(data)
})
}
}
The last thing we got was a Vue project that couldn't run directly on the browser side, and here's what the current publishing system supports. W hat do you say? I f your company's publishing system needs to be compiled online, you can upload the source file directly to the git repository, triggering the repository's WebHook to have the publishing system send the project for you. I f your publishing system requires you to commit the compilation files after compilation for publication, then you can build locally with the node command and produce HTML, CSS, JS. J ust submit it directly to the publishing system. Here, our task is almost - the specific core solidity has been mostly elaborated clearly, if there are any problems and irregularities in the implementation, but also welcome to discuss exchanges together! !
Implementing such a page-building system, I've actually simplified a lot of things here to provide you with an idea. I n addition, in fact, our pages are all in the service side of the building output, we can re-service side of this layer to do a lot of work, such as page performance optimization, because the page data we all have, we can also do page pre-rendering, skeleton screen, ssr, compilation optimization and so on. And we can also do data analysis on the activity pages of the output - there is a lot of room for imagination.