perf: optimize the diffPreferences logic and adjust the unit test (#4130)

pull/48/MERGE
Vben 2024-08-12 21:05:01 +08:00 committed by GitHub
parent 3f9ce63868
commit 7b46780af7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 177 additions and 99 deletions

View File

@ -14,8 +14,8 @@
::: :::
作者主要通过微信群提供帮助,如果你有问题,可以通过以下方式加入微信群 作者主要通过微信群提供帮助,如果你有问题,可以通过以下方式加入微信群
通过微信联系作者,注明加群来意: 通过微信联系作者,注明加群来意:
<img src="https://unpkg.com/@vbenjs/static-source@0.1.5/source/wechat.jpg" style="width: 300px;"/> <img src="https://unpkg.com/@vbenjs/static-source@0.1.5/source/wechat.jpg" style="width: 300px;"/>

View File

@ -25,6 +25,9 @@ const getDefaultPwaOptions = (name: string): Partial<PwaPluginOptions> => ({
}, },
}); });
/**
* importmap CDN
*/
const defaultImportmapOptions: ImportmapPluginOptions = { const defaultImportmapOptions: ImportmapPluginOptions = {
// 通过 Importmap CDN 方式引入, // 通过 Importmap CDN 方式引入,
// 目前只有esm.sh源兼容性好一点jspm.io对于 esm 入口要求高 // 目前只有esm.sh源兼容性好一点jspm.io对于 esm 入口要求高

View File

@ -80,7 +80,7 @@ async function viteImportMapPlugin(
const firstLayerKeys = Object.keys(scopes); const firstLayerKeys = Object.keys(scopes);
const inputMapScopes: string[] = []; const inputMapScopes: string[] = [];
firstLayerKeys.forEach((key) => { firstLayerKeys.forEach((key) => {
inputMapScopes.push(...Object.keys(scopes[key])); inputMapScopes.push(...Object.keys(scopes[key] || {}));
}); });
const inputMapImports = Object.keys(imports); const inputMapImports = Object.keys(imports);
@ -160,7 +160,10 @@ async function viteImportMapPlugin(
options.defaultProvider || DEFAULT_PROVIDER, options.defaultProvider || DEFAULT_PROVIDER,
); );
const resultHtml = await injectShimsToHtml(html, esModuleShimsSrc); const resultHtml = await injectShimsToHtml(
html,
esModuleShimsSrc || '',
);
html = await minify(resultHtml || html, { html = await minify(resultHtml || html, {
collapseWhitespace: true, collapseWhitespace: true,
minifyCSS: true, minifyCSS: true,

View File

@ -16,8 +16,8 @@ function resolvePackageVersion(
async function resolveMonorepoDependencies() { async function resolveMonorepoDependencies() {
const { packages } = await getPackages(); const { packages } = await getPackages();
const resultDevDependencies: Record<string, string> = {}; const resultDevDependencies: Record<string, string | undefined> = {};
const resultDependencies: Record<string, string> = {}; const resultDependencies: Record<string, string | undefined> = {};
const pkgsMeta: Record<string, string> = {}; const pkgsMeta: Record<string, string> = {};
for (const { packageJson } of packages) { for (const { packageJson } of packages) {

View File

@ -6,6 +6,14 @@ import { fs } from '@vben/node-utils';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
const getBoolean = (value: string | undefined) => value === 'true';
const getString = (value: string | undefined, fallback: string) =>
value ?? fallback;
const getNumber = (value: string | undefined, fallback: number) =>
Number(value) || fallback;
/** /**
* *
*/ */
@ -63,6 +71,7 @@ async function loadAndConvertEnv(
} & Partial<ApplicationPluginOptions> } & Partial<ApplicationPluginOptions>
> { > {
const envConfig = await loadEnv(match, confFiles); const envConfig = await loadEnv(match, confFiles);
const { const {
VITE_APP_TITLE, VITE_APP_TITLE,
VITE_BASE, VITE_BASE,
@ -74,22 +83,22 @@ async function loadAndConvertEnv(
VITE_PWA, VITE_PWA,
VITE_VISUALIZER, VITE_VISUALIZER,
} = envConfig; } = envConfig;
const compress = VITE_COMPRESS || '';
const compressTypes = compress const compressTypes = (VITE_COMPRESS ?? '')
.split(',') .split(',')
.filter((item) => item === 'brotli' || item === 'gzip'); .filter((item) => item === 'brotli' || item === 'gzip');
return { return {
appTitle: VITE_APP_TITLE ?? 'Vben Admin', appTitle: getString(VITE_APP_TITLE, 'Vben Admin'),
base: VITE_BASE || '/', base: getString(VITE_BASE, '/'),
compress: !!compress, compress: compressTypes.length > 0,
compressTypes: compressTypes as ('brotli' | 'gzip')[], compressTypes,
devtools: VITE_DEVTOOLS === 'true', devtools: getBoolean(VITE_DEVTOOLS),
injectAppLoading: VITE_INJECT_APP_LOADING === 'true', injectAppLoading: getBoolean(VITE_INJECT_APP_LOADING),
nitroMock: VITE_NITRO_MOCK === 'true', nitroMock: getBoolean(VITE_NITRO_MOCK),
port: Number(VITE_PORT) || 5173, port: getNumber(VITE_PORT, 5173),
pwa: VITE_PWA === 'true', pwa: getBoolean(VITE_PWA),
visualizer: VITE_VISUALIZER === 'true', visualizer: getBoolean(VITE_VISUALIZER),
}; };
} }

View File

@ -19,8 +19,13 @@
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
/* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */ /* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */
--muted: 220deg 6.82% 17.25%;
--muted-foreground: 215 20.2% 65.1%; /* --muted: 220deg 6.82% 17.25%; */
/* --muted-foreground: 215 20.2% 65.1%; */
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
/* 主题颜色 */ /* 主题颜色 */

View File

@ -19,8 +19,12 @@
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 222.2 84% 4.9%;
/* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */ /* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%; /* --muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%; */
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
/* 主题颜色 */ /* 主题颜色 */

View File

@ -3,58 +3,51 @@ import { describe, expect, it } from 'vitest';
import { diff } from './diff'; import { diff } from './diff';
describe('diff function', () => { describe('diff function', () => {
it('should correctly find differences in flat objects', () => { it('should return an empty object when comparing identical objects', () => {
const oldObj = { a: 1, b: 2, c: 3 }; const obj1 = { a: 1, b: { c: 2 } };
const newObj = { a: 1, b: 3, c: 3 }; const obj2 = { a: 1, b: { c: 2 } };
expect(diff(oldObj, newObj)).toEqual({ b: 3 }); expect(diff(obj1, obj2)).toEqual(undefined);
}); });
it('should correctly handle nested objects', () => { it('should detect simple changes in primitive values', () => {
const oldObj = { a: { b: 1, c: 2 }, d: 3 }; const obj1 = { a: 1, b: 2 };
const newObj = { a: { b: 1, c: 3 }, d: 3 }; const obj2 = { a: 1, b: 3 };
expect(diff(oldObj, newObj)).toEqual({ a: { b: 1, c: 3 } }); expect(diff(obj1, obj2)).toEqual({ b: 3 });
}); });
it('should correctly handle arrays`', () => { it('should detect nested object changes', () => {
const oldObj = { a: [1, 2, 3] }; const obj1 = { a: 1, b: { c: 2, d: 4 } };
const newObj = { a: [1, 2, 4] }; const obj2 = { a: 1, b: { c: 3, d: 4 } };
expect(diff(oldObj, newObj)).toEqual({ a: [1, 2, 4] }); expect(diff(obj1, obj2)).toEqual({ b: { c: 3 } });
}); });
it('should correctly handle nested arrays', () => { it('should handle array changes', () => {
const oldObj = { const obj1 = { a: [1, 2, 3], b: 2 };
a: [ const obj2 = { a: [1, 2, 4], b: 2 };
[1, 2], expect(diff(obj1, obj2)).toEqual({ a: [1, 2, 4] });
[3, 4],
],
};
const newObj = {
a: [
[1, 2],
[3, 5],
],
};
expect(diff(oldObj, newObj)).toEqual({
a: [
[1, 2],
[3, 5],
],
});
}); });
it('should return null if objects are identical', () => { it('should handle added keys', () => {
const oldObj = { a: 1, b: 2, c: 3 }; const obj1 = { a: 1 };
const newObj = { a: 1, b: 2, c: 3 }; const obj2 = { a: 1, b: 2 };
expect(diff(oldObj, newObj)).toBeNull(); expect(diff(obj1, obj2)).toEqual({ b: 2 });
}); });
it('should return differences between two objects excluding ignored fields', () => { it('should handle removed keys', () => {
const oldObj = { a: 1, b: 2, c: 3, d: 6 }; const obj1 = { a: 1, b: 2 };
const newObj = { a: 2, b: 2, c: 4, d: 5 }; const obj2 = { a: 1 };
const ignoreFields: (keyof typeof newObj)[] = ['a', 'd']; expect(diff(obj1, obj2)).toEqual(undefined);
});
const result = diff(oldObj, newObj, ignoreFields); it('should handle boolean value changes', () => {
const obj1 = { a: true, b: false };
const obj2 = { a: true, b: true };
expect(diff(obj1, obj2)).toEqual({ b: true });
});
expect(result).toEqual({ c: 4 }); it('should handle null and undefined values', () => {
const obj1 = { a: null, b: undefined };
const obj2: any = { a: 1, b: undefined };
expect(diff(obj1, obj2)).toEqual({ a: 1 });
}); });
}); });

View File

@ -1,4 +1,4 @@
type Diff<T = any> = T; // type Diff<T = any> = T;
// 比较两个数组是否相等 // 比较两个数组是否相等
@ -19,40 +19,78 @@ function arraysEqual<T>(a: T[], b: T[]): boolean {
} }
// 深度对比两个值 // 深度对比两个值
function deepEqual<T>(oldVal: T, newVal: T): boolean { // function deepEqual<T>(oldVal: T, newVal: T): boolean {
if ( // if (
typeof oldVal === 'object' && // typeof oldVal === 'object' &&
oldVal !== null && // oldVal !== null &&
typeof newVal === 'object' && // typeof newVal === 'object' &&
newVal !== null // newVal !== null
) { // ) {
return Array.isArray(oldVal) && Array.isArray(newVal) // return Array.isArray(oldVal) && Array.isArray(newVal)
? arraysEqual(oldVal, newVal) // ? arraysEqual(oldVal, newVal)
: diff(oldVal as any, newVal as any) === null; // : diff(oldVal as any, newVal as any) === null;
} else { // } else {
return oldVal === newVal; // return oldVal === newVal;
} // }
} // }
// 主要的 diff 函数 // // diff 函数
function diff<T extends object>( // function diff<T extends object>(
oldObj: T, // oldObj: T,
newObj: T, // newObj: T,
ignoreFields: (keyof T)[] = [], // ignoreFields: (keyof T)[] = [],
): { [K in keyof T]?: Diff<T[K]> } | null { // ): { [K in keyof T]?: Diff<T[K]> } | null {
const difference: { [K in keyof T]?: Diff<T[K]> } = {}; // const difference: { [K in keyof T]?: Diff<T[K]> } = {};
for (const key in oldObj) { // for (const key in oldObj) {
if (ignoreFields.includes(key)) continue; // if (ignoreFields.includes(key)) continue;
const oldValue = oldObj[key]; // const oldValue = oldObj[key];
const newValue = newObj[key]; // const newValue = newObj[key];
if (!deepEqual(oldValue, newValue)) { // if (!deepEqual(oldValue, newValue)) {
difference[key] = newValue; // difference[key] = newValue;
// }
// }
// return Object.keys(difference).length === 0 ? null : difference;
// }
type DiffResult<T> = Partial<{
[K in keyof T]: T[K] extends object ? DiffResult<T[K]> : T[K];
}>;
function diff<T extends Record<string, any>>(obj1: T, obj2: T): DiffResult<T> {
function findDifferences(o1: any, o2: any): any {
if (Array.isArray(o1) && Array.isArray(o2)) {
if (!arraysEqual(o1, o2)) {
return o2;
}
return undefined;
} }
if (
typeof o1 === 'object' &&
typeof o2 === 'object' &&
o1 !== null &&
o2 !== null
) {
const diffResult: any = {};
const keys = new Set([...Object.keys(o1), ...Object.keys(o2)]);
keys.forEach((key) => {
const valueDiff = findDifferences(o1[key], o2[key]);
if (valueDiff !== undefined) {
diffResult[key] = valueDiff;
}
});
return Object.keys(diffResult).length > 0 ? diffResult : undefined;
}
return o1 === o2 ? undefined : o2;
} }
return Object.keys(difference).length === 0 ? null : difference; return findDifferences(obj1, obj2);
} }
export { arraysEqual, diff }; export { arraysEqual, diff };

View File

@ -58,6 +58,20 @@ describe('page.vue', () => {
expect(contentDiv.classes()).toContain('custom-class'); expect(contentDiv.classes()).toContain('custom-class');
}); });
it('does not render title slot if title prop is provided', () => {
const wrapper = mount(Page, {
props: {
title: 'Test Title',
},
slots: {
title: '<p>Title Slot Content</p>',
},
});
expect(wrapper.text()).toContain('Title Slot Content');
expect(wrapper.html()).not.toContain('Test Title');
});
it('does not render description slot if description prop is provided', () => { it('does not render description slot if description prop is provided', () => {
const wrapper = mount(Page, { const wrapper = mount(Page, {
props: { props: {
@ -68,7 +82,7 @@ describe('page.vue', () => {
}, },
}); });
expect(wrapper.text()).toContain('Test Description'); expect(wrapper.text()).toContain('Description Slot Content');
expect(wrapper.html()).not.toContain('Description Slot Content'); expect(wrapper.html()).not.toContain('Test Description');
}); });
}); });

View File

@ -24,11 +24,20 @@ const props = withDefaults(defineProps<Props>(), {
v-if="description || $slots.description || title" v-if="description || $slots.description || title"
class="bg-card px-6 py-4" class="bg-card px-6 py-4"
> >
<div class="mb-2 flex justify-between text-xl font-bold leading-10"> <slot name="title">
{{ title }} <div
</div> v-if="title"
<template v-if="description">{{ description }}</template> class="mb-2 flex justify-between text-lg font-semibold"
<slot v-else name="description"></slot> >
{{ title }}
</div>
</slot>
<slot name="description">
<p v-if="description" class="text-muted-foreground">
{{ description }}
</p>
</slot>
</div> </div>
<div :class="contentClass" class="m-4"> <div :class="contentClass" class="m-4">