首页 web前端 js教程 具有流媒体和动态数据的高级 React SSR 技术

具有流媒体和动态数据的高级 React SSR 技术

Jan 05, 2025 am 03:49 AM

Advanced React SSR Techniques with Streaming and Dynamic Data

随着应用程序的增长,挑战也会随之增加。为了保持领先地位,掌握先进的 SSR 技术对于提供无缝和高性能的用户体验至关重要。

在上一篇文章中为 React 项目中的服务器端渲染奠定了基础,我很高兴与您分享可以帮助您保持项目可扩展性、有效地将数据从服务器加载到客户端以及解决水合问题的功能。

目录

  • SSR 中的流式传输
  • 延迟加载和 SSR
  • 使用延迟加载实现流式传输
    • 更新 React 组件
    • 更新流媒体服务器
  • 服务器到客户端数据
    • 在服务器上传递数据
    • 处理客户端上的环境变量
  • 水分问题
    • 示例场景
    • 解决水分问题
  • 结论

SSR 中的流式传输是什么

服务器端渲染 (SSR) 中的流式传输 是一种技术,服务器在生成 HTML 页面的各个部分时将其以块的形式发送到浏览器,而不是等待整个页面准备好在交付之前。这允许浏览器立即开始渲染内容,从而缩短加载时间并提高用户的性能。

流媒体对于以下方面特别有效:

  • 大页面:生成整个 HTML 可能需要大量时间。
  • 动态内容:当页面的某些部分依赖于外部 API 调用或动态生成的块时。
  • 高流量应用程序:减少高峰使用期间的服务器负载和延迟。

流媒体弥合了传统 SSR 和现代客户端交互性之间的差距,确保用户在不影响性能的情况下更快地看到有意义的内容。

延迟加载和 SSR

延迟加载是一种将组件或模块的加载推迟到实际需要时才加载的技术,从而减少初始加载时间并提高性能。与 SSR 结合使用时,延迟加载可以显着优化服务器和客户端工作负载。

延迟加载依赖于 React.lazy,它动态地将组件导入为 Promises。在传统的 SSR 中,渲染是同步的,这意味着服务器必须解析所有 Promise,然后才能生成完整的 HTML 并将其发送到浏览器。

流式处理允许服务器在渲染组件时以块的形式发送 HTML,从而解决了这些挑战。这种方法使 Suspense 回退能够立即发送到浏览器,确保用户尽早看到有意义的内容。当延迟加载的组件被解析时,它们渲染的 HTML 会逐渐传输到浏览器,无缝地替换后备内容。这可以避免阻塞渲染过程,减少延迟并缩短感知加载时间。

使用延迟加载实现流式传输

本指南基于上一篇文章中介绍的概念,构建生产就绪的 SSR React 应用程序,您可以在底部找到链接。为了通过 React 启用 SSR 并支持延迟加载组件,我们将对 React 组件和服务器进行多项更新。

更新 React 组件

服务器入口点

React 的 renderToString 方法通常用于 SSR,但它会等到整个 HTML 内容准备好后才将其发送到浏览器。通过切换到 renderToPipeableStream,我们可以启用流式传输,它会在生成 HTML 部分时发送它们。

// ./src/entry-server.tsx
import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server'
import App from './App'

export function render(options?: RenderToPipeableStreamOptions) {
  return renderToPipeableStream(<App />, options)
}
登录后复制
登录后复制
登录后复制

创建延迟加载组件

在此示例中,我们将创建一个简单的 Card 组件来演示该概念。在生产应用程序中,此技术通常与较大的模块或整个页面一起使用以优化性能。

// ./src/Card.tsx
import { useState } from 'react'

function Card() {
  const [count, setCount] = useState(0)

  return (
    <div className="card">
      <button onClick={() => setCount((count) => count + 1)}>
        count is {count}
      </button>
      <p>
        Edit <code>src/App.tsx</code> and save to test HMR
      </p>
    </div>
  )
}

export default Card
登录后复制
登录后复制
登录后复制

在应用程序中使用延迟加载组件

要使用延迟加载组件,请使用 React.lazy 动态导入它,并用 Suspense 包装它,以在加载期间提供后备 UI

// ./src/App.tsx
import { lazy, Suspense } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const Card = lazy(() => import('./Card'))

