该文章是学习react小册的React Playground项目的总结
创建一个react+typescript项目
npx create-vite
布局
首先从布局开始,可以看到Vue Playground可以拖动滚动条左右拖动控制大小
可以使用allotment 这个组件
安装
npm install --save allotment
Vue Playground分成Header、CodeEditor、Preview区域
添加这三个组件
Header
import { FC } from 'react';
const Header: FC = () => {
return <div style={{ borderBottom: '1px solid #000' }}>Header</div>;
};
export default Header;
CodeEditor
import { FC } from 'react';
const CodeEditor: FC = () => {
return <div>CodeEditor</div>;
};
export default CodeEditor;
Preview
import { FC } from 'react';
const Preview: FC = () => {
return <div>Preview</div>;
};
export default Preview;
因为CodeEditor和Preview是可拖动的,所以用Allotment.Pane包裹,minSize为0代表最小宽度,外层Allotment包裹,defaultSizes是设置初始化比例展示
import { Allotment } from 'allotment';
import 'allotment/dist/style.css';
import Header from './components/Header';
import CodeEditor from './components/CodeEditor';
import Preview from './components/Preview';
function App() {
return (
<div style={{ height: '100vh' }}>
<Header />
<Allotment defaultSizes={[100, 100]}>
<Allotment.Pane minSize={0}>
<CodeEditor />
</Allotment.Pane>
<Allotment.Pane minSize={0}>
<Preview />
</Allotment.Pane>
</Allotment>
</div>
);
}
export default App;
安装Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
tailwind.config.js添加解析文件的路径
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
引入 tailwind 的基础样式、组件样式、工具样式
@tailwind base;
@tailwind components;
@tailwind utilities;
Header
Header分成左边为logo区域,右边为下载、分享等功能区域后续再添加
设置Header的样式
import { FC } from 'react';
import logoSvg from '../../icons/logo.svg';
const Header: FC = () => {
return (
<div className="h-12 border-b border-solid border-gray-900 px-20 flex items-center justify-between box-border">
<div className="flex items-center text-xl ">
<img src={logoSvg} alt="logo" className="h-6 mr-2.5" />
<span className="text-lg font-semibold text-cyan-400">
React Playground
</span>
</div>
</div>
);
};
export default Header;
渲染效果
Editor
CodeEditor分成filenameList跟editor
首先安装@monaco-editor
npm install --save @monaco-editor/react
创建对应的文件
import { FC } from 'react';
const FileNameList: FC = () => {
return <div>FileNameList</div>;
};
export default FileNameList;
import Editor from './Editor';
import FileNameList from './FileNameList';
import { FC } from 'react';
const CodeEditor: FC = () => {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<FileNameList />
<Editor />
</div>
);
};
export default CodeEditor;
当切换每个文件时,需要展示对应的代码,所以需要将封装成通用组件
每个文件必须传入name、language、value,也可以传入自定义的options,在修改内容时的回调
import { EditorProps } from '@monaco-editor/react';
import { FC } from 'react';
import { editor } from 'monaco-editor';
export interface EditorFile {
name: string;
value: string;
language: string;
}
interface IProps {
file: EditorFile;
onChange?: EditorProps['onChange'];
options?: editor.IStandaloneEditorConstructionOptions;
}
const Editor: FC<IProps> = (props) => {
const { file, onChange, options } = props;
return (
<MonacoEditor
height="100%"
path={file.name}
language={file.language}
value={file.value}
onChange={onChange}
options={options}
/>
);
};
export default Editor;
在CodeEditor传入file数据
import Editor from './Editor';
import FileNameList from './FileNameList';
import { FC } from 'react';
const CodeEditor: FC = () => {
const file = {
name: 'test.tsx',
value: `
const btn = () =>{
return <button>按钮+1</button>
}
`,
language: 'typescript',
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<FileNameList />
<Editor file={file} />
</div>
);
};
export default CodeEditor;
上一章已经有说MonacoEditor编写ts跟react需要在onMount时设置编译选项
import MonacoEditor, { OnMount } from '@monaco-editor/react';
const Editor: FC<IProps> = (props) => {
const { file, onChange, options } = props;
const handleEditorMount: OnMount = (editor, monaco) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
// 输入 <div> 输出 <div>,保留原样
jsx: monaco.languages.typescript.JsxEmit.Preserve,
// 编译的时候自动加上 default 属性
esModuleInterop: true,
});
};
return (
<MonacoEditor
height="100%"
path={file.name}
language={file.language}
value={file.value}
onChange={onChange}
options={options}
onMount={handleEditorMount}
/>
);
}
滚动条太粗了,可以通过options设置滚动条大小
return (
<MonacoEditor
height="100%"
path={file.name}
language={file.language}
value={file.value}
onMount={handleEditorMount}
onChange={onChange}
options={{
+ fontSize: 14,
+ // 到达文件的最后一行时,编辑器将不再继续滚动
+ scrollBeyondLastLine: false,
+ // 设置滚动条大小
+ scrollbar: {
+ verticalScrollbarSize: 6,
+ horizontalScrollbarSize: 6,
+ },
...options,
}}
/>
);
设置保存快捷键
const handleEditorMount: OnMount = (editor, monaco) => {
+ // ctrl或者cmd+s保存
+ editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
+ editor.getAction('editor.action.formatDocument')?.run();
+ });
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
// 输入 <div> 输出 <div>,保留原样
jsx: monaco.languages.typescript.JsxEmit.Preserve,
// 编译的时候自动加上 default 属性
esModuleInterop: true,
});
};
通过editor.addCommand设置ctrl或者cmd+s保存,传入快捷键触发的回调,这里设置保存时格式化
editor.getAction执行格式化
创建ata.ts文件
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) => {
onDownloadFile(code, path);
},
},
});
return ata;
}
在onMount回调添加初始化跟内容修改时类型下载
const handleEditorMount: OnMount = (editor, monaco) => {
// ctrl或者cmd+s保存
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
editor.getAction('editor.action.formatDocument')?.run();
});
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
// 输入 <div> 输出 <div>,保留原样
jsx: monaco.languages.typescript.JsxEmit.Preserve,
// 编译的时候自动加上 default 属性
esModuleInterop: true,
});
+ const ata = createATA((code, path) => {
+ monaco.languages.typescript.typescriptDefaults.addExtraLib(
+ code,
+ `file://${path}`
+ );
+ });
+
+ editor.onDidChangeModelContent(() => {
+ ata(editor.getValue());
+ });
+
+ ata(editor.getValue());
};
总结
- 外层用allotment包裹,可以通过拖动改变大小
- 因为切换文件需要显示对应的文件代码,所以封装了Editor组件
- 在onMount时添加了ctrl+s的快捷键、对ts选项的设置、对代码中需要用到的包进行下载