做影视后期有哪些资源网站,wordpress首页调用所有分类,建个淘宝那样的网站需要多少钱,wordpress主题开拓右边栏前言 前面两篇文章#xff0c;我们简单讲述了 WebContainer/api 、Terminal 的基本使用#xff0c;离完备的在线代码编辑器就差一个代码编辑了。今天通过 monaco editor #xff0c;来实现初级代码编辑功能#xff0c;讲述的是整个应用的搭建#xff0c;并不单独针对monac…前言 前面两篇文章我们简单讲述了 WebContainer/api 、Terminal 的基本使用离完备的在线代码编辑器就差一个代码编辑了。今天通过 monaco editor 来实现初级代码编辑功能讲述的是整个应用的搭建并不单独针对monaco editor的使用哈因为Monaco editor 确实有些难度仅在使用到的API 、功能模块上做讲解。如果大家有需要可以留言会考虑后期做一篇monaco的保姆级教程。 页面布局 初始化 pnpm、vite、typescript的项目将页面初始化为下 文件树 此处的文件树是指项目左侧的文件列表使用ElementPlus tree 组件进行渲染如下 // 定义 filemenu tree data
export interface ITreeDataFile {id: string;icon?: string;label: string;suffix: string;
}
// 文件夹数据结构
export interface ITreeDataFolder {id: string;label: string;isFolder: boolean;children: ITreeDataFile[];
}
// 可能是新建文件
export interface ITreeDataIsNew {id: string;isNew: boolean;isFolder: boolean;
} 针对新建文件/文件夹时需要知道当前层级例如我是在 根目录新建 还是在src内新建因此需要监听tree 的click 事件 /*** 节点点击回调 - 通过该参数实现识别当前的目录层级* param data*/function nodeClick(data: ITree) {currentNodeKey.value data.id;} 同时在点击外部时还需要取消目录选中 /*** cancelChecked*/function cancelChecked() {// .is-current 通过该类实现的当前文件激活样式currentNodeKey.value ;treeRef.value?.setCurrentKey();}
事件响应 // 折叠所有文件function collapseAll() {// 全部展开 - 可用于定位某个文件// Object.values(treeRef.value!.store.nodesMap).forEach((v) v.expand())Object.values(treeRef.value!.store.nodesMap).forEach((v) v.collapse());} 新建文件/文件夹的核心就是blur后使用 newFileName push到指定位置上 /*** confirm 新建文件/文件夹确认事件*/function confirm() {removeNewItem(dataSource);if (!newFileName.value) return;// 不然就根据当前位置push 真实的数据到dataTree中通过 newFileFlag.value 识别是文件还是文件夹const fileSuffix newFileName.value.split(.)[1];const data: ITreeDataFile | ITreeDataFolder {id: ${new Date().getTime()},label: newFileName.value,isFolder: !newFileFlag.value,children: [],icon: newFileFlag.value ? getFileIcon(fileSuffix) : ,};if (currentNodeKey.value) {// 如果有节点被选中则看是文件还是文件夹是文件-在父级添加是文件夹-直接在当前添加const currentNode treeRef.value?.getNode(currentNodeKey.value);if (currentNode?.data.isFolder) {// 如果是文件夹则在当前节点下添加treeRef.value?.append(data, currentNodeKey.value);} else {// 如果是文件则在 Tree 中给定节点后插入一个节点treeRef.value?.insertAfter(data, currentNodeKey.value);}} else {// 如果没有节点被选中则直接添加到根目录dataSource.push(data);}} Terminal 这块应该是简单的参考上篇文章哈Terminal Web终端基础Web IDE 技术探索 二 往后可能需要拓展多终端场景因此设计上需要考虑周全剩下的功能待开发时再细说。
Web Container 这里强调下哈Web Container的API基本都是 async / await 方式因此在使用时一定需要注意执行时机和等待结果 配置 WebContainer/api 跨源隔离
headers: {Cross-Origin-Embedder-Policy: require-corp,Cross-Origin-Opener-Policy: same-origin,} WebContainer的很多事件都需要await执行在设计上需要考虑周全因为多处需要共享container的状态因此我们直接使用pinia实现全局状态管理
// Web Container 共享文件因为 fileTree Container对象需要在其他文件中共享
import { WebContainer } from webcontainer/api;
import { defineStore } from pinia;// 第一个参数是应用程序中商店的唯一 id
export const useContainerStore defineStore(container, {state: () {return {container: InstanceTypetypeof WebContainer | nullnull,boot: false, // 定义容器是否启动};},actions: {// 1. bootContainer 启动容器async bootContainer() {// ts-ignorethis.container await WebContainer.boot();this.boot true;},},
});在App页面监听 boot 实现loading效果 !-- loading --div classloading v-if!containerStore.bootdiv classloader/divspanWait for the web container to boot.../span/div 在Container中需要频繁监听输出流统一做事件封装处理 // 封装统一的输出函数 - 监听容器输出async output(stdout: WebContainerProcess, fun: voidFun) {stdout.output.pipeTo(new WritableStream({write(data) {fun(data);},}));}, 封装统一的命令执行函数提供给terminal执行 // 3. 执行 terminal 命令async runTerminal(cmd: string, fun: voidFun) {if (!this.container) return;const command cmd.split( ); // 这里是按空格进行分割const state await this.container.spawn(command[0], command.slice(1));// 如果是下载命令则需要获取状态码if (command[1] install || command[1] i) {const code await state.exit;if (code 0) // ... 执行相关代码}// 不管成功还是失败都输出this.output(state, fun);}, 在terminal 中监听 command事件直接传递到 container中执行通过回传参数实现terminal的终端显示
function command(cmdKey: string,command: string,success: voidFun,failed: voidFun,name: string
) {containerStore.runTerminal(command, (content) {success({ content });console.log(name, 执行command:, command);}); 文件菜单与FileSystemTree 在逻辑上是先有的文件才去执行 mounted 操作因此当我新建文件的时候都去调用 mounted 。在初始化时我们提供三种基本的项目结构mockVueProject、mockNodeProject、mockReactProject用Vue 举例哈其他类似具体的FileSystemTree可以参考我的上篇文章File System Tree 读取成树结构 通过以上的树结构读取成El-tree 组件的数据源应该不是难事递归实现即可在上一篇中已经实现了但是注意哈需要在结束时进行排序先排目录结构 isFolder在排name属性这样就是与vscode类似的效果 新增文件 /*** 将新建的文件/文件夹挂载到Web Container File System Tree 中*/function mountedFileSystemTree() {tryCatch(async () {let path /;// 如果有选中节点则需要处理选中节点的路径问题if (currentNodeKey.value) {// 需要在这里加上父级 - 这里还需要判断激活的是文件还是文件夹const currentNode treeRef.value?.getNode(currentNodeKey.value); // 当前激活节点const dataMap JSON.parse(JSON.stringify(dataSource)) as TFullData;let fullpath string[]getFullPath(dataMap, currentNodeKey.value);if (currentNode?.data.isFolder) path fullpath?.join(/);else {// 删除最后一项fullpath fullpath?.slice(0, -1);path fullpath?.join(/);}path /;}// 如果没有选中节点则直接拼接文件名称放置到根路径下即可// 例如 /vite.config.jspath newFileName.value;console.log(### path , path);newFileFlag.value? containerStore.addFile(path): containerStore.addFolder(path);});} Monaco Editor 上诉简单介绍了整个系统的文件系统、container与termina的关系与核心实现并通过新增文件/文件夹实现Web Container FileSystemTree的文件挂载、写入、创建文件夹但是还是没有实质性的文件内容编辑现在通过monaco editor 插件实现文件内容编辑monaco确实是有难度的本文不过及底层原理仅在应用层面上做叙述。
create
// use monaco editor
import { editor } from monaco-editor;/*** init monaco*/function initMonaco(selector: string) {const dom document.querySelector(selector) as HTMLElement;editor.create(dom, {value: function x() {\n\tconsole.log(Hello world!);\t\n},language: javascript,});} 但是这样是要报错的Uncaught Error: Unexpected usage详见ISSUES解决办法
// 解决 monaco editor 报错 Uncaught Error: Unexpected usageimport editorWorker from monaco-editor/esm/vs/editor/editor.worker?worker;
import jsonWorker from monaco-editor/esm/vs/language/json/json.worker?worker;
import cssWorker from monaco-editor/esm/vs/language/css/css.worker?worker;
import htmlWorker from monaco-editor/esm/vs/language/html/html.worker?worker;
import tsWorker from monaco-editor/esm/vs/language/typescript/ts.worker?worker;export function fixEnvError() {window.MonacoEnvironment {getWorker(_, label) {if (label json) {return new jsonWorker();}if (label css || label scss || label less) {return new cssWorker();}if (label html || label handlebars || label razor) {return new htmlWorker();}if (label typescript || label javascript) {return new tsWorker();}return new editorWorker();},};
}create 之前先调用 fixEnvError 方法导入需要的worker文件: function initMonaco(selector: string) {fixEnvError();const dom document.querySelector(selector) as HTMLElement;editor.create(dom, {value: function x() {\n\tconsole.log(Hello world!);\t\n},language: javascript,});} 动态设置属性 /** 为了避免Vue响应式对编辑器的影响使用toRaw转成普通对象 */getEditor() {return toRaw(this.editor);},/** 设置编辑器的值 设置语言模型 */setValue(value: string, language: string) {this.getEditor()?.setValue(value);// 1. 文件后缀与语言模型匹配const languageModel this.languages.find((item) {return item.extensions?.includes(.${language});});editor.setModelLanguage(this.getEditor()?.getModel()!,languageModel?.id || );},/** 获取编辑器的值 */getValue() {return this.getEditor()?.getValue();}, 在菜单点击时获取文件内容进行editor赋值,处理上直接使用 this.editor.setValue会导致页面卡死转成普通对象避免响应式的影响同时在设置值上需要动态调整语言类型不然不会高亮显示 监听保存事件 通过保存事件实现真正的文件存储 onKeyDownHandle(e: any) {// 通过keycode/ctrlKey/shiftKey/altKey 的状态唯一确定一个事件- 有值为true无值为falseconst eventMap: TKeyMapstring, voidFun {49/true/false/false: () {console.log(Ctrl S);},};const key ${e.keyCode}/${e.ctrlKey}/${e.shiftKey}/${e.altKey};if (eventMap[key]) {eventMap[key]();e.browserEvent.preventDefault();}}, // eventCtrlSeventSave() {const containerStore useContainerStore();const fileMenuStore useFileMenuStore();// 1. 获取当前编辑器的内容const contents this.getEditor()?.getValue() as string;// 2. 调用 container 的 saveFile 方法containerStore.writeFile(fileMenuStore.filePath, contents);}, 针对依赖下载的优化
// 特殊的命令需要单独处理if (installCmdList.includes(command)) {// 执行下载依赖应该用回显模式success(flash);containerStore.runTerminal(command, (content) {console.log(content, content.includes(Done));if (content.includes(Done)) {flash.finish();// 把最后的信息输出success({ content: ✅ content });} else flash.flush(content);});} 使用回显模式展示依赖下载会更加合适 多tab页模式 tab 切换的和核心是通过记录editor 的状态及语言模型实现的 // 1. 关键参数 mapconst fileStateMap new Map();// 切换文件 - 需要保存 stateasync switchFile(index: number) {const fileSuffix this.fileList[index].suffix;// 2. 跳转到指定文件this.currentFile index;// 3. 看看跳转后文件时候有 model 有的话直接使用没有就创建新的const file this.fileStateMap.get(this.getCurrentFileID());if (file file.model) {this.setModel(toRaw(file.model));this.restoreViewState(toRaw(file.state)); // 恢复文件的编辑状态} else {// 2. 读取文件内容赋给monacoconst contents await this.containerStore.readFile(this.fileMenuStore.filePath);const model this.createModel(contents || ,this.getLanguageModel(fileSuffix)?.id as string);this.setModel(model);this.fileStateMap.set(this.getCurrentFileID(), {model: this.getModel(),state: this.saveViewState(),});}this.getEditor()?.focus();}, 关闭则是通过监听事件实现
window.addEventListener(mouseup, (e: MouseEvent) {const span e.target as HTMLElement;if (e.button 1 span.getAttribute(data-key) closeFileButton) {// 1. 先保存monacoStore.eventSave();// 2. 关闭文件const index span.getAttribute(data-index);monacoStore.deleteFile(Number(index));}
}); 在你关闭的是其他tab页的时候涉及到不同的model获取内容因此需要先跳转到需要关闭的页面获取完内容再跳转回正常的页面类似VScode不然你获取的内容是不对的哈 总结 通过WebContainer、Terminal、MonacoEditor的结合初步实现了Web IDE在线编辑器的开发整体实现过程还是比较顺利的但是monaco的应用太痛苦了全英文官网API还是.d.ts类型文件 不过不得不说monaco的强大之处远不止这么简单支持git冲突模型对比 利用yjs 原生支持 y- monaco: 大家感兴趣后续会考虑整理Monaco Editor的保姆级使用教程大家多多支持呀~