UNPKG

@lcap/nasl

Version:

NetEase Application Specific Language

1,226 lines (1,217 loc) 63.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getOneFiles = exports.genFrontendBundleFiles = exports.genBundleFiles = exports.stringifyMetaData = exports.checkOfficalPermissTemplate = exports.officalEntityList37 = exports.entityList = void 0; const JSON5 = __importStar(require("json5")); const VueCompiler = __importStar(require("vue-template-compiler")); const vue_template_es2015_compiler_1 = __importDefault(require("vue-template-es2015-compiler")); const utils = __importStar(require("../utils")); const config_1 = require("../config"); const breakpoint_1 = require("../breakpoint"); const nasl_log_1 = require("@lcap/nasl-log"); const nasl_utils_1 = require("@lcap/nasl-utils"); const nasl_translator_1 = require("@lcap/nasl-translator"); const concepts_1 = require("../concepts"); const compileComponent_1 = require("./compileComponent"); const nasl_unified_frontend_generator_1 = require("@lcap/nasl-unified-frontend-generator"); const microApp_1 = require("./microApp"); // @ts-ignore FIXME 类型在构建前找不到的问题 const nasl_unified_frontend_generator_2 = require("@lcap/nasl-unified-frontend-generator"); const axios_1 = __importDefault(require("axios")); const officalEntityMap = { LCAPUser: ['id', 'createdTime', 'updatedTime', 'userId', 'userName', 'password', 'phone', 'email', 'displayName', 'status', 'source'], LCAPLogicViewMapping: ['id', 'logicIdentifier', 'resourceName', 'resourceType', 'group', 'changeTime'], LCAPRolePerMapping: ['id', 'createdTime', 'updatedTime', 'createdBy', 'updatedBy', 'roleId', 'permissionId'], LCAPPerResMapping: ['id', 'createdTime', 'updatedTime', 'createdBy', 'updatedBy', 'permissionId', 'resourceId'], LCAPUserRoleMapping: ['id', 'createdTime', 'updatedTime', 'createdBy', 'updatedBy', 'userId', 'roleId', 'userName', 'source'], LCAPRole: ['id', 'createdTime', 'updatedTime', 'createdBy', 'updatedBy', 'uuid', 'name', 'description', 'roleStatus', 'editable'], LCAPPermission: ['id', 'createdTime', 'updatedTime', 'createdBy', 'updatedBy', 'uuid', 'name', 'description'], LCAPResource: ['id', 'createdTime', 'updatedTime', 'createdBy', 'updatedBy', 'uuid', 'name', 'description', 'type', 'clientType'], LCAPUserDeptMapping: ['id', 'createdTime', 'updatedTime', 'createdBy', 'updatedBy', 'userId', 'deptId', 'isDeptLeader'], LCAPDepartment: ['id', 'createdTime', 'updatedTime', 'createdBy', 'updatedBy', 'name', 'deptId', 'parentDeptId'], }; exports.entityList = Object.keys(officalEntityMap); exports.officalEntityList37 = exports.entityList.filter(it => it !== "LCAPLogicViewMapping"); function checkOfficalPermissTemplate(app) { let defaultDS = app.dataSources.find((ds) => ds.name === 'defaultDS'); let appEntityList = defaultDS?.entities?.map((it) => it.name) ?? []; let isReact = false; const pcFrontendType = app?.frontendTypes?.find((it) => it?.kind === 'pc'); isReact = pcFrontendType?.frameworkKind === 'react'; if (isReact) { // 兼容:react 权限模板无人更新 // 临时方案可以特殊处理react 不判断这两张表 exports.officalEntityList37 = exports.officalEntityList37.filter(it => !['LCAPUserDeptMapping', 'LCAPDepartment'].includes(it)); } return exports.officalEntityList37.every((entity) => appEntityList.includes(entity)); } exports.checkOfficalPermissTemplate = checkOfficalPermissTemplate; // 将metaData转成字符串 function stringifyMetaData(obj) { // 处理对象类型 if (typeof obj === 'object' && obj !== null) { if (Array.isArray(obj)) { // 处理数组类型 const elements = obj.map(element => stringifyMetaData(element)); return `[${elements.join(',')}]`; } let newObj = obj; const properties = []; if (obj.concept === 'FrontendVariable') { // speed consideration newObj = obj.toJSON() || {}; newObj.defaultCode = obj.defaultCode; const defaultValueFn = obj.defaultValue?.toJS((0, nasl_translator_1.createCompilerState)('', { getVuePrototype: true })); if (obj.defaultCode.executeCode && defaultValueFn) { properties.push(`"defaultValueFn": (Vue) => { return ${defaultValueFn} }`); } } Object.keys(newObj).forEach(key => { const value = stringifyMetaData(newObj[key]); if (value !== undefined) properties.push(`${stringifyMetaData(key)}: ${value}`); }); return `{ ${properties.join(',')} }`; } return JSON5.stringify(obj); } exports.stringifyMetaData = stringifyMetaData; // 编译模板, 类似 vue-loader 的 compileTemplate function compileTemplate(template) { const toFunction = (code) => { return `function () {${code}}`; }; let code; try { const start = Date.now(); const { render, staticRenderFns } = VueCompiler.compile(template); code = (0, vue_template_es2015_compiler_1.default)(`var __render__ = ${toFunction(render)}\n` + `var __staticRenderFns__ = [${staticRenderFns.map(toFunction)}]`, { transforms: { stripWithFunctional: false } }); const duration = Date.now() - start; duration > 100 && console.info(`单页面 compileTemplate 耗时较长: ${duration}ms`); } catch (error) { console.error('编译模板失败:', error); } return code; } // 生成组件 function genComponentCode(component) { let renderCode = compileTemplate(component.template); const optionCode = renderCode ? `render: __render__,\n staticRenderFns: __staticRenderFns__,` : `template: \`${component.template.replace(/[`$]/g, (m) => `\\${m}`)}\`,`; return `(function(){ var componentOptions = ${component.script ? `(function(){\n${component.script.trim().replace(/export default |module\.exports +=/, 'return ')}\n})()` : '{}'}; ${renderCode} Object.assign(componentOptions, { ${optionCode} }); return componentOptions; })()`; } // 生成导入组件代码 function genImportComponetCode(componentPath) { return `() => importComponent('${componentPath}')`; } // 生成导出组件代码 function genExportComponetCode(component) { let renderCode = compileTemplate(component.template); const optionCode = renderCode ? `render: __render__,\n staticRenderFns: __staticRenderFns__,` : `template: \`${component.template.replace(/[`$]/g, (m) => `\\${m}`)}\`,`; return `var componentOptions = ${component.script ? `(function(){\n${component.script.trim().replace(/export default |module\.exports +=/, 'return ')}\n})()` : '{}'}; ${renderCode} Object.assign(componentOptions, { ${optionCode} }); return componentOptions;`; } // 获取文件路径 // name 或者 文件 至少有一个必填 const getCompletePath = (name, fileContent, config) => { let fileName = ''; if (name) { fileName += `${name}.`; } if (fileContent) { fileName += `${(0, nasl_utils_1.genHash)(fileContent)}.`; } fileName += `min.js`; // if(config?.isExport && !fileName.includes('bundle')&& !fileName.includes('router')){ // fileName =`${config.sysPrefixPath}/${fileName}` // } return fileName; }; function genRouterFileContent(routes, defaultRoute) { function routeToString(route) { let content = `{ path: '${route.path}',\n`; if (route?.lazyPath) { const routeLazyPath = route.lazyPath; content += `lazyPath: '${routeLazyPath}',\n`; content += `component: ${genImportComponetCode(route.lazyPath)},\n`; } if (route?.meta) { content += `meta: ${stringifyMetaData(route.meta)},`; } if (route?.children?.length) { content += `children: [ ${route.children.map(routeToString).join(',\n')} ],\n`; } if (route?.redirect) { content += `redirect: '${route?.redirect}',\n`; } content += '}'; return content; } // 生成路由配置 let routesStr = `const routers = [`; routes.forEach((route) => { routesStr += `${routeToString(route)},\n`; }); if (defaultRoute) { routesStr += `{ path: '*', redirect: '${defaultRoute}', }\n`; } routesStr += `]; return routers;`; return routesStr; } // 生成路由文件 function genRouteFiles(routes, defaultRoute, config) { // 生成路由文件列表 const routeFiles = []; function routeToFile(route) { if (route?.component?.script) { const content = genExportComponetCode(route.component); const lazyPath = getCompletePath(null, content, config); route.lazyPath = lazyPath; // let outFilePath =`${config?.sysPrefixPath? config?.sysPrefixPath +'/' :''}${lazyPath}`; routeFiles.push({ name: lazyPath, content, }); } if (route?.children?.length) { route.children.map(routeToFile); } } routes.forEach((route) => { routeToFile(route); }); let routerPath = getCompletePath('router', null, config); routeFiles.push({ name: routerPath, isRouterFile: true, content: genRouterFileContent(routes, defaultRoute), }); return routeFiles; } async function genBundleFiles(app, frontend, config) { config.sysPrefixPath = app?.sysPrefixPath; config.isPureFeMode = app?.preferenceMap?.viewMode === 'fe'; // 获取端类型 const frontendType = frontend.getAncestor('FrontendType'); // 获取业务组件 const businessComponents = frontendType?.businessComponents || []; const configEntrancePort = config?.entrancePort; const configLowcodeDomain = config?.lowcodeDomain; const fnNuimsDomain = config?.envNuimsDomain?.[config?.env] || config?.nuimsDomain; const fnLowcodeDomain = config?.envLcpDomain?.[config?.env]?.lcpDomain || config?.lowcodeDomain; const modules = []; const views = []; const STATIC_URL = config.isExport ? '' : config.STATIC_URL; app.dependencies && modules.push(...app.dependencies); app.interfaceDependencies && modules.push(...app.interfaceDependencies); app.connectorDependencies && modules.push(...app.connectorDependencies); frontend.views && views.push(...frontend.views); modules.forEach((module) => { module.views && views.push(...module.views); }); const componentMap = {}; // 需要放在清除断点节点之前 let compRegStr = ''; try { if ((!config?.isExport && config?.env === 'dev') || config?.debug) { (0, nasl_log_1.genLogs)(app); } config?.debug && (0, breakpoint_1.genBreakpoints)(app); app.curDeployEnv = config.env; console.time('toVueOptions'); let diffNodePaths = []; // 如果不需要全量发布,而且有配置diffNodePaths,表示只导出配置的节点,就只发列表中的 // 其余的情况都算需要发布 if (!config?.isFull && config?.diffNodePaths?.length) { diffNodePaths = config.diffNodePaths; } for (const view of views) { const toVueOptionsLam = ((nd) => { if (nd.concept !== 'View') { return; } let viewFlag = false; if (nd.toVueOptions) { if (diffNodePaths.length) { // 如果当前有列表长度,而且当前节点路径在列表中,表示需要生成代码 if (diffNodePaths.includes(nd.nodePath)) { viewFlag = true; } } else { // 如果没有配置diffNodePaths,表示全部导出 // 每个页面都发就对了 viewFlag = true; } } if (viewFlag) { componentMap[nd.id] = nd.toVueOptions({ isExport: !!config.isExport, isRelease: !!config.realRelease, }); } }); // @ts-ignore await traverseForToVueOptions(view.__v_raw ?? view, toVueOptionsLam); } console.timeEnd('toVueOptions'); businessComponents.forEach((businessComponent) => { let tempParentNode = businessComponent.parentNode; businessComponent.parentNode = frontend; const frontendI18nEnabled = frontend?.i18nInfo?.enabled; const vueOptions = businessComponent.toVueOptions({ frontendI18nEnabled }); const component = (0, compileComponent_1.compileComponent)(vueOptions); compRegStr += `window.Vue.component('bs-${businessComponent.name}', ${genComponentCode(component)},\n);`; businessComponent.parentNode = tempParentNode; }); } finally { config?.debug && (0, breakpoint_1.clearBreakpoints)(app); if ((!config?.isExport && config?.env === 'dev') || config?.debug) { (0, nasl_log_1.clearLogs)(app); } } const getBundleFileName = () => { if (config.isPreviewFe && config.previewVersion) { return `mockBundle-${config.previewVersion}`; } else { return 'bundle'; } }; let baseUrl = `${config.USER_STATIC_URL}/${config.tenant}/${app.id}/${config.env}`; let { basePath } = frontend; // isPreviewFe 资产中心的预览模式,优先级最高 if (config.isPreviewFe) { baseUrl = `${config.USER_STATIC_URL}/asset-center/preview/${config.tenant}/${app.id}/${config.env}`; } else if (config.isExport) { baseUrl = ''; } let completePath = `${baseUrl}${basePath || ''}/`; /** * vue.config.js page options */ const routes = []; // 开启了权限的页面 const authResourceViews = []; const baseResourcePaths = []; const rootViewData = []; let defaultRoute = ''; let viewCount = 0; // 遍历页面 const traverseViews = (view, isParentViewAuth) => { viewCount++; if (viewCount === 15) { // 预留时间做 minor gc,防止峰值内存过高 utils.delay(55); viewCount = 0; } const parentNode = view.parentNode || {}; const isRootView = parentNode.concept !== 'View'; const viewName = view.name; // 页面是否需要权限,如果父页面需要权限,子页面也一定需要 const isViewAuth = isParentViewAuth || view.auth; // 路由地址 let routePath = viewName; if (isRootView) { rootViewData.push({ name: viewName, title: view.title || viewName, isIndex: view.isIndex }); routePath = `${frontend.prefixPath}/${viewName}`; if (!isViewAuth) { if (viewName === 'notFound') { defaultRoute = routePath; } if (view.isIndex) { if (!defaultRoute) { defaultRoute = routePath; } } } } const route = { path: routePath, component: (0, compileComponent_1.compileComponent)(componentMap[view.id]), children: [], meta: view.getRouteMeta(), }; const viewChildren = view.children; if (Array.isArray(viewChildren) && viewChildren.length) { for (let i = 0; i < viewChildren.length; i++) { const subView = viewChildren[i]; // @ts-ignore const { route: subViewRoute } = traverseViews(subView.__v_raw ?? subView, isViewAuth); route.children.push(subViewRoute); if (subView.isIndex) { route.children.push({ path: '', redirect: subView.name, }); } } } const viewPath = view.path; if (isViewAuth) { authResourceViews.push(view); } if (!isViewAuth) { baseResourcePaths.push(viewPath); } return { route, isViewAuth, }; }; // 页面 views.forEach((view) => { // @ts-ignore const { route } = traverseViews(view.__v_raw ?? view); routes.push(route); if (view.isIndex) { routes.push({ path: frontend.prefixPath ? frontend.prefixPath : '/', redirect: `${frontend.prefixPath}/${view.name}`, }); } }); if (frontend.prefixPath) { routes.push({ path: '/', redirect: frontend.prefixPath, }); } let authResourcePathMap = {}; // 默认跳转子页面开启权限的情况,需要把父页面也都加入权限校验列表 if (Array.isArray(authResourceViews)) { authResourceViews.forEach((authResourceView) => { if (authResourceView) { authResourcePathMap[authResourceView.path] = true; let viewNode = authResourceView; while (viewNode.concept === 'View' && viewNode.isIndex) { const parentViewNode = viewNode.parentNode; if (parentViewNode.concept === 'View') { authResourcePathMap[parentViewNode.path] = true; } else { // viewNode是根页面 if (frontend.prefixPath) { authResourcePathMap[frontend.prefixPath] = true; } authResourcePathMap['/'] = true; } viewNode = parentViewNode; } } }); } if (!checkOfficalPermissTemplate(app)) { // authResourcePathMap={} } const authResourcePaths = Object.keys(authResourcePathMap); const routerFiles = genRouteFiles(routes, defaultRoute, config); routerFiles.forEach(item => { let temp = completePath; // if(completePath=='/'){temp=''}else{temp = `${completePath}/`} if (config?.isExport && config?.sysPrefixPath?.length > 0) { temp = config?.sysPrefixPath + temp; } item.name = temp + item.name; }); let fileI18nInfo = { enabled: false, locale: 'zh-CN', messages: {}, I18nList: [], }; if (frontend?.i18nInfo?.enabled) { const naslI18nInfo = frontend.i18nInfo?.toJSON(); // 因为内部还有对象嵌套,先序列化一下 fileI18nInfo = JSON.parse(JSON.stringify(naslI18nInfo)); fileI18nInfo.messages = fileI18nInfo.messageMap; delete fileI18nInfo.messageMap; const i18nMessages = fileI18nInfo.messages || {}; // 发布前公布下最新文本翻译 const frontEndI18nNodes = frontend.getI18nKeyNodes(); const frontEndI18nMap = {}; frontEndI18nNodes.forEach((node) => { frontEndI18nMap[node['i18nKey']] = node.value; }); // 如果其他语言内容这里的没有设置,就复用中文的翻译 const infoZhMessages = i18nMessages['zh-CN']; const AllZhMessages = { ...infoZhMessages, ...frontEndI18nMap }; // 中文列表直接覆盖 i18nMessages['zh-CN'] = AllZhMessages; // 遍历所有语言,然后把 别的语言没有的文本,赋值为中文的文本 // 循环遍历messages,中文的message 赋值到别的语言中 Object.keys(i18nMessages).forEach((key) => { if (key !== 'zh-CN') { const i18nMessage = i18nMessages[key]; Object.keys(AllZhMessages).forEach((zhKey) => { // 如果发布的时候国际化节点存储的内容和文本的不一样,其余语言的内容都要替换成文本的内容 // 前提是这个是nasl节点的内容 if (frontEndI18nMap[zhKey] && infoZhMessages[zhKey] !== frontEndI18nMap[zhKey]) { i18nMessage[zhKey] = AllZhMessages[zhKey]; } else if (!i18nMessage[zhKey]) { // 如果单纯的没配置,就用主语言的 i18nMessage[zhKey] = AllZhMessages[zhKey]; } }); } }); fileI18nInfo.I18nList = frontend.getCurrentLanguageList(); } const platformConfig = JSON5.stringify({ appConfig: { id: app.id, project: app.title, domainName: app.name, nuimsDomain: fnNuimsDomain, entrancePort: configEntrancePort, lowcodeDomain: configLowcodeDomain, envNuimsDomain: config.envNuimsDomain, tenantType: config.tenantType, tenantLevel: config.tenantLevel, extendedConfig: config.extendedConfig, envConfig: { lowcodeDomain: fnLowcodeDomain, }, tenant: config.tenant, documentTitle: frontend.documentTitle, rootViewData, basePath: frontend.prefixPath, frontendName: frontend.name, // 加上统一前缀 sysPrefixPath: app.sysPrefixPath, // 应用时区 appTimeZone: app?.appTimeZone, // 国际化 i18nInfo: fileI18nInfo, }, dnsAddr: app.dnsAddr, devDnsAddr: config.devDnsAddr, tenant: config.tenant, env: config.env, hasUserCenter: app.hasUserCenter, hasAuth: app.hasAuth, authResourcePaths, baseResourcePaths, miniEnable: config.miniEnable, isPreviewFe: config?.isPreviewFe, }, null, 4); config?.debug && (0, breakpoint_1.genBreakpoints)(app); let metaData; let metaDataStr; try { metaData = (0, nasl_unified_frontend_generator_1.genMetaData)(app, frontend, config); // 这个 genBundleFiles 里面不能写 await utils.delay,写了切出去后就暂停了…… if (utils.isDebugMode) { console.error('[DEBUG] metaData sanitycheck: 启用'); function sanityCheckMetaData(obj) { if (typeof obj !== 'object' || obj == null) { return; } // 如果obj instanceof BaseNode,或者 obj.__v_raw 非空,那么报错。否则检查属性。 if (obj instanceof concepts_1.BaseNode || obj.__v_raw) { console.error('metaData 中不能包含 BaseNode 或 Proxied 的对象', obj); throw new Error('metaData 中不能包含 BaseNode 或 Proxied 的对象'); } Object.keys(obj).forEach(key => sanityCheckMetaData(obj[key])); } sanityCheckMetaData({ ...metaData, frontendVariables: undefined }); } metaDataStr = stringifyMetaData(metaData); metaData = null; } catch (error) { throw error; } finally { config?.debug && (0, breakpoint_1.clearBreakpoints)(app); } const assetsInfo = app.genAllAssetsInfo(STATIC_URL, frontend.type, frontendType.frameworkKind); const customNames = JSON5.stringify(assetsInfo.custom.names); let content1 = `(async function(){ `; let contentScale = ''; if (frontend.globalScaleEnabled) contentScale = `{ // mobile 用 viewport 缩放,pc 和 ipad 用 iframe 缩放 if(navigator.userAgent.match(/mobile/i)) { // 使用viewport缩放,用js计算viewport的缩放比例 // 创建meta标签 const meta = document.createElement('meta'); meta.name = 'viewport'; meta.content = 'width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no'; function scalePage() { // 计算缩放比例 const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const baseWidth = ${frontend.canvasWidth || frontend.defaultCanvasWidth}; const scale = windowWidth / baseWidth; // 设置viewport缩放 document.querySelector('meta[name="viewport"]').content = 'width=' + baseWidth + ' , initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no'; } // 初始化缩放 scalePage(); } else { // 如果是顶层页面,才使用iframe缩放,否则会死循环 if(!window.frameElement || !window.frameElement.getAttribute('isScaledFrame')){ // 顶层页面里有个iframe,用来缩放页面 document.body.innerHTML = '<iframe src="' + location.href + '"></iframe>'; const iframe = document.querySelector('iframe'); function scalePage() { // 计算缩放比例 const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const baseWidth = ${frontend.canvasWidth || frontend.defaultCanvasWidth};; const scale = windowWidth / baseWidth; // iframe 按比例缩放 iframe.style.transform = 'scale(' + scale + ')'; iframe.style.transformOrigin = '0 0'; // iframe 的宽高需要缩放回去,使其等于顶层宽高 iframe.style.width = baseWidth + 'px'; iframe.style.height = 100 / scale + 'vh'; // 消除底部白边,参考:stackoverflow.com/a/21025344/5710452 iframe.style.display = 'block'; let marginRight = '0px'; let marginBottom = '0px'; if(scale < 1){ marginRight = (scale - 1)/scale * 100 + '%'; // 由于 marginBottom 的百分比也是基于宽度的,因此需要乘以 windowHeight / windowWidth marginBottom = (scale - 1)/scale * windowHeight / windowWidth * 100 + '%'; } iframe.style.marginRight = marginRight; iframe.style.marginBottom = marginBottom; } // 监听窗口变化 window.addEventListener('resize', scalePage); // 初始化缩放 scalePage(); // 禁止滚动条,原因:缩放后会出现滚动条 document.documentElement.style.setProperty('--scrollbar-size', 0); // 移除 iframe 边框 iframe.style.border = 'none'; // 标记全局缩放的iframe,原因:全局缩放页面可能在 iframe 中 iframe.setAttribute('isScaledFrame', 'true'); return; } else { // 点击跳转到外部页面时,通知顶层页面 document.body.addEventListener('click', (e) => { if(!e.target.href) return; if(typeof e.target.href !== 'string') return; if(e.target.tagName !== 'A') return; if(e.target.download) return; try { const url = new URL(e.target.href); // 只有绝对路径且不是新窗口打开的链接修改顶层页面地址 if(e.target.attributes.href.value.startsWith('http') && e.target.target !== '_blank'){ parent.location.href = e.target.href; } } catch(err) { console.error(err); } }); } } }`; let themeCSS = frontend.genThemeCSS(); let contentStyleCss = themeCSS ? `{ const el = document.createElement('style'); el.id = 'theme'; el.innerHTML = \`${themeCSS}\`; document.head.appendChild(el); } ` : ''; themeCSS = null; let contentDocIcon = frontend.documentIcon ? `{ const link = document.createElement('link'); link.rel = 'shortcut icon'; link.href = \`${frontend.documentIcon}\`; document.head.appendChild(link); }` : ''; let contentCustomNames = ` var customNames = ${customNames}; for(var i=0;i<customNames.length;i++){ var name = window.kebab2Camel(customNames[i]); if(window[name]){ const install = window.LcapInstall || (window.CloudUI && window.CloudUI.install) || (() => { console.error('未提供全局组件install方法') }) install(window.Vue, window[name]); } }`; let contentImport = ` var platformConfig = ${platformConfig}; var metaData = ${metaDataStr}; var getCompletePath = (path) => { return '${config?.isExport ? config.sysPrefixPath : ''}${completePath}' + path; } // 低版本 function importXMLHttpRequestComponent (scriptUrl) { return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('GET', scriptUrl, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { var scriptContent = xhr.responseText; try { var func = new Function('importComponent', 'window', scriptContent + '\\n//# sourceURL=' + scriptUrl); var result = func(window.importComponent, window); resolve(result); } catch (error) { if (typeof window.$appUtils.createErrorLayout === 'function') { window.$appUtils.createErrorLayout(error); } reject(error); } } else { reject(new Error('Failed to fetch script')); } } }; xhr.send(); }); }; function importFetchComponent(scriptUrl) { return fetch(scriptUrl) .then(function(response) { if (!response.ok) { throw new Error('Network response was not ok'); } return response.text(); }).then(function(scriptText) { var func = new Function('importComponent', 'window', scriptText + '\\n//# sourceURL=' + scriptUrl); var result = func(window.importComponent, window); return result; }) .catch(error => { console.error('Error loading script:', error); if (typeof window.$appUtils.createErrorLayout === 'function') { window.$appUtils.createErrorLayout(error); } }); } window.importComponent = (scriptUrl) => { scriptUrl = getCompletePath(scriptUrl); // 浏览器支持fetch,直接使用fetch if (window.fetch) { return importFetchComponent(scriptUrl); } return importXMLHttpRequestComponent(scriptUrl); } var routes = await importComponent('${getCompletePath('router', null, config)}?t=' + Date.now()); `; // 按页面维度发布先不考虑鉴权,页面维度新增的页面都算是不鉴权的数组中 // 只有完全页面维度新增的才会进入到这个数组中 utils.delay(100); let contentLogReportCode = config.env === 'dev' ? (0, nasl_log_1.logReportCode)() : ''; let contentRouter = config.env === 'dev' ? ` function iterateRouters(routers, parentPath = '') { routers.forEach(function(route) { var fullPath = parentPath + route.path; if (fullPath === '*') return; if (!(platformConfig.authResourcePaths.includes(fullPath) || platformConfig.baseResourcePaths.includes(fullPath))) { platformConfig.baseResourcePaths.push(fullPath); } if (route.children) { iterateRouters(route.children, fullPath + '/'); } }); } iterateRouters(routes);` : ''; let contentCreateLCAPApp = ` window.createLcapApp = () => { ${compRegStr} appVM = window.cloudAdminDesigner.init(platformConfig.appConfig, platformConfig, routes, metaData); try { var push = appVM.$router.history.push; appVM.$router.history.push = function (a, b) { push.apply(this, [a, b, console.warn]); }; } catch (e) { console.error(e) } return window.appVM = appVM; }; window.createLcapApp();`; let contentPCIframe = frontend.globalScaleEnabled ? ` // 同步逻辑仅在 iframe 内部执行 if(window.frameElement && window.frameElement.getAttribute('isScaledFrame')) { /** * iframe 路由同步的几种情况: * 1. 跳转时,iframe 同步到顶层 * 2. 回退时,顶层同步到 iframe,经测试,即便在 iframe 里调用 back 也会先触发顶层回退 * 3. 前进时,iframe 同步到顶层 * 原因:顶层永远在 iframe 后面 */ // 自增ID,用于判断路由是否回退 let incrementID = 0; // 当前历史ID,用于判断路由是否回退 let curHID = 0; // 当前顶层历史ID,用于判断顶层路由是否回退 let curParentHID = 0; // 是否是回退 let isBack = false; // 是否是前进 let isForward = false; appVM.$router.afterEach((to, from)=> { if(isBack) { // 重置回退状态 isBack = false; return; } // iframe 前进时,需要让顶层页面也前进 if(isForward) { isForward = false; window.parent.history.forward(); return; } // 自增ID,用于判断路由是否回退 incrementID++; window.history.replaceState({HID: incrementID}, ''); // iframe 路由变化时,需要同步顶层页面的路由 window.parent.history.pushState({HID: incrementID}, '', to.fullPath); // iframe 路由变化时,title 也要同步 window.parent.document.title = document.title; // 更新当前历史ID curHID = incrementID; curParentHID = incrementID; }); window.addEventListener('popstate', function (e) { // 如果有 state,但没有 HID,说明是未知跳转,不判定前进后退 if(e.state && e.state.HID === undefined) return; if(curHID > (e.state ? e.state.HID : undefined)){ isBack = true; } else { isForward = true; } // 更新当前历史ID curHID = e.state? e.state.HID : 0; }); // 顶层页面路由回退时,需要同步 iframe 的路由 window.parent.addEventListener('popstate', function (e) { if(curParentHID > (e.state ? e.state.HID : undefined)){ appVM.$router.back(); } // 更新当前顶层历史ID curParentHID = (e.state ? e.state.HID : undefined) || 0; }); // 首次加载同步 title 和 shortcut icon window.parent.document.title = document.title; const shortcutIconLink = document.querySelector('link[rel="shortcut icon"]'); if(shortcutIconLink) parent.document.head.appendChild(shortcutIconLink); } ` : ''; let content10 = ` })();`; let contentDevOnly = config.env === 'dev' ? ` var _div = document.createElement('div'); _div.classList = "div-load" _div.style.display = 'none'; _div.innerHTML = "<div class='loading-container'></div><div>正在更新最新发布内容...</div>" document.getElementsByTagName('body')[0].appendChild(_div); window.showLoading = function(){ document.querySelector(".div-load").style.display ="flex" } window.hideLoading = function(){ document.querySelector(".div-load").style.display ="none" } var style = document.createElement('style'); style.innerHTML=\`.div-load{ width: 208px; height: 40px; background: rgba(48, 48, 48, 0.8); box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1); border-radius: 4px; position: fixed; top: 120px; left: 50%; margin-left: -104px; color: #FFFFFF; font-size: 14px; justify-content: center; align-items: center; } .loading-container{ width: 12px; height: 12px; margin-right: 10px; animation: loading-animation 0.8s infinite linear; border: 2px solid #f3f3f3; border-top: 2px solid rgb(109, 108, 108); border-right: 2px solid rgb(109, 108, 108); border-bottom: 2px solid rgb(109, 108, 108); border-radius: 50%; } @keyframes loading-animation{ 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }\` document.getElementsByTagName('body')[0].appendChild(style); window.addEventListener('message', function (e) { if(e.data ==="release-start"){ showLoading() } if(e.data==="release-end"){ hideLoading() window.location.reload(); } }) ` : ''; let annoDep = app?.dependencies?.filter(dep => dep?.annotations?.length > 0); let contentVueDirectives = annoDep?.map(dep => dep?.annotations?.filter(anno => anno.applyTo.includes('ViewElement')) .map(anno => { let depName = dep.name; let annoName = anno.name; let annoStr = ` const ${annoName} = { async handle(el, binding, vnode, oldVnode) { if(window.Vue.prototype.$library && window.Vue.prototype.$library['${depName}']){ window.Vue.prototype.$library['${depName}'].${annoName}(el, binding, vnode, oldVnode) } }, bind(el, binding, vnode, oldVnode) { ${annoName}.handle(el, binding, vnode, oldVnode); }, update(el, binding, vnode, oldVnode) { ${annoName}.handle(el, binding, vnode, oldVnode); }, }; window.Vue.directive('${annoName}', ${annoName});`; return annoStr; })); let hasViewElementAnno = annoDep?.filter(dep => dep?.annotations?.filter(anno => anno.applyTo.includes('ViewElement'))?.length > 0)?.length > 0; let contentViewElemAnno = hasViewElementAnno ? ` function fetchAnnoData() { return new Promise(async (resolve, reject) => { if (window.annotationAllData) { resolve(window.annotationAllData); } else { let sysPrefixPath = ${config?.sysPrefixPath ? "'" + config?.sysPrefixPath + "'" : "'" + "'"} ||''; let urlEntity = sysPrefixPath + '/api/system/annotation/entityAll' let urlLogic = sysPrefixPath + '/api/system/annotation/logicAll' Promise.all([ fetch(urlEntity).then(response => response.json()), fetch(urlLogic).then(response => response.json()) ]) .then(([entityAll, logicAll]) => { let entityAllData = (entityAll && entityAll.Data) || entityAll let logicAllData = (logicAll && logicAll.Data) || logicAll return [entityAllData, logicAllData] }) .then(([entityAll, logicAll]) => { window.annotationAllData = { entityAll, logicAll }; resolve(window.annotationAllData); }).catch(err => { }) } }); }; fetchAnnoData(); ` : ''; let content = [content1, contentScale, contentStyleCss, contentDocIcon, contentCustomNames, contentImport, contentLogReportCode, contentRouter, contentCreateLCAPApp, contentPCIframe, content10, contentDevOnly, contentVueDirectives.join(''), contentViewElemAnno].join(''); content1 = null; contentScale = null; contentStyleCss = null; contentDocIcon = null; contentCustomNames = null; contentImport = null; contentLogReportCode = null; contentRouter = null; contentCreateLCAPApp = null; contentPCIframe = null; content10 = null; contentDevOnly = null; contentVueDirectives = null; contentViewElemAnno = null; const timestamp = Date.now(); utils.delay(100); let bundleMinPath = completePath + getCompletePath(getBundleFileName(), content, config); const otherJsList = frontend?.appletsConfig?.enable ? ['//res.wx.qq.com/open/js/jweixin-1.3.2.js'] : []; const microAppIntegration = (0, microApp_1.integrateMicroApp)(frontend); if (config?.debug) { otherJsList.push(`${STATIC_URL}/packages/@lcap/breakpoint-client@1.0.0/dist/index.js?time=${timestamp}`); } let jsAssetsList = assetsInfo.basic.js.concat(assetsInfo.custom.js, otherJsList).concat([bundleMinPath]); let cssAssetsList = assetsInfo.basic.css.concat(assetsInfo.custom.css); if (config.isPreviewFe) { const changeToRelativePath = (str) => { return str.replace(STATIC_URL, '').replace(config.USER_STATIC_URL, '').split('/').filter((v) => v).join('/'); }; jsAssetsList = jsAssetsList.map(changeToRelativePath); cssAssetsList = cssAssetsList.map(changeToRelativePath); } // 导出源码才处理 if (config.isExport) { // 以`${STATIC_URL}/packages`开头的正则 const prefixPackagesReg = new RegExp(`^${STATIC_URL}/packages/`); const prefixHostReg = new RegExp(`^${STATIC_URL}`); jsAssetsList = (jsAssetsList || []).map(url => { // packages下的需要下载 if (prefixPackagesReg.test(url)) { // 去除`?`及后面的内容 // eslint-disable-next-line prefer-destructuring const noQueryUrl = url.split('?')[0]; const path = noQueryUrl.replace(prefixHostReg, ''); const dir = path.split('/').slice(0, -1).join('/'); config_1.config.frontendPackagesResource.push({ path: dir, // 下载后存放的路径 isDir: true, url: `${config.STATIC_URL}${dir}`, }); const noHostUrl = url.replace(prefixHostReg, ''); return `${noHostUrl}`; } const prefixBasePathReg = new RegExp(`${bundleMinPath}`); if (prefixBasePathReg.test(url)) { // bundle.js return `${url}`; } return url; }); cssAssetsList = (cssAssetsList || []).map(url => { // 去除`?`及后面的内容 // eslint-disable-next-line prefer-destructuring const noQueryUrl = url.split('?')[0]; if (prefixPackagesReg.test(noQueryUrl)) { const path = noQueryUrl.replace(prefixHostReg, ''); const dir = path.split('/').slice(0, -1).join('/'); if (!config_1.config.frontendPackagesResource.some(item => item.path === dir)) { config_1.config.frontendPackagesResource.push({ path: dir, // 下载后存放的路径 isDir: true, url: `${config.STATIC_URL}${dir}`, }); } const noHostUrl = url.replace(prefixHostReg, ''); return `${noHostUrl}`; } return url; }); } // 组装 dynamicAssets // 配置驱动 + 顺序控制 const ASSET_TYPE_CONFIG = { // 框架代码文件 framework: { regex: /\/(vue|react)\.(min\.)?js$/, refresh: false, priority: 1, outputOrder: 1 }, // 包含 @lcap/pc-template @lcap/mobile-template 的 js template: { regex: /\/(@lcap\/(pc-template|mobile-template))/, refresh: true, priority: 2, outputOrder: 2 }, // 包含 @lcap/pc-ui @lcap/mobile-ui @lcap/element-ui 的 js ui: { regex: /\/(@lcap\/(pc-ui|mobile-ui|element-ui))/, refresh: true, priority: 3, outputOrder: 3 }, // 应用代码文件,outputOrder 确保最后 bundle: { regex: /\/bundle\.[a-zA-Z0-9]+\.(min\.)?js$/, refresh: false, priority: 4, outputOrder: 5 }, // 默认匹配所有,outputOrder 确保在 bundle 之前 other: { regex: /./, refresh: true, priority: 5, outputOrder: 4 } }; const OUTPUT_ORDER = Object.entries(ASSET_TYPE_CONFIG) .sort(([, a], [, b]) => a.outputOrder - b.outputOrder) .map(([key]) => key); // 单次遍历 O(n) + 智能分类 function optimizeAssetClassification(jsAssetsList) { const assetGroups = new Map(); // 初始化所有分组 OUTPUT_ORDER.forEach((type) => { assetGroups.set(type, []); }); // 单次遍历,按优先级匹配 jsAssetsList.forEach((asset) => { const matchedType = Object.entries(ASSET_TYPE_CONFIG) .sort(([, a], [, b]) => a.priority - b.priority) .find(([type, config]) => { // other类型特殊处理:不匹配其他任何类型 if (type === 'other') { return !Object.entries(ASSET_TYPE_CONFIG) .filter(([t]) => t !== 'other') .some(([, cfg]) => cfg.regex.test(asset)); } return config.regex.test(asset); }); if (matchedType) { assetGroups.get(matchedType[0]).push(asset); } }); // 按照预定义顺序输出,确保 bundle 在最后 return OUTPUT_ORDER.map((type) => ({ type, list: assetGroups.get(type), config: ASSET_TYPE_CONFIG[type], })) .filter(({ list }) => list.length > 0) .map(({ list, config }) => ({ list, type: 'js', refresh: config.refresh, })); } const dynamicAssets = optimizeAssetClassification(jsAssetsList); // CSS 资源默认最后 if (cssAssetsList.length > 0) { dynamicAssets.push({ list: cssAssetsList, type: 'css', refresh: true, }); } const doubleIndent = ' '; let loadAssets = `const loadAssets = () => { ${dynamicAssets .map((dyAssets, i) => `${i === 0 ? '' : doubleIndent}LazyLoad.${dyAssets.type}([${dyAssets.list .map((url) => `'${dyAssets.refresh ? `${url}?t=${timestamp}` : url}'`) .join(', ')}]);`) .join('\n')} }`; // isPreviewFe 资产中心的预览模式,优先级最高 if (config.isPreviewFe) { loadAssets = `const prefixDomain = (url) => { if (url.includes('mockBundle')) return (window.userAssetsDomain || '') + url; return (window.assetsDomain || '') + url; } const loadAssets = () => { ${dynamicAssets .map((dyAssets, i) => `${i === 0 ? '' : doubleIndent}LazyLoad.${dyAssets.type}([${dyAssets.list .map((url) => `prefixDomain('${url}')`) .join(', ')}]);`) .join('\n')} }`; } else if (config.isExport) { // 导出源码才处理 loadAssets = `const prefixUIPath = (url) => (window.UIBasePath || '') + url; const loadAssets = () => { ${dynamicAssets .map((dyAssets, i) => `${i === 0 ? '' : doubleIndent}LazyLoad.${dyAssets.type}([${dyAssets.list .map((url) => `prefixUIPath('${url}')`) .join(', ')}]);`) .join('\n')} }`; } jsAssetsList = null; cssAssetsList = null; const h5Debugger = () => { let code = `try { const searchParams = new URLSearchParams(window.location.search); if (searchParams.get('lcap_debug') === 'true') { LazyLoad.js([ 'https://cdn.jsdelivr.net/npm/eruda', 'https://cdn.jsdelivr.net/npm/eruda-monitor@1.0.2', 'https://cdn.jsdelivr.net/npm/eruda-timing@2.0.1', ], function() { if (window.eruda) { eruda.init(); if (window.erudaMonitor) { eruda.add(erudaMonitor); } if (window.erudaTiming) { eruda.add(erudaTiming); } } }); } } catch(e) { console.log('载入h5Debugger失败:', e) }`; return code; }; let assetsContent = `(function() { ${frontend.type === 'h5' ? h5Debugger() : ''} ${loadAssets} ${microAppIntegration || 'loadAssets();'} })() `; const outputs = [ { name: bundleMinPath, content, }, { name: basePath ? `${baseUrl}${basePath}/client.js` : `${baseUrl}/client.js`, content: assetsContent, }, ...routerFiles, ]; assetsContent = null; content = null; processAssetsInOutputs(outputs, frontend, config); return outputs; } exports.genBundleFiles = genBundleFiles; async function genFrontendBundleFiles(app, frontends, config) { const result = []; // 这里逐个处理并且中间还要等待,是为了腾出时间给主线程做垃圾回收 for (const frontend of frontends) { if (frontend.frameworkKind === 'react' || frontend.frameworkKind === 'vue3') { const httpClient = app.naslServer?.http ?? axios_1.default.create(); let files = await (0, nasl_unified_frontend_generator_2.compileNASLToReactDist)(app, frontend, config, httpClient); let outputs = files.map((x) => { return { name: x.path, content: x.content, frontend: { title: frontend.title, name: frontend.name, type: frontend.type, path: frontend.path, } }; }); files = null; processAssetsInOutputs(outputs, frontend