背景
typescript 编译器由 typescript 编写,所以可在浏览器端运行。monaco-editor 是一个 web 端可运行的 ide 编辑器内核。所以可以在浏览器端实现前端代码开发+编译,不需要服务端支持。
价值
可以更方便地开发一些简单的 H5/RN 页面、实现 SDK 业务库 playground 功能、封装业务逻辑支持特殊模块如编辑器集成内部登录系统,实现调用云端服务上传资源一键发布部署。
技术原理
1. monaco-editor 基本概念
vscode 架构图如下:
由内置类型定义、多平台启动器、vscode 核心层组成。
其中 vscode 核心层由以下几个部分组成:
- base layer 基础层:提供通用的基础工具和用户接口
- platform layer 平台层:提供服务注入能力和基础服务
- workbench layer 界面 UI 层:由 Electron 实现 UI 模块。
- editor layer 编辑器层:即 monaco-editor
monaco-editor 最初是作为 vscode 核心的编辑器层实现,后续才独立出来。
安装使用
需要同时安装 monaco-editor 和 monaco-editor-webpack-plugin
yarn add monaco-editor
yarn add monaco-editor-webpack-plugin -D
monaco-editor-webpack-plugin 需要配置到 webpack plugins。
editor
editor 是一个支持代码高亮、语法提示/检查、命令面板等的一个编辑器,和 vscode 代码编辑器基本一致。
创建一个编辑器:
// 使用 `monaco.editor.create` 来创建一个编辑器,
const editor = monaco.editor.create(document.querySelector('#root'), {
value: "function hello() {\n\talert('Hello world!');\n}",
language: 'javascript',
})
这样只是支持一个文件的编辑,如果想实现多个文件如 vscode 的 tab,就需要用到 model:
// 使用 `monaco.editor.createModel` 来创建一个model
// 每个文件都需要创建一个这样的model
const fileModel = monaco.editor.createModel(
"function hello() {\n\talert('Hello world!');\n}",
undefined, // language. infer from uri ext.
monaco.Uri.file('main.js')
)
// 将model设置到editor
const editor = monaco.editor.create(document.querySelector('#root'), {
model: fileModel,
wordWrap: 'on', // 代码超过屏幕显示自动换行
})
editor 还可以切换 model(当前编辑的文件),即 vscode 的 tab 切换,
// 使用setModel切换当前编辑的文件
editor.setModel(fileModel2)
这里的 setModel 对接 vscode 侧边资源管理器的文件树点击文件编辑。
- diffEditor
diffEditor 是一个支持文本差异对比的编辑器,多用于文件变更内容的查看,如 git diff。
创建对比前,先创建两个文件 model:
const originalFileModel = monaco.editor.createModel(
"console.log('hello')",
'javascript'
)
const modifiedFileModel = monaco.editor.createModel(
"console.log('hello world')",
'javascript'
)
然后创建一个 diff 编辑器:
const diffEditor = monaco.editor.createDiffEditor(container)
diffEditor.setModel({
original: originalFileModel,
modified: modifiedFileModel,
})
- language
monaco-editor 自带了 html、css、javascript、typescript、json 的编译器 worker,通过 monaco.languages 可以拿到各个语言的编译器,例如获取 typescript 编译器来编译 tsx 代码:
// 创建一个tsx文件
const fileModel = monaco.editor.createModel(
'const App = () => <div>hello</div>',
undefined, // language. infer from uri ext.
monaco.Uri.file('main.tsx')
)
// 获取ts编译器
monaco.languages.typescript.getTypeScriptWorker().then(async tsWorker => {
const client = await tsWorker(fileModel.uri)
// 获取文件编译后的内容
const result = await client.getEmitOutput(fileModel.uri.toString())
const files = result.outputFiles[0]
return files.text
})
- options
设置内置 ts 编译器的编译选项 tsconfig:
const typescriptDefaults = monaco.languages.typescript.typescriptDefaults
typescriptDefaults.setCompilerOptions({
jsx: monaco.languages.typescript.JsxEmit['React'],
target: monaco.languages.typescript.ScriptTarget['ES5'],
module: monaco.languages.typescript.ModuleKind['AMD'],
allowSyntheticDefaultImports: true,
esModuleInterop: true,
allowJs: true,
experimentalDecorators: true,
emitDecoratorMetadata: true,
downlevelIteration: true,
removeComments: true,
lib: ['dom', 'dom.iterable', 'esnext'],
})
- typescript ExtraLib
如果想要添加一个全局 d.ts 文件,可以使用 addExtraLib 方法:
const typescriptDefaults = monaco.languages.typescript.typescriptDefaults
typescriptDefaults.addExtraLib(`declare module "react" { }`, 'react.d.ts')
3. 借助 AMD 模块规范从 cdn 加载 npm 包
amd 规范是可以从 http 加载模块资源的,例如使用 requirejs 加载 axios:
require.config({
paths: {
axios: 'https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js',
},
})
require(['axios'], axios => {
console.log(axios.get)
})
利用 monaco-editor 配置 tsconfig 可以将 ts 代码编译到 amd 模块,然后再配置模块加载 amd/umd 资源 http 路径即可。前提是需要使用的 npm 包发布了 umd 版本。