Table of Contents
1. Functional features
2. Technologies used
1. Front-end
2. Backend
3. Tools
3. Scaffolding tools
4. Core code analysis
1. Store
2. Action
3. Immutable
4. Reducer
5. 工作流程
五、兼容性和打包优化
1. 兼容性
2. Antd按需加载
3. webpack配置externals属性
Home Web Front-end HTML Tutorial Detailed explanation of front-end page creation tool pagemaker

Detailed explanation of front-end page creation tool pagemaker

Feb 23, 2018 pm 01:24 PM
make tool

Pagemaker is a front-end page creation tool that facilitates product, operation, and visual students to quickly develop simple front-end pages, thereby liberating the workload of front-end students. The idea for this project comes from the pagemaker project in NetEase's internal project nfop. It turns out that the front end of the project is made using jquery and template ejs. Every time the component is updated, the entire dom will be redrawn, and the performance is not very good. Because react was very popular at the time, and the project itself was suitable, we finally decided to use react to test the waters. Because the entire project originally included many sub-projects, the backend implementation had no reference and was completely rewritten.

This project is just a simple implementation of the original project, removing less used and complex components. But although Sparrow is small and has all the internal organs, this project uses a complete set of react technology stacks, which is suitable for students who have previously studied react and want to deepen their understanding and practice through demos. It is recommended that before studying this demo, you first study/review relevant knowledge points: React technology stack tutorial series, Immutable detailed explanation and practice in React.

1. Functional features

  1. Rich components. There are titles, pictures, buttons, text, audio, video, statistics, and jscss input.

  2. Real-time preview. Every time you make a modification, you can immediately see the latest preview.

  3. Supports three import methods and supports exporting configuration files.

  4. Supports Undo/Redo operations. (Changes in the number of components are trigger points)

  5. You can publish, modify, or delete published pages at any time.

  6. Each page has a publishing password to prevent others from modifying it.

  7. The front-end architecture of the page uses react+redux and uses immutable data structure. Each component update can be minimized to optimize page performance.

  8. The background automatically compresses the uploaded images to prevent the files from being too large

  9. Adapted to the mobile terminal

2. Technologies used

1. Front-end

  1. React

  2. Redux

  3. React-Redux

  4. Immutable

  5. React-Router

  6. fetch

  7. es6

  8. es7

2. Backend

  1. Node

  2. Express

3. Tools

  1. Webpack

  2. Sass

  3. Pug

3. Scaffolding tools

Because the project uses more technologies , using scaffolding tools can save us time in building projects. After searching, I found that there are three commonly used ones:

  1. create-react-app Detailed explanation of front-end page creation tool pagemaker

  2. react-starter-kit Detailed explanation of front-end page creation tool pagemaker

  3. react-boilerplate Detailed explanation of front-end page creation tool pagemaker

The number of stars on github is very high. The first one is the react demo officially released by Facebook. But looking at it, the three projects are relatively large and introduce many unnecessary feature packages. After some searching, I found a useful scaffolding tool: yeoman. You can choose the corresponding generator. I chose react-webpack. The project is relatively simple and requires everyone to build their own redux and immutable environments, as well as backend express. In fact, it’s okay to exercise your ability to build projects.

4. Core code analysis

1. Store

Store is the place where data is saved. You can think of it as a container. There can only be one Store for the entire application.

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 after login

Redux provides the createStore function to generate a Store. Since the entire application has only one State object, which contains all data, for large applications, this State must be very large, resulting in a very large Reducer function. Redux provides a combineReducers method for splitting Reducers. You only need to define each sub-Reducer function, and then use this method to combine them into a large Reducer. Of course, we only have one unit Reducer here, which can be split or not.

devToolsEnhancer is a middleware. Used to use Redux DevTools to debug redux 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 the 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: () => dispatch({ type: 'Clear'}),
    insert: (data, index) => dispatch({ type: 'Insert', data, index}),
    moveUnit: (fid, tid) => dispatch({ type: 'MoveUnit', fid, tid }),
};

export default actions;
Copy after login

Changes in State will lead to changes in View. However, the user cannot access the State, only the View. Therefore, changes in State must be caused by View. Action is a notification sent by View, indicating that State should change. In the code, we define the actions object, which has many attributes, and each attribute is a function. The output of the function is to dispatch an action object, which is sent through Store.dispatch. Action is a type attribute that contains the necessary type attributes, as well as other incidental information.

3. Immutable

Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。详细介绍,推荐知乎上的Immutable 详解及 React 中实践。我们项目里用的是Facebook 工程师 Lee Byron 花费 3 年时间打造的immutable.js库。具体的API大家可以去官网学习。

熟悉 React 的都知道,React 做性能优化时有一个避免重复渲染的大招,就是使用 shouldComponentUpdate(),但它默认返回 true,即始终会执行 render() 方法,然后做 Virtual DOM 比较,并得出是否需要做真实 DOM 更新,这里往往会带来很多无必要的渲染并成为性能瓶颈。当然我们也可以在 shouldComponentUpdate() 中使用使用 deepCopy 和 deepCompare 来避免无必要的 render(),但 deepCopy 和 deepCompare 一般都是非常耗性能的。

Immutable 则提供了简洁高效的判断数据是否变化的方法,只需 ===(地址比较) 和 is( 值比较) 比较就能知道是否需要执行 render(),而这个操作几乎 0 成本,所以可以极大提高性能。修改后的 shouldComponentUpdate 是这样的:

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] !== nextProps[key] || !is(thisProps[key], nextProps[key])) {
      return true;
    }
  }

  for (const key in nextState) {
    if (thisState[key] !== nextState[key] || !is(thisState[key], nextState[key])) {
      return true;
    }
  }
  return false;
}
Copy after login

使用 Immutable 后,如下图,当红色节点的 state 变化后,不会再渲染树中的所有节点,而是只渲染图中绿色的部分:

Detailed explanation of front-end page creation tool pagemaker

本项目中,我们采用支持 class 语法的 pure-render-decorator 来实现。我们希望达到的效果是:当我们编辑组件的属性时,其他组件并不被渲染,而且preview里,只有被修改的preview组件update,而其他preview组件不渲染。为了方便观察组件是否被渲染,我们人为的给组件增加了data-id的属性,其值为Math.random()的随机值。效果如下图所示:

immutable实际效果图

4. Reducer

Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。

import immutable from 'immutable';

const unitsConfig = immutable.fromJS({
    META: {
        type: 'META',
        name: 'META信息配置',
        title: '',
        keywords: '',
        desc: ''
    },
    TITLE: {
        type: 'TITLE',
        name: '标题',
        text: '',
        url: '',
        color: '#000',
        fontSize: "middle",
        textAlign: "center",
        padding: [0, 0, 0, 0],
        margin: [10, 0, 20, 0]
    },
    IMAGE: {
        type: 'IMAGE',
        name: '图片',
        address: '',
        url: '',
        bgColor: '#fff',
        padding: [0, 0, 0, 0],
        margin: [10, 0, 20, 0]
    },
    BUTTON: {
        type: 'BUTTON',
        name: '按钮',
        address: '',
        url: '',
        txt: '',
        margin: [
            0, 30, 20, 30
        ],
        buttonStyle: "yellowStyle",
        bigRadius: true,
        style: 'default'
    },
    TEXTBODY: {
        type: 'TEXTBODY',
        name: '正文',
        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: '音频',
        address: '',
        size: 'middle',
        position: 'topRight',
        bgColor: '#9160c3',
        loop: true,
        auto: true
    },
    VIDEO: {
        type: 'VIDEO',
        name: '视频',
        address: '',
        loop: true,
        auto: true,
        padding: [0, 0, 20, 0]
    },
    CODE: {
        type: 'CODE',
        name: 'JSCSS',
        js: '',
        css: ''
    },
    STATISTIC: {
        type: 'STATISTIC',
        name: '统计',
        id: ''
    }
})

const initialState = immutable.fromJS([
    {
        type: 'META',
        name: 'META信息配置',
        title: '',
        keywords: '',
        desc: '',
        // 非常重要的属性,表明这次state变化来自哪个组件!
        fromType: ''
    }
]);


function reducer(state = initialState, action) {
    let newState, localData, tmp
    // 初始化从localstorage取数据
    if (state === initialState) {
        localData = localStorage.getItem('config');
        !!localData && (state = immutable.fromJS(JSON.parse(localData)));
        // 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;
    }
    // 更新localstorage,便于恢复现场
    localStorage.setItem('config', JSON.stringify(newState.toJS()));

    // 撤销,恢复操作(仅以组件数量变化为触发点,否则存储数据巨大,也没必要)
    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){
            // 组件的数量有变化,删除历史记录index指针状态之后的所有configs,将这次变化的config作为最新的记录
            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{
            // 组件数量没有变化,index不变。但是要更新存储的config配置
            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 after login

Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。unitsConfig是存储着各个组件初始配置的对象集合,所有新添加的组件都从里边取初始值。State有一个初始值:initialState,包含META组件,因为每个web页面必定有一个META信息,而且只有一个,所以页面左侧组件列表里不包含它。

reducer会根据action的type不同,去执行相应的操作。但是一定要注意,immutable数据操作后要记得赋值。每次结束后我们都会去修改fromType值,是因为有的组件,比如AUDIO、CODE等修改后,预览的js代码需要重新执行一次才可以生效,而其他组件我们可以不用去执行,提高性能。

当然,我们页面也做了现场恢复功能(localStorage),也得益于immutable数据结构,我们实现了Redo/Undo的功能。Redo/Undo的功能仅会在组件个数有变化的时候计作一次版本,否则录取的的信息太多,会对性能造成影响。当然,组件信息发生变化我们是会去更新数组的。

5. 工作流程

用户能接触到的只有view层,就是组件里的各种输入框,单选多选等。用户与之发生交互,会发出action。React-Redux提供connect方法,用于从UI组件生成容器组件。connect方法接受两个参数:mapStateToProps和mapDispatchToProps,按照React-Redux的API,我们需要将Store.dispatch(action)写在mapDispatchToProps函数里边,但是为了书写方便和直观看出这个action是哪里发出的,我们没有遵循这个API,而是直接写在在代码中。

然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。State 一旦有变化,Store 就会调用监听函数。在React-Redux规则里,我们需要提供mapStateToProps函数,建立一个从(外部的)state对象到(UI组件的)props对象的映射关系。mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发UI组件的重新渲染。大家可以看我们content.js组件的最后代码:

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

connect方法可以省略mapStateToProps参数,那样的话,UI组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。像header和footer组件,就是纯UI组件。

为什么我们的各个子组件都可以拿到state状态,那是因为我们在最顶层组件外面又包了一层 组件。入口文件index.js代码如下:

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 after login

我们的react-router采用的是browserHistory,使用的是HTML5的History API,路由切换交给后台。

五、兼容性和打包优化

1. 兼容性

为了让页面更好的兼容IE9+和android浏览器,因为项目使用了babel,所以采用babel-polyfill和babel-plugin-transform-runtime插件。

2. Antd按需加载

Antd完整包特别大,有10M多。而我们项目里主要是采用了弹窗组件,所以我们应该采用按需加载。只需在.babelrc文件里配置一下即可,详见官方说明。

3. webpack配置externals属性

项目最后打包的main.js非常大,有接近10M多。在网上搜了很多方法,最后发现webpack配置externals属性的方法非常好。可以利用pc的多文件并行下载,降低自己服务器的压力和流量,同时可以利用cdn的缓存资源。配置如下所示:

externals: {
    "jquery": "jQuery",
    "react": "React",
    "react-dom": "ReactDOM",
    'CodeMirror': 'CodeMirror',
    'immutable': 'Immutable',
    'react-router': 'ReactRouter'
}
Copy after login

externals属性告诉webpack,如下的这些资源不进行打包,从外部引入。一般都是一些公共文件,比如jquery、react等。注意,因为这些文件从外部引入,所以在npm install的时候,有些依赖这些公共文件的包安装会报warning,所以看到这些大家不要紧张。经过处理,main.js文件大小降到3.7M,然后nginx配置下gzip编码压缩,最终将文件大小降到872KB。因为在移动端,文件加载还是比较慢的,我又给页面加了loading效果。

The above is the detailed content of Detailed explanation of front-end page creation tool pagemaker. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

How to solve the complexity of WordPress installation and update using Composer How to solve the complexity of WordPress installation and update using Composer Apr 17, 2025 pm 10:54 PM

When managing WordPress websites, you often encounter complex operations such as installation, update, and multi-site conversion. These operations are not only time-consuming, but also prone to errors, causing the website to be paralyzed. Combining the WP-CLI core command with Composer can greatly simplify these tasks, improve efficiency and reliability. This article will introduce how to use Composer to solve these problems and improve the convenience of WordPress management.

Solve database connection problem: a practical case of using minii/db library Solve database connection problem: a practical case of using minii/db library Apr 18, 2025 am 07:09 AM

I encountered a tricky problem when developing a small application: the need to quickly integrate a lightweight database operation library. After trying multiple libraries, I found that they either have too much functionality or are not very compatible. Eventually, I found minii/db, a simplified version based on Yii2 that solved my problem perfectly.

How to optimize website performance: Experiences and lessons learned from using the Minify library How to optimize website performance: Experiences and lessons learned from using the Minify library Apr 17, 2025 pm 11:18 PM

In the process of developing a website, improving page loading has always been one of my top priorities. Once, I tried using the Miniify library to compress and merge CSS and JavaScript files in order to improve the performance of the website. However, I encountered many problems and challenges during use, which eventually made me realize that Miniify may no longer be the best choice. Below I will share my experience and how to install and use Minify through Composer.

Accelerate PHP code inspection: Experience and practice using overtrue/phplint library Accelerate PHP code inspection: Experience and practice using overtrue/phplint library Apr 17, 2025 pm 11:06 PM

During the development process, we often need to perform syntax checks on PHP code to ensure the correctness and maintainability of the code. However, when the project is large, the single-threaded syntax checking process can become very slow. Recently, I encountered this problem in my project. After trying multiple methods, I finally found the library overtrue/phplint, which greatly improves the speed of code inspection through parallel processing.

Solve CSS prefix problem using Composer: Practice of padaliyajay/php-autoprefixer library Solve CSS prefix problem using Composer: Practice of padaliyajay/php-autoprefixer library Apr 17, 2025 pm 11:27 PM

I'm having a tricky problem when developing a front-end project: I need to manually add a browser prefix to the CSS properties to ensure compatibility. This is not only time consuming, but also error-prone. After some exploration, I discovered the padaliyajay/php-autoprefixer library, which easily solved my troubles with Composer.

Solve caching issues in Craft CMS: Using wiejeben/craft-laravel-mix plug-in Solve caching issues in Craft CMS: Using wiejeben/craft-laravel-mix plug-in Apr 18, 2025 am 09:24 AM

When developing websites using CraftCMS, you often encounter resource file caching problems, especially when you frequently update CSS and JavaScript files, old versions of files may still be cached by the browser, causing users to not see the latest changes in time. This problem not only affects the user experience, but also increases the difficulty of development and debugging. Recently, I encountered similar troubles in my project, and after some exploration, I found the plugin wiejeben/craft-laravel-mix, which perfectly solved my caching problem.

Solve the PHP timeout problem: application of phpunit/php-invoker library Solve the PHP timeout problem: application of phpunit/php-invoker library Apr 17, 2025 pm 11:45 PM

When developing PHP projects, you often encounter the problem that some functions or methods have been executed for too long, causing program timeout. I've tried multiple solutions, but the results are not satisfactory until I discovered the phpunit/php-invoker library. This library completely solved my problem by setting the timeout time to call the executable function.

How to solve TYPO3CMS installation and configuration problems? It can be done easily with Composer! How to solve TYPO3CMS installation and configuration problems? It can be done easily with Composer! Apr 17, 2025 pm 10:51 PM

When using TYPO3CMS for website development, you often encounter problems with installation and configuration extensions. Especially for beginners, how to properly install and configure TYPO3 and its extensions can be a headache. I had similar difficulties in my actual project and ended up solving these problems by using Composer and TYPO3CMSComposerInstallers.

See all articles