微前端qiankun的实际应用


在这里插入图片描述

微前端:微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。

1. 业务当前的问题,什么是微前端,为什么要使用它?有哪些挑战

原来系统中嵌套各个模块,每个业务模块相对独立,各有独立的业务体系,单个模块体积较大,项目整体采用SPA + iframe的架构模式,其中的工单系统就是通过iframe嵌套的。在客服业务不断迭代的过程中,SPA + iframe的架构模式暴露出了很多问题

  1. 业务问题:
  • 问题一:SPA架构模式下,由于各个模块集中于一个架构下,导致首屏加载资源过多,首屏加载速度较慢;SPA只有入口文件,所以需要对各个模块做业务模式兼容,导致入口文件代码条件语句较多,代码紊乱,出现线上问题的时候,排查较为困难,如果有新的同学参与开发,梳理业务也较为困难,甚至有的时候难以理解。
  • 问题二:项目中嵌套大量的iframe,iframe也会拖累页面的加载速度,iframe使用postMessage通讯时也会带来数据延迟,数据丢失等各种问题,客服使用时间较长的时候,当切换iframe中的页面时,前一个页面中的无法被完全释放,导致浏览器所占的内存不停的飙升,最终导致浏览器崩溃。
  1. 微前端优点:
  • 复杂度可控: 业务模块解耦,避免代码过大,保持较低的复杂度,便于维护与开发效率。
  • 独立开发,独立部署: 模块部署,减少模块影响范围,单个模块发生错误,不影响全局,提升项目稳定性。
  • 技术选型灵活: 在同一项目下可以使用市面上所有前端技术栈,也包括未来的前端技术栈。
  • 扩展性,提升业务动态扩展的可能,避免资源浪费
  1. 主要挑战
  • 性能问题: 如果不同的微前端应用使用了不同的库或框架,可能会导致加载和运行的性能问题。
  • 一致性: 保持不同的微前端应用在用户体验、设计和行为上的一致性可能会比较困难。
  • 状态共享: 在微前端应用之间共享状态可能会比较复杂,需要使用特殊的工具或模式。
  • 复杂性: 尽管微前端可以解决大型项目的复杂性问题,但是它自身也带来了一些复杂性,比如需要管理和协调多个独立的应用。
  • 安全性: 微前端架构可能会增加跨域等安全问题。

2. 现在市面上主流的框架对比

1)qiankun 方案

  • 优点
    • 降低了应用改造的成本,通过html entry的方式引入子应用;
    • 提供了完备的沙箱方案,包括js沙箱和css沙箱;
    • 支持静态资源预加载能力。(start 函数接收一个可选的配置对象作为参数,prefetch:true,即在主应用 start 之后即刻开始预加载所有子应用的静态资源)
  • 缺点
    • 适配成本较高,包括工程化、生命周期、静态资源路径、路由等方面的适配;
    • css沙箱的严格隔离可能引发问题,js沙箱在某些场景下执行性能下降;
    • 无法同时激活多个子应用,不支持子应用保活;
    • 不支持vite等esmodule脚本运行。

2)micro-app 方案

  • 优点
    • 使用 webcomponent 加载子应用,更优雅;
    • 复用经过大量项目验证过的 qiankun 沙箱机制,提高了框架的可靠性;
    • 支持子应用保活;
    • 降低了子应用改造的成本,提供了静态资源预加载能力。
  • 缺点
    • 接入成本虽然降低,但路由依然存在依赖;
    • 多应用激活后无法保持各子应用的路由状态,刷新后全部丢失;
    • css 沙箱无法完全隔离,js 沙箱做全局变量查找缓存,性能有所优化;
    • 支持 vite 运行,但必须使用 plugin 改造子应用,且 js 代码没办法做沙箱隔离;
    • 对于不支持 webcomponent 的浏览器没有做降级处理。

