学习React Playground:布局

173 阅读3分钟

该文章是学习react小册的React Playground项目的总结

创建一个react+typescript项目

npx create-vite

布局

首先从布局开始,可以看到Vue Playground可以拖动滚动条左右拖动控制大小

chrome-capture-2024-7-19.gif 可以使用allotment 这个组件

image.png 安装

npm install --save allotment

Vue Playground分成Header、CodeEditor、Preview区域 image.png 添加这三个组件

image.png
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区域,右边为下载、分享等功能区域后续再添加 image.png 设置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;

渲染效果 image.png

Editor

CodeEditor分成filenameList跟editor

image.png 首先安装@monaco-editor

npm install --save @monaco-editor/react

创建对应的文件 image.png

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}
    />
  );
}

image.png 滚动条太粗了,可以通过options设置滚动条大小 image.png

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,
      }}
    />
  );

image.png 设置保存快捷键

  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保存,传入快捷键触发的回调,这里设置保存时格式化 image.png editor.getAction执行格式化 image.png 创建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());
  };

总结

  1. 外层用allotment包裹,可以通过拖动改变大小
  2. 因为切换文件需要显示对应的文件代码,所以封装了Editor组件
  3. 在onMount时添加了ctrl+s的快捷键、对ts选项的设置、对代码中需要用到的包进行下载