【4858美高梅】搭建React服务端渲染项目知识梳理及总计,详解React项目标服务端渲染改造

By admin in 4858美高梅 on 2019年4月11日

服务端渲染(SS奇骏: Server Side
Rendering)在React项目中全部广泛的施用场景

依照React虚拟DOM的特征,在浏览器端和服务端大家能够兑现同构(能够采取同样份代码来完毕多端的成效)

服务端渲染的帮助和益处主要由三点

  1. 利于SEO

  2. 提升首屏渲染速度

  3. 同构直出,使用相同份(JS)代码完结,便于开发和掩护

本项目github地址
react-koa2-ssr

因为对网页SEO的要求,要把前边的React项目改造为服务端渲染,经过壹番检察和研究,查阅了大批量互连网资料。成功踩坑。

导语:

实则demo早在四5月份就曾经写好了,只是那段时光后台项目太忙,加上自个儿推延症晚期,到近日才写那篇小说。小说结构大体上从介绍本文使用的前端技术,使用的原委出发,同时重组代码举行利用讲解,最终在配上怎么样遵照本文demo举办进一步开发,尽量做到浅显易懂,一连连串的特征。那篇小说应该是本身多年来内写的最终1篇前端小说。写完那篇小说,也终究从React系前端勉强结束学业了,同时也是给本身今年来前端学习的3个交代。文章有点长,我们渐渐看。也能够直接到最下边获取自小编的github地址,直接下载demo跑起来,只要求1个npm
start,童嫂无欺。
React

 

所用到技术栈 react1陆.x + react-router四.x + koa二.x

选型思路:实现服务端渲染,想用React最新的本子,并且不对现有的写法做大的改变,即使1初始就打算服务端渲染,提议直接用NEXT框架来写

框架概念介绍

所利用的有关前端技术框架:React+React-Router+Redux+webpack2.0,ui框架用的是Ant
Design
。异步请求用的是fetch api

1.React:那些就不要介绍了啊,这么些还用介绍,估摸那篇小说不太符合你。这么些不熟的建议先读书一下,可以从自家事先写的后台小白前端入门--React看起。

2.Reac-Router:使用React-Router,能够让您轻轻松松利用React实行多页面开发。React—Router是React官方的相似也是绝无仅有的React路由化解方案。不难的话,正是url改变,可是不经过后台,在前者举行路由,依照url的比不上取舍并渲染差异的路由组件。React-Router其实未有多少东西,就3个路由容器,还有部分api,未来也出粤语文书档案了,能够随着官方文书档案的demo敲1遍就好了。

3.Redux:个人觉得那是技巧栈里面比较难学的2个。不仅你要学习Redux是何许,也要读书怎样和React搭配使用。简单来讲,Redux就是在浏览器内部存款和储蓄器中弄了一个容器,用于存放及管理js状态的地点。然后经过一雨后春笋的正统和限制,让你能清晰地保管这个情状,同时用那么些情状去改变页面。

选用React做过复杂页面包车型大巴人也许会深有体会,当二个嵌套了三4层的零件,最尾巴部分的子组件想要改变父组件的动静,使得父组件通过那么些状态去改变其它1个最尾部的子组件的时候,不采纳新闻订阅方式的时候,无非就是写三个回调向来传到子组件供其调用,来改变那些父组件的景况(其实正是父组件的state),然后父组件又把那些场所一百年不遇传到其余一个子零部件,然后此外2个子零件通过props值获取到状态的转移,来渲染页面。那种代码写起来11分地恶意,壹多起来,基本不能阅读和保证。

于是今年,你就须求在叁个地方去联合管理那几个情状了,让具有的机件都去这一个地点获得状态的转变,恐怕改变状态。那么些就是Redux做的事情。React最初起步的时候,由法定推出的Flux也是做着就像的政工。然则后来Redux的出现,以进一步分明的store机制和利用形式,取代了Flux成为了主流。小编要好也试着写过Flux的代码,写起来Redux会更简美赞臣(Meadjohnson)些,所以那里就分选选拔Redux。

Redux学习的话,能够先从Redux国语文书档案起始看起,差不离了然一下Redux的相干机制。倘使英文好一下的话,能够去看2个英文的Redux使用录像,差不多20八个,照着敲下来,应该也大都。当然,本文后续也会介绍Redux结合React,React-Router使用。

四.框架功用小总括:为啥写个React还要加着么多东西啊?简单的话,便是你在初期使用React开发页面包车型地铁时候,你意识你的页面唯有一个url(单页),不可能成功通过url到达分化的页面,然后你使用React-Router,就足以行使React实行“多页面”开发。然后您的网址尤其复杂了,抽出来的公共组件越多,页面组件嵌套更深,状态(父组件于子组件之间的state)之间的涉嫌影响尤为复杂。那时候你就须求动用Redux来统1管理那几个意况了。

地点提到的连带资料站,都得以从本文最上边获取到有关的链接,大家按需得到。本demo的相干代码放到了github里面,链接也置于底下了。

手拉手探访怎么着在实际上的品种中完毕服务端渲染

前言

前段时间业余做了三个简易的古文网 ,可是项目是接纳React SPA
渲染的,不便宜SEO,便有了服务端渲染这几个要求。前边就想写个demo把方方面面经过总括一下,同时也强化自个儿对其的接头,时期出于工作,进程是断断续续
。总而言之后来就有了这一个类型吧。关于服务端渲染的优缺点,vue服务端渲染官方文书档案讲的最知道。讲的最掌握。
对于超越12分之伍意况最关键依然两点 提升首屏加载速度
和便利SEO.为了急速创设开发环境,这里直接使用create-react-app
和koa二.x生成一个基础项目
。整个项目就是以此视作主心骨举办支付的,如今也只是达成了最主旨的须要,
还有为数不少Bug 和能够优化的地点, 欢迎交流。

品种地址:

框架使用及评释

上边将介绍框架代码的布局和采用。先附上目录结构,介绍各类文件夹存放的文书及文件的协会:

front
|—actions(使用Redux开发,对应的action都置于那么些目录下,稍后详解)
|—build(webpack打包编写翻译后生成的js文件,实际html引用的也是此处的公文)
|—common(一些国有的变量和api都位居这么些文件夹下)
|—css
|—data(node服务器mock后台数据的文书放在那)
|—jsx(组件代码文件都位于那)
|—node_modules
|—reducers(使用Redux开发,对应的action都放到那些目录下,稍后详解)
|—store(使用Redux开发,store文件存放路径)
|—index.html(主页面)
|—package.json(npm)
|—proxy.config.js(node服务器请求接口映射配置文件)
4858美高梅,|—webpack.config.js

提议大家把demo下下来直接跑起来,先安装好node和webpack,然后在front目录下,使用npm
install安装以来,再利用webpack打包,最终动用npm
start运维node服务器,然后浏览器访问http://localhost:8000/即可。demo比较简单,只做了登六页,注册页和主页。不过也够用了。不知情node和webpack用法的能够先在本人的上1篇博客搭个简易例子,你就会有所了然了。后台小白前端入门--React前端框架搭建德姆o及介绍。

运维后,你就能看出如此的页面:

4858美高梅 1

image.png

点击右上角的名字,退出,能够跳到登六页,登六页点注册,可到注册页。

品种地址 ,欢迎围观!

服务端渲染最基本的理论知识梳理

首先前后端独家接纳create-react-app 和koa2的脚手架飞快转移,
然后再将多个品种联合到一起。那样我们省去了webpack的部分累赘配置
,同时服务端使用了babel编写翻译。看那一个以前 暗中认可已经控制webpack 和
koa二.x,babel的相关文化。
咱俩直切重要的手续吧。小编认为搭建二个react-ssr环境主要唯有三点
首先是react服务端提供的渲染API,二是上下端路由的同构,三则是开端化异步数据的同构。由此那个大约的demo主要从那3地点动手。

  • react 服务端渲染的规范
  • react-router4.x 与koa二.x 路由达成同构
  • redux 开端数据同构

脚手架选型:webpack叁.1一.0 + react Router肆 + Redux + koa2 + React1陆 +
Node八.x

重点代码详解

1.jsx/index.jsx

//省略引入第三方库的代码。
import store, {history} from '../store/store.jsx';
class RouterComponent extends React.Component {
    render() {
        return (
            <Provider store={store}>
                <Router history={history}>
                    <Route path={env.homePage} component={ContainerComponent}>
                        <IndexRoute component={MainComponent}/>
                        <Route path={env.login } component={LoginComponent}/>
                        <Route path={env.register } component={RegisterComponent}/>
                    </Route>
                </Router>
            </Provider>
        );
    }
}

前面谈到,我们早就将state交给了Redux举行保管,放在了store里面。那么如何在担负渲染页面包车型地铁组件里面获得到这个state呢?在React-Redux中,提供了Provider组件,把它当作根组件,同时把store作为参数字传送给它,你就能在嵌套在它下边包车型地铁零件中得到state。

