preface

Surely you must have used yiqixiu or Baidu H5 micro scene generation tools to make cool H5 pages, in addition to lamenting its magic have you ever thought about its implementation? In this paper, an H5 editor project from scratch to achieve the complete design ideas and main implementation steps, and open source front-end and back-end code. You can follow this tutorial to create your own H5 editor from scratch. (It’s not complicated to implement, this tutorial is just an idea, not a best practice)

Github: Portal demo address: Portal

Editor preview:

Technology stack

Front-end: VUE: Modular development requires Angular, React, and VUE. Vue is chosen here. Vuex: state management sASS: CSS precompiler. Element-ui: Instead of building wheels, there is already a good library of VUE components to use. Do not have their own encapsulation of some can be. Loadsh: utility class

Server: KOA: NodeJS is the back-end language, koA documentation and learning materials are also more, express original team built, this is suitable. Mongodb: a flexible database based on distributed file storage.

Preparation before Reading

1. Understand vUE technology stack development 2. Koa 3

Engineering structures,

Based on vuE-CLI3 environment

  • How to plan the directory structure of our project? First we need to have a directory as a front-end project and a directory as a back-end project. Therefore, we need to modify the project structure generated by VUe-CLI:
... | - client / / SRC directory, To the client as a front-end project directory | - new server to server project directory server / / | - engine - the template / / new engine - the template for the page template library directory | - docs / / new Docs is reserved for project documentationCopy the code
  • In this case, we need to make some changes to our webpack configuration file. First of all, we need to change the directory used to compile SRC to client. Second, we need to add a new directory to babel-loader to enable NPM run build to compile client.

    • Js is added to the root directory. The purpose is to change the project entry to :client/main.js

          module.exports = {    
            pages: {        
              index: {            
                entry: "client/main.js"        
              }    
            }
          }
      Copy the code
    • Babel-loader can compile client and engine-template directories properly. Add the following configuration in vue.config.js

      // Expand webPack configuration chainWebpack: config => { config.module .rule('js') .include.add(/engine-template/).end() .include.add(/client/).end() .use('babel') .loader('babel-loader').tap(options => {// Modify its options... return options }) }Copy the code

So we set up a simple project directory structure.

Project directory structure

| - client -- -- -- -- -- -- -- -- the front-end interface code project | - common -- -- -- -- -- -- -- -- the front-end interface corresponding static resources | - components -- -- -- -- -- -- -- -- component | - config -- -- -- -- -- -- -- - | - eventBus configuration file -- -- -- -- -- -- -- -- eventBus | - filter filter -- -- -- -- -- -- -- - | -- mixins -- -- -- -- -- -- -- - | - pages with -- -- -- -- -- -- -- -- page | - the router -- -- -- -- -- -- -- - | - store routing configuration -- -- -- -- -- -- -- -- vuex state management | - service -- -- -- -- -- -- -- -- axios package | -- App. Vue -- -- -- -- -- -- -- -- the App | -- main. Js -- -- -- -- -- -- -- -- the entry file | -- permission. Js -- -- -- -- -- -- -- -- access control | - server -- -- -- -- -- -- -- -- the server program code | - confog -- -- -- -- -- -- -- -- the database link | - middleware -- -- -- -- -- -- -- -- middleware | - models -- -- -- -- -- -- -- -- Schema and the Model | - routes routing | -- -- -- -- -- -- -- -- -- views -- -- -- -- -- -- -- -- ejs page template | - public -- -- -- -- -- -- -- - | - static resources utils method -- -- -- -- -- -- -- -- tools | -- app. Js -- -- -- -- -- -- -- - | - the service side entry Before and after the common -- -- -- -- -- -- -- -- the public code modules (such as encryption) | - engine - the template -- -- -- -- -- -- -- -- page template engine, Use webpack provide reference page | packaged into js - docs -- -- -- -- -- -- -- -- reserve writing project documents directory | -- config. Json -- -- -- -- -- -- -- -- the configuration fileCopy the code

Front-end editor implementation

The implementation idea of the editor is as follows: the editor generates JSON data of the page, the server is responsible for accessing JSON data, and takes data JSON from the server to the front-end template for processing during rendering.

The data structure

To confirm the implementation logic, the data structure is also very important. To define a page as a JSON data structure, the data structure looks something like this:

Page engineering data interface

