微前端乾坤方案

微前端乾坤方案


了解乾坤

官方文档

介绍

qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

qiankun 的核心设计理念

  • 🥄 简单

    由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。

  • 🍡 解耦/技术栈无关

    微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。

特性

  • 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
  • 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  • 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  • 🛡​ 样式隔离,确保微应用之间样式互相不干扰。
  • 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  • ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
  • 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。

qiankun 在 single-spa 的基础上都增加了啥?

以下是 qiankun 提供的特性:

  • 实现了子应用的加载,在原有 single-spa 的 JS Entry 基础上再提供了 HTML Entry
  • 样式和 JS 隔离
  • 更多的生命周期:beforeMount, afterMount, beforeUnmount, afterUnmount
  • 子应用预加载
  • umi 插件
  • 全局状态管理
  • 全局错误处理



qiankun 的使用

主应用

0、安装 qiankun
$ yarn add qiankun # 或者 npm i qiankun -S
1、引入
import {
   
    registerMicroApps, start } from 'qiankun';
2-1、注册子应用,并启动
registerMicroApps([
  {
   
   
    name: 'react app', // app name registered
    entry: '//localhost:7100',
    container: '#yourContainer',
    activeRule: '/yourActiveRule',
  },
  {
   
   
    name: 'vue app',
    entry: {
   
    scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);

// 启动 qiankun
start();

备注

自动注册模式:当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。

2-2、手动加载子应用

如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用。

import {
   
    loadMicroApp } from 'qiankun';

loadMicroApp({
   
   
  name: 'app',
  entry: '//localhost:7100',
  container: '#yourContainer',
});



子应用改造

微应用不需要额外安装任何其他依赖即可接入到 qiankun 主应用。

1、前提

子应用的资源和接口的请求都在主域名发起,所以会有跨域问题,子应用必须做cors 设置

// webpack

const {
   
    name } = require('./package');

module.exports = {
   
   
  devServer: (config) => {
   
   
    // 因为内部请求都是 fetch 来请求资源,所以子应用必须允许跨域
    config.headers = {
   
   
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Credentials': true,
      'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
      'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
      'Content-Type': 'application/json; charset=utf-8',
    };
    return config;
  },
};

2、生命周期改造

微应用需要在应用的入口文件 (通常就是你配置的 webpack 的 entry js) 导出 bootstrapmountunmountupdate 四个生命周期钩子,以供主应用在适当的时机调用。

具体操作可以参考下面示例
步骤一:在 src 目录新增 public-path.js,用于修改运行时的 publicPath
if (window.__POWERED_BY_QIANKUN__) {
   
   
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

备注

什么是运行时的 publicPath ?
运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。

步骤二:修改入口文件

主要改动:

  1. 引入 public-path.js
  2. export 生命周期函数
    • 将子应用路由的创建、实例的创建渲染挂载到mount函数上
    • 将实例的销毁挂载到unmount
// vue 2

// 入口文件 `main.js` 修改,为了避免根 id `#app` 与其他的 DOM 冲突,需要限制查找范围。

import './public-path';
import Vue from 'vue';
import App from './App.vue';
import VueRouter from 'vue-router';
import routes from './router';
import store from './store';

Vue.config.productionTip = false;

let router = null;
let instance = null;

function render(props = {
    
    }) {
   
   
  const {
   
    container } = props;
  router = new VueRouter({
   
   
    // histroy模式的路由需要设置base。app-vue 根据项目名称来定
    base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
    mode: 'history',
    // hash模式则不需要设置base
    routes,
  });

  instance = new Vue({
   
   
    router,
    store,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
   
   
  render();
}

export async function bootstrap() {
   
   
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
   
   
  console.log('[vue] props from main framework', props);
  render(props);
}

export async function unmount() {
   
   
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}
// react 16

// 以 `create react app` 生成的 `react 16` 项目为例,搭配 `react-router-dom` 5.x。
// 入口文件 `index.js` 修改,为了避免根 id `#root` 与其他的 DOM 冲突,需要限制查找范围。
// 这里需要特别注意的是,通过 ReactDOM.render 挂载子应用时,需要保证每次子应用加载都应使用一个新的路由实例。

import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

function render(props) {
   
   
  const {
   
    container } = props;
  ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}

if (!window.__POWERED_BY_QIANKUN__) {
   
   
  render({
   
   });
}

export async function bootstrap() {
   
   
  console.log('[react16] react app bootstraped');
}

export async function mount(props) {
   
   
  console.log('[react16] props from main framework', props);
  render(props);
}

export async function unmount(props) {
   
   
  const {
   
    container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}

备注

  • qiankun 基于 single-spa,所以你可以在这里找到更多关于微应用生命周期相关的文档说明。
  • 无 webpack 等构建工具的应用接入方式请见这里

注意点

  • 容器名

    • 修改 index.html 中项目初始化容器的根 id,不要使用 #app#root,避免与其他的项目冲突,建议换成 项目name 的驼峰写法
  • 微应用建议使用 history 路由模式。

  • 路由模式为 history 时,同时需要设置路由 base,值和它的 activeRule 是一样的:

    • vue:

      router = new VueRouter({
              
              
        mode: 'history',
        base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
      });
      
    • react:

      <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
      

3、构建工具配置

微应用分为有 webpack 构建和无 webpack 构建项目。

注意
qiankun v2 及以下版本只支持 webpack,并不支持 vite!!!

下面以 webpack 为构建工具的微应用为例(主要是指 Vue、React)的配置说明

配置点说明

构建工具的配置主要有两个:

  1. 允许跨域
  2. 打包成 umd 格式

为什么要打包成 umd 格式呢?是为了让 qiankun 拿到其 export 的生命周期函数。我们可以看下其打包后的 app.js 就知道了:
在这里插入图片描述

配置示例

微应用的打包工具需要增加如下配置:

react

修改 webpack 配置

安装插件 @rescripts/cli,当然也可以选择其他的插件,例如 react-app-rewired

npm i -D @rescripts/cli

根目录新增 .rescriptsrc.js

// webpack

const {
   
    name } = require('./package');

module.exports = {
   
   
  webpack: (config) => {
   
   
    config.output.library = `${
     
     name}-[name]`;
    config.output.libraryTarget = 'umd';
    // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
    config.output.jsonpFunction = `webpackJsonp_${
     
     name}`;
    config.output.globalObject = 'window';

    return config;
  },

  devServer: (_) => {
   
   
    const config = _;

    config.headers = {
   
   
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};

修改 package.json

-   "start": "react-scripts start",
+   "start": "rescripts start",
-   "build": "react-scripts build",
+   "build": "rescripts build",
-   "test": "react-scripts test",
+   "test": "rescripts test",
-   "eject": "react-scripts eject"
vue
// vue.config.js

const {
   
    name } = require('./package');

module.exports = {
   
   
  devServer: {
   
   
    headers: {
   
   
      'Access-Control-Allow-Credentials': true,
      'Access-Control-Allow-Origin': req.headers.origin || '*',
      'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
      'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
      'Content-Type': 'application/json; charset=utf-8',
    },
  },
  // 自定义webpack配置
  configureWebpack: {
   
   
    output: {
   
   
      library: `${
     
     name}-[name]`, // 微应用的包名,这里与主应用中注册的微应用名称一致
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式,可以在 AMD 或 CommonJS 的 require 访问。
      // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
      jsonpFunction: `webpackJsonp_${
     
     name}`, // webpack 用来异步加载 chunk 的 JSONP 函数。
    },
  },
};

注意

这个 name 默认从 package.json 获取,可以自定义,只要和父项目注册时的 name 保持一致即可。qiankun 拿这三个生命周期,是根据注册应用时,你给的 name 值,name 不一致则会导致拿不到生命周期函数。

webpack 不同版本的配置

webpack v5:

const packageName = require('./package.json').name;

module.exports = {
   
   
  output: {
   
   
    library: `${
     
     packageName}-[name]`,
    libraryTarget: 'umd',
    chunkLoadingGlobal: `webpackJsonp_${
     
     packageName}`,
  },
};

webpack v4:

const packageName = require('./package.json').name;

module.exports = {
   
   
  output: {
   
   
    library: `${
     
     packageName}-[name]`,
    libraryTarget: 'umd',
    // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
    jsonpFunction: `webpackJsonp_${
     
     packageName}`,
  },
};

备注

更多相关配置介绍可以查看 webpack 相关文档





子项目开发的一些注意事项

js 相关
给 body 、 document 等绑定的事件,请在 unmount 周期清除

js 沙箱只劫持了 window.addEventListener,而使用了 document.body.addEventListener 或者 document.body.onClick 添加的事件并不会被沙箱移除,会对其他的页面产生影响,因此请在 unmount 生命周期清除绑定的事件。


css 相关
避免 css 污染

组件内样式的 css-scoped 是必须的。

对于一些插入到 body 的弹窗,无法使用 scoped,请不要直接使用原 class 修改样式,请添加自己的 class,来修改样式。

.el-dialog {
   
   
  /* 不推荐使用组件原有的class */
}

.my-el-dialog {
   
   
  /* 推荐使用自定义组件的class */
}
谨慎使用 position:fixed

在父项目中,浮动定位未必准确,应尽量避免使用,确有相对于浏览器窗口定位需求,可以用 position: sticky,但是会有兼容性问题(IE 不支持)。如果定位使用的是 bottom 和 right,则问题不大。

还有个办法,位置可以写成动态绑定 style 的形式:

<div :style="{ top: isQiankun ? '10px' : '0'}"></div>

静态资源 相关
所有的资源(图片/音视频等)都应该放到 src 目录下,不要放在 public 或者 static

资源放 src 目录,会经过 webpack 处理,能统一注入 publicPath。否则在主项目中会 404。

参考:vue-cli3 的官方文档介绍:何时使用-public-文件夹


第三方库 相关
请给 axios 实例添加拦截器,而不是 axios 对象

后续会考虑子项目共享公共插件,这时就需要避免公共插件的污染

// 正确做法:给 axios 实例添加拦截器
const instance = axios.create();
instance.interceptors.request.use(function () {
   
   
  // ...
});

// 错误用法:直接给 axios 对象添加拦截器
axios.interceptors.request.use(function () {
   
   
  // ...
});




qiankun 功能介绍

运行模式

乾坤只有一种运行模式:单例模式

在微前端框架中,子应用会随着主应用页面的打开和关闭反复的激活和销毁(单例模式:生命周期模式)。

单例模式

子应用页面如果切走,会调用 unmount 销毁子应用当前实例,子应用页面如果切换回来,会调用 mount 渲染子应用新的实例。

在单例式下,改变 url 子应用的路由会发生跳转到对应路由。



生命周期

微应用需要在应用的入口文件导出 bootstrapmountunmountupdate 四个生命周期钩子,以供主应用在适当的时机调用。

注意

💡 qiankun 生命周期函数都必须要 export 暴露
💡 qiankun 生命周期函数都必须是 Promise函数,使用 async 会返回一个 Promise 对象

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
   
   
  console.log('app bootstraped');
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
   
   
  // ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
  console.log('app mount');
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount(props) {
   
   
  // ReactDOM.unmountComponentAtNode(props.container ? props.container.querySelector('#root') : document.getElementById('root'));
  console.log('app unmount');
}

/**
 * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
 */
export async function update(props) {
   
   
  console.log('update props', props);
}


通讯方式

项目之间的不要有太多的数据依赖,毕竟项目还是要独立运行的。通信操作需要判断是否 qiankun 模式,做兼容处理。

props 通信

如果父子应用都是vue项目,通过 props 传递父项目的 Vuex store,可以实现响应式;但假如子项目是不同的技术栈(jQuery/react/angular),这是就不能很好的监听到数据的变化了。

注意

vue 项目之间数据传递还是使用共享父组件的 Vuex 比较方便,与其他技术栈的项目之间的通信使用 qiankun 提供的 initGlobalState

通过 vuex/pinia 实现通信

第一步:在主应用中创建一个 store

// src/store/index.ts

const personModule = {
   
   
  state: {
   
   
    id: 0,
    name: '',
    age: 0,
  },
  mutations: {
   
   
    setId(state: Object, id: number) {
   
   
      state.id = id;
    },
    setName(state: Object, name: string) {
   
   
      state.name = name;
    },
    setAge(state: Object, age: number) {
   
   
      state.age = age;
    },
  },
  actions: {
   
   
    setId(context: Object, id: number) {
   
   
      context.commit('setId', id);
    },
    setName(context: Object, name: string) {
   
   
      context.commit('setName', name);
    },
    setAge(context: Object, age: number
<think> 首先,用户的问题是:“我想查找前端乾坤框架获取子应用的方法 请问前端乾坤框架如何获取子应用”。用户正在询问如何使用乾坤框架(qiankun)来获取子应用。 我有几个引用: - 引用[1]:解释了qiankun加载子应用的过程,使用import-html-entry库fetch加载HTML入口文件。 - 引用[2]:展示了主应用如何传递参数给子应用,使用registerMicroApps方法,其中props可以用来传递数据。 - 引用[3]:讨论了微前端框架的学习,但没有直接相关。 - 引用[4]:提到乾坤不支持Vite,但这不是直接相关。 用户的问题是“获取子应用的方法”,这可能意味着如何加载子应用或如何在主应用中访问子应用。 从引用中: - 引用[1]:qiankun通过加载子应用的HTML入口文件来获取子应用。这涉及到跨域问题。 - 引用[2]:主应用可以使用registerMicroApps注册子应用,并通过props传递参数。 系统级指令要求: - 所有行内数学表达式用$...$格式(但这个问题可能不需要数学表达式)。 - 独立公式用$$...$$(同样可能不需要)。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 回答结构应该清晰,帮助用户逐步解决问题。 关键点: - 获取子应用的方法主要是通过注册和加载过程。 - registerMicroApps是核心方法。 - 子应用的生命周期钩子可以用来访问子应用实例。 在乾坤框架中,“获取子应用”可能指的是: 1. 注册子应用:使用registerMicroApps或loadMicroApp。 2. 访问子应用的实例或属性:通过生命周期钩子或props。 从引用[2]:props可以传递参数给子应用,子应用可以在生命周期钩子中获取这些参数。 此外,乾坤框架允许主应用控制子应用的加载和卸载。 如何回答: 1. 解释基本概念:乾坤框架如何管理子应用。 2. 描述注册方法:使用registerMicroApps。 3. 讨论加载过程:如引用[1]所述,加载HTML入口文件。 4. 解释如何访问子应用:通过钩子函数或事件。 5. 提供代码示例:基于引用[2]。 引用标识:在相关部分添加[^1]、[^2]等。 生成相关问题:在回答末尾添加相关问题,如示例所示。 结构回答: - 引言:简要解释乾坤框架如何获取子应用。 - 步骤:逐步描述如何注册和加载子应用。 - 代码示例:展示registerMicroApps的使用。 - 访问方法:如何获取子应用实例。 - 注意事项:如跨域问题(引用[1])或其他限制。 - 结尾:总结。 最后,生成相关问题。 相关问题示例: 1. 乾坤框架如何传递数据给子应用? 2. 子应用的生命周期钩子有哪些? 3. 如何优化乾坤框架中子应用的加载? 确保回答真实可靠,基于提供的引用。 </think> ### 乾坤框架获取子应用的方法 乾坤(qiankun)是一个流行的微前端框架,它允许主应用动态加载和管理子应用。要“获取子应用”,核心是通过注册和加载机制来实现。这包括使用 `registerMicroApps` 或 `loadMicroApp` 方法注册子应用、加载其入口文件,并在生命周期钩子中访问子应用实例。下面我将逐步解释整个过程,确保回答真实可靠,基于引用内容。 #### 1. **注册子应用:使用 `registerMicroApps` 方法** 乾坤框架的核心是通过主应用注册子应用,使其可被加载和管理。引用[2]提到,`registerMicroApps` 方法用于注册子应用列表,并可通过 `props` 传递参数。子应用在加载后可通过生命周期钩子访问这些参数。 - **步骤详解**: - 在主应用中调用 `registerMicroApps`,传入子应用配置数组。 - 每个子应用配置包括 `name`(子应用标识)、`entry`(入口URL)、`container`(挂载点)、`activeRule`(激活路由)和 `props`(传递的参数)。 - 当路由匹配 `activeRule` 时,乾坤会自动加载并挂载子应用。 - **代码示例**(基于引用[2]): ```javascript import { registerMicroApps } from 'qiankun'; registerMicroApps( [ { name: 'app1', // 子应用名称,用于标识 entry: '//localhost:8080', // 子应用入口URL(HTML文件) container: '#container', // 挂载到主应用的DOM节点 activeRule: '/react', // 激活路由规则 props: { name: 'kuitos' }, // 可传递参数给子应用 }, // 可添加更多子应用 ], { beforeLoad: (app) => console.log(`Before load ${app.name}`), // 加载前钩子 beforeMount: [(app) => console.log(`Before mount ${app.name}`)], // 挂载前钩子 afterMount: [(app) => console.log(`After mount ${app.name}`)], // 挂载后钩子,可访问子应用实例 } ); ``` 在此示例中,`props` 可用于传递数据给子应用,子应用在生命周期钩子中通过参数获取[^2]。例如,`beforeMount` 钩子中的 `app` 对象包含子应用信息。 #### 2. **加载子应用:获取入口文件并处理** 乾坤通过加载子应用的入口HTML文件来动态获取子应用。引用[1]提到,qiankun 使用 `import-html-entry` 库来fetch子应用的HTML入口文件,这要求子应用资源允许跨域(CORS)。 - **过程说明**: - 当子应用被激活(如路由匹配),乾坤会fetch入口文件(通常是 `index.html`)。 - 解析HTML文件,加载子应用的JavaScript、CSS等资源。 - 资源加载完成后,乾坤执行生命周期钩子(如 `bootstrap`、`mount`),将子应用挂载到指定容器。 - **关键点**: - 子应用必须公开其入口文件,并设置跨域策略(如服务端添加 CORS 头)。 - 加载成功后,子应用实例可通过钩子函数访问,例如在 `afterMount` 钩子中获取加载完成的子应用对象[^1]。 #### 3. **访问子应用实例** 一旦子应用加载完成,主应用可通过以下方式“获取”子应用实例: - **生命周期钩子**:在 `registerMicroApps` 的钩子函数中,`app` 参数代表子应用对象。例如: - `afterMount(app)`: 当子应用挂载成功时,`app` 包含子应用的名称、状态和传递的 `props`,可直接操作子应用。 - **手动加载与获取**:使用 `loadMicroApp` 方法(替代 `registerMicroApps`)手动加载子应用,并返回一个微应用实例对象。 ```javascript import { loadMicroApp } from 'qiankun'; const appInstance = loadMicroApp({ name: 'app1', entry: '//localhost:8080', container: '#container', props: { name: 'kuitos' }, }); // 获取子应用实例后,可调用其方法(如卸载) appInstance.unmount(); ``` 这种方式更适合动态场景,如按需加载子应用。 - **从子应用内部访问**:子应用可通过全局API(如 `window.__POWERED_BY_QIANKUN__`)判断运行环境,并使用 `props` 获取主应用传递的数据[^2]。 #### 4. **注意事项** - **跨域问题**:子应用入口文件和资源必须允许跨域,否则加载失败(引用[1]强调这一点)。 - **框架兼容性**:乾坤不完全支持所有现代框架(如Vite),引用[4]提到可能需要额外插件或配置,建议优先使用兼容方案(如Webpack)。 - **性能优化**:确保子应用入口文件轻量,避免阻塞主应用渲染。 #### 总结 乾坤框架通过 `registerMicroApps` 或 `loadMicroApp` 注册和加载子应用,并使用生命周期钩子访问子应用实例。核心方法是加载HTML入口文件(需跨域支持)并利用钩子函数获取子应用对象[^1][^2]。实践中,确保主应用配置正确,子应用资源可访问。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值