前端 延迟加载和捆绑拆分_跨应用程序捆绑微前端的另一种方法

前端 延迟加载和捆绑拆分

Bundling Application code for dist versions has always been a challenge, especially in case you want to share code with several Apps or load multiple Apps on one Page.

将发行版的应用程序代码捆绑在一起一直是一个挑战,尤其是在要与多个应用程序共享代码或在一个页面上加载多个应用程序的情况下。

内容 (Content)

  1. Introduction

    介绍
  2. The “data flows downwards” paradigm

    “数据向下流动”范式
  3. How does importing JS modules directly into the Browser work?

    将JS模块直接导入浏览器如何工作?
  4. Is the Worker Scope supported as well?

    是否也支持工人范围?
  5. How can we achieve the same behaviour for our dist versions?

    我们如何才能为dist版本实现相同的行为?
  6. A Webpack Love Story

    Webpack爱情故事
  7. What is the tradeoff?

    权衡是什么?
  8. Introducing the neo.mjs v1.4 Release

    介绍neo.mjs v1.4版本
  9. Can we achieve the same without using neo.mjs?

    不使用neo.mjs就能达到相同的效果吗?
  10. What can we do with this now?

    现在我们该怎么办?

  11. What is coming next?

    接下来会发生什么?
  12. What is happening on the neo.mjs related job market?

    与neo.mjs相关的工作市场正在发生什么?

1.简介 (1. Introduction)

This article is not going to cover what “Micro Frontends” are in detail. There are very good blog posts out there on this topic, just google the term in case you have not heard about it yet. An in depth knowledge is not required to follow this article.

本文将不会详细介绍“微型前端”。 关于此主题的博客文章很好,如果您还没有听说过,请用谷歌这个词。 有深入了解,不需要遵循本文。

What we are going to talk about are the main problems which made the Micro Frontends topic popular in the first place:

我们要谈论的是使“微前端”主题首先流行的主要问题:

  1. How to create modular & scalable Javascript code

    如何创建模块化和可扩展的Javascript代码
  2. How to share code (modules) across Apps

    如何跨应用程序共享代码(模块)
  3. How to load multiple Apps on one page with close to 0 overhead

    如何在一页上加载接近0的多个应用程序
  4. How to bundle SharedWorkers driven multi Browser Window Apps

    如何捆绑SharedWorkers驱动的多个浏览器窗口应用程序

In case you have been following my recent blog posts, you have most likely seen the multi Browser Window Covid App:

如果您一直关注我最近的博客文章,则很可能已经看到了多浏览器窗口Covid应用程序:

Simply put: multiple main threads connect to a shared Application worker, which makes it possible to move Component trees around different Browser Windows. We can even move those trees around without the need to create new Javascript instances.

简而言之:多个主线程连接到共享的应用程序工作程序,这使得可以在不同的浏览器Windows中移动组件树。 我们甚至可以移动这些树,而无需创建新的Javascript实例。

When I started using SharedWorkers, the first Demo Apps did run inside the dist modes (including Firefox). However, Apps using multiple main threads did not. With the “cross Apps split chunks” concept, this is now possible and this is definitely a major breakthrough. Since this concept can solve a lot of client side architecture related problems, I would like to share my knowledge with you.

当我开始使用SharedWorkers时,第一个演示应用程序确实在dist模式(包括Firefox)中运行。 但是,使用多个主线程的Apps没有。 借助“跨应用程序拆分块”的概念,这已经成为可能,这绝对是一项重大突破。 由于此概念可以解决许多与客户端架构相关的问题,因此我想与您分享我的知识。

2.“数据向下流动”范式 (2. The “data flows downwards” paradigm)

Like in real life, parents are very much aware of their children and have (in theory) the ability to change them. Children however have not really a clue who their parents are and are not allowed to do so.

就像现实生活中一样,父母非常了解自己的孩子,并且(理论上)具有改变孩子的能力。 但是,孩子们并不真正了解父母是谁,也不被允许这样做。

When it comes to creating modular code, it is super important to stick to this idea. Components can adjust their child Components as they like, and it is fine to directly call methods on child Components.

在创建模块化代码时,坚持这一想法非常重要。 组件可以根据需要调整其子组件,可以直接在子组件上调用方法。