{title: ", // title description: ", // description: ", // cover auther: ", // author script: ", // page insert script width: Pages: [], // shareConfig: {}, // wechat shareConfig: {} 0, // Render mode for extended multi-mode rendering, page-turning H5 / long page /PC page etc.}Copy the code

Data structure of one of the pages:

BackgroundColor: ", backgrounddimage: ", backgroundSize: ", backgroundColor: ", backgroundColor: ", backgroundSize:" 'cover' }, config: {} }Copy the code

Element data structure:

{elName: ", // Component name animations: [], // Layer animations can support multiple animations [], // Event configuration data, each layer can add multiple event propsValue: {}, // attribute parameter value: ", // binding value valueType: 'String', // value type isForm: False // Is a form control used to get form data when the form is submitted}Copy the code

Editor overall design

  • A component selection area that allows users to select desired components
  • An edit preview palette that allows users to drag and drop page previews
  • A component property editor that gives users the ability to edit components internal props, common styles, and animations

As shown in figure:

The user selects components to add to the page in the left component area, and the editing area renders each element component through the dynamic component feature.

Finally, click Save to submit the page data to the database. There are many ways to convert data into static HTML. As well as page data we have all, we can do page pre-rendering, skeleton screen, SSR, compile time optimization and so on. And we can also do data analysis on the output of active pages ~ there is a lot of room for imagination.

The core code

Editor core code, based on Vue dynamic component features:

Attached is the official document of Vue: cn.vuejs.org/v2/api/#is

Artboard elements render

To edit the artboard, loop through the Pages [I]. Elements array, extract JSON data from elements, render each component through dynamic components, support drag and drop to change position size.

Element Component Management

Create plugins in the client directory to manage the component libraries. This component library can also be sent to the NPM project through NPM management

Component library

Write components with component libraries in mind, so it is possible to have our components support global import and on-demand import. If global import, then all components need to be registered with the Vue Component and exported:

Create an index.js entry file under client/plugins

/** */ import Text from './ Text 'const Components = [Text] Const install = function (Vue) {const install = function (Vue) { If (install.installed) return install.installed = true // Register all components components.map(Component => Vue.component(Component.name, Component)} // Execute only when Vue is detected, after all, we are based on Vue if (Typeof Window! == 'undefined' && window.vue) {install(window.vue)} export default {install, Vue. Use () Text} ' 'Copy the code

Component development

Example: Text text component

Create a text component directory under client/plugins

| - text -- -- -- -- -- -- -- -- text component | - SRC resources | -- -- -- -- -- -- -- -- -- index. Vue components -- -- -- -- -- -- -- - | -- index. Js -- -- -- -- -- -- -- -- the entranceCopy the code

text/index.js

// Provide the install method for the component, Import Component from './ SRC /index' component. install => {Vue.component(component. name, Component) } export default ComponentCopy the code

text/src/index.vue

<! --text.vue--> <template> <div class="qk-text"> {{text}} </div> </template> <script> export default { name: <qk-text></qk-text> </qk-text> props: {text: {type: String, default: </script> <style lang=" SCSS "scoped </style>Copy the code

Using component libraries in the editor:

Import QKUI from 'client/plugins/index' import QKUI from 'client/plugins/index' import QKUI from 'client/plugins/index' import Vue. Use (QKUI) // Use: <qk-text text=" This is a text "></qk-text>Copy the code

With this component development approach we can extend as many components as we want to enrich the component library

Note that the outer layer of the component is 100% wide and high

The configuration file

Optional components can be defined through a configuration file to create a new ele-config.js configuration file:

Export default [{title: 'base component ', components: [{elName: 'qk-text', // component name, same as component library name title:' text', icon: DefaultStyle: {height: 40}}]}, {title: 'Form component ', components: []}, {title: 'Components ', Components: []}, {title:' Components ', Components: []}]Copy the code

The public method provides a function to get the element component JSON by component name and defaultStyle, the getElementConfigJson(elName, defaultStyle) method

Element attribute editing

Public property style editing

Public style attribute editing is simple by editing the commonStyles field of the element JSON object

Props property editing

1. Develop a property editor component for each prop property of the component. For example, if the QkText component requires the text property, add an attr-qk-text component to handle this property 2. Obtaining a prop object 3. Traverse the prop object key to determine which properties to display to edit components

Element to add animation implementation

Animation effects are imported into the Animate. CSS animation library. Element component animation that can support multiple animations. The data is stored in the element JSON object animations array.

Select the Hover preview animation panel

Listen for mouseover and mouseleave, add the animation className to the element when the mouse moves in, and remove the animation lassName when the mouse moves out. This implements hover preview animation

Edit preview animation

Component editing supports animation preview and individual animation preview.Encapsulates an animation execution method

/** * Add animation CSS to the element, Returns promise to perform subsequent actions (resetting the animation) * @param $el the element of the animation currently being executed * @param animationList animationList * @param isDebugger animationList * @returns {Promise<void>} */ export default async function runAnimation($el, animationList = [], isDebug , callback){ let playFn = function (animation) { return new Promise(resolve => { $el.style.animationName = animation.type ${$el. Style. AnimationDuration = ` animation. Duration} s ` / / if it is looping will cycles were set to 1, This effectively avoid editing because preview looping animation can't trigger animationend to suspend the component animation $el. Style. AnimationIterationCount = animation. Infinite? (isDebug ? 1 : 'infinite') : animation.interationCount $el.style.animationDelay = `${animation.delay}s` $el.style.animationFillMode = 'both' let resolveFn = function(){ $el.removeEventListener('animationend', resolveFn, false); $el.addEventListener('animationcancel', resolveFn, false); resolve() } $el.addEventListener('animationend', resolveFn, false) $el.addEventListener('animationcancel', resolveFn, false); }) } for(let i = 0, len = animationList.length; i < len; i++){ await playFn(animationList[i]) } if(callback){ callback() } }Copy the code

AnimationIterationCount If the animation is executed only once in edit mode, the animationEnd event will not be heard otherwise

Caches the element style before executing the animation, and assigns the original style to the element when the animation is finished

let cssText = this.$el.style.cssText;
runAnimations(this.$el, animations, true, () => {
	this.$el.style.cssText = cssText
})
Copy the code

Element add event

Provide event mixins to blend into the component, with each event method returning a promise and executing the event methods in order when the element is clicked

Page insert JS script

Refer to Baidu H5 to embed the script in the form of script tags. Execute after the page loads. Mixins can also be incorporated into a page or component, which can be extended according to business needs.

Redo /undo historical operations

  1. Operation record existence state machine store. State. Editor. HistoryCache array.
  2. Each change edit pushes the entire pageDataJson field to historyCache
  3. Redo the page by retrieving pageDataJson from index when redo/undo is clicked

PSD design drawing import generates H5 page

Export each layer of each PSD design to a static resource server.

PSD dependencies are installed on the server

cnpm install psd --save
Copy the code

Add psD.js dependencies and provide interfaces to process data

var PSD = require('psd'); router.post('/psdPpload',async ctx=>{ const file = ctx.request.files.file; Let PSD = await psd.open (file.path) var timeStr = + new Date(); let descendantsList = psd.tree().descendants(); descendantsList.reverse(); let psdSourceList = [] let currentPathDir = `public/upload_static/psd_image/${timeStr}` for (var i = 0; i < descendantsList.length; i++){ if (descendantsList[i].isGroup()) continue; if (! descendantsList[i].visible) continue; try{ await descendantsList[i].saveAsPng(path.join(ctx.state.SERVER_PATH, currentPathDir + `/${i}.png`)) psdSourceList.push({ ... descendantsList[i].export(), type: 'picture', imageSrc: Ctx.state. BASE_URL + '/upload_static/psd_image/${timeStr}/${I}.png',})}catch (e) {ctx.state.BASE_URL + '/upload_static/psd_image/${timeStr}/${I}.png',})}catch (e) { } } ctx.body = { elements: psdSourceList, document: psd.tree().export().document }; })Copy the code

Finally, the obtained data is escaped and returned to the front end. After the front end obtains the data, it uses the unified method of the system to traverse and add the unified picture component

  • PSD source files should not be larger than 30M, which will cause the browser to freeze or even freeze
  • Merge layers as much as possible and rasterize all layers
  • More complex layer styles, such as Filter, layer Style, etc., cannot be read

Html2canvas generates thumbnails

Here we only need to pay attention to the problem of cross-domain image. Html2canvas: Proxy solution is officially provided. It converts the image to base64 format and uses proxy (theProxyURL) to access the image transformed into a good format under theProxyURL when drawing the cross-domain image, thus solving the problem of canvas pollution. Provide a cross-domain interface

/ / router. Get ('/html2canvas/corsproxy', async ctx => { ctx.body = await request(ctx.query.url) })Copy the code

Apply colours to a drawing template

Implementation logic

Create a new swiper-h5-engine page component in the engine-template directory. This component receives JSON data and renders the page. Similar to edit preview palette implementation logic.

Then use vue-CLI library packaging to package the components into the engine.js library file. The EJS template introduces this page component and renders the page with JSON data

Adaptation scheme

New = old * windows.x/pagejson.width; new = old * windows.x/pagejson.width; Pagejson.width is an initial value for the page and the default width for editing, and the viewport uses device-width. 2. Full-screen background. The page is vertically centered

Page vertical center only applies to full-screen H5, and will not be required for extended long and PC pages.

The template package

New package command in package.json

"lib:h5-swiper": "vue-cli-service build --target lib --name h5-swiper --dest server/public/engine_libs/h5-swiper engine-template/engine-h5-swiper/index.js"

Run NPM run lib:h5-swiper to generate engine template JS as shown

Page rendering

Importing templates into EJS

<script src="/third-libs/swiper.min.js"></script>

Using the component

<engine-h5-swiper :pageData="pageData" />

The back-end service

Initialize the project

The project directory is given above and can also be generated using the KOA-Generator scaffolding tool

Ejs-template Template engine configuration

app.js

Ejs-template render(app, {root: path.join(__dirname, 'views'), layout: false, viewExt: 'HTML ', cache: false, debug: false });Copy the code

Koa -static Static resource service

Because HTML2Canvas requires images to be cross-domain, set ‘access-Control-allow-Origin ‘:’*’ for all resource requests in static resource services.

app.js

Use (koaStatic(__dirname + '/public'), {gzip: true, setHeaders: function(res){ res.header( 'Access-Control-Allow-Origin', '*') }});Copy the code

Change the route registration mode and read the routes file through the routes folder

app.js

const fs =  require('fs')
fs.readdirSync(path.join(__dirname,'./routes')).forEach(route=> {
    let api = require(`./routes/${route}`)
    app.use(api.routes(), api.allowedMethods())
})
Copy the code

Add JWT authentication and filter routes that do not require authentication, such as obtaining a token

app.js

const jwt = require('koa-jwt')
app.use(jwt({ secret: 'yourstr' }).unless({
    path: [
        /^\/$/, /\/token/, /\/wechat/,
        { url: /\/papers/, methods: ['GET'] }
    ]
}));
Copy the code

Middleware implements uniform interface return data format, global error capture and response

middleware/formatresponse.js

module.exports = async (ctx, next) => { await next().then(() => { if (ctx.status === 200) { ctx.body = { message: 'success ', code: 200, body: ctx.body, status: True}} else if (CTX) status = = = 201) {/ / 201 process template rendering engine} else {CTX. Body = {message: CTX. Body | | 'interface is unusual, please try again, the code: Ctx. status, body: 'Interface request failed ', status: false}}}). Catch ((err) => {if (err. Status === 401) {ctx.status = 401; Ctx. body = {code: 401, status: false, message: 'Login expired, please login again'}} else {throw err}})}Copy the code

Koa2-cors cross-domain processing

When the interface is published online and the front end makes ajax requests, cross-domain errors are reported. Koa2 uses the koA2-CORS library to make cross-domain configuration very convenient and easy to use

const cors = require('koa2-cors');
app.use(cors());
Copy the code

Connecting to a Database

We use the mongodb database and the mongoose library in KOA2 to manage the operation of the entire database.

  • Creating a Configuration File

Create a config folder in the root directory and create mongo.js

// config/mongo.js const mongoose = require('mongoose').set('debug', true); const options = { autoReconnect: True} // username database username // password database password // localhost database IP address // dbname database name const url = 'mongodb://username:password@localhost:27017/dbname' module.exports = { connect: Connect (url,options) let db = mongoose. Connection db.on('error', console.error. Bind (console, 'connection error :')); db.once('open', ()=> { console.log('mongodb connect suucess'); }}})Copy the code

The mongodb configuration information is managed in config.json

  • Then it’s introduced in app.js
const mongoConf = require('./config/mongo');
mongoConf.connect();
Copy the code

. The specific interface of the server is not introduced in detail, that is, the increase, deletion, change and check of the page, and the user’s login registration is not difficult

Is up and running

Start the front-end

npm run dev-client
Copy the code

Starting the server

npm run dev-server
Copy the code

Note: if you did not generate engine template js file, you need to edit the engine template first, otherwise the preview page load page engine

Compile the engine. Js template engine
npm run lib:h5-swiper
Copy the code

🏆 technology project phase iii | data visualization of those things…