服务端渲染react代码页面
首先创建 ssr-react目录,进入ssr-react目录,初始化一个npm项目
mkdir ssr-reactcd ssr-reactnpm init -y
在根目录创建src文件夹,在src文件夹下创建server.js
采用node的一个框架 express来写。
首先安装express
yarn add express
接下来 用express写一个最简单的服务
const express = require('express');const app = express();const port = process.env.port || 3000;app.get('*', (req, res) => {res.writeHead(200,{'content-type': 'text/html;charset=utf8'})res.end('你好ssr')})app.listen(port, () => {console.log('http://localhost:3000')})
写完以后运行 node src/server.js就能在http://localhost:3000 看到 页面上的输入
因为要做服务端渲染,要在server.js中引入React等前端的包,也就是import,但是 node不认识 import
这个时候我们使用webpack来让node认识import
在根目录创建config文件夹,在config文件夹创建webpack.server.js
const path = require('path')const webpackExternals = require('webpack-node-externals')module.exports = {target: 'node',mode: process.env.NODE_ENV === 'production' ? 'production': 'development',entry: path.resolve(__dirname,'../src/server.js'),output: {path: path.resolve(__dirname,'../dist'),filename: 'bundle_server.js'},module: {rules: [{test: /\.js$/,loader: 'babel-loader',exclude: '/node_modules/'}]},externals: [webpackExternals()] // 不会把node_module中的源码打包}
这里同时使用了webpack-node-externals这个插件,这个插件功能是 在webpack打包的时候,不打包node_modules里面的源码。
为了在node中适配react和ES6的高级语法,我们需要使用babel来编译,安装babel插件
yarn add @babel/core @babel/preset-env "@babel/preset-react babel-loader
同时在根目录创建.babelrc文件
{"presets": ["@babel/preset-react","@babel/preset-env"]}
接着编写下scripts命令
"scripts": {"webpack:server": "webpack --config ./config/webpack.server.js --watch","webpack:start": "nodemon --watch dist --exec node dist/bundle_server.js","dev": "npm-run-all --parallel webpack:*"},
1.webpack:server 这个命令来打包 入口文件 server.js
2.webpack:start 这个命令来监听打包后的 bundle_server.js
3.dev 这个命令,使用npm-run-all第三方库 来监听所有的命令
接下来开始,写react组件,在node中进行渲染
首先在src目录下创建Home和Person两个组件
// src/pages/Home.jsimport React from 'react';const Home = () => {return <div>home</div>}export default Home;
// src/pages/Person.jsimport React from 'react';const Person = () => {return <div>Person</div>}export default Person;
然后开始编写路由,对应的查找这两个组件
在pages目录下创建routes.js文件
import React from 'react';import { Routes, Route, Link } from 'react-router-dom'import Home from './pages/Home';import Person from './pages/Person';const RoutesList = () => {return (<div><ul><li><Link to='/'>首页</Link></li><li><Link to='/person'>个人中心</Link></li></ul><Routes><Route exact path='/' element={<Home />} /><Route exact path='/person' element={<Person />} /></Routes></div>)}export default RoutesList;
最后在server.js中编写 react代码,能够让react代码在node中渲染
1.react-dom库中有个server库,就是react-dom/server,来专门在node中渲染react
2.在react-router-dom下也有个server库,就是react-router-dom/server,来渲染react路由
首先引入这两个库,以及路由文件
import React from 'react';import ReactDOMServer from 'react-dom/server';import { StaticRouter } from 'react-router-dom/server'import Routes from './routes'
然后通过ReactDOMServer中的renderToString来渲染react代码,而路由文件使用StaticRouter进行包裹,
代码如下:
const content = ReactDOMServer.renderToString(<StaticRouter location={req.url}><Routes /></StaticRouter>)
最后将 content 写成 html的格式,进行输出
const html = `<html><head></head><body><div id="root">${content}</div></body></html>`res.writeHead(200,{'content-type': 'text/html;charset=utf8'})res.end(html)
看下现在的效果
当切换的首页的路由时:
当切换到个人中心的路由时:
前端注水:
比如在 Home 组件中 添加一个点击事件
import React from 'react';const Home = () => {const handleClick = () => {console.log('click')}return <div>home<button onClick={handleClick}>点击</button></div>}export default Home;
当在页面点击的时候,日志没有被打印。
这是因为,Home组件是服务端渲染的,点击事件是在客户端进行的,客户端接收不到 这个点击事件,所以日志没有被打印。
下面通过让客户端 拦截 路由 实现 事件点击
首先在pages下创建client.js
在react-dom中有hydrate可以进行注水,也就是拦截。
通过hydrate进行注水,并且绑定到 id为root的div下面
代码如下:
import React from 'react';import ReactDom from 'react-dom';import { BrowserRouter } from 'react-router-dom'import Routes from './routes';ReactDom.hydrate(<BrowserRouter><Routes /></BrowserRouter>,document.getElementById('#root'))
这个时候我们需要将这个clent.js文件进行打包
在config目录下创建webpack.client.js,来进行client.js的打包
注意:这个时候需要把webpack-node-externals去掉,因为这个时候是打包的react客户端
const path = require('path')module.exports = {target: 'web',mode: process.env.NODE_ENV === 'production' ? 'production': 'development',entry: path.resolve(__dirname,'../src/client.js'),output: {path: path.resolve(__dirname,'../dist/public'),filename: 'bundle_client.js'},module: {rules: [{test: /\.js$/,loader: 'babel-loader',exclude: '/node_modules/'}]}}
然后在scripts中配置下命令
"webpack:client": "webpack --config ./config/webpack.client.js --watch"
最后在输出的html中引入打包后的client.js
const html = `<html><head></head><body><div id="root">${content}</div><script src="bundle_client.js"></script></body></html>`
这样重新 打包后,就能在页面上进行点击事件了
看下效果:
初始化 reactStore
使用 react-redux来管理状态
首先安装下redux
yarn add redux react-redux
在src目录下创建store文件夹
在store文件夹下创建index.js来管理store入口
在strore文件夹下创建 actions文件夹,actions文件夹下分别创建 home.js和 person.js来管理这两个的action
在store文件夹下创建reducers文件夹,在reducers文件夹下分别创建home.js和person.js来管理这两个的reducer
首先来写下action
// actions/home.jsexport const FETCH_HOME_DATA = 'fetch_home_data';export const fetchHomeData = async (dispatch) => {const data = await new Promise((resolve, reject) => {setTimeout(() => {resolve({articles: [{id: 1,title: 'title1',content: 'content1'},{id: 2,title: 'title2',content: 'content2'}]})},2000)})dispatch({type: FETCH_HOME_DATA,payload: data})}
export const FETCH_PERSON_DATA = 'fetch_person_data';export const fetchPersonData = async (dispatch) => {const data = await new Promise((resolve, reject) => {setTimeout(() => {resolve({userInfo: {username: 'curry',job: '前端工程师'}})},2000)})dispatch({type: FETCH_PERSON_DATA,payload: data})}
让开始写reducers
// reducers/home.jsimport { FETCH_HOME_DATA } from '../actions/home';const initState = {articles: []}export default (state = initState ,action) => {switch(action?.type){case FETCH_HOME_DATA:return action.payload;default:return state;}}
// reducers/person.jsimport { FETCH_PERSON_DATA } from '../actions/person';const initState = {info: {}}export default (state = initState ,action) => {switch(action?.type){case FETCH_PERSON_DATA:return action.payload;default:return state;}}
最后将这两个reducer合并起来
在 reducers/index.js中将两个合并
import { combineReducers } from 'redux'import homeReducer from './home'import personReducer from './person'export default combineReducers({home: homeReducer,person: personReducer})
最后在stroe中引入redux
import { createStore } from 'redux'import reducer from './reducers'const store = createStore(reducer)export default store;
开始使用store
在client.js中使用store
在使用store的时候,需要使用到react-redux提供的Provider,相当于context中的provider,
将Provider包裹住,将store传入Provider,这样的话,才能在组件中接受到store
import React from 'react';import ReactDOM from 'react-dom';import { BrowserRouter } from 'react-router-dom';import { Provider } from 'react-redux'import Routes from './routes';import store from './store'ReactDOM.hydrate(<Provider store={store}><BrowserRouter><Routes /></BrowserRouter></Provider>,document.querySelector('#root'));
同时也需要在server.js中引入Provider,并将store传入Provider
import React from 'react';import ReactDOMServer from 'react-dom/server';import { StaticRouter } from 'react-router-dom/server'import { Provider } from 'react-redux'import Routes from './routes'import store from './store'const express = require('express');const app = express();const port = process.env.port || 3000;app.use(express.static('dist/public'))app.get('*', (req, res) => {const content = ReactDOMServer.renderToString(<Provider store={store}><StaticRouter location={req.url}><Routes /></StaticRouter></Provider>)const html = `<html><head></head><body><div id="root">${content}</div><script src="bundle_client.js"></script></body></html>`res.writeHead(200,{'content-type': 'text/html;charset=utf8'})res.end(html)})app.listen(port, () => {console.log('http://localhost:3000')})
reduxThunk中间件
接下来我们在home组件中使用store
我们使用react-redux提供的hooks来使用
引入两个hooks
import { useSelector, useDispatch } from 'react-redux'
使用useDispatch这个hooks来获取dispatch
const dispatch = useDispatch();
使用useSelector这个hooks来获取reducer中的数据
const homeData = useSelector((state) => state.home)
接下来 我们使用 csr的方式 来获取数据
使用useEffect
import { fetchHomeData } from '../store/actions/home'useEffect(() => {dispatch(fetchHomeData)},[])
当我们刷新页面的时候,看到页面有报错
这个报错也提示,需要使用redux-thunk
因为 我们在action中 使用了 异步方式,所以要使用react-thunk来加载异步
首先来安装下redux-thunk
yarn add redux-thunk
redux提供了一个中间件来使用thunk,就是applyMiddleware中间件
最后在store中使用applyMiddleware来包裹这个thunk
import { createStore, applyMiddleware } from 'redux'import thunk from 'redux-thunk';import reducer from './reducers'const store = createStore(reducer, applyMiddleware(thunk))export default store;
这样 页面就不会报错了
我们在home组件中 通过 点击事件,来渲染 异步获取的数据
最后看下效果
使用ssr方式来异步加载数据
首先在routers.js中 写一个 路由配置
export const routesConfig = [{path: '/',component: Home,},{path: '/person',component: Person}]
参照一下next.js中的做法,next.js是提供了一个方法,来获取数据
我们也可以在 组件中 挂载一个方法 ,来获取数据
用Home组件来写
在home组件,因为home是一个函数,所有可以 挂载一个getInitData方法,参数是store,使用方法和csr一样,
通过store.dispatch(fetchHomeData)来获取数据
// home.jsHome.getInitData = async (store) => {return store.dispatch(fetchHomeData)}
然后在sever.js中引入
可以通过req获取当前访问的url,然后遍历路由的配置,当 当前访问的url和路由配置的一个匹配的时候,
就执行组件中的getInitData方法,同时传入store参数,这个时候返回的是promise
然后通过Promise.all方法,来执行所有的promise,渲染页面的数据
import Routes, { routesConfig } from './routes'const url =req.url;const promises = routesConfig.map(route => {const component = route.component;if(route.path === url && component.getInitData){return component.getInitData(store)}else{return null;}})Promise.all(promises).then(() => {const content = ReactDOMServer.renderToString(<Provider store={store}><StaticRouter location={req.url}><Routes /></StaticRouter></Provider>)const html = `<html><head></head><body><div id="root">${content}</div><script src="bundle_client.js"></script></body></html>`res.writeHead(200,{'content-type': 'text/html;charset=utf8'})res.end(html)})
最后看下效果
如下:是通过csr的方式渲染的数据
看下网页源代码:
这个是通过ssr的方式渲染的
因为 客户端不知道服务端已经渲染了数据,所有csr和ssr都渲染了数据。
这个时候来改造下
首先改造下store
这里给createStore传入一个默认的状态
import { createStore, applyMiddleware } from 'redux';import thunk from 'redux-thunk';import reducer from './reducers';export default function createStoreInstance(preloadedState = {}) {return createStore(reducer, preloadedState, applyMiddleware(thunk));}
然后改造server.js
1.首先引入store
2.在执行promise的时候通过store的getState方法,获取到异步获取后的stete,就是preloadedState
3.将preloadedState 注入到全局的变量PRELOAD_STATE中
import createStoreInstance from './store';const store = createStoreInstance();Promise.all(promises).then(() => {const preloadedState = store.getState();const content = ReactDOMServer.renderToString(<Provider store={store}><StaticRouter location={req.url}><Routes /></StaticRouter></Provider>)const html = `<html><head></head><body><div id="root">${content}</div><script>window.__PRELOAD_STATE__=${JSON.stringify(preloadedState)}</script><script src="bundle_client.js"></script></body></html>`res.writeHead(200,{'content-type': 'text/html;charset=utf8'})res.end(html)})
最后改造client.js
1.引入store
2.使用createStoreInstance方法,参数从全局中获取PRELOAD_STATE,这个时候ssr已经将PRELOAD_STATE的数据注入到了window中,这个时候在csr就可以直接获取数据,存放到store中,
然后将store传入provder
import React from 'react';import ReactDOM from 'react-dom';import { BrowserRouter } from 'react-router-dom';import { Provider } from 'react-redux'import Routes from './routes';// import store from './store'import createStoreInstance from './store';const store = createStoreInstance(window?.__PRELOAD_STATE__);ReactDOM.hydrate(<Provider store={store}><BrowserRouter><Routes /></BrowserRouter></Provider>,document.querySelector('#root'));
最后看下效果:
页面数据会很快,因为现在是ssr渲染的数据

