纯js制作的XML在线编辑器(⽀持修改本地⽂件)
前⾔
⼀年多没更新博客了,原因是疫情期间《骑马与砍杀2》发售,然后去写游戏MOD去了。
⽤C#⼤概写了7个⽉的游戏MOD,每天晚上肝到很晚,然后期间⼜因为介绍这个游戏MOD,学习了PR,然后做起了B站的UP主。
再到后⾯有了些别的想法和公司业务调整,也懒得写博客,不知不觉⼀年多也就过去了。
“我⾃⼰是⼀名从事了6年web前端开发的⽼程序员,今年年初我花了⼀个⽉整理了⼀份最适合2021年⾃学的web前端全套培训教程(视频+源码+笔记+项⽬实战),从最基础的HTML+CSS+JS到移动端HTML5以及各种框架和新技术都有整理,打包给每⼀位前端⼩伙伴,这⾥是前端学习者聚集地,欢迎初学和进阶中的⼩伙伴(所有前端教程关注我的微信公众号:web前端学习圈,关注后回复“web”即可领取
收获还是有的:
⽐如在断更这个MOD时,不论是在中⽂站还是3DM的MOD站,这个MOD的下载量都是排第⼀的,⽽且甩第⼆名相当远。如果有玩《骑砍2》MOD的朋友,应该猜出来我是谁了。
⼜⽐如在B站收获了五千多粉丝,从⼀开始说话结结巴巴,到最后也还是说得结结巴巴。不过因为⾃⼰的剪辑,观看效果也还不错。
⼜⽐如深刻认识到做个UP和主播有多⿇烦,就我这拉胯的数据其实已经领先了B站很多UP主了。UP主中更多的不是头部UP,⽽是视频0播放的UP主。你可以看⼀下B站的最新视频,翻了⼏⼗页全是0播放,极为壮观。
有趣的⼈⽣体验增加了
好了,⾔归正传。
kinda是什么意思现在基本MOD断更,UP主也懒得继续认真做了。
这⾥主要还是谈⼀下技术相关的,也就是⼀个纯前端实现,⽤于写MOD的XML在线编辑器。
它是⼀个仿VSCode风格的编辑器,可以⾃动学习游戏MOD⽂件⽣成约束规则,帮助我们实现代码提⽰和代码校验。
更重要的是它可以直接修改你电脑上的的⽂件。
以及⼀张成品展⽰图:
本篇博客所涉及到的技术:
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,都是查不到相对应的⽅案,所以只能⾃⼰写代码去实现。
在这个过程中,我对CodeMirror,xsd,htmllint都有了⽐较深的⼀个了解,最终完成了项⽬。
因为这是之前公司的代码,所以这⾥就不放出来了。gettogether
总之,在这个过程中了解到CodeMirror这么个东西,才有了⽤CodeMirror去做MOD的在线编辑器的想法。
最初形态:简单的在线XML编辑器
好了,废话不说,拿起键盘就是⽆脑⼲。
最初形态没有左侧的⽂件树,只有⼀个单纯的编辑器和⼀个规则学习弹框。
涉及到的技术就三个:
CodeMirror
FileReader
xmldom
⽤CodeMirror做编辑器
CodeMirror这块主要使⽤的react的⼀个封装版react-codemirror2,反正就是看⽂档和Demo⾃⼰配。
唯⼀的难度就是⽹上⼀⼤堆的CodeMirror配置介绍很多都是抄来抄去,转载来转载去,还是个错的,简直离谱。
我这⾥贴⼀段我封装的编辑器组件的配置代码吧,反正绝对可⽤,绝⼤多数编辑器的功能都OK,不过仅仅适⽤于编辑XML。
⾥⾯的注释⽐较详尽了,包括常⽤的代码折叠,代码格式化都有,我就懒得⼀⼀讲了,你可以参考官⽹⾃⼰看看。
其中的⼀些引⽤代码我就不贴了,有兴趣的可以去上⾯提到的代码仓库看看。
import { uEffect } 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/lection/active-line'
// 折叠代码
import 'codemirror/addon/fold/foldgutter.css'
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'
// 代码校验
bailoutimport 'codemirror/addon/lint/lint'
import 'codemirror/addon/lint/lint.css'
import CodeMirrorRegisterXmlLint from './xml-lint'
// 输⼊> 时⾃动键⼊结束标签
import 'codemirror/addon/edit/clotag.js'
/
/ 注释
import 'codemirror/addon/comment/comment.js'
// ⽤于调整codeMirror的主题样式
import style from './index.less'
// 注册Xml代码校验
CodeMirrorRegisterXmlLint(CodeMirror)
// 格式化相关
commentStart: "<!--",
commentEnd: "-->",
newlineAfterToken: function (type, content, textAfter, state) {
return (type === "tag" && />$/.test(content) && t) ||
/^</.test(textAfter);
}
});
// 格式化指定范围
uc是什么CodeMirror.defineExtension("autoFormatRange", function (from, to) {
var cm = this;
var outer = cm.getMode(), text = cm.getRange(from, to).split("\n");
var state = pyState(outer, cm.getTokenAt(from).state);
var tabSize = cm.getOption("tabSize");
var out = "", lines = 0, atSol = from.ch === 0;
function newline() {
urname是什么意思out += "\n";
atSol = true;
++lines;
}
for (var i = 0; i < text.length; ++i) {
var stream = new CodeMirror.StringStream(text[i], tabSize);
while (!l()) {
var inner = CodeMirror.innerMode(outer, state);
var style = ken(stream, state), cur = stream.current();
stream.start = stream.pos;
if (!atSol || /\S/.test(cur)) {
击鼓声out += cur;
atSol = fal;
}
if (!atSol && wlineAfterToken &&
}
if (!stream.pos && outer.blankLine) outer.blankLine(state);
if (!atSol && i < text.length - 1) newline();
}
}
cm.operation(function () {
for (var cur = from.line + 1, end = from.line + lines; cur <= end; ++cur)
cm.indentLine(cur, "smart");
cm.tSelection(from, cm.getCursor(fal));
});
});
// Xml编辑器组件
function XmlEditor(props) {
const { tags, value, onChange, onErrors, onGetEditor, onSave } = props
uEffect(() => {
/
/ tags 每次变动时,都会重新改变校验规则
CodeMirrorRegisterXmlLint(CodeMirror, tags, onErrors)
}, [onErrors, tags])
// 开始标签
function completeAfter(cm, pred) {
if (!pred || pred()) tTimeout(function () {
if (!pletionActive)
翻译官cm.showHint({
completeSingle: fal
});
}, 100);
return CodeMirror.Pass;
}
// 结束标签
function completeIfAfterLt(cm) {
return completeAfter(cm, function () {
var cur = cm.getCursor();
Range(CodeMirror.Pos(cur.line, cur.ch - 1), cur) === "<";
});
}
// 属性和属性值
function completeIfInTag(cm) {
return completeAfter(cm, function () {
var tok = cm.Cursor());
fgfgif (pe === "string" && (!/['"]/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length === 1)) return fal; var inner = CodeMirror.Mode(), tok.state).state;
return inner.tagName;
});
}
return (
<div className={style.editor} >
<ControlledCodeMirror
value={value}
options={{
mode: {
name: 'xml',
// xml 属性换⾏的时候是否加上标签的长度
multilineTagIndentPastTag: fal
},
indentUnit: 2, // 换⾏的默认缩进多少个空格
theme: 'ayu-dark', // 编辑器主题
lineNumbers: true,// 是否显⽰⾏号
autofocus: true,// ⾃动获取焦点
styleActiveLine: true,// 光标⾏代码⾼亮
autoCloTags: true, // 在输⼊>时⾃动键⼊结束元素
toggleComment: true, // 开启注释
// 折叠代码 begin