You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Electron中TypeScript类继承报错及循环引用问题求助

问题解决:Electron中类继承与循环引用错误处理

问题背景

编写了继承ProjectElement的TextElement类,编辑器IntelliSense无报错,但在Electron应用中先后出现两个错误:

  1. 初始使用CommonJS模块时,报错Class extends value undefined is not a constructor or null
  2. 将module改为非CommonJS后,出现Cannot access 'ProjectElement' before initialization,排查确认是project.tsx -> list.ts -> TextElement.tsx -> project.tsx的循环引用导致

相关代码与报错信息

初始报错(CommonJS模块下)

Uncaught TypeError: Class extends value undefined is not a constructor or null
    at eval (TextElement.tsx:9:37)
    at ./src/libs/elements/TextElement.tsx (main.js:993:1)
    at __webpack_require__ (main.js:1455:42)
    at eval (list.ts:6:39)
    at ./src/libs/elements/list.ts (main.js:1004:1)
    at __webpack_require__ (main.js:1455:42)
    at eval (project.tsx:10:32)
    at ./src/libs/project.tsx (main.js:1037:1)
    at __webpack_require__ (main.js:1455:42)
    at eval (Editor.tsx:45:35)

更新后报错(非CommonJS模块下)

Uncaught ReferenceError: Cannot access 'ProjectElement' before initialization
    at Module.ProjectElement (project.tsx:6:63)
    at eval (TextElement.tsx:11:65)
    at ./src/libs/elements/TextElement.tsx (main.js:993:1)
    at __webpack_require__ (main.js:1345:41)
    at eval (list.ts:5:70)
    at ./src/libs/elements/list.ts (main.js:1004:1)
    at __webpack_require__ (main.js:1345:41)
    at eval (project.tsx:12:72)
    at ./src/libs/project.tsx (main.js:1037:1)
    at __webpack_require__ (main.js:1345:41)

核心代码片段

TextElement.tsx

class TextElement extends ProjectElement {
    initialize() {
        this.name = "Text"
        this.elementID = "text"
        this.properties.addProperty(new ElementProperty("text", "Hi!"))
    }

    render(): any {
        return <div>
            {this.properties.getProp("text")}
        </div>
    }
}

project.tsx(关键部分)

import ELEMENT_LIST from './elements/list';

// ... 接口定义 ...

class ProjectElement {
    constructor() {
        this.uid = uuidV4()
        this.name = "Element"
        this.elementID = "generic"
        this.properties = new ElementProperties()

        this.initialize()
        this.render = this.render.bind(this)
    }

    // ... 其他方法 ...
}

// ... 其他类定义 ...

export default Project
export {
    ProjectElement,
    ElementProperty,
    ElementProperties,
    ELEMENT_LIST
}

list.ts

import TextElement from "./TextElement";

const ELEMENT_LIST: {[key: string]: any} = {
    "text": TextElement
}

export default ELEMENT_LIST

tsconfig.json

