• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

微前端项目难点解决

武飞扬头像
frontend_frank
帮助1

为什么要用微前端

  • 业务管理系统多,技术栈分别为 vue3/vue2/react16/react hook

  • 管理人员需要同时使用多系统,但是又不想切换系统重新登陆,页面会刷新,需要新开浏览器tab

  • 部分子应用需要支持子公司的业务,需要独立部署运行。

  • 对于开发者来说,如果需要在应用 A 实现应用B的某些功能,例如在应用A的页面弹出应用B的弹窗,如果是react、vue两种不同的框架的话,重新写一遍业务逻辑代码很明显是不理智的。

所以从技术角度来看,我们需要用一个父架构来集成这些子应用,把它们整合到统一平台上,同时子应用也可以脱离父架构独立部署运行。

微前端架构图

学新通

为什么放弃iframe

浏览记录无法自动被记录,浏览器刷新,状态丢失、后退前进按钮无法使用。

嵌套子应用弹窗蒙层无法覆盖全屏 页面通信比较麻烦,只能采用postMessage方式。 

每次子应用进入都需要重新请求资源,页面加载速度慢。

强调一下,目规模小、数量少的场景其实不建议使用微前端。

罗列一下碰到的问题

  • 多tab切换操作久了会越来越卡

  • 双应用切换数据缓存

  • 同一个基座如何同时并行加载两个应用

  • 子应用部署后,如何提示业务人员更新系统

  • 性能优化:父应用如何实现预加载和按需加载

qiankun 实现原理

微前端方案中我们最终选择了 qiankunqiankun是基于single-spa开发,它主要采用HTML Entry模式,直接将子应用打出来 HTML作为入口,通过 fetch html 的方式,解析子应用的html文件,然后获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。

应用切出/卸载后,同时卸载掉其样式表即可,浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样即能保证,在一个时间点里,只有一个应用的样式表是生效的。

HTML Entry 方案则天生具备样式隔离的特性,因为应用卸载后会直接移除去 HTML 结构,从而自动移除了其样式表。

子应用挂载时,会自动做一些特殊处理,可以确保子应用所有的资源dom(包括js添加的style标签等)都集中在子应用根节点dom下。子应用卸载时,对应的整个dom都移除了,这样也就避免了样式冲突。

提供了js沙箱,子应用挂载时,会对全局window对象代理、对全局事件监听进行劫持等,确保微应用之间 全局变量/事件 不冲突。

通过阅读qiankun源码。熟悉一下qiankun代码的执行流程

学新通

业务中碰到的难点解决

双应用切换数据缓存

不同系统之间切换数据缓存问题,同一个应用可以使用 keep-alive 去缓存页面,但是不同子应用之间切换的时候,会导致子应用被销毁,缓存失效

多开tab缓存方案

代码实现

通过display:none;控制不同子应用dom的显示隐藏

  1.  
    <template>
  2.  
      <div id="app">
  3.  
      <header>
  4.  
        <router-link to="/app1/">app1</router-link>
  5.  
        <router-link to="/app2/">app2</router-link>
  6.  
      </header>
  7.  
      <div id="appContainer1" v-show="$route.path.startsWith('/app1/')"></div>
  8.  
      <div id="appContainer2" v-show="$route.path.startsWith('/app2/')"></div>
  9.  
      <router-view></router-view>
  10.  
    </div>
  11.  
    </template>
解决方案

思考, 如何优化渲染性能:

每一个微应用实例都是运行在一个基座里,那我们如何尽可能多的复用沙箱,子系统切换时候不卸载,这样切换路由就快了

  1. 方案一

方案优势:直接调用官网api loadMicroApp,方便快捷 切换的时候不卸载子应用,tab切换速度比较快。方案不足:超级管理员应用太多,子应用切换时不销毁DOM,会导致DOM节点和事件监听过多,造成页面卡顿;子应用切换时未卸载,路由事件监听也未卸载,需要对路由变化的监听做特殊的处理。                   2. 方案二

  1.  
    start({
  2.  
        prefetch: 'all',
  3.  
        singular: false,
  4.  
    })

