Skip to content

Latest commit

 

History

History
131 lines (73 loc) · 11.8 KB

项目介绍.md

File metadata and controls

131 lines (73 loc) · 11.8 KB

图雀 -- 基于 Git 技术实战教程写作工具

问:请你介绍一下这个项目。

答:这个项目是利用 Git 追踪代码提交的特性来改进实战技术教程的写作,包含两个部分:1)一个命令行工具 tuture 2)和一个能够组织写作内容的 Web 端的编辑器 tuture-editor。主要流程是使用命令行工具 tuture 提供的 init 命令将一个现成的 Git 代码仓库初始化为一个 Tuture 教程,然后通过 tuture up 打开一个 Web 端编辑器用于写作。我主要负责开发 Web 端的编辑器,使用 TypeScript + React + Rematch 开发界面,这个 Web 端的编辑器的功能是能够将从 Git 提取出来的代码 Diff 以一个 Commit 为一个步骤进行展示,每一个 Commit 就是教程文章中的一个小节,然后为这一步提交修改的代码提供类似 Github 那样的 Diff 展示,接着提供前后的文字输入框,这个输入框是一个类似语雀、飞书文档那样的富文本+Markdown一体的编辑器,编辑器使用 Slate 富文本编辑器框架开发,支持热键、快捷键、工具栏等三种方式触发渲染,最后写完了文字之后,能够通过命令行工具构建成 Markdown 文件然后分享到技术社区,如掘金、知乎、思否。目前已经用这个工具写作了 40 多篇教程,在掘金获得了近 30万+ 的阅读量,此外,这个工具还开开源在 Github,目前有 140 颗星。

问:编辑器开发过程中的原理是怎么样的?

类似 React 对 Web 开发的抽象一样,界面与状态一一对应。Slate 也是将编辑器界面的开发和状态一一对应,具体来说就是维护了一个和 DOM 对应的 JS 对象树,这个对象树上的每个节点包含 type/props/children 属性,编辑器界面的开发就是通过 Slate 提供的一系列 API,比如说 Transform,Editor,Range 等,来对这些节点进行选中、以及增删改查。接着通过 slate-react 包将这颗 JS 对象树根据对应的渲染函数渲染成真正的 React 组件,然后呈现给用户,比如说代码块、引用块。

除此之外主要修改 JS 对象树的过程包含下面几点:

  • 通过响应用户的事件,如 keydown 等来修改
  • 通过插件机制,Slate 暴露出了一系列内部的事件,如 deleteBackward/insertBreak/insertText 等,然后允许用户去编写一个个这样的插件,然后去完成特定逻辑,接着传给下一个插件,顺序执行,增强 Slate 的功能。

正是通过这种事件+插件的机制,我们就可以去完成如快捷键、热键和工具栏操作来辅助 Markdown 的快速写作。

问:在开发过程中,遇到了那些难点?如何解决这些难点的?

  • 第一个难点就是,Slate 处于 Beta 阶段的库,只提供基本的一些操作,例子也是比较简单的功能实现,而对于代码块渲染、语言选择、代码高亮等需要自己摸索
  • 第二个难点就是,当功能复杂时,比如有 20 多个插件,各种事件处理等,因为 Slate 没有提供约定的项目结构和处理流程,所以导致事件处理和插件封装很乱,遇到错误很难定位
  • 第三个难点就是,当样式复杂的时候,或者内容特别长的时候,这颗 JS 对象会非常大,嵌套也会非常深,这会引起严重的渲染性能问题,造成界面掉帧。

第一个难点

Slate 提供了一些有现成参考的成型的例子,研究它们的源码,可以对应的借鉴,然后还有一些无法借鉴的,就需要研究 Slate 的源码处理过程,去做对应的扩展,最后将整个 Slate 和社区的一些比较有名的例子的源码都看了一边,能借鉴的借鉴,不能借鉴的就需要自己摸索的自研 。

