简介

服务端渲染学习笔记。

SSR 解决了什么问题 ?

1. seo优化

2. 首屏渲染时间

如何实现 SSR ?

此处以一个项目例子目录结构进行介绍。

package.json 配置

  • npm run build 执行 webpack 构建
  • npm run server 运行 dist 静态服务目录
{
  "scripts": {
    "build": "rm -rf dist && webpack",
    "server": "nodemon --watch dist --watch src --ext html,scss,css,js,ts,jsx src/server.js"
  }
}

webpack 配置

  • 配置多入口页面
  • HtmlWebpackPlugin 构建前端渲染页面
  • 开启 dist 目录构建 watch
module.exports = {
  mode: 'production',
  entry: {
    // == 前端渲染 root 节点
    main: ['@babel/polyfill', './app/main.jsx'],
    // == 服务端渲染 root 节点
    ssr: ['@babel/polyfill', './app/ssr.jsx'],
    // == 服务端渲染 header 头部
    head: ['@babel/polyfill', './app/header.jsx'],
  },
  output: {
    path: path.resolve('dist'),
    filename: '[name].[hash].js',
    publicPath: '/',
    libraryExport: 'default',
    libraryTarget: 'umd',
    globalObject: 'this'
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'app/index.html',
      chunks: ['main'],
      minify: {
        collapseWhitespace: true,
        minifyCSS: true
      }
    })
  ],
  watch: true
}

app.js

  • 将 build 目录挂载为静态服务目录
const Koa = require('koa');
const serve = require("koa-static");
const router = require('./router');

const app = new Koa();

app.use(router.routes());
app.use(serve(process.cwd() + '/dist'));

app.listen(80, () => {
  console.log('server started on port 80');
});

路由层

const Router = require('koa-router');
const controller = require('../controller');

const router = new Router();

router.get('home', '/', controller.home);

module.exports = router;

控制层

  • 获取 webpack 构建拿到的渲染页面 root 节点
  • 获取 webpack 构建拿到的渲染页面 header 头部
  • 渲染完成之后将数据挂载到 window 之上, 供前端渲染使用
const api = require('../api');

module.exports = {
  home: async ctx => {
    // == 获取 页面数据 和 tdk 数据
    const [pageData, headData] = await Promise.all([api.home(ctx), api.headers(ctx)]);

    const distDir = process.cwd() + '/dist';
    // == 找到 dist 目录构建完成的 ssr.js 文件
    const ssrModuleRegExp = /^ssr(\.[^.]*)?\.js$/;
    const ssrModuleName = fs.readdirSync(distDir).find(filename => ssrModuleRegExp.test(filename));
    const ssr = require(distDir + '/' + ssrModuleName);

    // == 找到 dist 目录构建完成的 head.js 文件
    const headModuleRegExp = /^header(\.[^.]*)?\.js$/;
    const headModuleName = fs.readdirSync(distDir).find(filename => headModuleRegExp.test(filename));
    const header = require(distDir + '/' + headModuleName);

    // == 1. 获取 webpack 构建拿到的渲染页面 root 节点
    const pageString = ssr({url: ctx.url, context: pageData});
    // == 2. 获取 webpack 构建拿到的渲染页面 header 头部
    const headerString = head(headData);

    // == chunkHash 值
    const chunkHash = ssrModuleName.match(ssrModuleRegExp)[1] || '';

    // == 3. 渲染完成之后将数据挂载到 window 之上, 供前端渲染使用
    ctx.body = `<!DOCTYPE html>
      <html lang="en">
        <head>
          ${headerString}
        <body>
          ${pageString}
          <script type="text/javascript">
            window.__initStores=${JSON.stringify(pageData)}
            window.ssr = true;
          </script>
          <script type="text/javascript" src="/main${chunkHash}.js"></script>
        </body>
      </html>`;
  }
}

ssr.jsx

  • 服务端渲染 DOM 是 React.renderToString
  • 服务端渲染路由是 StaticRouter
import React from 'react';
import ReactDOM from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { Provider, useStaticRendering } from 'mobx-react';
import App from './src/App';

export default function ssr({ url, context }) {
  useStaticRendering(true)
  const pageString = ReactDOM.renderToString(
    <div id="app">
      <Provider {...context} >
        <StaticRouter location={url} context={context}>
          <App />
        </StaticRouter>
      </Provider>
    </div>
  );
  return pageString;
}

header.jsx

  • 服务端渲染 DOM 是 React.renderToString
import React from 'react';
import ReactDOM from 'react-dom/server';

export default function head({title, description, keywords}) {
  return ReactDOM.renderToString(
    <React.Fragment>
      <title>{title}</title>
      <meta name="description" content={description}>
      <meta name="keywords" content={keywords}>
    </React.Fragment>
  );
}

main.jsx

  • 前端渲染 DOM 是 React.hydrate
  • 前端渲染路由是 BrowserRouter
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';

import App from './src/App';

ReactDOM.hydrate(
  <Provider {...window.__initStores}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById('app')
);

如何看待 SSR ?

是否使用 SSR 还是要看业务是否需要: seo 检索优化;首屏渲染性能要求较高。

可以看到 SSR 也增加了项目的复杂度和可维护性: 需要在前端和服务端写 2 套渲染逻辑。

参考资料

powered by Gitbook该文件修订时间: 2023-05-16 18:08:03

results matching ""

    No results matching ""