Electron中TypeScript类继承报错及循环引用问题求助
问题解决:Electron中类继承与循环引用错误处理
问题背景
编写了继承ProjectElement的TextElement类,编辑器IntelliSense无报错,但在Electron应用中先后出现两个错误:
- 初始使用CommonJS模块时,报错
Class extends value undefined is not a constructor or null - 将
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 } }
问题分析
- 第一个错误原因:
TextElement未正确导入ProjectElement类,编辑器靠类型推断不报错,但运行时ProjectElement为undefined;同时CommonJS模块的加载顺序导致TextElement加载时,project.tsx还未完成类的导出。 - 第二个错误原因:循环引用链导致非CommonJS模块加载时,出现先访问未初始化类的情况,触发"Cannot access before initialization"错误。
解决方案
方案1:拆分核心基类(彻底解决循环引用)
将ProjectElement、ElementProperty、ElementProperties及相关接口单独抽离到新文件,切断循环引用链:
- 创建
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 } } }
- 修改
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 }
- 修改
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
相关产品推荐
相关产品推荐

