koa-react-ssr 学习
编辑koa 创建一个简单的服务器
yarn add koa koa-router
src/server/app.js
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('*', async ctx => {
ctx.body = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>koa ssr</title>
</head>
<body>
<h1>简单的 koa http 服务</h1>
<div id="root"></div>
</body>
</html>
`
})
app.use(router.routes())
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
})
上面我们用 koa 简单创建了一个 http
服务,然后返回字符串, node app.js
打开 http://127.0.0.1:3000
可以看到效果了
搞事情,写一个 Home.jsx 组件
src/client/Home.jsx
import React from 'react'
const Home = () => <h2>Hello world</h2>
export default Home
而服务端的 src/server/app.js
文件需要改写为
import Koa from 'koa'
import Router from 'koa-router'
import ReactDOM from 'react-dom'
import Home from '../client/Home'
const app = new Koa()
const router = new Router()
// 原本的在浏览器端的运行方法
// ReactDOM.render(<Home />, document.getElementById('root'))
// 在服务器端运行的代码
const content = ReactDOM.renderToString(<Home />)
router.get('*', async ctx => {
ctx.body = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>koa ssr</title>
</head>
<body>
<div id="root">${content}</div>
</body>
</html>
`
})
app.use(router.routes())
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
})
上面用到了 import
/ export
语法,node
不支持,则我们需要使用 babel
对它进行转义。
yarn init -y
yarn add webpack webpack-cli -D
yarn add @babel/core @babel/preset-env @babel/preset-react @babel/runtime @babel/plugin-transform-runtime -D
yarn add webpack-node-externals -D # 我们不希望捆绑 koa koa-router 等模块...
touch webpack.server.js
webpack.server.js
const path = require('path')
const nodeExternals = require('webpack-node-externals')
module.exports = {
mode: 'production',
target: 'node',
externals: [nodeExternals()], // in order to ignore all modules in node_modules folder
entry: './src/server/app.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'build')
},
resolve: {
extensions: ['.js', '.json', '.jsx'] // import xxx from 'app.jsx' => import xxx from 'app'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
// @babel/preset-env 转义 es6+ 的箭头函数、类、async await 等为 ES5 语法
// @babel/preset-react : 转义 react
// @babel/plugin-transform-runtime : 自动 polyfill es5不支持的特性
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['@babel/plugin-transform-runtime']
}
}
}
]
}
}
在 package.json
中添加
"scripts": {
"start": "node ./build/bundle.js",
"build:server": "webpack --config webpack.server.js"
}
yarn build:server && yarn start
打开 http://127.0.0.1:3000/
就可以看到效果了...
目前为止 我们 webpack 编译了服务端的代码,其中还将 react
转成我们需要的形式。已经初步完成了我们的工作...
实现同构(添加事件绑定)
服务端渲染和客户端渲染的对比:
CSR
步骤:
- 浏览器下载
HTML
文档 - 浏览器下载
JS
文件 - 浏览器运行
React
代码 - 页面渲染
SSR
的出现,可以解决这些传统 CSR
的弊端, 且可以优化 SEO
方便 balabala....
重点是同构如何实现:
- 服务端运行
React
代码生成HTML
- 发送
HTML
文件给浏览器 - 浏览器收到内容显示
- 浏览器加载
JS
文件 JS
代码执行并接管页面的操作
在前面的实现过程中,我们只是运行 react
代码生成 string
显示在页面中,也仅此而已,那么我们为 Home.jsx
添加一个事件绑定试试:
import React, { Component } from 'react'
class Home extends Component {
render() {
return (
<div>
<h2>hello world</h2>
<button onClick={() => alert('click')}>click</button>
</div>
)
}
}
export default Home
yarn build:server && yarn start
发现无论怎么点也没有反应,原因很简单,react-dom/server
下的 renderToString
并没有做事件相关的处理,因此返回给浏览器的内容不会有事件绑定。
那怎么解决这个问题呢?
这就需要进行同构了。所谓同构,通俗的讲,就是一套 React
代码在服务器上运行一遍,到达浏览器又运行一遍。服务端渲染完成页面结构,浏览器端渲染完成事件绑定。
那如何进行浏览器端的事件绑定呢?
唯一的方式就是让浏览器去拉取 JS
文件执行,让 JS
代码来控制。于是服务端返回的代码变成了这样:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>koa ssr</title>
</head>
<body>
<div id="root">
<div data-reactroot="">
<h2>hello world</h2>
<button>click</button>
</div>
</div>
<script src="/index.js"></script>
</body>
</html>
有没有发现和之前的区别?区别就是多了一个 script
标签。而它拉取的 JS 代码就是来完成同构的。
那么这个 index.js
我们如何生产出来呢?
在这里,要用到 react-dom
。具体做法其实就很简单了:
src/client/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Home from './Home'
// ReactDOM.render(<Home />, document.getElementById('root')) 这是浏览器端运行的方式
ReactDOM.hydrate(<Home />, document.getElementById('root')) // 服务端渲染用 hydrate
webpack.client.js
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/client/index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'public')
},
resolve: {
extensions: ['.js', '.json', '.jsx'] // import xxx from 'app.jsx' => import xxx from 'app'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
// @babel/preset-env 转义 es6+ 的箭头函数、类、async await 等为 ES5 语法
// @babel/preset-react : 转义 react
// @babel/plugin-transform-runtime : 自动 polyfill es5不支持的特性
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['@babel/plugin-transform-runtime']
}
}
}
]
}
}
src/server/app.js
import Koa from 'koa'
import Router from 'koa-router'
import serve from 'koa-static'
import path from 'path'
// react...
import React from 'react'
import ReactDOM from 'react-dom/server'
import Home from '../client/Home'
const app = new Koa()
const router = new Router()
const content = ReactDOM.renderToString(<Home />)
// 设置静态服务器为根路径的 public
app.use(serve(path.join(process.cwd(), '/public')))
router.get('*', async ctx => {
ctx.body = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>koa ssr</title>
</head>
<body>
<div id="root">${content}</div>
<script src='/index.js'></script>
</body>
</html>
`
})
app.use(router.routes())
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
})
package.json
"scripts": {
"test": "yarn build:server && yarn build:client && yarn start",
"start": "node ./build/bundle.js",
"build:server": "webpack --config webpack.server.js",
"build:client": "webpack --config webpack.client.js"
}
yarn add koa-static
yarn test
打开页面点击后就可以发现事件已经绑定成功了!
添加路由
写一个路由的配置文件 src/client/Routes.js
import React, { Component } from 'react'
import { Route } from 'react-router-dom'
import Home from './Home'
import About from './About'
export default (
<div>
<Route path="/" exact component={Home} />
<Route path="/about" exact component={About} />
</div>
)
src/client/About.jsx
import React, { Component } from 'react'
class About extends Component {
render() {
return (
<div>
<h2>About page</h2>
</div>
)
}
}
export default About
修改打包 react
的入口文件 src/client/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import Routes from './Routes'
const App = () => <BrowserRouter>{Routes}</BrowserRouter>
ReactDOM.hydrate(<App />, document.getElementById('root')) // 服务端渲染用 hydrate
同时也要修改 src/server/app.js
每次请求根据路径不同生成不同的 content
import Koa from 'koa'
import Router from 'koa-router'
import serve from 'koa-static'
import path from 'path'
// react...
import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import Routes from '../client/Routes'
const app = new Koa()
const router = new Router()
// 设置静态服务器为根路径的 public
app.use(serve(path.join(process.cwd(), '/public')))
router.get('*', async ctx => {
//构建服务端的路由
const content = renderToString(<StaticRouter location={ctx.url}>{Routes}</StaticRouter>)
ctx.body = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>koa ssr</title>
</head>
<body>
<div id="root">${content}</div>
<script src='/index.js'></script>
</body>
</html>
`
})
app.use(router.routes())
app.listen(3000, () => {
console.log('http://127.0.0.1:3000')
})
现在路由的跳转就没有任何问题啦。 注意,这里仅仅是一级路由的跳转,多级路由的渲染在之后的系列中会用 react-router-config
中 renderRoutes
来处理。
引入 redux
yarn add redux react-redux redux-thunk axios
新建 src/redux.js
文件 (为了方便 将配置都写在一个文件)
import { combineReducers, compose, createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import axios from 'axios'
// constants
const ADD_COUNT = 'ADD_COUNT'
const GET_TITLE = 'GET_TITLE'
// actions
export const addCount = () => ({
type: ADD_COUNT
})
export const getTitle = () => {
return dispatch =>
axios.get('https://randomuser.me/api/').then(res => {
dispatch({
type: GET_TITLE,
payload: {
title: res.data.results[0].name.title
}
})
})
}
// default state
let defaultState = {
count: 1
}
// reducers
export const demoReducer = (state = defaultState, action) => {
switch (action.type) {
case ADD_COUNT:
return { ...state, count: ++state.count }
case GET_TITLE:
return { ...state, title: action.payload.title }
default:
return state
}
}
// combineReducers
const reducers = combineReducers({
demo: demoReducer
})
// generator store
const configureStore = (initialState = {}) => {
const storeEnhancers = applyMiddleware(thunk)
const store = createStore(reducers, initialState, storeEnhancers)
return store
}
// export
export default configureStore()
在客户端中引入 src/client/index.js
import { Provider } from 'react-redux'
import store from '../redux'
const App = () => (
<Provider store={store}>
<BrowserRouter>{Routes}</BrowserRouter>
</Provider>
)
在 src/client/Home.jsx
中使用
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { addCount, getTitle } from '../redux'
class Home extends Component {
componentDidMount() {
this.props.getTitle()
}
render() {
return (
<div>
<h2>
Count: {this.props.count} title: {this.props.title}
</h2>
<button onClick={this.props.addCount}>add count</button>
<button onClick={this.props.getTitle}>async getTitle</button>
</div>
)
}
}
const mapStateToProps = state => ({
count: state.demo.count,
title: state.demo.title
})
export default connect(
mapStateToProps,
{ addCount, getTitle }
)(Home)
在服务端中引入 src/server/app.js
import { Provider } from 'react-redux'
import store from '../redux'
//...
const content = renderToString(
<Provider store={store}>
<StaticRouter location={ctx.url}>{Routes}</StaticRouter>
</Provider>
)
yarn test
成功编译 说明你成功了
异步数据的服务端渲染方案(数据注水与脱水)
经过上面的改造 我们可以在异步获取到了数据。。当然,这是存在坑点的。。
componentDidMount() {
this.props.getTitle()
}
我们一般在组件的 componentDidMount
生命周期函数进行异步数据的获取。但是,在服务端渲染中却出现了问题。 启动服务后我们查看网页的源代码 可以发现
源代码里面并没有这些列表数据啊!那这是为什么呢?
让我们来分析一下客户端和服务端的运行流程,当浏览器发送请求时,服务器接受到请求,这时候服务器和客户端的 store
都是空的,紧接着客户端执行 componentDidMount
生命周期中的函数,获取到数据并渲染到页面,然而服务器端始终不会执行 componentDidMount,因此不会拿到数据,这也导致服务器端的 store
始终是空的。
换而言之,关于异步数据的操作始终只是客户端渲染。现在的工作就是让服务端将获得数据的操作执行一遍,以达到真正的服务端渲染的效果。
- 0
- 0
-
分享