第二个难点

  • 针对这些问题,基于 Slate 封装了一个包,叫做 editure,它清晰的分配了包括插件、序列化操作、光标操作、快捷键、热键操作以及对节点类型(Block 和 Mark)的处理,使得相关的功能便于维护和编写、排错
  • 然后进行了比较完善单元测试的覆盖,使用 Jest 来测试,实现了 97% 的单元测试覆盖率,大大降低了排错耗费的时间。

第三个难点

  • 首先在界面上允许用户收到对教程进行拆分,把一个项目里面的所有提交拆分成几篇教程,这样在每次只需要加载对应这篇教程的那几个提交,相应的 JS 对象树就要小很多,这样改进了一部分性能。
  • 第二个措施,是通过将整个 JS 嵌套对象树做一次 flatten 操作,将其压平渲染,然后等到内容修改写回原文件的时候再 unflatten 保存,这个又带了一次性能的提升,到这里如果不考虑巨长的代码 Diff 的渲染的话,其实性能已经还可以了
  • 第三个措施,对于涉及到几十上百行的代码 Diff 的展示,使用懒加载,等到代码块实际出现在视口的时候再加载,这样又可以优化一下首次加载性能
  • 第四个措施,就是对一些不长变化的组件使用 memo 缓存起来

问:在这次开发过程中,你学到了那些东西?

  • 因为整个项目比较大,所以在代码项目组织上我学会了使用 lerna 等现代化的工具组织项目
  • 因为编辑器的编写内部状态非常复杂,所以要学会抽象出一些不变的东西来控制状态的复杂度,比如做项目结构约定
  • 且编辑器的状态复杂,所以需要编写完整单元测试,提高测试 覆盖率,避免多次重复排错
  • 在这个过程中我还学会了如何抽离复杂的逻辑,然后发布成单独的包
  • 对前端的复杂项目的编写、排错、性能优化都有了非常深刻的认识,能够在没有现成工具的情况下自己造轮子解决问题

项目

  • cli
  • editor
  • local-server
  • core

如何开发

  • 使用 Lerna 做多包项目的管理
  • 使用 ESLint、Editorconfig、Prettier,结合 Husky,lint-staged,和 PR + Code Review 机制来规范代码的并行开发
  • 使用 rollup 来打包项目代码,是一个 ES 模块打包器,更适合打包库或者包
  • 具有良好的 README 和项目文档

核心难点

  • 可以直接在正常的项目里面初始化一个教程,而不需要影响原项目,并且还能具有流畅且跟随项目代码变化即使更新的写作体验,所以设计了一套写作流程:
    • 主要命令如下:
      • tuture init 用于将一个 Git 项目初始化成为一个 tuture 教程,主要会做如下工作:
        • 检查是不是 Git 项目,不是的话就询问是否要初始化成 Git 项目
        • 提取 Git 的提交记录,创建一个新文件夹 .tuture,然后将提取的代码内容变化保存为 diff.json,同时创建一个用于保存写作内容的 collection.json,以及一个资源文件夹用于保存上传的图片
        • 添加 .gitignore 规则,忽略 .tuture 文件夹
      • tuture up 用于在项目目录下打开 tuture 写作工具服务器以及在自动打开浏览器访问本地的 Web 界面写作编辑器,这个编辑器会渲染所有的 Diff 代码,并提供文章组织能力以及一个类似语雀和飞书文档的富文本和 Markdown 一体的编辑器用于基于 Git 代码的变化和项目的进展来撰写对应的实战教程,并且撰写的内容自动保存到 .tuture 文件夹下对应的 collection.json。
        • 且这个服务器开启的过程中还会通过 fs.watchFile 去监听本地的 .git 里面对于内容的提交变化,一旦有新的提交,就会 tuture reload 吧新的代码 diff 内容添加到 .tuture/diff.json 里面,同时也会更新 collection.json,追加对于的新的步骤。
      • tuture commit :背后基于 git commit,基于 master,新建本地的 tuture 分支,然后将 .tuture 文件夹里面的文件移动到项目中,并加上 tuture 前缀的提交信息
      • tuture sync :自动将远程的 tuture 分支内容新的修改合并到本地 tuture 分支,此时可能需要处理冲突,然后本地的 tuture 分支上传到远程,这条分支是实现多人以分布式协作写作的关键,类似基于 maste 分支的代码协作开发模型,这里可以是基于 tuture 分支的多人协作写作模型。
      • tuture build :将写作的内容构建成 markdown 文件,方便分享到其他技术社区如掘金、思否、知乎等, 这个过程会自动处理图片等文件、上传到图床,使用了图壳。
  • 其次就是类似语雀和飞书文档的富文本和 Markdown 一体的编辑器,基于富文本编辑器框架 Slate 开发,支持快捷键、热键和工具栏来辅助写 Markdown,支持复制 HTML、Markdown 文本自动解析并渲染,支持图片上传自动上传到图床等。
    • 项目是基于 React + TypeScript 的,然后使用了 Rematch 的 Redux 框架来做状态管理,样式使用了 emotion
    • 编辑器本身
      • slate 提高了用于进行节点操作(Transform)(修改属性,选中节点)、光标移动的功能,并提供了一个插件系统,允许用户编写接口,去接管删除、插入某段内容,或者应用某段样式的操作,然后应用修改并返回内容。
      • 同时 slate 提供一个基于 React 的绑定库,slate-react,暴露了一个类似 Provider 的 Slate,然后一个用于监听和处理用户事件的 Editable 组件,在上面可以决定如何将数据渲染到 React 节点,比如 Block 和 Mark 型节点
    • 难点1:
      • 数据量大的时候,比如有 10-20 个提交,并且代码很长,内容写了很多的时候,会发生卡顿,掉帧等现象。
    • 如何优化1?
      • 可以允许用户进行分页,即将一个项目的多个 Commit 分页,每一个包含 1 到多个 commit,然后独立展示,每次获取只属于这个页面的相关的数据,大大减少了一次需要渲染的数据,优化了性能,同时提升了写作体验。
      • 因为 Slate 的数据结构是模仿 DOM 的 JS 对象树,所以当内容比较复杂,比如引用块里面嵌入代码块,或者引用块里面的文本又有各种内联样式如加粗、斜体等,那么节点的嵌套可能非常深,所以渲染会比较慢,因为要递归的渲染子节点,所以我对数据做了 flatten 和 unflatten,即把每次要渲染的数据扁平化,然后渲染,等到实际保存数据的时候,unflatten。
      • 接着还存在一个比较严重的问题就是,一个文件的代码长度特别长,如果一开始没有出现在视口,但是却加载和渲染了也会影响性能,所以对于那些没有出现在视口的代码块,做了懒加载优化,具体就是对没有出现视口的代码块,渲染一个空的元素,等到实际滑动到这个位置再实际渲染内容。
      • 还有一个大量优化性能的就是一个是 rematch 提供的 reselect 插件对数据继续的缓存,不变的数据直接返回不需要经过计算,以及对组件使用 memo,把不常变化的组件缓存起来,这也提高了性能。
    • 难点2:
      • Slate 并没有提供一些约定的项目结构,一旦功能复杂,插件多起来之后,就会显得状态很乱,而且它的插件中不能处理事件,一旦需要处理多个事件,或者多个功能需要共用一个事件的处理函数,整个代码就会一团糟
    • 如何优化2?
      • 自己单独写了一个包,editure,底层基于 Slate,然后基于它做了扩展,将 序列化模块、光标操作模块、快捷键模块、事件处理模块、插件模块等都以良好的约定分离出来,然后合理调用
      • 为实际项目抽象出了一个编辑器必要的一些聚合操作,将复杂性屏蔽在包里。
      • 进行了单测的测试覆盖,大概有 95%,使用 Jest
        • describe
        • test
        • expect