React同构总结
最近花了点时间研究React同构实践,赶上过年回家,心也散了,两个月没写文章。 同构也算是前端的一个应用模式,目的是为了加速首屏显示时间和SEO优化,很多公司都将同构作为前端优化的一个优化点来做,同时Raect16版本中也添加了很多对同构的支持,可以看出FB也是默认支持这一场景使用的。 同构原理什么是同构一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用。简而言之,就是服务端直出和客户端渲染的组合,能够充分结合两者的优势,并有效避免两者的不足。 概括地说,同构就是服务端(Node)替客户端请求接口,获取到数据后,将有数据和结构的页面渲染好之后返回给客户端,这样避免了客户端页面首次渲染,同时服务端RPC比客户端请求要快。 为什么要同构
同构与SPA流程对比
SSR:服务端Node也可以运行React解析出页面内容,并且要比客户端更快;客户端通常要在render一次之后请求数据,数据返回之后再render一次,服务端渲染可以解决客户端重复渲染问题。 同构与SPA时间对比
进入页面,componentDidMount中请求数据,同时页面loading,请求返回后,取消loading,页面可交互。 SPA:
SSR:
通过上述流程图可发现,理论上同构要比客户端渲染要快,而且体验要好。 预期问题原理了解之后,动手之前思考一些可能出现的问题: 1. Node服务器如何识别es6以及React Node识别ES6可以使用 React中有一个 2. 服务端如何引入js,css,图片,字体等静态资源 实现方法有多种,我这里使用 3. 服务端如何路由匹配 通常我们只做首页,或者关键页面的服务端渲染,相当于从首页进去是服务端渲染,但是从项目其他页面进入就跟正常的SPA一样。所以在服务端将要ssr的路由匹配出来,其他的路由仍交给SPA。 4. SSR的Redux怎么办 通常来讲,我们从接口获取数据,都要将一些数据放到store中,便于其他页面共享。 SSR中,服务端跟SPA公用一部分action和reducer,相同的reducer生成的store是一样的,之后再通过createStore时候将store注入进入,返回给客户端。 5. SSR的开发流程怎样 实际上SSR开发通常是在一个项目基础上改,而不是重新搭建一个项目,比较很多人拿它当做优化,而不是重构。 通常来说我们一个项目按照SPA模式开发,针对特定页面做SSR的修改,修改之后的项目既可以SPA也可以SSR,只不过SPA模式时对应页面获取不到数据,因为获取数据的方法均被修改。 6. SSR之后,项目的JS体积是否会减小 不会减小,所谓同构,其实就是服务端借助客户端的JS去渲染页面,没有影响到客户端的JS,还是正常打包,客户端做代码分割也不会受影响。 同构实现带着上面的问题来看同构如何实现。 React实现同构方法有多重,而且都较为成熟,这里选用的 Next.js先插一嘴,现在有个叫Next框架,索性也试了下,真的很简便,快速搭建SSR项目,但是问题也很明显:
所以没有选用该框架进行尝试,不过该框架凭借着简单易上手,未来还是很有市场的。 webpack-isomorphic-tools上面第二个问题就提到了服务端如何处理静态资源,这里使用 首先一个 const webpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); const config = require('../config/'); module.exports = { webpack_assets_file_path: `${config.base_path}/webpack-assets.json`,webpack_stats_file_path: `${config.base_path}/webpack-stats.json`,assets: { images: { extensions: ['png','jpg','gif','ico','svg'] },fonts: { extensions: ['woff','woff2','eot','ttf','swf','otf'] },// styles: { // extensions: ['scss','css'],// filter: function(module,regex,options,log) { // if (options.development) { // // in development mode there's webpack "style-loader",// // so the module.name is not equal to module.name // return webpackIsomorphicToolsPlugin.style_loader_filter(module,log); // } else { // // in production mode there's no webpack "style-loader",// // so the module.name will be equal to the asset path // return regex.test(module.name); // } // },// // How to correctly transform kinda weird `module.name` // // of the `module` created by Webpack "css-loader" // // into the correct asset path: // path: webpackIsomorphicToolsPlugin.style_loader_path_extractor,// // // How to extract these Webpack `module`s' javascript `source` code. // // Basically takes `module.source` and modifies its `module.exports` a little. // parser: webpackIsomorphicToolsPlugin.css_loader_parser // } } } 该文件配置可以参考官方文档。 然后在webpack中,配置对应的资源的时候,引入该文件 // 同构处理静态资源的插件 const webpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); const webpackIsomorphicToolsPluginIns = new webpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools-configuration')).development(); ... module: { rules: [ ... { test: webpackIsomorphicToolsPluginIns.regular_expression('images'),loader: 'url-loader?limit=8192',// 这样在小于8K的图片将直接以base64的形式内联在代码中,可以减少一次http请求。 options: { name: 'assets/images/[name]_[hash:8].[ext]' } },{ test: webpackIsomorphicToolsPluginIns.regular_expression('fonts'),loader: 'url-loader',options: { name: 'assets/fonts/[name].[ext]' } } ] } plugins: [ webpackIsomorphicToolsPluginIns,.... ] 然后运行webpack,将文件打包之后,会生成一个 { "javascript": { "app": "/assets/js/app_360a53bf78ee0e398bb2.js","vendor": "/assets/js/vendor_360a53bf78ee0e398bb2.js" },"styles": { "app": "/assets/css/app_360a53bf78ee0e398bb2.css" },"assets": { "./public/images/react.svg": "data...." },"webpack": { "version": "2.7.0" } } 这样我们通过该json文件就可以获取到对应静态资源。 Express服务这里选用Express框架作为服务器,原因就是简单,很多人也选用koa,都一样。 这里服务端启动部分跟正常的Express启动类似: import render from "./render"; import fetch from "./fetch"; app.use('*',(req,res,next) => { const { promises,store } = fetch(req); Promise.all(promises).then(data => { const html = render(req,store); res.send(html); }).catch(err =>{ console.log('err'); console.log(err); res.end('server error,please visit later') }) }); 核心在于路由部分:这里匹配所有路由(也可以是首页路由),使用 公用Action,Reducer我们在SPA开发中,请求一般都封装成actionCreator,方便调用与修改,SSR中就共用了actionCreator和reducer。
import 'isomorphic-fetch'; import { createStore } from "redux"; import {actions} from '../src/actions/'; import reducer from "../src/reducers"; const fetchHomeList = (store) => { return fetch('http://localhost:9000/api/aaa') .then((response)=>{ console.log('then response------'); return response.json(); }) .then((res)=>{ console.log(res.data.length); store.dispatch(actions.updateHomeList(res.data)); return res; }) .catch((res)=>{ console.log('catch res------'); console.log(res); }); }; export default function (req) { const store = createStore(reducer); const promises = [ fetchHomeList(store) ]; return { promises,store } } 在 Render页面SSR返回的是首次渲染过后的html,首次渲染就是在 import fs from 'fs' import path from 'path' import React from 'react'; // import ReactDOM from 'react-dom'; import { StaticRouter as Router } from "react-router-dom" import { renderToString } from "react-dom/server" import { Provider } from "react-redux" import Routes from '../src/route'; function getAssets() { return getAssets.assets || (() => { getAssets.assets = JSON.parse(fs.readFileSync(path.join(__dirname,'../webpack-assets.json'))); return getAssets.assets })() } export default function render(req,store) { const context = {}; const html = renderToString( <Provider store={store}> <Router location={req.baseUrl} context={context}> <Routes /> </Router> </Provider> ); // <Route>中访问/,重定向到/home路由时 if (context.url) { res.redirect('/home'); return; } const main = getAssets(); const app = main.javascript.app; const vendor = main.javascript.vendor; const style = main.styles.app; return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"> <link href=${style} rel="stylesheet"></link> <title>SSR</title> </head> <body> <div id="root"> ${html} </div> </body> <script> window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())} </script> <script src=${vendor}></script> <script src=${app}></script> </html> ` } 这里显而易见,我们准备一段html模板,跟SPA那个html模板类似,将 效果我们制作一个接口,使用setTimeout 500ms模拟网络开销,效果如下: 可以看到SSR要比SPA明显的更快速得到首屏效果。 思考项目完成的同时,也在思考一些问题: 1. 既然SSR首屏速度快,为何不所有路由全都SSR 所有页面SSR可以做,这样每个页面的首屏都会很快,同时js也会小很多。但是带来的问题服务器压力会很大,维护起来成本较高。而且服务端毕竟是模拟客户端环境渲染,一些地方还是不一样的比如没有Document,没有window对象,无法进行DOM操作等。所以推荐首页等重要页面进行SSR。 2. 如果接口时间过长,是不是白屏时间较长 确实有这个问题,理论上讲,RPC要比客户端请求快很多,这样可以节省很多时间;但是如果接口很慢会造成白屏时间过长,得不偿失。所以接口很慢的页面不建议做SSR,同时接口也应该有严格的规范控制接口返回时间。 3. 如果项目首页有很重的逻辑,或者Layout中有重逻辑该如何 页面如果有很重的逻辑比如判断很多不同条件,做出很多相应处理;依次请求很多接口,或者一起请求大量数据等情况,这些逻辑处理都需要一同写进SSR中。 4. Node服务器带来的维护及并发压力等问题 使用Node服务器的话,还涉及到服务器的日常维护问题,日志收集,错误报警等问题,以及性能问题。要求前端(SA)有一定的Node服务器的维护经验,这时前端已经不是纯前端了。 5. 什么项目适合SSR 这个问题才是关键的问题。并不是所有项目都适合SSR,就好像不是所有项目都适合Redux一样。根据SSR特点适合场景:
欢迎大家提出些不同意见用以讨论。 参考
项目源码:https://github.com/Aus0049/re... (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |