Drcus | 王亚振

Drcus | 王亚振

随便写,记录点东西

使用koa搭建的react SSR

发布于:  

基于学习的角度, 实现了一下react 的ssr 原生方案,所谓原生即不使用第三方框架如 next.js

本文主要使用的技术栈有

  • babel-runtime 使用 ES6 很重要的依赖
  • webpack4
  • React 16 用到了 hydrate 这个接口
  • Koa2
  • redux
  • redux-sagaredux-thunk 两个都尝试了
  • react-router v4

本篇文章目的在于记录。借鉴学习了其他人的方法。 本文中的代码是使用的 redux-saga 方案。

整体框架

主要是 serverclient .

server

服务端的核心代码在此:

  1. 服务端把需要的数据准备好,通过 renderToString 返回给客户端 并把服务端的初始数据一起发送回客户端。
  2. 在用 saga 时,主要点就是服务器获取数据时要阻塞 response.
  3. 对于组件必要在服务端需要准备就绪的数据我定义在了组件的serverFatch 这个名字可以随意定义。然后再服务端渲染的时候就知道要准备什么数据了。
app.use(async ctx => {
  const store = createStore();
  const sagaTask = store.runSaga(rootSaga);
  store.dispatch(initialize());

  store.close = () => store.dispatch(END);

  const dataRequirements = routes
    .filter(route => matchPath(ctx.url, route)) // 匹配route
    .map(route => route.component)
    .filter(component => component.serverFatch) // 检查是否有 serverFetch
    .map(comp => store.dispatch(comp.serverFatch())); // dispatch data requirement
  
  // 触发第一次渲染, 可是返回值我们并不关心, 只要改变store即可
  renderToString(<Provider store={store} >
    <StaticRouter context={{}} location={ctx.url}>
      <App />
    </StaticRouter>
  </Provider>)
  // 关停saga, 第二次渲染的时候,忽略各种请求就好啦
  store.close();

  // 第二次渲染
  // await Promise.all(dataRequirements)
  await sagaTask.done;
  console.log('saga task done 完成✅')
  const jsx = (
    <Provider store={store}>
      <StaticRouter context={{}} location={ctx.url}>
        <App />
      </StaticRouter>
    </Provider>
  );
  const reduxState = store.getState();
  const bodystr = renderToString( jsx );  
  
  ctx.body = htmlTemplate(bodystr, reduxState);
});

client

客户端的主要点就在于把服务器发送回来的数据作为自己的 initState 装载起来。 主要代码:

const store = createStore(window.__PRELOADED_DATA || {});
store.runSaga(rootSaga);

const app = document.getElementById('root');
ReactDOM.hydrate(
  <Provider store={store}>
    <Router>
      <App/>
    </Router>
  </Provider>, app);

### 主要组件

App 组件

import React from 'react';
import { Link, Switch, Route } from "react-router-dom";
import routes from '../routes';

class App extends React.Component {
  state = {
    num: 0,
    name: 'wyz',
  }
  say = () => {
    this.setState({
      name: '我是传奇 振'
    })
  }
  render() {
    return (
      <div>
        <h1>react ssr {this.state.name}</h1>
        <div>
            <Link to="/">Home</Link>
            <Link to="/about">About</Link>
            <Link to="/contact">Contact</Link>
        </div>
        <p>ok, amazing!!</p>
        <button onClick={this.say}>名字</button>
        <Switch>
          {
            routes.map(route => <Route key={route.path} { ...route } />)
          }
        </Switch>
      </div>
    )
  }
}

export default App;

还有一个 routes

import Home from './client/components/Home';
import About from './client/components/About';
import Contact from './client/components/Contact';


export default [
  {
    path: '/',
    component: Home,
    exact: true,
  },
  {
    path: '/about',
    component: About,
    exact: true,
  },
  {
    path: '/contact',
    component: Contact,
    exact: true,
  },
]

当然还有 webpack 的配置处理

出于学习的角度 配置比较简单, 但足够使用了 贴出全部代码:

const dev = process.env.NODE_ENV !== "production";
const path = require( "path" );
const WebpackBar = require('webpackbar');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const { BundleAnalyzerPlugin } = require( "webpack-bundle-analyzer" );
const FriendlyErrorsWebpackPlugin = require( "friendly-errors-webpack-plugin" );

const plugins = [
  new WebpackBar(),
  new CleanWebpackPlugin(['dist']),
  new FriendlyErrorsWebpackPlugin()
];

if ( !dev ) {
    plugins.push( new BundleAnalyzerPlugin( {
        analyzerMode: "static",
        reportFilename: "webpack-report.html",
        openAnalyzer: false,
    } ) );
}

module.exports = {
  mode: dev ? "development" : "production",
  entry: {
    app: './src/client.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist/client')
  },
  devtool: dev ? "none" : "source-map",
  plugins: plugins,
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|svg)$/,
        use: ['file-loader']
      },
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        loader: "babel-loader",
      }
    ]
  }
}

总结

这个便于理解 React SSR 的工作原理。 对于实际项目开发可以使用比较成熟的框架 比如 next.js 官网在这里 已经封装好了对于webpack的配置。能满足一些日常的需要。 也有build 的解决方案。

参考文章:

厚颜一下 ~^_^~

赏赐