您当前的位置:首页 > 网站建设 > javascript
| php | asp | css | H5 | javascript | Mysql | Dreamweaver | Delphi | 网站维护 | 帝国cms | React | 考试系统 | ajax |

js制作xml在线编辑器实例

51自学网 2022-05-02 21:33:36
  javascript

前言

一年多没更新博客了,原因是疫情期间《骑马与砍杀2》发售,然后去写游戏MOD去了。

用C#大概写了7个月的游戏MOD,每天晚上肝到很晚,然后期间又因为介绍这个游戏MOD,学习了PR,然后做起了B站的UP主。

再到后面有了些别的想法和公司业务调整,也懒得写博客,不知不觉一年多也就过去了。

收获还是有的:

  • 比如在断更这个MOD时,不论是在中文站还是3DM的MOD站,这个MOD的下载量都是排第一的,而且甩第二名相当远。如果有玩《骑砍2》MOD的朋友,应该猜出来我是谁了。
  • 又比如在B站收获了五千多粉丝,从一开始说话结结巴巴,到最后也还是说得结结巴巴。不过因为自己的剪辑,观看效果也还不错。
  • 又比如深刻认识到做个UP和主播有多麻烦,就我这拉胯的数据其实已经领先了B站很多UP主了。UP主中更多的不是头部UP,而是视频0播放的UP主。你可以看一下B站的最新视频,翻了几十页全是0播放,极为壮观。
  • 有趣的人生体验增加了

好了,言归正传。

现在基本MOD断更,UP主也懒得继续认真做了。

这里主要还是谈一下技术相关的,也就是一个纯前端实现,用于写MOD的XML在线编辑器。

它是一个仿VSCode风格的编辑器,可以自动学习游戏MOD文件生成约束规则,帮助我们实现代码提示和代码校验。

更重要的是它可以直接修改你电脑上的的文件。

这是最终成品的代码仓库:https://gitee.com/vvjiang/mod-xml-editor

以及一张成品展示图:

本篇博客所涉及到的技术:

  • CodeMirror
  • react-codemirror2
  • xmldom
  • FileReader
  • IndexDB
  • Web Worker
  • File System Access

让我们从头开始讲起。

在线XML编辑器的需求

在做《骑砍2》的MOD时,需要经常写XML文件。

因为骑砍2的数据配置就是以XML的形式保存,然后MOD加载后,用MOD的XML去覆盖官方自己的XML。

通常我们做MOD数据这块,就是参考官方的XML自己去写XML文件。

但是这样会遇到一个问题,XML这东西没有代码提示和代码校验,写错一个字符也很难发现。

又或者有时候游戏更新,它的XML规则可能会改动。

官方是不会发布通知告诉你这些改动点的,所以如果你还是用的以前的元素和属性那就等于写错了。

写错的结果往往是游戏加载MOD时直接崩溃,也不会给你任何提示,你只能慢慢去寻找BUG。

而骑砍2作为一个大型游戏,每次启动时间都很长,导致你测试一个MOD数据是否配置正确的测试流程会非常长。

妈耶,多少个夜晚,游戏崩溃的那一瞬间,我人就崩溃了。

所以后来我就想着做一个XML在线编辑器去解决这个问题。

技术预研

可视化编程

其实我一开始没有做这个XML编辑器的想法,因为这玩意一看就难搞,而是想通过一个可视化编程,通过拖拉拽元素和属性的方式去实现。

你别说,我还真的做了一套初步方案出来,结果配置一个大型的XML这玩意拖拉拽无数次,心态逐渐爆炸,遂放弃此方案。

VSCODE插件

想看看有没有什么VSCode插件可以进行代码提示,有一个使用XSD进行代码校验的,貌似还是IBM提供的。

但是很可惜已经废弃,然后用不了了,放弃此方案。

在线编辑器

后来之所以使用在线编辑器的方式做这个,是因为三四月份公司这边想要做一个在线编辑java项目环境xml配置文件的一个东西。

然后我这边就尝试着做了一个,了解到了CodeMirror

CodeMirror通过自己配置tags来支持xml的代码提示,但是并不支持xml的代码校验,所以需要自己去做xml的代码校验。