3)EMP 方案

  • 优点
    • webpack 联邦编译可以保证所有子应用依赖解耦;
    • 支持应用间去中心化的调用、共享模块;
    • 支持模块远程 ts 支持。
  • 缺点
    • 对 webpack 强依赖,对于老旧项目不友好;
    • 没有有效的 css 沙箱和 js 沙箱,需要靠用户自觉;
    • 子应用保活、多应用激活无法实现;
    • 主、子应用的路由可能发生冲突。

4)无界方案

  • 优点
    • 基于 webcomponent 容器和 iframe 沙箱,充分解决了适配成本、样式隔离、运行性能、页面白屏、子应用通信、子应用保活、多应用激活、vite框架支持、应用共享等问题。
  • 缺点
    • 在继承了iframe优点的同时,缺点依旧还是存在

5)国外的single_spa
在这里插入图片描述

3. 业务接入微前端面临问题和自身收益

待解决的问题:

  • 微应用需要具备缓存(keep-alive)能力,应用切换状态不能丢失
    qiankun为我们提供了两个注册方法:registerMicroApps(路由改变会帮我们自动注册微应用和销毁上一个微应用,对于不需要缓存的应用简单易用),loadMicroApp(手动 加载/卸载 一个微应用)。
    loadMicroApp:在基座和微应用设置合适keep-alive缓存策略,然后通过“display: none”的方式去控制切换的显示和隐藏(DOM重新渲染会导致历史状态丢失),在基座中为每个微应用设置挂载点,应用切换的时候就不会导致前一个微应用DOM被卸载。
    结论:1.微应用初始化加载的时候,需要经历一次资源请求,页面渲染,会有一次大的性能开销;2.微应用加载成功之后,在此切换回来,采用“display: none”+keep-alive方式处理+路由过滤,虽然需要经历一次重流重绘,但也不会带来太大的性能开销(经过前后对比得知)。
  • 需要具备同一时刻加载多个微应用
  • 沙箱隔离和引入第三方资源
  • 基座-微应用,微应用-微应用之间如何进行通讯
  • 如何接入远程组件
    远程组件采用webpack5模块联邦去实现,需要将入口文件(mian.ts)转移到新的文件(bootstrap.ts),并在入口文件中导出qiankun生命周期,避免打包出两个入口文件,导致qiankun加载生命周期函数失败。
  • 样式隔离
    官方提供的API:sandbox(boolean)
    默认场景sandbox: true, 只能保证单实例下的样式隔离,无法保证多个微应用共存,基座-微应用之间的样式隔离;
    strictStyleIsolation: true ;表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响;

感想和收益:
过程艰难曲折,第一个难关就是微应用缓存能力的实现,社区中只有简短的demo,距离真正落地到项目差的还很远;其次我们的项目还需要考虑刷新页面,在当前微应用重载其他微应用的场景;有些微应用需要依赖第三方的插件,这个插件可能会是一个jQuery插件,可能还会遇到jsonp跨域的场景;还需要考虑微应用之间通用组件的复用问题;原始项目采用vite构建,面对qiankun对vite支持不友好的情况下,最终不得不选择webpack5。
任何技术框架都有其适用场景,对于特定的业务场景,可能原来的技术架构显得臃肿,但他可能是最合适的,微前端不是神话,正确的场景使用正确的技术才是最优选。

