Pagemaker is a front end page making tool, convenient product, operation and visual students quickly develop simple front end pages, thus can liberate the front end students’ workload. The idea of this project comes from Pagemaker project in nFOP, an internal project of netease Lede. The front-end of the original project is done by using jquery and template EJS, and each component update will redraw the whole DOM, which is not very good performance. React was very popular at that time, and the project itself was suitable, so WE finally decided to use React to test the waters. Because the original whole project is a lot of subprojects together, so the implementation of the background is no reference, completely rewrite.

This project is just a simple implementation of the original project, with less used and complex components removed. However, this project adopts a whole set of React stack, which is suitable for those students who have learned react in the early stage and want to deepen their understanding and practice through demo. React stack Immutable (mutable_mutable_mutable_immutable)

Online address

I. Functional features

  1. Component richness. There are titles, images, buttons, text, audio, video, statistics, JSCSS input.
  2. Live preview. You can see the latest preview immediately with each change.
  3. The configuration file can be exported in three import modes.
  4. Support to restore the site function (close the page configuration is not lost)
  5. Supports Undo/Redo operations. (Trigger point when the number of components changes)
  6. You can publish, modify, and delete published pages at any time.
  7. The password of this project adopts bcrypt code, so it will not leak its password even if it is dragged into the library.
  8. Each page has a publish password, which can be easily managed by many people and can prevent others from changing.
  9. The react+ Redux front-end architecture and imMUTABLE data structure are adopted. Each component update can be minimized to maximize page performance.
  10. The background automatically compresses uploaded pictures to prevent large files
  11. Adaptive mobile terminal

Second, the technology used

1. The front end

  1. React
  2. Redux
  3. React-Redux
  4. Immutable
  5. React-Router
  6. fetch
  7. es6
  8. es7

2. The background

  1. Node
  2. Express

3. The tool

  1. Webpack
  2. Sass
  3. Pug

Scaffolding tools

Because the project uses a lot of technology, using scaffolding tools can save us time to build the project. After searching, I found that there are three that are used more:

  1. create-react-app
    The create – react – app star number
  2. react-starter-kit
    React — starter kit number of star
  3. react-boilerplate
    The react – boilerplate star number

Github has a lot of stars, starting with Facebook’s React Demo. However, all three projects were quite large and introduced many unnecessary feature packs. A bit of research turned up a handy scaffold tool: Yeoman, which you can choose from as a generator. I chose the React – WebPack. Redux, IMMUTABLE, express. In fact, it is good to practice their ability to build projects.

Core code analysis

1. Store

A Store is a place where data is stored, you can think of it as a container. There can only be one Store for the entire app.

import { createStore } from 'redux';
import { combineReducers } from 'redux-immutable';

import unit from './reducer/unit';
// import content from './reducer/content';

let devToolsEnhancer = null;
if (process.env.NODE_ENV === 'development') {
    devToolsEnhancer = require('remote-redux-devtools');
}

const reducers = combineReducers({ unit });
let store = null;
if (devToolsEnhancer) {
    store = createStore(reducers, devToolsEnhancer.default({ realtime: true.port: config.reduxDevPort }));
}
else {
    store = createStore(reducers);
}
export default store;
Copy the code

Redux provides the createStore function to generate a Store. Because there is only one State object in the whole application, which contains all data, this State must be very large for a large application, resulting in a large Reducer function. Redux provides a combineReducers method for the Reducer split. You just define the individual Reducer functions and, using this method, combine them into one large Reducer. Of course, we only have a Reducer of unit here, which can be disassembled or not.

DevToolsEnhancer is middleware. Used to debug Redux using Redux DevTools in the development environment.

2. Action

Action describes what is currently happening. The only way to change State is to use Action. It will ship data to the Store.

import Store from '.. /store';

const dispatch = Store.dispatch;

const actions = {
    addUnit: (name) = > dispatch({ type: 'AddUnit', name }),
    copyUnit: (id) = > dispatch({ type: 'CopyUnit', id }),
    editUnit: (id, prop, value) = > dispatch({ type: 'EditUnit', id, prop, value }),
    removeUnit: (id) = > dispatch({ type: 'RemoveUnit', id }),
    clear: (a)= > dispatch({ type: 'Clear'}),
    insert: (data, index) = > dispatch({ type: 'Insert', data, index}),
    moveUnit: (fid, tid) = > dispatch({ type: 'MoveUnit', fid, tid }),
};

export default actions;
Copy the code

