欢迎来到Introzo百科
Introzo百科
当前位置:网站首页 > 技术 > React 同构实践:实现自己的同构模板

React 同构实践:实现自己的同构模板

日期:2023-10-05 22:17

当我第一次想学习服务端渲染时,我首先想到的是像 next.js 这样成熟的解决方案。看了一两天,有趣又优雅,但是是封装的,原理不清楚,感觉不能灵活集成到老项目中。于是我查阅了各种资料,试图梳理同构的线索,并一步步实现自己的同构模板。相关代码可以在我的GitHub上查看。谢谢阅读! !

待办事项列表

  • 数据:如何保持前端和后端应用状态一致
  • 路由:服务器与客户端之间的路由匹配方案
  • 代码:同构,哪里可以共享,哪里需要区分
  • 静态资源:如何在服务器端引入css/图片等
  • ssr直接资源:服务器渲染路由页面时如何匹配css/chunks资源
  • 打包方案:如何在服务器端和浏览器端编写各自的webpack配置文件
  • SEO:头痛的解决方案

同构基础

为了正常的网页运行,需要生成dom。 dom树加载完成后,js绑定相关的dom事件,监听页面的交互。服务器没有DOM执行环境,因此所有服务器端渲染实际上返回的是填充有初始数据的静态文本。在react中,除了常用的生成DOM的render方法之外,还提供了renderToString和renderToStaticMarkup方法来生成字符串。由于VitualDOM的存在,结合这些方法可以像之前的字符串模板一样生成普通字符。 String,返回给客户端接管,然后进行事件相关的绑定。最新的React v16+使用了 Hydro 和 ssr,它允许客户端渲染服务器端 VitualDOM 并重用它。客户端加载js后,不会重新刷边,减少了开销,避免了浏览器重新刷dom带来的问题。闪屏体验。至于react组件,还是照常写spa的时候写的,前后端共享。唯一的区别是入口渲染方法的名字变了,客户端会挂载dom。

// clinet.js
ReactDom. Hydro(, document.getElementById('app'))

// 服务器.jsconst html = ReactDom.renderToString()

同构后网站运行流程图

盗用阿里巴巴前端的一张图片。乍一看,ssr和csr的区别在于,在2 3 4 5中,spa模式只是返回一个空白的html页面,然后在11中加载数据来填充页面。在此之前,页面是空白的。 SSR会根据路由信息提前获取路由页的初始数据。当页面返回时,已经有了初步的内容,不会是空白,方便搜索引擎收录。

路线匹配

您无需担心浏览器端的路由匹配或遵循 spa。略过吧...

服务端路由需要注意,一是后端服务(如koa-router)的路由的匹配问题,二是react-router路由表的匹配问题匹配到反应应用程序后。

服务端路由可以通过/react前缀与其他API接口区分开来。这种路由匹配方式甚至可以让服务端渲染支持ejs等老项目的模板渲染方式,在系统升级改造时可以实现渐进式升级。

// app.js 文件(后端入口)
从'./controllers/react-controller'导入reactController
//API路由
app.use(apiController.routes())

//ejs页面路由
app.use(ejsController.routes())

// 反应页面路由
app.use(reactController.routes())

//react-controller.js 文件
从“koa-router”导入路由器

常量路由器 = 新路由器({
 前缀:'/react'
})

router.all('/', async (ctx, next) => {const html = 等待渲染(ctx)

 ctx.body = html

})

导出默认路由器

react-router 专门为 ssr 提供了一个 StaticRouter 接口,称为静态路由。确实,服务器与客户端不同。对应网络请求,路由就是当前请求的URL,唯一且不变。返回到SSR直接导出的页面后,页面交互导致地址栏发生变化。只要使用react-router提供的方法,无论是hash方法还是history方法,都是浏览器端react-router的工作,所以完美继承了spa的优点。只有在输入栏中按下回车键,才会发起新一轮的后台请求。

从 'react-router-dom' 导入 { StaticRouter }
 常量应用程序 = () => {

  返回 (
   

    <静态路由器
     位置=无数字噪音键 1017
 上下文=无数字噪音键 1016>
     
     

    

   
  )
 }

应用程序状态数据管理

