Procházet zdrojové kódy

perf: Improve the global loading display

vben před 1 rokem
rodič
revize
77d40dc763

+ 32 - 1
apps/antd-view/src/main.ts

@@ -17,7 +17,38 @@ async function initApplication() {
     overrides: overridesPreferences,
   });
 
-  import('./bootstrap').then((m) => m.bootstrap(namespace));
+  // 启动应用并挂载
+  // vue应用主要逻辑及视图
+  const { bootstrap } = await import('./bootstrap');
+  await bootstrap(namespace);
+
+  // 移除并销毁loading
+  destoryAppLoading();
+}
+
+/**
+ * 移除并销毁loading
+ * 放在这里是而不是放在 index.html 的app标签内,主要是因为这样比较不会生硬,渲染过快可能会有闪烁
+ * 通过先添加css动画隐藏,在动画结束后在移除loading节点来改善体验
+ */
+function destoryAppLoading() {
+  // 全局搜索文件 loading.html, 找到对应的节点
+  const loadingElement = document.querySelector('#__app-loading__');
+  if (loadingElement) {
+    loadingElement.classList.add('hidden');
+    const injectLoadingElements = document.querySelectorAll(
+      '[data-app-loading^="inject"]',
+    );
+    // 过渡动画结束后移除loading节点
+    loadingElement.addEventListener(
+      'transitionend',
+      () => {
+        loadingElement.remove();
+        injectLoadingElements.forEach((el) => el?.remove());
+      },
+      { once: true },
+    );
+  }
 }
 
 initApplication();

+ 1 - 1
apps/antd-view/vite.config.mts

