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

在NiceGUI中集成CodeMirror 6遇模块解析及404错误求助

问题描述

尝试按照NiceGUI文档的Node模块集成示例,将CodeMirror 6(含merge插件和lang-markdown)集成到NiceGUI应用中,但页面无法渲染,浏览器控制台报错:
Uncaught TypeError: Failed to resolve module specifier "codemirror". Relative references must start with either "/", "./", or "../".

环境信息:

  • NiceGUI v2.0.0
  • Python 3.12.7
  • Node v22.13.1
  • npm v11.0.0

目录结构

nicegui-codemirror-minimal/
├── dist/
│   └── codemirror-bundle.js
├── node_modules/
│   └── ...
├── codemirror_editor.js
├── codemirror_editor.py
├── main.py
├── package.json
├── package-lock.json
└── webpack.config.js

相关文件内容

package.json

{
  "name": "nicegui-codemirror-minimal-test",
  "version": "1.0.0",
  "description": "",
  "main": "codemirror_editor.js",
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@codemirror/commands": "^6.4.0",
    "@codemirror/lang-markdown": "^6.2.4",
    "@codemirror/merge": "^6.0.1",
    "@codemirror/state": "^6.4.1",
    "@codemirror/view": "^6.25.4",
    "codemirror": "^6.0.1"
  },
  "devDependencies": {
    "@babel/core": "^7.24.5",
    "@babel/preset-env": "^7.24.5",
    "babel-loader": "^9.1.3",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4",
    "vue": "^3.4.21"
  }
}

webpack.config.js

const path = require("path");

module.exports = {
  entry: "./codemirror_editor.js",
  mode: "development",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "codemirror-bundle.js",
    library: "CodeMirrorEditor",
    libraryTarget: "window",
    globalObject: 'this',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
    ],
  },
};

codemirror_editor.js

import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { markdown } from "@codemirror/lang-markdown";
import { MergeView } from "@codemirror/merge";
import { keymap } from "@codemirror/view";
import { defaultKeymap } from "@codemirror/commands";
import { nextTick } from 'vue';

export default {
  template: `
    <div style="display: flex; gap: 1em;"><div ref="editorA" style="flex: 1; border: 1px solid #ddd;"></div>
      <div ref="editorB" style="flex: 1; border: 1px solid #ddd;"></div>
    </div>
  `,
  mounted() {
    nextTick(() => {
      setTimeout(() => {
        const initialValueA = this.value_a || "";
        const initialValueB = this.value_b || "";

        this.mergeView = new MergeView({
          a: {
            doc: initialValueA,
            extensions: [
              basicSetup,
              markdown(),
              EditorView.editable.of(true),
              keymap.of(defaultKeymap),
              EditorView.updateListener.of((update) => {
                if (update.docChanged) {
                  this.sendUpdate('a', update.state.doc.toString());
                }
              })
            ],
          },
          b: {
            doc: initialValueB,
            extensions: [
              basicSetup,
              markdown(),
              EditorView.editable.of(true),
              keymap.of(defaultKeymap),
              EditorView.updateListener.of((update) => {
                if (update.docChanged) {
                  this.sendUpdate('b', update.state.doc.toString());
                }
              }),
            ],
          },
          parent: this.$refs.editorA.parentElement,
          root: document,
        });
      }, 0);
    });
  },
  methods: {
    sendUpdate(side, value) {
      this.$emit('update-value', { side, value });
    },
    setValue(side, value) {
      const editor = side === 'a' ? this.mergeView.a : this.mergeView.b;
      const tr = editor.state.update({
        changes: { from: 0, to: editor.state.doc.length, insert: value }
      });
      editor.dispatch(tr);
    },
  },
  props: {
    value_a: String,
    value_b: String,
  },
  beforeDestroy() {
    this.mergeView.destroy();
  }
};

codemirror_editor.py

from nicegui import ui
from typing import Callable, Optional

class CodeMirrorEditor(ui.element, component='codemirror_editor.js', dependencies=['dist/codemirror-bundle.js']):

    def __init__(self,
                 value_a: str = "",
                 value_b: str = "",
                 on_change_a: Optional[Callable] = None,
                 on_change_b: Optional[Callable] = None
                 ) -> None:
        super().__init__()
        self._props['value_a'] = value_a
        self._props['value_b'] = value_b
        self.on_change_a = on_change_a
        self.on_change_b = on_change_b
        self.on('update-value', self.handle_change)

    def handle_change(self, event):
        side = event.args['side']
        value = event.args['value']
        if side == 'a':
            self._props['value_a'] = value
            if self.on_change_a:
                self.on_change_a(value)
        elif side == 'b':
            self._props['value_b'] = value
            if self.on_change_b:
                self.on_change_b(value)
        self.update()

    def set_value(self, side: str, value: str):
        if side not in ('a', 'b'):
            raise ValueError("side must be 'a' or 'b'")
        self.run_method('setValue', side, value)

    @property
    def value_a(self) -> str:
        return self._props['value_a']

    @value_a.setter
    def value_a(self, value: str) -> None:
        self._props['value_a'] = value
        self.set_value('a', value)
        self.update()

    @property
    def value_b(self) -> str:
        return self._props['value_b']

    @value_b.setter
    def value_b(self, value: str) -> None:
        self._props['value_b'] = value
        self.set_value('b', value)
        self.update()