Since we want to use child Components in different spots, they are not allowed to touch their parents. Instead, child Components fire events, which parent Components can listen to.

由于我们想在不同的地方使用子组件,因此不允许他们触摸其父母。 而是,子组件触发事件,父组件可以侦听事件。

While creating the neo.mjs framework, I am trying to stick to this design pattern as good as possible. I can strongly recommend to do the same.

在创建neo.mjs框架时,我试图尽可能地坚持这种设计模式。 我强烈建议您这样做。

3.将JS模块直接导入浏览器如何工作? (3. How does importing JS modules directly into the Browser work?)

While most Javascript libraries and Frameworks still run in nodejs and you only see the compiled output inside the Browser, there is no good reason to stick to this.

尽管大多数Javascript库和框架仍在nodejs中运行,并且您只能在浏览器中看到编译后的输出,但没有充分的理由坚持这一点。

Modern Browsers support using JS modules directly. Take a look at:

现代浏览器支持直接使用JS模块。 看一眼:

Inside your JS files, you can use static imports to fetch the dependencies and you can use dynamic imports to (optionally) lazy load other modules.

在JS文件中,可以使用静态导入来获取依赖关系,还可以使用动态导入来(可选)延迟加载其他模块。

The W3C specs are really nice and Browsers do a great job at caching JS modules. An easy example: You create Component2 & Component3 classes (inside modules), which both extend Component1. The Component1 module will only get loaded once.

W3C规范非常好,浏览器在缓存JS模块方面做得很好。 一个简单的示例:创建Component2和Component3类(在模块内部),这两个类都扩展了Component1。 Component1模块将仅加载一次。

We definitely want to achieve the same behaviour inside our dist versions!

我们绝对希望在dist版本中实现相同的行为!

I recently stumbled upon the caching with the neo.mjs entrypoints for Apps. So far they looked like this:

我最近偶然发现了Apps的neo.mjs入口点进行缓存。 到目前为止,它们看起来像这样:

Image for post

While this works perfectly fine for Single Page Apps, there is a catch when it comes to multi Browser Window Apps, where a SharedWorker fetches multiple Apps.

尽管这对于单页应用程序来说非常完美,但是当涉及到多个浏览器窗口应用程序时,有一个陷阱,即一个SharedWorker获取多个应用程序。

The first App defines Neo.onStart() which was executed after the lazy loading was done. Then you load a second App to show it (e.g. inside a new Window) and Neo.onStart() got overridden. The new method got triggered and this worked fine as well. Do a reload on the first App (Browser Window), the app.mjs module was cached, Neo.onStart() was not overridden and the framework loaded the wrong App:

第一个应用程序定义了Neo.onStart(),它在延迟加载完成后执行。 然后,您加载另一个应用程序以显示它(例如,在新窗口内),并且Neo.onStart()被覆盖。 触发了新方法,并且效果很好。 在第一个应用程序(浏览器窗口)上重新加载,已缓存app.mjs模块,未覆盖Neo.onStart(),并且框架加载了错误的应用程序:

Image for post

It was easy to fix of course. The new entry points look like this:

当然很容易修复。 新的入口点如下所示:

Image for post

We just export the method => the cached module will return the correct method.

我们只导出方法=>缓存的模块将返回正确的方法。

4.是否也支持“工人范围”? (4. Is the Worker Scope supported as well?)

When looking at the bottom of the MDN article, you will notice that using JS modules inside main threads has a very good support, while using JS modules inside Workers is still only supported in Chromium.

查看MDN文章的底部时,您会注意到在主线程中使用JS模块有很好的支持,而在Workers中使用JS模块仅在Chromium中受支持。

This is still the reason for the neo.mjs dev mode (no builds or transpilations) to only be able to run in Chrome and now Edge. Keep in mind that your App logic runs inside the App Worker.

这仍然是neo.mjs开发人员模式(无构建或编译)只能在Chrome和现在的Edge中运行的原因。 请记住,您的App逻辑在App Worker中运行。

Webkit and Firefox have tickets for supporting JS modules inside the Worker scope (which is included in the W3C specs). We should keep an eye on those. It would be nice in case you add comments there, since it is an important topic for the future of UI development.

Webkit和Firefox具有在Worker范围内支持JS模块的票证(这已包含在W3C规范中)。 我们应该留意这些。 如果您在此处添加注释,那将是很好的,因为它是UI开发未来的重要主题。