@@ -1,7 +1,7 @@
 import { defineConfig } from '@vben/vite-config';
 
 export default defineConfig({
-  appcation: {
+  application: {
     compress: false,
     compressTypes: ['brotli', 'gzip'],
     importmap: false,

+ 1 - 0
internal/node-utils/src/index.ts

@@ -13,6 +13,7 @@ export { toPosixPath } from './path';
 export { prettierFormat } from './prettier';
 export type { Package } from '@manypkg/get-packages';
 export { consola } from 'consola';
+export { nanoid } from 'nanoid';
 export { readPackageJSON } from 'pkg-types';
 export { rimraf } from 'rimraf';
 export { $, chalk as colors, fs, spinner } from 'zx';

+ 4 - 6
internal/vite-config/src/config/application.ts

@@ -7,11 +7,11 @@ import { defineConfig, loadEnv, mergeConfig } from 'vite';
 import { getApplicationConditionPlugins } from '../plugins';
 import { getCommonConfig } from './common';
 
-import type { DefineAppcationOptions } from '../typing';
+import type { DefineApplicationOptions } from '../typing';
 
-function defineApplicationConfig(options: DefineAppcationOptions = {}) {
+function defineApplicationConfig(options: DefineApplicationOptions = {}) {
   return defineConfig(async ({ command, mode }) => {
-    const { appcation = {}, vite = {} } = options;
+    const { application = {}, vite = {} } = options;
     const root = process.cwd();
     const isBuild = command === 'build';
     const env = loadEnv(mode, root);
@@ -29,11 +29,10 @@ function defineApplicationConfig(options: DefineAppcationOptions = {}) {
       mock: true,
       mode,
       turboConsole: false,
-      ...appcation,
+      ...application,
     });
 
     const applicationConfig: UserConfig = {
-      // },
       build: {
         rollupOptions: {
           output: {
@@ -44,7 +43,6 @@ function defineApplicationConfig(options: DefineAppcationOptions = {}) {
         },
         target: 'es2015',
       },
-      //     },
       esbuild: {
         drop: isBuild
           ? [

+ 12 - 7
internal/vite-config/src/config/index.ts

@@ -1,7 +1,6 @@
+import { existsSync } from 'node:fs';
 import { join } from 'node:path';
 
-import { fs } from '@vben/node-utils';
-
 import { defineApplicationConfig } from './application';
 import { defineLibraryConfig } from './library';
 
@@ -18,13 +17,19 @@ function defineConfig(options: DefineConfig = {}) {
   // 根据包是否存在 index.html,自动判断类型
   if (type === 'auto') {
     const htmlPath = join(process.cwd(), 'index.html');
-    projectType = fs.existsSync(htmlPath) ? 'appcation' : 'library';
+    projectType = existsSync(htmlPath) ? 'application' : 'library';
   }
 
-  if (projectType === 'appcation') {
-    return defineApplicationConfig(defineOptions);
-  } else if (projectType === 'library') {
-    return defineLibraryConfig(defineOptions);
+  switch (projectType) {
+    case 'application': {
+      return defineApplicationConfig(defineOptions);
+    }
+    case 'library': {
+      return defineLibraryConfig(defineOptions);
+    }
+    default: {
+      throw new Error(`Unsupported project type: ${projectType}`);
+    }
   }
 }
 

+ 1 - 1
internal/vite-config/src/config/library.ts

@@ -33,7 +33,7 @@ function defineLibraryConfig(options: DefineLibraryOptions = {}) {
       build: {
         lib: {
           entry: 'src/index.ts',
-          fileName: () => 'index.mjs',
+          fileName: 'index.mjs',
           formats: ['es'],
         },
         rollupOptions: {

+ 6 - 10
internal/vite-config/src/plugins/extra-app-config.ts

@@ -35,7 +35,7 @@ async function viteExtraAppConfigPlugin({
 
   return {
     async configResolved(config) {
-      publicPath = config.base;
+      publicPath = ensureTrailingSlash(config.base);
       source = await getConfigSource();
     },
     async generateBundle() {
@@ -59,21 +59,13 @@ async function viteExtraAppConfigPlugin({
     },
     name: 'vite:extra-app-config',
     async transformIndexHtml(html) {
-      publicPath = publicPath.endsWith('/') ? publicPath : `${publicPath}/`;
       const hash = `v=${version}-${generatorContentHash(source, 8)}`;
 
       const appConfigSrc = `${publicPath}${GLOBAL_CONFIG_FILE_NAME}?${hash}`;
 
       return {
         html,
-        tags: [
-          {
-            attrs: {
-              src: appConfigSrc,
-            },
-            tag: 'script',
-          },
-        ],
+        tags: [{ attrs: { src: appConfigSrc }, tag: 'script' }],
       };
     },
   };
@@ -94,4 +86,8 @@ async function getConfigSource() {
   return source;
 }
 
+function ensureTrailingSlash(path: string) {
+  return path.endsWith('/') ? path : `${path}/`;
+}
+
 export { viteExtraAppConfigPlugin };

+ 2 - 2
internal/vite-config/src/plugins/index.ts

@@ -20,7 +20,7 @@ import { viteImportMapPlugin } from './importmap';
 import { viteInjectAppLoadingPlugin } from './inject-app-loading';
 
 import type {
-  AppcationPluginOptions,
+  ApplicationPluginOptions,
   CommonPluginOptions,
   ConditionPlugin,
   LibraryPluginOptions,
@@ -82,7 +82,7 @@ async function getCommonConditionPlugins(
  * 根据条件获取应用类型的vite插件
  */
 async function getApplicationConditionPlugins(
-  options: AppcationPluginOptions,
+  options: ApplicationPluginOptions,
 ): Promise<PluginOption[]> {
   // 单独取,否则commonOptions拿不到
   const isBuild = options.isBuild;

+ 5 - 8
internal/vite-config/src/plugins/inject-app-loading/index.ts

@@ -14,14 +14,14 @@ async function viteInjectAppLoadingPlugin(
 ): Promise<PluginOption | undefined> {
   const loadingHtml = await getLoadingRawByHtmlTemplate();
   const envRaw = isBuild ? 'prod' : 'dev';
-  const cacheName = `'__${env.VITE_APP_NAMESPACE}-${envRaw}-theme__'`;
+  const cacheName = `'${env.VITE_APP_NAMESPACE}-${envRaw}-preferences-theme'`;
 
   // 获取缓存的主题
   // 保证黑暗主题下,刷新页面时,loading也是黑暗主题
   const injectScript = `
-  <script>
+  <script data-app-loading="inject-js">
   var theme = localStorage.getItem(${cacheName});
-  document.documentElement.classList.toggle('dark', theme === 'dark');
+  document.documentElement.classList.toggle('dark', /dark/.test(theme));
 </script>
 `;
 
@@ -34,11 +34,8 @@ async function viteInjectAppLoadingPlugin(
     name: 'vite:inject-app-loading',
     transformIndexHtml: {
       handler(html) {
-        const re = /<div\s*id\s*=\s*"app"\s*>(\s*)<\/div>/;
-        html = html.replace(
-          re,
-          `<div id="app">${injectScript}${loadingHtml}</div>`,
-        );
+        const re = /<body\s*>/;
+        html = html.replace(re, `<body>${injectScript}${loadingHtml}`);
         return html;
       },
       order: 'pre',

+ 12 - 2
internal/vite-config/src/plugins/inject-app-loading/loading-antd.html

@@ -1,4 +1,4 @@
-<style>
+<style data-app-loading="inject-css">
   html {
     /* same as ant-design-vue/dist/reset.css setting, avoid the title line-height changed */
     line-height: 1.15;
@@ -13,6 +13,10 @@
   }
 
   .loading {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 9999;
     display: flex;
     flex-direction: column;
     align-items: center;
@@ -22,6 +26,12 @@
     background-color: #f4f7f9;
   }
 
+  .loading.hidden {
+    visibility: hidden;
+    opacity: 0;
+    transition: all 1s ease-out;
+  }
+
   .loading .dots {
     display: flex;
     align-items: center;
@@ -96,7 +106,7 @@
     }
   }
 </style>
-<div class="loading">
+<div class="loading" id="__app-loading__">
   <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
   <div class="title"><%= VITE_GLOB_APP_TITLE %></div>
 </div>

+ 11 - 2
internal/vite-config/src/plugins/inject-app-loading/loading.html

@@ -1,4 +1,4 @@
-<style>
+<style data-app-loading="inject-css">
   html {
     /* same as ant-design-vue/dist/reset.css setting, avoid the title line-height changed */
     line-height: 1.15;
@@ -8,6 +8,7 @@
     position: fixed;
     top: 0;
     left: 0;
+    z-index: 9999;
     display: flex;
     flex-direction: column;
     align-items: center;
@@ -15,6 +16,14 @@
     width: 100%;
     height: 100%;
     background-color: #f4f7f9;
+
+    /* transition: all 0.8s ease-out; */
+  }
+
+  .loading.hidden {
+    visibility: hidden;
+    opacity: 0;
+    transition: all 1s ease-out;
   }
 
   .dark .loading {
@@ -96,7 +105,7 @@
     }
   }
 </style>
-<div class="loading">
+<div class="loading" id="__app-loading__">
   <div class="loader"></div>
   <div class="title"><%= VITE_GLOB_APP_TITLE %></div>
 </div>

+ 11 - 10
internal/vite-config/src/typing.ts

@@ -4,7 +4,7 @@ import type { PluginOptions } from 'vite-plugin-dts';
 
 import viteTurboConsolePlugin from 'unplugin-turbo-console/vite';
 
-export interface IImportMap {
+interface IImportMap {
   imports?: Record<string, string>;
   scopes?: {
     [scope: string]: Record<string, string>;
@@ -40,7 +40,7 @@ interface CommonPluginOptions {
   /** 是否开启devtools */
   devtools?: boolean;
   /** 环境变量 */
-  env: Record<string, any>;
+  env?: Record<string, any>;
   /** 是否构建模式 */
   isBuild?: boolean;
   /** 构建模式 */
@@ -49,7 +49,7 @@ interface CommonPluginOptions {
   visualizer?: PluginVisualizerOptions | boolean;
 }
 
-interface AppcationPluginOptions extends CommonPluginOptions {
+interface ApplicationPluginOptions extends CommonPluginOptions {
   /** 开启 gzip 压缩 */
   compress?: boolean;
   /** 压缩类型 */
@@ -80,12 +80,12 @@ interface LibraryPluginOptions extends CommonPluginOptions {
   injectLibCss?: boolean;
 }
 
-interface AppcationOptions extends AppcationPluginOptions {}
+interface ApplicationOptions extends ApplicationPluginOptions {}
 
 interface LibraryOptions extends LibraryPluginOptions {}
 
-interface DefineAppcationOptions {
-  appcation?: AppcationOptions;
+interface DefineApplicationOptions {
+  application?: ApplicationOptions;
   vite?: UserConfig;
 }
 
@@ -95,17 +95,18 @@ interface DefineLibraryOptions {
 }
 
 type DefineConfig = {
-  type?: 'appcation' | 'auto' | 'library';
-} & DefineAppcationOptions &
+  type?: 'application' | 'auto' | 'library';
+} & DefineApplicationOptions &
   DefineLibraryOptions;
 
 export type {
-  AppcationPluginOptions,
+  ApplicationPluginOptions,
   CommonPluginOptions,
   ConditionPlugin,
-  DefineAppcationOptions,
+  DefineApplicationOptions,
   DefineConfig,
   DefineLibraryOptions,
+  IImportMap,
   ImportmapPluginOptions,
   LibraryPluginOptions,
 };

+ 10 - 4
packages/@vben-core/forward/preferences/src/preferences.ts

@@ -21,6 +21,8 @@ import { defaultPreferences } from './config';
 import type { Preferences } from './types';
 
 const STORAGE_KEY = 'preferences';
+const STORAGE_KEY_LOCALE = `${STORAGE_KEY}-locale`;
+const STORAGE_KEY_THEME = `${STORAGE_KEY}-theme`;
 
 interface initialOptions {
   namespace: string;
@@ -36,7 +38,7 @@ function isDarkTheme(theme: string) {
 }
 
 class PreferenceManager {
-  private cache: StorageManager<Preferences> | null = null;
+  private cache: StorageManager | null = null;
   private flattenedState: Flatten<Preferences>;
   private initialPreferences: Preferences = defaultPreferences;
   private isInitialized: boolean = false;
@@ -60,6 +62,8 @@ class PreferenceManager {
    */
   private _savePreferences(preference: Preferences) {
     this.cache?.setItem(STORAGE_KEY, preference);
+    this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);
+    this.cache?.setItem(STORAGE_KEY_THEME, preference.app.themeMode);
   }
 
   /**
@@ -89,7 +93,7 @@ class PreferenceManager {
    *  从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
    */
   private loadCachedPreferences() {
-    return this.cache?.getItem(STORAGE_KEY);
+    return this.cache?.getItem<Preferences>(STORAGE_KEY);
   }
 
   /**
@@ -231,8 +235,8 @@ class PreferenceManager {
 
   /**
    * 覆盖偏好设置
-   * @param overrides - 要覆盖的偏好设置
-   * @param namespace - 命名空间
+   * overrides  要覆盖的偏好设置
+   * namespace  命名空间
    */
   public async initPreferences({ namespace, overrides }: initialOptions) {
     // 是否初始化过
@@ -273,6 +277,8 @@ class PreferenceManager {
     this.savePreferences(this.state);
     // 从存储中移除偏好设置项
     this.cache?.removeItem(STORAGE_KEY);
+    this.cache?.removeItem(STORAGE_KEY_THEME);
+    this.cache?.removeItem(STORAGE_KEY_LOCALE);
   }
 
   /**

+ 3 - 3
packages/@vben-core/shared/chche/src/storage-manager.ts

@@ -10,7 +10,7 @@ interface StorageItem<T> {
   value: T;
 }
 
-class StorageManager<T> {
+class StorageManager {
   private prefix: string;
   private storage: Storage;
 
@@ -67,7 +67,7 @@ class StorageManager<T> {
    * @param defaultValue 当项不存在或已过期时返回的默认值
    * @returns 值,如果项已过期或解析错误则返回默认值
    */
-  getItem(key: string, defaultValue: T | null = null): T | null {
+  getItem<T>(key: string, defaultValue: T | null = null): T | null {
     const fullKey = this.getFullKey(key);
     const itemStr = this.storage.getItem(fullKey);
     if (!itemStr) {
@@ -103,7 +103,7 @@ class StorageManager<T> {
    * @param value 值
    * @param ttl 存活时间(毫秒)
    */
-  setItem(key: string, value: T, ttl?: number): void {
+  setItem<T>(key: string, value: T, ttl?: number): void {
     const fullKey = this.getFullKey(key);
     const expiry = ttl ? Date.now() + ttl : undefined;
     const item: StorageItem<T> = { expiry, value };