组装个支持记笔记的CodePen
前言
emmm。。。,有好长一段时间没码文了(近几个月实在是太忙了),这个玩具刚好是这两周抽空拼
的拿出来和大家分享一下
朋友最近刚学前端,经常问一些问题,通过聊天软件发代码和贴图实在是不太方便,就给它推荐了CodePen 和 🐴上掘金
前者国内访问实在不稳定,后者emm有个内容审核,导致内容实时性
较差。
就搜了搜GitHub上有不有类似的项目,搜了一圈还真有不少,这里贴两个感觉不错的 CodeRun 和 CodePan
朋友使用后反馈,问了问有不有啥支持直接在旁边 写笔记,贴图片(这个),方便做记录
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
场景,存储和鉴权MongoDB
与Redis
感觉就够了(也没有配置成本,安装即用)
服务端框架部分就直接拿自己的之前写的玩具flash-wolves开整
接下来就是组装了
实现
项目搭建
粗糙的原型图如下
布局也比较简单
<header>工具条...</header>
<!-- 主体内容 -->
<main>
<Note />
<Code />
<Render />
</main>
直接拿之前整的模板进行创建vite-vue3-template出前端工程
文本编辑部分
示例代码在线演示:https://code.sugarat.top/share/630b8793789c242a31e57c40
直接参照官方文档,CV起来就能运行,这里仅仅贴几个关键部分(避免代码占用太大篇幅,降低文章可读性)
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编辑器为例
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,参考尤大给的示例
// 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,不然应用就会卡死
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里页面最终结构如下
<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搞个样式,避免看不到
<style>
iframe {
width: 100%;
height: 500px;
}
</style>
然后来3个dom承载我们的三剑客
代码
// 一系列用户编写的代码
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了
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
详细如下
$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一下就能用
出参入参都比较简单
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连