(There is a patch added inside the last comment, so there is hope)

(在最后一条评论中添加了一个补丁,因此有希望)

5.如何为dist版本实现相同的行为? (5. How can we achieve the same behaviour for our dist versions?)

So far the build entry points for neo.mjs Apps looked like this:

到目前为止,neo.mjs Apps的构建入口点看起来像这样:

Image for post

Each App dist version was a combination of the App Worker and your real App code base. This did not only act differently than the dev mode (where the App Worker lazy loads Apps), but was also very limiting when it comes to sharing code (split chunks).

每个App dist版本都是App Worker和您实际的App代码库的组合。 这不仅与开发模式(App Worker懒加载应用程序)不同,而且在共享代码(拆分块)方面也有很大的限制。

Obviously, we can import modules from e.g. a shared folder and they do end up inside the build, but this approach made it impossible to load multiple dist versions of Apps on one page.

显然,我们可以从例如共享文件夹中导入模块,并且它们确实会在构建中结束,但是这种方法使得无法在一页上加载多个dist版本的Apps。

You could of course create multiple Apps inside the app.mjs files to get them into a build at the same time, but what we really want is to separate the build outputs for multiple Apps from the App Worker and also to get split chunks across multiple Apps.

您当然可以在app.mjs文件中创建多个Apps,以同时将它们放入构建中,但我们真正想要的是将多个Apps的构建输出与App Worker分开,并且还可以在多个应用。

In case we load App1 into the App Worker and at a later point want to lazy load App2, we want the dist versions to not include duplicate code (modules). This is not only bad from a file size perspective, but it can create bugs (imagine the IdGenerator does get multiple versions).

如果我们将App1加载到App Worker中,并且在以后要延迟加载App2,我们希望dist版本不包含重复的代码(模块)。 从文件大小的角度来看,这不仅很糟糕,而且会产生错误(假设IdGenerator确实有多个版本)。

The required changes where on my todo list for quite a while, now I have finally found the time to wrap them up.

所需的更改在待办事项列表上的位置已经存在了相当长的一段时间,现在我终于找到了将它们打包的时间。

6. Webpack爱情故事 (6. A Webpack Love Story)

I was actually not sure if this concept is too far away from the original Webpack Scope (bundling Apps) and if this was possible at all.

我实际上不确定这个概念是否与原始的Webpack Scope(捆绑应用程序)相距太远,是否完全有可能。

We want to bundle a framework as well as multiple Apps inside a Worker Scope, including split chunks for pretty much everything.

我们希望将一个框架以及多个应用程序捆绑在一个工人范围内,包括用于几乎所有事情的拆分块。

With a bit more thought and time, it worked out surprisingly well. So, at this point I need to say it:

经过更多的思考和时间,它取得了令人惊讶的出色效果。 所以,在这一点上我需要说:

A big Kudos to the Webpack Team for creating such an amazing product!

Webpack团队对于创建如此令人惊叹的产品非常感激!

Ok, now it is time to get technical :)

好的,现在是时候获取技术了:)

To visualise what we want to bundle:

可视化我们要捆绑的东西:

Image for post

Our App related index.html files look like this:

我们与应用程序相关的index.html文件如下所示:

Image for post

It is very important to look at the bottom => line 22. Each index file will only include the neo.mjs Main Thread. This one does support optional main thread addons for quite a while now (those are already bundled using split chunks)

查看底部=>第22行非常重要。每个索引文件将仅包含neo.mjs主线程。 这确实支持相当长一段时间的可选主线程插件(那些已经使用拆分块捆绑在一起)

Main.mjs will create the Workers (App, Data & Vdom) for us. As soon as the Workers are ready, the App Worker will fetch the app.mjs file of your App (as mentioned in Section 3).

Main.mjs将为我们创建工作者(应用程序,数据和Vdom)。 一旦Workers准备就绪,App Worker将获取您App的app.mjs文件(如第3节所述)。

Let us take a closer look at the new logic to do this:

让我们仔细看看执行此操作的新逻辑:

https://github.com/neomjs/neo/blob/dev/src/worker/App.mjs#L84

https://github.com/neomjs/neo/blob/dev/src/worker/App.mjs#L84

