• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

git-lt/isomorphism-koa2-react-antd: 对ReactJS同构直出方案的探索,结合了Koa2+reac ...

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称:

git-lt/isomorphism-koa2-react-antd

开源软件地址:

https://github.com/git-lt/isomorphism-koa2-react-antd

开源编程语言:

JavaScript 85.8%

开源软件介绍:

Koa2 + React + Redux + antd 同构直出探索

由来

在出现同构之前,我们使用后端的模板渲染引擎,C#的Razor,java的Velocity, nodejs的ejs,jade等,来渲染页面,输出到浏览器,浏览器异步请求数据,再使用各种渲染引擎来渲染数据至模板, 那么如果有样一个使用场景,加载一个列表数据:

  • 两端使用不同模板引擎渲染 如果需要服务端首屏加载时服务端渲染,异步加载时浏览器渲染,那么就需要写两套模板代码,同时维护两套模板处理逻辑

  • 纯前端渲染

  1. 不可避免出现白屏,等待异步加载,体验变差
  2. SEO优化问题,没有服务端渲染,蜘蛛抓取不到数据,无SEO可言

所以服务端渲染是不可或缺的一个环节,如何优化,只要我们前后端使用同一份业务逻辑,共一个技术框架,同一套模板,同一套路由处理逻辑,就能达到我们想要的效果。

ReactJS的生命周期

在了解之前,先来重温一下ReactJS的生命周期

ReactJS的生命周期可以分为三个阶段来看:实例化、存在期、销毁期

实例化

首次实例化

  • getDefaultProps
  • getInitialState
  • componentWillMount
  • render
  • componentDidMount 实例化之后更新,这一过程和上面一样,但没有getDefaultProps这个过程 简单记忆:props => state => mount => render => mounted

存在期

组件已经存在,状态发生改变时

  • componetWillReceiveProps
  • shouldComponentUpdate
  • ComponentWillUpdate
  • render
  • componentDidUpdate

简单记忆:receiveProps => shouldUpdate => update => render => updated

销毁期

componentWillUnmount

生命周期中10个API的作用说明

  1. getDefaultProps 作用于组件类,只调用一次,返回对象用于设置默认的props,对于引用值,会在实例中共享

  2. getInitialState 作用于组件实例,在实例创建时调用一次,用于初始化每个实例的state,此时可以访问this.props

  3. componentWillMount 在完成首次渲染之前调用,此时可以修改组件的state

  4. render 必选方法,创建虚拟DOM,该方法具有特殊规则:

  • 只能通过this.props 和this.state访问数据
  • 可以返回null、false或任何React组件
  • 只能出现一个顶级组件,数组不可以
  • 不能改变组件的状态
  • 不能修改DOM
  1. componentDidMount 真实的DOM被渲染出来后调用,可以在此方法中通过 this.getDOMNode()访问真实的DOM元素。此时可以使用其它类库操作DOM。服务端不会被调用

  2. componetWillReceiveProps 组件在接收到新的props时调用,并将其作为参数nextProps使用,此时可以更改组件的props及state

  3. shouldComponentUpdate 组件是否应当渲染新的props或state,返回false表示跳过后续的生命周期方法,通常不需要使用以避免出现bug。在出现应用性能瓶颈时,是一个可以优化的点。

  4. componetWillUpdate 接收新props或state后,进行渲染之前调用,此时不允许更新props或state

  5. componetDidUpdate 完成渲染新的props或state之后调用 ,此时可以访问DOM元素

  6. componetWillUnmount 组件被移除之前调用,可以用于做一些清理工作,在componentDidMount方法中添加的所有任务都需要在该方法中撤销,比如创建的定时器或添加的事件监听器。

var React = require("react");
var ReactDOM = require("react-dom");

var NewView = React.createClass({
    //1.创建阶段
    getDefaultProps:function() {
        console.log("getDefaultProps");
        return {};
    },

    //2.实例化阶段
    getInitialState:function() {
        console.log("getInitialState");
        return {
            num:1
        };
    },

    //render之前调用,业务逻辑都应该放在这里,如对state的操作等
    componentWillMount:function() {
        console.log("componentWillMount");
    },

    //渲染并返回一个虚拟DOM
    render:function() {
        console.log("render");
        return(
            <div>
            hello <strong> {this.props.name} </strong>
            </div>
            );
    },

    //该方法发生在render方法之后。在该方法中,ReactJS会使用render生成返回的虚拟DOM对象来创建真实的DOM结构
    componentDidMount:function() {
        console.log("componentDidMount");
    },

    //3.更新阶段
    componentWillReceiveProps:function() {
        console.log("componentWillReceiveProps");
    },

    //是否需要更新
    shouldComponentUpdate:function() {
        console.log("shouldComponentUpdate");
        return true;
    },

    //将要更新 不可以在该方法中更新state和props
    componentWillUpdate:function() {
        console.log("componentWillUpdate");
    },

    //更新完毕
    componentDidUpdate:function() {
        console.log("componentDidUpdate");
    },

    //4.销毁阶段
    componentWillUnmount:function() {
        console.log("componentWillUnmount");
    },

    // 处理点击事件
    handleAddNumber:function() {
        this.setProps({name:"newName"});
    }
});
ReactDOM.render(<NewView name="ReactJS"></NewView>, document.body);

