返回顶部
首页 > 资讯 > 前端开发 > html >react中如何实现同构模板
  • 221
分享到

react中如何实现同构模板

2024-04-02 19:04:59 221人浏览 泡泡鱼
摘要

这篇文章给大家分享的是有关React中如何实现同构模板的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。TODO List数据:如何保持前后端应用状态一致路由:路由在服务端和客户端中

这篇文章给大家分享的是有关React中如何实现同构模板的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

TODO List

  • 数据:如何保持前后端应用状态一致

  • 路由:路由在服务端和客户端中的匹配方案

  • 代码:同构,哪些地方可以共享,哪些地方需要差异化

  • 静态资源:服务端如何引入CSS/图片等

  • ssr直出资源:服务端在渲染路由页面时如何匹配css/chunks资源

  • 打包方案:服务端和浏览器端如何写各自的webpack配置文件

  • SEO: head头处理方案

同构的基础

正常的网页运行,需要生成dom,在dom树loaded之后由js绑定相关的dom事件,监听页面的交互。服务端并不具备dom的执行环境,因而所有的服务端渲染其实都是返回了一个填充了初始数据的静态文本。在react中,除了常用的render这个用于生成dom的方法,还提供了renderToString,renderToStaticMarkup方法用来生成字符串,由于VitualDOM的存在,结合这些方法就可以像以前的字符串模板那样生成普通的字符串,返回给客户端接管,再接着进行事件相关的绑定。最新的React v16+使用hydrate和ssr配套,能让客户端把服务端的VitualDOM渲染出来后得以复用,客户端加载js后不会重刷一边,减小了开销,也避免浏览器重刷dom时带来的闪屏体验。而react的组件,还是和往常写spa一样编写,前后端共享。不同的只是入口的渲染方法换了名字,且客户端会挂载dom而已。

// clinet.js
ReactDom.hydrate(<App />, document.getElementById('app'))

// server.js
const html = ReactDom.renderToString(<App />)

同构后网站运行流程图

盗用一张图,来自阿里前端。乍一看,ssr与csr的区别就在于2 3 4 5,spa模式简单粗暴地返回一个空白的html页面,然后在11里才去加载数据进行页面填充,在此之前,页面都处于空白状态。而ssr则会根据路由信息,提前获取该路由页面的初始数据,返回页面时已经有了初步的内容,不至于空白,也便于搜索引擎收录。

react中如何实现同构模板

路由匹配

浏览器端的路由匹配还是照着spa来做应该无需费心。略过了...

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

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

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

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

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

// react-controller.js文件
import Router from 'koa-router'

const router = new Router({
 prefix: '/react'
})

router.all('/', async (ctx, next) => {

 const html = await render(ctx)

 ctx.body = html

})

export default router

react-router专供了给ssr使用的StaticRouter接口,称之为静态的路由。诚然,服务端不像客户端,对应于一次网络请求,路由就是当前的请求url,是唯一的,不变的。在返回ssr直出的页面后,页面交互造成地址栏的变化,只要用的是react-router提供的方法,无论是hash方式,还是history方式,都属于浏览器端react-router的工作了,于是完美继承了spa的优势。只有在输入栏敲击Enter,才会发起新一轮的后台请求。

import { StaticRouter } from 'react-router-dom'
 const App = () => {

  return (
   <Provider store={store}>

    <StaticRouter
     location={ctx.url}
     context={context}>
     
     <Layout />

    </StaticRouter>

   </Provider>
  )
 }

应用状态数据管理

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

换成了react,其实套路也是一样一样的。但是区别在于:

传统的字符串模板,组件模板是彼此分离的,可各自单独引入数据,再拼装起来形成一份html。而在react的ssr里,页面只能通过defaultValue和defaultProps一次性render,无法rerender。

不能写死defaultValude,所以只能使用props的数据方案。在执行renderToString之前,提前准备好整个应用状态的所有数据。全局的数据管理方案可考虑redux和mobx等。

需要准备初始渲染数据,所以要精准获取当前地址将要渲染哪些组件。react-router-config和react-router同源配套,是个支持静态路由表配置的工具,提供了matchRoutes方法,可获得匹配的路由数组

import { matchRoutes } from 'react-router-config'

import loadable from '@loadable/component'

const Root = loadable((props) => import('./pages/Root'))
const Index = loadable(() => import("./pages/Index"))
const Home = loadable(() => import("./pages/Home"))

const routes = [
 {
  path: '/',
  component: Root,
  routes: [
   {
    path: '/index',
    component: Index,
   },
   {
    path: '/home',
    component: Home,
    syncData () => {}
    routes: []
   }
  ]
 }
]

router.all('/', async (url, next) => {
 const branch = matchRoutes(routes, url)
})

组件的初始数据接口请求,最美的办法当然是定义在各自的class组件的静态方法中去,但是前提是组件不能被懒加载,不然获取不到组件class,当然也无法获取class static method了,很多使用@loadable/component(一个code split方案)库的开发者多次提issue,作者也明示无法支持。不支持懒加载是绝对不可能的了。所以委屈一下代码了,在需要的route对象中定义一个asyncData方法。

服务端

// routes.js
{
 path: '/home',
 component: Home,
 asyncData (store, query) {
  const city = (query || '').split('=')[1]
 
  let promise = store.dispatch(fetchCityListAndTemperature(city || undefined))
  
  let promise2 = store.dispatch(setRefetchFlag(false))
 
  return Promise.all([promise, promise2])
  return promise
 }
}
// render.js
import { matchRoutes } from 'react-router-config'
import createStore from '../store/redux/index'

const store = createStore()
const branch = matchRoutes(routes, url)

const promises = branch.map(({ route }) => {
 // 遍历所有匹配路由,预加载数据
 return route.asyncData
  ? route.asyncData(store, query)
  : Promise.resolve(null)

})
// 完成store的预加载数据初始化工作
await Promise.all(promises)
// 获取最新的store
const preloadedState = store.getState()

const App = (props) => {

 return (
  <Provider store={store}>

   <StaticRouter
    location={ctx.url}
    context={context}>
    
    <Layout />

   </StaticRouter>

  </Provider>
 )
}
// 数据准备好后,render整个应用
const html = renderToString(<App />)

// 把预加载的数据挂载在`window`下返回,客户端自己去取
return 
  <html>
   <head></head>
   <body>
    <div id="app">${html}</div>
    <script>
     window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)};
    </script>
   </body>
  </html>

客户端

为保证两端的应用数据一致,客户端也要使用同一份数据初始化一次redux的store,再生成应用。如果两者的dom/数据不一致,导致浏览器接管的时候dom重新生成了一次,在开发模式下的时候,控制台会输出错误信息,开发体验完美。后续ajax的数据,在componentDidMount和事件中去执行,和服务端的逻辑天然剥离。

// 获取服务端提供的初始化数据
const preloadedState = window.__PRELOADED_STATE__ || undefined

delete window.__PRELOADED_STATE__

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

const App = () => {

 return (
  <Provider store={store}>

   <BrowserRouter>

    <Layout />
    
   </BrowserRouter>

  </Provider>
 )
}

// loadableReady由@loadabel/component提供,在code split模式下使用
loadableReady().then(() => {
 
 ReactDom.hydrate(<App />, document.getElementById('app'))

})

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

import { connect } from 'react-redux'

@connect(
 state => ({
  refetchFlag: state.weather.refetchFlag,
  quality: state.weather.quality
 }),
 dispatch => ({
  fetchCityListAndQuality: () => dispatch(fetchCityListAndQuality()),
  setRefetchFlag : () => dispatch(setRefetchFlag(true))
 })
)
export default class Quality extends Component {
 componentDidMount () {

  const {
   location: { search },
   refetchFlag,
   fetchCityListAndQuality,
   setRefetchFlag
  } = this.props

  const { location: city } = queryString.parse(search)

  refetchFlag 
   ? fetchCityListAndQuality(city || undefined)
   : setRefetchFlag()
 }
}

打包方案

客户端打包

我想说的是“照旧”。因为在浏览器端运行的还是spa。入门级的具体见GitHub,至于如何配置得赏心悦目,用起来得心应手,根据项目要求各显神通吧。