Image for post

The dynamic import has to run directly inside the Browser. It does need to work for the dist versions as well (Webpack). Webpack can not be aware of our custom path(s), so I am using the strategy to import literally all /app.mjs files.

动态导入必须直接在浏览器内部运行。 它也确实需要为dist版本工作(Webpack)。 Webpack无法识别我们的自定义路径,因此我正在使用该策略从字面上导入所有/app.mjs文件。

To keep it reasonable, we do not want to include node_modules. I might need to polish the webpackExclude a bit more (still needs testing). It is supposed to do the following:

为了使其合理,我们不想包含node_modules。 我可能需要进一步完善webpackExclude(仍然需要测试)。 应该执行以下操作:

  1. When building the App Worker for the neo.mjs framework itself, we want split chunks for all possible Apps inside the apps and the examples folder

    在为neo.mjs框架本身构建App Worker时,我们希望为apps和examples文件夹内的所有可能的Apps拆分块
  2. When using neo.mjs as a node module, we only want split chunks for our own Apps (and not the examples & apps included inside the neo.mjs node_module). This part still needs testing.

    当使用neo.mjs作为节点模块时,我们只希望为自己的Apps分割块(而不是neo.mjs node_module中包含的示例和应用程序)。 这部分仍然需要测试。
  3. When building the Online Examples, we trigger the build on the neo.mjs node_module, in which case we do want to get the content (This part works fine).

    在构建在线示例时,我们在neo.mjs node_module上触发构建,在这种情况下,我们确实希望获取内容(此部分工作正常)。

Talking about the various buildScripts in detail would go off topic and convert this article into a book. However, this is the beauty of Open Source: You are very much welcome to dive into the code base on your own.

详细讨论各种buildScript可能会引起话题,并将本文转换成书。 但是,这就是开放源代码的优点:非常欢迎您自己深入研究代码库。

https://github.com/neomjs/neo/tree/dev/buildScripts

https://github.com/neomjs/neo/tree/dev/buildScripts

In case we run a dist/development build for the App Thread, the build time takes 3.148s on my machine.

如果我们为App Thread运行dist / development构建,则构建时间在我的计算机上需要3.148s。

Image for post

The screenshot only shows a fraction of the chunks. The chunks are pretty big inside this mode, but we rarely need it anyway (sticking to the real dev mode & dist/prod).

屏幕截图仅显示了一部分。 在此模式下,这些块相当大,但无论如何我们很少需要它(坚持使用真正的dev模式和dist / prod)。

In case we open the Covid App (non SharedWorkers) inside the Browser, we now get:

如果我们在浏览器中打开Covid应用程序(非SharedWorkers),我们将得到:

Image for post

The important part is the console (dev tools). You can see the central App Worker chunk and split chunks across our different Demo Apps.

重要的部分是控制台(开发工具)。 您可以看到中心的App Worker块以及在我们不同的Demo Apps中拆分的块。

Let us do a dist/production build next:

接下来让我们进行dist / production构建:

Image for post

The prod build is a bit faster: 2.191s on my machine.

产品构建速度更快:我的机器上为2.191s。

The chunk file sizes are way smaller now. We do get numeric chunk names (I am mapping them into a chunks folder to keep the top level clean), which reduces the mapping overhead as well as tree shaking across our Apps.

该块文件大小现在的方式更小。 我们确实获得了数字块名称(我将它们映射到块文件夹中以保持顶层整洁),这减少了映射开销以及整个Apps的摇晃树。

Looking at it inside the Browser:

在浏览器中查看它:

Image for post

You will notice the numeric names now.

您现在会注意到数字名称。

7.什么是权衡? (7. What is the tradeoff?)

Like all things in life, changes come at a cost.

就像生活中的所有事物一样,变化是有代价的。

The old build strategy allowed us to parse a single entry point, so the build speed was faster.

旧的构建策略允许我们解析单个入口点,因此构建速度更快。

Now, when we want to build one of our Apps, we need to parse all Apps inside the workspace to get the split chunks.

现在,当我们要构建我们的一个应用程序时,我们需要解析工作区中的所有应用程序以获取拆分的块。

The build times are still very reasonable, but I polished this concept a bit more.

构建时间仍然非常合理,但是我对该概念进行了进一步完善。

Running buildThreads => app

运行buildThreads => app

will parse all /app.mjs files and create all index.html files.

将解析所有/app.mjs文件并创建所有index.html文件。

Running buildMyApps => Covid

运行buildMyApps => Covid

will still parse all /app.mjs files, but at least only re-generate the index file of your selected App(s).

仍会解析所有/app.mjs文件,但至少仅重新生成所选应用程序的索引文件。

Time wise, it does make not much of a difference though: 2.123s

在时间上,它并没有多大区别:2.123s

Image for post

8.介绍neo.mjs v1.4版本 (8. Introducing the neo.mjs v1.4 Release)

My original plan was to finish the Calendar implementation for v1.4, but the new build processes felt more important and are definitely worth a minor release on their own. The Calendar is pushed into v1.5 now. The drag&drop implementation is already in place.

我最初的计划是完成v1.4的Calendar实施,但是新的构建过程显得更加重要,绝对值得自己稍作发行。 日历现在已推送至v1.5。 拖放实现已经到位。

With the Cross Apps Split Chunks, we can now run our SharedWorkers driven Apps inside the dist modes.

借助Cross Apps Split Chunks,我们现在可以在dist模式下运行由SharedWorkers驱动的应用程序。

Image for post

This is the Multi Window Covid App in Firefox. Please keep in mind that Webkit / Safari does still not support SharedWorkers at all. The related ticket is here:

这是Firefox中的Multi Window Covid应用程序。 请记住,Webkit / Safari仍然完全不支持SharedWorkers。 相关票证在这里:

Please do add some weight on it. It would be sad to see Safari becoming the next IE6 and all iOS related Browsers have to use it.

请增加一些重量。 这将是可悲的Safari浏览器成为下一个IE6和所有iOS相关的浏览器必须使用它。

This article was not just theory-crafting. The Online Examples are updated already and all of them are using Cross App Split chunks already:

本文不只是理论上的技巧。 在线示例已经更新,并且所有示例都已使用Cross App Split块:

Image for post

You are very welcome to dive into them (Desktop) and check the sources inside the App Worker Scope on your own:

非常欢迎您深入研究它们(桌面)并自行检查App Worker范围内的源:

https://neomjs.github.io/pages/node_modules/neo.mjs/dist/production/apps/website/index.html#mainview=examples

https://neomjs.github.io/pages/node_modules/neo.mjs/dist/production/apps/website/index.html#mainview=examples

9.如果不使用neo.mjs,我们可以达到相同的目的吗? (9. Can we achieve the same without using neo.mjs?)

Absolutely yes!

绝对没错!

You will need to create a central entry point for your Apps & lazy load the real code to give Webpack a chance to figure out the Cross Apps Split Chunks.

您将需要为您的Apps创建一个中央入口点,并延迟加载真实代码,以使Webpack有机会找出Cross Apps Split Chunks。

You will need a webpackInclude regex pattern to fetch the Apps for which you want to create Split Chunks, but this is it already.

您将需要一个webpackInclude regex模式来获取您要为其创建“拆分块”的应用程序,但是已经足够了。

10.现在我们该怎么办? (10. What can we do with this now?)

In case you happen to have use Ext JS in the past, you are probably familiar with the packages concept. In case you want to share code across Apps, you needed to put this code into specific packages which then could be compiled & lazy loaded (oh gosh, this was very slow).

如果您过去碰巧使用过Ext JS,则您可能熟悉软件包的概念。 如果您想在Apps之间共享代码,则需要将此代码放入特定的程序包中,然后可以对其进行编译和延迟加载(天哪,这非常慢)。

With the new neo.mjs frameworks, you really don’t need to think about sharing & re-using modules inside different Apps anymore.

有了新的neo.mjs框架,您真的不再需要考虑在不同Apps中共享和重用模块。

You can just load multiple Apps (dynamically) into 1 page and with the split chunks in place, there will be close to 0 overhead. This is why I mentioned “Micro Frontends” as a comparison.

您只需将多个应用程序(动态)加载到一页中,并在适当的位置分割块,将产生接近0的开销。 这就是为什么我提到“ Micro Frontends”作为比较的原因。

A framework should be flexible & allow you to create Application architectures as you like. This is possible now.

框架应该是灵活的,并允许您根据需要创建应用程序体系结构。 现在这是可能的。

However, I strongly recommend to stick to common design patterns. While you can define a module inside App1 and directly import it into App2 inside the same workspace, it feels a lot cleaner to put shared module code into its own spot (to make it clear that it is used inside multiple spots).

但是,我强烈建议您遵循常见的设计模式。 虽然您可以在App1中定义一个模块并将其直接导入同一工作区中的App2中,但是将共享模块代码放入其自己的位置上感觉要干净得多(以便清楚地在多个位置内使用它)。

11.接下来会发生什么? (11. What is coming next?)

Well, once Browsers are ready to support JS modules inside the Worker Scope on their own, I definitely want to create a new build mode without bundling. Just minifying each module file on its own. While this is not perfect for tree shaking, it would feel more the same compared to the dev mode. It does not really make sense before Browsers are ready, since we do not want to re-write the Harmony imports logic.

好吧,一旦浏览器准备好独立支持Worker Scope内的JS模块,我肯定想创建一个新的构建模式而不进行捆绑。 只需最小化每个模块文件即可。 虽然这对于摇树不是很完美,但与dev模式相比,感觉会更加相同。 在浏览器准备就绪之前,这没有任何意义,因为我们不想重新编写Harmony导入逻辑。

On the short term: As mentioned, the Calendar implementation is still at the top of my todo list. We got feature requests for menus, toast messages, a buffered grid (this is a huge one). There is work on the core level left as well: improving the delta updates performance and creating benchmarks for the config system (e.g. the cloning part).

短期而言:如前所述,“日历”实现仍然是我的待办事项列表的顶部。 我们收到了菜单,吐司消息,缓冲网格(这是一个巨大的功能)的功能请求。 还需要在核心级别上做一些工作:提高增量更新性能并为配置系统创建基准(例如克隆部分)。

With the drag&drop logic in place, I definitely want to create a demo where we can drag in app dialogs from one Browser Window into another one:

有了拖放逻辑,我肯定要创建一个演示,在其中我们可以将应用程序对话框中的内容从一个浏览器窗口拖动到另一个窗口:

Image for post

This will get pretty exciting for Multi Screen Apps in general and is definitely worth a new blog post once ready.

一般而言,这对于Multi Screen Apps而言将非常令人兴奋,并且一旦准备就绪,绝对值得撰写新的博客文章。

12.与neo.mjs相关的工作市场正在发生什么? (12. What is happening on the neo.mjs related job market?)

I am very excited to announce that there are some big client projects starting right now which are based on using neo.mjs.

我非常高兴地宣布,现在有一些基于使用neo.mjs的大客户项目正在启动。

It is still my main goal to set up a Professional Services team soon, which can help clients on their way to successfully get neo.mjs Apps into prod.

我的主要目标仍然是尽快建立一个专业服务团队,该团队可以帮助客户成功地将neo.mjs Apps纳入产品。

I got asked “Tobi, can you recommend me for client projects?” recently quite a lot. Clients will ask: “What have you done using neo.mjs so far?”.

我被问到“ Tobi,您能推荐我参加客户项目吗?” 最近很多。 客户会问:“到目前为止,您使用neo.mjs做过什么?”。

Right now, there are still very few developers who are up to speed. The demand for neo.mjs devs (the market in general) is growing.

目前,仍然只有极少数的开发人员愿意跟上速度。 对neo.mjs开发人员(通常是市场)的需求正在增长。

The early bird wins. I can just strongly recommend you to dive into it and get up to speed.

早起的鸟获胜。 我可以强烈建议您深入了解它,并快速入门。

You are more than welcome to contribute to the code base:

非常欢迎您为代码库做出贡献:

You could just create an own portfolio (Demo Apps, custom components) as well. Completely up to you.

您也可以创建自己的投资组合(Demo Apps,自定义组件)。 完全取决于您。

Without having any neo.mjs related code out there, it is simply impossible for me to recommend you for client projects.

如果没有任何与neo.mjs相关的代码,那么我根本不可能向您推荐客户项目。

Best regards & happy coding,Tobias

致以诚挚的问候,Tobias

翻译自: https://medium.com/swlh/cross-app-bundling-a-different-approach-for-micro-frontends-e4f212b6a9a

前端 延迟加载和捆绑拆分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值