From b4e6e3f94d57ab608e690f66f678acfb4f25107f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AD=E5=A6=82?= <8775@163.com> Date: Fri, 3 Jan 2025 13:36:06 +0800 Subject: [PATCH] 5.1 --- sop-website/sop-website-frontend/build/cdn.ts | 60 ++++++++++ .../sop-website-frontend/build/compress.ts | 63 ++++++++++ .../sop-website-frontend/build/info.ts | 55 +++++++++ .../sop-website-frontend/build/optimize.ts | 34 ++++++ .../sop-website-frontend/build/plugins.ts | 76 ++++++++++++ .../sop-website-frontend/build/utils.ts | 110 ++++++++++++++++++ 6 files changed, 398 insertions(+) create mode 100755 sop-website/sop-website-frontend/build/cdn.ts create mode 100755 sop-website/sop-website-frontend/build/compress.ts create mode 100755 sop-website/sop-website-frontend/build/info.ts create mode 100755 sop-website/sop-website-frontend/build/optimize.ts create mode 100755 sop-website/sop-website-frontend/build/plugins.ts create mode 100755 sop-website/sop-website-frontend/build/utils.ts diff --git a/sop-website/sop-website-frontend/build/cdn.ts b/sop-website/sop-website-frontend/build/cdn.ts new file mode 100755 index 00000000..9e4bfe04 --- /dev/null +++ b/sop-website/sop-website-frontend/build/cdn.ts @@ -0,0 +1,60 @@ +import { Plugin as importToCDN } from "vite-plugin-cdn-import"; + +/** + * @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}", + modules: [ + { + name: "vue", + var: "Vue", + path: "vue.global.prod.min.js" + }, + { + name: "vue-router", + var: "VueRouter", + path: "vue-router.global.min.js" + }, + { + name: "vue-i18n", + var: "VueI18n", + path: "vue-i18n.runtime.global.prod.min.js" + }, + // 项目中没有直接安装vue-demi,但是pinia用到了,所以需要在引入pinia前引入vue-demi(https://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77) + { + name: "vue-demi", + var: "VueDemi", + path: "index.iife.min.js" + }, + { + name: "pinia", + var: "Pinia", + path: "pinia.iife.min.js" + }, + { + name: "element-plus", + var: "ElementPlus", + path: "index.full.min.js", + css: "index.min.css" + }, + { + name: "axios", + var: "axios", + path: "axios.min.js" + }, + { + name: "dayjs", + var: "dayjs", + path: "dayjs.min.js" + }, + { + name: "echarts", + var: "echarts", + path: "echarts.min.js" + } + ] +}); diff --git a/sop-website/sop-website-frontend/build/compress.ts b/sop-website/sop-website-frontend/build/compress.ts new file mode 100755 index 00000000..6178986b --- /dev/null +++ b/sop-website/sop-website-frontend/build/compress.ts @@ -0,0 +1,63 @@ +import type { Plugin } from "vite"; +import { isArray } from "@pureadmin/utils"; +import compressPlugin from "vite-plugin-compression"; + +export const configCompressPlugin = ( + compress: ViteCompression +): Plugin | Plugin[] => { + if (compress === "none") return null; + + const gz = { + // 生成的压缩包后缀 + ext: ".gz", + // 体积大于threshold才会被压缩 + threshold: 0, + // 默认压缩.js|mjs|json|css|html后缀文件,设置成true,压缩全部文件 + filter: () => true, + // 压缩后是否删除原始文件 + deleteOriginFile: false + }; + const br = { + ext: ".br", + algorithm: "brotliCompress", + threshold: 0, + filter: () => true, + deleteOriginFile: false + }; + + const codeList = [ + { k: "gzip", v: gz }, + { k: "brotli", v: br }, + { k: "both", v: [gz, br] } + ]; + + const plugins: Plugin[] = []; + + codeList.forEach(item => { + if (compress.includes(item.k)) { + if (compress.includes("clear")) { + if (isArray(item.v)) { + item.v.forEach(vItem => { + plugins.push( + compressPlugin(Object.assign(vItem, { deleteOriginFile: true })) + ); + }); + } else { + plugins.push( + compressPlugin(Object.assign(item.v, { deleteOriginFile: true })) + ); + } + } else { + if (isArray(item.v)) { + item.v.forEach(vItem => { + plugins.push(compressPlugin(vItem)); + }); + } else { + plugins.push(compressPlugin(item.v)); + } + } + } + }); + + return plugins; +}; diff --git a/sop-website/sop-website-frontend/build/info.ts b/sop-website/sop-website-frontend/build/info.ts new file mode 100755 index 00000000..92e910ba --- /dev/null +++ b/sop-website/sop-website-frontend/build/info.ts @@ -0,0 +1,55 @@ +import type { Plugin } from "vite"; +import { getPackageSize } from "./utils"; +import dayjs, { type Dayjs } from "dayjs"; +import duration from "dayjs/plugin/duration"; +import gradientString from "gradient-string"; +import boxen, { type Options as BoxenOptions } from "boxen"; +dayjs.extend(duration); + +const welcomeMessage = gradientString("cyan", "magenta").multiline(`Hello SOP`); + +const boxenOptions: BoxenOptions = { + padding: 0.5, + borderColor: "cyan", + borderStyle: "round" +}; + +export function viteBuildInfo(): Plugin { + let config: { command: string }; + let startTime: Dayjs; + let endTime: Dayjs; + let outDir: string; + return { + name: "vite:buildInfo", + configResolved(resolvedConfig) { + config = resolvedConfig; + outDir = resolvedConfig.build?.outDir ?? "dist"; + }, + buildStart() { + console.log(boxen(welcomeMessage, boxenOptions)); + if (config.command === "build") { + startTime = dayjs(new Date()); + } + }, + closeBundle() { + if (config.command === "build") { + endTime = dayjs(new Date()); + getPackageSize({ + folder: outDir, + callback: (size: string) => { + console.log( + boxen( + gradientString("cyan", "magenta").multiline( + `🎉 恭喜打包完成(总用时${dayjs + .duration(endTime.diff(startTime)) + .format("mm分ss秒")},打包后的大小为${size})` + ), + boxenOptions + ) + ); + } + }); + } + } + }; +} diff --git a/sop-website/sop-website-frontend/build/optimize.ts b/sop-website/sop-website-frontend/build/optimize.ts new file mode 100755 index 00000000..5f594911 --- /dev/null +++ b/sop-website/sop-website-frontend/build/optimize.ts @@ -0,0 +1,34 @@ +/** + * 此文件作用于 `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 = [ + "qs", + "mitt", + "dayjs", + "axios", + "pinia", + "vue-i18n", + "vue-types", + "js-cookie", + "vue-tippy", + "pinyin-pro", + "sortablejs", + "@vueuse/core", + "@pureadmin/utils", + "responsive-storage" +]; + +/** + * 在预构建中强制排除的依赖项 + * 温馨提示:所有以 `@iconify-icons/` 开头引入的的本地图标模块,都应该加入到下面的 `exclude` 里,因为平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好 + */ +const exclude = [ + "@iconify-icons/ep", + "@iconify-icons/ri", + "@pureadmin/theme/dist/browser-utils" +]; + +export { include, exclude }; diff --git a/sop-website/sop-website-frontend/build/plugins.ts b/sop-website/sop-website-frontend/build/plugins.ts new file mode 100755 index 00000000..37c8278a --- /dev/null +++ b/sop-website/sop-website-frontend/build/plugins.ts @@ -0,0 +1,76 @@ +import { cdn } from "./cdn"; +import vue from "@vitejs/plugin-vue"; +import { pathResolve } from "./utils"; +import { viteBuildInfo } from "./info"; +import svgLoader from "vite-svg-loader"; +import type { PluginOption } from "vite"; +import checker from "vite-plugin-checker"; +import vueJsx from "@vitejs/plugin-vue-jsx"; +import Inspector from "vite-plugin-vue-inspector"; +import { configCompressPlugin } from "./compress"; +import removeNoMatch from "vite-plugin-router-warn"; +import { visualizer } from "rollup-plugin-visualizer"; +import removeConsole from "vite-plugin-remove-console"; +import { themePreprocessorPlugin } from "@pureadmin/theme"; +import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite"; +import { genScssMultipleScopeVars } from "../src/layout/theme"; +import { vitePluginFakeServer } from "vite-plugin-fake-server"; + +export function getPluginsList( + VITE_CDN: boolean, + VITE_COMPRESSION: ViteCompression +): PluginOption[] { + const lifecycle = process.env.npm_lifecycle_event; + return [ + vue(), + // jsx、tsx语法支持 + vueJsx(), + VueI18nPlugin({ + jitCompilation: false, + include: [pathResolve("../locales/**")] + }), + checker({ + typescript: true, + vueTsc: true, + eslint: { + lintCommand: `eslint ${pathResolve("../{src,mock,build}/**/*.{vue,js,ts,tsx}")}`, + useFlatConfig: true + }, + terminal: false, + enableBuild: false + }), + // 按下Command(⌘)+Shift(⇧),然后点击页面元素会自动打开本地IDE并跳转到对应的代码位置 + Inspector(), + viteBuildInfo(), + /** + * 开发环境下移除非必要的vue-router动态路由警告No match found for location with path + * 非必要具体看 https://github.com/vuejs/router/issues/521 和 https://github.com/vuejs/router/issues/359 + * vite-plugin-router-warn只在开发环境下启用,只处理vue-router文件并且只在服务启动或重启时运行一次,性能消耗可忽略不计 + */ + removeNoMatch(), + // mock支持 + vitePluginFakeServer({ + logger: false, + include: "mock", + infixName: false, + enableProd: true + }), + // 自定义主题 + themePreprocessorPlugin({ + scss: { + multipleScopeVars: genScssMultipleScopeVars(), + extract: true + } + }), + // svg组件化支持 + svgLoader(), + VITE_CDN ? cdn : null, + configCompressPlugin(VITE_COMPRESSION), + // 线上环境删除console + removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }), + // 打包分析 + lifecycle === "report" + ? visualizer({ open: true, brotliSize: true, filename: "report.html" }) + : (null as any) + ]; +} diff --git a/sop-website/sop-website-frontend/build/utils.ts b/sop-website/sop-website-frontend/build/utils.ts new file mode 100755 index 00000000..64ac6d25 --- /dev/null +++ b/sop-website/sop-website-frontend/build/utils.ts @@ -0,0 +1,110 @@ +import dayjs from "dayjs"; +import { readdir, stat } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; +import { sum, formatBytes } from "@pureadmin/utils"; +import { + name, + version, + engines, + dependencies, + devDependencies +} from "../package.json"; + +/** 启动`node`进程时所在工作目录的绝对路径 */ +const root: string = process.cwd(); + +/** + * @description 根据可选的路径片段生成一个新的绝对路径 + * @param dir 路径片段,默认`build` + * @param metaUrl 模块的完整`url`,如果在`build`目录外调用必传`import.meta.url` + */ +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; +}; + +/** 设置别名 */ +const alias: Record = { + "@": pathResolve("../src"), + "@build": pathResolve() +}; + +/** 平台的名称、版本、运行所需的`node`和`pnpm`版本、依赖、最后构建时间的类型提示 */ +const __APP_INFO__ = { + pkg: { name, version, engines, dependencies, devDependencies }, + lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss") +}; + +/** 处理环境变量 */ +const wrapperEnv = (envConf: Recordable): ViteEnv => { + // 默认值 + const ret: ViteEnv = { + VITE_PORT: 9124, + VITE_PUBLIC_PATH: "", + VITE_ROUTER_HISTORY: "", + VITE_CDN: false, + VITE_HIDE_HOME: "false", + VITE_COMPRESSION: "none" + }; + + for (const envName of Object.keys(envConf)) { + let realName = envConf[envName].replace(/\\n/g, "\n"); + realName = + realName === "true" ? true : realName === "false" ? false : realName; + + if (envName === "VITE_PORT") { + realName = Number(realName); + } + ret[envName] = realName; + if (typeof realName === "string") { + process.env[envName] = realName; + } else if (typeof realName === "object") { + process.env[envName] = JSON.stringify(realName); + } + } + return ret; +}; + +const fileListTotal: number[] = []; + +/** 获取指定文件夹中所有文件的总大小 */ +const getPackageSize = options => { + const { folder = "dist", callback, format = true } = options; + readdir(folder, (err, files: string[]) => { + if (err) throw err; + let count = 0; + const checkEnd = () => { + ++count == files.length && + callback(format ? formatBytes(sum(fileListTotal)) : sum(fileListTotal)); + }; + files.forEach((item: string) => { + stat(`${folder}/${item}`, async (err, stats) => { + if (err) throw err; + if (stats.isFile()) { + fileListTotal.push(stats.size); + checkEnd(); + } else if (stats.isDirectory()) { + getPackageSize({ + folder: `${folder}/${item}/`, + callback: checkEnd + }); + } + }); + }); + files.length === 0 && callback(0); + }); +}; + +export { root, pathResolve, alias, __APP_INFO__, wrapperEnv, getPackageSize };