该文章是学习react小册的React Playground项目的总结
实现原理
怎么能实现一个类似Vue Playground呢
从布局来看,左边为代码输入区域,右边为预览区域,我们一个个来看
代码编写区域
借用了@monaco-editor/react,它把monaco editor 封装成了 react 组件,方便在react项目中使用
npm install @monaco-editor/react
导入Editor,并给defaultValue赋值代码数据,还需要设置代码的语言
import Editor from '@monaco-editor/react';
function App() {
const code = `import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>{count}</div>
);
}
export default App;
`;
return (
<Editor height="500px" defaultLanguage="javascript" defaultValue={code} />
);
}
export default App;
运行项目可以看到显示正常,并有代码提示,但是这是JavaScript代码,要是typescript代码还会正常吗?
替换成typescript后出现了两个问题
<Editor
height="500px"
+ defaultLanguage="javascript"
- defaultLanguage="typescript"
defaultValue={code}
/>
第一个问题是jsx没有被ts编译
为了让ts能正确的编译,是要配置tsconfig.json文件,但是怎么修改编辑器的tsconfig呢?
onMount是编辑器加载完的回调里,在回调中设置对ts编译
更多的配置详情可以看此链接
+const handleEditorMount: OnMount = (editor, monaco) => {
+ monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
+ // 输入 <div> 输出 <div>,保留原样
+ jsx: monaco.languages.typescript.JsxEmit.Preserve,
+ // 编译的时候自动加上 default 属性
+ esModuleInterop: true,
+ });
+ };
return (
<Editor
height="500px"
defaultLanguage="typescript"
defaultValue={code}
+ onMount={handleEditorMount}
path="App.tsx"
/>
);
editor是编辑器实例,提供了编辑器实例的创建、销毁、获取、设置等功能
monaco是monaco-editor实例
typescriptDefaults.setCompilerOptions可以设置ts相关的配置
第二个问题是react模块找不到
借助了@typescript/ata,这个库自动下载用到的 ts 类型的功能,它会扫描代码里的 import,然后自动下载类型
安装@typescript/ata
npm install --save @typescript/ata -f
根据文档代码传入receivedFile函数拿到下载包的代码和路径
创建createATA函数得到ata
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, path) => {
console.log('自动下载的包', path);
onDownloadFile(code, path);
},
},
});
return ata;
}
ata的类型是一个函数,入参是代码字符串
解析到需要下载的包之后用addExtraLib添加到编辑器中,在初始化跟内容改变时再调用ata获取类型
const handleEditorMount: OnMount = (editor, monaco) => {
+ const ata = createATA((code, path) => {
+ monaco.languages.typescript.typescriptDefaults.addExtraLib(
+ code,
+ `file://${path}`
+ );
+ });
+
+ editor.onDidChangeModelContent(() => {
+ ata(editor.getValue());
+ });
+
+ ata(editor.getValue());
};
getValue可以获取到编辑器值
onDidChangeModelContent是监听编辑器模型内容改变事件
addExtraLib是添加类型
效果预览区域
一说到代码编译就会想到babel,但是在浏览器上怎么使用?
那就是@babel/standalone
使用transform来编译代码
传入之前的代码,通过.code获取到编译后的代码
import Editor from '@monaco-editor/react';
import { transform } from '@babel/standalone';
function App() {
const code = `import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>{count}</div>
);
}
export default App;
`;
+ const result = transform(code, {
+ presets: ['react', 'typescript'],
+ filename: 'test.tsx',
+ }).code!;
+ console.log(result);
return (
<Editor height="500px" defaultLanguage="javascript" defaultValue={code} />
);
}
export default App;
可以看到div编译成React.createElement
怎么把编译后的代码渲染出来呢
VuePlayground是采用iframe
添加html代码,将编译后的代码做为一个模块
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview</title>
</head>
<body>
<script type="module">${result}</script>
</body>
</html>
`;
iframe的src需要提供url,但是怎么把html的字符串转变成url呢?
可以用blob url,通过URL.createObjectURL()方法生成的,它允许你将二进制数据对象转换为一个可以在浏览器中使用的url
+ const src = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
return (
<div style={{ display: 'flex' }}>
<Editor
height="500px"
defaultLanguage="typescript"
defaultValue={code}
onMount={handleEditorMount}
path="App.tsx"
/>
+ <iframe
+ src={src}
+ style={{
+ width: '500px',
+ height: '500px',
+ padding: 0,
+ border: 'none',
+ }}
+ />
</div>
);
但是界面没有显示
在编译后的代码中,React.createElement但是我们没有导入React,而且在react项目挂载是需要添加
ReactDOM.createRoot(document.getElementById('root'))
添加导入、挂载代码
const code = `
+ import React from 'react';
import { useState } from "react";
+ import ReactDOM from 'react-dom/client';
function App() {
const [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>{count}</div>
);
}
export default App;
+ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
`;
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview</title>
</head>
<body>
<script type="module">${result}</script>
+ <div id="root"></div>
</body>
</html>
`;
界面还是没有显示
虽然代码引入Reacthe和ReactDOM,但是模块不能识别,此时需要用到importmap
添加代码,导入esm.sh 就是提供 esm 模块的 CDN 服务
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview</title>
</head>
<body>
+<script type="importmap">
+{
+ "imports": {
+ "react": "https://esm.sh/react@18.2.0",
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0"
+ }
+}
+</script>
<script type="module">${result}</script>
<div id="root"></div>
</body>
</html>
`;
修改code代码
const code = `
import React from 'react';
import { useState } from "react";
import ReactDOM from 'react-dom/client';
function App() {
const [count, setCount] = useState(0);
return (
- <div onClick={() => setCount(count + 1)}>{count}</div>
+ <>
+ <span>count:{count}</span>
+ <button onClick={() => setCount(count + 1)}>+1</button>
+ </>
);
}
export default App;
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
`;
代码编写区域显示代码正常,效果预览区域渲染正常
总结
- 代码编辑器使用@monaco-editor/react
- 效果预览采用了@babel/standalone编译代码,再用iframe+blob url+importmap渲染效果