If the State changes, the View changes. However, the user does not touch State, only View. Therefore, the change in State must be caused by the View. An Action is a notification from the View that the State should change. In the code, we define an Actions object that has a number of properties, each of which is a function. The output of the function is an action object dispatched via store.dispatch. Action is an action that contains the required Type attribute, along with additional information.

3. Immutable

Immutable Data is Data that, once created, cannot be changed. Any modification or addition or deletion of an Immutable object returns a new Immutable object. React: Immutable mutable Our project uses the Immutable. Js library that Facebook engineer Lee Byron spent three years building. Specific API you can go to the official website to learn.

Remember that shouldComponentUpdate() is a great way to optimize React performance, but it returns true by default, so render() is always executed and the Virtual DOM comparison is performed. And figure out if you need to do a real DOM update, which often leads to a lot of unnecessary rendering and a performance bottleneck. We can also use deepCopy and deepCompare in shouldComponentUpdate() to avoid unnecessary render(), but deepCopy and deepCompare are usually very expensive.

Immutable provides a simple and efficient way to tell whether data is mutable by comparing ===(address comparison) with is(value comparison) to see if render() is required, which costs almost nothing and can greatly improve performance. The modified shouldComponentUpdate looks like this:

import { is } from 'immutable';

shouldComponentUpdate: (nextProps = {}, nextState = {}) = > {
  const thisProps = this.props || {}, thisState = this.state || {};

  if (Object.keys(thisProps).length ! = =Object.keys(nextProps).length ||
      Object.keys(thisState).length ! = =Object.keys(nextState).length) {
    return true;
  }

  for (const key in nextProps) {
    if(thisProps[key] ! [key] = = nextProps | |! is(thisProps[key], nextProps[key])) {return true; }}for (const key in nextState) {
    if(thisState[key] ! [key] = = nextState | |! is(thisState[key], nextState[key])) {return true; }}return false;
}
Copy the code

By using Immutable, when the state of a red node changes, it does not render all nodes in the tree, but only the green parts of the image:

Immutable demo

In this project, we use pure-render-decorator that supports class syntax. What we want to achieve is that when we edit the properties of a component, the other components are not rendered, and in Preview, only the modified Preview component is updated, while the other Preview components are not rendered. To make it easier to see if the component is rendered, we artificially add a data-id attribute to the component with a random value of math.random (). The effect is shown below:

Immutable actual illustration

As you can see, when we change the title text of the title component, only the title component and the title preview component will be re-rendered, but not the other components and preview components. This is where immutable provides a performance boost. In the original project, rendering would stagger and sometimes even go black for a short time when there were too many components.

4. Reducer

When the Store receives the Action, it must give a new State for the View to change. This State calculation process is called Reducer.

import immutable from 'immutable';

const unitsConfig = immutable.fromJS({
    META: {
        type: 'META'.name: 'META Information Configuration '.title: ' '.keywords: ' '.desc: ' '
    },
    TITLE: {
        type: 'TITLE'.name: 'title'.text: ' '.url: ' '.color: '# 000'.fontSize: "middle".textAlign: "center".padding: [0.0.0.0].margin: [10.0.20.0]},IMAGE: {
        type: 'IMAGE'.name: 'images'.address: ' '.url: ' '.bgColor: '#fff'.padding: [0.0.0.0].margin: [10.0.20.0]},BUTTON: {
        type: 'BUTTON'.name: 'button'.address: ' '.url: ' '.txt: ' '.margin: [
            0.30.20.30].buttonStyle: "yellowStyle".bigRadius: true.style: 'default'
    },
    TEXTBODY: {
        type: 'TEXTBODY'.name: The 'body'.text: ' '.textColor: '# 333'.bgColor: '#fff'.fontSize: "small".textAlign: "center".padding: [0.0.0.0].margin: [0.30.20.30].changeLine: true.retract: true.bigLH: true.bigPD: true.noUL: true.borderRadius: true
    },
    AUDIO: {
        type: 'AUDIO'.name: 'audio'.address: ' '.size: 'middle'.position: 'topRight'.bgColor: '#9160c3'.loop: true.auto: true
    },
    VIDEO: {
        type: 'VIDEO'.name: 'video'.address: ' '.loop: true.auto: true.padding: [0.0.20.0]},CODE: {
        type: 'CODE'.name: 'JSCSS'.js: ' '.css: ' '
    },
    STATISTIC: {
        type: 'STATISTIC'.name: 'statistics'.id: ' '}})const initialState = immutable.fromJS([
    {
        type: 'META'.name: 'META Information Configuration '.title: ' '.keywords: ' '.desc: ' '.// Very important property indicating which component this state change comes from!
        fromType: ' '}]);function reducer(state = initialState, action) {
    let newState, localData, tmp
    // Initialize fetching data from localStorage
    if (state === initialState) {
        localData = localStorage.getItem('config'); !!!!! localData && (state = immutable.fromJS(JSON.parse(localData)));
        // Initialization of sessionStorage
        sessionStorage.setItem('configs'.JSON.stringify([]));
        sessionStorage.setItem('index'.0);
    }
    switch (action.type) {
        case 'AddUnit': {
            tmp = state.push(unitsConfig.get(action.name));
            newState = tmp.setIn([0.'fromType'], action.name);
            break
        }
        case 'CopyUnit': {
            tmp = state.push(state.get(action.id));
            newState = tmp.setIn([0.'fromType'], state.getIn([action.id, 'type']));
            break
        }
        case 'EditUnit': {
            tmp = state.setIn([action.id, action.prop], action.value);
            newState = tmp.setIn([0.'fromType'], state.getIn([action.id, 'type']));
            break
        }
        case 'RemoveUnit': {
            const type = state.getIn([action.id, 'type']);
            tmp = state.splice(action.id, 1);
            newState = tmp.setIn([0.'fromType'], type);
            break
        }
        case 'Clear': {
            tmp = initialState;
            newState = tmp.setIn([0.'fromType'].'ALL');
            break
        }
        case 'Insert': {
            tmp = immutable.fromJS(action.data);
            newState = tmp.setIn([0.'fromType'].'ALL');
            break
        }
        case 'MoveUnit': {const {fid, tid} = action;
            const fitem = state.get(fid);
            if(fitem && fid ! = tid) { tmp = state.splice(fid,1).splice(tid, 0, fitem);
            } else {
                tmp = state;
            }
            newState = tmp.setIn([0.'fromType'].' ');
            break;
        }
        default:
            newState = state;
    }
    // Update localStorage for site recovery
    localStorage.setItem('config'.JSON.stringify(newState.toJS()));

    // Undo, restore operation (only with the change of the number of components as the trigger point, otherwise there is no need to store huge data)
    let index = parseInt(sessionStorage.getItem('index'));
    let configs = JSON.parse(sessionStorage.getItem('configs'));
    if(action.type == 'Insert' && action.index){
        sessionStorage.setItem('index', index + action.index);
    }else{
        if(newState.toJS().length ! = state.toJS().length){// The number of components has changed, delete all configs after index pointer state, and take this changed config as the latest record
            configs.splice(index + 1, configs.length - index - 1.JSON.stringify(newState.toJS()));
            sessionStorage.setItem('configs'.JSON.stringify(configs));
            sessionStorage.setItem('index', configs.length - 1);
        }else{
            // The number of components does not change, index does not change. But update the stored Config configuration
            configs.splice(index, 1.JSON.stringify(newState.toJS()));
            sessionStorage.setItem('configs'.JSON.stringify(configs)); }}// console.log(JSON.parse(sessionStorage.getItem('configs')));
    return newState
}

export default reducer;
Copy the code

Reducer is a function that takes Action and the current State as parameters and returns a new State. UnitsConfig is a collection of objects that stores the initial configuration of each component, from which all newly added components take their initial values. State has an initial value: initialState, which contains META components. Because every Web page must have one META and only one META, it is not included in the list of components on the left side of the page.

Reducer operations are performed based on action types. It is important to note, however, that assignments are made after an IMmutable data operation. We always modify the fromType value at the end of each session, because some components, such as AUDIO and CODE, need to re-execute the js CODE preview to take effect, while other components need not be executed to improve performance.

Of course, we also do localStorage, and thanks to the immutable data structure, we implement Redo/Undo. The Redo/Undo function only generates a version when the number of components changes. Otherwise, too much information is stored, which may affect performance. Of course, if the component information changes, we’ll update the array.

5. Workflow

As shown below:

The flow chart of story

The user can only touch the view layer, is the component of the various input boxes, radio and multiple options, etc. The user interacts with it and issues an action. React-redux provides the connect method for generating container components from UI components. The connect method takes two arguments: MapDispatchToProps = mapDispatchToProps = mapDispatchToProps = mapDispatchToProps = mapDispatchToProps = mapDispatchToProps But to make it easy to write and intuitive to see where the action is issued, we don’t follow this API, but write it directly in the code.