接下去正是Router组件了,Router组件使用起来和毫无Redux壹样,那里就不要详细介绍。可是大家能够把history和store做个绑定,下边有详实介绍。

2.store/store.jsx

//省略引入第三方库的代码。
import * as reducers from '../reducers/rootreducer.jsx';

const rootReducer = combineReducers({
    ...reducers,
    routing: RouterReducer
});

const store = createStore(rootReducer, applyMiddleware(thunk));

export const history = syncHistoryWithStore(browserHistory, store);

export default store;

上面的combineReducers,用于将多少个负担改变不相同的state的reducer合并成同3个reducer函数。那样你就能够分别依据作业情状编写分裂的reducer了,代码结构清晰。

接着是syncHistoryWithStore,用于将History中的变化体现到store里的state中。那里小编要好也不是很通晓,demo里面应该是未曾用到对应的事例,因为本身把这一个代码换到export
const history = browserHistory;

编写翻译运营都例行,有趣味的话能够做个深远商量。

3.jsx/component/MainComponent.jsx

好,接下去,写完上面那多个布局文件。React+React-Router+Redux的框架就大约的搭建起来了。以往起先写一些比较不难的页面组件。下边主页的代码相比较不难,相信我们看懂应该轻而易举。假使大家平素不选取Redux,其实就是把下部的loadData改成异步请求,同创立组件的有关state,把请求回来的参数字传送给这么些里面包车型大巴state,通过state组件的换代来更新页面。代码如下:

//以上略去了使用Redux需要声明和绑定和函数,后面会给出。
export default class MainComponent extends React.Component {
    constructor(props) {
        super(props);

    }

    loadData() {
        //这里是Redux的写法,如果是不使用Redux,则直接写异步请求并且将返回的数据setState到本组件的state中。
        const {actions} = this.props;
        actions.fetchArticleList();
    }


    componentDidMount() {
        this.loadData();
    }

    render() {
        var articleListHtml = [];
        //这里是Redux的写法,如果是不使用Redux,则直接从this.state中获得数据。
        const {articleList} = this.props;
        if (common.isNotEmpty(articleList) && articleList.length > 0) {
            for (var i = 0; i < articleList.length; i++) {
                articleListHtml.push(
                    <tr key={i}>
                        <td className="td-align-center" style={{width:"50%"}}>
                            <a href="" target="_blank">{articleList[i].title}</a>
                        </td>
                        <td className="td-align-center" style={{width:"25%"}}>{articleList[i].creator}</td>
                        <td className="td-align-center"
                            style={{width:"25%"}}>{articleList[i].commentNum}/{articleList[i].readedNum}</td>
                    </tr>
                )
            }
        }
        return (
            <div>
                <Row type="flex" justify="center">
                    <h1>博文列表</h1>
                </Row>
                <Row type="flex" className="add-code-lib-row standard-margin-top">
                    <Col span={24}>
                        <div className="code-manage-main">
                            <table>
                                <thead>
                                <tr>
                                    <th className="td-align-center">标题</th>
                                    <th className="td-align-center">作者</th>
                                    <th className="td-align-center">回复/查看</th>
                                </tr>
                                </thead>
                                <tbody>
                                {articleListHtml}
                                </tbody>
                            </table>
                        </div>
                    </Col>
                </Row>
            </div>
        );
    }
}

–.Redux怎么用,用的时候该留意些什么
写完UI组件,那里有必不可缺花1些篇幅不难介绍一下Redux的有的机制和条件,以便于实际不想看Redux是什么以及怎么用的读者知道接下去的代码。

近年来有聊起,Redux便是用来治本意况(state)的,不过它本身有一套自个儿的管理机制和专业,差不多的示意图如下:

4858美高梅 2

image.png

(壹)UI组件通过connet,绑定action和state(即图中的壹和2),使得组件能够dispatch
action,同时也能在props里面获得到store里对应的state。

(2)组件生成action,并且开始展览dispatch(即图中的三),那里关键为了调用异步请求,获取服务端数据。

(3)请求成功依旧败北后,会再在三的Action里,dispatch多少个Action(图中的四),这几个action,才是用来通告reducer,让其去改变store中对应的气象的。

(四)全部的Reducer都会收取该请求(图中的5),假若符合条件的reducer会更改store中的state,并且重回新的state到store中,假使不符合,则平昔回到旧的state,不做任何变动。

地点大约正是接纳Redux的大致开发流程及数量流向了。同时Redux规定了三条规则,来保管store中状态改变和行使丰富清晰。一.只有3个store,全部的动静都留存于那几个store中。二.因此action来触发store中状态的改变,reducer执行实际的state变化。其它任何时间任何措施的state是只读的。三.reducer函数必须是1个纯函数,即对应于任何三个相同的输入,只会有一种同等的出口。举个例子,你在这几个纯函数里面,Redux是不允许你使用诸如Math.random()可能new
Date()等函数来改变state的。那样做是为了确定保证状态变化的可预测性和清晰性。

通过下边壹多元原理和规则,Redux让前者开发者和帮助者对前者的代码和逻辑更佳清楚易懂,对于状态变化清晰可预测。

4.reducers/articleList.jsx

import * as constants from '../common/constants.jsx'

const initialState = {};

export default function (state = initialState, action) {
    switch (action.type) {
        case constants.RECEIVE_ARTICLELIST:
            const {articleList} = action;
            return {
                ...state,
                articleList
            };
            break;
        default:
            return state
    }
}


//同时在common/constants.jsx中添加如下内容,因为action和reducer都会用到,所以放倒一个公共变量里面比较好。编程习惯编程习惯
export const RECEIVE_ARTICLELIST = 'RECEIVE_ARTICLELIST';

//在reducers/rootreducer.jsx中添加如下代码,使得上面写的reducer能绑定到store中。
export articleList from './articleList.jsx'

Redux部分的代码讲解将服从Redux流程从后到前讲课,即从reducer,action,再到前端UI
component 的相继,方便我们尤其明亮地打听怎么进展Redux开发。

上边部分的代码正是写三个承受改变文章列表状态的reducer,当有3个type为’RECEIVE_AEscortTICLELIST’的action被分摊的时候,那么些reducer会实行响应,同时修改景况里的articleList,重新总计并回到新的state到store中。供给表达的是,当三个action被分摊的时候,全体绑定的reducer都会被触发执行。假诺符合则更新情形,假诺未有则平昔回到原state(即default:return
state),store里的事态就不会扭转。

小编们写完了这一个reducer,就要把它绑定到store中,前边我们已经经过如下代码进行配备了,所以只必要在reducers/rootreducer.jsx
添加即可绑定到store中。

import * as reducers from '../reducers/rootreducer.jsx';
const rootReducer = combineReducers({
    ...reducers,
    routing: RouterReducer
});

const store = createStore(rootReducer, applyMiddleware(thunk));

【4858美高梅】搭建React服务端渲染项目知识梳理及总计,详解React项目标服务端渲染改造。5.actions/receiveArticleList.jsx

有纯粹的 React,也有 Redux 作为气象管理

react 服务端渲染的基准

实在可以看 《深入React技术栈》的第8章, 介绍的那么些详细。
包蕴来说 React 之所以得以做到服务端渲染
是因为ReactDOM提供了服务端渲染的API

  • renderToString 把三个react 元素转换来带reactid的html字符串。
  • renderToStatic马克up
    转换来不带reactid的html字符串,假设是静态文本,用那些艺术会收缩大批判的reactid.
    那八个点子的留存
    ,实际上能够把react看做是一个模板引擎。解析jsx语法变成经常的html字符串。

咱们得以调用那七个API 达成传入ReactComponent
重返对应的html字符串到客户端。浏览器端接收到这段html未来不会重复去渲染DOM树,只是去做事件绑定等操作。那样就抓实了首屏加载的习性。

最主要经验:对React的相干文化进一步熟识,成功举行自个儿的技术领域,对服务端技术在实际项目上全部积累

随着往前写,写到Action,这里分派到Reducer的Action,是三个对象,此中有七个type属性,是必须的,用来和reducer里面接受参数里面包车型客车action.type是如出一辙的,也等于constants.jsx中的RECEIVE_ARTICLELIST

‘RECEIVE_A哈弗TICLELIST’。此外你能够在这一个指标里面添加壹些别的的参数,以便于reducer相配到将来拿那几个参数去改变store中的状态。如下代码:

const constants = require('../common/constants.jsx');

export function receiveArticleList(articleList) {
    return {
        type: constants.RECEIVE_ARTICLELIST,
        articleList
    }
}

6.actions/fetchArticleList.jsx

那边这一个Action,其实在笔者眼里,是1个伪“action”,因为它并不曾分派到reducer中,而是触发此外二个Action。那里那些Action是由UI组件分派的,为了进行异步请求,在呼吁成功照旧失败后,再分摊贰个确实的Action,去改变store中的state。