服务端打包

和客户端的异同:

同:

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

WEBpack v4+/babel v7+ ... 真香

... 留白

异:

入口文件不一样,出口文件不一样

这里既可以把整个服务端入口app.js作为打包入口,也可以把react路由的起点文件作为打包入口,配置输出为umd模块,再由app.js去require。以后者为例(好处在于升级改造项目时尽可能地降低对原系统的影响,排查问题也方便,断点调试什么的也方便):

// webpack.server.js
const webpackConfig = {
 entry: {
  server: './src/server/index.js'
 },
 output: {
  path: path.resolve(__dirname, 'build'),
  filename: '[name].js',
  libraryTarget: 'umd'
 }
}

// app.js
const reactKoaRouter = require('./build/server').default
app.use(reactKoaRouter.routes())

css、image资源正常来说服务端无需处理,如何绕开

偷懒,还没开始研究,占个坑

require的是node自带的模块时避免被webpack打包

const serverConfig = { ... target: 'node' }

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

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

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

... 留白

服务端直出时资源的搜集

服务端输出html时,需要定义好css资源、js资源,让客户端接管后下载使用。如果没啥追求,可以直接把客户端的输出文件全加上去,暴力稳妥,简单方便。但是上面提到的@loadable/component库,实现了路由组件懒加载/code split功能后,也提供了全套服务,配套套装的webpack工具,ssr工具,帮助我们做搜集资源的工作。

// webpack.base.js
const webpackConfig = {
 plugins: [ ..., new LoadablePlugin() ]
}

// render.js
import { ChunkExtractor } from '@loadable/server'

const App = () => {

 return (
  <Provider store={store}>

   <StaticRouter
    location={ctx.url}
    context={context}>
    
    <Layout />

   </StaticRouter>

  </Provider>
 )
}

const webStats = path.resolve(
 __dirname,
 '../public/loadable-stats.json', // 该文件由webpack插件自动生成
)

const webExtractor = new ChunkExtractor({ 
 entrypoints: ['client'],  // 为入口文件名
 statsFile: webStats
})


const jsx = webExtractor.collectChunks(<App />)

const html = renderToString(jsx)

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

const preloadedState = store.getState()

const helmet = Helmet.renderStatic()

return `
 <html>
  <head>
   ${helmet.title.toString()}
   ${helmet.meta.toString()}
   ${linkTags}
   ${styleTags}
  </head>
  <body>
   <div id="app">${html}</div>
   <script>
    window.STORE = 'love';
    window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)};
   </script>
   ${scriptTags}
  </body>
 </html>
`

SEO信息

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

感谢各位的阅读!关于“react中如何实现同构模板”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

--结束END--

本文标题: react中如何实现同构模板

