最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React服务端渲染二三事

    正文概述 掘金(倒影)   2021-02-04   669

    React服务端渲染二三事

    为何使用

    传统SSR

    SSR 优缺点

    服务端直出HTML会让首屏较快展现,且利于SEO。但所有页面的加载都需向服务端请求,如果访问量较大,会对服务器造成压力。此外,页面之间的跳转,页面局部内容的变动都会引起页面刷新,体验不够友好。

    CSR

    CSR(SPA) 优缺点

    只有首次进入或刷新时需要请求服务器,页面之间的跳转由JS脚本完成,响应较快。但由于服务端只返回一个空节点的HTML,页面内容的呈现需等待JS脚本加载执行完毕,首屏时间较长,对SEO也不友好。

    React SSR

    相比于客户端渲染SPA应用

    由于首次进入或刷新页面时,服务端直接将有内容的页面返回给客户端,大大降低了白屏时间,同样也便于做SEO

    相比于传统的SSR应用

    不必跳转到不同页面都需要刷新一次浏览器,只在第一次访问的时候服务端直出HTML,后续的页面跳转走CSR(客户端渲染)

    如何使用

    组件同构

    服务端

    对于一个React应用,想在首次进入或刷新页面服务端就直接返回完整的HTML,需要在服务端将当前页面需渲染的组件转换成HTML,React为此提供了相应方法。

    import { renderToString } from 'react-dom/server'
    
    const html = renderToString(
      <App />
    )
    

    renderToString方法外,Reeact也提供了ReactDom.renderToNodeStream方法,返回一个可输出HTML 字符串的可读流。

    客户端

    虽然服务端已经直出了需要渲染的HTML,但一些事件绑定的操作还是需要客户端JS脚本来完成。如果客户端依旧执行ReactDOM.render方法,会在首次调用时将容器节点下的所有DOM元素替换,这显然不是我们想要的结果,好在React提供了ReactDom.hydrate方法。

    import ReactDom from 'react-dom'
    
    ReactDom.hydrate(
      <App />,
      document.getElementById('root')
    )
    

    在客户端渲染时,hydrate方法会比较双端渲染结果是否一致,如一致则保留服务端渲染的结果,如不一致则使用客户端渲染的结果。

    路由同构

    SPA应用的路由完全由客户端控制,跳转到不同路径时JS脚本会替换组件使之呈现出不同内容。

    而对React SSR应用来说,只要用户首次进入或刷新页面时服务端能渲染正确的组件即可,后续的路由切换则完全由客户端JS脚本控制。react-router提供StaticRouter组件可根据当前请求路径匹配渲染不同组件。

    import { StaticRouter } from 'react-router'
    
    // path为请求路径
    <StaticRouter location={path}>
      <App />
    </StaticRouter>
    

    数据同构

    有时需要在服务端请求数据,直接渲染出带数据的HTML页面。而由于客户端无此数据,渲染内容就会和服务端不一致。那如何将服务端请求的数据“注入”客户端呢?

    数据注水

    服务端可以控制直出的HTML内容,既然如此就可以在直出的HTML内容中插入一段脚本。next.js便是如此实现的。

    React服务端渲染二三事

    数据脱水

    服务端已将数据写入script标签中,客户端渲染时便可直接用该数据进行渲染。

    SEO TDK支持

    如果只是要做SEO支持,可以全部放在服务端,根据不同页面路径直出不同的TDK数据。

    但为保证体验和提高可维护性,最好是能将TDK写在页面组件里。这需要服务端渲染时能获取到当前页面的TDK数据,直出到HTML,客户端渲染时能够比较TDK数据,做DOM操作用新页面的TDK数据替换掉老页面的。

    react-helmet对此提供了较好的支持

    服务端示例(Koa)

    import React from 'react'
    import { renderToString } from 'react-dom/server'
    import { Helmet } from 'react-helmet'
    
    
    export default async (ctx, next) => {
      const html = renderToString(
        <App />
      )
    
      const helmet = Helmet.renderStatic()
    
      ctx.body = `<!DOCTYPE html>
        <html lang="en">
          <head>
            <meta charset="UTF-8">
              ${helmet.title.toString()}
              ${helmet.meta.toString()}
          </head>
          <body>
            <div id="root">${html}</div>
          </body>
        </html>
      `
     
      return next();
    }
    
    

    客户端示例

    import { Helmet } from 'react-helmet'
    import tempData from './data'
    
    const Index = () => {
      return (
        <>
          <Helmet>
            <title>index title</title>
            <meta name="description" content="index description"></meta>
            <meta name="keyword" content="index keyword"></meta>
          </Helmet>
          <div>Index</div>
        </>
      )
    }
    
    export default Index
    
    

    CSS同构

    目前next.jsegg-react-ssr都是将css代码最终打包到一个文件内作为资源进行加载。

    按这种方式,服务端直出的HTML结果应包含css文件的link标签,而客户端JS脚本无需插入link标签。

    客户端

    客户端使用mini-css-extract-plugin插件提取css文件,如果使用了按需加载还需将所有css提取为单个文件

    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    
    module.exports = {
      optimization: {
        splitChunks: {
          cacheGroups: {
            styles: {
              name: 'styles',
              type: 'css/mini-extract',
              // For webpack@4
              // test: /\.css$/,
              chunks: 'all',
              enforce: true,
            },
          },
        },
      },
      plugins: [
        new MiniCssExtractPlugin({
          filename: '[name].css',
        }),
      ],
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader'],
          },
        ],
      },
    };
    
    

    在打包时一般静态资源文件名会带上hash值,这时为保证服务端能获取到正确的路径还需使用webpack-manifest-plugin插件生成文件名和路径的映射文件,方便服务端获取正确的路径。

    如果使用react-loadable做按需加载,则可使用其提供的react-loadable-ssr-addon插件生成映射文件。

    服务端

    如果没有启用css模块化,客户端打包时已将用到的css打包到了一个文件,服务端就无需处理css文件。

    webpack中可使用ignore-loader忽略css文件的处理

    module.exports = {
      // other configurations
      module: {
        loaders: [
          { test: /\.css$/, loader: 'ignore-loader' }
        ]
      }
    };
    

    如果启用了css模块化,css-loader会生成标识符映射,服务端也需要生成标识符以保证双端渲染结果一致。css-loadermodule.exportOnlyLocals选项提供了仅导出标识符映射,而不签入css的功能

    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/i,
            loader: "css-loader",
            options: {
              modules: {
                exportOnlyLocals: true,
              },
            },
          },
        ],
      },
    };
    
    

    性能优化

    按需加载(代码分割)

    对一个较大项目来说,我们访问其中的一个页面时,只需加载当前页面的代码,其他页面的代码只要在访问的时候再加载即可。这可以有效减少访问一个页面时需加载的js文件体积,提高页面响应速度。

    在一个SPA应用中实现按需加载,只需使用dynamic import语法,webpack支持该特性。使用动态import语法导入的模块会被单独打包到一个文件。

    React SSR实现按需加载的一些坑

    1. 服务端渲染组件时,无法将按需加载的组件渲染成HTML
    2. 即使服务端能够直出按需加载组件的HTML,客户端接管后由于异步JS代码尚未加载,会先展示中间状态(一般中间状态会先渲染loading),这样双端的初次渲染结果就不一致了
    3. 既然采取了服务端渲染,在服务端渲染时直出的HTML最后能包含当前页面需异步加载的JS代码,即script标签,无需客户端动态创建

    针对第一点,其实服务端无需按需加载,应直接渲染出按需加载的组件;

    第二点,为保证双端初次渲染结果一致,客户端应该等待当前页面按需加载的异步JS代码下载后再进行渲染;

    第三点,为使服务端渲染的HTML能够包含按需加载的JS代码的script,需要获取到当前页面按需加载的组件名,和组件名对应JS路径名的映射。在直出HTML时将当前页面按需加载的script标签拼接进去。

    react-loadable提供了上述问题的解决方案

    react-loadable实现

    组件

    按需加载的组件使用Loadable包裹,其中moduleswebpack选项标识组件加载的是哪个模块,这样服务端就能根据渲染的组件获取到需加载的模块。

    如果使用babel插件react-loadable/babel,便无需使用moduleswebpack选项。

    import Loadable from 'react-loadable';
    
    Loadable({
      loader: () => import('./Bar'),
      modules: ['./Bar'],
      webpack: () => [require.resolveWeak('./Bar')],
    });
    

    客户端

    使用preloadReady方法,等待按需加载的script脚本加载完毕后再渲染。window.main方法将在服务端直出的script脚本加载后调用。

    import Loadable from 'react-loadable'
    
    window.main = () => {
      Loadable.preloadReady().then(() => {
        ReactDom.render(
          <App />
          document.getElementById('root')
        )
      })
    }
    

    生成模块映射(webpack)

    生成加载的模块与webpack打包后的bundles的映射,服务端可据此判断应直出的scirpt

    const ReactLoadableSSRAddon = require('react-loadable-ssr-addon');
    
    module.exports = {
      entry: {
        // ...
      },
      output: {
        // ...
      },
      module: {
        // ...
      },
      plugins: [
        new ReactLoadableSSRAddon({
          filename: 'react-loadable.json',
        }),
      ],
    };
    

    服务端(Koa)

    将渲染的组件用Loadable.Capture包裹,它提供一个回调report方法,可以记录当前页面按需加载的模块名,根据生成的模块映射可获取到webpack打包后的bundles,由此直出当前页面需按需加载的scirpt,无需客户端动态创建。

    mport React from 'react';
    import { renderToString } from 'react-dom/server';
    import Loadable from 'react-loadable'
    import { getBundles } from 'react-loadable-ssr-addon';
    import manifest from '@dist/server/react-loadable.json';
    
    export default async (ctx, next) => {
    
      const modules = new Set();
    
      const html = renderToString(
        <Loadable.Capture report={moduleName => {
          modules.add(moduleName)
        }}>
          <App />
        </Loadable.Capture>
      );
    
      const modulesToBeLoaded = [...manifest.entrypoints, ...Array.from(modules)];
      const bundles = getBundles(manifest, modulesToBeLoaded);
    
      ctx.body = `<!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            ${bundle.css.join('\n')}
        </head>
        <body>
            <div id="root">${html}</div>
        </body>
        </html>
        ${assets.js.join('\n')}
        <script type="text/javascript">
            window.main()
        </script>
    `;
    
      return next();
    }
    
    

    参考文档

    • next.js官方网站
    • webpack官方网站
    • React SSR 服务端渲染原理解析与同构实践
    • 从头开始,彻底理解服务端渲染原理(8千字汇总长文)

    起源地下载网 » React服务端渲染二三事

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元