** 因为服务端渲染,不存在挂载组件,所以挂载以后的生命周期将不会在服务端渲染时触发, 所以在做服务端组件状态或数据初始化时,要做特殊处理,后面会讲到 **

Redux的基本概念

Redux 提供了一套类似 Flux 的单向数据流,整个应用只维护一个 Store,以及面向函数式的特性让它对服务器端渲染支持很友好。

关于 Store:

  • 整个应用只有一个唯一的 Store
  • Store 对应的状态树(State),由调用一个 reducer 函数(root reducer)生成
  • 状态树上的每个字段都可以进一步由不同的 reducer 函数生成
  • Store 包含了几个方法比如 dispatch, getState 来处理数据流
  • Store 的状态树只能由 dispatch(action) 来触发更改

Redux 的数据流:

  • action 是一个包含 { type, payload } 的对象
  • reducer 函数通过 store.dispatch(action) 触发
  • reducer 函数接受 (state, action) 两个参数,返回一个新的 state
  • reducer 函数判断 action.type 然后处理对应的 action.payload 数据来更新状态树

同构的应用场景

关键API

ReactJS官网提供了两个API用于服务端渲染,使其服务端渲染成为可能:

React.renderToString 是把 React 元素转成一个 HTML 字符串,因为服务端渲染已经标识了 reactid,所以在浏览器端再次渲染,React 只是做事件绑定,而不会将所有的 DOM 树重新渲染,这样能带来高性能的页面首次加载!同构黑魔法主要从这个 API 而来。

React.renderToStaticMarkup,这个 API 相当于一个简化版的 renderToString,如果你的应用基本上是静态文本,建议用这个方法,少了一大批的 reactid,DOM 树自然精简了,在 IO 流传输上节省一部分流量。

配合 renderToStringrenderToStaticMarkup 使用,createElement 返回的 ReactElement 作为参数传递给前面两个方法。

关键要点

  • 数据状态如何共享
  • 路由状态何同步
  • 组件如何共用

组件共用

原理

对于整个应用来说,一个 Store 就对应一个 UI 快照,服务器端渲染就简化成了在服务器端初始化 Store,将 Store 传入应用的根组件,针对根组件调用 renderToString 就将整个应用输出成包含了初始化数据的 HTML,服务端在输出的时候将 state 注入到页面的全局属性中,客户端 render 时拿到 state,同步初始化状态,检验服务端生成的HTML结构,接管页面的渲染工作。

路由同步

场景

  • 在用户第一次访问页面时,由服务端路由处理,输出相关页面内容
  • 客户端用户点击链接跳转,由客户端路由处理,渲染相关组件并展示
  • 用户在前端跳转后刷新页面,此时被服务端路由截获,并由服务端处理渲染并返回页面内容

共用路由 /app/router.js 放在client端,server端也共用这一个文件

export default (
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <IndexRoute component={Home}/>
      <Route path="news" component={News} />
      <Route path="about" component={About} />
    </Route>
  </Router>
)

Web页面请求

Server端页面请求

使用 react-routermatch 方法,拿到的页面请求地址匹配到定义的 routes,解析成和客户端一致的 props 对象 传递给组件。

React-router官网文档有前后端共用路由的相关介绍ServerRendering - React-router

export default async (ctx, next) => {
  try{
    //Server端路由与前端路由共用 **页面路由** ../../../app/routes
    const { redirectLocation, renderProps } = await _match({ routes: require('../../../app/routes'), location: ctx.url })
    //重定向
    if(redirectLocation){
      ctx.redirect(redirectLocation.pathname + redirectLocation.search)
    }else if(renderProps){
      //调用页面渲染控制器,开始服务端渲染
      await renderCtrl(ctx, next, renderProps)
    }else{
      await next()
    }
  }catch(e){
    console.error('Server-Render Error Occurs: %s', e.stack)
    await ctx.render('500', {
      msg: ctx.app.env === 'development' ? e.message : false
    })
  }
}

renderCtrl 负责服务端处理数据并渲染页面输出到浏览器端,这里调用了react 服务端渲染的核心方法 renderToString()

server/controller/renderCtrl.js