{
  "compilerOptions": {
    "target": "es2016",
    "jsx": "react",
    "module": "CommonJS",
    "rootDir": "./",
    "moduleResolution": "node",
    "outDir": "./ts-build",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

问题分析

  1. 第一个错误原因:TextElement未正确导入ProjectElement类,编辑器靠类型推断不报错,但运行时ProjectElement为undefined;同时CommonJS模块的加载顺序导致TextElement加载时,project.tsx还未完成类的导出。
  2. 第二个错误原因:循环引用链导致非CommonJS模块加载时,出现先访问未初始化类的情况,触发"Cannot access before initialization"错误。

解决方案

方案1:拆分核心基类(彻底解决循环引用)

将ProjectElement、ElementProperty、ElementProperties及相关接口单独抽离到新文件,切断循环引用链:

  1. 创建src/libs/element-base.ts:
import React from 'react';
import { v4 as uuidV4 } from 'uuid';

// 接口定义
export interface ProjectElement {
    uid: string
    properties: ElementProperties
    name: string
    render(): any,
    elementID: string
}

export interface ElementProperty {
    value: any
    name: string
    uid: string
    editable: boolean
}

export interface ElementProperties {
    properties: {
        [key: string]: ElementProperty
    }
}

// 基类实现
export class ProjectElement {
    constructor() {
        this.uid = uuidV4()
        this.name = "Element"
        this.elementID = "generic"
        this.properties = new ElementProperties()

        this.initialize()
        this.render = this.render.bind(this)
    }

    serialize() {
        return {
            uid: this.uid,
            properties: this.properties.getAllProps(),
            elementID: this.elementID
        }
    }

    render() {
        return <div>
            Here you put your custom component logic!
        </div>
    }

    initialize() {
        console.log(`Component "${this.uid}" initialized!`)
    }

    load(json: any) {
        this.uid = json.uid
        this.name = json.name
        this.elementID = json.elementID
        this.properties.loadProperties(json.properties)
    }
}

export class ElementProperties {
    constructor() {
        this.properties = {};
    }

    addProperty(property: ElementProperty) {
        this.properties[property.name] = property
    }

    updateProperty(name: string, value: any) {
        this.properties[name].changeValue(value)
    }

    deleteProperty(name: string) {
        delete this.properties[name]
    }

    getProp(name: string) {
        let property = this.properties[name]
        return property.value
    }

    getAllProps() {
        let properties: {
            [key: string]: any
        } = {}
        Object.keys(this.properties).map((property_key: string) => {
            const property = this.properties[property_key]
            properties[property_key] = property.value
        })
        return properties
    }

    loadProperties(json: {
        [key: string]: any
    }) {
        Object.keys(json).map(key => {
            const property = json[key]
            const newProperty = new ElementProperty(property.name, property.value)
            this.addProperty(newProperty)
        })
    }
}

export class ElementProperty {
    constructor(name: string, value: any) {
        this.value = value
        this.name = name
        this.uid = uuidV4()
        this.editable = true
    }

    changeValue(newValue: any) {
        this.value = newValue
    }

    serialize() {
        return {
            name: this.name,
            value: this.value,
            uid: this.uid,
            editable: this.editable
        }
    }
}
  1. 修改project.tsx:
import React from 'react';
import { v4 as uuidV4 } from 'uuid';
import ELEMENT_LIST from './elements/list';
import { ProjectElement, ElementProperty, ElementProperties } from './element-base';

interface Project {
    elements: {
        [key: string]: ProjectElement
    }
    name: string
    id: string
}

class Project {
    constructor() {
        this.elements = {}
        this.name = ""
        this.id = ""
    }

    addElement(element: ProjectElement) {
        this.elements[element.uid] = element
    }

    serialize() {
        let serializedElements: {
            [key: string]: {}
        } = {}
        Object.keys(this.elements).map((element_key) => {
            const element: ProjectElement = this.elements[element_key]
            const serializedElement = element.serialize()
            serializedElements[element.uid] = serializedElement
        })

        return {
            name: this.name,
            elements: serializedElements,
        }
    }

    load(json: any) {
        const elements: {
            [key: string]: any
        } = json.elements
        const name: string = json.name

        this.name = name
        this.id = json.id
        Object.keys(elements).map(key => {
            const element = elements[key]
            const elementID: string = element.elementID
            const ElementConstructor = ELEMENT_LIST[elementID]
            const newElement = new ElementConstructor()
            newElement.load(element)
            this.addElement(newElement)
        })
    }
}

export default Project
export { ELEMENT_LIST }
  1. 修改TextElement.tsx:
import { ProjectElement, ElementProperty } from '../element-base';

class TextElement extends ProjectElement {
    initialize() {
        this.name = "Text"
        this.elementID = "text"
        this.properties.addProperty(new ElementProperty("text", "Hi!"))
    }

    render(): any {
        return <div>
            {this.properties.getProp("text")}
        </div>
    }
}

export default TextElement

方案2:动态导入延迟加载(临时修复循环引用)

若暂时不想拆分文件,可通过动态导入延迟加载ELEMENT_LIST,避免初始化时的循环:

修改project.tsx的load方法:

async load(json: any) {
    const elements: {
        [key: string]: any
    } = json.elements
    const name: string = json.name
    // 动态导入ELEMENT_LIST,避免初始化循环
    const ELEMENT_LIST = (await import('./elements/list')).default;

    this.name = name
    this.id = json.id
    Object.keys(elements).map(key => {
        const element = elements[key]
        const elementID: string = element.elementID
        const ElementConstructor = ELEMENT_LIST[elementID]
        const newElement = new ElementConstructor()
        newElement.load(element)
        this.addElement(newElement)
    })
}

同时确保TextElement.tsx顶部正确导入基类:

import { ProjectElement, ElementProperty } from '../project';

方案3:优化tsconfig配置

保持module: "ESNext",并开启moduleResolution: "bundler",让webpack更好处理模块加载顺序:

{
  "compilerOptions": {
    "target": "es2016",
    "jsx": "react",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "rootDir": "./",
    "outDir": "./ts-build",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

验证与总结

优先选择方案1,拆分基类是最彻底的解决方式,既消除循环引用,又优化了代码结构。方案2适合快速临时修复,但会引入异步逻辑复杂度。方案3配合方案1使用,能进一步提升模块加载的顺畅性。

修改完成后重新编译运行Electron应用,两个错误将被消除,TextElement可正常继承ProjectElement并工作。

内容的提问来源于stack exchange,提问作者TechStudent10

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 04:21:49