playground
分析
参考Vue SFC Playground 实现效果,我们用 react 实现一个 react playground
- 左边代码编译,右边实时预览
- 可以分享代码
- 主题色切换
- 代码下载
代码编辑器
编辑器用的 monaco-react
<MonacoEditor
height="100%"
path={name}
language={language}
value={value}
onChange={onChange}
onMount={handleEditorMount}
options={editorOptions}
/>
它提供的配置项,直接传入对于的文件类型即可。当我们编辑的时候会触发 onChange 回调拿到的是当前代码的字符串形式.
例如:
;`import React, { useState } from 'react'
import './App.css'
const App: React.FC = () => {
const [count, setCount] = useState(0)
return (
<>
<h1>Hello World13</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
</>
)
}
export default App`
这样的代码直接在浏览器上面运行不了的,所以我们要利用 babel 进行编译.
代码编译
编译用的 @babel/standalone babel 的浏览器版本,可以把 tsx 编译成 js
babel/standalone 示例
我们从代码编辑器拿到的值就是类似下面 code 变量,当前代码的字符串形式.
import { transform } from '@babel/standalone'
const code = `import { useEffect, useState } from "react";
import App1 from './App.tsx';
function App() {
const [num, setNum] = useState(0);
return (
<div>
<App1 />
<div onClick={() => setNum((prevNum) => prevNum + 1)}>{num}</div>
</div>
);
}
export default App;
`
const res = transform(code, {
presets: ['react', 'typescript'],
filename: 'test.tsx',
})
// 指定他的presets为react和typescript。
console.warn(res.code)
把这一段放到代码里面去跑,可以看到控制台输出:
import { useState } from 'react'
import App1 from './App.tsx'
function App() {
const [num, setNum] = useState(0)
return /*#__PURE__*/ React.createElement(
'div',
null,
/*#__PURE__*/ React.createElement(App1, null),
/*#__PURE__*/ React.createElement(
'div',
{
onClick: () => setNum((prevNum) => prevNum + 1),
},
num
)
)
}
export default App
对于文件引入的情况,比如 import App from './App.tsx
,我们可以把 App.tsx 内容变成 blob url,然后替换 import。
blob url 示例:
简单来说就是将 js 文件变成 url 使用
const code1 = `
function add(a, b) {
return a + b;
}
export { add };
`
const url = URL.createObjectURL(
new Blob([code1], { type: 'application/javascript' })
)
// url = `blob:https://developer.mozilla.org/968ee7ac-87df-4566-8890-e388d67fed8d`
// 可以看到code1这段代码被转换成了blob url
// 这里因为是在mdn控制台跑的,所以地址前缀是mdn网站的.
const code2 = `import { add } from "${url}"; console.log(add(2, 3));`
// code2 = 'import { add } from "blob:https://developer.mozilla.org/968ee7ac-87df-4566-8890-e388d67fed8d"; console.log(add(2, 3));'
const script = document.createElement('script')
script.type = 'module'
script.textContent = code2
document.body.appendChild(script)
在浏览器控制台跑下这段代码如下: 可以看到输出了 5
import maps 示例
对于 import { useState } from 'react';
这样代码没在左边写的模块,引入我们可以采用 import maps.
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/[email protected]"
}
}
</script>
<script type="module">
import React from 'react'
console.log(React)
</script>
将这一段放到 html 的 script 标签里面去跑,可以看到控制台输出了 React 的对象。 esm是专门提供 es6 模块的 cdn 它返回的也是 import url 的方式.
替换 import 的 source
比如 import App from './App.tsx;
我们拿到到 App.tsx 的内容,然后通过 Bob 和 URL.createObjectURL 的方式把 App.tsx 内容变为一个 blob url,替换 import 的路径.
这个替换过程我们可以利用 babel 自定义插件来完成
babel 编译流程分为 parse、transform、generate 三个阶段,是在 transform 的阶段增删改 AST 的. 对 ImportDeclaration 的 AST 做处理,把 source.value 替换为对应文件的 blob url 就行了
比如:
import { transform } from '@babel/standalone'
import type { PluginObj } from '@babel/core'
function App() {
const code1 = `
function add(a, b) {
return a + b;
}
export { add };
`
const url = URL.createObjectURL(
new Blob([code1], { type: 'application/javascript' })
)
const transformImportSourcePlugin: PluginObj = {
visitor: {
ImportDeclaration(path) {
path.node.source.value = url
},
},
}
const code = `import { add } from './add.ts'; console.log(add(2, 3));`
function onClick() {
const res = transform(code, {
presets: ['react', 'typescript'],
filename: 'file.ts',
plugins: [transformImportSourcePlugin],
})
console.log(res.code)
}
return (
<div>
<button onClick={onClick}>编译</button>
</div>
)
}
export default App