react+express实现服务端客户端同构渲染

最近开始研究服务端渲染,之前对这块不是很熟悉,现在做的项目本身是用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...

相关推荐
我要洋人死44 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風6 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#