diff --git a/.gitignore b/.gitignore index dd2a871..a0e2170 100644 --- a/.gitignore +++ b/.gitignore @@ -61,7 +61,6 @@ lerna-debug.log* /dist/ /nbdist/ /.nb-gradle/ -build/ !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/generator-code-web/build/buildEnv.ts b/generator-code-web/build/buildEnv.ts new file mode 100644 index 0000000..7c3c4ab --- /dev/null +++ b/generator-code-web/build/buildEnv.ts @@ -0,0 +1,53 @@ +import type { BuildOptions } from 'vite'; + +import { pathResolve } from './utils'; + +export const buildEnv = (): BuildOptions => { + return { + target: 'es2015', + assetsInlineLimit: 20000, + // 构建输出的目录,默认值为"dist" + outDir: 'docker/dist', + // 用于指定使用的代码压缩工具。在这里,minify 被设置为 'terser',表示使用 Terser 进行代码压缩。默认值terser + // esbuild 打包更快,但是不能去除 console.log,terser打包慢,但能去除 console.log + minify: 'terser', // "esbuild" + // 用于配置 Terser 的选项 + terserOptions: { + // 用于配置压缩选项 + compress: { + drop_console: true, // 是否删除代码中的 console 语句, 默认值false + drop_debugger: true, // 是否删除代码中的 debugger 语句, 默认值false + }, + }, + // 禁用 gzip 压缩大小报告,可略微减少打包时间 + reportCompressedSize: false, + // 用于指定是否生成源映射文件。源映射文件可以帮助调试和定位源代码中的错误。当设置为false时,构建过程不会生成源映射文件 + sourcemap: false, + // 用于配置 CommonJS 模块的选项 + commonjsOptions: { + // 用于指定是否忽略 CommonJS 模块中的 try-catch 语句。当设置为false时,构建过程会保留 CommonJS 模块中的 try-catch 语句 + ignoreTryCatch: false, + }, + // 规定触发警告的 chunk 大小, 当某个代码分块的大小超过该限制时,Vite 会发出警告 + chunkSizeWarningLimit: 2000, + rollupOptions: { + external: ['md-editor-v3', 'echarts'], + input: { + // @ts-ignore + index: pathResolve('../index.html', import.meta.url), + }, + // 静态资源分类打包 + output: { + chunkFileNames: 'static/js/[name]-[hash].js', + entryFileNames: 'static/js/[name]-[hash].js', + assetFileNames: 'static/[ext]/[name]-[hash].[ext]', + manualChunks: (id) => { + // 如果是包含在包中则打包成 vendor + if (id.includes('node_modules')) { + return `vendor`; + } + }, + }, + }, + }; +}; diff --git a/generator-code-web/build/cdn.ts b/generator-code-web/build/cdn.ts new file mode 100644 index 0000000..51e0a3b --- /dev/null +++ b/generator-code-web/build/cdn.ts @@ -0,0 +1,47 @@ +import { Plugin as importToCDN } from 'vite-plugin-cdn-import'; + +import { wrapperEnv } from './utils'; + +/** + * @description 打包时采用`cdn`模式,仅限外网使用(默认不采用,如果需要采用cdn模式,请在 .env.production 文件,将 VITE_CDN 设置成true) + * 平台采用国内cdn:https://www.bootcdn.cn,当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com + * 注意:上面提到的仅限外网使用也不是完全肯定的,如果你们公司内网部署的有相关js、css文件,也可以将下面配置对应改一下,整一套内网版cdn + */ +export const cdn = importToCDN({ + //(prodUrl解释: name: 对应下面modules的name,version: 自动读取本地package.json中dependencies依赖中对应包的版本号,path: 对应下面modules的path,当然也可写完整路径,会替换prodUrl) + // prodUrl: 'https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}', + prodUrl: 'https://unpkg.com/{name}@{version}/{path}', + modules: [ + { + name: 'vue', + var: 'Vue', + path: 'dist/vue.global.prod.js', + }, + { + name: 'vue-router', + var: 'VueRouter', + path: 'dist/vue-router.global.js', + }, + { + name: 'pinia', + var: 'Pinia', + path: 'dist/pinia.iife.js', + }, + { + name: 'axios', + var: 'axios', + path: 'dist/axios.min.js', + }, + { + name: 'dayjs', + var: 'dayjs', + path: 'dayjs.min.js', + }, + ], +}); + +/* 是否使用CDN加速 */ +export const useCDN = (mode) => { + const env = wrapperEnv(mode, 'VITE'); + return env.VITE_CDN ? cdn : null; +}; diff --git a/generator-code-web/build/css.ts b/generator-code-web/build/css.ts new file mode 100644 index 0000000..76778ba --- /dev/null +++ b/generator-code-web/build/css.ts @@ -0,0 +1,12 @@ +import type { CSSOptions } from 'vite'; + + +export const css = (mode: string): CSSOptions => { + return { + preprocessorOptions: { + scss: { + additionalData: `@use "@/assets/styles/minix/sidebar" as *;`, + }, + }, + }; +}; diff --git a/generator-code-web/build/define.ts b/generator-code-web/build/define.ts new file mode 100644 index 0000000..44ce782 --- /dev/null +++ b/generator-code-web/build/define.ts @@ -0,0 +1,14 @@ +import dayjs from 'dayjs'; + +import { dependencies, devDependencies, engines, name, version } from '../package.json'; + +const __APP_INFO__ = { + pkg: { name, version, engines, dependencies, devDependencies }, + lastBuildTime: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'), +}; + +export const define = () => { + return { + __APP_INFO__: JSON.stringify(__APP_INFO__), + }; +}; diff --git a/generator-code-web/build/info.ts b/generator-code-web/build/info.ts new file mode 100644 index 0000000..da94b93 --- /dev/null +++ b/generator-code-web/build/info.ts @@ -0,0 +1,58 @@ +import boxen, { type Options as BoxenOptions } from 'boxen'; +import dayjs, { type Dayjs } from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import gradientString from 'gradient-string'; + +import { logOutputSize, wrapperEnv } from './utils'; + +dayjs.extend(duration); + +const boxenOptions: BoxenOptions = { + padding: 0.94, + borderColor: 'cyan', + borderStyle: 'round', + textAlignment: 'left', +}; + +/* 输出日志信息 */ +const printLogMessage = (VITE_PORT: number) => { + return gradientString('cyan', 'magenta').multiline( + `欢迎使用此项目,项目访问地址如下: +http://localhost:${VITE_PORT}` + ); +}; + +export const viteConsoleLog = (mode: string) => { + const { VITE_PORT } = wrapperEnv(mode); + + let config: { command: string }; + let startTime: Dayjs; + let endTime: Dayjs; + return { + name: 'vite:buildInfo', + configResolved(resolvedConfig) { + config = resolvedConfig; + }, + buildStart() { + console.log(boxen(printLogMessage(VITE_PORT), boxenOptions)); + if (config.command === 'build') { + startTime = dayjs(new Date()); + } + }, + closeBundle() { + if (config.command === 'build') { + endTime = dayjs(new Date()); + const format = dayjs.duration(endTime.diff(startTime)).format('mm分ss秒'); + + console.log( + boxen( + gradientString('cyan', 'magenta').multiline( + `🎉 恭喜打包完成(总用时${format})打包大小(${logOutputSize()})` + ), + boxenOptions + ) + ); + } + }, + }; +}; diff --git a/generator-code-web/build/optimize.ts b/generator-code-web/build/optimize.ts new file mode 100644 index 0000000..65fb25f --- /dev/null +++ b/generator-code-web/build/optimize.ts @@ -0,0 +1,14 @@ +/** + * 此文件作用于 `vite.config.ts` 的 `optimizeDeps.include` 依赖预构建配置项 + * 依赖预构建,`vite` 启动时会将下面 include 里的模块,编译成 esm 格式并缓存到 node_modules/.vite 文件夹,页面加载到对应模块时如果浏览器有缓存就读取浏览器缓存,如果没有会读取本地缓存并按需加载 + * 尤其当您禁用浏览器缓存时(这种情况只应该发生在调试阶段)必须将对应模块加入到 include里,否则会遇到开发环境切换页面卡顿的问题(vite 会认为它是一个新的依赖包会重新加载并强制刷新页面),因为它既无法使用浏览器缓存,又没有在本地 node_modules/.vite 里缓存 + * 温馨提示:如果您使用的第三方库是全局引入,也就是引入到 src/main.ts 文件里,就不需要再添加到 include 里了,因为 vite 会自动将它们缓存到 node_modules/.vite + */ +const include = ['vue', 'vue-router', 'dayjs', 'axios', 'pinia', 'vue-types', 'js-cookie']; + +/** + * 在预构建中强制排除的依赖项 + */ +const exclude: string[] = []; + +export { exclude, include }; diff --git a/generator-code-web/build/plugins.ts b/generator-code-web/build/plugins.ts new file mode 100644 index 0000000..6ac4c2a --- /dev/null +++ b/generator-code-web/build/plugins.ts @@ -0,0 +1,45 @@ +import UnoCssIcons from '@unocss/preset-icons'; +import vue from '@vitejs/plugin-vue'; +import vueJsx from '@vitejs/plugin-vue-jsx'; +import { presetIcons } from 'unocss'; +import UnoCSS from 'unocss/vite'; +import type { PluginOption } from 'vite'; +import removeConsole from 'vite-plugin-remove-console'; +import Inspector from 'vite-plugin-vue-inspector'; + +import { useCDN } from './cdn'; +import { viteConsoleLog } from './info'; +import { compressPack, report } from './utils'; + +export const plugins = (mode: string): PluginOption[] => { + return [ + vue(), + vueJsx(), + Inspector(), + report(), + removeConsole(), + useCDN(mode), + viteConsoleLog(mode), + UnoCSS({ + hmrTopLevelAwait: false, + inspector: true, // 控制台是否打印 UnoCSS inspector + presets: [ + presetIcons({ + prefix: '', + extraProperties: { + display: 'inline-block', + 'vertical-align': 'middle', + }, + }), + UnoCssIcons({ + prefix: '', + extraProperties: { + display: 'inline-block', + 'vertical-align': 'middle', + }, + }), + ], + }), + compressPack(mode), + ]; +}; diff --git a/generator-code-web/build/resolve.ts b/generator-code-web/build/resolve.ts new file mode 100644 index 0000000..20cd7fe --- /dev/null +++ b/generator-code-web/build/resolve.ts @@ -0,0 +1,9 @@ +import { pathResolve } from './utils'; + +export const resolve = () => { + return { + alias: { + '@': pathResolve('../src'), + }, + }; +}; diff --git a/generator-code-web/build/server.ts b/generator-code-web/build/server.ts new file mode 100644 index 0000000..87da10a --- /dev/null +++ b/generator-code-web/build/server.ts @@ -0,0 +1,34 @@ +import type { ServerOptions } from 'vite'; + +import { wrapperEnv } from './utils'; + +/* 开发服务配置 */ +export const server = (mode: string) => { + const { VITE_PORT, VITE_APP_URL, VITE_STRICT_PORT } = wrapperEnv(mode); + + const options: ServerOptions = { + strictPort: VITE_STRICT_PORT, + port: VITE_PORT, + host: '0.0.0.0', + open: true, + cors: true, + proxy: { + '/api': { + target: VITE_APP_URL, + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/api/, '/api'), + }, + '/mock': { + target: VITE_APP_URL, + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/mock/, '/mock'), + }, + }, + // 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布 + warmup: { + clientFiles: ['./index.html', './src/{views,components}/*'], + }, + }; + + return options; +}; diff --git a/generator-code-web/build/utils.ts b/generator-code-web/build/utils.ts new file mode 100644 index 0000000..baabe97 --- /dev/null +++ b/generator-code-web/build/utils.ts @@ -0,0 +1,118 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import fs from 'fs'; +import path from 'path'; +import { visualizer } from 'rollup-plugin-visualizer'; +import { loadEnv } from 'vite'; +import viteCompression from 'vite-plugin-compression'; + +import { buildEnv } from './buildEnv'; + +export const root: string = process.cwd(); + +/** + * @description 根据可选的路径片段生成一个新的绝对路径 + * @param dir 路径片段,默认`build` + * @param metaUrl 模块的完整`url`,如果在`build`目录外调用必传`import.meta.url` + */ +// @ts-ignore +export const pathResolve = (dir = '.', metaUrl = import.meta.url) => { + // 当前文件目录的绝对路径 + const currentFileDir = dirname(fileURLToPath(metaUrl)); + // build 目录的绝对路径 + const buildDir = resolve(currentFileDir, 'build'); + // 解析的绝对路径 + const resolvedPath = resolve(currentFileDir, dir); + // 检查解析的绝对路径是否在 build 目录内 + if (resolvedPath.startsWith(buildDir)) { + // 在 build 目录内,返回当前文件路径 + return fileURLToPath(metaUrl); + } + // 不在 build 目录内,返回解析后的绝对路径 + return resolvedPath; +}; + +/** + * 封装环境变量配置 + * @param mode 当前模式 + * @param prefix 需要过滤的前缀 + * @link 参考:https://cn.vite.dev/config/#using-environment-variables-in-config + */ +// @ts-ignore +export const wrapperEnv = (mode: string, prefix: string = ''): ViteEnv => { + const env: any = loadEnv(mode, root, prefix); + + // 将变量转换指定类型 + for (const envName of Object.keys(env)) { + let realName: string | boolean | number = env[envName].replace(/\\n/g, '\n'); + realName = realName === 'true' ? true : realName === 'false' ? false : realName; + + if (envName === 'VITE_PORT') { + realName = Number(realName); + } + env[envName] = realName; + // @ts-ignore + process.env[envName] = realName; + } + return env; +}; + +/* 打包分析 */ +export const report = () => { + const lifecycle = process.env.npm_lifecycle_event; + return lifecycle === 'report' + ? visualizer({ open: true, brotliSize: true, filename: 'report.html' }) + : (null as any); +}; + +/* 启用gzip压缩 */ +export const compressPack = (mode: string) => { + const { VITE_COMPRESSION } = wrapperEnv(mode); + + return VITE_COMPRESSION == 'gzip' ? viteCompression({ threshold: 1024000 }) : null; +}; + +/** + * 计算打包后文件夹大小 + * @returns + */ +export const logOutputSize = (): string => { + const outDir = `../${buildEnv().outDir}`; + + function convertSize(size: number) { + const units: Array = ['byte', 'KB', 'MB', 'GB']; + + // 输入的单位是否存在 + let index = 0; + + while (size >= 1024) { + size /= 1024; + index++; + } + + return `${size.toFixed(2)} ${units[index]}`; + } + + // 计算文件夹字节大小 + function getFolderSize(folderPath: string) { + let size = 0; + + fs.readdirSync(folderPath).forEach((fileName: string) => { + const filePath = path.join(folderPath, fileName); + const stats = fs.statSync(filePath); + + if (stats.isFile()) { + size += stats.size; + } else if (stats.isDirectory()) { + size += getFolderSize(filePath); + } + }); + + return size; + } + + const folderSize = getFolderSize(path.resolve(__dirname, outDir)); + + return convertSize(folderSize); +};