过去的服务器端渲染,客户端网页下载后需要立即看到的数据都放在服务器上,提前准备好,可以延迟显示。通过ajax请求数据的交互逻辑放在页面加载的js文件中。去。

我改成反应,但套路其实是一样的。但不同的是:

在传统的字符串模板中,组件模板是相互分离的。数据可以单独导入,然后组合在一起形成一段 HTML。在react的SSR中,页面只能通过defaultValue和defaultProps渲染一次,并且不能重新渲染。

defaultValue 无法硬编码,因此只能使用 props 的数据方案。在执行renderToString之前,提前准备好整个应用程序状态的所有数据。全局数据管理方案可以考虑redux、mobx等

需要准备初始渲染数据,因此需要准确获取当前地址将渲染哪些组件。 React-router-config和react-router来自同一来源。它是一个支持静态路由表配置的工具。它提供了matchRoutes方法来获取匹配的路由数组。

从“react-router-config”导入{matchRoutes}

从“@loadable/component”导入可加载文件

const Root = 可加载((props) => import('./pages/Root'))
const Index = 可加载(() => import("./pages/Index"))
const Home = 可加载(() => import("./pages/Home"))

常量路由 = [
 {
  小路: '/',
  成分:根,
  路线:[
   {
    路径:'/索引',
    组成部分:索引,
   },
   {
    路径: '/home',
    组件:首页,
    同步数据 () => {}
    路线:[]
   }
  ]
 }
]

router.all('/', async (url, next) => {
 const 分支 = matchRoutes(路线, url)
})

最漂亮的方式当然是在各自类组件的静态方法中定义组件的初始数据接口请求,但前提是组件不能延迟加载,否则无法获取组件类,当然不能获取类静态方法是的,很多使用@loadable/component(一种代码分割解决方案)库的开发者多次提出问题,作者也明确表示不能支持。不支持延迟加载是绝对不可能的。因此,让我们分解代码并在所需的路由对象中定义一个 asyncData 方法。

服务器

// 路线.js
{路径: '/home',
 组件:首页,
 asyncData(存储,查询){
  const city = (查询 || '').split('=')[1]
 
  让 Promise = store.dispatch(fetchCityListAndTemperature(city || undefined))
  
  让promise2 = store.dispatch(setRefetchFlag(false))
 
  返回 Promise.all([promise,promise2])
  回报承诺
 }
}

// 渲染.js
从“react-router-config”导入{matchRoutes}
从 '../store/redux/index' 导入 createStore

常量存储 = createStore()
const 分支 = matchRoutes(路线, url)

const Promise = www.introzo.com(({ 路线 }) => {
 // 遍历所有匹配的路由并预加载数据
 返回路由.asyncData
  ?路线.asyncData(存储,查询)
  : Promise.resolve(null)

})
// 完成store的预加载数据初始化工作
等待 Promise.all(承诺)
// 获取最新的商店
const preloadedState = store.getState()

const App = (props) => {

 返回 (
  

   <静态路由器
    位置=无数字噪音键 1014
 上下文=无数字噪音键 1013>
    
    

   

  
 )
}
// 数据准备好后,渲染整个应用程序const html = renderToString()

// 将预加载的数据挂载到`window`下并返回,客户端可以自行获取
返回
  
   
   
    ${html}
    
   
  

客户

为了保证两端应用程序数据一致,客户端也必须使用相同的数据来初始化redux store,然后生成应用程序。如果两者之间的DOM/数据不一致,浏览器接管时会重新生成DOM。开发模式下,控制台会输出错误信息,开发体验完美。后续的ajax数据都是在componentDidMount和events中执行的,自然与服务端逻辑分离。

// 获取服务器提供的初始化数据
const preloadedState = window.__PRELOADED_STATE__ ||不明确的

删除窗口.__PRELOADED_STATE__

//客户端存储初始化
const store = createStore(preloadedState)

常量应用程序 = () => {

 返回 (
  

   

    
    
   

  
 )
}

// loadableReady由@loadabel/component提供,用于代码分割模式
loadableReady().then(() => {
 
 ReactDom. Hydro(, document.getElementById('app'))

})

客户端也必须有服务端调用的接口。这就带来了如何避免重复请求的问题。我们知道componentDidMount方法只执行一次。如果服务器请求的数据带有标识符,则可以根据该标识符决定是否向客户端发起新的请求。需要说明的是,判断完成后,标识符会被重置。

从 'react-redux' 导入 { connect }

@连接(
 状态 => ({
  refetchFlag: state.weather.refetchFlag,
  质量:状态.天气.质量
 }),
 调度 => ({
  fetchCityListAndQuality: () => 调度(fetchCityListAndQuality()),
  setRefetchFlag: () => 调度(setRefetchFlag(true))
 })
)
导出默认类质量扩展组件{
 组件DidMount(){

  常量{
   位置:{搜索},
   重新获取标志,
   获取城市列表和质量,
   设置重新获取标志
  } = this.props

  const { 位置:城市 } = queryString.parse(search)

  重新获取标志
   ? fetchCityListAndQuality(城市 || 未定义)
   : setRefetchFlag()
 }
}

包装方案

客户包装

我想说的是“一切照旧”。因为浏览器端运行的是spa。有关入门级详细信息,请参阅 github。至于如何配置得顺眼又好用,大家可以根据项目需求大显身手。

服务器端打包

与客户的异同:

与:

Bable需要兼容不同版本的js语法

webpack v4+/babel v7+...真的很好吃

...留空

不同于:

导入文件不同,导出文件不同

这里既可以使用整个服务器入口app.js作为打包入口,也可以使用react路由的起点文件作为打包入口,配置输出为umd模块,然后从app中require .js。以后者为例(优点是升级项目时,对原系统的影响降到最低,也方便排查问题,断点调试也方便):

// webpack.server.js
常量 webpackConfig = {
 入口: {
  服务器:'./src/server/index.js'
 },
 输出: {
  路径:path.resolve(__dirname, 'build'),
  文件名: '[名称].js',
  库目标:'umd'
 }
}

// 应用程序.js
const ReactKoaRouter = require('./build/server').default
app.use(reactKoaRouter.routes())

一般情况下,css和图片资源不需要服务器处理。如何绕过

我比较懒,还没开始研究,就趁着这个机会

当需要的模块是node自带的模块时,避免被webpack打包

const serverConfig = { ... 目标:'节点' }

需要第三方模块时如何避免被打包

const serverConfig = { ... externals: [ require('webpack-node-externals')() ]

生产环境代码无需混淆和压缩

...留空

服务器退出时直接收集资源

服务器输出HTML时,需要定义css资源和js资源,以便客户端接管后可以下载使用。如果你没什么追求,可以直接添加客户端的所有输出文件,暴力破解,可靠,简单,方便。不过上面提到的@loadable/component库,在实现了路由组件的懒加载/代码分割功能之后,还提供了一整套的服务,包括一套配套的webpack工具和ssr工具,帮助我们收集资源。

// webpack.base.js
常量 webpackConfig = {
 插件:[ ...,新的 LoadablePlugin() ]
}

// 渲染.js
从'@loadable/server'导入{ChunkExtractor}

常量应用程序 = () => {

 返回 (
  

   <静态路由器
    位置=无数字噪音键 1009
 上下文=无数字噪音键 1008>
    
    

   

  
 )
}

const webStats = 路径.resolve(
 __目录名,
 '../public/loadable-stats.json', // 该文件是webpack插件自动生成的
)

const webExtractor = new ChunkExtractor({
 Entrypoints: ['client'], // 是入口文件名
 统计文件:webStats
})


const jsx = webExtractor.collectChunks()

const html = renderToString(jsx)

const scriptTags = webExtractor.getScriptTags()
const linkTags = webExtractor.getLinkTags()
const styleTags = webExtractor.getStyleTags()const preloadedState = store.getState()

const 头盔 = Helmet.renderStatic()

返回`
 
  
   ${helmet.title.toString()}
   ${helmet.meta.toString()}
   ${链接标签}
   ${样式标签}
  
  
   ${html}
   
   ${脚本标签}
  
 
`

SEO信息

上面已经透露了。使用了一个react-helmet库。具体使用请查看官方仓库。信息可以直接写在组件上,最后根据优先级提升到头部。

关灯