perf: Improve the global loading display

This commit is contained in:
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,
});
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();

View File

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

View File

@ -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';

View File

@ -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
? [

View File

@ -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}`);
}
}
}

View File

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

View File

@ -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 };

View File

@ -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;

View File

@ -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',

View File

@ -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>

View File

@ -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>

View File

@ -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,
};

View File

@ -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);
}
/**

View File

@ -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 };