4. qiankun应用之间的通信方式

  1. localStoragesessionStorage
    有人说这个方案必须主应用和子应用是同一个域名下。其实不是的,子应用使用不同的域名也是可以的,因为在 qiankun 中,主应用是通过 fetch 来拉取子应用的模板,然后渲染在主应用的 dom 上的,说白了还是运行在主应用上,所以还是运行在同一个域名上,也就是主应用的域名。

  2. 路由参数
    因为只有一个 url,不管子应用还是主应用给 url 上拼接一些参数,那么父子应用都可以通过 route 来获取到。

  3. Actions 通信(官方提供,比较适合业务划分清晰,应用间通信较少的微前端应用场景)

    • 这种通信方式主要通过 setGlobalState 设置 globalState,并通过 onGlobalStateChange 和 offGlobalStateChange 来注册和取消 观察者 函数,从而实现通信。

    • qiankun 内部提供了 initGlobalState 方法用于注册 MicroAppStateActions 实例用于通信,该实例有三个方法,分别是:

      • setGlobalState:设置 globalState - 设置新的值时,内部将执行 浅检查,如果检查到 globalState 发生改变则触发通知,通知到所有的 观察者 函数。
      • onGlobalStateChange:注册 观察者 函数 - 响应 globalState 变化,在 globalState 发生改变时触发该 观察者 函数。
      • offGlobalStateChange:取消 观察者 函数 - 该实例不再响应 globalState 变化。
  4. Shared 通信

    • 主应用基于 redux(或别的状态管理库) 维护一个状态池,通过 shared 实例暴露一些方法给子应用使用。同时,子应用需要单独维护一份 shared 实例,在独立运行时使用自身的 shared 实例,在嵌入主应用时使用主应用的 shared 实例,这样就可以保证在使用和表现上的一致性
      1、在主应用中维护一个公共状态池,暴露set、get方法,在定义子应用的时候通过 props 传递给子应用
      2、子应用接受prop,并在自己代码库中维护一套相同的空状态池(目的是为了可以独立运行)
      3、子应用调用方法获取修改。

5. qiankun工作原理和生命周期

  • 应用加载:qiankun 通过动态创建 script 标签的方式加载子应用的入口文件。加载完成后,会执行子应用暴露出的生命周期函数。
  • 生命周期管理:qiankun 要求每个子应用都需要暴露出 bootstrap、mount 和 unmount 三个生命周期函数。bootstrap 函数在应用加载时被调用,mount 函数在应用启动时被调用,unmount 函数在应用卸载时被调用。
  • 沙箱隔离:qiankun 通过 Proxy 对象创建了一个 JavaScript 沙箱,用于隔离子应用的全局变量,防止子应用之间的全局变量污染。
  • 样式隔离:qiankun 通过动态添加和移除样式标签的方式实现了样式隔离。当子应用启动时,会动态添加子应用的样式标签,当子应用卸载时,会移除子应用的样式标签。
  • 通信机制:qiankun 提供了一个全局的通信机制,允许子应用之间进行通信。

6. js沙箱,css沙箱

js沙箱,css沙箱

7. 为什么不用iframe

  1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中…
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程

8. qiankun的js entry和html entry区别及对比(加载子应用的方法)

JS entry :子应用将资源打成一个 entry script,比如 single-spa 的 example 中的方式。但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上。
HTML entry :将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题。
在这里插入图片描述

html entry内部是通过import-html-entry包实现的。

  • 加载 HTML 入口文件:import-html-entry 会通过创建一个 标签来加载子应用的 HTML 入口文件。这样可以确保子应用的资源得到正确加载,并在加载完成后进行处理。
  • 解析 HTML 入口文件:一旦 HTML 入口文件加载完成,import-html-entry 将解析该文件的内容,提取出子应用的 JavaScript 和 CSS 资源的 URL。
  • 动态加载 JavaScript 和 CSS 资源:import-html-entry 使用动态创建 <script><link> 标签的方式,按照正确的顺序加载子应用的 JavaScript 和 CSS 资源。
  • 创建沙箱环境:在加载子应用的 JavaScript 资源时,import-html-entry 会创建一个沙箱环境(sandbox),用于隔离子应用的全局变量和运行环境,防止子应用之间的冲突和污染。
  • 返回子应用的入口模块:最后,import-html-entry 返回一个可以加载子应用的 JavaScript 模块。这个模块通常是一个包含子应用初始化代码的函数,可以在主应用中调用以加载和启动子应用。

通过使用 qiankun import-html-entry,开发者可以方便地将子应用的 HTML 入口文件作为模块加载,并获得一个可以加载和启动子应用的函数,简化了子应用的加载和集成过程。

9. 微前端优缺点,引入的问题