在Redux中,把异步请求数据的代码和逻辑放在Action里面是相比好的。当然你能够向来在UI组件里面一向进行异步请求,然后派发Action去改变store中的状态。可是那样做的话,你的作业代码和您的UI组件又耦合在联合了。代码也不够通用,感觉不太好。这有个别的代码如下:

import fetch from 'isomorphic-fetch'
require('es6-promise').polyfill();
import * as actions from './actions.jsx';
import env from '../common/env.js';

export function fetchArticleList() {
    return (dispatch, getState) => {
        fetch(env.getArticleList, {
            credentials: 'same-origin',
            headers: {
                "Content-Type": "application/json"
            }
            // body: formData
        }).then(function (response) {
            if (response.status >= 400) {
                throw new Error("Bad response from server");
            }
            return response.json();
        }).then(function (result) {
            var code = result.code;
            if (code == "0") {
                    //这里才是真正触发Aciton的地方,去store的状态。
                dispatch(actions.receiveArticleList(result.data))
            }
        }.bind(this)).catch(function (error) {
            alert("请求失败,请检查网络");
            //这里才是真正触发Aciton的地方,去store的状态。
            dispatch(actions.receiveArticleList([]))
        });
    }
}

7.jsx/component/MainComponent.jsx

最后,又赶回了大家的UI组件。大家在前方已经写好了异步请求的Action,改变Store中状态的Action,以及怎么样改变Store状态的Reducer。

接下去,就是UI组件如何去取获得store中的状态,如何去一向或然直接(必要异步请求的时候)去派发一个Action,以及store中的state变化时,UI组件是何等过获取到变化的。先贴上代码,上面再开始展览代码表明。

import env from '../../common/env.js';
import common from '../../common/common.js';
import {fetchArticleList} from '../../actions/actions.jsx';
import {connect} from 'React-Redux';

const mapDispatchToProps = dispatch => {
    return {
        actions: {
            fetchArticleList: () => dispatch(fetchArticleList())
        }
    }
};


// Map Redux state to component props
const mapStateToProps = createSelector(
    state => state.articleList.articleList,
    (articleList) => {
        return {
            articleList
        }
    }
);


@connect(mapStateToProps, mapDispatchToProps)
export default class MainComponent extends React.Component {

//这部分的代码和  3.jsx/component/MainComponent.jsx中贴出的代码一样,不再给出。

}

收取里,咱们种种消除地点的题材。

我们首先写一个mapDispatchToProps函数,封装一下索要派发的action,使得UI组件中的代码能相比便利调用action。

附带,再写二个mapStateToProps函数,重返看要订阅的store的场合(在本例子中,正是储存在store中的articleList)。须求极度表达的是,这些state.articleList.articleList,
中间的articleList,是你在rootreducer.jsx中export的值。

终极动用React-Redux库中的connet,将地方七个函数绑定到UI组件上。那样,UI组件就能够透过this.props中得到store中的状态值了,并且每一次store中的状态值变化,也会触发React组件中props的变更,触发React生命周期的相干函数回调。同时,假诺UI组件中想要触发二次Action派发,也是在props里面举办this.props.actions.fetch阿特icleList();即可。上边八个函数已经把action和store中的state都绑定到props对象里了。

从那之后,已经把全路德姆o的显要代码进行逐1说明。详细的代码提出大家去本人的github上下载下来看。那里就不提供全量代码了。

使用 webpack
监听编译文件,nodemon
监听服务器文件变动

react-router四.x 和 服务端的路由完结同构。

react-router肆.x 相对于以前的版本,做了较大的变动。
整个路由变得组件化了。
能够重点看那里
官方给出了详尽的事例和文书档案能够看成着力思索的和规范参照。

服务端渲染与客户端渲染的差别之处在于其路由是未有动静的,所以我们须求通过3个无状态的router组件
来包裹应用程式,通过服务端请求的url来合作到现实的路由数组和其有关属性。
之所以我们在客户端选择 BrowserRouter,服务端则利用无状态的 StaticRouter。

  • BrowserRouter 使用 HTML5 提供的 history API (pushState, replaceState
    和 popstate 事件) 来保持 UI 和 URL 的同步。
  • StaticRouter 是3个不会变动地址的router组件 。
    参考代码如下所示:

// 服务端路由配置
import { createServer } from 'http'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { StaticRouter } from 'react-router'
import App from './App'

createServer((req, res) => {
  const context = {}

  const html = ReactDOMServer.renderToString(
    <StaticRouter
      location={req.url}
      context={context}
    >
      <App/>
    </StaticRouter>
  )

  if (context.url) {
    res.writeHead(301, {
      Location: context.url
    })
    res.end()
  } else {
    res.write(`
      <!doctype html>
      <div id="app">${html}</div>
    `)
    res.end()
  }
}).listen(3000)
And then the client:import ReactDOM from 'react-dom'

// 客户端路由配置
import { BrowserRouter } from 'react-router-dom'
import App from './App'

ReactDOM.render((
  <BrowserRouter>
    <App/>
  </BrowserRouter>
), document.getElementById('app'))

小编们把koa的路由url传入 <StaticRouter /> ,后者会基于url
自动相配对应的React组件,那样大家就能兑现,刷新页面,服务端重返的对应路由组件与客户端一致。
到这一步我们早就能够兑现页面刷新 服务端和客户端保持壹致了。

注意点:使用框架前必定肯定当前webpack版本为3.x
Node为捌.x以上,读者最棒用React在4个月以上,并有实在React项目经验

依据demo代码实行连锁支付步骤

上边代码已经详尽到,从零开头搭壹套React大礼包了。可是一旦你实际不想本身搭1套,去折腾什么依赖包难题,webpack难点,中间件难题,fetch请求包容性难点等一大堆难点。想在本demo上直接支出,也是足以的。大概步骤如下:

1.写好UI组件。

贰.在index.jsx中配备好路由。

借使有异步请求,则还必要以下步骤:

3.在constants.jsx中设置action.type,供reducer和action使用。

四.在reducers文件夹中添加reducer。

伍.在rootreducer。jsx中添加四中的文件

陆.在actions文件夹中添加相关action,倘使有ajax请求,提议分为fetch,receive七个action

柒.在actions。jsx中添加六中的文件

八.在对应的组件中编辑对应的逻辑。etc:@connect(mapStateToProps,
mapDispatchToProps)等

使用 redux-saga
处理异步action,使用 express
处理页面渲染

Redux 服务端同构

先是下官方文书档案做了简易的介绍介绍http://cn.redux.js.org/docs/recipes/ServerRendering.html.

其拍卖步骤如下:

  • 一 大家依据对应的服务端请求API 获得相应的异步方法赢获得异步数据。
  • 二 使用异步数据变化一个开端化的store
    const store = createStore(counterApp, preloadedState),
  • 3然后调用const finalState = store.getState()主意获得到store的起头化state.
  • 肆 将开头的initState 作为参数字传送递到客户端
  • 伍 客户端开端化的时候回来判断 window.INITIAL_STATE
    下边是不是有数据,倘诺有则作为早先数据再次生成二个客户端的store.
    如上边代码所示。

服务端

 <html>
      <head>
        <title>Redux Universal Example</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(finalState)}
        </script>
        <script src="/static/bundle.js"></script>
      </body>
    </html>    

客户端

...
// 通过服务端注入的全局变量得到初始 state
const preloadedState = window.__INITIAL_STATE__

// 使用初始 state 创建 Redux store
const store = createStore(counterApp, preloadedState)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

以此差不离正是二个正经的redux同构流程,
其实越多的法定是在给大家提供壹种口径的思路,我们得以顺着这么些做愈来愈多的优化。
第二我们并不须要直接通过API作为映射
服务端和客户端各搞一套异步加载的主意,那样显得煞是冗余。
react-router
包里面提供了react-router-config注重用以静态路由配置。
提供的 matchRoutes API能够依据传入的url
再次回到对应的路由数组。大家能够经过这一个法子在服务端直接待上访问到相应的React组件。
假诺要从路由中平素获得异步方法,笔者看了过多近似的同构方案,

  • 重视有三种办法一种是直接在路由中加进八个thunk方法,通过那几个点子直接去取得开始化的异步数据,
    本人觉着可取是比较鲜明直观,直接在路由层就把那些工作消除了。
  • 第三种是运用class
    的静态方法。大家得以因此路由访问到零部件的类上面包车型大巴static方法。
    那样大家就一向能够在容器组件内部同时评释服务端开始化方法和客户端早先化方法了
    那样处理的层级放到了组件里面笔者要好认为更能反映组件的独立性吧。

本项目利用了第3种方案,先看一下代码:

/**
 * 渲染服务端路由
 */