本文链接: https://lsjlt.com/news/71121.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • react中如何实现同构模板
    这篇文章给大家分享的是有关react中如何实现同构模板的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。TODO List数据:如何保持前后端应用状态一致路由:路由在服务端和客户端中...
    99+
    2024-04-02
  • PHP中Smarty模板如何实现模板继承
    这篇文章主要介绍PHP中Smarty模板如何实现模板继承,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!{extends}模板继承中,你可以在子模板内使用{extends}标签来扩展父...
    99+
    2024-04-02
  • knockoutjs模板如何实现树形结构列表
    小编给大家分享一下knockoutjs模板如何实现树形结构列表,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!数据结构  ...
    99+
    2024-04-02
  • Xamarin XAML语言中如何实现控件模板的模板绑定
    这篇文章主要介绍Xamarin XAML语言中如何实现控件模板的模板绑定,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完! 控件模板的模板绑定为了可以轻松更改控件模板中控件上的属性值,可以在控件模板中实现模板绑...
    99+
    2023-06-04
  • JavaScript如何实现模板文字
    小编给大家分享一下JavaScript如何实现模板文字,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!模板文字我们通常使用“+”运算符连接字符串值和变量。有了ES6...
    99+
    2023-06-27
  • React服务端渲染和同构的实现
    目录背景第一阶段第二阶段第三阶段创建一个服务端渲染应用同构流程总结路由路由同构背景 第一阶段 很久以前, 一个网站的开发还是前端和服务端在一个项目来维护, 可能是用php+jquer...
    99+
    2024-04-02
  • Java如何实现树的同构?
    树的同构 备忘! 定义:给定两棵树r1、r2,如果r1可以通过若干次的左子树和右子树互换,使之与r2完全相同,这说明两者同构。 举例 树的构造 树可以由数组或链表来构造: 举例:上...
    99+
    2024-04-02
  • react如何实现弹出模态框
    这篇文章主要介绍“react如何实现弹出模态框”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“react如何实现弹出模态框”文章能帮助大家解决问题。react实现弹出模态框的方法:1、用createP...
    99+
    2023-07-05
  • React服务端渲染和同构怎么实现
    这篇文章主要讲解了“React服务端渲染和同构怎么实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“React服务端渲染和同构怎么实现”吧!背景第一阶段很久以前, 一个网站的开发还是前端和服...
    99+
    2023-06-30
  • SpringBoot中如何使用Freemarker构建邮件模板
    本篇文章给大家分享的是有关SpringBoot中如何使用Freemarker构建邮件模板,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。引入依赖第一步当然是引入freemarke...
    99+
    2023-06-19
  • 如何使用JavaScript实现模板方法模式
    模板方法模式是一种行为设计模式,它是指将一个算法的骨架定义在一个操作中,将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下重新定义算法的某些步骤。在本文中,我们将介绍如何使用 JavaScript 实现模板方法模式。实...
    99+
    2023-05-14
  • Vue模板语法中如何实现数据绑定
    这篇文章主要为大家展示了“Vue模板语法中如何实现数据绑定”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Vue模板语法中如何实现数据绑定”这篇文章吧。1.单项数...
    99+
    2024-04-02
  • Django模板中如何实现常用的过滤器
    这篇文章主要介绍Django模板中如何实现常用的过滤器,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!模版常用过滤器在模版中,有时候需要对一些数据进行处理以后才能使用。一般在Python中我们是通过函数的形式来完成的。...
    99+
    2023-06-15
  • Xamarin XAML语言中如何实现模板视图TemplatedView
    这篇文章给大家分享的是有关Xamarin XAML语言中如何实现模板视图TemplatedView的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。模板视图TemplatedView与模板页面相对的是Template...
    99+
    2023-06-04
  • OpenCV-Python如何实现多模板匹配
    小编给大家分享一下OpenCV-Python如何实现多模板匹配,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!多模板匹配在上一篇的实战中,我们通过人物眼睛的子图,找...
    99+
    2023-06-15
  • Discuz! 中实现不同版块使用不同的模板文件的方法
    通过此修改可在Discuz!X1后台为不同版块设置不同的模板文件。在需要为论坛不同版块设置不同模板风格的时候特别有用,比如你专门一个版块用来发布活动,那么可以为这个版块专门做一个模板文件,从而实现个性化的要求: 安装步骤...
    99+
    2022-06-12
    版块 模板
  • 利用ChatGPT实现快速网站模板构建
    要利用ChatGPT实现快速网站模板构建,可以按照以下步骤进行:1. 确定需求:首先,确定你的网站模板的需求和功能。考虑你需要的页面...
    99+
    2023-10-11
    ChatGPT
  • Xamarin XAML语言中如何构建ControlTemplate控件模板实现主题的切换功能
    小编给大家分享一下Xamarin XAML语言中如何构建ControlTemplate控件模板实现主题的切换功能,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!(3...
    99+
    2023-06-04
  • 微信小程序如何实现template模板
    小编给大家分享一下微信小程序如何实现template模板,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!微信小程序template模板使用前言微信小程序中提供了template使用,即相同的...
    99+
    2024-04-02
  • 微信小程序如何实现tabBar模板
    这篇文章给大家分享的是有关微信小程序如何实现tabBar模板的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。具体如下:众所周知,微信小程序的tabBar都是新开页面的,而微信文档上...
    99+
    2024-04-02
软考高级职称资格查询
推荐阅读
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作