main.py

from nicegui import ui
from codemirror_editor import CodeMirrorEditor

@ui.page('/')
async def page():  # Must be async
    editor = CodeMirrorEditor(value_a="# Initial A", value_b="Initial B")
    # That's it! No other UI elements or callbacks.

ui.run()

更新内容

调整后解决了模块路径问题,但出现404错误:
GET http://127.0.0.1:8080/dist/merge.js net::ERR_ABORTED 404 (Not Found)

调整后的目录结构

nicegui-codemirror-minimal/
├── dist/
│   ├── merge.js
│   ├── state.js
│   └── view.js
├── node_modules/
│   └── ...
├── codemirror_editor.js
├── codemirror_editor.py
├── main.py
├── package.json
├── package-lock.json
└── webpack.config.js

调整后的相关文件

package.json

{
  "scripts": {
    "build": "webpack --config webpack.config.js"
  },
  "dependencies": {
    "@codemirror/merge": "^6.0.1"
  },
  "devDependencies": {
    "@babel/core": "^7.24.5",
    "@babel/preset-env": "^7.24.5",
    "babel-loader": "^9.1.3",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4"
  }
}

codemirror_editor.js

let mergeViewInstance;

export default async function createCodeMirrorEditor(element, props) {
  const initialValueA = props.value_a || "";
  const initialValueB = props.value_b || "";

  // Dynamically import CodeMirror modules
  const { MergeView } = await import("/dist/merge.js");
  const { EditorView } = await import("/dist/view.js");
  const { EditorState } = await import("/dist/state.js");

  mergeViewInstance = new MergeView({
    a: {
      doc: initialValueA,
      extensions: [
        EditorView.editable.of(true),
        // Removed other extensions for simplicity - you can add them back if needed
        EditorView.updateListener.of((update) => {
          if (update.docChanged) {
            sendUpdate('a', update.state.doc.toString());
          }
        }),
      ],
    },
    b: {
      doc: initialValueB,
      extensions: [
        EditorView.editable.of(true),
        // Removed other extensions for simplicity - you can add them back if needed
        EditorView.updateListener.of((update) => {
          if (update.docChanged) {
            sendUpdate('b', update.state.doc.toString());
          }
        }),
      ],
    },
    parent: element,
    root: document,
  });

  function sendUpdate(side, value) {
    element.dispatchEvent(new CustomEvent('update-value', { detail: { side, value } }));
  }

  function setValue(side, value) {
    const editor = side === 'a' ? mergeViewInstance.a : mergeViewInstance.b;
    const transaction = editor.state.update({
      changes: { from: 0, to: editor.state.doc.length, insert: value }
    });
    editor.dispatch(transaction);
  }

  return {
    setValue: setValue,
    destroy: () => { mergeViewInstance.destroy(); }
  };
}

codemirror_editor.py

from nicegui import ui
from typing import Callable, Optional

class CodeMirrorEditor(ui.element, component='codemirror_editor.js', dependencies=[
    'dist/merge.js',
    'dist/view.js',
    'dist/state.js',
]):
    def __init__(self,
                 value_a: str = "",
                 value_b: str = "",
                 on_change_a: Optional[Callable] = None,
                 on_change_b: Optional[Callable] = None
                 ) -> None:
        super().__init__()
        self._props['value_a'] = value_a
        self._props['value_b'] = value_b
        self.on_change_a = on_change_a
        self.on_change_b = on_change_b
        self.on('update-value', self.handle_change)

    def handle_change(self, event):
        side = event.args['detail']['side']
        value = event.args['detail']['value']
        if side == 'a':
            self._props['value_a'] = value
            if self.on_change_a:
                self.on_change_a(value)
        elif side == 'b':
            self._props['value_b'] = value
            if self.on_change_b:
                self.on_change_b(value)
        self.update()

    def set_value(self, side: str, value: str):
        if side not in ('a', 'b'):
            raise ValueError("side must be 'a' or 'b'")
        self.run_method('setValue', side, value)


    @property
    def value_a(self) -> str:
        return self._props['value_a']

    @value_a.setter
    def value_a(self, value: str) -> None:
        self._props['value_a'] = value
        self.set_value('a', value)
        self.update()

    @property
    def value_b(self) -> str:
        return self._props['value_b']

    @value_b.setter
    def value_b(self, value: str) -> None:
        self.props['value_b'] = value
        self.set_value('b', value)
        self.update()

webpack.config.js

const path = require("path");

module.exports = {
  entry: {
    merge: "@codemirror/merge",
    view: "@codemirror/view",
    state: "@codemirror/state",
  },
  mode: "development",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
    library: "CodeMirrorMerge",
    libraryTarget: "umd",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
    ],
  },
};

需分析错误原因并提供解决方案,可从webpack配置、模块导入方式等方面给出建议。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 00:19:51