module.exports.render = async(ctx,next) =>{
    const { store ,history} = getCreateStore(ctx);
    const branch = matchRoutes(router, ctx.req.url);
    const promises = branch.map(({route}) => {
        const fetch = route.component.fetch;
        return fetch instanceof Function ? fetch(store) : Promise.resolve(null)
    });
    await Promise.all(promises).catch((err)=>{
        console.log(err);
    }); 

    const html = ReactDOMServer.renderToString(
                <Provider store={store}>
                            <StaticRouter
                            location={ctx.url}
                            context={{}}>
                                <App/>
                            </StaticRouter>
                </Provider>
        )
        let initState=store.getState();
        const body =  layout(html,initState);
   ctx.body =body;
}

对应容器组件提供了1个静态的fetch方法

class Home extends Component {
  ...
  static fetch(store){
        return store.dispatch(fetchBookList({page:1,size:20}))
  }

那是大家的 actions

/**
 * 获取书籍目录
 * @param {*} param 
 */
export const fetchBookList = (params) => {
    return async (dispatch, getState) => {
        await axios.get(api.url.booklist, {
            params: params
        }).then((res) => {
            dispatch(booklist(res.data.result));
        }).catch((err) => {

        })
    }
}

先是大家通过 matchRoutes
得到最近路由下全数的路由,再对其遍历得到关于三个异步方法的Promise数组,那里大家所谓的异步方法就是actions中的异步方法。由于大家在服务端也开头化的store所以大家能够一贯在服务端调用actions,那里大家要求给容器组件的static方法传入store
,那样大家就足以因而store.dispatch(fetchBookList({page:1,size:20}))调用actions了。上面包车型的士法门我们获得了贰个Promise
数组。大家利用 Promise.all将异步全体执行。那个时候实在
store的周转跟客户端是壹律的。 大家在异步的进程中 将起来数据总体写入了
store中。所以我们透过store.getState()就能够获得起始化数据了。客户端的初步化跟Redux官方例子是同样的。间接判断是不是传入开端化state,尽管传入就做为起初化数据。大家服务端的初步化异步和客户端的早先化异步
怎么着制止再次。 那里大家一贯先得到store中的对应开首数据
,看是不是存在,固然不存在我们再拓展加载。

到这一步大家曾经得以兑现刷新页面异步数据服务端处理,不刷新页日前端处理,叁个主旨的同构方案主体就出去了,剩下的正是一对优化项和部分项目定制性的东西了。

项目目录介绍

总结

本来,那几个demo代码比较不难,只是为了用React大礼包而用React大礼包,太过度复杂的代码也不相符实行教学表明。而且,那套框架搭起来了,依然有好多标题,很多规范须求去消除。比如如何时候把组件中的state放到store中,怎么样接着精简代码,去掉rootreducer.jsx、actions.jsx等等等等。都值得举办深切的上学和探究,也欢迎大家那篇小说上面评论切磋,建议化解方案。

想把那套框架吃下来,合理利用起来,光是这个代码和上学也是遥远不够的。还必要越来越多的就学和执行,可是希望那篇文章提供部分信心,成为2个好的源点。

本项目涵盖三个页面,多种组成,满满的干货,文字恐怕说不清楚,就去看代码吧!

服务端页面分发

对此服务器而言不仅会收下前端路由的伏乞还会接到各个别的静态财富的呼吁
import {matchPath} from 'react-router-dom';
大家那里运用react-router-dom包里面包车型客车 matchPath API
来合作当前恳请路由是不是与大家客户端的路由配置相同纵然不相同大家暗中同意为请求的是静态财富或其余。假设不相称当前路由大家一直实施
next() 进入到下三个个中件
。因为大家那么些连串实在依然是1个上下端分离的档次
只可是增添了服务端渲染的办法而已。
假若服务端还要处理任何请求,那么实际上大家也得以在经过劳务端 扩张别的路由
,通过炫耀来同盟对应的渲染页面和API。

├── assets
│ └── index.css //放置一些全局的资源文件 可以是js 图片等
├── config
│ ├── webpack.config.dev.js 开发环境webpack打包设置
│ └── webpack.config.prod.js 生产环境webpack打包设置
├── package.json
├── README.md
├── server server端渲染文件,如果对不是很了解,建议参考[koa教程](http://wlxadyl.cn/2018/02/11/koa-learn/)
│ ├── app.js
│ ├── clientRouter.js // 在此文件中包含了把服务端路由匹配到react路由的逻辑
│ ├── ignore.js
│ └── index.js
└── src
 ├── app 此文件夹下主要用于放置浏览器和服务端通用逻辑
 │ ├── configureStore.js //redux-thunk设置
 │ ├── createApp.js  //根据渲染环境不同来设置不同的router模式
 │ ├── index.js
 │ └── router
 │  ├── index.js
 │  └── routes.js  //路由配置文件! 重要
 ├── assets
 │ ├── css    放置一些公共的样式文件
 │ │ ├── _base.scss  //很多项目都会用到的初始化css
 │ │ ├── index.scss
 │ │ └── my.scss
 │ └── img
 ├── components    放置一些公共的组件
 │ ├── FloatDownloadBtn 公共组件样例写法
 │ │ ├── FloatDownloadBtn.js
 │ │ ├── FloatDownloadBtn.scss
 │ │ └── index.js
 │ ├── Loading.js
 │ └── Model.js   函数式组件的写法
 │
 ├── favicon.ico
 ├── index.ejs    //渲染的模板 如果项目需要,可以放一些公共文件进去
 ├── index.js    //包括热更新的逻辑
 ├── pages     页面组件文件夹
 │ ├── home
 │ │ ├── components  // 用于放置页面组件,主要逻辑
 │ │ │ └── homePage.js
 │ │ ├── containers  // 使用connect来封装出高阶组件 注入全局state数据
 │ │ │ └── homeContainer.js
 │ │ ├── index.js  // 页面路由配置文件 注意thunk属性
 │ │ └── reducer
 │ │  └── index.js // 页面的reducer 这里暴露出来给store统一处理 注意写法
 │ └── user
 │  ├── components
 │  │ └── userPage.js
 │  ├── containers
 │  │ └── userContainer.js
 │  └── index.js
 └── store
  ├── actions   // 各action存放地
  │ ├── home.js
  │ └── thunk.js
  ├── constants.js  // 各action名称汇集处 防止重名
  └── reducers
   └── index.js  // 引用各页面的所有reducer 在此处统一combine处理

临走在此之前,作者还有话要说

实则在1伍年的时候就曾经早先写前端代码了,用的是jQuery。然后到2018年,起头认真研商React及其相关生态系统。感觉今后的前端,处于3个群雄割据的时期。老派的还在用jQuery,然后以往可比主流的叁大框架React,Vue,Angular,都占据相当大的使用比例。那对于前端学习者,小编以为已经算是非常大的承担,并且那么些技能还在全速迭代着。当然那也无可厚非,大家都想一统天下,可是,在那种急于的方向,导致众多时候,本人都没统1团结。作者在用webpack的时候,刚好经历webpack壹连接到webpack2,然后发现webpack二竟然不包容webpack一,要做代码升级及卓殊。听前端的同时说,Angular贰和Angular1,差异也非常的大。那壹密密麻麻的改动,也导致了相应的插件和api库跟着不相配,要求进步。导致这些进度十二分的悲苦。暂别前端,希望下次回来,前端有1番新气象。

  1. React
  2. React +
    SSR
  3. React +
    Redux
  4. React + Redux +
    SSR

其他

写那么些demo看了无数的github项目以及相关文章,这么些素材对本项目有一点都不小的启迪

Vue.js
服务器端渲染指南

react-server

beidou

react-ssr-optimization

React-universal-ssr

fairy

D二 –
构建高可相信与高品质的React同构化解方案

Egg + React
服务端渲染开发指南

服务端渲染与 Universal React
App

React同构直出优化总计

React移动web极致优化

https://github.com/joeyguo

项目标营造思路

连带技能资料站

  • React Router
    中文文书档案
  • Redux
    国语文书档案
  • LearnRedux录像教程
  • happypoulp/Redux-tutorial
  • webpack官方文书档案
  • wuqke的github:React+React-Router+Redux demo
    例子

 

总结

咱俩掌握服务端渲染的
优势在于能够非常的慢的首屏优化
,帮助SEO,与守旧的SPA比较多了①种多少的处理情势。
症结也充足引人注目,服务端渲染也便是是把客户端的拍卖流程部分移植到了服务端,那样就充实了服务端的负荷。因而要做3个好的SS奥迪Q三方案,缓存是至关重要的。与此同时工程化方面也是有千千万万值得优化的地点。那里只是半途而废,并从未做连锁的拍卖,臆度前面有时间会做1些优化欢迎大家关切。

本项目github地址
https://github.com/yangfan0095/react-koa2-ssr

以上です

  1. 本地开发应用webpack-dev-server,达成热更新,基本流程跟之前react开发近乎,仍是浏览器端渲染,因而在编写制定代码时要思虑到一套逻辑,三种渲染环境的题材。
  2. 当前端页面渲染完毕后,其Router跳转将不会对服务端举办呼吁,从而减轻服务端压力,从而页面包车型客车进去艺术也是三种,还要怀念两种渲染环境下路由同构的题材。
  3. 生产环境要选用koa做后端服务器,实现按需加载,在服务端获取数据,并渲染出全方位HTML,利用React1陆流行的能力来归并整个场馆树,达成服务端渲染。

一、React

落到实处贰个最中央的React组件,就能搞掂第二个页面了

/**
 * 消息列表
 */
class Message extends Component {
    constructor(props) {
        super(props);

        this.state = {
            msgs: []
        };
    }

    componentDidMount() {
        setTimeout(() => {
            this.setState({
                msgs: [{
                    id: '1',
                    content: '我是消息我是消息我是消息',
                    time: '2018-11-23 12:33:44',
                    userName: '王羲之'
                }, {
                    id: '2',
                    content: '我是消息我是消息我是消息2',
                    time: '2018-11-23 12:33:45',
                    userName: '王博之'
                }, {
                    id: '3',
                    content: '我是消息我是消息我是消息3',
                    time: '2018-11-23 12:33:44',
                    userName: '王安石'
                }, {
                    id: '4',
                    content: '我是消息我是消息我是消息45',
                    time: '2018-11-23 12:33:45',
                    userName: '王明'
                }]
            });
        }, 1000);
    }

    // 消息已阅
    msgRead(id, e) {
        let msgs = this.state.msgs;
        let itemIndex = msgs.findIndex(item => item.id === id);

        if (itemIndex !== -1) {
            msgs.splice(itemIndex, 1);

            this.setState({
                msgs
            });
        }
    }

    render() {
        return (
            <div>
                <h4>消息列表</h4>
                <div className="msg-items">
                {
                    this.state.msgs.map(item => {
                        return (
                            <div key={item.id} className="msg-item">
                                <p className="msg-item__header">{item.userName} - {item.time}</p>
                                <p className="msg-item__content">{item.content}</p>
                                <a href="javascript:;" className="msg-item__read" onClick={this.msgRead.bind(this, item.id)}>&times;</a>
                            </div>
                        )
                    })
                }
                </div>
            </div>
        )
    }
}

render(<Message />, document.getElementById('content'));

是还是不是很不难,代码比较不难就背着了

来看看页面效果

4858美高梅 3

能够观望页面白屏时间比较长

那里有多个白屏

  1. 加载完JS后才早先化标题

  2. 拓展异步请求数据,再将音信列表渲染

看起来是搁浅地相比久的,那么使用服务端渲染有何样意义啊?

 

本土开发介绍

二. React + SSR

在讲怎样落实此前,先看看最后效果

能够看看页面是直出的,未有间断

4858美高梅 4

 

在React 15中,实现服务端渲染主要靠的是 ReactDOMServer 的 renderToString
和 renderToStatic马克up方法。

let ReactDOMServer = require('react-dom/server');

ReactDOMServer.renderToString(<Message preloadState={preloadState} />)

ReactDOMServer.renderToStaticMarkup(<Message preloadState={preloadState} />)

将零件直接在服务端处理为字符串,大家根据传入的启幕状态值,在服务端举行零部件的早先化

然后在Node环境中回到,比如在Express框架中,再次来到渲染三个模板文件

      res.render('messageClient/message.html', {
            appHtml: appHtml,
            preloadState: JSON.stringify(preloadState).replace(/</g, '\\u003c')
        });

此处安装了三个变量传递给模板文件

appHtml 即为处理未来的零件字符串

preloadState
为服务器中的起初状态,浏览器的接轨工作要依据那些开首状态,所以需求将此变量传递给浏览器开头化

        <div id="content">
            <|- appHtml |>
        </div>
        <script id="preload-state">
            var PRELOAD_STATE = <|- preloadState |>
        </script>

express框架再次来到之后即为在浏览器中观望的上马页面

内需留意的是那里的ejs模板举行了自定义分隔符,因为webpack在进展编写翻译时,HtmlWebpackPlugin
插件中自带的ejs处理器恐怕会和那些模板中的ejs变量争论

在express中自定义即可

// 自定义ejs模板
app.engine('html', ejs.__express);
app.set('view engine', 'html');
ejs.delimiter = '|';

接下去,在浏览器环境的零部件中(以下那个文件为公共文件,浏览器端和劳动器端共用),我们要依据 PRELOAD_STATE 这几个开始状态来早先化组件

class Message extends Component {
    constructor(props) {
        super(props);

        this.state = {
            msg: []
        };

        // 根据服务器返回的初始状态来初始化
        if (typeof PRELOAD_STATE !== 'undefined') {
            this.state.msgs = PRELOAD_STATE;
            // 清除
            PRELOAD_STATE = null;
            document.getElementById('preload-state').remove();
        }
        // 此文件为公共文件,服务端调用此组件时会传入初始的状态preloadState
        else {
            this.state.msgs = this.props.preloadState;
        }

        console.log(this.state);
    }

    componentDidMount() {
        // 此处无需再发请求,由服务器处理
    }
...

大旨正是那几个了,那就完了么?

哪有那么快,还得精晓哪些编写翻译文件(JSX并不是原生协理的),服务端如何处理,浏览器端怎么着处理

接下去看看项指标文本结构

4858美高梅 5 
 4858美高梅 6

把集中力集中到红框中

一直由webpack.config.js同时编写翻译浏览器端和服务端的JS模块

module.exports = [
    clientConfig,
    serverConfig
];

浏览器端的配备使用 src 下的 client目录,编译到 dist 目录中

服务端的配置利用 src 下的 server 目录,编写翻译到 distSSCRUISER目录中。在服务端的陈设中就不须求展开css文件提取等毫无干系的拍卖的,关心编写翻译代码开头化组件状态即可

其余,服务端的配备的ibraryTarget记得使用 ‘commonjs二’,才能为Node环境所识别

// 文件输出配置
    output: {
        // 输出所在目录
        path: path.resolve(__dirname, '../public/static/distSSR/js/'),
        filename: '[name].js',
        library: 'node',
        libraryTarget: 'commonjs2'
    },

 

client和server只是进口,它们的公物部分在 common 目录中

在client中,直接渲染导入的零部件  

import React, {Component} from 'react';
import {render, hydrate, findDOMNode} from 'react-dom';
import Message from '../common/message';

hydrate(<Message />, document.getElementById('content'));

此间有个 render和hydrate的分歧

在开始展览了服务端渲染之后,浏览器端使用render的话会依据气象重新开始化1次组件,恐怕会有震动的事态;使用
hydrate则只进行零部件事件的开头化,组件不会起来早先化状态

提出选拔hydrate方法,在React壹7中 使用了服务端渲染之后,render将不再协助

在 server中,导出这一个组件给 express框架调用

import Message from '../common/message';

let ReactDOMServer = require('react-dom/server');

/**
 * 提供给Node环境调用,传入初始状态
 * @param  {[type]} preloadState [description]
 * @return {[type]}              [description]
 */
export function init(preloadState) {
    return ReactDOMServer.renderToString(<Message preloadState={preloadState} />);
};

亟需小心的是,那里不能够一向运用 module.exports = …
因为webpack不协理ES陆的 import 和那几个混用

在 common中,处理部分浏览器端和劳务器端的反差,再导出

那边的差距主借使变量的应用难题,在Node中绝非window document navigator
等对象,直接采纳会报错。且Node中的严苛形式直接待上访问未定义的变量也会报错

为此需求用typeof
举办变量检查评定,项目中援引的第一方插件组件有利用到了这一个浏览器环境目的的,要小心抓好协作,最简便易行的办法是在
componentDidMount 中再引进那些插件组件

其余,webpack的style-loader也凭借了这么些指标,在服务器配置文件中要求将其移除

 {
            test: /\.css$/,
            loaders: [
                // 'style-loader',
                'happypack/loader?id=css'
            ]
        }

在Express的服务器框架中,messageSS奥迪Q3 路由
渲染页面从前做1些异步操作获取数据

// 编译后的文件路径
let distPath = '../../public/static/distSSR/js';

module.exports = function(req, res, next) {
    // 如果需要id
    let id = 'req.params.id';

    console.log(id);

    getDefaultData(id);

    async function getDefaultData(id) {
        let appHtml = '';
        let preloadState = await getData(id);

        console.log('preloadState', preloadState);

        try {
            // 获取组件的值(字符串)
            appHtml = require(`${distPath}/message`).init(preloadState);
        } catch(e) {
            console.log(e);
            console.trace();
        }

        res.render('messageClient/message.html', {
            appHtml: appHtml,
            preloadState: JSON.stringify(preloadState).replace(/</g, '\\u003c')
        });
    }
};

利用到Node来打开服务,每一遍改了服务器文件从此就得重启相比费力

动用 nodemon工具来监听文件修改自动更新服务器,添加配置文件 nodemon.json

{
    "restartable": "rs",
    "ignore": [
        ".git",
        "node_modules/**/node_modules"
    ],
    "verbose": true,
    "execMap": {
        "js": "node --harmony"
    },
    "watch": [
        "server/",
        "public/static/distSSR"
    ],
    "env": {
        "NODE_ENV": "development"
    },
    "ext": "js,json"
}

本来,对于Node环境不扶助JSX那些难题,除了采纳webpack进行编写翻译之外,

仍可以够在Node中履行 babel-node
来即时地编写翻译文件,然而那种办法会导致每一趟编译卓殊久(至少比webpack久)

 

在React1陆 中,ReactDOMServer 除了具有 renderToString 和
renderToStatic马克up那八个点子之外,

还有 renderToNodeStream  和 renderToStaticNodeStream 五个流的章程

它们不是重临一个字符串,而是重回1个可读流,三个用以发送字节流的靶子的Node
Stream类

渲染到流能够减掉你的剧情的率先个字节(TTFB)的时光,在文书档案的下1部分生成在此之前,将文书档案的上马至最终发送到浏览器。
当内容从服务器流式传输时,浏览器将早先解析HTML文书档案

以下是使用实例,本文不开始展览

// using Express
import { renderToNodeStream } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
  res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
  res.write("<div id='content'>"); 
  const stream = renderToNodeStream(<MyPage/>);
  stream.pipe(res, { end: false });
  stream.on('end', () => {
    res.write("</div></body></html>");
    res.end();
  });
});

 

那就是在React中展开服务端渲染的流水生产线了,说得有点泛泛,依旧友好去看 花色代码 吧

 

查看本地开发首要涉及的文本是src目录下的index.js文件,判断当前的周转环境,唯有在开发环境下才会选取module.hot的API,实现当reducer产生变化时的页面渲染更新文告,注意个中的hydrate方法,那是v1陆本子的三个专程为服务端渲染新增的API方法,它在render方法的基本功上完毕了对服务端渲染内容的最大可能重用,完结了静态DOM到动态NODES的经过。实质是代表了v15本子下判断checksum标记的历程,使得重用的长河特别火速优雅。

三、React + Redux

React的中的数据是单向流动的,即父组件状态改变现在,能够通过props将质量传递给子组件,但子组件并无法向来改动父级的组件。

貌似须要经过调用父组件传来的回调函数来直接地修改父级状态,只怕接纳Context ,使用 事件宣布订阅机制等。

引进了Redux进行情状管理之后,就有利于一些了。不过会增添代码复杂度,别的要注意的是,React
16的新的Context特性貌似给Redux带来了许多碰上

 

在React项目中接纳Redux,当有些处理有相比多逻辑时,遵从胖action瘦reducer,比较通用的提出时将重大逻辑放在action中,在reducer中只实行翻新state的等简便的操作

一般还必要中间件来处理异步的动作(action),相比广泛的有八种redux-thunk  redux-saga  redux-promise  redux-observable
,它们的对照

那里选拔了 redux-saga,它比较优雅,管理异步也很有优势

 

来看望项目组织

4858美高梅 7

咱俩将 home组件拆分出多少个子组件便于维护,也有益和Redux举行关联

home.js 为进口文件

行使 Provider 包装组件,传入store状态渲染组件

import React, {Component} from 'react';
import {render, findDOMNode} from 'react-dom';
import {Provider} from 'react-redux';

// 组件入口
import Home from './homeComponent/Home.jsx';
import store from './store';

/**
 * 组装Redux应用
 */
class App extends Component {
    render() {
        return (
            <Provider store={store}>
                <Home />
            </Provider>
        )
    }
}

render(<App />, document.getElementById('content'));

store/index.js 中为状态成立的进度

此间为了有利于,就把服务端渲染的局部也坐落一起了,实际上它们的差异不是相当大,仅仅是
defaultState伊始状态的两样而已

import {createStore, applyMiddleware, compose} from 'redux';
import createSagaMiddleware from 'redux-saga';
// import {thunk} from 'redux-thunk';

import reducers from './reducers';
import wordListSaga from './workListSaga';
import state from './state';

const sagaMiddleware = createSagaMiddleware();

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

let defaultState = state;

// 用于SSR
// 根据服务器返回的初始状态来初始化
if (typeof PRELOAD_STATE !== 'undefined') {
    defaultState = Object.assign({}, defaultState, PRELOAD_STATE);
    // 清除
    PRELOAD_STATE = null;
    document.getElementById('preload-state').remove();
}

let store = createStore(
    reducers,
    defaultState,
    composeEnhancers(
        applyMiddleware(sagaMiddleware)
    ));

sagaMiddleware.run(wordListSaga);

export default store;

大家将有个别action(基本是异步的)交给saga处理

在workListSaga.js中,

4858美高梅 84858美高梅 9

