React+Redu 同构应用开发
背景 随着众多React + Redux 项目在团队中落地,基于此模式的单向数据流应用受到了广泛的推崇。但是在项目开发过程中,尤其是复杂单页应用,JS文件的体积往往高达数百KB。相较于以往开发模式(Kissy、jQuery、Zepto&8230;)几十KB的体积,极大地增加了页面首次加载的时间。PC端中,这些问题并不突出,但对于移动端,尤其是弱网环境下,会大大增加用户的等待时间,从用户体验上来说,是极不友好的。
针对上述问题,一个现在十分火热概念浮出水面 服务端渲染 & 同构
服务端渲染(Server Rendering)React中提出了 虚拟DOM 的概念,虚拟DOM以对象树的形式保存在内存中,与真实DOM相映射,通过ReactDOM的Render方法,渲染到页面中,并维护DOM的创建、销毁、更新等过程,以最高的效率,得到相同的DOM结构。
虚拟DOM 给页面带来了前所未有的性能提升,但它的精髓不仅局限于此,还给我们带来了另一个福利: 服务端渲染 。
不同于 ReactDOM.render
将DOM结构渲染到页面,React中还提供了另外两个方法:ReactDOMServer.renderToString 和 ReactDOMServer.renderToStaticMarkup 。二者将虚拟DOM渲染为一段字符串,代表了一段完整的HTML结构。
同构(Isomorphic)通过React提供的服务端渲染方法,我们可以在服务器上生成DOM结构,让用户尽早看到页面内容,但是一个能够work的页面不仅仅是DOM结构,还包括了各种事件响应、用户交互。那么意味着,在客户端上,还得执行一段JS代码绑定事件、处理异步交互,在React中,意味着整个页面的组件需要重新渲染一次,反而带来了额外的负担。
因此,在服务端渲染中,有一个十分重要的概念, 同构(Isomorphic) ,在服务端和客户端中,使用完全一致的React组件,这样能够保证两个端中渲染出的DOM结构是完全一致的,而在这种情况下,客户端在渲染过程中,会判断已有的DOM结构是否和即将渲染出的结构相同,若相同,不重新渲染DOM结构,只是进行事件绑定。
在同构应用中,一套代码(不局限于组件),能够同时在客户端和服务端运行,总体结构如下:[br]
配合Redux上述 服务端渲染 帮我们完成了组件层面的同构问题,对于要使用何种数据流并没有约束,在本次实践中,使用了Redux模式,关于Redux服务端渲染,参看官方文档。其中最重要的一点就是,在服务端和客户端保持 store
一致。
store
的初始状态在Server端生成,为了保持两个端中 store
的一致,官方示例中通过在页面插入脚本的方式,写入 store
初始值到window:
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
此处输出initialState到页面中,是十分危险的,一定要注意XSS的防范。[br]Redux推荐使用 serialize-javascript 序列化JS对象,这一点十分必要。
实践要进行服务端渲染,一个node server必不可少,Koa、Express 都是流行的Node端Web框架,前者似乎更受开发者青睐。
KoaServer端使用 Koa。配合如 xtemplate,koa-jade之类的视图模板,能够快速完成HTML页面的渲染。
关于Koa的使用,并不是本文的重点,在此不过多阐述,选择一个顺手可靠的框架即可。
目录结构工程的整体结构如下:
├── app //服务端│ ├── controllers //控制器│ ├── routes //路由│ ├── service //接口│ └── views //视图├── assets├── bin ├── build //构建,css、js├── client //客户端│ ├── actions│ ├── api│ ├── components│ ├── constants│ ├── containers│ ├── less│ ├── reducers│ └── store├── lib├── logs├── mock└── webpack //Webpack配置
其中,所有React组件和Redux 模块都放在 /client
目录下,该目录下存放着一个和我们日常开发React+Redux完全一致的APP。
配置 Koa我们的HTML页面不再通过静态服务器获取,而是通过Koa Server,配置一个新路由,作为页面的入口。使用xtemplate for Koa作为View层,在 /app/routes
中新建路由:
'use strict';var HomeController = require('../controllers/home');var router = new (require('koa-router'))();router.get('/home.html', HomeController.index);module.exports = router;
在 /app/controllers
中新建控制器:
'use strict';exports.index = function* () { //do something yield this.render('home');};'home'
, 对应 /app/views
下的视图文件
服务端ES6/7支持通常,在客户端代码中,我们的编程风格里使用了大量的ES6/7语法,如 import
, class
等,但在服务端,这些语言特性Node还不能完全支持,这就需要我们使用相关的插件,帮助服务端识别此类语法。
引入babel-register One of the ways you can use Babel is through the require hook. The require hook will bind itself to node&8217;s require and automatically compile files on the fly.
babel-register 通过绑定 require函数的方式(require hook),在 require jsx文件时,使用babel转换语法,因此,应该在任何 jsx 代码执行前,执行 require('babel-register')(config)
,同时通过配置项 config
,配置babel语法等级、插件等。具体配置方法可参看官方文档。
处理CSS/LESS文件 babel-register
帮助服务端识别特殊的js语法,但对 less/css
文件无能为力,庆幸的是,在一般情况下,服务端渲染不需要样式文件的参与,css文件只要引入到HTML文件中即可,因此,可以通过配置项,忽略所有 css/less
文件:
require("babel-register")({ // Optional ignore regex - if any filenames do match this regex then they // aren't compiled. ignore: /(.css|.less)$/,});
完成jsx语法支持,就可以引入React组件或APP,通过 renderToString
方法进行服务端渲染:
'use strict';import React from 'react';import { renderToString } from 'react-dom/server';import { createStore } from 'redux';import configureStore from '../../client/store/configureStore';import { Provider } from 'react-redux';import App from '../../client/containers/home';exports.index = function* () { //do something // 生成store const store = configureStore(); // 从store中获取state const finalState = store.getState(); const html = renderToString( ); //将生成的html结构插入模版中 yield this.render('home', {html: html});};
使用CSS Modules通过 babel-register
能够使用babel解决jsx语法问题,对 css/less 只能进行忽略,但在使用了 CSS Modules 的情况下,服务端必须能够解析 less文件,才能得到转换后的类名,否者服务端渲染出的 HTML 结构和打包生成的客户端 css 文件中,类名无法对应。
为了解决这个问题,需要一个额外的工具 webpack-isomorphic-tools
,帮助识别less文件。
webpack-isomorphic-tools简单地说,webpack-isomorphic-tools,完成了两件事:
- 以webpack插件的形式,预编译less(不局限于less,还支持图片文件、字体文件等),将其转换为一个 assets.json
文件保存到项目目录下。1. require hook,所有less文件的引入,代理到生成的 JSON 文件中,匹配文件路径,返回一个预先编译好的 JSON 对象。上述过程解决了服务端渲染中不能解析非js文件的痛点,让我们使用CSS Modules时欲罢不能的快感,在服务端得以延续。
配置文件十分冗长,配置细节可查阅官方文档。这里需要注意的是,webpack-isomorphic-tools 的 require hook,是通过一个回调函数进行的:
var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-config')) .development(__DEVELOPMENT__) .server(rootDir, function () { //回调 require('./app.js'); //启动 server });
webpack-isomorphic-tools 启动时,会先等待指定目录下 assets.json
文件生成,只有该文件就绪后,require hook 才会进行,进而触发 server
回调,只有在此回调中执行的代码,才能保证进行了require hook。
最终,这个系统大致结构如下图所示:
带来的问题webpack-isomorphic-tools 这种 hook 方式,将整个Koa Server置于自身的回调中,仿佛『劫持』了整个server,总不是显得那么的优雅。
环境变量另一个在同构应用中的常见问题就是环境变量,客户端开发中,只需要判断链接、URL参数等,但在server端,并没有清晰的 host
概念,同一个Server可以在多个host下被访问。 那么,一些环境参数的判断,就要通过环境变量进行。
在webpack中,使用 DefinePlugin
定义环境变量(其实就是global变量):
plugins: [ ... new webpack.DefinePlugin({ '__ENV__': JSON.stringify('development'), __CLIENT__: true, __SERVER__: false, __DEVELOPMENT__: true, __DEVTOOLS__: true }), ...]
配置了不同环境变量的 webpack 配置文件,打包得到的也只是固定JS文件,如果要和服务端上多个环境(dev、prod)一一对应,需要使用多个配置文件来完成,发布到不同环境时,使用对应环境下的配置。
构建通过 gulp,使用不同的 webpack 配置进行打包,生成对应静态资源,发布到CDN即可。
这里需要注意的是,合理使用环境变量,和webpack插件,可以大大减少js文件的体积:
gulp.task("env:prod", function () { env({ BABEL_ENV: 'production', NODE_ENV: 'production' }); prodConfig.plugins = prodConfig.plugins.concat( new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') }, 'NODE_ENV': JSON.stringify('production'), '__ENV__': JSON.stringify('production') }), new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurenceOrderPlugin(), // Compresses javascript files new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) );});
通过gulp任务,设置环境变量为 production
,webpack 将不会把 React 中如 PropTypes
检查之类的非必需代码打包,同时能够避免babel引入开发环境下的插件。
DedupePlugin
和 UglifyJsPlugin
两个webpack插件必不可少,前者帮我们去除重复引入的js代码,后者进行js混淆压缩。
本次项目中,开发阶段的代码 4MB+ 最终被压缩到了 300KB,发布到CDN,浏览器以gzip格式加载,实际大小约为 100KB,即使对于移动端而言,也是一个可以接受的大小。
后续### 性能评估服务端渲染性能的评估、白屏时间优化,需要更为专业和准确的数据来进行判断,有哪些优秀的工具和测试方法,还请不奢赐教~!
总结本次实践 React+Redux 同构应用,一是出于对新的架构方式的探索,二是需要开发页面本身不复杂,适合用于新技术实践。
其间坎坎坷坷,踩坑无数,也构造了一个同构应用的雏形,虽然还不够完善,但是也希望对后续开发同构应用的同学带来启发。
关键字:AliUED, React, Redux, 同构
版权声明
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!