perf: Improve the global loading display

pull/48/MERGE
vben 2024-06-02 23:50:58 +08:00
parent e650a0b863
commit 77d40dc763
14 changed files with 111 additions and 57 deletions

View File

@ -17,7 +17,38 @@ async function initApplication() {
overrides: overridesPreferences, overrides: overridesPreferences,
}); });
import('./bootstrap').then((m) => m.bootstrap(namespace)); // 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
destoryAppLoading();
}
/**
* loading
* index.html app
* cssloading
*/
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(); initApplication();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,8 @@ import { defaultPreferences } from './config';
import type { Preferences } from './types'; import type { Preferences } from './types';
const STORAGE_KEY = 'preferences'; const STORAGE_KEY = 'preferences';
const STORAGE_KEY_LOCALE = `${STORAGE_KEY}-locale`;
const STORAGE_KEY_THEME = `${STORAGE_KEY}-theme`;
interface initialOptions { interface initialOptions {
namespace: string; namespace: string;
@ -36,7 +38,7 @@ function isDarkTheme(theme: string) {
} }
class PreferenceManager { class PreferenceManager {
private cache: StorageManager<Preferences> | null = null; private cache: StorageManager | null = null;
private flattenedState: Flatten<Preferences>; private flattenedState: Flatten<Preferences>;
private initialPreferences: Preferences = defaultPreferences; private initialPreferences: Preferences = defaultPreferences;
private isInitialized: boolean = false; private isInitialized: boolean = false;
@ -60,6 +62,8 @@ class PreferenceManager {
*/ */
private _savePreferences(preference: Preferences) { private _savePreferences(preference: Preferences) {
this.cache?.setItem(STORAGE_KEY, preference); 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() { private loadCachedPreferences() {
return this.cache?.getItem(STORAGE_KEY); return this.cache?.getItem<Preferences>(STORAGE_KEY);
} }
/** /**
@ -231,8 +235,8 @@ class PreferenceManager {
/** /**
* *
* @param overrides - * overrides
* @param namespace - * namespace
*/ */
public async initPreferences({ namespace, overrides }: initialOptions) { public async initPreferences({ namespace, overrides }: initialOptions) {
// 是否初始化过 // 是否初始化过
@ -273,6 +277,8 @@ class PreferenceManager {
this.savePreferences(this.state); this.savePreferences(this.state);
// 从存储中移除偏好设置项 // 从存储中移除偏好设置项
this.cache?.removeItem(STORAGE_KEY); this.cache?.removeItem(STORAGE_KEY);
this.cache?.removeItem(STORAGE_KEY_THEME);
this.cache?.removeItem(STORAGE_KEY_LOCALE);
} }
/** /**

View File

@ -10,7 +10,7 @@ interface StorageItem<T> {
value: T; value: T;
} }
class StorageManager<T> { class StorageManager {
private prefix: string; private prefix: string;
private storage: Storage; private storage: Storage;
@ -67,7 +67,7 @@ class StorageManager<T> {
* @param defaultValue * @param defaultValue
* @returns * @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 fullKey = this.getFullKey(key);
const itemStr = this.storage.getItem(fullKey); const itemStr = this.storage.getItem(fullKey);
if (!itemStr) { if (!itemStr) {
@ -103,7 +103,7 @@ class StorageManager<T> {
* @param value * @param value
* @param ttl * @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 fullKey = this.getFullKey(key);
const expiry = ttl ? Date.now() + ttl : undefined; const expiry = ttl ? Date.now() + ttl : undefined;
const item: StorageItem<T> = { expiry, value }; const item: StorageItem<T> = { expiry, value };