最近开始研究服务端渲染,之前对这块不是很熟悉,现在做的项目本身是用react+express实现的,刚好熟悉项目架构的同时也把这块搞清楚。
1.客户端项目构建,
npx创建react项目,就按照之前创建正常的项目一样先把客户端的项目创建完成。实现一个简单的路由和页面,分别实现Home组件和About组件, 这一块对前端的同学很熟悉,就不着重讲了,这时候我们npm run start
就可以看到项目跑在3000端口,这时候是一个纯粹的客户端项目。
yaml
const routes = [
{
path: '/',
extract: true,
component: <Home />
},
{
path: '/about',
extract: true,
component: <About />
},
{
path: '/home',
extract: true,
component: <Home />
},
];
这时候我们如果禁用js, 则会显示pulic文件夹下的index.html模版,写着你需要打开js才能正常运行项目。
所以服务端渲染就是需要渲染一个html模版。
2.服务端项目构建
我们选择express框架实现服务端渲染。首先在项目下建立server目录,用于存放我们的服务端代码。为了方便代码分割,我们建立app/routes/views三个文件夹,分别对应执行/路由/模版。
服务端代码的撰写原理,就是提前将客户端的代码渲染成为HTML模版然后直接直接展示,由客户端加载的js来执行其余需要js执行的操作。在这当中,express和react都为我们提供了非常方便的API我们直接使用即可。
首先编写入口文件index.js 主要负责启动项目,我们通过node server/index.js可以启动服务端项目,前面的依赖文件都是为了能够让react代码在服务端运行。
js
// ignore `.scss` imports
require('ignore-styles');
// transpile imports on the fly
require('@babel/register')({
presets: [
['@babel/preset-env'],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [],
});
const Loadable = require('react-loadable');
const app = require('./app/development');
const PROT = 9000;
// import express server
Loadable.preloadAll()
.then(() => {
app.listen(PROT, (err) => {
console.log('the app is now in http://localhost:9000')
if (err) {
console.error(err);
}
});
})
.catch((err) => {
console.log('server fail', err);
console.error(err);
});
app/development.js 在这个项目里我们使用了webpackDevMiddleware中间件,主要是打包我们的react生成bundle.js后注入模版,让服务端正确拿到对应的js文件,以便到客户端的时候正确执行。
js
const path = require('path');
const webpack = require('webpack');
const routes = require('../routes/development');
const webpackConfig = require('../../config/webpack.config.ssr.js');
const webpackDevMiddleware = require('webpack-dev-middleware');
// webpack init dev && hot middleware add webpack打包
const config = webpackConfig(process.env.NODE_ENV);;
const compiler = webpack(config);
const devMiddleware = webpackDevMiddleware(compiler, {
serverSideRender: true,
publicPath: config.output.publicPath,
});
const express = require('express');
// 创建应用程序
const createApplication = (inject) => {
const orinalApp = express();
// 注入并初始化一些路由或者webpack热更新中间件
inject(orinalApp);
return orinalApp;
};
const app = createApplication((orinalApp) => {
// 模版设置文件夹
orinalApp.set('view engine', 'ejs');
orinalApp.set('views', path.resolve(__dirname, '../views/'));
orinalApp.enable('view cache');
// 静态资源访问
orinalApp.use(devMiddleware);
orinalApp.use('/', routes);
});
module.exports = app;
views/index.ejs 这个文件夹里存放的就是我们的服务端模版,使用ejs语法,在渲染的时候通过变量注入获取正确的appHtml和js/css的地址。
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
<meta name="referrer" content="origin" />
<link href="<%= PUBLIC_URL + 'static/css/main.css' %>" rel="stylesheet" />
</head>
<body>
<div id="root"><%- appHtml %></div>
<script src="<%= PUBLIC_URL + 'static/js/bundle.js' %>"></script>
</body>
</html>
routes/development.js 这里是对服务端的路由做了配置,在服务端因为路由是持久化的,所以使用StaticRouter来包裹路由,提前将客户端的路由下的components renderToString再注入到服务端模版中。
js
import { Router as originalRouter } from 'express';
import { renderToString } from 'react-dom/server';
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { StaticRouter } from "react-router-dom/server";
import routes from '../../src/routes';
const webpackConfig = require('../../config/webpack.config.ssr.js');
const config = webpackConfig(process.env.NODE_ENV);;
const router = originalRouter();
router.get('*', async (req, res) => {
let appHtml = renderToString(
<StaticRouter location={req.url}>
<Routes>
{routes.map(({ path, exact, component }) => (
<Route path={path} exact={exact} key={path} element={component} />
))}
</Routes>
</StaticRouter>);
let tplName = 'index';
res.render(tplName, {
title: '腾讯新闻',
appHtml: appHtml,
PUBLIC_URL: config.output.publicPath,
});
});
module.exports = router;
以上就实现了简单的服务端渲染,这个时候我们启动服务端项目,就可以看见项目运行,这时候如果禁用js,也可以看到模版被正确渲染出来。
TIPS:
在webpack打包ssr的时候有几点需要注意, 基本配置和webpack.config.js相同,但需要修改如下几点: 1.css的解析需要使用MiniCssExtractPlugin,将css单独提取出来成css文件后通过通过link标签注入,并且需要改为css-loader,。cra模版项目在本地默认是将css和js打包在一起,使用style-loader来讲样式通过标签写入html中的,这样的话在服务端就不能正确加载css。 2.入口文件默认是index.js里面的渲染方式是使用reactDOM.render(),在服务端我们更推荐使用hydrateRoot来渲染,这样在客户端渲染的时候就不是清空所有服务端代码重绘,而是在服务端模版基础上绘制,对于性能和体验都更好,因此可以另外创建一个root.js,并且修改打包的入口文件。
项目git地址 github.com/sissi144/ss...