手摸手打造类码上掘金在线IDE(二)——编辑器篇

2,904 阅读9分钟

image.png

前言

不熟悉的朋友可能不知道,我叫老骥,前端切图仔,单位内卷,疯狂加班

在上一篇的文章中,我们详细介绍了 在线IDE的优劣势, 市面上的在线IDE种类,IDE的大致的实现方式,以及简单的实现原理

算是水了一篇吧, 属于是,主要介绍了, 理论性的东西 ,可谓,听君一席话,如听一席话

听着好有道理,实则并没有什么卵用,

第二篇了,得直入正题了,接下来跟大家一块实现一个残废版——码上掘金

钻研原型

所谓知己知彼,才能百战百胜, 既然要抄码上掘金, 那么我们就要了解东家,也就是官方版本-- 这个项目的结构

整个码上掘金, 从大块上包含两个部分

IDE跟用户交互部分

跟用户交互部分,自不必过多介绍, 大致,就是那老几样,crud而已,比如:包含 发布,当前用户发布列表 ,收藏,点赞 , fork 代码,布局,等等功能

听到这,相信各位jym 脑袋蹭蹭蹭全是画面,干业务不天天都是这玩意吗

确实,坦率的讲,对于一个技术项目,这个东西在技术难度层面,就是侮辱人的智商的,很多人都对他嗤之以鼻, 很是不屑

然而,我想说的是,在我们的日常工作中,很多人都是都是靠着这么多crud 去养活没有这些东西,一个产品就不能是一个好产品,

这也东西也是有着很重要的业务方面的思考的,他的业务复杂度,也很高深。 他的重要程度其实并不比技术差

举个例,比如我这个页面的ui 风格,怎样影响用户的留存,再比如,什么样的布局,能提升用户体验,

还比如,增加怎样的交互,能增加用户的易用性

其实这些都需要思考的,他的难度也不比技术差, 给这些琢磨明白了,你也能值钱,并不一定是技术好

所以,我就想在这里发出一个疑问?在我们这个行当,技术好一定是绝对优势吗

不一定, 他还跟你您的性格,能耐,运气,选择有很大的关系,所以,别人下次,叫你大佬,您一定要有空杯心态

您要知道,三人行,必有我师,您要知道,别人夸你,可能就是商业互吹,一说一乐

我就发现,很多人,有点技术, 就不知道自己是谁了,行为张狂, 表情夸张,天天好为人师, 指手画脚

其实你就是个井底之蛙,垃圾键盘侠 。。。。

到这,你可能觉得我在批评那些 除了技术别的一概听不进去的伪大佬,

呃,其实,这里只是为了告诫自己,警醒自己,三省吾身,请大家不要对号入座。。。

到这,你以为我在虚怀若谷?

呃,其实,我也可能在为了挣钱不要脸的凑字数

总结起来就是一句话, 互联网江湖,真真假假,虚虚实实,大家一定要,提高警惕,擦亮双眼,

走正确的路,学正确的技术,干正确的事。

至于是什么是正确?

俺以为,大多数人走的路,那就是正确,不要想着标新立异,这也会那也会,有一项安身立命的资本即可。

对于俺,干好vue这一家,走到哪里都不怕!

所以,俺这个残废版——码上掘金,还用vue

git地址如下: 残废版--码上掘金

(最近单位比较忙,后期慢慢给代码补上)

额,有点跑题了,但刚才着实痛快了一把,说了点心里话,

我们言归正传,继续往下走

说完了,交互部分,在此在此强调一下,这一部分也很重要,这一部分也是您安身立命的资本之一

只是,不是我们的主要研究内容,所以暂时按下不表!

下面高潮开始,上主菜

IDE主体部分

码上掘金,从结构上来说只有三个部分,分别是编辑器部分,渲染编译器部分,以及 错误提示控制台部分

由于他的初心是是为了轻便,简洁,所见即所得, 所以省略了文件系统

那,既然这样的话,我们也不需要了吧, 毕竟残废版

其实,我在之前的文章中写了个文件系统

git 地址如下,有兴趣的jym 可自取

tree list

接下来,我们一个个梳理他的这几个模块

编辑器部分

东家的编辑器部分,可谓非常传统 ,他用微软干了很多年头的在线编辑器-也就是vscode 的前身

