diff --git a/app.platform/x.txt b/app.platform/x.txt new file mode 100644 index 00000000..e69de29b diff --git a/io.sc.engine.mv.frontend/public/webjars/luckysheet/2.1.13/css/waffle_sprite.png b/io.sc.engine.mv.frontend/public/webjars/luckysheet/2.1.13/css/waffle_sprite.png new file mode 100644 index 00000000..336ce4f1 Binary files /dev/null and b/io.sc.engine.mv.frontend/public/webjars/luckysheet/2.1.13/css/waffle_sprite.png differ diff --git a/io.sc.engine.mv.frontend/util-components-generator.cjs b/io.sc.engine.mv.frontend/util-components-generator.cjs new file mode 100644 index 00000000..dcf297d2 --- /dev/null +++ b/io.sc.engine.mv.frontend/util-components-generator.cjs @@ -0,0 +1,32 @@ +/** + * 用于自动生成前端组件 + * 通过 src/routes/routes.json 文件构建 src/components/index.ts 文件 + */ +const fs = require('fs'); +const Json5 =require('json5'); + +// 解析前端路由配置文件 +const routesJson = Json5.parse(fs.readFileSync('./src/routes/routes.json', 'utf8')); + +let content =''; +content +='/**\n'; +content +=' * 此文件为自动生成文件,请勿修改\n'; +content +=' */\n\n'; +for(const route of routesJson){ + const componentName =route.component.substring(route.component.lastIndexOf('.')+1); + const componentPath =route.componentPath; + content +=`import ${componentName} from '${componentPath}';\n`; +} + +content +='\n'; +content +='const localComponents = { \n'; +for(const route of routesJson){ + const componentName =route.component.substring(route.component.lastIndexOf('.')+1); + content +=`'${route.component}': ${componentName},\n`; +} +content +='}\n\n'; +content +='export default localComponents;\n'; + +fs.writeFileSync('./src/components/index.ts', content); + +console.info('components generated!'); \ No newline at end of file diff --git a/io.sc.engine.mv.frontend/util-frontend-register.cjs b/io.sc.engine.mv.frontend/util-frontend-register.cjs new file mode 100644 index 00000000..da69db21 --- /dev/null +++ b/io.sc.engine.mv.frontend/util-frontend-register.cjs @@ -0,0 +1,173 @@ +/** + * 用于将前端模块注册到后端服务器 + */ +const packageJson = require('./package.json'); +const { ModuleFederationPlugin } = require('webpack').container; +const Server = require('webpack-dev-server'); +const mf = require('./webpack.config.mf.cjs'); +const fs = require('fs'); +const http = require('http'); +const https = require('https'); +const Json5 =require('json5'); + +// 解析前端注册器配置文件 +const frontendRegisterConfigure = Json5.parse(fs.readFileSync('./frontend-register.json', 'utf8')); +// 解析前端路由配置文件 +const frontendRoutes =Json5.parse(fs.readFileSync('./src/routes/routes.json', 'utf8')); + +/** + * 远程组件注册器类 + */ +class RemoteFrontEndModuleRegister { + /** + * 构造函数,传入配置信息, 包括远程和本地服务器配置信息 + * 配置信息定义格式如下: + * // 远程服务器配置信息 + * remoteServerConfig: { + * protocol: 'http', + * host: 'localhost', + * port: 8080, + * path: '/api/system/frontend/regist', + * }, + * // 本地服务器配置信息 + * localServerConfig: { + * protocol: devServer.options.server.type, + * host: Server.internalIPSync("v4"), + * port: devServer.options.port, + * path: '/', + * } + * @param devServer webpack dev server 对象 + */ + constructor(devServer) { + if (!devServer) { + throw new Error('webpack-dev-server is not defined'); + } + this.devServer = devServer; + this.registSuccess = null; + this.remoteServerConfig = { + protocol: frontendRegisterConfigure.protocol, + host: frontendRegisterConfigure.host, + port: frontendRegisterConfigure.port, + path: frontendRegisterConfigure.path, + }; + this.localServerConfig = { + protocol: devServer.options.server.type, + host: Server.internalIPSync("v4"), + port: devServer.options.port, + path: '/', + }; + } + + /** + * 周期性向服务器注册前端模块 + * @param delay 延迟执行(单位:毫秒) + * @param interval 固定频率执行(单位:毫秒) + */ + regist(delay,interval) { + if(frontendRegisterConfigure.enable){ + setTimeout(() => { + let remoteServerUrl = this.remoteServerConfig.protocol + '//' + this.remoteServerConfig.host + ':' + this.remoteServerConfig.port + this.remoteServerConfig.path; + console.info('regist frontend module to server --> ' + remoteServerUrl); + setInterval(this.doRegist.bind(this), delay); + }, delay); + } + } + + /** + * 向服务器注册前端模块 + */ + doRegist() { + const data = JSON.stringify(this.getRegistJson()); + if (data) { + let request = this.getRequest(this.remoteServerConfig.protocol); + let This = this; + request.on('error', error => { + if (This.registSuccess == null || This.registSuccess) { + This.registSuccess = false; + console.error('regist frontend module to server, Failed!', error); + } + }); + request.write(data); + request.end(); + } + } + + /** + * 获取前端模块的注册信息 + * @returns 前端模块的注册信息 + */ + getRegistJson() { + return { + protocol: this.localServerConfig.protocol, + host: this.localServerConfig.host, + port: this.localServerConfig.port, + contextPath: this.localServerConfig.contextPath, + name: packageJson.name, + components: this.getComponents(), + routes: frontendRoutes, + } + } + + /** + * 获取前端模块的注册信息(组件集合) + * @returns 前端模块的注册信息(组件集合) + */ + getComponents() { + const plugins = mf.plugins; + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin instanceof ModuleFederationPlugin) { + const exposes = plugin._options.exposes; + if (exposes) { + const components = []; + let keyIndex = 0; + for (let key in exposes) { + components[keyIndex] = key; + keyIndex++; + } + return components; + } + } + } + return null; + } + + /** + * 获取 http/https 请求 + * @param {*} protocol 请求协议 + */ + getRequest(protocol) { + let request = http; + if (protocol == 'https:') { + request = https; + } + let This = this; + return request.request({ + protocol: this.remoteServerConfig.protocol + ":", + host: this.remoteServerConfig.host, + port: this.remoteServerConfig.port, + path: this.remoteServerConfig.path, + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }, request => { + request.setEncoding('utf-8'); + request.on('data', d => { + const data = JSON.parse(d); + if (data.code === 200) { + if (This.registSuccess == null || !This.registSuccess) { + This.registSuccess = true; + console.info('regist frontend module to server, Success!'); + } + } else { + console.error('regist frontend module to server, Failed!', d); + } + }) + }); + } +} + +module.exports = { + RemoteFrontEndModuleRegister +} diff --git a/io.sc.engine.mv.frontend/webpack.config.common.cjs b/io.sc.engine.mv.frontend/webpack.config.common.cjs new file mode 100644 index 00000000..25aee0c0 --- /dev/null +++ b/io.sc.engine.mv.frontend/webpack.config.common.cjs @@ -0,0 +1,159 @@ +/** + * webpack 通用配置 + */ +const path = require('path'); // path +const webpack = require('webpack'); // webpack +const json5 = require('json5'); // json5 +const HtmlWebpackPlugin = require('html-webpack-plugin'); // webpack html 生成插件 +const CopyWebpackPlugin = require('copy-webpack-plugin'); // webpack copy 插件 +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 抽取 css 插件 +const { VueLoaderPlugin } = require('vue-loader'); // vue loader 插件 +const ESLintPlugin = require('eslint-webpack-plugin'); // eslint 插件 +const packageJson = require('./package.json'); // package.json +const projectName =packageJson.name; // 项目名称 + +module.exports = { + // 入口文件 + entry: './src/main', + // 输出 + output: { + // 输出路径(为兼容后端和多个前端项目) + // 1. 兼容后端: 将 dist 目录作为资源目录, 其中 public 种的静态资源可以直接访问 + // 2. 兼容多个前端项目: 每个项目发布到 public 目录下的唯一项目名称目录 + path: path.resolve(__dirname, `dist/public/${projectName}`), + // 输出文件名 + filename: `javascript/[name].[contenthash:5].js`, + // 指定发布路径,使用 auto 可具有更多灵活性 + publicPath: 'auto', + // 每次构建时,首先删除 output.path 目录所有内容,保证每次得到最新的构建结果 + clean: true, + }, + + module: { + rules: [ + // babel(包含处理: typescript) + { + test: /\.(t|j)s$/, + exclude: /node_modules/, + use: [ + { + loader: "babel-loader", + options: { + cacheDirectory: true, + } + } + ] + }, + + // css + { + test: /\.(sa|sc|c)ss$/, + use: [{ + loader: MiniCssExtractPlugin.loader, + }, + { + loader: 'css-loader', + }, + { + loader: 'postcss-loader', + }] + }, + + // 字体文件 + { + test: /\.(woff|woff2|eot|ttf|otf)(\?.*)?$/, + type: 'asset/resource', + generator: { + filename: `fonts/[name].[contenthash:5].[ext]`, + } + }, + + // json5 + { + test: /\.json$/, + type: 'json', + parser: { + parse: json5.parse, + }, + }, + + // vue loader + { + test: /\.vue$/, + exclude: /node_modules/, + use: [ + { + loader: 'vue-loader', + } + ] + }, + ], + }, + + // 插件 + plugins: [ + new webpack.DefinePlugin({ + __VUE_OPTIONS_API__: JSON.stringify(true), + __VUE_PROD_DEVTOOLS__: JSON.stringify(false) + }), + + // 进度显示插件 + new webpack.ProgressPlugin(), + + // css 抽取插件 + new MiniCssExtractPlugin({ + filename: `css/[name].[contenthash:5].css`, + chunkFilename: `css/[name].[contenthash:5].css` + }), + + // 自动生成静态 index.html 文件 + new HtmlWebpackPlugin({ + template: 'public/index.html', + filename: `index.html`, + minify: false, + inject: 'body', + timestamp: new Date().getTime(), + }), + + // 拷贝静态资源到 output.path 指定的目录 + new CopyWebpackPlugin({ + patterns: [ + { + from: 'public', + toType: 'dir', + filter: async (resourcePath) => { + // 不复制 index.html 因为 index.html 已经由 HtmlWebpackPlugin 插件生成了 + if (resourcePath.endsWith('index.html') || resourcePath.endsWith('.DS_Store')) { + return false; + } + return true; + }, + info: { minimized: true }, + } + ] + }), + + // vue loader 插件 + new VueLoaderPlugin(), + + // eslint 插件 + new ESLintPlugin({ + fix: true, + formatter: 'stylish', + extensions: ['js', 'ts', 'vue', 'cjs'], + exclude: [ + 'node_modules', + ], + }), + ], + + // 配置模块如何被解析, + resolve: { + // 设置模块别名,方便引用 + alias: { + '@': path.resolve(__dirname, 'src'), + }, + // 设置支持的模块扩展名,即这些扩展名的文件可以作为模块被使用 + extensions: ['.ts', '.js', '.cjs', '.vue'] + }, +}; diff --git a/io.sc.engine.mv.frontend/webpack.config.mf.cjs b/io.sc.engine.mv.frontend/webpack.config.mf.cjs new file mode 100644 index 00000000..e35ea57a --- /dev/null +++ b/io.sc.engine.mv.frontend/webpack.config.mf.cjs @@ -0,0 +1,66 @@ +/** + * webpack module federation 配置 + */ +const fs = require('fs'); // 文件读取 +const Json5 =require('json5'); // json5 +const { ModuleFederationPlugin } = require('webpack').container; // webpack 模块联邦插件 +const packageJson = require('./package.json'); // package.json +const projectName =packageJson.name; // 项目名称 +const deps = packageJson.dependencies; // 项目依赖 + +// 读取本地路由配置, 通过其中 component 和 componentPath 两个属性构建 webpack 模块联邦的 exposes 属性值 +const data = fs.readFileSync('./src/routes/routes.json', 'utf8'); +const routes =Json5.parse(data); +const mfExposes ={}; +for(const route of routes){ + mfExposes[route.component]= route.componentPath; +} + +// 导出 webapck 配置的模块联邦部分 +module.exports = { + plugins: [ + new ModuleFederationPlugin({ + // 模块联邦的模块名称 + name: `${projectName}`, + // 模块联邦的远程入口文件 + filename: `javascript/remoteEntry.js`, + // 通过浏览器 window 对象保存模块联邦对象 + library: { type: 'window', name: `${projectName}` }, + remoteType: 'window', + // 模块联邦的导出组件 + exposes: mfExposes, + // 模块联邦共享库 + shared: { + '@codemirror/autocomplete': { requiredVersion: deps['@codemirror/autocomplete'], singleton: true }, + '@codemirror/commands': { requiredVersion: deps['@codemirror/commands'], singleton: true }, + '@codemirror/lang-html': { requiredVersion: deps['@codemirror/lang-html'], singleton: true }, + '@codemirror/lang-java': { requiredVersion: deps['@codemirror/lang-java'], singleton: true }, + '@codemirror/lang-javascript': { requiredVersion: deps['@codemirror/lang-javascript'], singleton: true }, + '@codemirror/lang-json': { requiredVersion: deps['@codemirror/lang-json'], singleton: true }, + '@codemirror/lang-sql': { requiredVersion: deps['@codemirror/lang-sql'], singleton: true }, + '@codemirror/lang-xml': { requiredVersion: deps['@codemirror/lang-xml'], singleton: true }, + '@codemirror/language': { requiredVersion: deps['@codemirror/language'], singleton: true }, + '@codemirror/search': { requiredVersion: deps['@codemirror/search'], singleton: true }, + '@codemirror/state': { requiredVersion: deps['@codemirror/state'], singleton: true }, + '@codemirror/view': { requiredVersion: deps['@codemirror/view'], singleton: true }, + '@vueuse/core': { requiredVersion: deps['@vueuse/core'], singleton: true }, + 'axios': { requiredVersion: deps['axios'], singleton: true }, + 'codemirror': { requiredVersion: deps['codemirror'], singleton: true }, + 'dayjs': { requiredVersion: deps['dayjs'], singleton: true }, + 'echarts':{ requiredVersion: deps['echarts'], singleton: true }, + 'exceljs':{ requiredVersion: deps['exceljs'], singleton: true }, + 'file-saver':{ requiredVersion: deps['file-saver'], singleton: true }, + 'luckyexcel':{ requiredVersion: deps['luckyexcel'], singleton: true }, + "mockjs": { requiredVersion: deps['mockjs'], singleton: true }, + 'pinia': { requiredVersion: deps['pinia'], singleton: true }, + 'platform-core': { requiredVersion: deps['platform-core'], singleton: true }, + 'quasar': { requiredVersion: deps['quasar'], singleton: true }, + 'vue': { requiredVersion: deps['vue'], singleton: true }, + 'vue-codemirror6': { requiredVersion: deps['vue-codemirror6'], singleton: true }, + 'vue-dompurify-html':{ requiredVersion: deps['vue-dompurify-html'], singleton: true }, + 'vue-i18n': { requiredVersion: deps['vue-i18n'], singleton: true }, + 'vue-router': { requiredVersion: deps['vue-router'], singleton: true }, + } + }), + ] +}; diff --git a/io.sc.engine.mv.frontend/webpack.env.build.cjs b/io.sc.engine.mv.frontend/webpack.env.build.cjs new file mode 100644 index 00000000..dcc45fb7 --- /dev/null +++ b/io.sc.engine.mv.frontend/webpack.env.build.cjs @@ -0,0 +1,76 @@ +/** + * 开发环境构建 + */ +const { merge } = require('webpack-merge'); // webpack 配置合并函数 +const common = require('./webpack.config.common.cjs'); // webpack 通用配置 +const mf = require('./webpack.config.mf.cjs'); // webpack 模块联邦配置 + +module.exports = merge(common, mf, { + mode: 'development', + // ------------------------------------------------------------------------------------------------------------------------------- + // devtool | performance | comment + // (none) | build:fastest, rebuild:fastest | Recommended choice for production builds with maximum performance. + // eval | build:fast, rebuild:fastest | Recommended choice for development builds with maximum performance. + // eval-source-map| build:slowest, rebuild:ok | Recommended choice for development builds with high quality SourceMaps. + // source-map | build:slowest, rebuild:slowest | Recommended choice for production builds with high quality SourceMaps. + // ------------------------------------------------------------------------------------------------------------------------------- + devtool: 'eval-source-map', + optimization: { + minimize: false, + moduleIds: 'named', + chunkIds: 'named', + + splitChunks: { + cacheGroups: { + 'vue': { + name: 'vue', + test: /[\\/]node_modules[\\/](vue|vue-dompurify-html|vue-i18n|vue-router)[\\/]/, + priority: 20, + chunks: 'all', + enforce: true + }, + 'dnd':{ + name: 'dnd', + test: /[\\/]node_modules[\\/](vue3-dnd|react-dnd-html5-backend|@vueuse[\\/]core)[\\/]/, + priority: 20, + chunks: 'all', + enforce: true + }, + 'quasar': { + name: 'quasar', + test: /[\\/]node_modules[\\/](quasar)[\\/]/, + priority: 20, + chunks: 'all', + enforce: true + }, + 'excel': { + name: 'excel', + test: /[\\/]node_modules[\\/](exceljs|luckyexcel|)[\\/]/, + priority: 20, + chunks: 'all', + enforce: true + }, + 'platform-core': { + name: 'platform-core', + test: /[\\/]node_modules[\\/]platform-core[\\/]/, + priority: 20, + chunks: 'all', + enforce: true + }, + 'view': { + name: 'view', + test: /[\\/]view[\\/]/, + priority: 20, + chunks: 'all', + enforce: true + }, + 'vendors': { + name: 'vendors', + test: /[\\/]node_modules[\\/]/, + chunks: 'all', + enforce: true + }, + } + } + }, +}); \ No newline at end of file diff --git a/io.sc.engine.mv.frontend/webpack.env.prod.cjs b/io.sc.engine.mv.frontend/webpack.env.prod.cjs new file mode 100644 index 00000000..ab9a725c --- /dev/null +++ b/io.sc.engine.mv.frontend/webpack.env.prod.cjs @@ -0,0 +1,35 @@ +/** + * 生产环境构建 + */ +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); // css 压缩插件 +const TerserPlugin = require("terser-webpack-plugin"); // js 压缩插件 +const { merge } = require('webpack-merge'); // webpack 配置合并函数 +const build = require('./webpack.env.build.cjs'); // 开发环境构建配置 + +module.exports = merge(build, { + mode: 'production', + // ------------------------------------------------------------------------------------------------------------------------------- + // devtool | performance | comment + // (none) | build:fastest, rebuild:fastest | Recommended choice for production builds with maximum performance. + // eval | build:fast, rebuild:fastest | Recommended choice for development builds with maximum performance. + // eval-source-map| build:slowest, rebuild:ok | Recommended choice for development builds with high quality SourceMaps. + // source-map | build:slowest, rebuild:slowest | Recommended choice for production builds with high quality SourceMaps. + // ------------------------------------------------------------------------------------------------------------------------------- + devtool: 'source-map', + optimization: { + minimize: true, + minimizer: [ + new CssMinimizerPlugin(), // css 压缩插件 + new TerserPlugin({ // js 压缩插件 + extractComments: false, + terserOptions: { + format: { + comments: false, + }, + }, + }), + ], + moduleIds: 'named', + chunkIds: 'named', + }, +}); diff --git a/io.sc.engine.mv.frontend/webpack.env.serve.cjs b/io.sc.engine.mv.frontend/webpack.env.serve.cjs new file mode 100644 index 00000000..e3f793b4 --- /dev/null +++ b/io.sc.engine.mv.frontend/webpack.env.serve.cjs @@ -0,0 +1,35 @@ +/** + * 开发环境下启动 webpack dev server + */ +const path = require('path'); // path +const { merge } = require('webpack-merge'); // webpack 配置合并函数 +const common = require('./webpack.config.common.cjs'); // webpack 通用配置 +const mf = require('./webpack.config.mf.cjs'); // webpack 模块联邦配置 +const { RemoteFrontEndModuleRegister } = require('./util-frontend-register.cjs'); // 远程模块注册器 + +module.exports = (env)=> merge(common, mf,{ + mode: 'development', + devtool: 'eval', + + devServer: { + client: { + overlay: false, + }, + static: { + directory: path.join(__dirname, 'public'), + }, + compress: false, + port: 3000, + hot: true, + // 保证在出现 404 错误时,能够导航到 index.html + historyApiFallback: true, + + setupMiddlewares: (middlewares, devServer) => { + // 注册前端模块到远程服务器 + const register = new RemoteFrontEndModuleRegister(devServer); + // 延后 5 秒执行, 且每 5 秒执行一次 + register.regist(5000,5000); + return middlewares; + } + }, +});