有点:代码量少,通过registerMicroApps注册子应用,通过start的prefetch预加载, 但是有个问题就是子应用在切换的时候会unmount,导致数据丢失,导致之前填写的表单数据丢失&重新打开速度也慢

看了一下 基于微前端qiankun的多页签缓存方案实践:https://zhuanlan.zhihu.com/p/548520855 3.1章节的实现方法,我感觉太复杂了,而且还需要同时实现react和vue两种方案,代码量也比较大。

当时就想着要是微应用切换的时候不卸载dom就好了。

方案二优化

调用了start方法后,子应用切换怎么才能不卸载dom呢 通过查阅文献以及阅读qiankun生命周期钩子函数的源码,最终找到了解决方案

首先修改子项目的render()和unmount()方法

子项目修改

  1.  
    let instance
  2.  
    export async function render() {
  3.  
      if(!instance){
  4.  
         instance = ReactDOM.render(
  5.  
            app,
  6.  
            container
  7.  
                ? container.querySelector("#root")
  8.  
                : document.querySelector("#root")
  9.  
        );ount('#app1History');
  10.  
      }
  11.  
    }
  12.  
     
  13.  
    export async function unmount(props) { 
  14.  
        //     const { container } = props;
  15.  
        //     ReactDOM.unmountComponentAtNode(
  16.  
        //         container
  17.  
        //             ? container.querySelector("#root")
  18.  
        //             : document.querySelector("#root")
  19.  
        //     );
  20.  
    }
学新通

vue项目同理

然后,主应用调用

  1.  
    start({
  2.  
        prefetch: 'all',
  3.  
        singular: false,
  4.  
    })

然后借助patch-package修改qiankun源码

patch-package的使用方法这里就不赘述了,网上有很多,很容易搜到

