集成移动端轻量级框架

安装依赖

cir-framework 版本为 3.x ?

yarn add @cvte/cir-framework@3.4.5 @cvte/tz-framework@0.1.0 @cvte/resource-center-sdk@1.7.42

cir-framework 版本为 4.x ?

yarn add @cvte/cir-framework@4.1.4 @cvte/tz-framework@0.1.0 @cvte/resource-center-sdk@1.7.42

其中@cvte/cir-framework@4.1.4需要依赖工程中的elastic-apm-node依赖,如果没有的话则需要安装:

yarn add elastic-apm-node@3.49.1

依赖说明

@cvte/cir-framework:CSB工程原有的框架套件
@cvte/tz-framework:天舟云新版框架套件
@cvte/resource-center-sdk:资源中心开发者套件

工程改造

移除模块联邦配置,替换为资源中心提供运行时共享域

原有的在webpack_override.js中,由于资源中心的机制,需要宿主增加「webpack模块联邦插件」用于共享依赖库;

现在资源中心SDK内部提供了运行时共享域的,所以可以将原有的「webpack模块联邦插件」声明移除;

// plugins.push(
  //   new webpack.container.ModuleFederationPlugin({
  //     name: 'csb',
  //     shared: {
  //       '@cvte/cir-framework': {
  //         eager: true,
  //         singleton: true,
  //       },
  //       '@cvte/wuli-antd': {
  //         eager: true,
  //         singleton: true,
  //       },
  //       'ag-grid-react': {
  //         eager: true,
  //         singleton: true,
  //       },
  //       '~modules/antd': {
  //         eager: true,
  //         shareKey: 'antd',
  //         // singleton: true,
  //         version: '4.23.1',
  //       },
  //       axios: {
  //         eager: true,
  //         singleton: true,
  //       },
  //       mobx: {
  //         eager: true,
  //         singleton: true,
  //       },
  //       react: {
  //         eager: true,
  //         singleton: true,
  //         strictVersion: true,
  //         requiredVersion: packageJson.dependencies.react,
  //       },
  //       'react-dom': {
  //         eager: true,
  //         singleton: true,
  //         strictVersion: true,
  //         requiredVersion: packageJson.dependencies.react,
  //       },
  //     },
  //   })
  // );

同时需要增加从资源中心SDK内部提供的运行时共享域的工具类,此处建议单独放置在一个文件中,我们建议取名为rcShared.ts,如下示例中我们提供了普通CSB工程需要用到的共享依赖库:

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as ReactIntl from 'react-intl';
import * as antd from 'antd';
import axios from 'axios';
import * as mobx from 'mobx';
import { ShareScopeGenerator } from '@cvte/resource-center-sdk';

const scope = new ShareScopeGenerator({
  packerAppName: 'lcp-system', // 此处建议使用宿主应用资源的名称
});
scope.addDependency({
  key: 'react',
  version: '17.0.2',
  dependency: React,
  loaded: 1,
  eager: true,
});
scope.addDependency({
  key: 'react-dom',
  version: '17.0.2',
  dependency: ReactDOM,
  loaded: 1,
  eager: true,
});
scope.addDependency({
  key: 'react-intl',
  version: '3.12.1',
  dependency: ReactIntl,
  loaded: 1,
  eager: true,
});
scope.addDependency({
  key: 'antd',
  version: '4.24.8',
  dependency: antd,
  loaded: 1,
  eager: true,
});
scope.addDependency({
  key: 'axios',
  dependency: axios,
  loaded: 1,
  eager: true,
});
scope.addDependency({
  key: 'mobx',
  version: '5.0.3',
  dependency: mobx,
  loaded: 1,
  eager: true,
});

export default scope;

有了上面的rcShared.ts,我们在如下示例中展示如何来按需使用它:

import Loader, { ReactRemoteLoaderComponent } from '@cvte/resource-center-sdk';
import {
  CENV,
  lcpLoaderName,
  lcpAppName,
} from '~app/common/tools/injectConfig';
import shared from '~app/common/rcShared';

const CSBDetail = ReactRemoteLoaderComponent(
  new Loader({
    appName: lcpAppName,
    name: lcpLoaderName,
    env: CENV,
  }),
  'LCPDetailTemplate',
  {
    useShared: true,
    mode: 'page',
    // 将共享的依赖注入到资源组件的加载参数中
    sharedScope: shared.toSharedScope(),
  }
);

export default CSBDetail;

增加apollo配置,用于提供轻量级框架是否启动和必要的渲染参数

增加apollo配置项

我们建议在tianzhou.env命名空间(如果你有天舟云独立的命名空间请放在对应的空间下)中增加如下键值对配置项:

tzBootstrapConfig = { "isOpen":"1", "entry":"/app/modules/FormTemplate/TZBootstrap.ts", "cdn": [ { "key":"react", "type":"js","src":"https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/react/17.0.2/umd/react.production.min.js" }, { "key":"react-dom", "type":"js","src":"https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/react-dom/17.0.2/umd/react-dom.production.min.js" }, {"key":"react-intl", "type":"js","src":"https://rctest.cvte.com/api/resource/:app/tz-plugin/pro/1.0.3.2/cdnLibs/react-intl@3.12.1/react-intl.min.js"}, { "key":"antd", "type":"js","src":"https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/antd/4.18.9/antd-with-locales.min.js"}, { "key":"antdStyle", "type":"css", "src":"https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-y/antd/4.18.9/antd.min.css"}, { "key":"axios", "type":"js","src":"https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/axios/0.17.1/axios.min.js" }, {"key":"mobx", "type":"js","src":"https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/mobx/5.0.3/mobx.umd.min.js"} ] }

配置项参数说明:

tzBootstrapConfig: {
  isOpen: '1', // 是否开启天舟云轻量级框架,0关闭,1开启
  entry: 'src/index.tsx', // 框架入口引导文件路径
  cdn:[{ // 轻量级框架使用了cdn的方式来加载第三方依赖库
    key: '', // 依赖包key
    type: 'js', // 依赖包类型,js或css
    src: 'https://cdn/react.min.js', // cdn地址
  }]
}

更新server.js

由于apollo配置项与CSB工程的环境变量需要一一对应,所以还需要在config/server.js中做如下新增:

module.exports = {
    // 其他内容
    // 2024.11.11 增加天舟云轻量级框架配置数据 --yuanzihan
    /**
    * tzBootstrapConfig:{
    *  isOpen: '1', // 是否开启天舟云轻量级框架,0关闭,1开启
    *  entry: 'src/index.tsx', // 框架入口引导文件路径
    *  cdn:[{
    *    key: '', // 依赖包key
    *    type: 'js', // 依赖包类型,js或css
    *    src: 'https://cdn/react.min.js', // cdn地址
    *  }]
    * }
    */
    tzBootstrapConfig: process.env.tzBootstrapConfig,
};

增加middleware

ATzBootstrap.js

由于在访问的过程中我们在请求头增加了自定义参数,用来判断当前环境是否应该使用移动端框架来渲染,所以在工程中需要再增加一个移动端渲染适配器中间件,用于拦截请求判断是否应该使用移动端方式渲染

我们建议在app/middleware目录下增加ATzBootstrap.js文件,内容如下:
(为什么文件名要用A开头,是为了在cir-framework框架层遍历读取文件时能够保持在最早读取)

/* eslint-disable max-len */
const {
  tzBootstrapRenderUsedGenerator,
} = require('@cvte/tz-framework/middlewares/useTzBootstrapRender');

// 该文件命名为 AtzBootstrap.js 原因是koa中间件注入时按照字母排序,该文件需要在较早的位置执行,所以命名为 A 开头的文件
module.exports = tzBootstrapRenderUsedGenerator({
  canUseTzBootstrapRender: (ctx) => {
    const UA_MOBILE = [
      'AppleWebKit.*Mobile',
      'Android',
      'BlackBerry',
      'IEMobile',
      'iPhone',
      'iPad',
      'iPod',
      'MIDP',
      'SymbianOS',
      'NOKIA',
      'SAMSUNG',
      'LG',
      'NEC',
      'TCL',
      'Alcatel',
      'BIRD',
      'DBTEL',
      'Dopod',
      'PHILIPS',
      'HAIER',
      'LENOVO',
      'MOT-',
      'Nokia',
      'SonyEricsson',
      'SIE-',
      'Amoi',
      'ZTE',
    ];
    const isMobile = (ua) => new RegExp(UA_MOBILE.join('|')).test(ua);
    // 2024.12.19 健壮场景,在本地开发环境下,referer为空,所以需要使用ctx?.url,在线上环境下,referer不为空,所以需要使用ctx.request.header.referer,此处优先使用referer --yuanzihan
    const url = `${ctx.request.header.referer || ctx?.url}`;
    const [, searchParams] = url.split('?');
    // 在oms系统下,判断是否是低代码页面,如果是低代码页面,不使用tz-bootstrap-render
    // 判断低代码页面的条件是:url的search参数中包含appId或者classId,并且不包含apis则认为是低代码页面
    const result =
      // 2024.12.19 修复bug,在本地开发环境下,searchParams为空,所以需要使用`${searchParams}`,在线上环境下,searchParams不为空,所以需要使用`${searchParams}` --yuanzihan
      (`${searchParams}`.includes('appId') ||
        `${searchParams}`.includes('classId')) &&
      isMobile(ctx.header['user-agent']);
    console.log(
      '🚀 ~ ctx.header url, searchParams,  canUseTzBootstrapRender:',
      url,
      searchParams,
      result
    );
    return result;
  },
});

auth.js

由于轻量级框架会调用渲染用户登陆页,考虑到我们没有再依赖原有的框架加载登陆页面,所以为此我们单独增加了鉴权的中间件用于拦截轻量级登陆页的请求接口

我们建议在app/middleware目录下增加auth.js文件,内容如下:

const { request: requestHelper, redisHelper, iacService, messageHelper } = require('@cvte/cir-framework/server');
const { isDevelopment, needSecureAndSameSite } = require('@cvte/cir-framework/common/tools');
const appConfig = require('@cvte/cir-framework/config').default.app;
const { authMiddlewareGenerator } = require('@cvte/tz-framework/middlewares/auth');
const apiConfig = require('../../config/server');

// 如果开启了tzBootstrapConfig,且是移动端,且配置了轻量级框架entry,则拦截登录、登出、获取用户信息接口
module.exports = authMiddlewareGenerator({
  requestHelper,
  redisHelper,
  iacService,
  messageHelper,
  isDevelopment,
  needSecureAndSameSite,
  appConfig,
  apiConfig,
});

增加框架入口引导启动文件

在上面的apollo配置中,我们已经配置了tzBootstrapConfig内容,其中entry字段就是指定根目录下哪一个文件用来做为轻量级框架的引导启动,我们建议将该文件命名为TZBootstrap.ts,如下就是关于实现它的示例:

⏰ 关于登陆页面资源组件:
如果你的天舟云版本是2.9及其以上,请使用tz-app@1.0.8的资源版本;否则请使用cir-login@1.0.1的资源版本;

import '../App/style.less'; // 此处是宿主系统中所使用的基础样式,一般是在global.js中引入的样式,需要将其在此处引入
import GlobalRootApp from '@cvte/resource-center-sdk/src/loader/rootAppController'; // 资源全局根宿主应用管理实例
import { ShareScopeGenerator } from '@cvte/resource-center-sdk/src/lib/shareScopeGenerator'; // 资源运行时共享域管理类
import { Loader } from '@cvte/resource-center-sdk/src/loader'; // 资源加载类
// eslint-disable-next-line max-len
import { ReactRuntimeRemoteLoaderComponent } from '@cvte/resource-center-sdk/src/lib/components/ReactRuntimeRemoteLoaderComponent'; // 资源react远程加载组件的运行时版本
import { Bootstrap, Booter } from '@cvte/tz-framework'; // 天舟轻量级框架引导类和启动器类型
import {
  TZRenderContainer,
  RCRenderTemplate,
} from '@cvte/tz-framework/TZRenderContainer'; // 天舟轻量级框架渲染容器

const FrameworkBootstrap = new Bootstrap(); // 初始化框架启动器实例

// 启动执行内容
const tianzhou = (_booter?: Booter) => {
  // 从window中获取当前CDN加载的实例
  const depSource = globalThis;
  const { React, ReactDOM, ReactIntl, antd, mobx, axios } = depSource as any;

  if (!React || !ReactDOM) {
    throw new Error(
      '全局依赖中不包含React和ReactDOM,请确保全局CDN依赖中包含React和ReactDOM'
    );
  }

  if (!_booter) {
    throw new Error('框架启动器Booter实例未初始化!');
  }

  const scope = new ShareScopeGenerator({
    packerAppName: 'lcp-system',
  });

  // 在原有的逻辑中,需要有一层XHR的劫持修改api请求的地址,在其前面加上当前应用前缀
  const customizeOpen = (booter: Booter) => {
    const { routerPrefix } = booter.runtimeContext.getExtra() || {};
    if (!(XMLHttpRequest.prototype as any).nativeOpen) {
      (XMLHttpRequest.prototype as any).nativeOpen =
        XMLHttpRequest.prototype.open;
      const _customizeOpen = function (method, url, ...params) {
        if (
          routerPrefix !== '/pages' &&
          // 热更新不处理
          url.indexOf('hot-update.json') < 0 &&
          // 跨域直接请求不处理
          url.indexOf('https://') < 0 &&
          url.indexOf('http://') < 0 &&
          // apm 上报不处理
          url.indexOf('/rum/events') < 0
        ) {
          // eslint-disable-next-line no-param-reassign
          url = `${routerPrefix}_${url.slice(1)}`;
        }
        // url = `/cir_framework_${url.slice(1)}`
        this.nativeOpen(method, url, ...params);
        // this.setRequestHeader('x-forwarded-protocol', window.location.protocol);
      };

      XMLHttpRequest.prototype.open = _customizeOpen;
    }
  };
  customizeOpen(_booter);
  // 调用启动器提供的身份校验api
  const authMe = _booter.auth.init();

  // 天舟云渲染容器API
  TZRenderContainer(_booter, {
    // 准备第三方依赖库生命周期回调
    onPrepareThridDeps: () => {
      const deps = [
        {
          key: 'react',
          version: '17.0.2',
          dep: React,
          loaded: 1,
          eager: true,
        },
        {
          key: 'react-dom',
          version: '17.0.2',
          dep: ReactDOM,
          loaded: 1,
          eager: true,
        },
        {
          key: 'react-intl',
          version: '3.12.1',
          dep: ReactIntl,
          loaded: 1,
          eager: true,
        },
        {
          key: 'antd',
          version: '4.18.9',
          dep: antd,
          loaded: 1,
          eager: true,
        },
        {
          key: 'mobx',
          version: '5.0.3',
          dep: mobx,
          loaded: 1,
          eager: true,
        },
        {
          key: 'axios',
          dep: axios,
          loaded: 1,
          eager: true,
        },
      ] as any[];
      deps.forEach((item) => {
        const { key, dep, ...oItem } = item || {};
        scope.addDependency({
          ...(oItem || {}),
          key,
          dependency: dep,
        });
      });
      return {
        deps,
      };
    },
    // 准备第三方依赖库之后的装载生命周期回调
    onMount: (booter: Booter) => {
      const { lcpAppName } = booter.runtimeContext.getExtra() || {};
      // 2022.08.23 设置根宿主容器资源名称 --yuanzihan
      GlobalRootApp.setRootApp({
        name: lcpAppName || 'lcp-2-9-app',
        sharedScope: scope.toSharedScope(),
      });
    },
    // 装载之后的准备本地国际化内容生命周期回调
    onPrepareI18NLocale: () => {},
    // 准备本地国际化内容之后的准备内容渲染生命周期回调
    onPrepareContentRenderer: async (booter: Booter) => {
      const resp = await authMe.then();
      console.log('🚀 ~ onPrepareContentRenderer: ~ resp:', resp);

      /**
       * 主内容渲染逻辑
       *
       * @return {*}
       */
      const mainContent = () => {
        // 获取用户信息成功时,根据URL的资源参数跳转到不同的渲染模版
        const urlParams = {};
        new URLSearchParams(globalThis.location.search).forEach((v, k) => {
          urlParams[decodeURIComponent(k)] = decodeURIComponent(v);
        });
        const [pageTypeFlag] = `${globalThis.location.pathname}`
          .split('/')
          .slice(-1);

        const { resourceName, exposeName, ...props } = (urlParams || {}) as any;
        // 在search参数中找到对应的资源参数,根据资源参数渲染对应的页面
        if (resourceName && exposeName) {
          let [sourceName, exposesKey] = [resourceName, exposeName];
          if (
            [
              'lcp-form-components',
              'lcp-layout-components',
              'lcp-page-components',
              'cir-csb-common-view',
              'cir-csb-generator',
              'lcp-data-object',
              'cir-datart',
              'viewTable',
            ].includes(resourceName)
          ) {
            sourceName = 'tz-render';
          }
          if (
            ['cir-formtransfer', 'cir-state-flow-manager'].includes(sourceName)
          ) {
            const splitToCannal = (str: string): string => {
              const reg = /-(\w)/g;
              // eslint-disable-next-line no-param-reassign
              str = str.replace(reg, ($: string, $1: string) => {
                return $1.toUpperCase();
              });
              return str;
            };
            exposesKey = `${splitToCannal(sourceName)}.${exposeName}`;
            sourceName = 'tz-design';
          }
          return RCRenderTemplate(
            booter,
            {
              Loader,
              ReactRemoteLoaderComponent: ReactRuntimeRemoteLoaderComponent,
              resource: sourceName,
              expose: exposesKey,
              extra: {
                deps: {
                  react: React,
                  reactDom: ReactDOM,
                  antd,
                },
              },
            },
            {
              ...props,
              ref: {},
              booter,
            }
          );
        }
        // 在path中找到对应的资源参数,根据资源参数渲染对应的页面
        if (['detail', 'list'].includes(pageTypeFlag)) {
          const exposesKey = pageTypeFlag === 'detail' ? 'Detail' : 'List';
          return RCRenderTemplate(
            booter,
            {
              Loader,
              ReactRemoteLoaderComponent: ReactRuntimeRemoteLoaderComponent,
              resource: 'tz-render',
              expose: exposesKey,
              extra: {
                deps: {
                  react: React,
                  reactDom: ReactDOM,
                  antd,
                },
              },
            },
            {
              ...props,
              ref: {},
              booter,
            }
          );
        }

        // 如果URL中未能找到对应的资源参数,则返回空页面
        const { Empty } = booter.runtimeContext.getThridParty('antd') || {};
        return React.createElement(Empty, {
          description: `当前URL中未能找到对应的内容(${globalThis.location.href})`,
          style: { marginTop: '25vh' },
        });
      };

      // 获取用户信息失败时,跳转到登录页
      if (!resp) {
        return () => {
          return RCRenderTemplate(
            booter,
            {
              Loader,
              ReactRemoteLoaderComponent: ReactRuntimeRemoteLoaderComponent,
              resource: 'tz-app', // 如果没有升级到天舟云2.9及其以上,需要使用cir-login@1.0.1的资源版本
              expose: 'MobileLiteLogin',
              extra: {
                deps: {
                  react: React,
                  reactDom: ReactDOM,
                  antd,
                },
              },
            },
            {
              booter,
              onAfterSuccessRender: () => {
                return mainContent();
              },
            }
          );
        };
      }

      return mainContent;
    },
  });
};
//启动器开始渲染启动执行内容
FrameworkBootstrap.render(tianzhou);
作者:袁子涵  创建时间:2024-11-26 08:49
最后编辑:袁子涵  更新时间:2025-05-08 10:37