构建 Chrome 应用版代码编辑器mini-code-edit 示例深度解析CodeMirror chrome.fileSystem【免费下载链接】chrome-extensions-samplesChrome Extensions Samples项目地址: https://gitcode.com/gh_mirrors/ch/chrome-extensions-samples本篇技术指南以 chrome-extensions-samples 仓库中_archive/apps/samples/mini-code-edit示例为对象剖析一个非平凡non-trivialChrome App 代码编辑器的完整实现。该示例展示了如何在打包应用Packaged App中集成 CodeMirror 编辑器实现语法检测syntax detection与语法高亮syntax highlight并借助扩展版 FileSystem APIchrome.fileSystem让用户从磁盘选取文件、应用即可对该文件执行读取与写入。读完本文你将掌握chrome.fileSystem打开/保存文件的完整调用链、基于文件扩展名的语法模式自动切换方案以及应用窗口、快捷键命令与上下文菜单的组织方式。示例概览一个麻雀虽小五脏俱全的代码编辑器mini-code-edit是 Chrome Apps 时代的代表性示例它以 640×400建议的独立应用窗口呈现一个可用的代码编辑器核心功能包括语法检测与高亮根据文件扩展名自动识别 JSON / HTML / CSS / JavaScript 语法模式文件系统读写通过chrome.fileSystem.chooseEntry()打开磁盘上的文件读取内容进编辑器编辑后回写磁盘多窗口与快捷键通过chrome.app.window.create()创建窗口并注册全局命令CtrlShift1随时新建窗口代码片段注入通过上下文菜单chrome.contextMenus将预置的 Hello World、Servo 串口示例等代码片段插入光标处。官方 README_archive/apps/samples/mini-code-edit/README.md将其定位为“非平凡示例”即它不是一行 hello world而是将若干真实 API 组合成一个可实际使用的应用。应用运行的界面效果见官方截图从截图可以看到顶部工具栏提供 New / Open / Save 三个按钮中央是带行号的深色代码编辑区当前示例编辑的是index.js底部状态栏显示Filename与Mode两项信息这正是该示例语法检测功能的直观体现。用到的 API 与权限设计README 明确列出本示例依赖的三类 API它们在 manifest 与源码中有清晰的落点API用途仓库中的证据chrome.fileSystem让用户选择磁盘文件应用获得读取/写入该文件的权限editor.js中的chrome.fileSystem.chooseEntry()chrome.app.runtime应用启动onLaunched时创建主窗口background.jschrome.app.window创建、管理应用窗口尺寸、边界约束background.js与editor.js中的chrome.app.window.create()Manifest_archive/apps/samples/mini-code-edit/manifest.json中的权限声明如下{ name: MiniCodeEdit, version: 0.1.12, manifest_version: 2, minimum_chrome_version: 23, description: A very small code editor., app: { background: { scripts: [background.js] } }, permissions: [ {fileSystem: [write]}, unlimitedStorage, contextMenus ], icons: { 16: img/16x16/file_edit.png, 32: img/32x32/file_edit.png, 64: img/64x64/file_edit.png, 128: img/128x128/file_edit.png }, commands : { cmdNew: { suggested_key: { default: CtrlShift1 }, global: true, description: Create new window } } }关键配置点说明{fileSystem: [write]}chrome.fileSystem权限必须显式声明写权限若缺少write应用只能读取用户选择的文件而无法回写。这正是本示例能读写磁盘文件的权限基础。unlimitedStorage解除应用本地数据存储的配额限制配合 FileSystem API 的写入操作避免 QUOTA_EXCEEDED。contextMenus用于注册代码片段注入的右键菜单。commands.cmdNewglobal: true注册一个全局命令CtrlShift1即使应用窗口不在前台也能触发新建窗口动作。minimum_chrome_version: 23声明需要 Chrome 23彼时chrome.fileSystem扩展 API 的可用版本前提。应用入口app.background.scripts指向background.js这是 Chrome App 生命周期onLaunched的起点。图标资源按 16 / 32 / 64 / 128 四档尺寸提供存放于_archive/apps/samples/mini-code-edit/img/目录。应用入口窗口创建与全局命令background.js是应用的后台脚本包含两个核心监听器1. 启动事件chrome.app.runtime.onLaunched——应用被启动时创建主窗口chrome.app.runtime.onLaunched.addListener(function() { // width 640 for font size 12 // 720 for font size 14 chrome.app.window.create(main.html, { frame: chrome, id: codewin, innerBounds: { width: 720, height: 400, minWidth:720, minHeight: 400 } }); });窗口参数要点frame: chrome使用系统原生窗口边框相对无边框窗口noneid: codewin窗口标识符Chrome 会据此恢复窗口状态同一 id 的窗口在会话间维持几何状态与单实例约束innerBounds: { width: 720, height: 400, minWidth: 720, minHeight: 400 }限定窗口内边界尺寸同时设置最小宽高保证编辑器布局不塌陷。源码注释提醒字号 12 时窗口宽 640 即可字号 14 时需 720——这是对 CodeMirror 渲染宽度的经验取值。2. 命令事件chrome.commands.onCommand——监听 manifest 中注册的cmdNew命令chrome.commands.onCommand.addListener(function(command) { console.log(Command triggered: command); if (command cmdNew) { chrome.app.window.create(main.html, { frame: chrome, id: codewin, innerBounds: { width: 720, height: 400, minWidth:720, minHeight: 400 } }); } });注意一个细节由于窗口id同为codewin当该 id 的窗口已存在时chrome.app.window.create()会聚焦已有窗口而不是无限叠加新窗口而editor.js的handleNewButton()中其实也保留了一个false分支的清空当前编辑器新建文件逻辑从代码结构看示例最终选择了新建窗口路径体现了两种新建策略的取舍。界面骨架main.html 与样式main.html是编辑器唯一的窗口页面结构非常精简script srcsnippets.js/script script srceditor.js/script script srccm/lib/codemirror.js/script script srccm/mode/css/css.js/script script srccm/mode/xml/xml.js/script script srccm/mode/javascript/javascript.js/script script srccm/mode/htmlmixed/htmlmixed.js/script link relstylesheet hrefstyle.css link relstylesheet hrefcm/lib/codemirror.css link relstylesheet hrefcm/theme/lesser-dark.css加载顺序很讲究先加载应用逻辑snippets.js、editor.js再加载 CodeMirror 核心库与各语言模式模块最后引入样式与主题。本示例内置了完整的 CodeMirror 2 发行包位于_archive/apps/samples/mini-code-edit/cm/包含lib/codemirror.js、十余种语言模式css、xml、javascript、htmlmixed、python、ruby、go 等、theme/下的 12 套主题ambiance、eclipse、monokai、lesser-dark 等以及keymap/下的 emacs / vim 键位映射。本文示例仅按需加载了 4 个模式与lesser-dark主题其余模式可按需扩展。正文的 DOM 结构只有三块div classbuttons button idnew img srcimg/16x16/file_add.png/ New /button button idopenimg srcimg/16x16/file.png/ Open /button button idsaveimg srcimg/16x16/diskette.png/ Save /button /div div ideditor/div div classinfo labelFilename: /labelspan idtitle/span labelMode: /labelspan idmode/span /div工具栏New新建、Open打开、Save保存三个按钮配 16×16 图标_archive/apps/samples/mini-code-edit/img/16x16/#editorCodeMirror 的挂载容器.info底部状态栏动态显示当前文件名#title与语法模式#mode。style.css通过绝对定位将#editor铺满工具栏与状态栏之间的区域top: 29px; bottom: 24px并隐藏页面滚动overflow: hidden让编辑器滚动条接管交互.CodeMirror-scroll被设置为纵向隐藏、横向自动配合onresize手动同步容器尺寸。核心逻辑editor.js 的读写闭环editor.js是本示例的灵魂完整实现了打开 → 读取 → 编辑 → 保存的闭环。全局状态与错误处理var newButton, openButton, saveButton; var editor; var fileEntry; // 当前绑定的文件句柄chrome.fileSystem 的 Entry var hasWriteAccess; // 当前文件是否可写fileEntry与hasWriteAccess是读写权限模型的核心通过openWritableFile打开的文件可写通过只读方式打开的文件则只能查看。errorHandler()将FileError错误码QUOTA_EXCEEDED_ERR、NOT_FOUND_ERR、SECURITY_ERR、INVALID_MODIFICATION_ERR、INVALID_STATE_ERR映射为可读文本并输出到控制台是典型的 FileSystem API 错误处理模板。语法检测handleDocumentChangefunction handleDocumentChange(title) { var mode javascript; var modeName JavaScript; if (title) { title title.match(/[^/]$/)[0]; // 截取文件名去掉路径 document.getElementById(title).innerHTML title; document.title title; // 同步窗口标题 if (title.match(/.json$/)) { mode {name: javascript, json: true}; // JSON 复用 JS 模式 json 标志 modeName JavaScript (JSON); } else if (title.match(/.html$/)) { mode htmlmixed; modeName HTML; } else if (title.match(/.css$/)) { mode css; modeName CSS; } } else { document.getElementById(title).innerHTML [no document loaded]; } editor.setOption(mode, mode); document.getElementById(mode).innerHTML modeName; }语法检测策略一目了然默认语法模式为 JavaScriptmodeName: JavaScript.json→ 复用 CodeMirror 的 javascript 模式并开启json: true标志界面显示 JavaScript (JSON).html→htmlmixed模式HTML 混合模式可同时高亮内嵌的 CSS 与 JS.css→css模式其余扩展名一律回退到 JavaScript。这种按扩展名映射到 CodeMirror mode的实现正是 README 所述 syntax detection 的落地方式高亮则由 CodeMirror 各模式模块cm/mode/下加载的css.js、xml.js、javascript.js、htmlmixed.js负责。读取文件readFileIntoEditorfunction readFileIntoEditor(theFileEntry) { if (theFileEntry) { theFileEntry.file(function(file) { var fileReader new FileReader(); fileReader.onload function(e) { handleDocumentChange(theFileEntry.fullPath); editor.setValue(e.target.result); }; fileReader.onerror function(e) { console.log(Read failed: e.toString()); }; fileReader.readAsText(file); }, errorHandler); } }读取链路为Entry.file() → FileReader.readAsText() → handleDocumentChange() 检测语法 → editor.setValue() 载入内容。这里使用标准的 HTML5FileReader读取文本用Entry.fullPath作为文件名来源。写入文件writeEditorToFilefunction writeEditorToFile(theFileEntry) { theFileEntry.createWriter(function(fileWriter) { fileWriter.onerror function(e) { console.log(Write failed: e.toString()); }; var blob new Blob([editor.getValue()]); fileWriter.truncate(blob.size); fileWriter.onwriteend function() { fileWriter.onwriteend function(e) { handleDocumentChange(theFileEntry.fullPath); console.log(Write completed.); }; fileWriter.write(blob); } }, errorHandler); }写入链路为editor.getValue() → 构造 Blob → createWriter() → truncate() 截断旧内容 → write() 写入新内容。写入完成后再次调用handleDocumentChange()刷新标题与模式状态。truncate在write之前执行确保新内容比旧内容短时文件不会残留尾部旧数据——这是 FileWriter 覆盖写入的标准姿势。打开 / 保存的用户路径chrome.fileSystem的三个选择入口分别绑定不同回调var onChosenFileToOpen function(theFileEntry) { setFile(theFileEntry, false); // 只读打开 readFileIntoEditor(theFileEntry); }; var onWritableFileToOpen function(theFileEntry) { setFile(theFileEntry, true); // 可写打开 readFileIntoEditor(theFileEntry); }; var onChosenFileToSave function(theFileEntry) { setFile(theFileEntry, true); writeEditorToFile(theFileEntry); }; function handleOpenButton() { chrome.fileSystem.chooseEntry({ type: openWritableFile }, onWritableFileToOpen); } function handleSaveButton() { if (fileEntry hasWriteAccess) { writeEditorToFile(fileEntry); // 已持有可写句柄直接写回 } else { chrome.fileSystem.chooseEntry({ type: saveFile }, onChosenFileToSave); } }handleSaveButton()体现了权限模型的关键分支若当前文件已绑定且hasWriteAccess为真直接写入原文件不弹选择框否则新文件或只读打开的文件调用chrome.fileSystem.chooseEntry({ type: saveFile })让用户指定保存位置。chrome.fileSystem.chooseEntry()的type取值在本示例中出现两种openWritableFile以可写方式打开已有文件与saveFile选择/新建保存目标。需要说明的是type: saveFile在较新的 API 版本中已被saveFile的许可模式细化示例本身按旧版 API 编写但其读写权限分离、按需申请的设计思路在 Manifest V3 的chrome.fileSystemoffscreen 场景中依然适用。窗口内的新建handleNewButtonfunction handleNewButton() { if (false) { newFile(); // 预留的清空当前编辑器实现未启用 editor.setValue(); } else { chrome.app.window.create(main.html, { frame: chrome, id: codewin, innerBounds: { width: 720, height: 400} }); } }从源码结构看newFile()置空fileEntry与hasWriteAccess显示[no document loaded]是当前窗口内新建空白文件的实现但示例通过if (false)关闭了该分支实际行为是创建新的编辑窗口——两种新建策略都完整保留在代码中便于对照学习。代码片段上下文菜单与 SNIPPETSsnippets.js定义了一个SNIPPETS对象每个键是菜单标题值是待插入的代码文本例如Hello World: Manifest: {\n manifest_version: 2,\n name: Hello World,\n ... }\n, Hello World: main.js: chrome.app.runtime.onLaunched.addListener(function() {\n chrome.app.window.create(window.html, {\n bounds: { \n width: 400,\n height: 400\n }});\n}), Servo: onRead: function onRead(readInfo) { ... chrome.serial.read(connectionId, onRead); };文件头注释说明这些片段源自 2012 年 6 月 Google I/O 演示API 已演进片段不保证可用——它们更多是教学示例素材而非可运行代码。editor.js中的注入机制function initContextMenu() { chrome.contextMenus.removeAll(function() { for (var snippetName in SNIPPETS) { chrome.contextMenus.create({ title: snippetName, id: snippetName, contexts: [all] }); } }); } chrome.contextMenus.onClicked.addListener(function(info) { // Context menu command wasnt meant for us. if (!document.hasFocus()) { return; } editor.replaceSelection(SNIPPETS[info.menuItemId]); });机制要点应用启动时onload调用initContextMenu()先removeAll()清空再逐个创建菜单项避免重复注册每个片段以title显示在右键菜单中id用片段名充当点击回调先检查document.hasFocus()确保菜单事件确实发生在当前编辑窗口内上下文菜单可能来自应用的其他页面最终editor.replaceSelection()将片段插入编辑器光标处——这是 CodeMirror 的选区替换 API。CodeMirror 集成编辑器初始化与自适应布局editor.js的onload中完成编辑器装配editor CodeMirror( document.getElementById(editor), { mode: {name: javascript, json: true }, lineNumbers: true, theme: lesser-dark, fixedGutter: true, extraKeys: { Cmd-S: function(instance) { handleSaveButton() }, Ctrl-S: function(instance) { handleSaveButton() }, } }); newFile(); onresize();关键选项lineNumbers: true显示行号与截图一致theme: lesser-dark使用cm/theme/lesser-dark.css定义的深色主题fixedGutter: true行号槽固定水平滚动时行号不随内容滚动extraKeys注册Cmd-S/Ctrl-S快捷键直接触发保存——把桌面编辑器的肌肉记忆搬进 Web 应用初始模式为 JSON 形态的 JavaScript{name: javascript, json: true}。布局适配由onresize完成onresize function() { var container document.getElementById(editor); var containerWidth container.offsetWidth; var containerHeight container.offsetHeight; var scrollerElement editor.getScrollerElement(); scrollerElement.style.width containerWidth px; scrollerElement.style.height containerHeight px; editor.refresh(); }它监听窗口resize将 CodeMirror 滚动容器getScrollerElement()的宽高同步为#editor容器的实际尺寸再调用editor.refresh()重绘。这与style.css中#editor的绝对定位布局配合保证编辑器在窗口缩放时始终铺满可用空间。整体架构与调用链梳理综合源码本示例的运行流程可归纳为Chrome 启动应用 └─ chrome.app.runtime.onLaunched ──→ background.js 创建窗口(main.html) └─ 页面 onload ──→ 初始化 CodeMirror 上下文菜单 用户操作 ├─ New ──→ chrome.app.window.create 新开编辑窗口 ├─ Open ──→ chrome.fileSystem.chooseEntry({type:openWritableFile}) │ └─ Entry.file() → FileReader → handleDocumentChange(语法检测) → editor.setValue ├─ Save ──→ 已有可写句柄 ? 直接写回 : chooseEntry({type:saveFile}) │ └─ createWriter → truncate → write(Blob) ├─ CtrlShift1 ──→ chrome.commands.onCommand(cmdNew) → 新窗口 └─ 右键菜单 ──→ chrome.contextMenus.onClicked → editor.replaceSelection(SNIPPETS[id])技术栈分层清晰应用外壳层chrome.app.runtime/chrome.app.window/chrome.commandsbackground.js文件权限层chrome.fileSystem HTML5FileReader/FileWritereditor.js编辑交互层CodeMirror 2cm/内置发行包与页面布局main.html、style.css内容增强层上下文菜单注入代码片段snippets.js。运行与验证该示例位于仓库_archive/apps/samples/mini-code-edit/属 Manifest V2 时代的 Chrome Apps 形态manifest_version: 2运行方式为在 Chrome 的扩展程序页开启开发者模式→ 选择加载已解压的扩展程序 → 指向该目录随后从应用启动器打开 MiniCodeEdit。需要说明的适用前提示例声明minimum_chrome_version: 23依赖当时稳定的 Chrome Apps /chrome.fileSystem扩展 APIChrome Apps 已被 Chrome 官方逐步停用2021 年起 Chrome 93 之后不再支持 Chrome Apps因此该示例当前主要作为源码学习与 API 用法参考而不是可长期部署的运行时方案其核心知识点——chrome.fileSystem的读写权限模型、FileReader/FileWriter的读写闭环、CodeMirror 的按需模式加载与快捷键绑定——对现代 Web 应用与扩展开发如 MV3 中借助 offscreen 文档使用chrome.fileSystem依然具有直接的迁移价值。小结mini-code-edit用不到 400 行代码把语法检测 语法高亮 磁盘文件读写三个能力组合成了一个可用的代码编辑器是学习以下技能的极佳范本chrome.fileSystem权限模型openWritableFile/saveFile两种选择入口与hasWriteAccess状态机理解应用如何安全地读写用户指定文件语法检测与高亮扩展名 → CodeMirror mode 的映射表实现以及json: true、htmlmixed等模式的精细配置桌面化体验全局命令CtrlShift1、Cmd/Ctrl-S保存快捷键、上下文菜单片段注入、窗口尺寸约束与自适应布局。仓库中还保留着完整的 CodeMirror 2 源码包_archive/apps/samples/mini-code-edit/cm/下的doc/manual.html、test/、mode/、theme/读者可以继续深入 CodeMirror 的 API 文档与测试用例将该编辑器示例扩展出查找替换、折叠、Emacs/Vim 键位cm/keymap/等更多能力。【免费下载链接】chrome-extensions-samplesChrome Extensions Samples项目地址: https://gitcode.com/gh_mirrors/ch/chrome-extensions-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
