Merge pull request #3195 from timongh/change-code-loading

改进代码字符串的加载方式2
This commit is contained in:
Grant Howard 2022-04-05 11:31:17 +08:00 committed by GitHub
commit e651e33863
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 486 additions and 48 deletions

View File

@ -1,10 +1,10 @@
import {
addComponentListener,
getComponentSettings,
getGeneralSettings,
isComponentEnabled,
getComponentSettings,
} from '@/core/settings'
import { fullyLoaded, contentLoaded } from '@/core/life-cycle'
import { contentLoaded, fullyLoaded } from '@/core/life-cycle'
import { LoadingMode } from '@/core/loading-mode'
import { Widget } from '@/components/widget'
import { ComponentMetadata } from './types'
@ -26,7 +26,10 @@ const loadI18n = async (component: ComponentMetadata) => {
}
const { addI18nData } = await import('@/components/i18n/helpers')
Object.entries(component.i18n).forEach(([language, data]) => {
const { map = [], regex = [] } = data
const {
map = [],
regex = [],
} = data
addI18nData(language, map, regex)
})
}
@ -35,14 +38,20 @@ const getComponentWidgetName = (component: ComponentMetadata) => `${component.na
/** 提取组件中的Widget */
const loadWidget = async (component: ComponentMetadata) => {
if (component.widget) {
const widget: Widget = { ...component.widget, name: getComponentWidgetName(component) }
const widget: Widget = {
...component.widget,
name: getComponentWidgetName(component),
}
const { addData } = await import('@/plugins/data')
const { WidgetsPlugin } = await import('./settings-panel')
addData(WidgetsPlugin, (widgets: Widget[]) => {
if (widgets.find(w => w.name === widget.name)) {
return
}
const { urlInclude, urlExclude } = widget
const {
urlInclude,
urlExclude,
} = widget
if (component.urlInclude) {
if (urlInclude) {
urlInclude.push(...component.urlInclude)
@ -66,7 +75,7 @@ const loadedComponents: Record<string, Record<string, any>> = {}
/**
* @param name
*/
export const importComponent = <ComponentExportType> (
export const importComponent = <ComponentExportType>(
name: string,
): Record<string, ComponentExportType> => {
if (!(name in loadedComponents)) {
@ -137,12 +146,17 @@ export const loadComponent = async (component: ComponentMetadata) => {
/** 加载所有用户组件的定义 (不运行) */
export const loadAllUserComponents = async () => {
const { settings } = await import('@/core/settings')
const { batchParseCode } = await import('@/core/external-input')
const {
loadFeaturesFromCodes,
FeatureKind,
} = await import('@/core/external-input/load-features-from-codes')
const loadUserComponent = (component: ComponentMetadata) => {
components.push(component)
componentsMap[component.name] = component
}
const userComponents = await batchParseCode<ComponentMetadata>(
const userComponents = await loadFeaturesFromCodes(
FeatureKind.Component,
Object.keys(settings.userComponents),
Object.values(settings.userComponents).map(it => it.code),
)
userComponents.forEach(loadUserComponent)
@ -153,12 +167,12 @@ export const loadAllComponents = async () => {
const { loadAllPlugins } = await import('@/plugins/plugin')
const loadComponents = () => loadAllPlugins(components)
.then(() => Promise.allSettled(components.map(loadI18n)))
.then(() => Promise.allSettled(components.map(loadComponent)))
.then(async () => {
.then(() => Promise.allSettled(components.map(loadComponent))).then(async () => {
if (generalSettings.devMode) {
const { componentLoadTime, componentResolveTime } = await import(
'@/core/performance/component-trace'
)
const {
componentLoadTime,
componentResolveTime,
} = await import('@/core/performance/component-trace')
const { logStats } = await import('@/core/performance/stats')
logStats('components block', componentLoadTime)
logStats('components resolve', componentResolveTime)

View File

@ -1,4 +1,4 @@
import { pickFile } from './file-picker'
import { pickFile } from '@/core/file-picker'
/** 外部输入包装类型, 详见`parseExternalInput`的文档 */
export type ExternalInput<T> = undefined | string | T
@ -54,22 +54,3 @@ export const parseExternalInput = async <T>(input: ExternalInput<T>): Promise<T>
return input
}
}
export const batchParseCode = async<T>(inputs: string[]): Promise<T[]> => {
try {
const exports = {}
const result = inputs.map(input => eval(input) as T)
if (Object.values(exports).length > 0) {
const { coreApis } = await import('@/core/core-apis')
return Object.values(exports).map(value => {
if (typeof value === 'function') {
return value(coreApis) as T
}
return value as T
})
}
return result
} catch (error) {
console.error(error)
return null
}
}

View File

@ -0,0 +1,31 @@
import {
loadFeatureCode, LoadFeatureCodeResult,
} from '@/core/external-input/load-feature-code'
type LdRes<X> = LoadFeatureCodeResult<X>
type SettledRes<T> = PromiseSettledResult<T>
type FilledRes<T> = PromiseFulfilledResult<T>
const unwrapSettledResult = <T>(r: SettledRes<T>): T => (r as FilledRes<T>).value
const mapSettledArray = <T>(arr: SettledRes<T>[]): T[] => (
arr.map(unwrapSettledResult)
)
const mapSettleResult = <T>(p: Promise<SettledRes<T>[]>): Promise<T[]> => (
p.then(mapSettledArray)
)
/**
* feature
*
* @param codes
* @returns `Promise`
*/
export const loadFeatureCodeAllSettled = <X>(
codes: string[],
): Promise<LoadFeatureCodeResult<X>[]> => lodash(codes)
.map<Promise<LdRes<X>>>(loadFeatureCode)
.thru<Promise<SettledRes<LdRes<X>>[]>>(arr => Promise.allSettled(arr))
.thru(mapSettleResult)
.value()

View File

@ -0,0 +1,164 @@
import {
loadFeatureCode, LoadFeatureCodeResult, LoadFeatureCodeResultError,
LoadFeatureCodeResultOk,
} from '@/core/external-input/load-feature-code'
import { FeatureBase } from '@/components/types'
interface ResultInstance {
readonly isOk: <X extends FeatureBase>(
this: LoadFeatureCodeAllResult<X>
) => this is LoadFeatureCodeAllResultOk<X>
readonly isError: (
this: LoadFeatureCodeAllResult<FeatureBase>
) => this is LoadFeatureCodeAllResultError
readonly isNoExport: (
this: LoadFeatureCodeAllResult<FeatureBase>
) => this is LoadFeatureCodeAllResultNoExport
readonly isCodeThrew: (
this: LoadFeatureCodeAllResult<FeatureBase>
) => this is LoadFeatureCodeAllResultCodeThrew
}
/**
* features
*
* @namespace
* @property features
*/
interface LoadFeatureCodeAllResultOk<X extends FeatureBase> extends ResultInstance {
readonly tag: 'Ok',
readonly features: X[],
}
/** 代码没有导出任何值 */
interface LoadFeatureCodeAllResultNoExport extends ResultInstance {
readonly tag: 'NoExport',
}
/**
*
*
* @namespace
* @property thrown
*/
interface LoadFeatureCodeAllResultCodeThrew extends ResultInstance {
readonly tag: 'CodeThrew',
readonly thrown: unknown,
}
type LoadFeatureCodeAllResultError =
LoadFeatureCodeAllResultNoExport
| LoadFeatureCodeAllResultCodeThrew
type LoadFeatureCodeAllResult<X extends FeatureBase> =
LoadFeatureCodeAllResultOk<X>
| LoadFeatureCodeAllResultError
const resultProto: ResultInstance = {
isOk() { return this.tag === 'Ok' },
isError() { return this.tag !== 'Ok' },
isNoExport() { return this.tag === 'NoExport' },
isCodeThrew() { return this.tag === 'CodeThrew' },
}
const okResult = <X extends FeatureBase>(
features: X[],
): LoadFeatureCodeAllResultOk<X> => lodash.create(
resultProto,
{
tag: 'Ok' as const,
features,
},
)
const noExportResult = lodash.create(resultProto, {
tag: 'NoExport' as const,
})
const codeThrewResult = (thrown: unknown): LoadFeatureCodeAllResultCodeThrew => lodash.create(
resultProto,
{
tag: 'CodeThrew' as const,
thrown,
},
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type Task<Ok, Err = never> = Promise<Ok>
type LdRes<X> = LoadFeatureCodeResult<X>
type LdOk<X> = LoadFeatureCodeResultOk<X>
type LdErr = LoadFeatureCodeResultError
type LdAllRes<X> = LoadFeatureCodeAllResult<X>
type LdAllOk<X> = LoadFeatureCodeAllResultOk<X>
type LdAllErr = LoadFeatureCodeAllResultError
type LoadCodesTask<X> = Task<LdOk<X>[], [number, LdErr]>
// covert `Task<LdRes<X>>` to `Task<LdOk<X>, LdErr>`
const rejectErrorResult = <X>(t: Task<LdRes<X>>): Task<LdOk<X>, LdErr> => (
t.then(r => (r.isOk() ? r : Promise.reject(r)))
)
// load feature code, and return `Task<LdOk<X>, LdErr>`
const loadCode = <X>(code: string): Task<LdOk<X>, LdErr> => (
rejectErrorResult(loadFeatureCode(code))
)
// covert `Task`'s `Err` type from `T` to `[number, T]`
const addIndexToRejected = <N extends number, O, E>(
t: Task<O, E>,
i: N,
// eslint-disable-next-line prefer-promise-reject-errors
): Task<O, [N, E]> => t.catch(e => Promise.reject([i, e]))
// create `LdAllOk` from an array of `LdOk`
const createOkResult = <X>(arr: LdOk<X>[]): LdAllOk<X> => (
okResult(arr.map(r => r.feature))
)
// create `LdAllErr` from `[number, LdErr]`
const createErrResult = (t: [number, LdErr]): LdAllErr => (
t[1].isCodeThrew()
? codeThrewResult(t[1].thrown)
: noExportResult
)
// load all feature codes, and return `LoadCodesTask`
const loadCodes = <X>(codes: string[]): LoadCodesTask<X> => lodash(codes)
.map<Task<LdOk<X>, LdErr>>(loadCode)
.map(addIndexToRejected)
.thru<LoadCodesTask<X>>(arr => Promise.all(arr))
.value()
// create a `LdAllRes` wrapped by `Task`
const createTaskResult = <X>(t: LoadCodesTask<X>): Task<LdAllRes<X>> => t
.then(createOkResult)
.catch(createErrResult)
/**
* feature
*
*
*
* @param codes
* @returns `Promise` `LoadFeatureCodeAllResult`
*/
const loadFeatureCodeAll = <X>(codes: string[]): Promise<LoadFeatureCodeAllResult<X>> => (
lodash(codes)
.thru<LoadCodesTask<X>>(loadCodes)
.thru(createTaskResult)
.value()
)
export {
loadFeatureCodeAll,
LoadFeatureCodeAllResult,
LoadFeatureCodeAllResultOk,
LoadFeatureCodeAllResultError,
LoadFeatureCodeAllResultNoExport,
LoadFeatureCodeAllResultCodeThrew,
}

View File

@ -0,0 +1,112 @@
import { FeatureBase } from '@/components/types'
interface ResultInstance {
readonly isOk: <X extends FeatureBase>(
this: LoadFeatureCodeResult<X>
) => this is LoadFeatureCodeResultOk<X>
readonly isError: (
this: LoadFeatureCodeResult<FeatureBase>
) => this is LoadFeatureCodeResultError
readonly isNoExport: (
this: LoadFeatureCodeResult<FeatureBase>
) => this is LoadFeatureCodeResultNoExport
readonly isCodeThrew: (
this: LoadFeatureCodeResult<FeatureBase>
) => this is LoadFeatureCodeResultCodeThrew
}
/**
* feature
*
* @namespace
* @property feature
*/
interface LoadFeatureCodeResultOk<X extends FeatureBase> extends ResultInstance {
readonly tag: 'Ok',
readonly feature: X,
}
/** 代码没有导出任何值 */
interface LoadFeatureCodeResultNoExport extends ResultInstance {
readonly tag: 'NoExport',
}
/**
*
*
* @namespace
* @property thrown
*/
interface LoadFeatureCodeResultCodeThrew extends ResultInstance {
readonly tag: 'CodeThrew',
readonly thrown: unknown,
}
type LoadFeatureCodeResultError =
LoadFeatureCodeResultNoExport
| LoadFeatureCodeResultCodeThrew
type LoadFeatureCodeResult<X extends FeatureBase> =
LoadFeatureCodeResultOk<X>
| LoadFeatureCodeResultError
const resultProto: ResultInstance = {
isOk() { return this.tag === 'Ok' },
isError() { return this.tag !== 'Ok' },
isNoExport() { return this.tag === 'NoExport' },
isCodeThrew() { return this.tag === 'CodeThrew' },
}
const okResult = <X extends FeatureBase>(feature: X): LoadFeatureCodeResultOk<X> => lodash.create(
resultProto,
{
tag: 'Ok' as const,
feature,
},
)
const noExportResult = lodash.create(resultProto, {
tag: 'NoExport' as const,
})
const codeThrewResult = (thrown: unknown): LoadFeatureCodeResultCodeThrew => lodash.create(
resultProto,
{
tag: 'CodeThrew' as const,
thrown,
},
)
/**
* feature
*
* @param code
* @returns `Promise` {@link LoadFeatureCodeResult}
*/
const loadFeatureCode = async <X extends FeatureBase>(
code: string,
): Promise<LoadFeatureCodeResult<X>> => {
// 收集代码导出值
const exports = {}
try {
eval(code)
} catch (thrown) {
return codeThrewResult(thrown)
}
const values = Object.values(exports)
if (values.length === 0) {
return noExportResult
}
return okResult(values[0] as X)
}
export {
loadFeatureCode,
LoadFeatureCodeResult,
LoadFeatureCodeResultOk,
LoadFeatureCodeResultError,
LoadFeatureCodeResultNoExport,
LoadFeatureCodeResultCodeThrew,
}

View File

@ -0,0 +1,124 @@
import {
LoadFeatureCodeResultError, LoadFeatureCodeResultOk,
} from '@/core/external-input/load-feature-code'
import { Toast } from '@/core/toast'
import { useScopedConsole } from '@/core/utils/log'
import { ComponentMetadata } from '@/components/types'
import { PluginMetadata } from '@/plugins/plugin'
import {
loadFeatureCodeAllSettled,
} from '@/core/external-input/load-feature-code-all-settled'
const curConsole = useScopedConsole(
'@/core/external-input/load-features-from-codes.ts',
)
export enum FeatureKind {
Component = 'Component',
Plugin = 'Plugin',
}
const logError = (kind: FeatureKind): (
featureName: string, err: LoadFeatureCodeResultError) => void => {
const prefix = kind === FeatureKind.Component ? 'component' : 'plugin'
return (featureName, err) => {
if (err.isNoExport()) {
curConsole.error(
`${prefix} '${featureName}' exports no value, failed to load`,
)
} else {
curConsole.error(
`${prefix} '${featureName}' throws something when importing, failed to load`,
{ thrown: err.thrown },
)
}
}
}
const reportErrToUser = (
featureKind: FeatureKind,
errNames: string[],
): void => {
type ErrInfo = number | string[]
const emptyErrInfo: () => string[] = () => []
const accErrInfo = (acc: ErrInfo, featureName: string): ErrInfo => {
if (Array.isArray(acc)) {
if (acc.length < 3) {
acc.push(featureName)
return acc
}
return 4
}
return acc + 1
}
const reportErrInfo = (kind: FeatureKind, info: ErrInfo): void => {
const kindName = kind === FeatureKind.Component ? '组件' : '插件'
if (Array.isArray(info)) {
Toast.error(
`${kindName} "${info.join('", "')}" 加载失败。请向我们反馈,以解决此问题。`,
`${kindName}加载失败`,
)
} else {
Toast.error(
`${info}${kindName}加载失败,请向我们反馈,以解决此问题。`,
`${kindName}加载失败`,
)
}
}
const errInfo = errNames.reduce(accErrInfo, emptyErrInfo())
reportErrInfo(featureKind, errInfo)
}
export type FeatureMetadata = ComponentMetadata | PluginMetadata
/**
*
*
* console
*
* `names` `codes`
*
* @param kind
* @param names
* @param codes
* @return
*/
export async function loadFeaturesFromCodes(
kind: FeatureKind.Component,
names: string[],
codes: string[],
): Promise<ComponentMetadata[]>
export async function loadFeaturesFromCodes(
kind: FeatureKind.Plugin,
names: string[],
codes: string[],
): Promise<PluginMetadata[]>
export async function loadFeaturesFromCodes(
kind: FeatureKind,
names: string[],
codes: string[],
): Promise<FeatureMetadata[]> {
const results = await loadFeatureCodeAllSettled<FeatureMetadata>(codes)
const [namedOk, namedErr] = lodash(results)
.map((r, i) => [names[i], r] as const)
.partition(([, r]) => r.isOk())
.value()
// 输出日志
lodash.forEach(namedErr, lodash.spread(logError(kind)))
// 向用户输出错误报告
if (namedErr.length > 0) {
const errNames = namedErr.map(([name]) => name)
reportErrToUser(kind, errNames)
}
return lodash.map(
namedOk,
([, r]) => (r as LoadFeatureCodeResultOk<FeatureMetadata>).feature,
)
}

View File

@ -1,7 +1,7 @@
import { ComponentMetadata, FeatureBase } from '@/components/component'
import { deleteValue } from '@/core/utils'
import { CoreApis } from '../core/core-apis'
import { addData, registerData, registerAndGetData } from './data'
import { CoreApis } from '@/core/core-apis'
import { addData, registerAndGetData, registerData } from './data'
import { addHook, getHook } from './hook'
/** 插件初始化时的传入参数, 可以解构并调用 */
@ -23,6 +23,7 @@ export interface PluginMinimalData extends FeatureBase {
/** 显示名称, 默认同插件名称 */
displayName?: string
}
type PartialRequired<Target, Props extends keyof Target> = Target & {
[P in Props]-?: Target[P]
}
@ -48,7 +49,7 @@ export const plugins: PluginMetadata[] = getBuiltInPlugins()
/**
*
* @param input
* @param code
*/
export const installPlugin = async (code: string) => {
const { parseExternalInput } = await import('../core/external-input')
@ -95,13 +96,11 @@ export const installPlugin = async (code: string) => {
*/
export const uninstallPlugin = async (nameOrDisplayName: string) => {
const { settings } = await import('@/core/settings')
const existingPlugin = Object.entries(settings.userPlugins)
.find(([name, { displayName }]) => {
if (name === nameOrDisplayName || displayName === nameOrDisplayName) {
return true
}
return false
})
const existingPlugin = Object.entries(settings.userPlugins).find(
([name, { displayName }]) => (
name === nameOrDisplayName || displayName === nameOrDisplayName
),
)
if (!existingPlugin) {
throw new Error(`没有找到与名称'${nameOrDisplayName}'相关联的插件`)
}
@ -155,20 +154,33 @@ export const loadPlugin = async (plugin: PluginMetadata) => {
* @param components
*/
export const loadAllPlugins = async (components: ComponentMetadata[]) => {
const { settings, getGeneralSettings } = await import('@/core/settings')
const { batchParseCode } = await import('@/core/external-input')
const otherPlugins = components
const {
settings,
getGeneralSettings,
} = await import('@/core/settings')
const {
loadFeaturesFromCodes,
FeatureKind,
} = await import('@/core/external-input/load-features-from-codes')
const otherPlugins = lodash(components)
.map(extractPluginFromComponent)
.filter(p => p !== null)
.concat(await batchParseCode<PluginMetadata>(
.map(p => p as PluginMetadata)
.concat(await loadFeaturesFromCodes(
FeatureKind.Plugin,
Object.keys(settings.userPlugins),
Object.values(settings.userPlugins).map(p => p.code),
))
.value()
plugins.push(...otherPlugins)
return Promise.allSettled(
plugins.map(loadPlugin),
).then(async () => {
if (getGeneralSettings().devMode) {
const { pluginLoadTime, pluginResolveTime } = await import('@/core/performance/plugin-trace')
const {
pluginLoadTime,
pluginResolveTime,
} = await import('@/core/performance/plugin-trace')
const { logStats } = await import('@/core/performance/stats')
logStats('plugins block', pluginLoadTime)
logStats('plugins resolve', pluginResolveTime)