该文章是学习react小册的React Playground项目的总结
编译
在原理篇中有介绍是通过@babel/standalone来编译
安装
npm install --save @babel/standalone
npm install --save-dev @types/babel__standalone
首先看一下编译后的代码是怎么样的
将编译逻辑单独抽离出来
渲染只需要编译入口文件
import { ENTRY_FILE_NAME } from "@/files";
import { Files } from "@/store";
import { transform } from "@babel/standalone";
export const babelTransform = (filename: string, code: string, files: Files) => {
let result = ""
try {
result = transform(code, {
// 指定 react 和 typescript,也就是对 jsx 和 ts 语法做处理
presets: ['react', 'typescript'],
filename,
plugins: [],
// 编译后保持原有行列号不变
retainLines: true
}).code!
} catch (error) {
console.error('编译出错', error);
}
return result
}
export const compile = (files: Files) => {
const main = files[ENTRY_FILE_NAME]
return babelTransform(ENTRY_FILE_NAME, main.value, files)
}
在Preview组件引入Editor组件,将通过compile编译后的代码传入Editor组件,并监听files文件,在修改files文件时同步编译
import useStore from '@/store';
import { FC, useEffect, useState } from 'react';
import { compile } from './compiler';
import Editor from '../CodeEditor/Editor';
const Preview: FC = () => {
const files = useStore(state => state.files)
const [compiledCode, setCompiledCode] = useState('')
useEffect(() => {
const res = compile(files)
setCompiledCode(res)
}, [files])
return <div style={{ height: '100%' }}>
<Editor file={{
name: 'dist.js',
value: compiledCode,
language: 'javascript'
}} />
</div>
};
export default Preview;
可以看到jsx编译成React.createElement,也同步了修改
但是内容中引入的文件路径,babel并识别不了,可以用blob url的方式
但是怎么替换编译后代码中的路径呢?使用@babel/core
安装
npm i @babel/core
通过 astexplorer.net看到ImportDeclaration节点的source.value保存路径
可以看到是可以获取到路径,那就可以在transform的plugin
result = transform(code, {
presets: ['react', 'typescript'],
filename,
- plugins: [],
+ plugins: [customResolver(files)],
retainLines: true
}).code!
可以借鉴一下文档中的示例,添加ImportDeclaration方法,拿到path再判断文件的类型创建对应
const customResolver = (files: Files): PluginObj => {
return {
visitor: {
ImportDeclaration(path) {
const modulePath = path.node.source.value
if (modulePath.startsWith('.')) {
const file = getModuleFile(files, modulePath)
if (!file)
return
if (file.name.endsWith('.css')) {
path.node.source.value = css2Js(file)
} else if (file.name.endsWith('.json')) {
path.node.source.value = json2Js(file)
} else {
path.node.source.value = URL.createObjectURL(
new Blob([babelTransform(file.name, file.value, files)], {
type: 'application/javascript',
})
)
}
}
}
}
}
}
json是没有导出,需要加上export default,file.value就是一个对象
css是添加到head里的style标签里
js就默认
const json2Js = (file: EditorFile) => {
const js = `export default ${file.value}`
return URL.createObjectURL(new Blob([js], { type: 'application/javascript' }))
}
const css2Js = (file: EditorFile) => {
const randomId = new Date().getTime()
const js = `
(() => {
const stylesheet = document.createElement('style')
stylesheet.setAttribute('id', 'style_${randomId}_${file.name}')
document.head.appendChild(stylesheet)
const styles = document.createTextNode(\`${file.value}\`)
stylesheet.innerHTML = ''
stylesheet.appendChild(styles)
})()
`
return URL.createObjectURL(new Blob([js], { type: 'application/javascript' }))
}
可以看到./App编译成blob url
既然是个url,那能不能请求这个url呢?
直接访问./App的blob url
可以看到是返回了App编译后的内容
但是有个问题,createElement是通过React使用的,但是React我们并没有导入
一种可以通过自己写导入,另外一种是在编译的时候自动加
在compiler.ts添加beforeTransformCode函数,判断是否导入React,没有就加导入React
export const beforeTransformCode = (filename: string, code: string) => {
let _code = code
const regexReact = /import\s+React/g
if ((filename.endsWith('.jsx') || filename.endsWith('.tsx')) && !regexReact.test(code)) {
_code = `import React from 'react';\n${code}`
}
return _code
}
export const babelTransform = (filename: string, code: string, files: Files) => {
+ let _code = beforeTransformCode(filename, code);
let result = ''
try {
- result = transform(code, {
+ result = transform(_code, {
presets: ['react', 'typescript'],
filename,
plugins: [customResolver(files)],
retainLines: true
}).code!
} catch (error) {
console.error('编译出错', error);
}
return result
}
再次访问App的blob url,可以看到加入了
预览
现在编译工作完成了,可以开始渲染了
新增iframe的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"></script>
<script type="module"></script>
<div id="root"></div>
</body>
</html>
分别准备type为importmap、module的script标签,这两个内容都是未知,所以都需要替换
在Preview组件中,将importmap和编译后的main文件填充到空的script标签中,再根据最新的html内容创建blob url给iframe标签展示
import iframeRaw from './iframe.html?raw'
import { IMPORT_MAP_FILE_NAME } from '@/files';
const getIframeUrl = () => {
const res = iframeRaw.replace(
'<script type="importmap"></script>',
`<script type="importmap">${files[IMPORT_MAP_FILE_NAME].value
}</script>`
).replace(
'<script type="module"></script>',
`<script type="module">${compiledCode}</script>`,
)
return URL.createObjectURL(new Blob([res], { type: 'text/html' }))
}
const [iframeUrl, setIframeUrl] = useState(getIframeUrl());
useEffect(() => {
setIframeUrl(getIframeUrl())
}, [files[IMPORT_MAP_FILE_NAME].value, compiledCode]);
<iframe
src={iframeUrl}
style={{
width: '100%',
height: '100%',
padding: 0,
border: 'none',
}}
/>
可以看到点击按钮跟修改文件代码都是显示正常