并且因为通常我们去校验xml用的是xsd,所以还需要将xsd转换成CodeMirror的tags配置。

这个不论是百度Google,还是说Github,都是查不到相对应的方案,所以只能自己写代码去实现。

在这个过程中,我对CodeMirrorxsdhtmllint都有了比较深的一个了解,最终完成了项目。

因为这是之前公司的代码,所以这里就不放出来了。

总之,在这个过程中了解到CodeMirror这么个东西,才有了用CodeMirror去做MOD的在线编辑器的想法。

最初形态:简单的在线XML编辑器

好了,废话不说,拿起键盘就是无脑干。

最初形态没有左侧的文件树,只有一个单纯的编辑器和一个规则学习弹框。

涉及到的技术就三个:

CodeMirror

FileReader

xmldom

用CodeMirror做编辑器

CodeMirror这块主要使用的react的一个封装版react-codemirror2,反正就是看文档和Demo自己配。

唯一的难度就是网上一大堆的CodeMirror配置介绍很多都是抄来抄去,转载来转载去,还是个错的,简直离谱。

总之你想玩的话最好还是看官方文档(https://codemirror.net/) 和文档上的Demo,然后自己研究下,抄别人配置的话水很深,你把握不住的。

我这里贴一段我封装的编辑器组件的配置代码吧,反正绝对可用,绝大多数编辑器的功能都OK,不过仅仅适用于编辑XML。

里面的注释比较详尽了,包括常用的代码折叠,代码格式化都有,我就懒得一一讲了,你可以参考官网自己看看。

其中的一些引用代码我就不贴了,有兴趣的可以去上面提到的代码仓库看看。

import { useEffect } from 'react'import { Controlled as ControlledCodeMirror } from 'react-codemirror2'import CodeMirror from 'codemirror'import 'codemirror/lib/codemirror.css'import 'codemirror/theme/ayu-dark.css'import 'codemirror/mode/xml/xml.js'// 光标行代码高亮import 'codemirror/addon/selection/active-line'// 折叠代码import 'codemirror/addon/fold/foldgutter.css'import 'codemirror/addon/fold/foldcode.js'import 'codemirror/addon/fold/xml-fold.js'import 'codemirror/addon/fold/foldgutter.js'import 'codemirror/addon/fold/comment-fold.js'// 代码提示补全和import 'codemirror/addon/hint/xml-hint.js'import 'codemirror/addon/hint/show-hint.css'import './hint.css'import 'codemirror/addon/hint/show-hint.js'// 代码校验import 'codemirror/addon/lint/lint'import 'codemirror/addon/lint/lint.css'import CodeMirrorRegisterXmlLint from './xml-lint'// 输入> 时自动键入结束标签import 'codemirror/addon/edit/closetag.js'// 注释import 'codemirror/addon/comment/comment.js'// 用于调整codeMirror的主题样式import style from './index.less'// 注册Xml代码校验CodeMirrorRegisterXmlLint(CodeMirror)// 格式化相关CodeMirror.extendMode("xml", {commentStart: "<!--",commentEnd: "-->",newlineAfterToken: function (type, content, textAfter, state) {    return (type === "tag" && />$/.test(content) && state.context) ||    /^</.test(textAfter);}});// 格式化指定范围CodeMirror.defineExtension("autoFormatRange", function (from, to) {var cm = this;var outer = cm.getMode(), text = cm.getRange(from, to).split("/n");var state = CodeMirror.copyState(outer, cm.getTokenAt(from).state);var tabSize = cm.getOption("tabSize");var out = "", lines = 0, atSol = from.ch === 0;function newline() {    out += "/n";    atSol = true;    ++lines;}for (var i = 0; i < text.length; ++i) {    var stream = new CodeMirror.StringStream(text[i], tabSize);    while (!stream.eol()) {    var inner = CodeMirror.innerMode(outer, state);    var style = outer.token(stream, state), cur = stream.current();    stream.start = stream.pos;    if (!atSol || //S/.test(cur)) {        out += cur;        atSol = false;    }    if (!atSol && inner.mode.newlineAfterToken &&        inner.mode.newlineAfterToken(style, cur, stream.string.slice(stream.pos) || text[i + 1] || "", inner.state))        newline();    }    if (!stream.pos && outer.blankLine) outer.blankLine(state);    if (!atSol && i < text.length - 1) newline();}cm.operation(function () {    cm.replaceRange(out, from, to);    for (var cur = from.line + 1, end = from.line + lines; cur <= end; ++cur)    cm.indentLine(cur, "smart");    cm.setSelection(from, cm.getCursor(false));});});// Xml编辑器组件function XmlEditor(props) {const { tags, value, onChange, onErrors, onGetEditor, onSave } = propsuseEffect(() => {    // tags 每次变动时,都会重新改变校验规则    CodeMirrorRegisterXmlLint(CodeMirror, tags, onErrors)}, [onErrors, tags])// 开始标签function completeAfter(cm, pred) {    if (!pred || pred()) setTimeout(function () {    if (!cm.state.completionActive)        cm.showHint({        completeSingle: false        });    }, 100);    return CodeMirror.Pass;}// 结束标签function completeIfAfterLt(cm) {    return completeAfter(cm, function () {    var cur = cm.getCursor();    return cm.getRange(CodeMirror.Pos(cur.line, cur.ch - 1), cur) === "<";    });}// 属性和属性值function completeIfInTag(cm) {    return completeAfter(cm, function () {    var tok = cm.getTokenAt(cm.getCursor());    if (tok.type === "string" && (!/['"]/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length === 1)) return false;    var inner = CodeMirror.innerMode(cm.getMode(), tok.state).state;    return inner.tagName;    });}return (    <div className={style.editor} >    <ControlledCodeMirror        value={value}        options={{        mode: {            name: 'xml',            // xml 属性换行的时候是否加上标签的长度            multilineTagIndentPastTag: false        },        indentUnit: 2, // 换行的默认缩进多少个空格        theme: 'ayu-dark', // 编辑器主题        lineNumbers: true,// 是否显示行号        autofocus: true,// 自动获取焦点        styleActiveLine: true,// 光标行代码高亮        autoCloseTags: true, // 在输入>时自动键入结束元素        toggleComment: true, // 开启注释        // 折叠代码 begin        lineWrapping: true,        foldGutter: true,        gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],        // 折叠代码 end        extraKeys: {            // 代码提示            "'<'": completeAfter,            "'/'": completeIfAfterLt,            "' '": completeIfInTag,            "'='": completeIfInTag,            // 注释功能            "Ctrl-/": (cm) => {            cm.toggleComment()            },            // 保存功能            "Ctrl-S": (cm) => {            onSave()            },            // 格式化            "Shift-Alt-F": (cm) => {            const totalLines = cm.lineCount();            cm.autoFormatRange({ line: 0, ch: 0 }, { line: totalLines })            },            // Tab自动转换为空格            "Tab": (cm) => {            if (cm.somethingSelected()) {// 选中后整体缩进的情况                cm.indentSelection('add')            } else {                cm.replaceSelection(Array(cm.getOption("indentUnit") + 1).join(" "), "end", "+input")            }            }        },        // 代码提示        hintOptions: { schemaInfo: tags, matchInMiddle: true },        lint: true        }}        editorDidMount={onGetEditor}        onBeforeChange={onChange}    />    </div>)}export default XmlEditor

学习XML,并提取出tags规则

当我们使用CodeMirror做一个简单的编辑器时,想要进行一个XML的代码提示,是需要使用到tags。

很明显,不同的游戏有不同的XML规则,包括游戏更新之后XML规则也会更改。

所以我们必须要保证有一个机制去不断地学习这些XML规则,所以这里我做了一个学习XML文件规则的弹窗去做这个事情。

点击编辑器左上方的 约束规则
13个JavaScript 一行程序,让你看起来就是个专家
JavaScript简写技巧

51自学网,即我要自学网,自学EXCEL、自学PS、自学CAD、自学C语言、自学css3实例,是一个通过网络自主学习工作技能的自学平台,网友喜欢的软件自学网站。
京ICP备13026421号-1