第二章 实战案例:构建Markdown编辑器

本文所有源码均在:github.com/Sunny-117/e...

本文收录在《Electron桌面客户端应用程序开发入门到原理》掘金专栏

本文介绍

本章会从原理着手:

  • Markdown原理
    • 抽象语法树
    • 常见的 Markdown 实现原理
  • 使用 Electron 构建 Markdown 编辑器
    • 打开一个 Markdown 文档
    • 保存文档
    • 拖动一个文档到编辑器
    • ...

Markdown编辑器实现原理

Markdown简单回顾

Markdown实际上是一种轻量级的标记语言。

作为一门标记语言,Markdown 里面自然也会提供各种各样的标记:

  • 标题:通过 # 来创建
  • 强调 :通过 ** 或者 __ 来创建
  • 斜体 :通过 * 或者 _ 来创建
  • 链接
  • 图片
  • 代码
  • ....

关于 Markdown 更加详尽的语法,你可以参阅:www.markdownguide.org/basic-synta...

Markdown 最早出现在 2004 年,它的发明者是一个叫做 John Gruber 的人

在 Markdown 出现之前,人们要进行文档编辑,要么使用富文本编辑器,要么使用 HTML,但是这两种方式或多或少有一些缺陷:

  • 富文本编辑器:最常见的就是 Word,像 Word 这种富文本编辑器提供了一个所见即所得(WYSIWYG),但是这些编辑器生成的通常是特定格式的二进制文件,并非纯文本,意味着你要打开这些文件还是必须要用对应的富文本编辑器来打开。
  • HTML:本身就是用来创建网页,里面所提供的标记语法对于非技术人员来讲,是比较复杂的,并且可阅读性不强。

Markdown 的出现为我们的纯文本编辑带来了一种新的可能性:

  • 易读易写
  • 可扩展
  • 版本控制友好

Markdown实现原理

用户所书写的 Markdown 文本,最终是要被转换为 HTML 格式的。

编译相关的基础知识

什么是编译 ?

所谓编译(Compile),就是将一种语言 A 翻译成另外一种语言 B,其中语言 A,我们称之为源代码(Source code),语言 B 我们称之为目标代码(target code)。

编译的工作是由编译器来做,编译器本质上就是一段程序,或者你可以理解为一个函数:

js 复制代码
function compile(text){
  // ...
  return text2;
}

完整的编译过程,通常包含这么几个部分:

  1. 词法分析

词法分析是编译过程中的第一个阶段,它的任务就是将源码转换为一系列的词法单元(Token)

所谓词法单元,指的是最小的不可再拆分的单元:关键字、标识符、操作符、数字、字符串

例如:

java 复制代码
int x = 10 + 5;

经过词法分析,就会将上面源码中的 token 拆解出来:

arduino 复制代码
int(关键字)
x(标识符)
=(操作符)
10(数字)
+(操作符)
5(数字)
;(分号)
  1. 语法分析

这一个步骤主要就是根据上一步所得到的 token 形成一颗抽象语法树(Abstract Syntax Tree,AST)

AST 是一个树形结构,会将比较重要的 token 包含到树结构里面,会忽略不太重要的部分。

例如根据上面的代码,所形成的抽象语法树如下:

lua 复制代码
Declaration
   |
   +-- Type: int
   |
   +-- Identifier: x
   |
   +-- Assignment
         |
         +-- Addition
               |
               +-- Number: 10
               |
               +-- Number: 5
  1. 语义分析

这个阶段会去检查程序是否符合语法规则,确保你的程序在执行的时候,有一个良好的行为。

java 复制代码
int x = "hello";

像上面所举例子的错误,就是在语义分析阶段被检查出来。

  1. 中间代码生成

根据 AST 先生成一遍中间的代码

  1. 优化
  2. 目标代码生成

注意,从大的方面去分类的话,还可以分为两大类:

  • 编译前端(词法分析、语法分析、语义分析):通常是和目标平台无关的,仅仅负责分析源代码
  • 编译后端(中间代码生成、优化、目标代码生成):通常就和你的目标平台有关系

AST

AST 英语全称 Abstract Syntax Tree,翻译成中文叫做"抽象语法树"。

咱们这里可以采用分词的方式来理解它:抽象、语法、树

树其实是数据结构里面的一种,用于表示某一个集合的层次关系,每个节点都有一个父节点和零个或者多个子节点。

css 复制代码
    A
   / \
  B   C
 / \   \
D   E   F

树这种结构在平时做搜索、排序以及想要表示层次关系的时候,是用的比较多的。DOM、路由算法、数据库索引。

了解了什么是树之后,那么语法树的概念也就比较好理解,比如我们平时所写的代码:

