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
- Operation record existence state machine store. State. Editor. HistoryCache array.
- Each change edit pushes the entire pageDataJson field to historyCache
- 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…