perf: Improve the global loading display
parent
e650a0b863
commit
77d40dc763
|
@ -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,7 +1,7 @@
|
|||
import { defineConfig } from '@vben/vite-config';
|
||||
|
||||
export default defineConfig({
|
||||
appcation: {
|
||||
application: {
|
||||
compress: false,
|
||||
compressTypes: ['brotli', 'gzip'],
|
||||
importmap: false,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
? [
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ function defineLibraryConfig(options: DefineLibraryOptions = {}) {
|
|||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
fileName: () => 'index.mjs',
|
||||
fileName: 'index.mjs',
|
||||
formats: ['es'],
|
||||
},
|
||||
rollupOptions: {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in New Issue