monaco-editor 这玩意什么都好,毕竟是vscode 的祖宗,就是文档,是真费解啊

当然你也可以另辟蹊径,找了另一个极端codemirror5

这也是跟monaco-editor 可以分庭抗礼的编辑器,支持语言众多而且接入方便,文档,齐全, 虽然也是英文,

可我们有翻译软件啊

而在,在社区繁荣的今天,更是有大佬在他的基础上做出了专门用于vue 的定制版本vue-codemirror

使得我们的接入更加方便, 如此一来,挣钱也就更容易了!

有很多jym 对这个一块可能还相当陌生,那么我们就来分别对这两个编辑器的使用方式来一个简单的介绍

monaco-editor

monaco-editor 虽然也有vue的版本接入 vue-monaco-editor

但是目前在社区的认可度还不够高,所以暂时不要不要使用

我们还是使用原始的接入方法


// 引入 monaco-editor

<template>
    <div id="codeEditBox"></div>
</template>
<script lang="ts" setup>
import * as monaco from 'monaco-editor'
import { onMounted, ref } from 'vue'
const editor = ref(null)
const language = ref("html")
const initEditor = () => {
    // 初始化编辑器,确保dom已经渲染
    editor.value = monaco.editor.create(document.getElementById('codeEditBox'), {
        value: '', //编辑器初始显示文字
        language: language.value, //语言支持自行查阅demo
        theme: 'vs-dark', //编辑器主题
        selectOnLineNumbers: true,//显示行号
        roundedSelection: false,
        readOnly: false, // 只读
        cursorStyle: 'line', //光标样式
        automaticLayout: true, //自动布局
        glyphMargin: true, //字形边缘
        useTabStops: false,
        fontSize: 15, //字体大小
        autoIndent: 'full', //自动布局
        quickSuggestionsDelay: 100, //代码提示延时
    });
}
onMounted(() => {
    initEditor()
})
</script>
<style>
#codeEditBox {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    height: 400px;
    padding: 0;
    overflow: hidden;
    margin-bottom: 15px;
}
</style>

将以上代码导入vue 即可初始化编辑器,然而比较坑的是,他还需要导入一些包和做一些配置,来运行编辑器, 不然会出现以下错误

image.png

万幸的是,社区的力量是伟大的,他们有针对webpack的插件,自动导入。来省去了自己配置的繁琐


// vue.config.js
const path = require('path')
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')

module.exports = {
  
    lintOnSave: false,
    productionSourceMap: false,
    configureWebpack: {
   
        plugins: [
            new MonacoWebpackPlugin({
                languages: ['css', 'html', 'javascript', 'less', 'pug', 'scss', 'typescript', 'coffee']
            })
        ]
    }

当然还有vite 版本的

//vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from "path";
import monacoEditorPlugin from 'vite-plugin-monaco-editor';
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), monacoEditorPlugin.default({})]
})

这里需要注意的是,在高版本的vite中 有个esm的bug ,所以需要手动添加default

能跑通编辑器之后,我们就需要来接入主题美化了在vscode中俺以为最美的主题莫过于OneDarkPro

于是,俺在网上找到了他的移植版本

image.png

其实就是一堆的配置文件,我们只需要引入即可

import { json } from 'OneDarkPro.js'
import * as monaco from 'monaco-editor'

// 安装主题

 monaco.editor.defineTheme('OneDarkPro', json)

然后引入之后你就会发现变成了这样

image.png

还不如官方主题,其实我们要做的还有一步, 关联语法,由于我们是要使用vscode 语法,但是vscodemonaco-editor 本质上又不是一个东西

vscode 使用的是 vscode-textmate

来解解析,做的关联,但是monaco-editor这玩意没有啊?

那么问题来了? 我们要怎么关联怎么解析呢?

好在,社区的力量是强大的,我翻了codesandbox的源码

在他的源码中找到了蛛丝马迹

monaco-textmate

这个库,专门用来解析monaco-editor 他的功能类似于vscode-textmate

但是,他们俩虽然配对成功了,但是却还有层窗户纸没有捅破,他们还没有建立连接

