babel编译流程
- parse
- transform(babel插件在此阶段,增删改AST)
- generate
核心库: @babel/standalone
可以将tsx代码编译为js
如何让编译后的代码运行?
- 在运行代码时,如果有import模块,此时会找不到,因此需要使用blob url来作为import的路径
- 再交给babel插件去做import 的source替换
// 如下方式可以将一段JS代码,变成一个URL
URL.createObjectURL(new Blob([code], { type: 'application/javascript' }))
如果引入的是react、react-dom这类的包呢?
- 使用import map机制,配合esm.sh网站来引入
<body>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0"
}
}
</script>
<script type="module">
import React from "react";
console.log(React);
</script>
</body>
核心库
npm install @monaco-editor/react
处理jsx报错
const editorOnMount: OnMount = (editor, monaco) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
jsx: monaco.languages.typescript.JsxEmit.Preserve, // 处理tsconfig对jsx的报错
esModuleInterop: true,
});
};
const editorOnMount: OnMount = (editor, monaco) => {
// 快捷键(CMD + K)实现代码格式化
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
// 底层是执行预设好的action
editor.getAction('editor.action.formatDocument')?.run();
});
};
核心库: @typescript/ata
/**
* ata(自动类型获取)
* 通过传入源码,自动分析出需要的ts类型包,然后自动下载
* @param onDownloadFile
* @returns
*/
function createATA(onDownloadFile: (code: string, path: string) => void) {
const ata = setupTypeAcquisition({
projectName: 'test-ata',
typescript,
logger: console,
delegate: {
receivedFile: (code, path) => {
console.log('自动下载包: ', path)
onDownloadFile(code, path)
}
}
})
return ata
}
const editorOnMount: OnMount = (editor, monaco) => {
const ata = createATA((code, path) => {
// 类型获取完后,通过addExtraLib加到ts里
monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${path}`)
})
// 代码内容发生改变后,再获取一次ts类型
editor.onDidChangeModelContent(() => {
ata(editor.getValue())
})
// 一开始就获取一次类型
ata(editor.getValue())
};
原理:
iframe
+ 通信机制- 代码编辑器将结果用babel编译后,传到iframe渲染
- 在预览生成的内容时,url同样是将内容内嵌到src,生成blob url
具体实现:
- 定义一个iframe html文件,提供import maps机制和src内容区
- 替换import maps
- 将编译后的代码,填充到src内容区
- 创建blob url,设置到iframe的src属性,完成渲染
<body>
<script type="importmap"></script>
<script type="module" id="appSrc"></script>
<div id="root"></div>
</body>
import iframeRaw from 'iframe.html?raw'
const [iframeUrl, setIframeUrl] = useState(getIframeUrl())
function getIframeUrl() {
const res = iframeRaw
.replace(
'<script type="importmap"></script>',
`<script type="importmap">${files[IMPORT_MAP_FILE_NAME].value}</script>`
)
.replace(
'<script type="module" id="appSrc"></script>',
// compiledCode babel编译后的代码
`<script type="module" id="appSrc">${compiledCode}</script>`
);
return URL.createObjectURL(new Blob([res], { type: 'text/html' }));
};
<iframe src={iframeUrl} />
涉及到多组件数据共享的问题,可以使用Context实现
文件区数据结构:
{
'App.vue': {
name: 'App.vue',
value: '<template><div>app</div></template>',
language: 'vue'
}
}
多组件共享的数据:
- 文件区
- 选中的文件
- 文件区的操作方法
编辑器中产生的错误,在iframe预览里不会显示,错误仅出现在devtools里,因此需要提供一个组件给用户提示
实现思路:
- 封装一个组件,用于展示错误信息
- 错误信息来源于iframe,iframe可以将错误信息通过postMessage传递至父窗口,父窗口监听postMessage的通信事件就能拿到错误信息并展示
<body>
<script>
// 监听错误,并通过postMessage传递错误信息至父窗口
window.addEventListener('error', (e) => {
window.parent.postMessage({
type: 'ERROR',
message: e.message
})
})
</script>
</body>
useEffect(() => {
// handleErrorInfo 保存error state
window.addEventListener('message', handleErrorInfo)
return () => {
window.removeEventListener('message', handleErrorInfo)
}
}, [])
原理
- absolute 布局,拖动时改变width
现成库: allotment
实现思路:
- CSS变量 + 切换html class
- css变量可以在子元素中生效,子元素写样式时,基于这些变量,在切换class后,也就切换了变量值,从而实现主题切换
.light {
--bg-color: #0ea5e9;
}
.dark {
--bg-color: #0f172a;
}
切换darkMode为手动
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'selector',
}
实现方式:
- 通过为body添加class dark即可生效
原理:
- 分享出去的关键内容就是context中的files(包含所有文件信息)
- 将files JSON字符串化后,放在url后,读取时直接parse一下就能作为文件区的初始化数据
const [files] = useState(getFilesFromUrl() || initFiles)
useEffect(() => {
const hash = JSON.stringify(files)
window.location.hash = encodeURIComponent(hash)
}, [files])
function getFilesFromUrl() {
let files
try {
const hash = decodeURIComponent(window.location.hash.slice(1))
files = decodeURIComponent(hash)
} catch (error) {
console.error(error)
}
return files
}
文件内容放在url上,太长了,需要压缩
npm install --save fflate
import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate';
/**
* URL内容压缩
* @param data
* @returns ASC码
*/
export function urlCompress(data: string): string {
// 字符串转字节数组
const buffer = strToU8(data);
// zlibSync压缩
const zipped = zlibSync(buffer, { level: 9 });
// strFromU8转字符串
const binary = strFromU8(zipped, true);
// btoa将base64编码后的字符串转ASC码
return btoa(binary);
}
/**
* URL内容解压缩
* @param base64
* @returns
*/
export function urlUncompressed(base64: string): string {
const binary = atob(base64);
const buffer = strToU8(binary, true);
const unzipped = unzlibSync(buffer);
return strFromU8(unzipped);
}