优点:

  • 复杂度可控: 业务模块解耦,避免代码过大,保持较低的复杂度,便于维护与开发效率。
  • 独立开发,独立部署: 模块部署,减少模块影响范围,单个模块发生错误,不影响全局,提升项目稳定性。
  • 技术选型灵活: 在同一项目下可以使用市面上所有前端技术栈,也包括未来的前端技术栈。
  • 扩展性,提升业务动态扩展的可能,避免资源浪费

缺点:

  • 复杂性:微前端需要使用复杂的技术和工具来实现,需要考虑到不同子应用程序之间的通信和交互,这会增加开发和维护的复杂性。
  • 性能问题:由于微前端需要在运行时加载和卸载模块,因此可能会影响应用程序的性能。
  • 安全问题:由于微前端允许在同一个页面上共存不同的子应用程序,因此可能会带来安全隐患,需要进行严格的安全控制。微前端架构可能会增加跨域等安全问题。

10. 对比ifream有什么优缺点,应用场景

  1. qiankun和ifream对比
  • qiankun 是基于 single-spa 的微前端解决方案,它通过 JavaScript 的 import 功能动态加载子应用,然后在主应用的 DOM 中挂载子应用的 DOM。qiankun 提供了一种 JavaScript 沙箱机制,可以隔离子应用的全局变量,防止子应用之间的全局变量污染。此外,qiankun 还提供了一种样式隔离机制,可以防止子应用的 CSS 影响其他应用。这些特性使得 qiankun 在处理复杂的微前端场景时具有很高的灵活性。
  • iframe 是一种较为传统的前端技术,它可以在一个独立的窗口中加载一个 HTML 页面。iframe 本身就是一种天然的沙箱,它可以完全隔离子应用的 JavaScript 和 CSS,防止子应用之间的相互影响。然而,iframe 的这种隔离性也是它的缺点,因为它使得主应用和子应用之间的通信变得困难。此外,iframe 还有一些其他的问题,比如性能问题、SEO 问题等。

在选择 qiankun 和 iframe 时,需要根据具体的使用场景来决定。如果你的子应用是基于现代前端框架(如 React、Vue、Angular 等)开发的单页应用,那么 qiankun 可能是一个更好的选择,因为它可以提供更好的用户体验和更高的开发效率。如果你的子应用是基于 jQuery 或者其他传统技术开发的多页应用,或者你需要在子应用中加载一些第三方的页面,那么 iframe 可能是一个更好的选择,因为它可以提供更强的隔离性。

  1. ifream优点
  • 代码重用性高;
  • 提高页面性能;
  • 可以跨域访问,可以在主网页和嵌套网页之间进行跨域通信
  1. 缺点
  • 安全性问题,恶意网站可能会通过 iframe 标签加载用户的敏感信息,导致信息泄露或盗取。
  • SEO 不友好
  • url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  • 难以控制样式,例如:滚动条难以处理,屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中
  • 通信困难,目前只能使用路由、postMessage
  • 阻塞window.onload()触发,iframe加载完毕后才会触发window.onload事件,动态设置src可解决这个问题。
  • 性能开销,每个 <iframe> 都需要加载独立的HTML文档,这可能会增加页面的加载时间和网络请求数量,从而导致性能开销。
  • 样式和脚本需要额外链入,调用外部页面,需要额外调用css,增加页面额外的请求次数,增加服务器的http请求

不要过多使用ifream?

  • iframe会阻塞主页面的onload事件
  • iframe和主页面共享连接池,会影响页面的并行加载。
  • iframes阻塞页面加载,影响网页加载速度
  1. 应用场景
  • 所见即所得的网页编辑器
  • 跨域通信。JavaScript跨域总结与解决办法 ,类似的还有浏览器多页面通信,比如音乐播放器,用户如果打开了多个tab页,应该只有一个在播放
  • 用iframe实现无刷新文件上传,在FormData不可用时作为替代方案
  • 用来加载广告,例如联盟广告
  • 需要独立样式和带有交互的内容,例如幻灯片
  • sandbox沙箱隔离

qiankun

11. qiankun遇到的挑战

1. 在使用 qiankun 时,如果子应用是基于 jQuery 的多页应用,你会如何处理静态资源的加载问题?

在使用 qiankun 时,如果子应用是基于 jQuery 的多页应用,静态资源的加载问题可能会成为一个挑战。这是因为在微前端环境中,子应用的静态资源路径可能需要进行特殊处理才能正确加载。这里有几种可能的解决方案:

  • 方案一:使用公共路径
    在子应用的静态资源路径前添加公共路径前缀。例如,如果子应用的静态资源存放在 http://localhost:8080/static/,那么可以在所有的静态资源路径前添加这个前缀。

  • 方案二:劫持标签插入函数

    • 对于 HTML 中已有的 img/audio/video 等标签,qiankun 支持重写 getTemplate 函数,可以将入口文件 index.html 中的静态资源路径替换掉。
      🌰:传递一个 getTemplate 函数,将图片的相对路径转为绝对路径,它会在处理模板时使用
    • 对于动态插入的 img/audio/video 等标签,劫持 appendChild、innerHTML、insertBefore 等事件,将资源的相对路径替换成绝对路径。
      🌰:对于动态插入的标签,劫持其插入 DOM 的函数,注入前缀。
// qiankun 的 start 函数是用来启动微前端应用的。在注册完所有的子应用之后,我们需要调用 start 函数来启动微前端应用。
start({
  getTemplate(tpl,...rest) {
   // 为了直接看到效果,所以写死了,实际中需要用正则匹配
  	return tpl.replace('<img src="./img/jQuery1.png">', '<img src="http://localhost:3333/img/jQuery1.png">');
  }
})
beforeMount: app => {
  if(app.name === 'purehtml'){
     // jQuery 的 html 方法是一个挺复杂的函数,这里只是为了看效果,简写了
     $.prototype.html = function(value){
        const str = value.replace('<img src="/img/jQuery2.png">', '<img src="http://localhost:3333/img/jQuery2.png">')
        this[0].innerHTML = str;
     }
  }
}
2. qiankun 如何实现 keep-alive 的需求吗?

在 qiankun 中,实现 keep-alive 的需求有一定的挑战性。这是因为 qiankun 的设计理念是在子应用卸载时,将环境还原到子应用加载前的状态,以防止子应用对全局环境造成污染。这种设计理念与 keep-alive 的需求是相悖的,因为 keep-alive 需要保留子应用的状态,而不是在子应用卸载时将其状态清除。

  • 方案1:在子应用的生命周期函数中保存和恢复子应用的状态。例如,我们可以在子应用的 unmount 函数中保存子应用的状态,然后在 mount 函数中恢复这个状态
    缺点:需要手动保存和恢复子应用的状态,这可能会增加开发的复杂性。此外,这种方法也不能保留子应用的 DOM 状态,只能保留 JavaScript 的状态。
  • 方案2:手动*loadMicroApp*+display:none,直接隐藏Dom
3. 如何处理多个子项目的调试问题?

将每个子项目作为一个独立的应用进行开发和调试。每个子项目都可以在本地启动,并通过修改主应用的配置,让主应用去加载本地正在运行的子应用,这样就可以对子应用进行调试了。这种方式的好处是,子应用与主应用解耦,可以独立进行开发和调试,不会相互影响。

使用npm-run-all这个工具去启动多个项目

