Finish Playground
This commit is contained in:
58
src/components/Playground/Output/Preview/iframe.html
Normal file
58
src/components/Playground/Output/Preview/iframe.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Preview</title>
|
||||
<!-- es-module-shims -->
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.addEventListener("error", (e) => {
|
||||
window.parent.postMessage({ type: "ERROR", msg: e.message });
|
||||
});
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
window.parent.postMessage({ type: "LOADED", msg: "" });
|
||||
});
|
||||
|
||||
window.addEventListener("message", ({ data }) => {
|
||||
if (data?.type === "UPDATE") {
|
||||
// Record old styles that need to be removed
|
||||
const appStyleElement = document.querySelectorAll("style[id^=\"style_\"]") || [];
|
||||
|
||||
// Remove old app
|
||||
const appSrcElement = document.querySelector("#appSrc");
|
||||
const oldSrc = appSrcElement.getAttribute("src");
|
||||
appSrcElement.remove();
|
||||
|
||||
// Add new app
|
||||
const script = document.createElement("script");
|
||||
script.src = URL.createObjectURL(
|
||||
new Blob([data.data.compiledCode], {
|
||||
type: "application/javascript"
|
||||
})
|
||||
);
|
||||
script.id = "appSrc";
|
||||
script.type = "module";
|
||||
script.onload = () => {
|
||||
// Remove old style
|
||||
appStyleElement.forEach(element => {
|
||||
element.remove();
|
||||
});
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
URL.revokeObjectURL(oldSrc);
|
||||
window.parent.postMessage({ type: "DONE", msg: "" });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script type="module" id="appSrc"></script>
|
||||
<div id="root">
|
||||
<div
|
||||
style="position:absolute;top: 0;left:0;width:100%;height:100%;display: flex;justify-content: center;align-items: center;">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
92
src/components/Playground/Output/Preview/index.tsx
Normal file
92
src/components/Playground/Output/Preview/index.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { IFiles, IImportMap } from '@/components/Playground/shared'
|
||||
import iframeRaw from '@/components/Playground/Output/Preview/iframe.html?raw'
|
||||
import { useUpdatedEffect } from '@/util/hooks'
|
||||
import Compiler from '@/components/Playground/compiler'
|
||||
import '@/components/Playground/Output/Preview/preview.scss'
|
||||
|
||||
interface PreviewProps {
|
||||
iframeKey: string
|
||||
files: IFiles
|
||||
importMap: IImportMap
|
||||
}
|
||||
|
||||
interface IMessage {
|
||||
type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE'
|
||||
msg: string
|
||||
data: {
|
||||
compiledCode: string
|
||||
}
|
||||
}
|
||||
|
||||
const getIframeUrl = (iframeRaw: string) => {
|
||||
const shimsUrl = '//unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js'
|
||||
// 判断浏览器是否支持esm ,不支持esm就引入es-module-shims
|
||||
const newIframeRaw =
|
||||
typeof import.meta === 'undefined'
|
||||
? iframeRaw.replace(
|
||||
'<!-- es-module-shims -->',
|
||||
`<script async src="${shimsUrl}"></script>`
|
||||
)
|
||||
: iframeRaw
|
||||
return URL.createObjectURL(new Blob([newIframeRaw], { type: 'text/html' }))
|
||||
}
|
||||
|
||||
const iframeUrl = getIframeUrl(iframeRaw)
|
||||
|
||||
const Preview: React.FC<PreviewProps> = ({ iframeKey, files, importMap }) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
const handleMessage = ({ data }: { data: IMessage }) => {
|
||||
const { type, msg } = data
|
||||
switch (type) {
|
||||
case 'LOADED':
|
||||
setLoaded(true)
|
||||
break
|
||||
case 'ERROR':
|
||||
setErrorMsg(msg)
|
||||
break
|
||||
case 'DONE':
|
||||
setErrorMsg('')
|
||||
}
|
||||
}
|
||||
|
||||
useUpdatedEffect(() => {
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useUpdatedEffect(() => {
|
||||
Compiler.compile(files, importMap)
|
||||
.then((result) => {
|
||||
if (loaded) {
|
||||
iframeRef.current?.contentWindow?.postMessage({
|
||||
type: 'UPDATE',
|
||||
data: { compiledCode: result.outputFiles[0].text }
|
||||
} as IMessage)
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
setErrorMsg(`编译失败:${e.message}`)
|
||||
})
|
||||
}, [files, Compiler, loaded])
|
||||
|
||||
return (
|
||||
<div data-component={'playground-preview'}>
|
||||
<iframe
|
||||
key={iframeKey}
|
||||
ref={iframeRef}
|
||||
src={iframeUrl}
|
||||
sandbox="allow-popups-to-escape-sandbox allow-scripts allow-popups allow-forms allow-pointer-lock allow-top-navigation allow-modals allow-same-origin"
|
||||
/>
|
||||
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Preview
|
||||
8
src/components/Playground/Output/Preview/preview.scss
Normal file
8
src/components/Playground/Output/Preview/preview.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
[data-component=playground-preview] {
|
||||
display: flex;
|
||||
position: relative;
|
||||
iframe {
|
||||
border: none;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
80
src/components/Playground/Output/Transform/index.tsx
Normal file
80
src/components/Playground/Output/Transform/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useState } from 'react'
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
import { Loader } from 'esbuild-wasm'
|
||||
import '@/components/Playground/Output/Transform/transform.scss'
|
||||
import { useUpdatedEffect } from '@/util/hooks.tsx'
|
||||
import { IFile, ITheme } from '@/components/Playground/shared.ts'
|
||||
import Compiler from '@/components/Playground/compiler.ts'
|
||||
import { cssToJs, jsonToJs } from '@/components/Playground/files.ts'
|
||||
import { MonacoEditorConfig } from '@/components/Playground/CodeEditor/Editor/monacoConfig.ts'
|
||||
import { addReactImport } from '@/components/Playground/utils.ts'
|
||||
|
||||
interface OutputProps {
|
||||
file: IFile
|
||||
theme?: ITheme
|
||||
}
|
||||
|
||||
const Transform: React.FC<OutputProps> = ({ file, theme }) => {
|
||||
const [compiledCode, setCompiledCode] = useState('')
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
|
||||
const compile = (code: string, loader: Loader) => {
|
||||
let _code = code
|
||||
if (['jsx', 'tsx'].includes(loader)) {
|
||||
_code = addReactImport(code)
|
||||
}
|
||||
|
||||
Compiler?.transform(_code, loader)
|
||||
.then((value) => {
|
||||
setCompiledCode(value.code)
|
||||
setErrorMsg('')
|
||||
})
|
||||
.catch((e) => {
|
||||
setErrorMsg(`编译失败:${e.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
useUpdatedEffect(() => {
|
||||
if (file) {
|
||||
try {
|
||||
const code = file.value
|
||||
|
||||
switch (file.language) {
|
||||
case 'typescript':
|
||||
compile(code, 'tsx')
|
||||
break
|
||||
case 'javascript':
|
||||
compile(code, 'jsx')
|
||||
break
|
||||
case 'css':
|
||||
setCompiledCode(cssToJs(file))
|
||||
break
|
||||
case 'json':
|
||||
setCompiledCode(jsonToJs(file))
|
||||
break
|
||||
case 'xml':
|
||||
setCompiledCode(code)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
setCompiledCode('')
|
||||
}
|
||||
} else {
|
||||
setCompiledCode('')
|
||||
}
|
||||
}, [file, Compiler])
|
||||
|
||||
return (
|
||||
<div data-component={'playground-transform'}>
|
||||
<MonacoEditor
|
||||
theme={theme}
|
||||
language={'javascript'}
|
||||
value={compiledCode}
|
||||
options={{ ...MonacoEditorConfig, readOnly: true }}
|
||||
/>
|
||||
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Transform
|
||||
@@ -0,0 +1,3 @@
|
||||
[data-component=playground-transform] {
|
||||
position: relative;
|
||||
}
|
||||
40
src/components/Playground/Output/index.tsx
Normal file
40
src/components/Playground/Output/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useState } from 'react'
|
||||
import FlexBox from '@/components/common/FlexBox.tsx'
|
||||
import FileSelector from '@/components/Playground/CodeEditor/FileSelector'
|
||||
import Transform from '@/components/Playground/Output/Transform'
|
||||
import { IFiles, IImportMap } from '@/components/Playground/shared.ts'
|
||||
import Preview from '@/components/Playground/Output/Preview'
|
||||
|
||||
interface OutputProps {
|
||||
files: IFiles
|
||||
selectedFileName: string
|
||||
importMap: IImportMap
|
||||
}
|
||||
|
||||
const Output: React.FC<OutputProps> = ({ files, selectedFileName, importMap }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('Preview')
|
||||
|
||||
return (
|
||||
<FlexBox data-component={'playground-code-output'}>
|
||||
<FileSelector
|
||||
files={{
|
||||
Preview: { name: 'Preview', language: 'json', value: '' },
|
||||
Transform: { name: 'Transform', language: 'json', value: '' }
|
||||
}}
|
||||
selectedFileName={selectedTab}
|
||||
onChange={(tabName) => setSelectedTab(tabName)}
|
||||
readonly
|
||||
/>
|
||||
{selectedTab === 'Preview' && (
|
||||
<Preview
|
||||
iframeKey={JSON.stringify(importMap)}
|
||||
files={files}
|
||||
importMap={importMap}
|
||||
/>
|
||||
)}
|
||||
{selectedTab === 'Transform' && <Transform file={files[selectedFileName]} />}
|
||||
</FlexBox>
|
||||
)
|
||||
}
|
||||
|
||||
export default Output
|
||||
Reference in New Issue
Block a user