概述:
react有一个比较成熟的服务端渲染框架,next.js,它还支持预渲染。vue也有一个服务端渲染框架nuxt.js,这篇文章主要讲解不借助框架,如何从零实现服务端渲染的搭建。
至于服务端的优势不再赘述,大致是提高首屏渲染速度以提高用户体验,同时便于seo。这里谈一下劣势,一是需要消耗服务器的资源进行计算渲染react。二是因为增加了渲染服务器会增加运维的负担,诸如增加反向代理、监控react渲染服务器防止服务器挂掉导致页面无法响应。因为使用客户端渲染实际上就是让nginx、iis这类服务器直接返回 html、js文件,即便出错,它也只是在客户端出错,而不影响服务器对其他用户的服务,而如果使用了服务端渲染,一旦因为某种错误导致渲染服务器挂掉,那么它将导致所有用户都无法得到页面响应,这会增加运维负担。
服务端执行react代码实现回送字符串:
服务端渲染有个关键是,需要在服务端执行react代码。显然node本身是无法直接执行react代码到,这需要通过webpack将react代码编译为node可执行到代码
下面搭建一个最基础的服务端渲染,展示其基本原理
webpack.server.js
const path=require('path')
const nodeExternals=require('webpack-node-externals')
module.exports={
target:'node',
mode:'development',
entry:'./src/index.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'build')
},
externals:[nodeExternals()],
module:{
rules:[{
test:/\.js?$/,
loader:'babel-loader',
exclude:/node_modules/,
options:{
presets:['@babel/preset-react','@babel/preset-env']
}
}]
}
}
这里有个至关重要的配置项,就是externals:[nodeExternals()],告诉webpack在打包node服务端文件时,不会将node_modules里的包打包进去,也就是诸如express、react等都不会打包进bundle.js文件里。
src/server/index.js,webpck编译的入口文件
//const express=require('express')
import express from 'express'
import React from 'react'
import Home from './containers/Home'
import { renderToString } from 'react-dom/server'
const app=express()
app.get('/',(req,res)=>{
res.send(renderToString( ))
})
const server=app.listen(3000,()=>{
const host=server.address().address
const port=server.address().port
console.log('aaa',host,port)
})
入口文件中引入里React,这是因为使用在renderToString(
src/containers/home.js
import React from 'react'
const Home=()=>{
return (
hello world
)
}
export default Home
这个home.js就是上面index.js引入的home.js的组件。
上面做到了通过renderToString()将react组件转为字符串回送给浏览器,但是每次修改后,需要手动执行命令重新编译和重新启动。
package.json
"scripts": {
"start": "nodemon --watch build --exec node \"./build/bundle.js\"",
"build": "webpack --config webpack.server.js --watch"
},
通过webapck命令加--watch,可以实现我们修改了代码之后,让webpack自动重新编译生成新的bundle.js文件。然后通过nodemon监听build目录,一旦监听到文件发生变动,就执行--exec后面到命令,即重新执行node "./build/bundle.js"文件重新启动服务,这里由于外部使用了双引号,因此内部到要使用双引号需要使用反斜杠进行转义。
通过上面的配置,可以实现文件修改后自动重新编译,自动重新启动node服务,但网页还是需要手动刷新才会呈现出最新的内容。同时上面的命令还有一个问题,那就是需要执行两个命令导致需要启动两个命令行窗口。下面通过一个第三方包实现一个窗口启动上述两条命令。
package.json
"scripts": {
"dev":"npm-run-all --parallel dev:**",
"dev:start": "nodemon --watch build --exec node \"./build/bundle.js\"",
"dev:build": "webpack --config webpack.server.js --watch"
},
需要【npm i -g npm-run-all】,npm-run-all --parallel dev:** 中的--parallel表示并行执行,dev:** 表示执行以dev:命名空间名称开头的命令。现在既实现了一条命令一个窗口。
同构:
上面仅仅只是实现了react在服务端上渲染,服务端将react转为字符串回送给客户端显示。但是如果react代码中如果绑定了事件,这就需要服务端执行了react回送字符串后,客户端还要再执行一次react以在客户端上实现事件绑定。这就需要同构。
同构,一套react代码在服务端执行一次,在客户端执行一次。服务端执行一次时renderToString()只会渲染字符串内容,对于react代码中的事件是无法渲染的,此时需要客户端环境执行一次这套react代码,将事件渲染到浏览器上。
此时需要更改server/index.js文件
src/server/index.js
import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import Home from '../containers/Home'
const app=express()
app.use(express.static('public'))
app.get('*',(req,res)=>{
const content=renderToString((
))
res.send(`
ssr
${content}
`)
})
const server=app.listen(3000,()=>{
const host=server.address().address
const port=server.address().port
console.log('aaa',host,port)
})
上面代码有个关键就是,回送到html中多了一行