Pagemaker est un outil de création de pages frontales qui permet aux étudiants en produits, en fonctionnement et en visuels de développer rapidement des pages frontales simples, libérant ainsi la charge de travail des étudiants front-end. L'idée de ce projet vient du projet pagemaker du projet interne nfop de NetEase. Il s'avère que le front-end du projet est réalisé à l'aide de jquery et de templates ejs. Chaque fois que le composant est mis à jour, l'ensemble du dom sera redessiné et les performances ne sont pas très bonnes. Parce que React était très populaire à l'époque et que le projet lui-même était adapté, nous avons finalement décidé d'utiliser React pour tester le terrain. Étant donné que l'ensemble du projet comprenait à l'origine de nombreux sous-projets, l'implémentation backend n'avait aucune référence et a été complètement réécrite.
Ce projet n'est qu'une simple implémentation du projet original, supprimant les composants les moins utilisés et les plus complexes. Mais bien que Sparrow soit petit et possède tous les organes internes, ce projet utilise un ensemble complet de piles technologiques de réaction, qui conviennent aux étudiants qui ont déjà étudié la réaction et souhaitent approfondir leur compréhension et leur pratique grâce à des démos. Il est recommandé qu'avant d'étudier cette démo, vous étudiiez/révisiez d'abord les points de connaissances pertinents : série de didacticiels sur la pile technologique React, explication détaillée d'Immutable et pratique dans React.
Composants riches. Il y a des titres, des images, des boutons, du texte, de l'audio, de la vidéo, des statistiques et des entrées jscss.
Aperçu en temps réel. Chaque fois que vous effectuez une modification, vous pouvez immédiatement voir le dernier aperçu.
Prend en charge trois méthodes d'importation et prend en charge l'exportation de fichiers de configuration.
Prend en charge les opérations Annuler/Rétablir. (Les changements dans le nombre de composants sont des points de déclenchement)
Vous pouvez publier, modifier et supprimer des pages publiées à tout moment.
Chaque page possède un mot de passe de publication pour empêcher les autres de le modifier.
L'architecture frontale de la page utilise React+Redux et utilise une structure de données immuable. Chaque mise à jour de composant peut être minimisée pour optimiser les performances de la page.
Compressez automatiquement les images téléchargées en arrière-plan pour éviter que les fichiers ne soient trop volumineux
Adapté aux appareils mobiles
React
Redux
React-Redux
Immuable
React-Router
récupérer
es6
es7
Noeud
Express
Webpack
Sass
Pug
Parce que le projet utilise plus de technologies , l'utilisation d'outils d'échafaudage peut nous faire gagner du temps dans les projets de construction. Après avoir cherché, j'ai découvert qu'il y en avait trois couramment utilisés :
create-react-app
kit de démarrage réaction
réaction-boilerplate
Le nombre d'étoiles sur github est très élevé. La première est la démo de réaction officiellement publiée par Facebook. Mais à bien y regarder, les trois projets sont relativement volumineux et introduisent de nombreux packages de fonctionnalités inutiles. Après quelques recherches, j'ai trouvé un outil d'échafaudage utile : yeoman Vous pouvez choisir le générateur correspondant. J'ai choisi réagir-webpack. Le projet est relativement simple et nécessite que chacun construise ses propres environnements redux et immuables, ainsi que backend express. En fait, vous pouvez exercer votre capacité à construire des projets.
Le magasin est l'endroit où les données sont enregistrées. Vous pouvez le considérer comme un conteneur. Il ne peut y avoir qu’un seul Store pour l’ensemble de l’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;
Redux fournit la fonction createStore pour générer un Store. Étant donné que l'application entière n'a qu'un seul objet State, qui contient toutes les données, pour les applications volumineuses, cet State doit être très grand, ce qui entraîne une très grande fonction Réducteur. Redux fournit une méthode combineReducers pour diviser les réducteurs. Il vous suffit de définir chaque fonction de sous-réducteur, puis d'utiliser cette méthode pour les combiner dans un grand réducteur. Bien entendu, nous n’avons ici qu’une seule unité de réduction, qui peut être divisée ou non.
devToolsEnhancer est un middleware. Utilisé pour utiliser Redux DevTools pour déboguer Redux dans l'environnement de développement.
L'action décrit ce qui se passe actuellement. La seule façon de changer d’état est d’utiliser Action. Il livrera les données au 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;
Les changements d'état entraîneront des changements dans la vue. Cependant, l'utilisateur ne peut pas accéder à l'État, mais uniquement à la vue. Par conséquent, les changements d’état doivent être provoqués par View. L'action est une notification envoyée par View, indiquant que l'état doit changer. Dans le code, nous définissons l'objet actions, qui possède de nombreux attributs, chacun étant une fonction. Le résultat de la fonction est de distribuer un objet action, qui est envoyé via Store.dispatch. L'action est un attribut de type qui contient les attributs de type nécessaires, ainsi que d'autres informations accessoires.
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; }
使用 Immutable 后,如下图,当红色节点的 state 变化后,不会再渲染树中的所有节点,而是只渲染图中绿色的部分:
本项目中,我们采用支持 class 语法的 pure-render-decorator 来实现。我们希望达到的效果是:当我们编辑组件的属性时,其他组件并不被渲染,而且preview里,只有被修改的preview组件update,而其他preview组件不渲染。为了方便观察组件是否被渲染,我们人为的给组件增加了data-id的属性,其值为Math.random()
的随机值。效果如下图所示:
immutable实际效果图
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;
Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。unitsConfig是存储着各个组件初始配置的对象集合,所有新添加的组件都从里边取初始值。State有一个初始值:initialState,包含META组件,因为每个web页面必定有一个META信息,而且只有一个,所以页面左侧组件列表里不包含它。
reducer会根据action的type不同,去执行相应的操作。但是一定要注意,immutable数据操作后要记得赋值。每次结束后我们都会去修改fromType值,是因为有的组件,比如AUDIO、CODE等修改后,预览的js代码需要重新执行一次才可以生效,而其他组件我们可以不用去执行,提高性能。
当然,我们页面也做了现场恢复功能(localStorage),也得益于immutable数据结构,我们实现了Redo/Undo的功能。Redo/Undo的功能仅会在组件个数有变化的时候计作一次版本,否则录取的的信息太多,会对性能造成影响。当然,组件信息发生变化我们是会去更新数组的。
用户能接触到的只有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);
connect方法可以省略mapStateToProps参数,那样的话,UI组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。像header和footer组件,就是纯UI组件。
为什么我们的各个子组件都可以拿到state状态,那是因为我们在最顶层组件外面又包了一层
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') );
我们的react-router采用的是browserHistory,使用的是HTML5的History API,路由切换交给后台。
为了让页面更好的兼容IE9+和android浏览器,因为项目使用了babel,所以采用babel-polyfill和babel-plugin-transform-runtime插件。
Antd完整包特别大,有10M多。而我们项目里主要是采用了弹窗组件,所以我们应该采用按需加载。只需在.babelrc文件里配置一下即可,详见官方说明。
项目最后打包的main.js非常大,有接近10M多。在网上搜了很多方法,最后发现webpack配置externals属性的方法非常好。可以利用pc的多文件并行下载,降低自己服务器的压力和流量,同时可以利用cdn的缓存资源。配置如下所示:
externals: { "jquery": "jQuery", "react": "React", "react-dom": "ReactDOM", 'CodeMirror': 'CodeMirror', 'immutable': 'Immutable', 'react-router': 'ReactRouter' }
externals属性告诉webpack,如下的这些资源不进行打包,从外部引入。一般都是一些公共文件,比如jquery、react等。注意,因为这些文件从外部引入,所以在npm install
的时候,有些依赖这些公共文件的包安装会报warning,所以看到这些大家不要紧张。经过处理,main.js文件大小降到3.7M,然后nginx配置下gzip编码压缩,最终将文件大小降到872KB。因为在移动端,文件加载还是比较慢的,我又给页面加了loading效果。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!