总共修改五处地方,基于qiankun2.9.1

  1.  
    diff --git a/node_modules/qiankun/es/loader.js b/node_modules/qiankun/es/loader.js
  2.  
    index 6f48575..285af0e 100644
  3.  
    --- a/node_modules/qiankun/es/loader.js
  4.  
     b/node_modules/qiankun/es/loader.js
  5.  
    @@ -286,11  286,14 @@ function _loadApp() {
  6.  
               legacyRender = 'render' in app ? app.render : undefined;
  7.  
               render = getRender(appInstanceId, appContent, legacyRender); // 第一次加载设置应用可见区域 dom 结构
  8.  
               // 确保每次应用加载前容器 dom 结构已经设置完毕
  9.  
    -          render({
  10.  
    -            element: initialAppWrapperElement,
  11.  
    -            loading: true,
  12.  
    -            container: initialContainer
  13.  
    -          }, 'loading');
  14.  
              console.log("qiankun-loader--loading", getContainer(initialContainer).firstChild)
  15.  
              if (!getContainer(initialContainer).firstChild) {
  16.  
                render({
  17.  
                  element: initialAppWrapperElement,
  18.  
                  loading: true,
  19.  
                  container: initialContainer
  20.  
                }, 'loading');
  21.  
              }
  22.  
               initialAppWrapperGetter = getAppWrapperGetter(appInstanceId, !!legacyRender, strictStyleIsolation, scopedCSS, function () {
  23.  
                 return initialAppWrapperElement;
  24.  
               });
  25.  
    @@ -305,8  308,8 @@ function _loadApp() {
  26.  
               speedySandbox = _typeof(sandbox) === 'object' ? sandbox.speedy !== false : true;
  27.  
               if (sandbox) {
  28.  
                 sandboxContainer = createSandboxContainer(appInstanceId,
  29.  
    -            // FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
  30.  
    -            initialAppWrapperGetter, scopedCSS, useLooseSandbox, excludeAssetFilter, global, speedySandbox);
  31.  
                  // FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
  32.  
                  initialAppWrapperGetter, scopedCSS, useLooseSandbox, excludeAssetFilter, global, speedySandbox);
  33.  
                 // 用沙箱的代理对象作为接下来使用的全局对象
  34.  
                 global = sandboxContainer.instance.proxy;
  35.  
                 mountSandbox = sandboxContainer.mount;
  36.  
    @@ -409,11  412,18 @@ function _loadApp() {
  37.  
                             appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);
  38.  
                             syncAppWrapperElement2Sandbox(appWrapperElement);
  39.  
                           }
  40.  
    -                      render({
  41.  
    -                        element: appWrapperElement,
  42.  
    -                        loading: true,
  43.  
    -                        container: remountContainer
  44.  
    -                      }, 'mounting');
  45.  
                          //修改2
  46.  
                          if (!getContainer(remountContainer).firstChild) {
  47.  
                            render({
  48.  
                              element: appWrapperElement,
  49.  
                              loading: true,
  50.  
                              container: remountContainer
  51.  
                            }, 'mounting');
  52.  
                          }
  53.  
                         case 3:
  54.  
                         case "end":
  55.  
                           return _context5.stop();
  56.  
    @@ -458,11  468,18 @@ function _loadApp() {
  57.  
                     return _regeneratorRuntime.wrap(function _callee8$(_context8) {
  58.  
                       while (1switch (_context8.prev = _context8.next) {
  59.  
                         case 0:
  60.  
    -                      return _context8.abrupt("return", render({
  61.  
    -                        element: appWrapperElement,
  62.  
    -                        loading: false,
  63.  
    -                        container: remountContainer
  64.  
    -                      }, 'mounted'));
  65.  
                          return _context8.abrupt("return", () => {
  66.  
                            console.log(initialContainer, remountContainer)
  67.  
                            //修改3
  68.  
                            console.log("qiankun-loader-mounted", getContainer(initialContainer).firstChild)
  69.  
                            if (!getContainer(remountContainer).firstChild) {
  70.  
                              render({
  71.  
                                element: appWrapperElement,
  72.  
                                loading: false,
  73.  
                                container: remountContainer
  74.  
                              }, 'mounted')
  75.  
                            }
  76.  
                          });
  77.  
                         case 1:
  78.  
                         case "end":
  79.  
                           return _context8.stop();
  80.  
    @@ -554,15  571,17 @@ function _loadApp() {
  81.  
                     return _regeneratorRuntime.wrap(function _callee15$(_context15) {
  82.  
                       while (1switch (_context15.prev = _context15.next) {
  83.  
                         case 0:
  84.  
    -                      render({
  85.  
    -                        element: null,
  86.  
    -                        loading: false,
  87.  
    -                        container: remountContainer
  88.  
    -                      }, 'unmounted');
  89.  
    -                      offGlobalStateChange(appInstanceId);
  90.  
    -                      // for gc
  91.  
    -                      appWrapperElement = null;
  92.  
    -                      syncAppWrapperElement2Sandbox(appWrapperElement);
  93.  
                          //修改4
  94.  
                          console.log('qiankun-loader-unmounted')
  95.  
                        // render({
  96.  
                        //   element: null,
  97.  
                        //   loading: false,
  98.  
                        //   container: remountContainer
  99.  
                        // }, 'unmounted');
  100.  
                        // offGlobalStateChange(appInstanceId);
  101.  
                        // // for gc
  102.  
                        // appWrapperElement = null;
  103.  
                        // syncAppWrapperElement2Sandbox(appWrapperElement);
  104.  
                         case 4:
  105.  
                         case "end":
  106.  
                           return _context15.stop();
  107.  
    diff --git a/node_modules/qiankun/es/sandbox/patchers/dynamicAppend/forStrictSandbox.js b/node_modules/qiankun/es/sandbox/patchers/dynamicAppend/forStrictSandbox.js
  108.  
    index 724a276..1dd3da1 100644
  109.  
    --- a/node_modules/qiankun/es/sandbox/patchers/dynamicAppend/forStrictSandbox.js
  110.  
     b/node_modules/qiankun/es/sandbox/patchers/dynamicAppend/forStrictSandbox.js
  111.  
    @@ -91,8  91,9 @@ export function patchStrictSandbox(appName, appWrapperGetter, proxy) {
  112.  
           rebuildCSSRules(dynamicStyleSheetElements, function (stylesheetElement) {
  113.  
             var appWrapper = appWrapperGetter();
  114.  
             if (!appWrapper.contains(stylesheetElement)) {
  115.  
    -          var mountDom = stylesheetElement[styleElementTargetSymbol] === 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
  116.  
    -          rawHeadAppendChild.call(mountDom, stylesheetElement);
  117.  
              console.log("qiankun-forStrictSandbox")
  118.  
              // var mountDom = stylesheetElement[styleElementTargetSymbol] === 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
  119.  
              // rawHeadAppendChild.call(mountDom, stylesheetElement);
  120.  
               return true;
  121.  
             }
  122.  
             return false;
学新通

多个子应用并行加载,子应用嵌套

  1. 同一个基座并行加载两个或者多个子应用

可以使用loadMicroApp加载多个子应用 

       2. 多路由系统共存带来的 冲突/抢占 问题如何解决?

  1.  
    let historyPath=window.location.pathname.startWith('/vue1/')?process.env.BASE_URL '/vue1/':process.env.BASE_URL
  2.  
    const router = createRouter({
  3.  
        history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? historyPath : process.env.BASE_URL),
  4.  
        // history: createWebHashHistory(),
  5.  
        routes: constantRoutes,
  6.  
    })

同时调用startloadMicroApp会导致子应用render两次,虽然对页面结构和样式没有影响,但是接口都会调用两次,所以在跳出子应用的时候一定要loadMicroApp.unmount()卸载不需要的子应用

qiankun代码入口函数封装

  1.  
    import type { MicroAppStateActions } from 'qiankun';
  2.  
    import QiankunBridge from '@/qiankun/qiankun-bridge'
  3.  
    import {
  4.  
        initGlobalState,
  5.  
        registerMicroApps,
  6.  
        start,
  7.  
        loadMicroApp,
  8.  
        addGlobalUncaughtErrorHandler,
  9.  
        runAfterFirstMounted
  10.  
    } from 'qiankun';
  11.  
    import { getAppStatus, unloadApplication } from 'single-spa';
  12.  
     
  13.  
    export default class Qiankun {
  14.  
        private actions: MicroAppStateActions | null = null
  15.  
        private appMap: any = {}
  16.  
        private prefetchAppMap: any = {}
  17.  
        init() {
  18.  
            this.registerApps()
  19.  
            this.initialState()
  20.  
            this.prefetchApp();
  21.  
            this.errorHandler();
  22.  
        }
  23.  
        registerApps(){
  24.  
            const parentRoute = useRouter();
  25.  
            registerMicroApps([{
  26.  
                name: 'demo-vue',
  27.  
                entry: `${publicPath}/demo-vue`,
  28.  
                container: `#demo-vue`,
  29.  
                activeRule: `${publicPath}${'demo-vue'}`,
  30.  
                props: {
  31.  
                    parentRoute,
  32.  
                    QiankunBridge:new QiankunBridge()
  33.  
                }
  34.  
            }, ...]);
  35.  
        }
  36.  
        initialState(){  
  37.  
            const initialState = {};
  38.  
            // 初始化 state
  39.  
            this.actions = initGlobalState(initialState);
  40.  
            this.actions.onGlobalStateChange((state, prev) => {
  41.  
                // state: 变更后的状态; prev 变更前的状态
  42.  
                console.log(state, prev);
  43.  
            });
  44.  
        }
  45.  
        setState(state: any) {
  46.  
            this.actions?.setGlobalState(state);
  47.  
        }
  48.  
        //预加载
  49.  
        prefetchApp() {
  50.  
            start({
  51.  
                prefetch: 'all',
  52.  
                singular: false,
  53.  
            });
  54.  
        }
  55.  
        //按需加载
  56.  
        demandLoading(apps){
  57.  
           let installAppMap = {
  58.  
              ...store.getters["tabs/installAppMap"],
  59.  
           };
  60.  
           if (!installAppMap[config.name]) {
  61.  
              installAppMap[config.name] = loadMicroApp({
  62.  
                ...config,
  63.  
                configuration: {
  64.  
                    // singular: true
  65.  
                    sandbox: { experimentalStyleIsolation: true },
  66.  
                },
  67.  
                props: {
  68.  
                    getGlobalState: actions.getGlobalState,
  69.  
                    fn: {
  70.  
                        parentRoute: useRouter(),
  71.  
                        qiankunBridge: qiankunBridge,
  72.  
                    },
  73.  
                },
  74.  
            });
  75.  
           }
  76.  
        }
  77.  
        /**
  78.  
         * @description: 卸载app
  79.  
         * @param {Object} app 卸载微应用name, entry
  80.  
         * @returns false
  81.  
         */
  82.  
        async unloadApp(app) {
  83.  
            // await clearCatchByUrl(getPrefetchAppList(addVisitedRoute, router)[0])
  84.  
            const appStatus = getAppStatus('utcus');
  85.  
            if (appStatus !== 'NOT_LOADED') {
  86.  
                unloadApplication(app.name);
  87.  
                // 调用unloadApplication时,Single-spa将执行以下步骤。
  88.  
     
  89.  
                // 在要卸载的注册应用程序上调用卸载生命周期。
  90.  
                // 将应用程序状态设置为NOT_LOADED
  91.  
                // 触发重新路由,在此期间,单spa可能会挂载刚刚卸载的应用程序。
  92.  
                // 由于unloadApplication调用时可能会挂载已注册的应用程序,因此您可以指定是要立即卸载还是要等待直到不再挂载该应用程序。这是通过该waitForUnmount选项完成的。
  93.  
            }
  94.  
        }
  95.  
        //重新加载微应用
  96.  
        reloadApp(app) {
  97.  
            this.unloadApp(app).then(() => {
  98.  
                loadMicroApp(app);
  99.  
            });
  100.  
        }
  101.  
        //加载单个app
  102.  
        loadSingleApp(name) {
  103.  
            if (!this.appMap[name]) {
  104.  
                this.appMap[name] = loadMicroApp(this.prefetchAppMap[name]);
  105.  
            }
  106.  
        }
  107.  
        // 切出单个app,和unloadApp用法不同unloadApp 是卸载start方法生成的应用,unmountSingleApp是卸载loadMicroApp方法生成的应用
  108.  
        async unmountSingleApp(name) {
  109.  
            if (this.appMap[name]) {
  110.  
                await this.appMap[name].unmount();
  111.  
                this.appMap[name] = null;
  112.  
            }
  113.  
        }
  114.  
        //错误处理
  115.  
        errorHandler() {
  116.  
            addGlobalUncaughtErrorHandler((event: any) => {
  117.  
                console.log('addGlobalUncaughtErrorHandler', event);
  118.  
                if (
  119.  
                    event?.message &&
  120.  
                    event?.message.includes('died in status LOADING_SOURCE_CODE')
  121.  
                ) {
  122.  
                    Message('子应用加载失败,请检查应用是否运行''error'false);
  123.  
                }
  124.  
                //子应用发版更新后,原有的js会找不到,所以会报错
  125.  
                if (event?.message && event?.message.includes("Unexpected token '<'")) {
  126.  
                    Message('检测到项目更新,请刷新页面''error'false);
  127.  
                }
  128.  
            });
  129.  
        }
  130.  
    }
学新通

应用事件通信

应用场景:子应用a调用子应用b的事件

  1.  
    const isDuplicate = function isDuplicate(keys: string[], key: string) {
  2.  
        return keys.includes(key);
  3.  
    };
  4.  
    export default class QiankunBridge {
  5.  
        private handlerMap: any = {}
  6.  
        // 单例判断
  7.  
        static hasInstance = () => !!(window as any).$qiankunBridge
  8.  
        constructor() {
  9.  
            if (!QiankunBridge.hasInstance()) {
  10.  
                ; (window as any).$qiankunBridge = this;
  11.  
            } else {
  12.  
                return (window as any).$qiankunBridge;
  13.  
            }
  14.  
        }
  15.  
        //注册
  16.  
        registerHandlers(handlers: any) {
  17.  
            const registeredHandlerKeys = Object.keys(this.handlerMap);
  18.  
            Object.keys(handlers).forEach((key) => {
  19.  
                const handler = handlers[key];
  20.  
                if (isDuplicate(registeredHandlerKeys, key)) {
  21.  
                    console.warn(`注册失败,事件 '${key}' 注册已注册`);
  22.  
                } else {
  23.  
                    this.handlerMap = {
  24.  
                        ...this.handlerMap,
  25.  
                        [key]: {
  26.  
                            key,
  27.  
                            handler,
  28.  
                        },
  29.  
                    };
  30.  
                    console.log(`事件 '${key}' 注册成功`);
  31.  
                }
  32.  
            });
  33.  
            return true;
  34.  
        }
  35.  
        removeHandlers(handlerKeys: string[]) {
  36.  
            handlerKeys.forEach((key) => {
  37.  
                delete this.handlerMap[key];
  38.  
            });
  39.  
            return true;
  40.  
        }
  41.  
        // 获取某个事件
  42.  
        getHandler(key: string) {
  43.  
            const target = this.handlerMap[key];
  44.  
            const errMsg = `事件 '${key}' 没注册过`;
  45.  
            if (!target) {
  46.  
                console.error(errMsg);
  47.  
            }
  48.  
            return (
  49.  
                (target && target.handler) ||
  50.  
                (() => {
  51.  
                    console.error(errMsg);
  52.  
                })
  53.  
            );
  54.  
        }
  55.  
    }
学新通

子应用a注册

  1.  
    import React from "react";
  2.  
    export async function mount(props) {
  3.  
        if (!instance) {
  4.  
            React.$qiankunBridge = props.qiankunBridge;
  5.  
            render(props);
  6.  
        }
  7.  
    }
  8.  
     
  9.  
    React.$qiankunBridge &&
  10.  
    React.$qiankunBridge.registerHandlers({
  11.  
        event1: event1Fn
  12.  
    });

子应用b调用

Vue.$qiankunBridge.getHandler('event1')

项目部署,提示用户更新系统

插件在注册 serviceWorker 时判断 registration 的 waiting 状态, 从而判定 serviceWorker 是否存在新版本, 再执行对应的更新操作, 也就是弹窗提示; 有个弊端就是项目有可能需要经常发版改一些bug,导致更新弹窗频繁出现,所以只能弃用。

对于主项目简单一点的方案就是把版本号数据写入cookie里,通过webpack生成一个json文件部署到服务器,前端代码请求到json文件的版本号做对比,如果不是最新版就弹窗系统更新弹窗。

子项目更新的话,js,css资源会重新编译生成新的链接,所以请求不到, addGlobalUncaughtErrorHandler监听到资源请求错误,直接提示更新弹窗就好

  1.  
    addGlobalUncaughtErrorHandler((event: any) => {
  2.  
        //子应用发版更新后,原有的文件会找不到,所以会报错
  3.  
        if (event?.message && event?.message.includes("Unexpected token '<'")) {
  4.  
            Message('检测到项目更新,请刷新页面''error'false);
  5.  
        }
  6.  
    });

主应用组件重新渲染导致子应用dom消失

问题出现场景:ipad移动端切到宽屏,布局发生变价,vue组件重新渲染导致微服务里面的dom消失 。

解决方案:用single-spaunloadApplication方法卸载子应用,用qiankunloadMicroApp方法重新加载该子应用。

详见上文的 reloadApp方法

切换应用后原有子应用路由变化监听失效

  1.  
    function render(props = {}) {
  2.  
      const { container } = props
  3.  
      router=Router() //添加此行代码,react子应用清空下dom重新调用下ReactDOM.render即可
  4.  
      instance = createApp(App)
  5.  
      instance
  6.  
        .use(router)
  7.  
        .mount(
  8.  
          container
  9.  
            ? container.querySelector('#app-vue')
  10.  
            : '#app-vue'
  11.  
        )
  12.  
    }

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhghcbkb
系列文章
更多 icon
同类精品
更多 icon
继续加载