The Store then automatically calls the Reducer with two parameters: the current State and the received Action. The Reducer returns the new State. Whenever State changes, Store calls the listener function. In the React-Redux rule, we need to provide the mapStateToProps function to establish a mapping between the (external) state object and the (UI component) props object. MapStateToProps subscribes to the Store, and every time state is updated, it automatically recalculates the PARAMETERS of the UI component, triggering a re-rendering of the UI component. You can see the final code of our Content.js component:

export default connect(
    state= > ({
        unit: state.get('unit'),
    })
)(Content);
Copy the code

The connect method can omit the mapStateToProps parameter so that the UI component does not subscribe to the Store, meaning that Store updates do not cause UI component updates. Components like header and Footer are pure UI components.

The reason why our child components get state is because we have a layer of components wrapped around the topmost component. The entry file index.js has the following code:

import "babel-polyfill";
import React from 'react';
import ReactDom from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';

import './index.scss';

import Store from './store';

import App from './components/app';

ReactDom.render(
    <Provider store={Store}>
        <Router history={browserHistory}>
            <Route path="/" component={App}>

            </Route>
        </Router>
    </Provider>.document.querySelector('#app'));Copy the code

Our React-Router uses browserHistory, using HTML5’s History API, and switches the route to the background.

5. Instructions for use

The left column is the list of components, which can be seen by clicking the double right arrow in the upper left corner on the mobile side. Click the corresponding component, and the corresponding component information will appear in the middle of the web page. Click the resulting component header to toggle expanding and hiding. Updated component information. The real-time preview is displayed on the right. To move the terminal, click the yellow button in the lower right corner (drag is supported).

At the top of the middle area is a content configuration area. The left side has import, export and clear functions. Import Support Supports the import of JSON configuration files, which can be generated when we are ready to publish the configuration by clicking export. You can also directly enter the name of the publication directory, for example: LMLC; Or enter a complete online address, such as: https://pagemaker.wty90.com/release/lmlc.html; Pasting configuration file content is also supported. Clearing clears all currently configured components. To the right of the content configuration area is the Redo/Undo function. For the sake of performance, only the change in the number of components is used as the trigger point.

The preview area is on the right. As soon as the content in the middle area changes, the right side will be updated in real time. When the project is configured and you want to publish, click the publish button in the upper left corner of the right area and a popup window will appear. The first input box is the publishing directory, and if it’s a new project you need to create a publishing password. If you want to update an existing project, you need to confirm the publication password. The platform password is pagemaker. To change the value, change the value of the password.json file in the data folder. We use bcrypt encoding. You can go to the BCrypt Calculator website and you can easily calculate the code values. There is a view button in the upper right to see pages that have been published using Pagemaker.

Hidden features: Hitting the iPhone’s home button in the preview area will bring up a pop-up to clean up unwanted files, because downloading files creates a cached file on the server. There are also some pictures uploaded by users that have not been published and will be accumulated on the server side. This requires providing the background password to change the password of the same platform in the data folder server_code.json file. This function is for the administrator, ordinary users do not need to ignore.

Compatibility and packaging optimization

1. The compatibility

In order to make the page more compatible with IE9+ and Android browsers, babel-Polyfill and babel-plugin-transform-Runtime plug-ins are adopted since Babel is used in the project.

2. Antd is loaded on demand

Antd complete package is very large, more than 10M. In our project, popover components are mainly used, so we should use on-demand loading. Just configure it in the.babelrc file, as described in the official instructions.

3. Configure externals for webpack

The main.js package at the end of the project is very large, approaching more than 10M. I searched a lot of methods on the web and found that webpack’s method of configuring the externals property is very good. You can use PC to download multiple files in parallel to reduce the pressure and traffic of your server. At the same time, you can use CDN cache resources. The configuration is as follows:

externals: {
    "jquery": "jQuery"."react": "React"."react-dom": "ReactDOM".'CodeMirror': 'CodeMirror'.'immutable': 'Immutable'.'react-router': 'ReactRouter'
}
Copy the code

The externals attribute tells Webpack that the following resources are not packaged and brought in from outside. They are usually public files, such as jquery, React, etc. NPM install will warn packages that rely on these public files because these files are imported from outside. After processing, the main.js file size was reduced to 3.7m, and then gzip was compressed under the Nginx configuration to bring the file size down to 872KB. Since the loading of files is slow on mobile, I added loading effect to the page.

Welcome to star learning communication: making the | my blog address