Egg + React SSR SEO 实现

MVVM 服务端渲染相比前端渲染,支持 SEO,更快的首屏渲染,相比传统的模板引擎,更好的组件化,前后端模板共用。在 Egg + React 的方案里面, HTML head 里面 meta 信息也作为 React 服务端渲染的一部分, 和普通的数据绑定没有什么差别。

layout.jsx 组件实现

  1. // framework/layout/layout.jsx 组件
  2. import React, { Component } from 'react';
  3. export default class Layout extends Component {
  4. render() {
  5. return <html>
  6. <head>
  7. <title>{this.props.title}</title>
  8. <meta charSet="utf-8"></meta>
  9. <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"></meta>
  10. <meta name="keywords" content={this.props.keywords}></meta>
  11. <meta name="description" content={this.props.description}></meta>
  12. <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"></link>
  13. </head>
  14. <body><div id="app">{this.props.children}</div></body>
  15. </html>;
  16. }
  17. }

服务端统一入口 Webpack loader 实现

下面是一个简单的 Webpack SSR 渲染 Entry Loader 模板实现, 结合 layout.jsx, 通过统一入口实现 React 初始化。 具体页面无需关心 HTML, header, body 以及热更新之列的配置, 只需要编写组件自己的功能实现。 服务端渲染出来的是完整的 HTML 结构,所以这里需要 layout.jsx

  1. // app/web/framework/entry/server-loader.js
  2. module.exports = function() {
  3. this.cacheable();
  4. return `
  5. import React, { Component } from 'react';
  6. import Layout from 'framework/layout/layout.jsx';
  7. import Header from 'component/header/header.jsx';
  8. import App from '${this.resourcePath.replace(/\\/g, '\\\\')}';
  9. export default class Page extends Component {
  10. render() {
  11. return <Layout {...this.props}><App {...this.props} /></Layout>;
  12. }
  13. }
  14. `;
  15. };

客户端统一入口 Webpack loader 实现

下面是一个简单的 Webpack 前端渲染 Entry Loader 模板实现, 通过统一入口实现 React 初始化。 这里无需 layout.jsx, 因为SSR渲染时已经把 HTML 渲染好了。前端只需要渲染 <div id="app"></div>的内容。

  1. // app/web/framework/entry/client-loader.js
  2. module.exports = function() {
  3. this.cacheable();
  4. return `
  5. import React from 'react';
  6. import ReactDom from 'react-dom';
  7. import { AppContainer } from 'react-hot-loader';
  8. import Entry from '${this.resourcePath.replace(/\\/g, '\\\\')}';
  9. const state = window.__INITIAL_STATE__;
  10. const render = (App)=>{
  11. ReactDom.hydrate(EASY_ENV_IS_DEV ? <AppContainer><App {...state} /></AppContainer> : <App {...state} />, document.getElementById('app'));
  12. };
  13. if (EASY_ENV_IS_DEV && module.hot) {
  14. module.hot.accept('${this.resourcePath.replace(/\\/g, '\\\\')}', () => { render(Entry) });
  15. }
  16. render(Entry);
  17. `;
  18. };