Skip to content

组装个支持记笔记的CodePen

前言

emmm。。。,有好长一段时间没码文了(近几个月实在是太忙了),这个玩具刚好是这两周抽空的拿出来和大家分享一下

朋友最近刚学前端,经常问一些问题,通过聊天软件发代码和贴图实在是不太方便,就给它推荐了CodePen🐴上掘金

前者国内访问实在不稳定,后者emm有个内容审核,导致内容实时性较差。

就搜了搜GitHub上有不有类似的项目,搜了一圈还真有不少,这里贴两个感觉不错的 CodeRunCodePan

朋友使用后反馈,问了问有不有啥支持直接在旁边 写笔记贴图片(这个),方便做记录

emm...,检索了一圈记忆中除了 VsCode 好像还真没有这种东西(可能是我孤陋寡闻了,读者有推荐的可以评论区补充)

那就给他造个吧,练练手,有段时间没写自己的代码了

等不及了?先体验体验👉🏻 示例1:应用首页示例2:分享代码

技术选型

行动肯定是要站在巨人的"键盘上"(手滑打错了,那就这样吧),先看看有哪些可用的 "零件"

内容编辑器

先是写笔记部分,挑了几个库玩了一下

  • wangEditor - 开源 Web 富文本编辑器,开箱即用,配置简单
  • prosemirror - 支持部分MarkDown语法,可拓展定制的富文本编辑器
  • 🧴 Lotion - 基于Vue3开发的块编辑器
  • Editor.js - 官方:"Next generation block styled editor.",下一代块编辑器

因为屏幕中需要展示 笔记/代码/预览 3个部分,直接使用markdown语法会有个切换的动作(这里就不考虑markdown格式做编辑了)

上述的几个库分大体上分为两类富文本编辑器块编辑器

富文本编辑器块编辑器
图片图片

前者是比较传统的编辑器,后者从社区反应来看像是下一代趋势,但国内好像还没看到使用此方案的成熟产品(可能是我孤陋寡闻了,读者有推荐的可以评论区补充)

最后本着技术尝鲜(喜欢折腾)的精神选了Editor.js,官方提供了一系列的基础插件,提供了简单的API支持自定义的插件(后面单独开文章介绍)

代码编辑器

这个就monaco-editor 没得跑了

不过在使用之前先看了一下最近阿里开源的OpenSumi,看之前以为是个可直接用的NPM包,文档翻了半天,只给了个demo,emm 拉下来,果然如官方预料 卡在了 yarn install,感觉有一定上手成本,先不看了

后端部分

思考了一下都是简单的CRUD场景,存储和鉴权MongoDBRedis感觉就够了(也没有配置成本,安装即用)

服务端框架部分就直接拿自己的之前写的玩具flash-wolves开整

接下来就是组装了

实现

项目搭建

粗糙的原型图如下

图片

布局也比较简单

html
<header>工具条...</header>
<!-- 主体内容 --> 
<main>
    <Note />
    <Code />
    <Render />
</main>

直接拿之前整的模板进行创建vite-vue3-template出前端工程

文本编辑部分

示例代码在线演示:https://code.sugarat.top/share/630b8793789c242a31e57c40

直接参照官方文档,CV起来就能运行,这里仅仅贴几个关键部分(避免代码占用太大篇幅,降低文章可读性)

ts
 const editor = new EditorJS({
    holder: 'note-editor',
    placeholder: '在这里开始记录你的笔记',
    onReady: () => {
      console.log('Editor.js is ready to work!')
      // 内容初始化
      // 下方拿到的outputData直接塞进来就行
      // 可以在页面加载的时候从数据库取历史数据进行展示
      editor.render(xxx)
    },
    onChange: (api, e) => {
      editor
        .save()
        .then((outputData) => {
            // 取得编写的内容
            // 可以将这个内存存起来
            // 在合适的时机调接口存入数据库即可
        })
        .catch((error) => {
          console.log('Saving failed: ', error)
        })
    },
    tools: {
      // 图片处理
      image: {
        class: Image,
        config: {
          uploader: {
            uploadByFile(file: File) {
              // 需要自己处理图片上传逻辑 
              // 按结构进行返回即可
              return {
                success: 1,
                file: {
                  url: 'https://img.cdn.sugarat.top/online-editor/6302403434e52962875fbf3e/1661169105550/pupza3m486'
                }
              }
            }
          }
        }
      },
    // 一系列官方插件
      header: Header,
      list: List,
    //   ...
    },
    i18n: {
      // 国际化相关配置
    }
  })

代码编辑

参照官方文档,几步就起飞,直接调用 monaco-editor 进行初始化,以HTML编辑器为例

js
import * as monaco from 'monaco-editor'

const htmlEditor = monaco.editor.create(
  document.getElementById('html-editor'),
  {
    // 初始化展示的内容
    value: '<h1>hello world</h1>',
    language: 'html',
    theme: 'vs-dark',
    fontSize: 18,
    automaticLayout: true
  }
)
// 如果数据是异步从接口拿,那么可以调用setValue方法,设置内容
setTimeout(() => {
  htmlEditor.setValue('<h2>hello world</h2>')
}, 2000);

// 在这个方法监听编辑器的内容变动
htmlEditor.onDidChangeModelContent(() => {
  const newValue = htmlEditor.getValue()
})

通过上述3个简单的方法即可实现1个简单编辑器的内容读写

当然笔者这里用的Vite + Vue3,这里还有几个坑

导入worker,参考尤大给的示例

ts
// main.ts
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import JSONWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import CSSWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import HTMLWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import TSWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
self.MonacoEnvironment = {
  getWorker(_: any, label: string) {
    if (label === 'json') {
      return new JSONWorker()
    }
    if (label === 'css' || label === 'scss' || label === 'less') {
      return new CSSWorker()
    }
    if (label === 'html' || label === 'handlebars' || label === 'razor') {
      return new HTMLWorker()
    }
    if (label === 'typescript' || label === 'javascript') {
      return new TSWorker()
    }
    return new EditorWorker()
  }
}

editor使用ref绑定调用的时候需要toRaw,不然应用就会卡死

ts
const htmlEditor = ref<monaco.editor.IStandaloneCodeEditor>(null as any)
// 根据props更新
watchEffect(() => {
  if (htmlEditor.value && props.html) {
    toRaw(htmlEditor.value).setValue(props.html)
  }
})
onMounted(()=>{
  // 初始化实例
  htmlEditor.value = monaco.editor.create({...ops})
    toRaw(htmlEditor.value).onDidChangeModelContent(() => {
    console.log(toRaw(htmlEditor.value).getValue())
  })
})

代码渲染

这里使用iframe承载内容,期望iframe里页面最终结构如下

html
<head>
  <style>
    /* 内置一些样式重载 */
  </style>
  <style>
    /* 用户书写样式 */
  </style>
</head>
<body>
  <!--console信息面板 -->
  <script src="//cdn.jsdelivr.net/npm/eruda"></script>
  <script>
    if(window.eruda){
      window.eruda.init({
        defaults: {
          displaySize: 25,
          transparency: 0.9,
        }
      })
      window.eruda.show()
    }
  </script>

  <div>
    <!-- ...用户html代码 -->
  </div>

  <script>
    // 用户js代码
  </script>
</body>

其中 eruda 主要用于展示 iframe 页面的 log 日志信息、 Dom 结构等,避免打开Chrome DevTools

所有dom的创建均通过 Dom API 完成

在线预览示例代码效果:https://code.sugarat.top/share/6312ffa045a77f0bc4b881dc

下面简单过一下实现的代码

先给iframe搞个样式,避免看不到

html
<style>
iframe {
  width: 100%;
  height: 500px;
}
</style>

然后来3个dom承载我们的三剑客代码

ts
// 一系列用户编写的代码
const cssCode = `h1{
    color:red;
}`;
const htmlCode = `<h1>hello world</h1>`;
const jsCode = `console.log("hello world")`;

// 3个dom
const $style = document.createElement("style");
$style.innerHTML = cssCode;

const $html = document.createElement("div");
$html.innerHTML = htmlCode;

const $userScript = document.createElement("script");
$userScript.textContent = jsCode;

紧接着创建iframe将其装进去 就okk了

ts
const $iframe = document.createElement("iframe");
$iframe.addEventListener("load", () => {
  $iframe.contentDocument?.head.append($style);
  $iframe.contentDocument.body.append($html, $userScript);
});

document.body.append($iframe);

如果要引入eruda,咱们需要先等eruda加载完再插入咱们得script,不然捕获不到代码console

详细如下

ts
$iframe.addEventListener("load", () => {
  $iframe.contentDocument.head.append($style);

  // eruda cdn资源
  const $eruda = document.createElement("script");
  $eruda.src = "//cdn.jsdelivr.net/npm/eruda";
  // 打开面板的代码
  const debugExec = document.createElement("script");
  debugExec.textContent = `window.eruda.init({
      defaults: {
        displaySize: 25,
        transparency: 0.9,
      }
    })
    eruda.show()
    `;
  $iframe.contentDocument?.body.append($eruda);

  // eruda 加载完再加载HTML与用户脚本
  $eruda.onload = function () {
    $iframe.contentDocument?.body.append(debugExec, $html, $userScript);
  };
});

代码格式化

写完代码后,格式化是很有必要的,避免乱糟糟

笔者这里直接采用prettier,啥都提供好了,CV一下就能用

出参入参都比较简单

ts
import prettier from 'prettier/standalone'
import htmlPlugin from 'prettier/parser-html'
import cssPlugin from 'prettier/parser-postcss'
import jsPlugin from 'prettier/parser-babel'

export function formatHTML(html: string) {
  return prettier.format(html, {
    parser: 'html',
    plugins: [htmlPlugin]
  })
}
export function formatJS(tsCode: string) {
  return prettier.format(tsCode, {
    parser: 'babel',
    plugins: [jsPlugin]
  })
}

export function formatCSS(css: string) {
  return prettier.format(css, {
    parser: 'css',
    plugins: [cssPlugin]
  })
}

到这里构成应用的几个主要开源库的使用介绍差不多完了

前人栽树后人乘凉,对UI/UE要求不高的时候,拼凑出一个新的应用花费时间不高

当有一丝想法的时候,得及时抓住。

最后

EditorJS还是有很大的可玩性,后面有机会围绕输出一些插件实践,试着整个编辑器替代日常的Markdown

这个在线的Code也会在后续中会把一些想法迭代进去,目前看主要是代码编辑部分不太友好(代码提示这一块),访问了其它的在线编码的,基本都有这个问题(无论怎么搞都达不到本地VS Code的地步)

后面准备另辟蹊径搞一搞,先把Web版的交互优化一下下

三连破X,更新下一期(手动滑稽)

读者对其它实现细节感兴趣的话,可以试玩后,评论区留言,下一期送上解读

项目完整源码见GitHub

-------------华丽的分割线------------

成签约作者了,后面几个月输出可能会稍微频繁一点了,到时候就打扰了,大家不要吝啬3连

图片