您好,登錄后才能下訂單哦!
這篇文章主要講解了“js制作xml在線編輯器代碼分享”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“js制作xml在線編輯器代碼分享”吧!
前言
在線XML編輯器的需求
技術預研
可視化編程
VSCODE插件
在線編輯器
最初形態:簡單的在線XML編輯器
用CodeMirror做編輯器
學習XML,并提取出tags規則
進化形態:加載樹形文件結構和全文件校驗功能的在線XML編輯器
左側文件樹
全文件校驗功能
非遞歸遍歷樹
IndexDB保存文件內容
究極進化形態:突破瀏覽器沙盒限制,實現對電腦本地文件的增刪改
更多的功能與細節
不足與總結
這里主要還是談一下技術相關的,也就是一個純前端實現,用于寫MOD的XML在線編輯器。
它是一個仿VSCode風格的編輯器,可以自動學習游戲MOD文件生成約束規則,幫助我們實現代碼提示和代碼校驗。
更重要的是它可以直接修改你電腦上的的文件。
這是最終成品的代碼倉庫:https://gitee.com/vvjiang/mod-xml-editor
以及一張成品展示圖:
本篇博客所涉及到的技術:
CodeMirror
react-codemirror2
xmldom
FileReader
IndexDB
Web Worker
File System Access
讓我們從頭開始講起。
在做《騎砍2》的MOD時,需要經常寫XML文件。
因為騎砍2的數據配置就是以XML的形式保存,然后MOD加載后,用MOD的XML去覆蓋官方自己的XML。
通常我們做MOD數據這塊,就是參考官方的XML自己去寫XML文件。
但是這樣會遇到一個問題,XML這東西沒有代碼提示和代碼校驗,寫錯一個字符也很難發現。
又或者有時候游戲更新,它的XML規則可能會改動。
官方是不會發布通知告訴你這些改動點的,所以如果你還是用的以前的元素和屬性那就等于寫錯了。
寫錯的結果往往是游戲加載MOD時直接崩潰,也不會給你任何提示,你只能慢慢去尋找BUG。
而騎砍2作為一個大型游戲,每次啟動時間都很長,導致你測試一個MOD數據是否配置正確的測試流程會非常長。
媽耶,多少個夜晚,游戲崩潰的那一瞬間,我人就崩潰了。
所以后來我就想著做一個XML在線編輯器去解決這個問題。
其實我一開始沒有做這個XML編輯器的想法,因為這玩意一看就難搞,而是想通過一個可視化編程,通過拖拉拽元素和屬性的方式去實現。
你別說,我還真的做了一套初步方案出來,結果配置一個大型的XML這玩意拖拉拽無數次,心態逐漸爆炸,遂放棄此方案。
想看看有沒有什么VSCode插件可以進行代碼提示,有一個使用XSD進行代碼校驗的,貌似還是IBM提供的。
但是很可惜已經廢棄,然后用不了了,放棄此方案。
后來之所以使用在線編輯器的方式做這個,是因為三四月份公司這邊想要做一個在線編輯java項目環境xml配置文件的一個東西。
然后我這邊就嘗試著做了一個,了解到了CodeMirror。
CodeMirror通過自己配置tags來支持xml的代碼提示,但是并不支持xml的代碼校驗,所以需要自己去做xml的代碼校驗。
并且因為通常我們去校驗xml用的是xsd,所以還需要將xsd轉換成CodeMirror的tags配置。
這個不論是百度Google,還是說Github,都是查不到相對應的方案,所以只能自己寫代碼去實現。
在這個過程中,我對CodeMirror,xsd,htmllint都有了比較深的一個了解,最終完成了項目。
因為這是之前公司的代碼,所以這里就不放出來了。
總之,在這個過程中了解到CodeMirror這么個東西,才有了用CodeMirror去做MOD的在線編輯器的想法。
好了,廢話不說,拿起鍵盤就是無腦干。
最初形態沒有左側的文件樹,只有一個單純的編輯器和一個規則學習彈框。
涉及到的技術就三個:
CodeMirror
FileReader
xmldom
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 } = props useEffect(() => { // 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
當我們使用CodeMirror做一個簡單的編輯器時,想要進行一個XML的代碼提示,是需要使用到tags。
很明顯,不同的游戲有不同的XML規則,包括游戲更新之后XML規則也會更改。
所以我們必須要保證有一個機制去不斷地學習這些XML規則,所以這里我做了一個學習XML文件規則的彈窗去做這個事情。
點擊編輯器左上方的 約束規則——>新增約束規則
會彈出這樣一個彈窗:
通過FileReader讀取指定文件夾的XML文件,然后使用xmldom來依次解析這些xml文件的文本,生成文檔對象。
再分析這些文檔對象得到最終的tags規則。
這一步驟只需要對xml有所了解,其實也蠻基礎的,所以不講了。
總之現在我們完成了它的最初形態,你每次使用它需要將你編輯的XML文件內容復制到這個在線編輯器,編輯完后,再將完成的文本復制到原XML文件保存覆蓋。
上面的編輯器其實使用場景非常窄,只能在新寫一個XML時使用。
一個MOD往往幾十上百,甚至幾千個文件,不可能一個個粘貼到編輯器中進行校驗。
所以我們需要在這個編輯器中,加載MOD的所有XML文件,并進行一個代碼校驗。
涉及到的技術就兩個:
FileReader
Web Worker
左側這個文件樹使用Ant Design的Tree組件完成,這里配置什么的就不講了。
在點擊打開文件夾這個按鈕時
同樣使用FileReader來讀取MOD文件夾中的文件。
但是FileReader獲取到的是一個文件數組,要想生成我們左側的樹形結構需要自己手動解析每個XML文件的路徑,并據此生成一個樹形結構。
在打開文件夾的一瞬間,我們需要對全部的XML文件進行一次代碼校驗,如果校驗有誤,需要在左側文件夾上將相關的文件及它父級祖級的一系列文件夾全部標紅。
這個功能表面上很簡單,其實坑點很大,因為校驗的計算量實際上并不小,特別是你的MOD中有幾百幾千個文件的時候,非常容易搞得你js阻塞,頁面無響應。
在這里我使用了Web Worker新開一個線程去處理這個校驗過程,在校驗完成后將結果返回給我。
在這個過程中,我對Web Worker的使用也有了更多的了解。
印象中一直以為是一個new Worker(某js文件)這樣的方式去玩,感覺很難結合react的模塊化開發來使用。
但是實際上現在在webpack里配置上worker-loader,可以很方便使用Web Worker。
首先我們的worker代碼可以寫成下面這樣:
import { lintFileTree } from '@/utils/files' onmessage = ({ data }) => { lintFileTree(data.fileTree, data.currentTags).then(content => { postMessage(content) }) }
然后我們使用這個Worker時,可以如下所示
import { useWebWorkerFromWorker } from 'react-webworker-hook' import lintFileTreeWorker from '@/utils/webWorker/lintFileTree.webworker' const worker4LintFileTree = new lintFileTreeWorker() const [lintedFileTree, startLintFileTree] = useWebWorkerFromWorker(worker4LintFileTree)
然后你再用個useEffect依賴這個lintedFileTree,如果變動了就做某些操作,所以寫起來就像用useState一樣輕松。
大家可以看到上面我們用到的這些東西,很多都與樹相關,比如遍歷文件樹去校驗代碼。
又或者我們切換了某個約束規則后,也是需要遍歷整個文件樹進行重新校驗的。
遍歷的過程中,之前我用的是遞歸遍歷整個樹,這樣做不好的地方在于遞歸的時候內存得不到釋放,所以后來我換了一種算法,采用非遞歸的方式遍歷整個樹。
因為我們的MOD文件內容比較多比較大,所以內存占用可能會很大,不可能一直把這些文件內容放到內存中。
所以我讀取到文件內容會依次放入IndexDB中,只展示當前編輯文件的內容。
只有在需要的時候,比如全文件校驗或者切換文件時,才從IndexDB再次獲取文件內容。
通過之前的操作,我們終于完成了一個基本可用的在線XML編輯器。
但是它有一個致命缺點,就是受到瀏覽器沙盒環境的限制,我們在修改了文件后,沒法直接保存到電腦上,而必須依靠手動將修改好的代碼一一復制到對應的文件中。
這個操作繁瑣復雜,導致我們編輯器的功能可能只能用來輔助編寫代碼和批量校驗。
之前我以為只能做到這種程度,但是后來我在知乎上偶然看了一個帖子,發現Chrome86+的版本多了一個功能API:FileSystemAccess。
另外,除非是本地localhost環境,否則這個API只在https環境下才能調用,也就是說你在一個http的網站上,即使你用的是Chrome86+或者是Edge86+,那也是調用不了的。
這個API可以讓我們直接操作本地電腦上的文件,而不是像FileReader一樣只能讀,或者像FileSystem一樣只能在瀏覽器沙盒內操作。
通過FileSystemAccess我們不僅可以實現對文件夾中的文件進行讀取修改,還能新增和刪除文件。
所以我使用這個API全面替換了之前使用FileReader的各個點,實現了在文件樹上右鍵進行文件夾和文件的新增和刪除。(這里是不支持對文件進行重命名的,不過其實我們可以使用刪除后再新增的方式來模擬重命名,但是我就懶得做了)
同時在按保存按鈕或者按保存的快捷鍵Ctrl+S后,就可以直接對文件進行保存操作。
下面是一個使用FileSystemAccess打開文件夾的組件代碼:
import React from 'react' // 自定義的打開文件夾組件 const FileInput = (props) => { const { children, onChange } = props const handleClick = async () => { const dirHandle = await window.showDirectoryPicker() dirHandle.requestPermission({ mode : "readwrite" }) onChange(dirHandle) } return <span onClick={handleClick}> {children} </span> } export default FileInput
只要被這個組件包裹的元素(比如按鈕)被點擊后,會立即調用showDirectoryPicker,請求打開文件夾。
在打開文件夾后,通過獲得的文件夾handle去請求文件夾寫入權限,然后再把這個文件夾handle傳到外部,獲取文件樹結構。
這里的操作是有瑕疵的,因為請求打開文件夾時瀏覽器會彈框向用戶獲取讀取文件夾的權限,
打開完畢后又直接會彈第二次框獲取寫入權限,也就是說在打開文件夾時會彈兩次框。
但是我也只能通過這種手法一次性請求到所有的權限,要不然等到要保存時再去請求權限也不太好。
不過瑕不掩瑜,通過這個API不僅實現了文件的增刪改,還解除了對IndexDB的使用。
因為我們隨時可以通過文件Handle獲取到相應的文件內容,所以沒必要將文件內容保存到IndexDB中。
感謝各位的閱讀,以上就是“js制作xml在線編輯器代碼分享”的內容了,經過本文的學習后,相信大家對js制作xml在線編輯器代碼分享這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。