4. 在主项目中使用qiankun注册子项目时,如何解决子项目路由的hash与history模式之争?
  • 如果主项目使用 history 模式,并且子项目可以使用 history 或 hash 模式,这是 qiankun 推荐的一种形式。在这种情况下,子项目可以选择适合自己的路由模式,而且对于已有的子项目不需要做太多修改。但是子项目之间的跳转需要通过父项目的 router 对象或原生的 history 对象进行
  • 如果主项目和所有子项目都采用 hash 模式,可以有两种做法:
    • 使用 path 来区分子项目:这种方式不需要对子项目进行修改,但所有项目之间的跳转需要借助原生的 history 对象。
    • 使用 hash 来区分子项目:这种方式可以通过自定义 activeRule 来实现,但需要对子项目进行一定的修改,将子项目的路由加上前缀。这样的话,项目之间的跳转可以直接使用各自的 router 对象或 。
  • 如果主项目采用 hash 模式,而子项目中有些采用 history 模式,这种情况下,子项目间的跳转只能借助原生的 history 对象,而不使用子项目自己的 router 对象。对于子项目,可以选择使用 path 或 hash 来区分不同的子项目。

12. 微前端如何解决跨域?

  1. 子应用的网络请求都是在自身域名下请求的
    使用 html entry 将子应用打包成一个 html 文件,然后通过主应用中的一个 iframe 来加载这个 html 文件,这样子应用中所有的请求都会通过主应用的域名来访问,避免了跨域问题
// 主应用引入子应用
import { loadMicroApp } from 'qiankun';

loadMicroApp({
  name: 'subapp',
  entry: '//localhost:7100',
  container: '#subapp-container',
  htmlEntry: '/subapp.html',
});
  1. 当主应用和子应用不在同一个域下
    跨域解决方案
    • 在子应用的服务器端配置CORS
    • 在开发环境中,可以配置前端代理,将子应用的请求代理到相应的服务器,以避免跨域。
    • 在生产环境中,可以使用Nginx反向代理,将请求从主应用代理到子应用。
    • 通过postMessage API,主应用和子应用可以在不同域名间进行安全的跨域通信。
    • 对于静态资源(如图片、CSS、JavaScript 文件等),可以在子应用服务器端配置CORS头部信息,允许跨域访问。

13. qiankun如何实现子应用的按需加载?

  1. 路由匹配
    Qiankun 使用基于路由的机制来实现子应用的按需加载。只有当用户访问特定路由时,才会加载对应的子应用。通过配置主应用和子应用的路由规则,可以实现对子应用的按需加载。

  2. 子应用注册
    在主应用中,可以通过 registerMicroApps 方法注册多个子应用,并为每个子应用指定匹配路由、加载函数和生命周期钩子。只有当路由匹配时,才会触发子应用的加载。

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:7100',
    container: '#container',
    activeRule: '/app1',
  },
  {
    name: 'app2',
    entry: '//localhost:7101',
    container: '#container',
    activeRule: '/app2',
  },
]);

// 启动 qiankun
start();

在上面的例子中,app1 和 app2 是两个子应用,分别配置了它们的入口、渲染容器和激活规则。当用户访问 /app1 路径时,app1 会被加载并渲染到指定容器中,而 app2 则在访问 /app2 路径时被加载。

  1. 动态加载资源
    Qiankun 会在路由匹配时,动态地通过 HTTP 请求加载子应用的资源(如 HTML、JavaScript、CSS 文件等)。加载完毕后,Qiankun 会将这些资源注入到主应用的 DOM 中。

  2. 沙箱机制
    Qiankun 提供了沙箱机制,确保子应用在独立的上下文中运行,不影响其他子应用或主应用。沙箱机制配合按需加载,可以确保子应用的资源和状态在进入和离开时得到正确的处理。

  3. 生命周期钩子
    Qiankun 提供了一系列生命周期钩子(如 beforeLoad、beforeMount、afterMount、beforeUnmount、afterUnmount 等),在子应用加载和卸载的过程中,可以在这些钩子中执行特定的逻辑,如资源初始化、数据预处理、资源清理等。

14. 子应用生命周期函数

在qiankun中,子应用需要导出三个生命周期函数:

  • bootstrap:子应用初始化时调用,只会调用一次;
  • mount:子应用挂载时调用,可能会调用多次;
  • unmount:子应用卸载时调用,可能会调用多次;
    在这里插入图片描述
  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值