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,完成了两件事:

  1. 以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, 同构


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部