js 复制代码
var a = 42;
var b = 5;
function addA(d) {
    return a + d;
}
var c = addA(2) + b;

对于 JS 引擎来讲,上面的代码实际上就是一串字符串:

js 复制代码
"var a = 42;var b = 5;function addA(d){return a + d;}var c = addA(2) + b;"

接下来 JS 引擎第一步仍然是分词,将上面的字符串提取成一个一个的 Token

scss 复制代码
Keyword(var) Identifier(a) Punctuator(=) Numeric(42) Punctuator(;) Keyword(var) Identifier(b) Punctuator(=) Numeric(5) Punctuator(;) Keyword(function) Identifier(addA) Punctuator(() Identifier(d) Punctuator()) Punctuator({) Keyword(return) Identifier(a) Punctuator(+) Identifier(d) Punctuator(;) Punctuator(}) Keyword(var) Identifier(c) Punctuator(=) Identifier(addA) Punctuator(() Numeric(2) Punctuator()) Punctuator(+) Identifier(b) Punctuator(;)

接下来根据上面所得到的 token 形成一颗树结构:

最后解释一下为什么要用抽象这个词。

抽象在计算机科学里面是一个非常重要的概念。

这里所指的抽象和现实生活中的抽象是有区别。

  • 现实生活:模糊、含糊不清、难以理解
  • 计算机科学:将关键部分抽取出来,忽略不必要的细节

我们在将 token 形成树结构的时候,只会关心诸如关键字、标识符、运算符这一类关键的信息,会忽略诸如空格、换行符这一类非关键信息。因此叫做抽象语法树。

抽象语法树这个概念非常的重要,但凡是涉及到转换的场景,基本上都是基于抽象语法树来运作

  • Typescript
  • Babel
  • Prettier
  • ESLint
  • Markdown

Markdown转换流程

整个 Markdown parser 的执行部分分为三步:

  • 词法分析:解析器会对用户的 Markdown 文本进行词法解析,将文本分割成一系列的 token.
  • 语法分析:根据上一步所得到的 token,构建抽象语法树。
  • 目标代码的生成:解析器遍历 AST,将每个节点转换为相应的 HTML 代码。

树结构

假设用户书写的 markdown 文本为:

markdown 复制代码
## 标题
这是一个段落。

- 列表项 1
- 列表项 2

这是第二个段落。

1. 西瓜
2. 哈密瓜

到目前为止我们已经能够将该文本进行分词,拆解成一个一个的 token

js 复制代码
[
  { type: 'heading', level: 2, text: '标题' },
  { type: 'paragraph', text: '这是一个段落。' },
  { type: 'list-item', ordered: false, text: '列表项 1' },
  { type: 'list-item', ordered: false, text: '列表项 2' },
  { type: 'paragraph', text: '这是第二个段落。' },
  { type: 'list-item', ordered: true, text: '西瓜' },
  { type: 'list-item', ordered: true, text: '哈密瓜' }
]

接下来我们就要根据上面的 token 形成抽象语法树的结构:

实现在线mini版Markdown

解析 Markdown 文本常见的两种方式:

  • Markdown 解析器
    • 词法分析(token)
    • 语义分析(ast)
    • 代码生成(html)
  • 正则表达式

例如这个正则:

shell 复制代码
/#{1}\s?([^\n]+)/g
  • g:全局搜索
  • \s :匹配任何的空白字符,包括空格、制表符、换行符之类的
  • ? :表示前面 \s 是可选的,意思是空白字符可以出现 0 次或者 1 次
  • ( ) : 表示一个捕获组,之后能够捕获小括号所匹配上的内容,之后能够通过 $1 获取到所匹配的内容
  • [^\n] :匹配除了换行符之外的任何字符
  • +: 表示前面所匹配的字符集可以出现一次或者多次

在使用正则这种方式来进行解析的时候,正则的顺序非常重要!

例如:

js 复制代码
[/#{1}\s?([^\n]+)/g, '<h1>$1</h1>'],
[/#{2}\s?([^\n]+)/g, '<h1>$2</h1>']

即便用户书写的二级标题,也会被解析为一级标题,因为一级标题的正则在前面,会优先被匹配上。

这里的解决方案有两个:

  • 调换正则顺序
  • 调整正则规则

关于正则表达式的书写和调试,介绍一个在线的工作 regex101: regex101.com/

构建Markdown桌面应用

SimpleMDE

SimpleMDE(Simple Markdown Editor)是一个轻量级、简单易用的 Markdown 编辑器。它基于 JavaScript 开发,可以轻松集成到网页和网页应用中。SimpleMDE 提供了丰富的功能和一个美观的用户界面,使得编辑 Markdown 文本变得便捷和高效。

官网地址:simplemde.com/

主要特点

  1. 易用的界面:
    • SimpleMDE 提供一个清晰、直观的用户界面,支持即时预览,让用户可以实时看到 Markdown 文本的渲染效果。
    • 它还包括一个工具栏,让用户可以快速访问常用的格式化选项。
  2. Markdown 编辑功能:
    • 支持标准的 Markdown 语法,如标题、列表、代码块、链接、图片等。
    • 支持一些扩展功能,如表格、任务列表和自定义渲染。
  3. 自定义和扩展性:
    • SimpleMDE 允许开发者自定义编辑器的行为和外观,例如通过 CSS 样式定制外观。
    • 它还提供了一些配置选项,以适应不同的使用场景。
  4. 集成和兼容性:
    • 可以轻松地集成到现有的网页中,只需简单的 HTML 和 JavaScript。
    • 兼容主流的现代浏览器。

因此,我们可以在我们的项目中安装 SimpleMDE

bash 复制代码
npm install simplemde

热更新

关于热更新,涉及到两个东西:

  • nodemon

    • 这是 Node.js 环境下的一个实用工具,用于检测到文件发生更改的时候自动重启应用程序。
    js 复制代码
    "dev": "nodemon --exec NODE_ENV=development electron . --watch ./ --ext .js"
  • electron-reload

    • 用于监听渲染进程下文件的改变
js 复制代码
// index.js
require("electron-reload")(__dirname);

meta

当我们在 window/index.html 文件中添加了如下的 meta 标签:

html 复制代码
<meta
  http-equiv="Content-Security-Policy"
  content="
      default-src 'self';
      style-src 'self';
      font-src 'self';
    "
/>

会发现 Markdown 编辑器不好使了,连加载都加载不出来。

究其原因,是因为该 meta 标签用于设置内容安全策略,全部指定的都是 self,所以无法从其他域来加载内容。

要解决这个问题也很简单,只需要将自己能够信任的域添加上去就 OK 了

html 复制代码
<meta
  http-equiv="Content-Security-Policy"
  content="
        default-src 'self';
        style-src 'self' 'unsafe-inline' https://maxcdn.bootstrapcdn.com https://cdn.jsdelivr.net;
        font-src 'self' https://maxcdn.bootstrapcdn.com https://cdn.jsdelivr.net;
        connect-src 'self' https://cdn.jsdelivr.net;
        img-src 'self' https://xiejie-typora.oss-cn-chengdu.aliyuncs.com;
    "
/>
  • style-src 'self' 'unsafe-inline' https://maxcdn.bootstrapcdn.com https://cdn.jsdelivr.net;

    • self : 允许加载同源的样式
    • unsafe-inline:允许使用内联样式
    • https://maxcdn.bootstrapcdn.com https://cdn.jsdelivr.net : 允许从该源上面加载样式
  • font-src : 和字体相关的内容安全设置

  • img-src :和图片相关的内容安全设置

本文所有源码均在:github.com/Sunny-117/e...

「❤️ 感谢大家」

如果你觉得这篇内容对你挺有有帮助的话: 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。觉得不错的话,也可以阅读 Sunny 近期梳理的文章(感谢掘友的鼓励与支持 🌹🌹🌹):

我的博客:

Github: https://github.com/sunny-117/

前端八股文题库: sunny-117.github.io/blog/

前端面试手写题库: github.com/Sunny-117/j...

手写前端库源码教程: sunny-117.github.io/mini-anythi...

热门文章

专栏

相关推荐
今天也想MK代码2 天前
在Swift开发中简化应用程序发布与权限管理的解决方案——SparkleEasy
前端·javascript·chrome·macos·electron·swiftui
yqcoder2 天前
electron 中 ipcRenderer 的常用方法有哪些?
前端·javascript·electron
yqcoder2 天前
electron 中 ipcRenderer 作用
前端·javascript·electron
伍嘉源3 天前
关于electron进程管理的一些认识
前端·javascript·electron
yqcoder3 天前
electron 设置最小窗口缩放
前端·javascript·electron
yqcoder5 天前
区分 electron 全屏和最大化
前端·javascript·electron
li.siyuan5 天前
electron + vue 打包完成后,运行提示 electrion-updater 不存在
前端·vue.js·electron
努力挣钱的小鑫5 天前
【客户端开发】electron 中无法使用 js-cookie 的问题
前端·javascript·electron
蓝胖子不是胖子5 天前
尝鲜electron --将已有vue/react项目转换为桌面应用
vue.js·react.js·electron
非晓为骁5 天前
windows 下 electron-builder ERR_ELECTRON_BUILDER_CANNOT_EXECUTE 报错处理
javascript·windows·electron