fix: 兼容 Chrome 90 前端运行环境
- 为管理端与用户中心补齐旧浏览器运行时 API 和回归测试 - 增加现代 CSS 与选择器降级,保持现有界面效果 - 固定 Chrome 90 构建目标并补充兼容依赖
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
"@easyflow/types": "workspace:*",
|
||||
"@easyflow/utils": "workspace:*",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@ungap/structured-clone": "1.3.0",
|
||||
"@vueuse/core": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"element-plus": "catalog:",
|
||||
@@ -43,6 +44,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/ungap__structured-clone": "1.2.0",
|
||||
"unplugin-element-plus": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,11 @@ const sizeMap = {
|
||||
<template>
|
||||
<div
|
||||
class="tag"
|
||||
:class="[`tag--${size}`, { 'tag--round': round }, `tag--border-${border}`]"
|
||||
:class="[
|
||||
`tag--${size}`,
|
||||
{ 'tag--closable': closable, 'tag--round': round },
|
||||
`tag--border-${border}`,
|
||||
]"
|
||||
:style="[
|
||||
tagStyle,
|
||||
{
|
||||
@@ -212,15 +216,15 @@ const sizeMap = {
|
||||
}
|
||||
|
||||
/* 为可关闭标签调整内边距 */
|
||||
.tag:has(.tag__close) {
|
||||
.tag--closable {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.tag:has(.tag__close).tag--small {
|
||||
.tag--closable.tag--small {
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.tag:has(.tag__close).tag--large {
|
||||
.tag--closable.tag--large {
|
||||
padding-right: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable perfectionist/sort-imports -- 兼容层必须先于所有业务依赖执行。 */
|
||||
import './polyfills';
|
||||
|
||||
import { initPreferences } from '@easyflow/preferences';
|
||||
import { unmountGlobalLoading } from '@easyflow/utils';
|
||||
|
||||
|
||||
82
easyflow-ui-usercenter/app/src/polyfills.test.ts
Normal file
82
easyflow-ui-usercenter/app/src/polyfills.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
installArrayAtPolyfill,
|
||||
installObjectHasOwnPolyfill,
|
||||
installPromiseWithResolversPolyfill,
|
||||
installStringAtPolyfill,
|
||||
installStructuredClonePolyfill,
|
||||
installUrlCanParsePolyfill,
|
||||
} from './polyfills';
|
||||
|
||||
const descriptors = new Map<
|
||||
string,
|
||||
[object, PropertyKey, PropertyDescriptor | undefined]
|
||||
>();
|
||||
|
||||
function remember(key: string, target: object, property: PropertyKey) {
|
||||
descriptors.set(key, [
|
||||
target,
|
||||
property,
|
||||
Object.getOwnPropertyDescriptor(target, property),
|
||||
]);
|
||||
}
|
||||
|
||||
function remove(target: object, property: PropertyKey) {
|
||||
Reflect.deleteProperty(target, property);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
remember('arrayAt', Array.prototype, 'at');
|
||||
remember('stringAt', String.prototype, 'at');
|
||||
remember('hasOwn', Object, 'hasOwn');
|
||||
remember('withResolvers', Promise, 'withResolvers');
|
||||
remember('structuredClone', globalThis, 'structuredClone');
|
||||
remember('canParse', URL, 'canParse');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const [target, property, descriptor] of descriptors.values()) {
|
||||
if (descriptor) Object.defineProperty(target, property, descriptor);
|
||||
else remove(target, property);
|
||||
}
|
||||
descriptors.clear();
|
||||
});
|
||||
|
||||
describe('chrome 90 browser polyfills', () => {
|
||||
it('restores missing collection and object APIs', () => {
|
||||
remove(Array.prototype, 'at');
|
||||
remove(String.prototype, 'at');
|
||||
remove(Object, 'hasOwn');
|
||||
installArrayAtPolyfill();
|
||||
installStringAtPolyfill();
|
||||
installObjectHasOwnPolyfill();
|
||||
|
||||
expect(['first', 'last'].at(-1)).toBe('last');
|
||||
expect('EasyFlow'.at(-1)).toBe('w');
|
||||
expect(Object.hasOwn({ ready: true }, 'ready')).toBe(true);
|
||||
});
|
||||
|
||||
it('restores missing async, clone and URL APIs', async () => {
|
||||
remove(Promise, 'withResolvers');
|
||||
remove(globalThis, 'structuredClone');
|
||||
remove(URL, 'canParse');
|
||||
installPromiseWithResolversPolyfill();
|
||||
installStructuredClonePolyfill();
|
||||
installUrlCanParsePolyfill();
|
||||
|
||||
// eslint-disable-next-line n/no-unsupported-features/es-syntax -- 测试目标就是验证该 API 的兼容实现。
|
||||
const { promise, resolve } = Promise.withResolvers<string>();
|
||||
resolve('ready');
|
||||
await expect(promise).resolves.toBe('ready');
|
||||
|
||||
const source: { map: Map<string, number>; self?: unknown } = {
|
||||
map: new Map([['answer', 2026]]),
|
||||
};
|
||||
source.self = source;
|
||||
const clone = structuredClone(source);
|
||||
expect(clone.self).toBe(clone);
|
||||
expect(clone.map.get('answer')).toBe(2026);
|
||||
expect(URL.canParse('/chat', 'https://easyflow.example')).toBe(true);
|
||||
});
|
||||
});
|
||||
208
easyflow-ui-usercenter/app/src/polyfills.ts
Normal file
208
easyflow-ui-usercenter/app/src/polyfills.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/* eslint-disable no-extend-native -- 该文件用于集中安装旧浏览器缺失的原生 API。 */
|
||||
|
||||
import { deserialize, serialize } from '@ungap/structured-clone';
|
||||
|
||||
type AtFunction = (index: number) => unknown;
|
||||
type StructuredCloneFunction = <T>(
|
||||
value: T,
|
||||
options?: StructuredSerializeOptions,
|
||||
) => T;
|
||||
type WithResolversResult<T> = {
|
||||
promise: Promise<T>;
|
||||
reject: (reason?: unknown) => void;
|
||||
resolve: (value: PromiseLike<T> | T) => void;
|
||||
};
|
||||
|
||||
const objectConstructor = Object as ObjectConstructor & {
|
||||
hasOwn?: (object: unknown, property: PropertyKey) => boolean;
|
||||
};
|
||||
const arrayPrototype = Array.prototype as unknown[] & {
|
||||
at?: AtFunction;
|
||||
toReversed?: () => unknown[];
|
||||
toSorted?: (
|
||||
compareFn?: (left: unknown, right: unknown) => number,
|
||||
) => unknown[];
|
||||
toSpliced?: (
|
||||
start: number,
|
||||
deleteCount?: number,
|
||||
...items: unknown[]
|
||||
) => unknown[];
|
||||
};
|
||||
const stringPrototype = String.prototype as string & {
|
||||
at?: AtFunction;
|
||||
};
|
||||
const promiseConstructor = Promise as PromiseConstructor & {
|
||||
withResolvers?: <T>() => WithResolversResult<T>;
|
||||
};
|
||||
const urlConstructor = URL as typeof URL & {
|
||||
canParse?: (url: string | URL, base?: string | URL) => boolean;
|
||||
};
|
||||
const browserGlobal = globalThis as typeof globalThis & {
|
||||
structuredClone?: StructuredCloneFunction;
|
||||
};
|
||||
|
||||
/** 将索引转换为 at 方法使用的整数。 */
|
||||
function toIntegerOrInfinity(index: number): number {
|
||||
const numericIndex = Number(index);
|
||||
if (Number.isNaN(numericIndex) || numericIndex === 0) return 0;
|
||||
return numericIndex > 0 ? Math.floor(numericIndex) : Math.ceil(numericIndex);
|
||||
}
|
||||
|
||||
/** 安装 Object.hasOwn 兼容实现。 */
|
||||
export function installObjectHasOwnPolyfill() {
|
||||
if (typeof objectConstructor.hasOwn === 'function') return;
|
||||
Object.defineProperty(Object, 'hasOwn', {
|
||||
configurable: true,
|
||||
value(object: unknown, property: PropertyKey) {
|
||||
if (object === null || object === undefined) {
|
||||
throw new TypeError('Cannot convert undefined or null to object');
|
||||
}
|
||||
// eslint-disable-next-line unicorn/new-for-builtins -- Object() 保留 ToObject 语义。
|
||||
return Object.prototype.hasOwnProperty.call(Object(object), property);
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** 安装 Array.prototype.at 兼容实现。 */
|
||||
export function installArrayAtPolyfill() {
|
||||
if (typeof arrayPrototype.at === 'function') return;
|
||||
Object.defineProperty(Array.prototype, 'at', {
|
||||
configurable: true,
|
||||
value(this: ArrayLike<unknown> | null | undefined, index: number): unknown {
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError('Array.prototype.at called on null or undefined');
|
||||
}
|
||||
const target = this as ArrayLike<unknown>;
|
||||
const numericLength = Number(target.length);
|
||||
const length =
|
||||
Number.isNaN(numericLength) || numericLength <= 0
|
||||
? 0
|
||||
: Math.min(Math.floor(numericLength), Number.MAX_SAFE_INTEGER);
|
||||
const relativeIndex = toIntegerOrInfinity(index);
|
||||
const actualIndex =
|
||||
relativeIndex >= 0 ? relativeIndex : length + relativeIndex;
|
||||
return actualIndex < 0 || actualIndex >= length
|
||||
? undefined
|
||||
: target[actualIndex];
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** 安装 String.prototype.at 兼容实现。 */
|
||||
export function installStringAtPolyfill() {
|
||||
if (typeof stringPrototype.at === 'function') return;
|
||||
Object.defineProperty(String.prototype, 'at', {
|
||||
configurable: true,
|
||||
value(this: unknown, index: number): string | undefined {
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError('String.prototype.at called on null or undefined');
|
||||
}
|
||||
const target = String(this);
|
||||
const relativeIndex = toIntegerOrInfinity(index);
|
||||
const actualIndex =
|
||||
relativeIndex >= 0 ? relativeIndex : target.length + relativeIndex;
|
||||
return actualIndex < 0 || actualIndex >= target.length
|
||||
? undefined
|
||||
: target[actualIndex];
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** 安装不会修改原数组的数组复制方法。 */
|
||||
export function installArrayCopyingPolyfills() {
|
||||
if (typeof arrayPrototype.toReversed !== 'function') {
|
||||
Object.defineProperty(Array.prototype, 'toReversed', {
|
||||
configurable: true,
|
||||
value() {
|
||||
return Array.prototype.slice.call(this).reverse();
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (typeof arrayPrototype.toSorted !== 'function') {
|
||||
Object.defineProperty(Array.prototype, 'toSorted', {
|
||||
configurable: true,
|
||||
value(compareFn?: (left: unknown, right: unknown) => number) {
|
||||
return Array.prototype.slice.call(this).sort(compareFn);
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (typeof arrayPrototype.toSpliced !== 'function') {
|
||||
Object.defineProperty(Array.prototype, 'toSpliced', {
|
||||
configurable: true,
|
||||
value(start: number, deleteCount?: number, ...items: unknown[]) {
|
||||
const copy = Array.prototype.slice.call(this);
|
||||
if (arguments.length === 1) copy.splice(start);
|
||||
else copy.splice(start, deleteCount as number, ...items);
|
||||
return copy;
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** 安装 Promise.withResolvers 兼容实现。 */
|
||||
export function installPromiseWithResolversPolyfill() {
|
||||
// eslint-disable-next-line n/no-unsupported-features/es-syntax -- 此处专门检测并补齐该新 API。
|
||||
if (typeof promiseConstructor.withResolvers === 'function') return;
|
||||
Object.defineProperty(Promise, 'withResolvers', {
|
||||
configurable: true,
|
||||
value<T>(this: PromiseConstructor): WithResolversResult<T> {
|
||||
let reject!: (reason?: unknown) => void;
|
||||
let resolve!: (value: PromiseLike<T> | T) => void;
|
||||
const promise = new this<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
return { promise, reject, resolve };
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** 安装可处理复杂值与循环引用的 structuredClone 兼容实现。 */
|
||||
export function installStructuredClonePolyfill() {
|
||||
if (typeof browserGlobal.structuredClone === 'function') return;
|
||||
Object.defineProperty(globalThis, 'structuredClone', {
|
||||
configurable: true,
|
||||
value: <T>(value: T) => deserialize(serialize(value)) as T,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** 安装 URL.canParse 兼容实现。 */
|
||||
export function installUrlCanParsePolyfill() {
|
||||
if (typeof urlConstructor.canParse === 'function') return;
|
||||
Object.defineProperty(URL, 'canParse', {
|
||||
configurable: true,
|
||||
value(url: string | URL, base?: string | URL) {
|
||||
try {
|
||||
const parsedUrl =
|
||||
arguments.length > 1
|
||||
? new URL(String(url), String(base))
|
||||
: new URL(String(url));
|
||||
return parsedUrl instanceof URL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** 在业务模块加载前安装 Chrome 90 缺失的运行时能力。 */
|
||||
export function installLegacyBrowserPolyfills() {
|
||||
installObjectHasOwnPolyfill();
|
||||
installArrayAtPolyfill();
|
||||
installStringAtPolyfill();
|
||||
installArrayCopyingPolyfills();
|
||||
installPromiseWithResolversPolyfill();
|
||||
installStructuredClonePolyfill();
|
||||
installUrlCanParsePolyfill();
|
||||
}
|
||||
|
||||
installLegacyBrowserPolyfills();
|
||||
Reference in New Issue
Block a user