于是同样还是这个大佬(在此我放上他的github:Neek Sandhu

又做了个插件 monaco-editor-textmate

将他们关联起来了!

有了大佬的贡献,我们说干就干

在开始之前,我们还需要一样东西onigasm

这个东西简单的来说,就是一个web版本的正则表达式的库 ,他脱胎于c语言编写Oniguruma

简单的来说,就是将 Oniguruma 编译为 WebAssembly

WebAssembly可能很多人都比较陌生

简而言之,他就是能在浏览器直接跑的非js代码,这个玩意非常神奇, 他让在浏览器跑node 成为了可能。

有了这些,我们就可以开始干了

我们就以vue文件的主题为例,少废话,先看东西:

image.png

效果基本可vscode一模一样了

首先,我们需要引入用到的字体包

// 引入主题包

import { json } from '../assets/themes/OneDarkPro'
import html from '../assets/grammars/html.tmLanguage.json'
import css from '../assets/grammars/css.tmLanguage.json'
import js from '../assets/grammars/JavaScript.tmLanguage.json'

// 引入解析器包
import { loadWASM } from 'onigasm'
import { onMounted, ref } from 'vue'
import { Registry } from 'monaco-textmate'
import { wireTmGrammars } from 'monaco-editor-textmate'

然后导入这个web 版本的正则,配置运行环境

  await loadWASM(`/onigasm/onigasm.wasm`)
  // 这里按需加载不同的运行环境
   window.MonacoEnvironment = {
        getWorkerUrl: function (moduleId, label) {
            hasGetWorkUrl = true
            if (label === 'json') {
                return 'monaco/json.worker.bundle.js'
            }
            if (label === 'css' || label === 'scss' || label === 'less') {
                return 'monaco/css.worker.bundle.js'
            }
            if (label === 'html' || label === 'handlebars' || label === 'razor') {
                return 'monaco/html.worker.bundle.js'
            }
            if (label === 'typescript' || label === 'javascript') {
                return 'monaco/ts.worker.bundle.js'
            }
            return 'monaco/editor.worker.bundle.js'
        }
    }

然后确定更改默认的主题解释器即可


  const grammars = new Map()
    grammars.set('html', 'text.html.basic')
    const registry = new Registry({
        getGrammarDefinition: (scopeName: string) => {
            return new Promise((resolve) => {
                resolve({
                    format: 'json',
                    content: map[scopeName]
                })
            })
        }
    })
    monaco.languages.register({ id: 'html' })
    let loop = () => {
        if (hasGetWorkUrl) {
            Promise.resolve().then(async () => {
                await wireTmGrammars(monaco, registry, grammars, editor.value)
            })
        } else {
            setTimeout(() => {
                loop()
            }, 100)
        }
    }
    loop()

完整代码如下

<template>
    <div id="codeEditBox"></div>
</template>
<script lang="ts" setup>
import { json } from '../assets/themes/OneDarkPro'
import html from '../assets/grammars/html.tmLanguage.json'
import css from '../assets/grammars/css.tmLanguage.json'
import js from '../assets/grammars/JavaScript.tmLanguage.json'
import * as monaco from 'monaco-editor'
import { loadWASM } from 'onigasm'
import { onMounted, ref } from 'vue'
import { Registry } from 'monaco-textmate'
import { wireTmGrammars } from 'monaco-editor-textmate'

const map = {
    'text.html.basic': html,
    'source.css': css,
    'source.js': js,
}
const editor = ref(null)
let hasGetWorkUrl = false
const language = ref("html")
const initEditor = () => {
    // 初始化编辑器,确保dom已经渲染
    editor.value = monaco.editor.create(document.getElementById('codeEditBox'), {
        value: ``, //编辑器初始显示文字
        minimap: {
            enabled: false // 关闭小地图
        },
        language: language.value, //语言支持自行查阅demo
        theme: 'OneDarkPro', //编辑器主题
        selectOnLineNumbers: true,//显示行号
        roundedSelection: false,
        cursorStyle: 'line', //光标样式
        automaticLayout: true, //自动布局
        glyphMargin: true, //字形边缘
        useTabStops: false,
        fontSize: 18, //字体大小
        autoIndent: 'full', //自动布局
        // quickSuggestionsDelay: 100, //代码提示延时
        fontFamily: 'MonoLisa, monospace',
        contextmenu: false, // 不显示右键菜单
        fixedOverflowWidgets: true, // 让语法提示层能溢出容器
    });

}
onMounted(async () => {
    await loadWASM(`/onigasm/onigasm.wasm`)
    window.MonacoEnvironment = {
        getWorkerUrl: function (moduleId, label) {
            hasGetWorkUrl = true
            if (label === 'json') {
                return 'monaco/json.worker.bundle.js'
            }
            if (label === 'css' || label === 'scss' || label === 'less') {
                return 'monaco/css.worker.bundle.js'
            }
            if (label === 'html' || label === 'handlebars' || label === 'razor') {
                return 'monaco/html.worker.bundle.js'
            }
            if (label === 'typescript' || label === 'javascript') {
                return 'monaco/ts.worker.bundle.js'
            }
            return 'monaco/editor.worker.bundle.js'
        }
    }
    monaco.editor.defineTheme('OneDarkPro', json)
    monaco.editor.setTheme('OneDarkPro')
    initEditor()
    const grammars = new Map()
    grammars.set('html', 'text.html.basic')
    const registry = new Registry({
        getGrammarDefinition: (scopeName: string) => {
            return new Promise((resolve) => {
                resolve({
                    format: 'json',
                    content: map[scopeName]
                })
            })
        }
    })
    monaco.languages.register({ id: 'html' })
   // 这里为了防止worker还没加载完就执行,所以加了个延时
    let loop = () => {
        if (hasGetWorkUrl) {
            Promise.resolve().then(async () => {
                await wireTmGrammars(monaco, registry, grammars, editor.value)
            })
        } else {
            setTimeout(() => {
                loop()
            }, 100)
        }
    }
    loop()
    // Promise.resolve().then(async () => {
    //     await wireTmGrammars(monaco, registry, grammars, editor.value)
    // })
})
</script>
<style>
#codeEditBox {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    height: 100vh;
    padding: 0;
    overflow: hidden;
    margin-bottom: 15px;
}
</style>