function App() {
  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <Suspense fallback='Loading...'>
        <Card />
      </Suspense>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App
登录后复制
登录后复制
登录后复制

更新流媒体服务器

为了启用流式传输,开发和生产设置都需要支持一致的 HTML 渲染过程。由于两个环境的过程相同,因此您可以创建一个可重用函数来有效处理流内容。

创建流内容函数

// ./server/constants.ts
export const ABORT_DELAY = 5000
登录后复制
登录后复制
登录后复制

streamContent 函数启动渲染过程,将增量 HTML 块写入响应,并确保正确的错误处理。

// ./server/streamContent.ts
import { Transform } from 'node:stream'
import { Request, Response, NextFunction } from 'express'
import { ABORT_DELAY, HTML_KEY } from './constants'
import type { render } from '../src/entry-server'

export type StreamContentArgs = {
  render: typeof render
  html: string
  req: Request
  res: Response
  next: NextFunction
}

export function streamContent({ render, html, res }: StreamContentArgs) {
  let renderFailed = false

  // Initiates the streaming process by calling the render function
  const { pipe, abort } = render({
    // Handles errors that occur before the shell is ready
    onShellError() {
      res.status(500).set({ 'Content-Type': 'text/html' }).send('<pre class="brush:php;toolbar:false">Something went wrong
') }, // Called when the shell (initial HTML) is ready for streaming onShellReady() { res.status(renderFailed ? 500 : 200).set({ 'Content-Type': 'text/html' }) // Split the HTML into two parts using the placeholder const [htmlStart, htmlEnd] = html.split(HTML_KEY) // Write the starting part of the HTML to the response res.write(htmlStart) // Create a transform stream to handle the chunks of HTML from the renderer const transformStream = new Transform({ transform(chunk, encoding, callback) { // Write each chunk to the response res.write(chunk, encoding) callback() }, }) // When the streaming is finished, write the closing part of the HTML transformStream.on('finish', () => { res.end(htmlEnd) }) // Pipe the render output through the transform stream pipe(transformStream) }, onError(error) { // Logs errors encountered during rendering renderFailed = true console.error((error as Error).stack) }, }) // Abort the rendering process after a delay to avoid hanging requests setTimeout(abort, ABORT_DELAY) }
登录后复制
登录后复制

更新开发配置

// ./server/dev.ts
import { Application } from 'express'
import fs from 'fs'
import path from 'path'
import { StreamContentArgs } from './streamContent'

const HTML_PATH = path.resolve(process.cwd(), 'index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx')

// Add to args the streamContent callback
export async function setupDev(app: Application, streamContent: (args: StreamContentArgs) => void) {
  const vite = await (
    await import('vite')
  ).createServer({
    root: process.cwd(),
    server: { middlewareMode: true },
    appType: 'custom',
  })

  app.use(vite.middlewares)

  app.get('*', async (req, res, next) => {
    try {
      let html = fs.readFileSync(HTML_PATH, 'utf-8')
      html = await vite.transformIndexHtml(req.originalUrl, html)

      const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH)

      // Use the same callback for production and development process
      streamContent({ render, html, req, res, next })
    } catch (e) {
      vite.ssrFixStacktrace(e as Error)
      console.error((e as Error).stack)
      next(e)
    }
  })
}
登录后复制
登录后复制

更新生产配置

// ./server/prod.ts
import { Application } from 'express'
import fs from 'fs'
import path from 'path'
import compression from 'compression'
import sirv from 'sirv'
import { StreamContentArgs } from './streamContent'

const CLIENT_PATH = path.resolve(process.cwd(), 'dist/client')
const HTML_PATH = path.resolve(process.cwd(), 'dist/client/index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'dist/ssr/entry-server.js')

// Add to Args the streamContent callback
export async function setupProd(app: Application, streamContent: (args: StreamContentArgs) => void) {
  app.use(compression())
  app.use(sirv(CLIENT_PATH, { extensions: [] }))

  app.get('*', async (req, res, next) => {
    try {
      const html = fs.readFileSync(HTML_PATH, 'utf-8')

      const { render } = await import(ENTRY_SERVER_PATH)

      // Use the same callback for production and development process
      streamContent({ render, html, req, res, next })
    } catch (e) {
      console.error((e as Error).stack)
      next(e)
    }
  })
}
登录后复制

更新 Express 服务器

将streamContent函数传递给每个配置:

// ./server/app.ts
import express from 'express'
import { PROD, APP_PORT } from './constants'
import { setupProd } from './prod'
import { setupDev } from './dev'
import { streamContent } from './streamContent'

export async function createServer() {
  const app = express()

  if (PROD) {
    await setupProd(app, streamContent)
  } else {
    await setupDev(app, streamContent)
  }

  app.listen(APP_PORT, () => {
    console.log(`http://localhost:${APP_PORT}`)
  })
}

createServer()
登录后复制

实施这些更改后,您的服务器将:

  • 将 HTML 增量式传输到浏览器,减少首次绘制的时间。
  • 无缝处理延迟加载的组件,提高性能和用户体验。

服务器到客户端数据

在将 HTML 发送到客户端之前,您可以完全控制服务器生成的 HTML。这允许您根据需要添加标签、样式、链接或任何其他元素来动态修改结构。

一种特别强大的技术是注入一个<script>标记到 HTML 中。这种方法使您能够将动态数据直接传递给客户端。</script>

在此示例中,我们将重点关注传递环境变量,但您可以传递您需要的任何 JavaScript 对象。通过将环境变量传递给客户端,您可以避免在这些变量发生更改时重建整个应用程序。在底部链接的示例存储库中,您还可以看到配置文件数据是如何动态传递的。

在服务器上传递数据

定义 API_URL

在服务器上设置 API_URL 环境变量。默认情况下,这将指向 jsonplaceholder。 __INITIAL_DATA__ 将充当全局窗口对象上存储数据的键。

// ./src/entry-server.tsx
import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server'
import App from './App'

export function render(options?: RenderToPipeableStreamOptions) {
  return renderToPipeableStream(<App />, options)
}
登录后复制
登录后复制
登录后复制

将初始数据注入 HTML

创建一个实用函数,在将初始数据发送到客户端之前将其注入到 HTML 字符串中。此数据将包括 API_URL 等环境变量。

// ./src/Card.tsx
import { useState } from 'react'

function Card() {
  const [count, setCount] = useState(0)

  return (
    <div className="card">
      <button onClick={() => setCount((count) => count + 1)}>
        count is {count}
      </button>
      <p>
        Edit <code>src/App.tsx</code> and save to test HMR
      </p>
    </div>
  )
}

export default Card
登录后复制
登录后复制
登录后复制

更新流内容

使用applyInitialData函数将初始数据注入到HTML中并发送给客户端。

// ./src/App.tsx
import { lazy, Suspense } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const Card = lazy(() => import('./Card'))

function App() {
  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <Suspense fallback='Loading...'>
        <Card />
      </Suspense>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App
登录后复制
登录后复制
登录后复制

处理客户端上的环境变量

扩展全局窗口类型

更新全局类型声明以包含 __INITIAL_DATA__ 键及其结构。

// ./server/constants.ts
export const ABORT_DELAY = 5000
登录后复制
登录后复制
登录后复制

从窗口对象访问 API_URL

// ./server/streamContent.ts
import { Transform } from 'node:stream'
import { Request, Response, NextFunction } from 'express'
import { ABORT_DELAY, HTML_KEY } from './constants'
import type { render } from '../src/entry-server'

export type StreamContentArgs = {
  render: typeof render
  html: string
  req: Request
  res: Response
  next: NextFunction
}

export function streamContent({ render, html, res }: StreamContentArgs) {
  let renderFailed = false

  // Initiates the streaming process by calling the render function
  const { pipe, abort } = render({
    // Handles errors that occur before the shell is ready
    onShellError() {
      res.status(500).set({ 'Content-Type': 'text/html' }).send('<pre class="brush:php;toolbar:false">Something went wrong
') }, // Called when the shell (initial HTML) is ready for streaming onShellReady() { res.status(renderFailed ? 500 : 200).set({ 'Content-Type': 'text/html' }) // Split the HTML into two parts using the placeholder const [htmlStart, htmlEnd] = html.split(HTML_KEY) // Write the starting part of the HTML to the response res.write(htmlStart) // Create a transform stream to handle the chunks of HTML from the renderer const transformStream = new Transform({ transform(chunk, encoding, callback) { // Write each chunk to the response res.write(chunk, encoding) callback() }, }) // When the streaming is finished, write the closing part of the HTML transformStream.on('finish', () => { res.end(htmlEnd) }) // Pipe the render output through the transform stream pipe(transformStream) }, onError(error) { // Logs errors encountered during rendering renderFailed = true console.error((error as Error).stack) }, }) // Abort the rendering process after a delay to avoid hanging requests setTimeout(abort, ABORT_DELAY) }
登录后复制
登录后复制

使用动态 API_URL 发出请求

// ./server/dev.ts
import { Application } from 'express'
import fs from 'fs'
import path from 'path'
import { StreamContentArgs } from './streamContent'

const HTML_PATH = path.resolve(process.cwd(), 'index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx')

// Add to args the streamContent callback
export async function setupDev(app: Application, streamContent: (args: StreamContentArgs) => void) {
  const vite = await (
    await import('vite')
  ).createServer({
    root: process.cwd(),
    server: { middlewareMode: true },
    appType: 'custom',
  })

  app.use(vite.middlewares)

  app.get('*', async (req, res, next) => {
    try {
      let html = fs.readFileSync(HTML_PATH, 'utf-8')
      html = await vite.transformIndexHtml(req.originalUrl, html)

      const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH)

      // Use the same callback for production and development process
      streamContent({ render, html, req, res, next })
    } catch (e) {
      vite.ssrFixStacktrace(e as Error)
      console.error((e as Error).stack)
      next(e)
    }
  })
}
登录后复制
登录后复制

现在,您的客户端代码中可以使用动态环境变量,使您能够管理服务器到客户端的数据,而无需重建 JavaScript 包。这种方法简化了配置,并使您的应用程序更加灵活和可扩展。

水分问题

现在您可以将数据从服务器传递到客户端,如果您尝试直接在组件内使用此数据,则可能会遇到水合问题。发生这些错误是因为服务器渲染的 HTML 与客户端上的初始 React 渲染不匹配。

示例场景

考虑在组件中使用 API_URL 作为简单字符串

// ./src/entry-server.tsx
import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server'
import App from './App'

export function render(options?: RenderToPipeableStreamOptions) {
  return renderToPipeableStream(<App />, options)
}
登录后复制
登录后复制
登录后复制

在这种情况下,服务器会将 API_URL 的组件渲染为空字符串,但在客户端上,API_URL 已经具有来自 window 对象的值。这种不匹配会导致水合错误,因为 React 检测到服务器渲染的 HTML 和客户端的 React 树之间存在差异。

虽然用户可能会看到内容快速更新,但 React 在控制台中记录了水合警告。要解决此问题,您需要确保服务器和客户端呈现相同的初始 HTML 或将 API_URL 显式传递到服务器入口点。

解决水合作用问题

要解决错误,请通过服务器入口点将initialData传递给App组件。

更新流内容

// ./src/Card.tsx
import { useState } from 'react'

function Card() {
  const [count, setCount] = useState(0)

  return (
    <div className="card">
      <button onClick={() => setCount((count) => count + 1)}>
        count is {count}
      </button>
      <p>
        Edit <code>src/App.tsx</code> and save to test HMR
      </p>
    </div>
  )
}

export default Card
登录后复制
登录后复制
登录后复制

在渲染函数中处理数据

// ./src/App.tsx
import { lazy, Suspense } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

const Card = lazy(() => import('./Card'))

function App() {
  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <Suspense fallback='Loading...'>
        <Card />
      </Suspense>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App
登录后复制
登录后复制
登录后复制

在应用程序组件中使用initialData

// ./server/constants.ts
export const ABORT_DELAY = 5000
登录后复制
登录后复制
登录后复制

现在,您的服务器渲染的 HTML 将与客户端上的初始 React 渲染相匹配,从而消除水合错误。 React 将正确协调服务器和客户端树,确保无缝体验。

对于 API_URL 这样的动态数据,请考虑使用 React Context 来管理和在服务器和客户端之间传递默认值。这种方法简化了跨组件共享数据的管理。您可以在底部的链接存储库中找到示例实现。

结论

在本文中,我们探索了 React 的高级 SSR 技术,重点关注实现流式传输、管理服务器到客户端的数据以及解决水合问题。这些方法可确保您的应用程序具有可扩展性、高性能并创造无缝的用户体验。

探索代码

  • 示例:react-ssr-advanced-example
  • 模板:react-ssr-streaming-template
  • Vite 额外模板: template-ssr-react-streaming-ts

相关文章

这是我的 React SSR 系列的一部分。更多文章敬请期待!

  • 构建生产就绪的 SSR React 应用程序
  • 使用流和动态数据的高级 React SSR 技术(你在这里)
  • 在 SSR React 应用程序中设置主题(即将推出)

保持联系

我始终乐于接受反馈、合作或讨论技术想法 - 请随时与我们联系!

  • 投资组合:maxh1t.xyz
  • 电子邮件:m4xh17@gmail.com

以上是具有流媒体和动态数据的高级 React SSR 技术的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

<🎜>:泡泡胶模拟器无穷大 - 如何获取和使用皇家钥匙
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系统,解释
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆树的耳语 - 如何解锁抓钩
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1670
14
CakePHP 教程
1428
52
Laravel 教程
1329
25
PHP教程
1274
29
C# 教程
1256
24
Python vs. JavaScript:学习曲线和易用性 Python vs. JavaScript:学习曲线和易用性 Apr 16, 2025 am 12:12 AM

Python更适合初学者,学习曲线平缓,语法简洁;JavaScript适合前端开发,学习曲线较陡,语法灵活。1.Python语法直观,适用于数据科学和后端开发。2.JavaScript灵活,广泛用于前端和服务器端编程。

从C/C到JavaScript:所有工作方式 从C/C到JavaScript:所有工作方式 Apr 14, 2025 am 12:05 AM

从C/C 转向JavaScript需要适应动态类型、垃圾回收和异步编程等特点。1)C/C 是静态类型语言,需手动管理内存,而JavaScript是动态类型,垃圾回收自动处理。2)C/C 需编译成机器码,JavaScript则为解释型语言。3)JavaScript引入闭包、原型链和Promise等概念,增强了灵活性和异步编程能力。

JavaScript和Web:核心功能和用例 JavaScript和Web:核心功能和用例 Apr 18, 2025 am 12:19 AM

JavaScript在Web开发中的主要用途包括客户端交互、表单验证和异步通信。1)通过DOM操作实现动态内容更新和用户交互;2)在用户提交数据前进行客户端验证,提高用户体验;3)通过AJAX技术实现与服务器的无刷新通信。

JavaScript在行动中:现实世界中的示例和项目 JavaScript在行动中:现实世界中的示例和项目 Apr 19, 2025 am 12:13 AM

JavaScript在现实世界中的应用包括前端和后端开发。1)通过构建TODO列表应用展示前端应用,涉及DOM操作和事件处理。2)通过Node.js和Express构建RESTfulAPI展示后端应用。

了解JavaScript引擎:实施详细信息 了解JavaScript引擎:实施详细信息 Apr 17, 2025 am 12:05 AM

理解JavaScript引擎内部工作原理对开发者重要,因为它能帮助编写更高效的代码并理解性能瓶颈和优化策略。1)引擎的工作流程包括解析、编译和执行三个阶段;2)执行过程中,引擎会进行动态优化,如内联缓存和隐藏类;3)最佳实践包括避免全局变量、优化循环、使用const和let,以及避免过度使用闭包。

Python vs. JavaScript:社区,图书馆和资源 Python vs. JavaScript:社区,图书馆和资源 Apr 15, 2025 am 12:16 AM

Python和JavaScript在社区、库和资源方面的对比各有优劣。1)Python社区友好,适合初学者,但前端开发资源不如JavaScript丰富。2)Python在数据科学和机器学习库方面强大,JavaScript则在前端开发库和框架上更胜一筹。3)两者的学习资源都丰富,但Python适合从官方文档开始,JavaScript则以MDNWebDocs为佳。选择应基于项目需求和个人兴趣。

Python vs. JavaScript:开发环境和工具 Python vs. JavaScript:开发环境和工具 Apr 26, 2025 am 12:09 AM

Python和JavaScript在开发环境上的选择都很重要。1)Python的开发环境包括PyCharm、JupyterNotebook和Anaconda,适合数据科学和快速原型开发。2)JavaScript的开发环境包括Node.js、VSCode和Webpack,适用于前端和后端开发。根据项目需求选择合适的工具可以提高开发效率和项目成功率。

C/C在JavaScript口译员和编译器中的作用 C/C在JavaScript口译员和编译器中的作用 Apr 20, 2025 am 12:01 AM

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。 1)C 用于解析JavaScript源码并生成抽象语法树。 2)C 负责生成和执行字节码。 3)C 实现JIT编译器,在运行时优化和编译热点代码,显着提高JavaScript的执行效率。

See all articles