 1 import {delay} from 'redux-saga';
 2 import {put, fork, takeEvery, takeLatest, call, all, select} from 'redux-saga/effects';
 3 
 4 import * as actionTypes from './types';
 5 
 6 /**
 7  * 获取用户信息
 8  * @yield {[type]} [description]
 9  */
10 function* getUserInfoHandle() {
11     let state = yield select();
12 
13     return yield new Promise((resolve, reject) => {
14         setTimeout(() => {
15             resolve({
16                 sex: 'male',
17                 age: 18,
18                 name: '王羲之',
19                 avatar: '/public/static/imgs/avatar.png'
20             });
21         }, 500);
22     });
23 }
24 
25 /**
26  * 获取工作列表
27  * @yield {[type]} [description]
28  */
29 function* getWorkListHandle() {
30     let state = yield select();
31 
32     return yield new Promise((resolve, reject) => {
33         setTimeout(() => {
34             resolve({
35                 todo: [{
36                     id: '1',
37                     content: '跑步'
38                 }, {
39                     id: '2',
40                     content: '游泳'
41                 }],
42 
43                 done: [{
44                     id: '13',
45                     content: '看书'
46                 }, {
47                     id: '24',
48                     content: '写代码'
49                 }]
50             });
51         }, 1000);
52     });
53 }
54 
55 /**
56  * 获取页面数据,action.payload中如果为回调,可以处理一些异步数据初始化之后的操作
57  * @param {[type]} action        [description]
58  * @yield {[type]} [description]
59  */
60 function* getPageInfoAsync(action) {
61     console.log(action);
62 
63     let userInfo = yield call(getUserInfoHandle);
64 
65     yield put({
66         type: actionTypes.INIT_USER_INFO,
67         payload: userInfo
68     });
69 
70     let workList = yield call(getWorkListHandle);
71 
72     yield put({
73         type: actionTypes.INIT_WORK_LIST,
74         payload: workList
75     });
76 
77     console.log('saga done');
78 
79     typeof action.payload === 'function' && action.payload();
80 }
81 
82 /**
83  * 获取页面数据
84  * @yield {[type]} [description]
85  */
86 export default function* getPageInfo() {
87     yield takeLatest(actionTypes.INIT_PAGE, getPageInfoAsync);
88 }

View Code

监听页面包车型大巴开头化action actionTypes.INIT_PAGE
,获取数据之后再接触3个action ,转交给reducer即可

let userInfo = yield call(getUserInfoHandle);

    yield put({
        type: actionTypes.INIT_USER_INFO,
        payload: userInfo
    });

reducer中做的事根本是翻新情形,

import * as actionTypes from './types';
import defaultState from './state';

/**
 * 工作列表处理
 * @param  {[type]} state  [description]
 * @param  {[type]} action [description]
 * @return {[type]}        [description]
 */
function workListReducer(state = defaultState, action) {
    switch (action.type) {
        // 初始化用户信息
        case actionTypes.INIT_USER_INFO:
            // 返回新的状态
            return Object.assign({}, state, {
                userInfo: action.payload
            });

        // 初始化工作列表
        case actionTypes.INIT_WORK_LIST:
            return Object.assign({}, state, {
                todo: action.payload.todo,
                done: action.payload.done
            });

        // 添加任务
        case actionTypes.ADD_WORK_TODO:
            return Object.assign({}, state, {
                todo: action.payload
            });

        // 设置任务完成
        case actionTypes.SET_WORK_DONE:
            return Object.assign({}, state, {
                todo: action.payload.todo,
                done: action.payload.done
            });

        default:
            return state
    }
}

在 action.js中能够定义壹些正规的action,比如

export function addWorkTodo(todoList, content) {
    let id = Math.random();

    let todo = [...todoList, {
        id,
        content
    }];

    return {
        type: actionTypes.ADD_WORK_TODO,
        payload: todo
    }
}

/**
 * 初始化页面信息
 * 此action为redux-saga所监听,将传入saga中执行
 */
export function initPage(cb) {
    console.log(122)
    return {
        type: actionTypes.INIT_PAGE,
        payload: cb
    };
}

重回刚才的 home.js入口文件,在其引进的主模块
home.jsx中,我们需求将redux的东西和那个 home.jsx绑定起来

import {connect} from 'react-redux';

// 子组件
import User from './user';
import WorkList from './workList';

import  {getUrlParam} from '../util/util'
import '../../scss/home.scss';

import {
    initPage
} from '../store/actions.js';

/**
 * 将redux中的state通过props传给react组件
 * @param  {[type]} state [description]
 * @return {[type]}       [description]
 */
function mapStateToProps(state) {
    return {
        userInfo: state.userInfo,
        // 假如父组件Home也需要知悉子组件WorkList的这两个状态,则可以传入这两个属性
        todo: state.todo,
        done: state.done
    };
}

/**
 * 将redux中的dispatch方法通过props传给react组件
 * @param  {[type]} state [description]
 * @return {[type]}       [description]
 */
function mapDispatchToProps(dispatch, ownProps) {
    return {
        // 通过props传入initPage这个dispatch方法
        initPage: (cb) => {
            dispatch(initPage(cb));
        }
    };
}

...

class Home extends Component {
...

export default connect(mapStateToProps, mapDispatchToProps)(Home);

本来,并不是不得不给store绑定三个组件

一经有个别组件的动静能够被此外零件共享,只怕这么些组件需求拜访store,按根组件1层壹层通过props传入很麻烦的话,也得以直接给那一个组件绑定store

诸如那里的 workList.jsx
也进展了绑定,user.jsx那种只要求出示数据的零部件,大概其余部分自治(状态在内部管理,和外部非亲非故)的零件,则不必要引入redux的store,也挺麻烦的

 

绑定之后,大家须要在 Home组件中调用action,开首获取数据

   /**
     * 初始获取数据之后的某些操作
     * @return {[type]} [description]
     */
    afterInit() {
        console.log('afterInit');
    }

    componentDidMount() {
        console.log('componentDidMount');

        // 初始化发出 INIT_PAGE 操作
        this.props.initPage(() => {
            this.afterInit();
        });
    }

此间有个小技巧,假使在收获异步数据以后要接着举行其他操作,能够流传
callback ,大家在action的payload中置入了这一个 callback,方便调用

下一场Home组件中的已经远非多少state了,已经交由store管理,通过mapStateToProps传入

据此能够遵照props得到那么些属性

<User {...this.props.userInfo} />

抑或调用传入的 reducer ,直接地派发一些action

    // 执行 ADD_WORK_TODO
        this.props.addWorkTodo(this.props.todo, content.trim());

 

页面展现

4858美高梅 10

 

const renderApp=()=>{
 let application=createApp({store,history});
 hydrate(application,document.getElementById('root'));
}
window.main = () => {
 Loadable.preloadReady().then(() => {
 renderApp()
 });
};

if(process.env.NODE_ENV==='development'){
 if(module.hot){
 module.hot.accept('./store/reducers/index.js',()=>{
  let newReducer=require('./store/reducers/index.js');
  store.replaceReducer(newReducer)
 })
 module.hot.accept('./app/index.js',()=>{
  let {createApp}=require('./app/index.js');
  let newReducer=require('./store/reducers/index.js');
  store.replaceReducer(newReducer)
  let application=createApp({store,history});
  hydrate(application,document.getElementById('root'));
 })
 }
}

四、React + Redux + SSR

能够看到上海体育地方是有一对眨眼的,因为数量不是壹早先就存在

设想投入SS陆风X八,先来看看最终页面效果,效用大致,但直接出来了,看起来相当漂亮好呀~

4858美高梅 11

在Redux中插足SS福睿斯, 其实跟纯粹的React组件是近似的。

合法给了二个简易的例证

都以在劳务器端获取开首状态后甩卖组件为字符串,分歧主若是React直接利用state,
Redux直接利用store

浏览器中我们得以为两个页面使用同3个store,但在劳动器端不行,大家须要为每二个呼吁成立3个store

 

再来看项目组织,Redux的SS昂Cora使用到了红框中的文件

4858美高梅 12

劳务端路由homeSSOdyssey与messageSS奥迪Q7类似,都以回去数据

服务端入口文件 server中的home.js 则是成立3个新的 store,
然后传出ReactDOMServer举行处理回来

import {createStore} from 'redux';
import reducers from '../store/reducers';
import App from '../common/home';
import defaultState from '../store/state';

let ReactDOMServer = require('react-dom/server');

export function init(preloadState) {
    // console.log(preloadState);

    let defaultState = Object.assign({}, defaultState, preloadState);

    // 服务器需要为每个请求创建一份store,并将状态初始化为preloadState
    let store = createStore(
        reducers,
        defaultState
    );

    return ReactDOMServer.renderToString(<App store={store} />);
};

无差别于的,大家供给在common文件中处理 Node环境与浏览器环境的部分差距

比如在 home.jsx 中,加入

// 公共部分,在Node环境中无window document navigator 等对象
if (typeof window === 'undefined') {
    // 设置win变量方便在其他地方判断环境
    global.win = false;
    global.window = {};
    global.document = {};
}

除此以外组件加载之后也不必要发请求获取数据了

/**
     * 初始获取数据之后的某些操作
     * @return {[type]} [description]
     */
    afterInit() {
        console.log('afterInit');
    }

    componentDidMount() {
        console.log('componentDidMount');

        // 初始化发出 INIT_PAGE 操作;
        // 已交由服务器渲染
        // this.props.initPage(() => {
            this.afterInit();
        // });
    }

common中的home.js入口文件用于给组件管理store,
与未用SS奥德赛的公文分裂(js目录下边包车型地铁home.js入口)

它需求同时为浏览器端和劳务器端服务,所以扩大部分判断,然后导出

if (module.hot) {
    module.hot.accept();
}

import React, {Component} from 'react';
import {render, findDOMNode} from 'react-dom';
import Home from './homeComponent/home.jsx';
import {Provider} from 'react-redux';
import store from '../store';

class App extends Component {
    render() {
        // 如果为Node环境,则取由服务器返回的store值,否则使用 ../store中返回的值
        let st = global.win === false ? this.props.store : store;

        return (
            <Provider store={st}>
                <Home />
            </Provider>
        )
    }
}

export default App;

浏览器端的输入文件 home.js 直接引用渲染即可

import React, {Component} from 'react';
import {render, hydrate, findDOMNode} from 'react-dom';
import App from '../common/home';

// render(<App />, document.getElementById('content'));
hydrate(<App />, document.getElementById('content'));

 

那就是Redux 加上 SSRubicon之后的流水生产线了

 

实际上还漏了贰个Express的server.js服务文件,也就一丢丢代码

4858美高梅 134858美高梅 14

 1 const express = require('express');
 2 const path = require('path');
 3 const app = express();
 4 const ejs = require('ejs');
 5 
 6 // 常规路由页面
 7 let home = require('./routes/home');
 8 let message = require('./routes/message');
 9 
10 // 用于SSR服务端渲染的页面
11 let homeSSR = require('./routes/homeSSR');
12 let messageSSR = require('./routes/messageSSR');
13 
14 app.use(express.static(path.join(__dirname, '../')));
15 
16 // 自定义ejs模板
17 app.engine('html', ejs.__express);
18 app.set('view engine', 'html');
19 ejs.delimiter = '|';
20 
21 app.set('views', path.join(__dirname, '../views/'));
22 
23 app.get('/home', home);
24 app.get('/message', message);
25 
26 app.get('/ssr/home', homeSSR);
27 app.get('/ssr/message', messageSSR);
28 
29 let port = 12345;
30 
31 app.listen(port, function() {
32     console.log(`Server listening on ${port}`);
33 });

View Code

 

小说说得错错乱乱的,或然没那么好精晓,依旧去看
品种文件
自身雕刻吧,本身弄下来编写翻译运维看看

 

留神window.main那一个函数的定义,结合index.ejs能够通晓那么些函数是装有脚本加载成功后才触发,里面用的是react-loadable的写法,用于页面包车型客车懒加载,关于页面分别打包的写法要结合路由设置来教学,那里有个大体影象即可。必要小心的是app这一个文件下暴暴光的多少个章程是在浏览器端和劳动器端通用的,接下去首要正是那有的的思绪。

五、其他

一旦项目选择了其余服务器语言的,比如PHP Yii框架 Smarty
,把服务端渲染整起来或者没那么简单

那些是 smarty的模版语法和ejs的不太搞得来

其2是Yii框架的路由和Express的长得不太相同

 

在Nginx中配置Node的反向代理,配置一个 upstream ,然后在server中相配location ,举办代理配置

upstream connect_node {
    server localhost:54321;
    keepalive 64;
}

...

server
{
    listen 80;
        ...

    location / {
        index index.php index.html index.htm;
    }

        location ~ (home|message)\/\d+$ {
            proxy_pass http://connect_node;
        }

    ...

更加多配备

 

想得头大,干脆就不想了,有用过Node进行转载代理达成SSLacrosse的情人,欢迎评论区分享哈~

 

路由处理

接下去看之下src/app目录下的文书,index.js揭破了七个点子,这当中涉及的多少个主意在服务端和浏览器端开发都会用到,那壹部分至关首要讲其下的router文件里面包车型地铁代码思路和createApp.js文件对路由的拍卖,那里是贯彻两端路由相互打通的关键点。

router文件夹下的routes.js是路由配置文件,将各种页面下的路由配置都推荐来,合成三个配备数组,可以由此那一个布局来灵活决定页面上下线。同目录下的index.js是RouterV四的正规化写法,通过遍历配置数组的方法传入路由配置,ConnectRouter是用以合并Router的贰个零件,注意到history要作为参数传入,须要在createApp.js文件里做单独的处理。先差不多看一下Route组件中的多少个布局项,值得注意的是里面包车型地铁thunk属性,这是落到实处后端获取数据后渲染的严重性一步,正是那个性格达成了接近Next里面包车型地铁零件提前获取数据的生命周期钩子,别的的性质都得以在连带React-router文档中找到表达,那里不在赘述。

import routesConfig from './routes';
const Routers=({history})=>(
 <ConnectedRouter history={history}>
 <div>
  {
  routesConfig.map(route=>(
   <Route key={route.path} exact={route.exact} path={route.path} component={route.component} thunk={route.thunk} />
  ))
  }
 </div>
 </ConnectedRouter>
)
export default Routers;

查看app目录下的createApp.js里面包车型客车代码能够窥见,本框架是本着分化的劳作环境做了不一样的拍卖,只有在生养条件下才使用Loadable.Capture方法达成了懒加载,动态引进不一样页面对应的包装之后的js文件。到那边还要看一下零部件里面的路由配置文件的写法,以home页面下的index.js为例。注意/*
webpackChunkName: ‘Home’
*/那串字符,实质是点名了包装后此页面对应的js文件名,所以本着区别的页面,那么些注释也须求修改,制止卷入到一块儿。loading这一个布局项只会在付出环境生效,当页面加载未形成前体现,这几个实际上项目花费假如不必要能够去除此组件。

import {homeThunk} from '../../store/actions/thunk';

const LoadableHome = Loadable({
 loader: () =>import(/* webpackChunkName: 'Home' */'./containers/homeContainer.js'),
 loading: Loading,
});

const HomeRouter = {
 path: '/',
 exact: true,
 component: LoadableHome,
 thunk: homeThunk // 服务端渲染会开启并执行这个action,用于获取页面渲染所需数据
}
export default HomeRouter

此间多说一句,有时大家要改造的品种的页面文件里有从window.location里面获取参数的代码,改造成服务端渲染时要全体去掉,或许是要在render之后的生命周期中采纳。并且页面级别组件都早就注入了有关路由新闻,能够经过this.props.location来博取UEvoqueL里面的参数。本项目用的是BrowserRouter,假设用HashRouter则带有参数可能略有差异,依据实情取用。

据他们说React1陆的服务端渲染的API介绍:

  1. 浏览器端使用的流入ConnectedRouter中的history为:import createHistory
    from ‘history/createBrowserHistory’
  2. 服务器端使用的history为import createHistory from
    ‘history/createMemoryHistory’

服务端渲染

此间就不会涉及到koa二的部分基础知识,假如对koa2框架不熟知能够参考作者的其它一篇博文。那里是看server文件夹下都以服务端的代码。首先是不难的app.js用于保障每趟接二连三都回到的是一个新的劳务器端实例,那对于单线程的js语言是很关键的思绪。必要注重介绍的正是clientRouter.js那一个文件,结合/src/app/configureStore.js这几个文件共同通晓服务端渲染的多寡获得流程和React的渲染机制。

/*configureStore.js*/
import {createStore, applyMiddleware,compose} from "redux";
import thunkMiddleware from "redux-thunk";
import createHistory from 'history/createMemoryHistory';
import { routerReducer, routerMiddleware } from 'react-router-redux'
import rootReducer from '../store/reducers/index.js';

const routerReducers=routerMiddleware(createHistory());//路由
const composeEnhancers = process.env.NODE_ENV=='development'?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;

const middleware=[thunkMiddleware,routerReducers]; //把路由注入到reducer,可以从reducer中直接获取路由信息

let configureStore=(initialState)=>createStore(rootReducer,initialState,composeEnhancers(applyMiddleware(...middleware)));

export default configureStore;

window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__本条变量是浏览器里面包车型大巴Redux的开发者工具,开发React-redux应用时提出设置,不然会有报错提醒。那其中山高校部分都以redux-thunk的言传身教代码,关于这一部分只要看不懂提议看一下redux-thunk的合法文书档案,那里要小心的是configureStore那么些法子要传播的initialState参数,这一个渲染的切实思路是:在服务端判断路由的thunk方法,假诺存在则需求履行这几个获取数据逻辑,那是个闭塞进度,能够看做同步,获取后放置全局State中,在前者输出的HTML中注入window.__INITIAL_STATE__以此全局变量,当html载入完毕后,那一个变量赋值已有多少的全局State作为initState提须求react应用,然后浏览器端的js加载达成后会通过复用页面樱笋时部分dom和起首的initState作为发轫,合并到render后的生命周期中,从而在componentDidMount中早已可以从this.props中拿走渲染所需数据。

但还要思念到页面切换也有希望在前者执行跳转,此时用作React的选取不会接触对后端的伸手,由此在componentDidMount这么些生命周期里并未获取数据,为了消除这一个标题,笔者建议在那个生命周期中都调用props中盛传的action触发函数,但在action内部实行一层逻辑判断,制止重新的伸手,实际项目中呼吁数据往往会有个标识性ID,就能够将以此ID存入store中,然后就足以拓展一遍比较校验来提前再次来到,幸免重新发送ajax请求,具体可看store/actions/home.js`中的逻辑处理。

import {ADD,GET_HOME_INFO} from '../constants'
export const add=(count)=>({type: ADD, count,})

export const getHomeInfo=(sendId=1)=>async(dispatch,getState)=>{
 let {name,age,id}=getState().HomeReducer.homeInfo;
 if (id === sendId) {
 return //是通过对请求id和已有数据的标识性id进行对比校验,避免重复获取数据。
 }
 console.log('footer'.includes('foo'))
 await new Promise(resolve=>{
 let homeInfo={name:'wd2010',age:'25',id:sendId}
 console.log('-----------请求getHomeInfo')
 setTimeout(()=>resolve(homeInfo),1000)
 }).then(homeInfo=>{
 dispatch({type:GET_HOME_INFO,data:{homeInfo}})
 })
}

小心这里的async/await写法,那里涉及到服务端koa二选择这么些来做多少请求,由此需求联合再次回到async函数,这块不熟的同桌建议看下ES7的学识,首若是async怎么样同盟Promise完毕异步流程改造,并且只要涉及koa2的服务端工作,对async函数用的越多,那也是本项目要求Node版本为八.x之上的案由,从八始发就足以一向用那多少个第3字。

不过到实际品种中,往往会提到到一些服务端参数的流入难点,但那块遵照差别门类须求差距十分大,并且不属于那一个React服务端改造的1有的,无法统壹享受,借使真是集团项目要用到对那块有要求咨询能够打赏后加我微信切磋。

以Home页面为例的渲染流程

为了便利我们知晓,笔者以三个页面为例整理了须臾间数据流的总体进程,看一下思路:

  1. 服务端接收到请求,通过/home找到呼应的路由配置
  2. 判断路由存在thunk方法,此时施行store/actions/thunk.js里面包车型地铁暴表露的函数
  3. 异步获取的数据会注入到全局state中,此时的dispatch分发其实并不奏效
  4. 要出口的HTML代码中会将取获得数量后的大局state放到window.__INITIAL_STATE__本条全局变量中,作为initState
  5. window.__INITIAL_STATE__将在react生命周期起功效前合并入全局state,此时react发现dom已经成形,不会再一次触发render,并且数据状态获得同步

4858美高梅 15

服务端直出HTML

基本的流程已经介绍截至,至于有个别Reducer的函数式写法,还有actions的地方都是参考网上的有的分析来协会的,具体见仁见智,那一个只要顺应自个儿的接头,并且有助于集体开发就好。假使您符合自个儿在篇章壹早先设定的读者背景,相信本文的讲述丰富您点亮本身的服务端渲染技术点啊。若是对React精晓偏少也没涉及,能够参见这里来补充部分React的基础知识

以上正是本文的全部内容,希望对我们的学习抱有帮助,也期望我们多多帮助脚本之家。

您只怕感兴趣的文章:

  • 详解React+Koa完结服务端渲染(SS安德拉)

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 美高梅手机版4858 版权所有