//renderProps:从路由组件中获取的路由与组件的信息
export default async (ctx, next, renderProps) => {
  const route = renderProps.routes[renderProps.routes.length - 1]
  let prefetchTasks = []

  // 遍历路由中注册的组件,创建加载数据请求,至数组中
  for (let component of renderProps.components) {
    if (component && component.WrappedComponent && component.WrappedComponent.fetch) {
      const _tasks = component.WrappedComponent.fetch(store.getState(), store.dispatch)
      if (Array.isArray(_tasks)) {
        prefetchTasks = prefetchTasks.concat(_tasks)
      } else if (_tasks.then) {
        prefetchTasks.push(_tasks)
      }
    }
  }

  //当所有组件的数据加载完成后,
  await Promise.all(prefetchTasks)
  // 渲染组件
  await ctx.render('index', {
    title: config.title,
    dev: ctx.app.env === 'development',
    //将state输出到页面,用于浏览器端redux初始化state
    reduxData: store.getState(),
    // render之后生成的HTML字符串在app这个对象中,通过ejs渲染至view中,最后输出
    app: renderToString(<Provider store={store}>
      <RouterContext {...renderProps} />
    </Provider>)
  })
}

Client端页面请求

浏览器端,从服务端注入到全局对象中获取redux需要的应用状态state,初始化state 导入路由共用的配置模块,初始化路由 调用 ReactDOM.render() 方法来渲染页面

import React from 'react'
import ReactDOM from 'react-dom'
import routes from '../../app/routes'
import { Provider } from 'react-redux'

// 和服务端共用的redux状态管理
import configureStore from '../../app/store/configureStore'

// 页面加载时,从全局对象中获取服务端注入到页面的State数据
const store = configureStore(window.__REDUX_STATE__)

// 浏览器端使用 ReactDOM.render 初始化页面,首屏渲染
ReactDOM.render(
  <Provider store={store}>
    {routes}
  </Provider>,
  document.querySelector('.react-container')
)

数据请求共用

Server端api请求

Server端由于渲染组件时,不会执行到 componentWillMount 方法(因为不存在挂载操作),那么就要手动去处理数据状态的初始化工作,方法是给组件添加一个静态方法 fetch(),服务端在 renderCtrl 中渲染之前,先加载完数据,初始化state,再将state注入到页面,渲染至浏览器端

渲染部分的代码上面已经展示了,下面来看看给组件添加的静态方法 fetch()

import { fetchNews } from '../actions/news'

@connect( state => state.news )

class News extends Component{
//这里声明一个数据,为的是可以获取多个接口的数据,接口请求是异步请求,返回之后,render之前调用 Promise.all() 保证所有异步请求完成后,再渲染页面
  static fetch (state, dispatch) {
   const fetchTasks = []
   fetchTasks.push(
     dispatch(fetchNews(state))
   )
   return fetchTasks
 }
  render(){}
}

Client端api请求

当页面从服务端返回后,那么浏览器端就接管了页面的控制,比如点击下一页这个功能,数据请求就是ajax 异步请求服务端,服务端返回 json 数据,那么这里,只需要调用组件的静态方法 fetch(),获取数据即可。

class News extends Component{
 static fetch (state, dispatch) {
   const fetchTasks = []
   fetchTasks.push(
     dispatch(fetchNews(state))
   )
   return fetchTasks
 }
 getNextPage(){
	//调用组件的静态方法异步获取数据
   this.constructor.fetch(this.props, this.props.dispatch);
 }
 render(){
	<div style={{ background: '#ECECEC', padding: '30px' }}>
        <Button type="primary" loading={!loaded} onClick={this.getNextPage.bind(this)}>
         下一页
       </Button>
        <div>
        { newsList }
        </div>
     </div>
	}
}

api请求最终都是调用redux 中的 actions 去做异步请求处理,那么在action中,可以使用 isomorphic-fetch 去做请求去差异化,服务端使用http.request方法需要完整路径,客户端使用ajax,使用相对路径,具体如下

const fetchStateUrl = __SERVER__
  ? `http://localhost:${require('../../platforms/common/config').port}/api/news`
  : '/api/news'

export function fetchNews(state){
  return (dispatch) => {
    dispatch(newsRequest())
    return fetch(fetchStateUrl)
      .then(res => res.json())
      .then(data => {
        console.log('===>news')
        console.log(data)
        dispatch(newsSucceed(data))
      })
      .catch(e => dispatch(newsFailed(e)))
  }
}

这里的 __SERVER__ 是webpack中配置的全局变量

new webpack.DefinePlugin({
  __SERVER__: true
})

应用状态同步

状态同步主要使用redux去同步,服务端渲染时,生成一个 state,在返回页面时,将这个state注入页面,浏览器端拿到state,接管页面状态的管理。

服务器端在 render 中,先获取数据,初始化state,注入页面

ctx.render('index', {
   title: config.title,
   dev: ctx
                      

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap