说了半天,playGround的原理其实就是,你动态在body标签里面加入一个script标签,当前script标签内容执行完成以后,浏览器会自动创建一个新的task,来执行这个新的script标签里面的内容。在这个基础上,我们就可以实现一个左边编辑器,右边预览器的面板出来。编辑器和预览器的上面有一个context,来接受当前用户输入的代码,这个代码是以字符串的形式存放在context里面的。预览器拿到新代码以后,对他们进行编译,编译完以后把他塞入iframe里面。这样页面上相当于出现了一个新窗口,用来展示我们的成果。
在这里面的难点有3个:
1.在页面编辑器里面敲得代码,是以字符串的形式存放在浏览器内存中的,我们拿到这个字符串以后,会动态的塞到iframe的script标签里面,来实现了页面展示。
2.playGround的编辑区是支持多文件的,但是他们的本质就是一个个字符串,如何在iframe的script标签里面引用这些字符串,主要利用的就是blob转化,然后获取她的内存地址。
3.playGround的编辑区里面的文件是不是也需要导入react,使用的是 esm.sh 而不是用CDN导入,<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>。用CDN导入以后,编译就是个大问题,为了快速处理我们只能选择 esm.sh
4.我们从页面上拿到的html文件也是个字符串,但是iframe标签要的是url,这和2一样,把这个html字符串用blob转化成html二进制,然后用window.URL.createObjectURL()获取内存地址。
上节已经初步把整个原理过了一遍,现在我们来布局页面,看看样例
1.自己写个左右拖动的布局
playground的左右是可以移动的,我们就做这个布局,大概如下:
import { useRef, useState } from 'react';
import './index.scss';
export default function ReactPlayground() {
const editorRef = useRef<HTMLDivElement>(null);
const readerRef = useRef<HTMLDivElement>(null);
const [startSite, setStartSite] = useState(0);
const onDragStart = (event: any) => {
setStartSite(event.pageX);
};
const onDragEnd = (event: any) => {
const editorDiv = editorRef.current;
const readerDiv = readerRef.current;
const endSite = event.pageX;
if (editorDiv && readerDiv) {
const editorWidth = editorDiv.offsetWidth;
const readerWidth = readerDiv.offsetWidth;
editorDiv.style.width = `${editorWidth - (startSite - endSite)}px`;
readerDiv.style.width = `${readerWidth + (startSite - endSite)}px`;
}
};
return <div className="content">
<div className="editor" ref={editorRef}>编辑区</div>
<div className="moveBox"
draggable
onDragStart={onDragStart}
onDragEnd={onDragEnd}>
⁝
</div>
<div className="reader" ref={readerRef}>预览区</div>
</div >;
}
样式:
.content {
display: flex;
.editor,
.reader {
border: 1px solid #ccc;
height: 500px;
width: 50%;
}
.moveBox {
width: 6px;
}
}
效果:
2.利用工具组件做左右拖动
因为我们自己写的左右拖动很粗糙,细节还得自己打磨,咱们就用一个组件: allotment他是一个专门做左右拖动的组件,而且用起来也比较方便。
import { Allotment } from "allotment";
import 'allotment/dist/style.css';
export default function ReactPlayground() {
return <div style={{ height: '100vh' }}>
<Allotment defaultSizes={[100, 100]}>
<Allotment.Pane minSize={500}>
<div>
111
</div>
</Allotment.Pane>
<Allotment.Pane minSize={0}>
<div>
222
</div>
</Allotment.Pane>
</Allotment>
</div>;
}
整个页面由三个部分组成,1.Header,2.编辑区,3.预览区
2.1 Header
实现结果
2.2 编辑区
playground是多文件的,文件之间可以用import相互导入导出的,所有编辑区由两部分组成,一个是fileList还有一个是编辑面板。
引入编辑器@monaco-editor/react
如果你想要你的编辑器指出ts,那就给editor配置一下:
import React from 'react';
import MonacoEditor, { OnMount } from '@monaco-editor/react';
import './index.scss';
export default function Editor() {
const code = `export default function App() {
return <div>hello play ground</div>
}`;
const handleEditorMount: OnMount = (editor, monaco) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
jsx: monaco.languages.typescript.JsxEmit.Preserve,
esModuleInterop: true,
});
};
return <div className='editor-content'>
<MonacoEditor
height='100%'
path={'guang.tsx'}
language={"typescript"}
onMount={handleEditorMount}
value={code}
/>
</div>;
}
主要就是这个:
在编辑器加载的时候就去配置ts,ts 默认的编辑器的默认 compilerOptions。
在commonjs里面没有默认的导出,如果我们要在文件里面使用node的fs就必须用import * as fs from 'fs'
但是现在配置了compilerOptions以后,咱们可以直接 import fs from 'fs'
配置编辑器样式不要滚动条
@monaco-editor/react这个编辑器的好处就你可以通过配置不但可以编译jsx,还能编译ts,同时也可以配置快捷键。同时你可以在option里面配置他们的显示样式
<MonacoEditor
height={'100%'}
path={'guang.tsx'}
language={"typescript"}
onMount={handleEditorMount}
value={code}
options={
{
fontSize: 14,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
scrollbar: {
verticalScrollbarSize: 6,
horizontalScrollbarSize: 6,
},
}
}
配置ts的类型检测
现在我们的编辑器值支持ts,当然再写代码的时候,也应该由type提示,我们可以把@typescript/ata包给他配到编辑器里面去。
npm install --save @typescript/ata -f
上面这个说明他已经支持了
完整代码 editor.tsx
import React from 'react';
import MonacoEditor, { OnMount } from '@monaco-editor/react';
import { createATA } from './ata';
import './index.scss';
export default function Editor() {
const code = `export default function App() {
return <div>hello play ground</div>
}`;
const handleEditorMount: OnMount = (editor, monaco) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
jsx: monaco.languages.typescript.JsxEmit.Preserve,
esModuleInterop: true,
});
const ata = createATA((code, path) => {
monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${path}`);
});
editor.onDidChangeModelContent(() => {
ata(editor.getValue());
});
ata(editor.getValue());
};
return <div className='editor-content'>
<MonacoEditor
height={'100%'}
path={'guang.tsx'}
language={"typescript"}
onMount={handleEditorMount}
value={code}
options={
{
fontSize: 14,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
scrollbar: {
verticalScrollbarSize: 6,
horizontalScrollbarSize: 6,
},
}
}
/>
</div>;
}
ata.tsx
import { setupTypeAcquisition } from '@typescript/ata';
import typescriprt from 'typescript';
export function createATA(onDownloadFile: (code: string, path: string) => void) {
const ata = setupTypeAcquisition({
projectName: 'my-ata',
typescript: typescriprt,
logger: console,
delegate: {
receivedFile: (code: any, path: any) => {
console.log('自动下载的包', path);
onDownloadFile(code, path);
}
},
});
return ata;
}
总结:
用@monaco-editor/react编辑器最大的好处就是,不但帮我们解决了编译的问题,还帮我解决了样式问题,如果自己写写个编辑器,那还是又很大难度了。通过配置,就可以完成对jsx,ts的编译,还能用@typescript/ata来配置type的检测功能。
2.3 多文件切换
在fileList里面又很多文件,你可以所以切换文件就和vscode一样
之前就说了,在编辑器里面的代码对我们的项目而言,他就是个字符串,一个文件是一个字符串,如何把这些字符串全部收集起来,然后在editor里面共用呢?就要看context了
创建数据中心context
所以先创建一个fileContext出来,把context的创建和派发都封装在一起。
创建:
虽然在这里,file是File类型,但最终加入iframe以后都会处理成字符串,在我们的代码里面他的本质就是个字符串。这一点一定要明确,不然很难理解后面的原理。
派发:操作files里面的方法是增删改,就是操作object
在index.tsx里面使用看看
创建模板文件
首先从vue的playground里面看,fileList就是一个tab,来切换面板。而且文件都应该有一些模板的,就是实现这个功能
首先先创造出文件模板出来
有了这个基础文件,当我们的playground一旦启动,他就会在预览区出现基础画面了,和我们启动脚手架搭建的项目一样。
定义初始数据
接下来我们要写一个tsb页,把这些文件渲染到页面上去,渲染之前,要定义好初始数据。
有了这一份数据就难呢过很快的实现当我切换tab的时候,编辑器的数据发生变化。所以先把这份初始数据放到数据中心context里面去,这样,不管是fileList还是editor,我们都能快速的获取到动态数据
创建文件tab
首先看看files的类型
files是一个对象,每个文件名对应一个文件对象。所以在tab.tsx里面,我们直接拿到files的keys然后直接遍历出文件名就好了。
但是传入onchange就是为了实现,当我点击文件的时候,修改当前选中文件,然后把对应文件名传给setSelectedFileName。
编辑器关联文件数据
有了这些数据以后,你会发现是不是edotor编辑器也得变化了?
首先要把当前选中的文件传递给Editor编辑器,然后当编辑器里面的内容发生变化的时候,我们要把变化值同步给context数据中心去。
编辑器使用这个onchang起到实时监控的效果。
测试结果。
总结
最原始的一个playground就处理好了。
- 我们使用allotment组件来实现左右拖动,
- 我们选用@monaco-editor/react做react的编辑器,它最大的好处是你可以直接配置就难呢过帮你编译jsx和ts了,不用你再用babel处理。
3.文件模型用vite的静态方式导入成字符串:
import AppCss from './template/App.css?raw';这样做的好处是浏览器不会执行它,可以直接丢进编辑器以值的形式显示出来。 4.文件切换器就是个tab,咱们自己做的时候可以借助antd的组件,也可以自己写。 5.文件和编辑区和预览区的数据中心只有一个,就是context里面存储的文件。 6.ts类型检测使用的包是: @typescript/ata
难点: 1.左右拖动布局, 2.编辑器如何做jsx和ts编译 3.如何做ts类型检测,如果有错就给出提示。
参考文件: