基于学习的角度, 实现了一下react 的ssr 原生方案,所谓原生即不使用第三方框架如 next.js
本文主要使用的技术栈有
babel-runtime
使用 ES6 很重要的依赖webpack4
React 16
用到了hydrate
这个接口Koa2
redux
redux-saga
或redux-thunk
两个都尝试了react-router
v4
本篇文章目的在于记录。借鉴学习了其他人的方法。
本文中的代码是使用的 redux-saga
方案。
整体框架
主要是 server
和 client
.
server
服务端的核心代码在此:
- 服务端把需要的数据准备好,通过
renderToString
返回给客户端 并把服务端的初始数据一起发送回客户端。 - 在用
saga
时,主要点就是服务器获取数据时要阻塞response
. - 对于组件必要在服务端需要准备就绪的数据我定义在了组件的
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
的解决方案。
参考文章: