前言
所谓同构,简而言之就是,第一次拜访后盾服务时,后盾间接把前端要显示的界面全副返回,而不是像 SPA
我的项目只渲染一个 <div id="root"></div>
剩下的都是靠 JavaScript
脚本去加载。这样一来能够大大减少首屏等待时间。
同构概念并不简单,它也非我的项目必需品,然而摸索它的原理却是必须的。
浏览本文须要你具备以下技术根底: Node.js
、 React
、 React Router
、 Redux
、 webpack
。
本文将分以下两局部去讲述:
- 同构思路剖析,让你对同构有一个概念上的理解;
- 手写同构框架,深刻了解同构原理。
同构思路
CSR 客户端渲染
CSR
客户端渲染,这个就是很好了解了,应用 React
, React Router
前端本人管制路由的 SPA
我的项目,就能够了解成客户端渲染。它有一个十分大的劣势就是,只是首次拜访会申请后盾服务加载相应文件,之后的拜访都是前端本人判断 URL
展现相干组件,因而除了首次访问速度慢些之外,之后的访问速度都很快。
执行命令: create-react-app react-csr
创立一个 React SPA
单页面利用我的项目 。
执行命令: npm run start
启动我的项目。
查看网页源代码: 只有一个 <div id="root"></div>
和 一些 script
脚本。最终出现进去的界面却是这样的: 原理很简略,置信学习过 webpack
的同学都晓得,那就是 webpack
把所有代码都打包成相应脚本并插入到 HTML
界面中,浏览器会解析 script
脚本,通过动静插入 DOM
的形式展现出相应界面。
客户端渲染的优劣势
客户端渲染流程如下:
劣势:
- 前端负责渲染页面,后端负责实现接口,各自干好各自的事件,对开发效率有极大的晋升;
- 前端在跳转界面的时候不须要申请后盾,减速了界面跳转的速度,进步用户体验。
劣势:
- 因为须要期待
JS
文件加载以及后盾接口数据申请因而首屏加载工夫长,用户体验较差; - 因为大部分内容都是通过
JS
加载因而搜索引擎无奈爬取剖析网页内容导致网站无奈SEO
。
SSR 服务端渲染
SSR
是服务端渲染技术,它自身是一项比拟一般的技术, Node.js
应用 ejs
模板引擎输入一个界面这就是服务端渲染。每次拜访一个路由都是申请后盾服务,从新加载文件渲染界面。
同样咱们也来创立一个简略的 Node.js
服务:
mkdir express-ssrcd express-ssrnpm init -ytouch app.jsnpm i express --save
app.js
const express = require('express')const app = express()app.get('/',function (req,res) { res.send( `<html> <head> <title>express ssr</title> </head> <body> <h1>Hello SSR</h1> </body> </html>` )})app.listen(3000);
启动服务: node app.js
这就是最简略的服务端渲染一个界面了。服务端渲染的实质就是页面显示的内容是服务器端生产进去的。参考 前端进阶面试题具体解答
服务端渲染的优劣势
服务端渲染流程:
劣势:
- 整个
HTML
都通过服务端间接输入SEO
敌对; - 加载首页不须要加载整个利用的
JS
文件,首页加载速度快。
劣势:
- 拜访一个应用程序的每个界面都须要拜访服务器,体验比照
CSR
稍差。
咱们会发现一件很有意思的事,服务端渲染的长处就是客户端渲染的毛病,服务端渲染的毛病就是客户端渲染的长处,反之亦然。那为何不将传统的纯服务端直出的首屏劣势和客户端渲染站内跳转劣势联合,以获得最优解?这就引出了以后风行的服务端渲染( Server Side Rendering
),或者称之为“同构渲染”更为精确。
同构渲染
所谓同构,艰深的讲,就是一套 React
代码在服务器上运行一遍,达到浏览器又运行一遍。
服务端渲染实现页面构造,客户端渲染绑定事件。它是在 SPA
的根底上,利用服务端渲染直出首屏,解决了单页面利用首屏渲染慢的问题。
同构渲染流程
简略同构案例
要实现同构,简略来说就是以下两步:
- 服务端要能运行
React
代码; - 浏览器同样运行
React
代码。
1、创立我的项目
mkdir react-ssrcd react-ssrnpm init -y
2、我的项目目录构造剖析
├── src│ ├── client│ │ ├── index.js // 客户端业务入口文件│ ├── server│ │ └── index.js // 服务端业务入口文件│ ├── container // React 组件│ │ └── Home│ │ └── Home.js│ │├── config // 配置文件夹│ ├── webpack.client.js // 客户端配置文件│ ├── webpack.server.js // 服务端配置文件│ ├── webpack.common.js // 共有配置文件├── .babelrc // babel 配置文件├── package.json
首先咱们编写一个简略的 React
组件, container/Home/Home.js
import React from "react";const Home = ()=>{ return ( <div> hello world <br/> <button onClick={()=> alert("hello world")}>按钮</button> </div> )}export default Home;
装置客户端渲染的常规,咱们写一个客户端渲染的入口文件, client/index.js
import React from "react";import ReactDom from "react-dom";import Home from "../containers/Home";ReactDom.hydrate(<Home/>,document.getElementById("root"));// ReactDom.render(<Home/>,document.getElementById("root"));
以前看到的都是调用 render
办法,这里应用 hydrate
办法,它的作用是什么?
ReactDOM.hydrate
与render()
雷同,但它用于在ReactDOMServer
渲染的容器中对HTML
的内容进行hydrate
操作。React
会尝试在已有标记上绑定事件监听器。
咱们都晓得纯正的 React
代码放在浏览器上是无奈执行的,因而须要打包工具进行解决,这里咱们应用 webpack
,上面咱们来看看 webpack
客户端的配置:
webpack.common.js
module.exports = { module:{ rules:[ { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", } ] }}
.babelrc
{ "presets":[ ["@babel/preset-env"], ["@babel/preset-react"] ]}
webpack.client.js
const path = require("path");const {merge} = require("webpack-merge");const commonConfig = require("./webpack.common");const clientConfig = { mode: "development", entry:"./src/client/index.js", output:{ filename:"index.js", path:path.resolve(__dirname,"../public") },}module.exports = merge(commonConfig,clientConfig);
代码解析:通过 entry
配置的入口文件,对 React
代码进行打包,最初输入到 public
目录下的 index.js
。
在以往,间接在 HTML
引入这个打包后的 JS
文件,界面就显示进去了,咱们称之为纯客户端渲染。这里咱们就不这样应用,因为咱们还须要服务端渲染。
接下来,看看服务端渲染文件 server/index.js
import express from "express";import { renderToString } from "react-dom/server";import React from "react";import Home from "../containers/Home";const app = express(); // {1}app.use(express.static('public')) // {2}const content = renderToString(<Home />); //{3}app.get('/',function (req,res) { // {4} res.send(` <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>React SSR</title> </head> <body> <div id="root">${content}</div> <script src="/index.js"></script> </body> </html> `)})app.listen(3000);
代码解析:
- {1},创立一个
express
实例对象 - {2},开启一个动态资源服务,监听
public
目录,还记得客户端的打包文件就放到了public
目录了把,这里通过监听,咱们就能够这样localhost:3000/index.js
拜访该动态资源 - {3},把
React
组件通过renderToString
办法生成HTML
- {4},当用户拜访
localhost:3000
时便会返回res.send
中的HTML
内容,该HTML
中把React
生成的HTML
片段也插入进去一起返回给用户了,这样就实现了服务端渲染。通过<script src="/index.js"></script>
这段脚本加载了客户端打包后的React
代码,这样就实现了客户端渲染,因而一个简略同构我的项目就这样实现了。
你会发现一个奇怪的景象,为什么写 Node.js
代码应用的却是 ESModule
语法,是的没错,因为咱们要在服务端解析 React
代码,作为同构我的项目,因而对立语法也是十分必要的。所以 Node.js
也须要配置相应的 webpack
编译文件:
webpack.server.js
const path = require("path");const nodeExternals = require("webpack-node-externals");const {merge} = require("webpack-merge");const commonConfig = require("./webpack.common");const serverConfig = { target:"node", //为了不把nodejs内置模块打包进输入文件中,例如: fs net模块等; mode: "development", entry:"./src/server/index.js", output:{ filename:"bundle.js", path:path.resolve(__dirname,"../build") }, externals:[nodeExternals()], //为了不把node_modules目录下的第三方模块打包进输入文件中,因为nodejs默认会去node_modules目录上来寻找和应用第三方模块。};module.exports = merge(serverConfig,commonConfig);
到此咱们就实现了一个简略的同构我的项目,这里您应该会有几个疑难?
renderToString
有什么作用?- 为什么服务端加载了一次,客户端还须要再次加载呢?
- 服务端加载了
React
输入的代码片段,客户端又执行了一次,这样是不是会加载两次导致资源节约呢?
ReactDOMServer.renderToString(element)
将 React
元素渲染为初始 HTML
。 React
将返回一个 HTML
字符串。你能够应用此办法在服务端生成 HTML
,并在首次申请时将标记下发,以放慢页面加载速度,并容许搜索引擎爬取你的页面以达到 SEO
优化的目标。
为什么服务端加载了一次,客户端还须要再次加载呢?
起因很简略,服务端应用 renderToString
渲染页面,而 react-dom/server
下的 renderToString
并没有做事件相干的解决,因而返回给浏览器的内容不会有事件绑定,渲染进去的页面只是一个动态的 HTML
页面。只有在客户端渲染 React
组件并初始化 React
实例后,能力更新组件的 state
和 props
,初始化 React
的事件零碎,让 React
组件真正“ 动” 起来。
是否加载两次?
如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate()
办法, React
将会保留该节点且只进行事件处理绑定,从而让你有一个十分高性能的首次加载体验。因而不用放心加载屡次的问题。
是否意犹未尽?那就让咱们更加深刻的学习它,手写一个同构框架,彻底了解同构渲染的原理。
手写同构框架
实现一个同构框架,咱们还有很多问题须要解决:
- 兼容路由;
- 兼容
Redux
; - 兼容异步数据申请;
- 兼容
CSS
款式渲染。
问题很多,咱们一一击破。
兼容路由
同构我的项目中当在浏览器中输出 URL
后,浏览器是如何找到对应的界面?
- 浏览器收到
URL
地址例如:http://localhost:3000/login
; - 后盾路由找到对应的
React
组件传入到renderToString
中,而后拼接HTML
输入页面; - 浏览器加载打包后的
JS
文件,并解析执行前端路由,输入相应的前端组件,发现是服务端渲染,因而只做事件绑定解决,不进行反复渲染,此时前端路由路由开始接管界面,之后跳转界面与后盾无关。
既然须要路由咱们就先装置下: npm install react-router-dom
之前咱们只定义了一个 Home
组件,为了演示路由,咱们再定义一个 Login
组件:
...import { Link } from "react-router-dom";const Login = ()=>{ return ( <div> <h1>登录页</h1> <br/> <Link to="/">跳转到首页</Link> </div> )}
革新 Home
组件
const Home = ()=>{ return ( <div> <h1>首页</h1> <br/> <Link to="/login">跳转到登录页</Link> <br/> <button onClick={() => console.log("click me")}>点击</button> </div> )}
当初咱们有两个组件了,能够开始定义相干路由:
src/Routes.js
...import {Route} from "react-router-dom";export default ( <div> <Route path="/" exact component={Home} /> // 拜访根门路时展现Home组件 <Route path="/login" component={Login} /> // 拜访/login门路时展现Login组件 </div>)
革新客户端路由:src/client/index.js
...import { BrowserRouter } from "react-router-dom";import Routes from "../Routes";const App = ()=>{ return ( <BrowserRouter> {Routes} </BrowserRouter> )}ReactDom.hydrate(<App />,document.getElementById("root"));
与一般 SPA
我的项目没有任何区别。
革新服务端路由:src/server/index.js
...import { StaticRouter } from "react-router-dom";import Routes from "../Routes";const app = express();app.use(express.static('public'))const render = (req)=>{ const content = renderToString(( <StaticRouter location={req.path}> {Routes} </StaticRouter> )); return ` <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>React SSR</title> </head> <body> <div id="root">${content}</div> <script src="/index.js"></script> </body> </html> `}app.get('*',function (req,res) { res.send(render(req))})
服务端跟之前的区别就是这段代码:
<StaticRouter location={req.path}> {Routes}</StaticRouter>
为什么不是 BrowserRouter
而是 StaticRouter
呢?
次要是因为 BrowserRouter
应用的是 History API
记录地位,而 History API
是属于浏览器的 API
,在 SSR
的环境下,服务端不能应用浏览器 API
。
StaticRouter
动态路由,通过初始传入的 location
地址找到相应组件。区别于客户端的动静路由。
兼容 Redux
Redux
始终以来都是 React
技术栈里最难了解的局部,它的概念繁多,如果想要彻底了解本大节及当前的内容,须要您对 Redux
有肯定的理解
安装包:
npm i redux react-redux redux-thunk --save
redux
库;react-redux
是react
与redux
的桥梁;redux-thunk
是redux
中间件,redux
解决异步申请计划。
src/store/index.js
import {createStore, applyMiddleware} from "redux";import thunk from "redux-thunk";const reducer = (state={name:"Lion"},action)=>{ return state;}const getStore = ()=>{ return createStore(reducer,applyMiddleware(thunk));}export default getStore;
输入一个办法 getStore
用于创立全局 store
对象。
革新 server
端, src/server/render.js
... 省略import { Provider } from "react-redux";import getStore from "../store";export const render = (req)=>{ const content = renderToString(( <Provider store={getStore()}> <StaticRouter location={req.path}> {Routes} </StaticRouter> </Provider> )); return ` ... 省略 `}
通过 Provider
组件把 store
对象数据共享给所有子组件,它的实质还是通过 context
共享数据。
革新 client
端, src/client/index.js
...import { Provider } from "react-redux";import getStore from "../store";const App = ()=>{ return ( <Provider store={getStore()}> <BrowserRouter> {Routes} </BrowserRouter> </Provider> )}ReactDom.hydrate(<App />,document.getElementById("root"));
同 server
端革新十分相似。
redux
都增加结束后,最初咱们在组件中应用 redux
的形式获取数据,革新 Home
组件:
import React from "react";import { Link } from "react-router-dom";import { connect } from "react-redux";const Home = (props)=>{ return ( <div> <h1>首页</h1> <div>{props.name}</div> <br/> <Link to="/login">跳转到登录页</Link> <br/> <button onClick={() => console.log("click me")}>点击</button> </div> )}const mapStateToProps = (state)=>({ name:state.name})export default connect(mapStateToProps,null)(Home);
其实外围就是这几行代码:
const mapStateToProps = (state)=>({ name:state.name})export default connect(mapStateToProps,null)(Home);
connect
接管 mapStateToProps
、 mapDispatchToProps
两个办法,返回一个高阶函数,这个高阶函数接管一个组件,返回一个新组件,其实就是给传入的组件减少一些属性和性能。
这样一来咱们的 Home
组件就能够应用 name
属性了。革新结束
能够失常应用,这样咱们就轻松的集成了 redux
。
兼容异步数据申请
在构建企业级我的项目时, redux
应用就更为简单,而且实战中咱们个别都须要申请后盾数据,让咱们来革新革新我的项目,使他成为企业级我的项目。
redux 革新
个别咱们会把 redux
相干的代码都放入 store
文件夹下,咱们来看看它的新目录:
├── src│ ├── store│ │ ├── actions.js│ │ ├── constans.js│ │ └── reducer.js└───────└── index.js
actions
负责生成action
;constans
定义常量;reducer
定义reducer
;index
输入store
。
actions.js
import axios from 'axios';import {CHANGE_USER_LIST} from "./constants";const changeUserList = (list)=>{ return { type:CHANGE_USER_LIST, list }}export const getUserList = (dispatch)=>{ return ()=>{ axios.get('https://reqres.in/api/users').then((res)=>{ dispatch(changeUserList(res.data.data)); }); }}
导出 getUserList
办法,它的主要职责是向后盾发送实在数据申请。
[留神] 这里发送的申请是实在的
constants.js
export const CHANGE_USER_LIST = 'HOME/CHANGE_USER_LIST';
输入常量,定义常量能够保障您在调用时不容易出错。
reducer.js
import { CHANGE_USER_LIST } from "./constants";// {1}const defaultState = { userList:[]};export default (state = defaultState , action)=>{ switch (action.type) { // {2} case CHANGE_USER_LIST: return { ...state, userList:action.list } default: return state; }}
代码解析:
- {1},定义默认
state
,userList
为空数组; - {2},当接管到
type
为CHANGE_USER_LIST
的dispatch
时,更新用户列表,这也是咱们在actions
那里接管到后盾申请数据之后发送的dispatch
,dispatch(changeUserList(res.data.data));
redux
革新的差不多了,接下来革新 Home
组件:src/containers/Home/index.js
import React,{useEffect} from "react";import { Link } from "react-router-dom";import { connect } from "react-redux";import { getUserList } from "../../store/actions";const Home = ({getUserList,name,userList})=>{ // {2} useEffect(()=>{ getUserList(); },[]) return ( <div> <h1>首页</h1> <ul> { {/* 3 */} userList.map(user=>{ const { first_name, last_name, email, avatar, id } = user; return <li key={id}> <img src={avatar} alt="用户头像" style={{width:"30px",height:"30px"}}/> <div>姓名:{`${first_name}${last_name}`}</div> <div>email:{email}</div> </li> }) } </ul> <br/> <Link to="/login">跳转到登录页</Link> <br/> <button onClick={() => console.log("click me")}>点击</button> </div> )}const mapStateToProps = (state)=>({ name:state.name, userList:state.userList});// {1}const mapDispatchToProps = (dispatch)=>({ getUserList(){ dispatch(getUserList(dispatch)) }})export default connect(mapStateToProps,mapDispatchToProps)(Home);
代码解析:
- {1},
mapDispatchToProps
同mapStateToProps
作用统一都是connect
的入参,把相干的dispatch
与state
传入Home
组件中。 - {2},
useEffect Hook
中调用getUserList
办法,获取后盾实在数据 - {3},依据实在返回的
userList
渲染组件
咱们来看看实际效果:
看起来很不错, react-router
与 redux
都曾经反对了,然而当你查看下网页源码时会发现一个问题:
用户列表数据并不是服务端渲染的,而是通过客户端渲染的。为什么会这样呢?咱们一起剖析下申请过程你就会明确:
接下来咱们次要的指标就是服务端如何可获取到数据?既然 useEffect
不会在服务端执行,那么咱们就本人创立一个 “Hook”
。
在 Next.js
中 getInitialProps
就是这个被创立的 “Hook”
,它的主要职责就是使服务端渲染能够获取初始化数据。
getInitialProps 实现
在 Home
组件中咱们先增加这个静态方法:
Home.getInitialData = (store)=>{ return store.dispatch(getUserList());}
在 getInitialData
中做的事件同 useEffect
雷同,都是去发送后盾申请获取数据。
在 React Router 文档中对于服务端渲染想要先获取到数据须要把路由改为动态路由配置。
src/Routes.js
import { Home, Login } from "./containers";export default [ { key:"home", path: "/", exact: true, component: Home, }, { key:"login", path: "/login", exact: true, component: Login, }];
当初剩下最次要的工作就是服务端渲染网页之前拿到后盾数据了。
react-router-config 这个包是 React Router
提供给咱们用于剖析动态路由配置的包。咱们先装置它 npm install react-router-config --save
src/server/render.js
... 省略import {matchRoutes, renderRoutes} from "react-router-config";import Routes from "../Routes";export const render = (req,res)=>{ const store = getStore(); // {1} const promises = matchRoutes(Routes, req.path).map(({ route }) => { const component = route.component; return component.getInitialData ? component.getInitialData(store) : null; }); // {2} Promise.all(promises).then(()=>{ const content = renderToString(( <Provider store={store}> // {3} <StaticRouter location={req.path}>{renderRoutes(Routes)}</StaticRouter> </Provider> )); res.send( ` ... `) })}
代码解析:
- {1},
matchRoutes
获取以后拜访路由所匹配到的组件,匹配到的组件如果有getInitialData
办法就间接调用; - {2},
component.getInitialData(store)
返回都是Promise
, 期待全副Promise
执行实现后,store
中的state
就有数据了,此时服务端就曾经获取到相应组件的后盾数据; - {3},
renderRoutes
它的作用是依据动态路由配置渲染出<Route />
组件,相似上面代码,不过renderRoutes
边界解决的更加欠缺。
{routes.map(route => ( <Route {...route} />))}
仔细的你必定会发现,明明服务器曾经拿到数据了为什么刷新浏览器会一闪一闪呢,起因在于,客户端渲染接管时,初始化的用户列表仍然是个空数组,通过发送后盾申请获取到数据这个异步过程,导致的页面一闪一闪的。它的解决方案有一个术语叫做数据的脱水与注水。
数据脱水与注水
其实非常简单,在渲染服务端时,曾经拿到了后盾申请数据,因而咱们能够做:
res.send( ` <!doctype html> <html lang="en"> ... <body> <div id="root">${content}</div> <script> window.INITIAL_STATE = ${JSON.stringify(store.getState())} </script> <script src="/index.js"></script> </body> </html> `)
通过 INITIAL_STATE
全局变量把后盾申请到的数据存起来。客户端创立 store
时,当做初始化的 state
应用即可:
src/store/index.js
export const getClientStore = ()=>{ const defaultState = window.INITIAL_STATE; return createStore(reducer,defaultState,applyMiddleware(thunk));}
这样创立进去的 store
初始化的 state
中就曾经有了用户列表。界面就不再会呈现一闪一闪的成果了。
到这里为止,一个繁难的同构框架曾经有了。
兼容 CSS 款式渲染
在 Home
组件中增加一个款式文件: styles.module.css
,轻易写点款式
.box{ background: red; margin-top: 100px;}
在 Home
组件中引入款式:
import styles from "./styles.module.css";<div className={styles.box}>...</div>
间接编译必定报错,咱们须要在 webpack
中增加相应的 loader
webpack.client.js
module:{ rules:[ { test:/\.css$/i, // 正则匹配到.css款式文件 use:[ 'style-loader', // 把失去的CSS内容插入到HTML中 { loader: 'css-loader', options: { modules: true // 开启 css modules } } ] } ] }
webpack.server.js
module:{ rules:[ { test:/\.css$/i, use:[ 'isomorphic-style-loader', { loader: 'css-loader', options: { modules: true } }, ] } ] }
仔细的你必定会发现, server
端的配置应用了 isomorphic-style-loader
而 client
端应用了 style-loader
,它们的区别是什么?
isomorphic-style-loader vs style-loader
style-loader
它的作用是把生成进去的 css
款式动静插入到 HTML
中,然而在服务端渲染是没有方法应用 DOM
的,因而服务端渲染不能应用它。
isomorphic-style-loader
次要是导出了3个函数, _getCss
、 _insertCss
与_getContent
,供使用者调用,而不再是简略粗犷的插入 DOM
中。
server 端反对款式
src/server/render.js
export const render = (req,res)=>{ const context = { css: [] }; Promise.all(promises).then(()=>{ const content = renderToString(( <Provider store={store}> <StaticRouter location={req.path} context={context}>{renderRoutes(Routes)}</StaticRouter> </Provider> )); const css = context.css.length ? context.css.join('\n') : ''; res.send( ` <!doctype html> <html lang="en"> <head> ... <style>${css}</style> </head> ... </html> `)}
StaticRouter
反对传入一个 context
属性,这样被拜访的组件则能够共享该属性。在被拜访组件的生命周期中通过调用 _getCss()
办法向 staticContext
中推入款式。最初在服务端拼接出所有款式插入到 HTML
中。
Home
组件(革新成 class
组件)
componentWillMount() { if(this.props.staticContext){ this.props.staticContext.css.push(styles._getCss()); } }
在 componentWillMount
生命周期(服务端渲染会调用该生命周期),向 staticContext
中推入组件应用的款式。最初在服务端拼接成残缺的款式文件。
这里应用 staticContext
能够实现,应用 redux
也一样能够实现。
总结
到此为止咱们就实现了一个繁难的同构框架。上面做一个简略的总结:
- 同构渲染其实就是将同一套
react
代码在服务端执行一遍渲染动态页面,又在客户端执行一遍实现事件绑定。 - 它的劣势是,放慢首页访问速度以及
SEO
敌对,如果你的我的项目没有这方面的需要,则不须要抉择同构。 - 它的毛病是,不能在服务端渲染期间操作
DOM
、BOM
等api
,比方document
、window
对象等,并且它减少了代码的复杂度,某些代码操作须要辨别运行环境。 - 在理论我的项目中,倡议应用
Next.js
框架去做,站在伟人的肩旁上,能够少踩很多坑。