react 服务器端渲染_React和Angular应用程序中服务器端渲染的比较

react 服务器端渲染

在本文中,我们将讨论什么是服务器端渲染(SSR),并讨论如何在React和Angular应用程序中实现服务器端渲染(SSR)。 最后,我们还将简要比较启用SSR的难易程度和为此所需采取的方法。

为了理解这两个框架中的SSR,我们将创建具有一些基本路线的示例应用程序,并进行API调用以模拟现实世界的场景。 然后,我们将为每个示例应用程序启用SSR,同时讨论实现预期结果所需的任何变通方法或怪癖。

本文不比较它们本身的框架,而是比较实现结果所需的更改,在这种情况下为服务器端渲染。

什么是服务器端渲染(SSR)?

通过单页应用程序(SPA)的接管,硬编码到模板上的内容量很小,并且网页被拆分为许多不同的组件和/或模板。 这些组件使用自定义路由器进行路由和加载,这些路由器在所有主要框架中都可用。

搜索引擎(又名Web爬网程序)有时会尝试访问我们应用程序中的嵌套路由,这有时并不成功,因为爬网程序必须先下载并执行JavaScript包(理解并执行我们的路由逻辑)才能访问该路由。 尽管某些爬网程序可以下载并执行JavaScript,但更好地控制并仅提供我们希望的内容总是更好。

另一个用例是当我们希望尽可能完整地渲染页面以解决较慢的Internet速度(即下载预渲染的index.html文件,而不是下载CSS和JS在客户端上渲染它)时,或者只是更快地在折叠负载之上,以便在我们的应用程序中缩短交互时间。

服务器端渲染帮助我们将部分极动态的Web应用程序转换为静态Web应用程序,在其中我们可以在服务器端创建并渲染请求路由的内容。 在下载其余应用程序(CSS,JS等)并在后台引导时,此静态页面充当占位符。

因此,当客户端下载并在后台呈现实际的应用程序时,SSR的页面将充当我们应用程序的“启动屏幕”。 因此,有些时候我们可以看到页面,但还不能与其交互,因为页面仍在后台加载。

何时使用SSR?

最好了解SSR何时有价值,何时不重要。

如果我们可以随着项目的进展进行更改以支持SSR(从下面的示例中看到),那么从项目的第一天开始实施SSR就很容易了,但是如果现有应用程序非常复杂,请评估需求在实施之前用于SSR。 以下是我通常使用的一些快速规则:

  1. 整个应用程序是否隐藏在身份验证后面? 如果是,则出于搜索引擎优化(SEO)的考虑而进行SSR毫无意义。 但是,为了使应用程序加载更快,某些人仍然选择执行SSR,但我的首选是依靠服务工作者(取决于用例)来缓存和增强页面加载。
  2. 内容可以设为静态吗? 例如,如果我们有一个要由网络搜寻器索引的演示页面,那么内容可以硬编码到模板中吗? 是否可以在不使用路由器的情况下直接访问这些模板? 我们可以尝试预加载这些资源吗?
  3. 还有一些缺点需要考虑,例如应用程序复杂性增加,初始页面大小增加,服务器响应速度变慢(因为它不再返回在客户端上构造的精益HTML页面)。

一旦讨论了上面列出的几种(或更多)方案,我们将更好地了解我们希望用户如何与我们的应用程序交互,SEO的重要性以及如何使用SSR来增强整体效果。经验。

Angular应用中的SSR

在Angular应用程序中,可以使用Angular Universal启用服务器端渲染。

在开始编写代码之前,让我们快速看一下常规应用程序结构中必须更改的内容以及在Angular应用程序中尝试使用SSR时需要警惕的内容。

  1. 现在,我们需要一台服务器-而不是使用NGINX之类的东西来服务dist文件夹,我们现在将使用一台服务器(在此示例中为NodeJS / Express)来启用SSR。
  2. 我们需要两个主要模块-一个用于客户端,一个用于服务器
  3. 我们需要使用绝对URL而不是相对URL进行API调用
  4. 缓存API调用以避免重新加载SSR期间加载的数据
  5. 这里还有其他陷阱。

有了这些,我们现在就可以开始动手了。

基本应用程序设置

首先,使用@ angular / cli npm包创建一个Angular应用程序。

ng new ng-ssr cd ng-ssr

接下来,创建一些基本组件,我们将在我们的应用程序中路由这些组件:

ng generate component home ng generate component about ng generate component settings

然后,在src / app文件夹中创建一个名为app.routing.ts的文件,并设置访问这些组件所需的基本路由,如下所示:

最后,将新创建的AppRoutingModule包含在Browser.Module之后,在imports部分下的app.module.ts中找到的主应用程序模块中。 还可以在我们的app.component.html中添加链接,以根据路线定义导航到这些组件中的每个组件。

有了这些更改,就可以开始我们的基本应用程序了。 从项目的根目录运行命令npm start并导航到http:// localhost:4200以查看您的应用程序正在运行。

服务器端渲染设置

SSR设置可以分为三个高级阶段:

  1. 使应用程序SSR兼容
  2. 生成可以在SSR和非SSR模式下工作的应用程序捆绑包
  3. 服务于SSR模板的服务器端逻辑。

使应用程序SSR兼容

对于第一阶段,我们需要安装所有必需的依赖项以启用SSR,如下所示:

npm i -S @angular/platform-server @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader

接下来,让我们创建服务器模块,它是应用程序模块的包装。 到目前为止,我们仅使用AppModule在客户端上引导我们的应用程序:

在此阶段,我们仅导入ServerModule和ModuleMapLoaderModule并定义要引导的组件。 当我们尝试在服务器上访问和呈现应用程序时,这两个导入的模块都将非常有用。

由于我们已经导入了AppModule,该AppModule导入/声明/导出/提供应用程序所需的其他所有内容,因此我们仅根据其对服务器端渲染的需求在此模块中提供服务。

当我们的应用程序在客户端上引导时,它通常会执行以下在main.ts中定义的初始化步骤

platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.error(err));

由于我们希望现在处理的事情有所不同,因此我们需要在服务器上渲染时执行服务器模块。 为方便起见,我们在main.ts旁边创建一个main.server.ts,然后从中导出新的AppServerModule。 这主要是为了使SSR的更改尽可能与客户端保持一致。

接下来,我们需要一种方法来告诉Angular如何编译和使用这些更改。 我们可以通过扩展现有的tsconfig.json文件并仅覆盖entryModule以指向我们的新服务器模块来做到这一点。

最后但并非最不重要的一点是,我们需要告知客户端主模块,它必须使用AppModule中的以下更改从服务器端呈现的应用程序过渡到客户端呈现的应用程序:

BrowserModule.withServerTransition({ appId: 'ng-ssr' }),

现在,我们不再用像现在那样仅提供BrowserModule的方式,而是使用ServerTransition调用它,并传递一个唯一的应用程序ID来指示它是什么应用程序(这需要与下面列出的angular.json文件中的项目名称匹配)。 一旦客户端引导应用程序,这将从页面中删除服务器端渲染期间应用的所有CSS。

完成这些更改后,我们的代码现在可以与客户端和服务器端设置一起使用。 现在,我们可以编译并生成可以从我们的新服务器提供的正确分发包。

生成应用程序包

在下一阶段,我们将需要对当前应用程序配置进行一些调整,以适应在上一阶段中创建的新文件。 当前,当我们运行build命令时,所有打包文件都直接放置在dist文件夹中。

但是,我们现在需要分别保留客户端和服务器端入口模块的最终应用程序捆绑包。 我们这样做是因为我们的服务器端捆绑包具有不同的入口点,并且将比客户端更精简,因为与客户端相比,它只需要功能的一小部分。

为了进行更改,我们需要查看我们的angular.json文件,其中包含应用程序的所有配置架构。 在angular.json中,我们有一个名为projects的条目(是的,Angular 6+支持同一存储库中的多个项目),其中包含ng-ssr项目的条目。

在ng-ssr项目下,我们可以根据执行的操作正确看到不同的目标。 其中一项是build,我们将首先对其进行修改,以确保生成的分发代码位于/ dist / browser下,而不是其默认位置。 要进行此更改,请在build.options下更新outputPath。

"architect": { "build": { "options": { "outputPath": "dist/browser",

这确保了当我们运行常规的ng build命令时,生成的包位于dist / browser文件夹下。

为了在服务器上运行启用SSR的代码,我们需要使用服务器模块作为入口点来生成分发包。 为此,我们需要在ng-ssr项目中列出的其他目标旁边添加一个名为server的新目标。

在选项下,我们列出了tsConfig,它指向我们先前创建的文件以及希望编译后的代码驻留的新outputPath。 我们还用main属性列出了需要在服务器端执行的主文件路径。

要生成客户端特定的dist文件夹,我们可以简单地运行ng build --prod命令,该命令现在将放置在我们定义的新路径下。 为了将代码编译为在服务器上执行,我们需要针对要使用的特定目标运行ng run命令。 在这种情况下,将要执行ng-ssr:server。

要注意的一件事是,由于Angular 6.x一切都是基于Angular的原理图,因此Angular CLI会运行这些原理图,除了一些默认的原理图,例如build,test,serv等。如上所示传递其他参数。 这就是@ angular-devkit / build-angular在上面指定的构建器中的角色。

为客户端构建和服务器构建生成的代码之间的一个重要区别是,不再需要很多特定于客户端的代码,例如基本index.html文件,polyfills都可以忽略,因为现在我们只渲染直接在服务器上安装应用程序,然后将其修补到视图上,然后再返回。

为简化起见,我们可以使用新脚本更新package.json文件,以根据需要生成dist文件:

"build:ssr": "npm run build:client && npm run build:server", "build:client": "ng build --prod", "build:server": "ng run ng-ssr:server",

服务器端逻辑

最后阶段,即阶段3,是我们编写Express服务器以呈现和提供分发包中文件的阶段。 我们还将使用TypeScript对该文件进行编码,因此要对其进行编译,我们需要一个单独的mini webpack配置文件, 为简便起见 ,我将其跳过。 我们服务器的第一次迭代类似于在Angular文档中有关通用渲染的内容。 将此文件放在项目的根目录。

该文件最重要的部分是从第27–32行设置应用程序引擎。 以下是Angular文档的摘录,其中对此进行了解释:

ngExpressEngine是通用的renderModuleFactory函数的包装,该函数将客户端的请求转换为服务器呈现HTML页面。 您将在适合您的服务器堆栈的模板引擎中调用该函数。
第一个参数是AppServerModule。 它是通用服务器端渲染器和您的应用程序之间的桥梁。
第二个参数是extraProviders。 它是可选的Angular依赖项注入提供程序,在此服务器上运行时适用。
当您的应用需要仅由当前运行的服务器实例确定的信息时,可以提供extraProviders。

我们包含在服务器模块中的两个模块可帮助交付AppServerModuleNgFactory(这是我们模块和LAZY_MODULE_MAP的机器可理解的翻译),它实际上是在初始化而非运行时提供所有延迟加载的模块。 在这里查看更多详细信息。

现在,我们已经准备好通过完整的服务器端渲染支持来编译和提供应用程序中的更改。 但是在执行此操作之前,让我们还添加一个API调用,以从API服务器获取数据并将其呈现到我们的模板中。

使用SSR处理API调用

由于没有API调用就无法完成任何应用程序,因此我们还将实现一个示例api调用以从https://jsonplaceholder.typicode.com/posts提取数据并将其显示在UI上。 这也将帮助我们了解如何在将响应返回给客户端之前,在服务器端进行API调用并将其呈现为模板。

要启用HTTP调用,我们首先需要在AppModule中导入HttpClientModule,以便可以进行API调用。 然后,将HttpClient包含在我们选择的组件中,以分派实际的API请求。

非SSR模式下的Http呼叫

这在本地开发期间特别有用。 由于我们正在向第三方服务器发出请求,因此我们可以轻松地通过代理进行处理,只需添加代理配置即可在Angular中启用该代理。 由于api服务器的调用没有/ api前缀,因此我们将在代理之前将其临时添加并删除,以便我们可以区分API调用和UI状态更改(以确保SSR和非SSR模式之间的一致性) )。

对于代理配置,我们在项目的根目录下创建一个JSON文件,如下所示:

注意上面的pathRewrite选项,它指示我们正在剥离将添加到请求中的/ api前缀。

为了启用代理,我们需要将package.json中的启动脚本更改为以下内容:

"start": "ng serve --proxy-config proxy.conf.json",

现在,我们可以在任何组件中添加逻辑以获取数据,以下是HomeComponent的示例:

并在模板上显示这些帖子:

SSR模式下的Http呼叫

在SSR模式下,以两种方式触发API调用:

  1. 最初在服务器上渲染模板时即加载组件
  2. 当模板在客户端上呈现时(即,从服务器呈现的模板过渡出来时)

上面列出的两种方法之间的唯一区别是API调用的触发和处理方式。

由于我们直接从服务器上的模板进行API调用,因此在发出请求时,我们需要提供一个绝对URL。

在下一阶段,由于我们的模板从客户端进行API调用,因此我们可以捕获Web服务器上的请求并将其代理到API服务器,从而绕过CORS限制。

对于#1,API是直接从服务器调用的,我们需要将应用程序更新为能够直接与API服务器通信,我们可以通过在服务器模块中传入新的提供程序APP_BASE_HREF以及API的基本URL值来做到这一点。服务器,然后在我们的API调用中使用它,如下所示:

然后,我们可以在HomeComponent中注入并使用它,如下所示:

我们正在检查是否只有log语句具有PLATFORM_ID,可以在必要时将其删除。

上面的URL构造就是神奇的地方。 在服务器上,当我们直接从模板发出请求时,我们跳过/ api前缀,而当从客户端执行请求时,我们将其附加并让我们的Express服务器为我们捕获,修改和代理请求。

由于API调用将在客户端和服务器上相隔几秒钟,因此强烈建议您实施某种形式的缓存,以使API加载更快,或者使用状态存储(例如ngrx)

对于#2,当客户端版本将服务器端呈现的模板移出时,我们需要更新服务器的工作方式。 现在,我们需要将来自Web服务器的请求代理到外部API服务器。 为此,我们可以通过Express传递请求。 我们可以使用任何HTTP请求库(例如本例中的请求)来简化管道:

npm i -S request

然后我们替换现有的API逻辑,如下所示在server.ts文件中:

这将捕获所有以前缀/ api开头的请求,然后将其从URL中删除,然后再将请求转发到我们的API服务器。

建立并运行

在启动服务器之前,最后要做的就是编译它(因为我们已经用打字稿编写了它)。 合并所有更改后,package.json文件的scripts部分的最终状态如下:

要立即构建和服务项目,请运行以下命令:

npm run build:ssr npm run serve:ssr

由于已经添加了日志语句,因此我们应该能够在服务器上看到如下所示的日志:

在浏览器上:

我们可以看到,在渲染模板并将其返回给客户端一秒钟后,客户端从SSR提供的模板过渡到常规应用程序,并重新渲染了路由。

路线之间的所有后续导航都将在客户端中执行,并且仅当执行了整页重新加载时才会在服务器上呈现。

该项目整个代码库可以发现在这里 ,可以找到具体的变化对SSR 这里

React应用程序中的SSR

在React应用程序中,对于简单项目,设置非常简单。 但是,棘手的部分是处理我们可能最终在项目中使用的多个库。 如果希望在使用不兼容库的服务器端呈现路由,我们可能会遇到潜在的问题。 幸运的是,大多数常用和常用的库都提供了SSR功能,因此在大多数情况下我们应该都可以。 为了简单起见,下面的示例应用程序中不会包含很多库。

与Angular部分类似,让我们列出必须进行的更改,以便我们的应用程序与SSR兼容:

  1. 现在,我们需要一个服务器-而不是使用NGINX之类的东西来服务构建文件夹,我们现在将使用服务器(在本示例中为NodeJS / Express)来启用SSR。
  2. 如果我们想显示经过SSR处理的API数据,则必须使用Redux(或类似方法)
  3. 有条件检查以避免重新加载在SSR期间在服务器上加载并在存储中更新的数据
  4. 对组件的API调用的静态声明有助于SSR
基本应用程序设置

与Angular应用程序类似,我们首先使用create-react-app创建react应用程序,其中包括一些基本路线。

我们将使用create-react-app创建应用程序。 让我们开始:

create-react-app react-ssr cd react-ssr

让我们添加必要的库以启用路由

npm i -S react-router react-router-dom

接下来,让我们设置我们希望随每条路线加载的基本组件,在此阶段,它们看起来都相似,如下所示:

让我们还创建一个路由文件,该文件可以读取并显示在App.js中的基本组件上。 如果使用诸如react-helmet之类的文件,也可以在此文件中提供其他信息。

更新App.js以显示上面列出的路由:

现在可以启动该应用程序,并且我们可以导航到上面定义的所有路线。

服务器端渲染设置

与Angular相比(至少在没有要进行的API调用时),在React中使应用程序SSR准备就绪所需的设置非常简单。 不需要额外的代码或特殊的爵士乐就可以在服务器上渲染应用程序。

我们需要的是一台服务器(在本例中为Express),该服务器可以提供静态文件并了解用户尝试访问的路由,将该路由设置为应用程序位置,然后呈现应用程序。 如果我们需要静态上下文形式的其他上下文 ,我们也可以传递它。 所有这些都是由React-router库以StaticRouter的形式提供的,与通常在客户端使用的BrowserRouter相反。

让我们创建文件夹服务器并向其中添加以下server.js文件,其中包含3种不同的中间件,用于加载静态资源和处理路由逻辑:

由于涉及到一些JSX,我们将安装并使用@ babel / core,@ babel / cli,@ babel / preset-env和@ babel / preset-react,以便节点可以理解我们的server.js文件:

npm i -S express @babel/core @babel/cli @babel/preset-env @babel/preset-react

然后,要编译此server.js文件,我们可以内联调用一个小的babel脚本,如下所示:

babel server/server.js --out-file server/index.js --presets=@babel/env,@babel/react

成功编译服务器后,我们现在可以运行我们的应用程序了。 但是在执行此操作之前,在服务器端渲染模板时,我们将需要忽略组件的样式,但是这些样式将从生成的main.css文件中应用,并将其放置在build文件夹中。我们创建的最终发行版。 要忽略这些样式,我们可以使用ignore-styles npm插件。

忽略样式意味着在服务器最初返回模板时,不会对其进行样式设置,这导致页面在客户端上呈现后闪烁,目前,我们将忽略此问题,但是可以修复此问题。从这个非常简单的例子可以看出使用webpack和同构样式加载器

由于我们已编译的服务器文件仍在尝试访问App模块,因此我们需要编译代码,以便可以使用@ babel / register插件并提供必要的预设以在运行时进行编译。

将所有内容放在一起,然后启动服务器。 我们可以在项目的根目录使用以下标记为start.js的脚本。

现在,我们可以将该调用添加到脚本下的package.json中,以使我们更轻松。

使用SSR处理API调用

在运行和测试更改之前,让我们还添加一个简单的API调用,该调用从http://jsonplaceholder.typicode.com/posts检索数据,就像前面的Angular示例一样。

要进行API调用,我们将使用Axios npm软件包。 通常,在UI上呈现API响应。 一旦使用componentDidMount生命周期挂钩安装了组件,我们将进行API调用,然后调用setState将API调用的响应存储到组件状态。 此setState调用再次触发render方法,该方法现在可以访问更新后的状态。

由于我们正在服务器上编译组件,而实际上没有将其安装到实际的DOM上,因此我们不能使用前面讨论的方法,因为它不会触发服务器端的componentDidMount生命周期。 但是,componentWillMount在服务器端和客户端都被触发。 不幸的是,不能使用此方法,因为它是一种反模式,会在componentWillMount生命周期挂钩中引起任何副作用。

解决此问题的最简单方法是使用状态存储,例如Redux。

让我们首先谈谈它在客户端如何工作:

  1. 组件加载并调用componentDidMount
  2. 我们检查存储中是否已存在数据,如果不存在,则进行API调用并更新存储
  3. 渲染组件

服务器端的相同流程将有所不同:

  1. 确定用户正在加载的路线
  2. 确定与此路线关联的组件
  3. 如果附加了静态数据获取方法来检索数据,请执行此操作。
  4. 使用检索到的数据更新商店
  5. 使用商店数据渲染模板
  6. 返回模板和已在服务器上创建的存储。

在进行任何代码更改之前,让我们添加所有必需的库。

npm i -S axios redux redux-thunk request

非SSR模式下的Http呼叫

因为我们希望我们的应用程序以SSR模式(在生产中)和非SSR模式(用于本地开发)运行。 我们可以简单地将一个代理添加到package.json文件中以代理我们的请求(类似于Angular应用程序),如下所示:

现在,我们可以在componentDidMount生命周期挂钩中针对我们选择的任何组件进行API调用。

这在仅客户端模式下可以正常工作。 由于我们还需要考虑SSR模式,因此我们必须修改组件以使用Redux存储来保存可在服务器和浏览器上使用的帖子。

为此,我们需要首先创建我们的Redux存储,在这种情况下,它非常精简,如下所示:

在上述情况下,操作,reduce和API调用都位于同一文件中,以提高可读性,但您可能希望在项目中将其分开。

现在,我们可以从Home组件调用fetchPosts方法。

该文件中唯一有特色的是在类上的API方法的静态声明,我们将在下一部分中讨论对此的需要。

另一个奇怪的怪癖是我们不再像通常那样在根元素上定义状态:
state = { something: '' };
这给出了运行时错误,要求我们安装@ babel / plugin-proposal-class-properties。 相反,我们需要将其包装在构造函数中,如下所示:
constructor(props) { super(props); this.state = { something: '' }; }

将所有这些联系在一起的最后更改是在应用程序在浏览器上呈现时创建商店:

SSR模式下的Http呼叫

要在服务器上启用API调用,我们可以轻松地基于为客户端设置的更改。 唯一需要修改的是在客户端上呈现应用程序时将有效的默认状态(即,已加载到服务器上的数据)传递给商店。

为了计算服务器端存储的准确状态,我们需要首先确定需要调度的动作(可以使用必要的数据更新存储)。 更改后的服务器如下所示:

在我们的示例中,我们使用一个API调用来填充主模板。 如果我们有多个,我们可以简单地将所有请求添加到serverSideFetch方法中,这将确保在呈现模板之前将所有数据加载到商店中。

需要注意的另一件事是,最后,在渲染模板时,我们还将商店的当前状态添加到window.REDUX_DATA。 这样,当应用程序在客户端上初始化时,我们可以简单地读取window对象上的值并将其传递到我们的商店创建中。 这样,我们就可以避免在客户机上重复调用服务器上呈现的相同数据。

另外,我们需要修改公共文件夹中的基本index.html文件,以添加将在服务器上替换的REDUX_DATA的占位符:

建立并运行

完成这些更改后,我们现在可以使用package.json中定义的脚本来运行我们的应用程序。

npm run build:ssr npm run serve:ssr

在客户端上,我们可以看到触发的初始请求随服务器上加载的数据一起返回。 在服务器上,我们看到按预期方式打印的日志语句。

上面显示的示例的完整代码库在此处提供

比较与结论

尽管React和Angular在核心原理和构建块方面都大不相同,但两者之间的共同点是启用SSR是一项容易的任务。

与React相比,在Angular应用程序中添加的样板代码数量更多,因为添加了封装浏览器主模块的服务器特定模块会包含几个不同的文件。

由于Angular包装了Express并为我们提供了暴露易于使用的方法的ExpressEngine(至少适用于基于NodeJS的后端),因此Angular和React的SSR的服务器端逻辑非常相似。

在Angular和React应用程序中,我们都需要在服务器端进行编译,尽管如果使用香草JavaScript而不是TypeScript,可以在Angular中跳过该步骤,但是对于React应用程序,由于在服务器端代码中引入了JSX,因此最终使用babel 。

由于React中的许多功能来自其他库(路由器,redux等),因此我们需要在服务器端显式集成它们,而Angular应用程序仅需要访问已编译的主模块工厂。

在Angular和React应用程序中,进行API调用的复杂度几乎相同。 但是,只有在我们的React应用程序非常基础的情况下,这才是正确的,大多数项目的确需要某种形式的状态存储,并且其复杂性不断提高,因此在这种情况下使用Redux绝对不是负面的。

上面显示的两个示例的代码库都可以在这里找到: AngularReact

如果您喜欢这个博客,请务必给它一些鼓掌, 阅读更多 或在 LinkedIn 上关注我

翻译自: https://hackernoon.com/a-comparison-of-server-side-rendering-in-react-and-angular-applications-fb95285fb716

react 服务器端渲染

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值