vue-codemirror

在介绍vue-codemirror之前,我们先来介绍

codemirror 这个老牌编辑器

CodeMirror 是通过 JavaScript 实现的文本编辑器。专门用于编辑代码,带有大量的语言模式和实现更高级的插件功能。

拥有丰富的编程 API 和 CSS 主题化系统可用于定制 CodeMirror ,使它更适合你的应用和扩展新功能。

现在他已经跟新到了codemirror5

vue-codemirror其实就是在他的基础上做了个vue 的封装

接下来我们就直接使用vue这个版本来封装一个属于我们的编辑器

用到的包相对于monaco-editor 就简单很多了
主要包含:

  • 编辑器包vue-codemirror
  • 主题包 @codemirror/theme-one-dark 自带暗黑主题
  • js 语言包@codemirror/lang-javascript
  • css 语言包@codemirror/lang-css
  • html 语言包 @codemirror/lang-html
  • json 语言包 @codemirror/lang-json
  • markdown 语言包 @codemirror/lang-markdown

组件代码如下

<template>
    <div class="box">
        <Codemirror style="font-size: 16px;" ref="CodemirrorRef" :modelValue="code" :style="{ height: '100%' }"
            :autofocus="true" :indent-with-tab="true" :tabSize="2" :extensions="extensions" @change="change">
        </Codemirror>
    </div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Codemirror } from 'vue-codemirror'
import { oneDark } from '@codemirror/theme-one-dark'
import { javascript } from '@codemirror/lang-javascript'
import { css } from "@codemirror/lang-css";
import { html } from "@codemirror/lang-html";
import { json } from "@codemirror/lang-json";
import { markdown } from "@codemirror/lang-markdown";
const extensions = [html(), oneDark]
const code = ref('')
const CodemirrorRef = ref(null)
const change = (e) => {
    console.log(e)
}
</script>
<style>
.box {
    width: 100%;
    height: 100vh;
}
</style>

image.png

效果如下,看着也还可以,虽然比不上monaco-editor,但是好在简单啊,大大降低了使用门槛

然而,我是那种喜欢安逸的人吗?所以咱们的残废版码上掘金,我要毅然决然的选择 monaco-editor

毕竟有难度,才够装x,面试官才能能佩服,佩服才能高薪,高薪才能走向人生巅峰!

总结

我们本期解决了编辑器选型问题,接下来,就要开始做编译器,的处理了 ,

欲知后事如何,且听下回分解,其实我也想这回分解的,但是东家不让啊!