js制作xml在線編輯器實(shí)例
前言
一年多沒更新博客了,原因是疫情期間《騎馬與砍殺2》發(fā)售,然后去寫游戲MOD去了。
用C#大概寫了7個(gè)月的游戲MOD,每天晚上肝到很晚,然后期間又因?yàn)榻榻B這個(gè)游戲MOD,學(xué)習(xí)了PR,然后做起了B站的UP主。
再到后面有了些別的想法和公司業(yè)務(wù)調(diào)整,也懶得寫博客,不知不覺一年多也就過去了。
收獲還是有的:
- 比如在斷更這個(gè)MOD時(shí),不論是在中文站還是3DM的MOD站,這個(gè)MOD的下載量都是排第一的,而且甩第二名相當(dāng)遠(yuǎn)。如果有玩《騎砍2》MOD的朋友,應(yīng)該猜出來我是誰了。
- 又比如在B站收獲了五千多粉絲,從一開始說話結(jié)結(jié)巴巴,到最后也還是說得結(jié)結(jié)巴巴。不過因?yàn)樽约旱募糨?,觀看效果也還不錯(cuò)。
- 又比如深刻認(rèn)識到做個(gè)UP和主播有多麻煩,就我這拉胯的數(shù)據(jù)其實(shí)已經(jīng)領(lǐng)先了B站很多UP主了。UP主中更多的不是頭部UP,而是視頻0播放的UP主。你可以看一下B站的最新視頻,翻了幾十頁全是0播放,極為壯觀。
- 有趣的人生體驗(yàn)增加了
好了,言歸正傳。
現(xiàn)在基本MOD斷更,UP主也懶得繼續(xù)認(rèn)真做了。
這里主要還是談一下技術(shù)相關(guān)的,也就是一個(gè)純前端實(shí)現(xiàn),用于寫MOD的XML在線編輯器。
它是一個(gè)仿VSCode風(fēng)格的編輯器,可以自動(dòng)學(xué)習(xí)游戲MOD文件生成約束規(guī)則,幫助我們實(shí)現(xiàn)代碼提示和代碼校驗(yàn)。
更重要的是它可以直接修改你電腦上的的文件。
這是最終成品的代碼倉庫:https://gitee.com/vvjiang/mod-xml-editor
以及一張成品展示圖:
本篇博客所涉及到的技術(shù):
- CodeMirror
- react-codemirror2
- xmldom
- FileReader
- IndexDB
- Web Worker
- File System Access
讓我們從頭開始講起。
在線XML編輯器的需求
在做《騎砍2》的MOD時(shí),需要經(jīng)常寫XML文件。
因?yàn)轵T砍2的數(shù)據(jù)配置就是以XML的形式保存,然后MOD加載后,用MOD的XML去覆蓋官方自己的XML。
通常我們做MOD數(shù)據(jù)這塊,就是參考官方的XML自己去寫XML文件。
但是這樣會遇到一個(gè)問題,XML這東西沒有代碼提示和代碼校驗(yàn),寫錯(cuò)一個(gè)字符也很難發(fā)現(xiàn)。
又或者有時(shí)候游戲更新,它的XML規(guī)則可能會改動(dòng)。
官方是不會發(fā)布通知告訴你這些改動(dòng)點(diǎn)的,所以如果你還是用的以前的元素和屬性那就等于寫錯(cuò)了。
寫錯(cuò)的結(jié)果往往是游戲加載MOD時(shí)直接崩潰,也不會給你任何提示,你只能慢慢去尋找BUG。
而騎砍2作為一個(gè)大型游戲,每次啟動(dòng)時(shí)間都很長,導(dǎo)致你測試一個(gè)MOD數(shù)據(jù)是否配置正確的測試流程會非常長。
媽耶,多少個(gè)夜晚,游戲崩潰的那一瞬間,我人就崩潰了。
所以后來我就想著做一個(gè)XML在線編輯器去解決這個(gè)問題。
技術(shù)預(yù)研
可視化編程
其實(shí)我一開始沒有做這個(gè)XML編輯器的想法,因?yàn)檫@玩意一看就難搞,而是想通過一個(gè)可視化編程,通過拖拉拽元素和屬性的方式去實(shí)現(xiàn)。
你別說,我還真的做了一套初步方案出來,結(jié)果配置一個(gè)大型的XML這玩意拖拉拽無數(shù)次,心態(tài)逐漸爆炸,遂放棄此方案。
VSCODE插件
想看看有沒有什么VSCode插件可以進(jìn)行代碼提示,有一個(gè)使用XSD進(jìn)行代碼校驗(yàn)的,貌似還是IBM提供的。
但是很可惜已經(jīng)廢棄,然后用不了了,放棄此方案。
在線編輯器
后來之所以使用在線編輯器的方式做這個(gè),是因?yàn)槿脑路莨具@邊想要做一個(gè)在線編輯java項(xiàng)目環(huán)境xml配置文件的一個(gè)東西。
然后我這邊就嘗試著做了一個(gè),了解到了CodeMirror。
CodeMirror通過自己配置tags來支持xml的代碼提示,但是并不支持xml的代碼校驗(yàn),所以需要自己去做xml的代碼校驗(yàn)。
并且因?yàn)橥ǔN覀內(nèi)バr?yàn)xml用的是xsd,所以還需要將xsd轉(zhuǎn)換成CodeMirror的tags配置。
這個(gè)不論是百度Google,還是說Github,都是查不到相對應(yīng)的方案,所以只能自己寫代碼去實(shí)現(xiàn)。
在這個(gè)過程中,我對CodeMirror,xsd,htmllint都有了比較深的一個(gè)了解,最終完成了項(xiàng)目。
因?yàn)檫@是之前公司的代碼,所以這里就不放出來了。
總之,在這個(gè)過程中了解到CodeMirror這么個(gè)東西,才有了用CodeMirror去做MOD的在線編輯器的想法。
最初形態(tài):簡單的在線XML編輯器
好了,廢話不說,拿起鍵盤就是無腦干。
最初形態(tài)沒有左側(cè)的文件樹,只有一個(gè)單純的編輯器和一個(gè)規(guī)則學(xué)習(xí)彈框。
涉及到的技術(shù)就三個(gè):
CodeMirror
FileReader
xmldom
用CodeMirror做編輯器
CodeMirror這塊主要使用的react的一個(gè)封裝版react-codemirror2,反正就是看文檔和Demo自己配。
唯一的難度就是網(wǎng)上一大堆的CodeMirror配置介紹很多都是抄來抄去,轉(zhuǎn)載來轉(zhuǎn)載去,還是個(gè)錯(cuò)的,簡直離譜。
總之你想玩的話最好還是看官方文檔(https://codemirror.net/) 和文檔上的Demo,然后自己研究下,抄別人配置的話水很深,你把握不住的。
我這里貼一段我封裝的編輯器組件的配置代碼吧,反正絕對可用,絕大多數(shù)編輯器的功能都OK,不過僅僅適用于編輯XML。
里面的注釋比較詳盡了,包括常用的代碼折疊,代碼格式化都有,我就懶得一一講了,你可以參考官網(wǎng)自己看看。
其中的一些引用代碼我就不貼了,有興趣的可以去上面提到的代碼倉庫看看。
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' // 光標(biāo)行代碼高亮 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' // 代碼提示補(bǔ)全和 import 'codemirror/addon/hint/xml-hint.js' import 'codemirror/addon/hint/show-hint.css' import './hint.css' import 'codemirror/addon/hint/show-hint.js' // 代碼校驗(yàn) import 'codemirror/addon/lint/lint' import 'codemirror/addon/lint/lint.css' import CodeMirrorRegisterXmlLint from './xml-lint' // 輸入> 時(shí)自動(dòng)鍵入結(jié)束標(biāo)簽 import 'codemirror/addon/edit/closetag.js' // 注釋 import 'codemirror/addon/comment/comment.js' // 用于調(diào)整codeMirror的主題樣式 import style from './index.less' // 注冊Xml代碼校驗(yàn) CodeMirrorRegisterXmlLint(CodeMirror) // 格式化相關(guān) 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 每次變動(dòng)時(shí),都會重新改變校驗(yàn)規(guī)則 CodeMirrorRegisterXmlLint(CodeMirror, tags, onErrors) }, [onErrors, tags]) // 開始標(biāo)簽 function completeAfter(cm, pred) { if (!pred || pred()) setTimeout(function () { if (!cm.state.completionActive) cm.showHint({ completeSingle: false }); }, 100); return CodeMirror.Pass; } // 結(jié)束標(biāo)簽 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 屬性換行的時(shí)候是否加上標(biāo)簽的長度 multilineTagIndentPastTag: false }, indentUnit: 2, // 換行的默認(rèn)縮進(jìn)多少個(gè)空格 theme: 'ayu-dark', // 編輯器主題 lineNumbers: true,// 是否顯示行號 autofocus: true,// 自動(dòng)獲取焦點(diǎn) styleActiveLine: true,// 光標(biāo)行代碼高亮 autoCloseTags: true, // 在輸入>時(shí)自動(dòng)鍵入結(jié)束元素 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自動(dòng)轉(zhuǎn)換為空格 "Tab": (cm) => { if (cm.somethingSelected()) {// 選中后整體縮進(jìn)的情況 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
學(xué)習(xí)XML,并提取出tags規(guī)則
當(dāng)我們使用CodeMirror做一個(gè)簡單的編輯器時(shí),想要進(jìn)行一個(gè)XML的代碼提示,是需要使用到tags。
很明顯,不同的游戲有不同的XML規(guī)則,包括游戲更新之后XML規(guī)則也會更改。
所以我們必須要保證有一個(gè)機(jī)制去不斷地學(xué)習(xí)這些XML規(guī)則,所以這里我做了一個(gè)學(xué)習(xí)XML文件規(guī)則的彈窗去做這個(gè)事情。
點(diǎn)擊編輯器左上方的 約束規(guī)則——>新增約束規(guī)則
會彈出這樣一個(gè)彈窗:
通過FileReader讀取指定文件夾的XML文件,然后使用xmldom來依次解析這些xml文件的文本,生成文檔對象。
再分析這些文檔對象得到最終的tags規(guī)則。
這一步驟只需要對xml有所了解,其實(shí)也蠻基礎(chǔ)的,所以不講了。
總之現(xiàn)在我們完成了它的最初形態(tài),你每次使用它需要將你編輯的XML文件內(nèi)容復(fù)制到這個(gè)在線編輯器,編輯完后,再將完成的文本復(fù)制到原XML文件保存覆蓋。
進(jìn)化形態(tài):加載樹形文件結(jié)構(gòu)和全文件校驗(yàn)功能的在線XML編輯器
上面的編輯器其實(shí)使用場景非常窄,只能在新寫一個(gè)XML時(shí)使用。
一個(gè)MOD往往幾十上百,甚至幾千個(gè)文件,不可能一個(gè)個(gè)粘貼到編輯器中進(jìn)行校驗(yàn)。
所以我們需要在這個(gè)編輯器中,加載MOD的所有XML文件,并進(jìn)行一個(gè)代碼校驗(yàn)。
涉及到的技術(shù)就兩個(gè):
- FileReader
- Web Worker
左側(cè)文件樹
左側(cè)這個(gè)文件樹使用Ant Design的Tree組件完成,這里配置什么的就不講了。
在點(diǎn)擊打開文件夾這個(gè)按鈕時(shí)
同樣使用FileReader來讀取MOD文件夾中的文件。
但是FileReader獲取到的是一個(gè)文件數(shù)組,要想生成我們左側(cè)的樹形結(jié)構(gòu)需要自己手動(dòng)解析每個(gè)XML文件的路徑,并據(jù)此生成一個(gè)樹形結(jié)構(gòu)。
全文件校驗(yàn)功能
在打開文件夾的一瞬間,我們需要對全部的XML文件進(jìn)行一次代碼校驗(yàn),如果校驗(yàn)有誤,需要在左側(cè)文件夾上將相關(guān)的文件及它父級祖級的一系列文件夾全部標(biāo)紅。
這個(gè)功能表面上很簡單,其實(shí)坑點(diǎn)很大,因?yàn)樾r?yàn)的計(jì)算量實(shí)際上并不小,特別是你的MOD中有幾百幾千個(gè)文件的時(shí)候,非常容易搞得你js阻塞,頁面無響應(yīng)。
在這里我使用了Web Worker新開一個(gè)線程去處理這個(gè)校驗(yàn)過程,在校驗(yàn)完成后將結(jié)果返回給我。
在這個(gè)過程中,我對Web Worker的使用也有了更多的了解。
印象中一直以為是一個(gè)new Worker(某js文件)這樣的方式去玩,感覺很難結(jié)合react的模塊化開發(fā)來使用。
但是實(shí)際上現(xiàn)在在webpack里配置上worker-loader,可以很方便使用Web Worker。
首先我們的worker代碼可以寫成下面這樣:
import { lintFileTree } from '@/utils/files' onmessage = ({ data }) => { lintFileTree(data.fileTree, data.currentTags).then(content => { postMessage(content) }) }
然后我們使用這個(gè)Worker時(shí),可以如下所示
import { useWebWorkerFromWorker } from 'react-webworker-hook' import lintFileTreeWorker from '@/utils/webWorker/lintFileTree.webworker' const worker4LintFileTree = new lintFileTreeWorker() const [lintedFileTree, startLintFileTree] = useWebWorkerFromWorker(worker4LintFileTree)
然后你再用個(gè)useEffect依賴這個(gè)lintedFileTree,如果變動(dòng)了就做某些操作,所以寫起來就像用useState一樣輕松。
非遞歸遍歷樹
大家可以看到上面我們用到的這些東西,很多都與樹相關(guān),比如遍歷文件樹去校驗(yàn)代碼。
又或者我們切換了某個(gè)約束規(guī)則后,也是需要遍歷整個(gè)文件樹進(jìn)行重新校驗(yàn)的。
遍歷的過程中,之前我用的是遞歸遍歷整個(gè)樹,這樣做不好的地方在于遞歸的時(shí)候內(nèi)存得不到釋放,所以后來我換了一種算法,采用非遞歸的方式遍歷整個(gè)樹。
IndexDB保存文件內(nèi)容
因?yàn)槲覀兊腗OD文件內(nèi)容比較多比較大,所以內(nèi)存占用可能會很大,不可能一直把這些文件內(nèi)容放到內(nèi)存中。
所以我讀取到文件內(nèi)容會依次放入IndexDB中,只展示當(dāng)前編輯文件的內(nèi)容。
只有在需要的時(shí)候,比如全文件校驗(yàn)或者切換文件時(shí),才從IndexDB再次獲取文件內(nèi)容。
究極進(jìn)化形態(tài):突破瀏覽器沙盒限制,實(shí)現(xiàn)對電腦本地文件的增刪改
通過之前的操作,我們終于完成了一個(gè)基本可用的在線XML編輯器。
但是它有一個(gè)致命缺點(diǎn),就是受到瀏覽器沙盒環(huán)境的限制,我們在修改了文件后,沒法直接保存到電腦上,而必須依靠手動(dòng)將修改好的代碼一一復(fù)制到對應(yīng)的文件中。
這個(gè)操作繁瑣復(fù)雜,導(dǎo)致我們編輯器的功能可能只能用來輔助編寫代碼和批量校驗(yàn)。
之前我以為只能做到這種程度,但是后來我在知乎上偶然看了一個(gè)帖子,發(fā)現(xiàn)Chrome86+的版本多了一個(gè)功能API:FileSystemAccess。
另外,除非是本地localhost環(huán)境,否則這個(gè)API只在https環(huán)境下才能調(diào)用,也就是說你在一個(gè)http的網(wǎng)站上,即使你用的是Chrome86+或者是Edge86+,那也是調(diào)用不了的。
這個(gè)API可以讓我們直接操作本地電腦上的文件,而不是像FileReader一樣只能讀,或者像FileSystem一樣只能在瀏覽器沙盒內(nèi)操作。
通過FileSystemAccess我們不僅可以實(shí)現(xiàn)對文件夾中的文件進(jìn)行讀取修改,還能新增和刪除文件。
所以我使用這個(gè)API全面替換了之前使用FileReader的各個(gè)點(diǎn),實(shí)現(xiàn)了在文件樹上右鍵進(jìn)行文件夾和文件的新增和刪除。(這里是不支持對文件進(jìn)行重命名的,不過其實(shí)我們可以使用刪除后再新增的方式來模擬重命名,但是我就懶得做了)
同時(shí)在按保存按鈕或者按保存的快捷鍵Ctrl+S后,就可以直接對文件進(jìn)行保存操作。
下面是一個(gè)使用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
只要被這個(gè)組件包裹的元素(比如按鈕)被點(diǎn)擊后,會立即調(diào)用showDirectoryPicker,請求打開文件夾。
在打開文件夾后,通過獲得的文件夾handle去請求文件夾寫入權(quán)限,然后再把這個(gè)文件夾handle傳到外部,獲取文件樹結(jié)構(gòu)。
這里的操作是有瑕疵的,因?yàn)檎埱蟠蜷_文件夾時(shí)瀏覽器會彈框向用戶獲取讀取文件夾的權(quán)限,
打開完畢后又直接會彈第二次框獲取寫入權(quán)限,也就是說在打開文件夾時(shí)會彈兩次框。
但是我也只能通過這種手法一次性請求到所有的權(quán)限,要不然等到要保存時(shí)再去請求權(quán)限也不太好。
不過瑕不掩瑜,通過這個(gè)API不僅實(shí)現(xiàn)了文件的增刪改,還解除了對IndexDB的使用。
因?yàn)槲覀冸S時(shí)可以通過文件Handle獲取到相應(yīng)的文件內(nèi)容,所以沒必要將文件內(nèi)容保存到IndexDB中。
更多的功能與細(xì)節(jié)
以上我只是對技術(shù)上的核心功能進(jìn)行了概述,實(shí)際上這個(gè)編輯器還有N多的細(xì)節(jié)。
比如調(diào)整tags規(guī)則的面板,比如那些工具欄的按鈕,比如對dva的簡單封裝處理,比如對xml進(jìn)行分析時(shí),如果屬性值是數(shù)字,那么就不進(jìn)行提示,而是直接忽略,因?yàn)閿?shù)字往往沒太大意義而且枚舉值太大。
這一切的一切,都太多太多,但是它們的應(yīng)用都比較基礎(chǔ),所以不想贅述細(xì)節(jié),否則這篇博客就會變得非常長,而且難以突出核心思路。
不足與總結(jié)
這里的不足更多的是因?yàn)閼?,比如之前說的文件夾和文件重命名功能,還有調(diào)整tags規(guī)則的自定義規(guī)則那里不支持修改刪除。
可以實(shí)現(xiàn),只是懶得做了。
這個(gè)東西前前后后做了幾個(gè)月,也不是說每天晚上都在寫這個(gè),主要是有靈感了就來寫一下,或者發(fā)現(xiàn)哪里可以更好地改進(jìn)一下就再寫一下。
合起來約摸著有兩三周的每個(gè)晚上在做這個(gè)事情,然后當(dāng)它愈加趨近于完善和可用時(shí),就愈加懶得做了。
因?yàn)槭O碌牟僮鞑惶匾?,且腦補(bǔ)一下就可以完成,沒有太多有挑戰(zhàn)性的地方了。
不過總體來說,這個(gè)東西現(xiàn)在的可用性還是很強(qiáng)的。
不僅僅可以用于《騎馬與砍殺2》、《了不起的修真模擬器》、《文明6》等一系列游戲的XML文件的輔助編寫,還可以用于那些沒有XSD規(guī)則,又過于復(fù)雜的XML配置,甚至它還可以學(xué)習(xí)你自定義的XML規(guī)則。
到此這篇關(guān)于js制作xml在線編輯器實(shí)例的文章就介紹到這了,更多相關(guān)js制作xml在線編輯器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
小程序?qū)崿F(xiàn)頁面頂部選項(xiàng)卡效果
這篇文章主要為大家詳細(xì)介紹了小程序?qū)崿F(xiàn)頁面頂部選項(xiàng)卡效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-11-11如何在面試中手寫出javascript節(jié)流和防抖函數(shù)
這篇文章主要介紹了如何在面試中手寫出javascript節(jié)流和防抖函數(shù),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10javascript設(shè)計(jì)模式之模塊模式學(xué)習(xí)筆記
這篇文章主要為大家詳細(xì)介紹了javascript設(shè)計(jì)模式之模塊模式學(xué)習(xí)筆記,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02微信小程序調(diào)用攝像頭實(shí)現(xiàn)拍照功能
這篇文章主要為大家詳細(xì)介紹了微信小程序調(diào)用攝像頭實(shí)現(xiàn)拍照功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07詳解如何在Javascript中使用Object.freeze()
這篇文章主要介紹了詳解如何在Javascript中使用Object.freeze(),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10