如何自定义一个npm组件(react+antd+ts)

4,517 阅读4分钟

通过包装 codemirror 介绍一下如何发布一个 npm 组件

环境搭建

创建一个目录

mkdir my-code-editor
cd my-code-editor
  • 创建完以后文件夹当然是空的,我们进入到这个文件夹后直接运行 npm 的安装包命令即可
yarn install react webpack -D || npm install --save react webpack -D
  • 此时的目录结构为两个文件(package.json、yarn.lock)和一个文件夹(node_modules)
my-code-editor
├─ node_modules
├─ package.json
├─ yarn.lock

安装必备依赖

  • 为了更好的适应其他人的项目,以及进行我们的项目编写,我们还需要安装如下包
yarn add @babel/core @babel/preset-env @babel/preset-react babel-loader css-loader style-loader less less-loader webpack webpack-cli -D
或者
npm install -D @babel/core @babel/preset-env @babel/preset-react babel-loader css-loader style-loader less less-loader webpack webpack-cli

peerDependencies

  • peerDependencies:"让使用我们这个 my-component 包的人必须拥有跟我一样的 peerDependencies 里面罗列出来的包的对应版本"
  • 将 package.json 文件稍作修改
  • 修改前
{
  "devDependencies": {
    "@babel/core": "^7.18.5",
    "@babel/preset-env": "^7.18.2",
    "@babel/preset-react": "^7.17.12",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "mini-css-extract-plugin": "^2.6.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "style-loader": "^3.3.1",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
  }
}
  • 修改后
{
  "devDependencies": {
    "@babel/core": "^7.18.5",
    "@babel/preset-env": "^7.18.2",
    "@babel/preset-react": "^7.17.12",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "mini-css-extract-plugin": "^2.6.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "style-loader": "^3.3.1",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
  },
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

配置 .babelrc

  • 创建.babelrc 文件
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript",
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-proposal-object-rest-spread"
  ]
}

配置 webpack

  • 创建 webpack.config.js 文件
const { resolve } = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  // 组件库的起点入口
  entry: "./src/index.tsx",
  output: {
    // filename: "index.js", // 打包后的文件名
    filename: "main.js", // 输出文件
    path: resolve(__dirname, "dist"), // 打包后的文件目录:根目录/dist/
    libraryTarget: "commonjs2", // 导出库为UMD形式
  },
  resolve: {
    extensions: [".js", ".jsx", ".ts", ".tsx"],
  },
  externals: {
    // 打包过程遇到以下依赖导入,不会打包对应库代码,减小打包的体积,以下这些包可以配置peerDependencies中配置
    // 可以通过调用window上的react和react-dom
    react: "react",
    "react-dom": "react-dom",
    "@ant-design/icons": "@ant-design/icons", // 我的组件还依赖 antd和 @ant-design/icons
    antd: "antd",
  },
  // 模块
  module: {
    // 规则
    rules: [
      {
        test: /\.tsx?$/,
        use: "babel-loader",
      },
      {
        test: /\.less$/,
        use: [
          // webpack中的顺序是【从后向前】链式调用的
          // 所以对于less先交给less-loader处理,转为css
          // 再交给css-loader
          // 最后导出css(MiniCssExtractPlugin.loader)
          // 所以注意loader的配置顺序
          {
            loader: MiniCssExtractPlugin.loader,
          },
          "css-loader",
          "less-loader",
        ],
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    // 插件用于最终的导出独立的css的工作
    new MiniCssExtractPlugin({
      filename: "main.css",
    }),
  ],
};
  • 在 package.json 中添加"main": "dist/main.js"(此处的 dist 与以上 webpage 中配置的 output 相对应)
{
    ...
    "main": "dist/main.js",
    ...
}

typescript 配置

  • 我的组件是用 typescript 实现的所以需要 typescript 相关配置,js 可以忽略此步骤
  • 新建 tsconfig.json 文件
{
  "compileOnSave": false,
  "compilerOptions": {
    "jsx": "react-jsx",
    "declaration": false,
    "emitDeclarationOnly": false,
    "emitDecoratorMetadata": true,
    "noImplicitAny": true,
    "strict": true,
    "allowJs": true,
    "outDir": "./lib",
    "target": "es5",
    "module": "commonjs",
    "skipLibCheck": true,
    "experimentalDecorators": true,
    "noEmitOnError": true,
    "strictNullChecks": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "importHelpers": true,
    "esModuleInterop": true
  },
  "include": ["src", "index.d.ts"],
  "exclude": ["node_modules/",  "dist"]
}

  • 新建 typings.d.ts 文件
declare module "*.css";
declare module "*.less";
declare module "*.png";
declare module "*.svg" {
  export function ReactComponent(
    props: React.SVGProps<SVGSVGElement>
  ): React.ReactElement;
  const url: string;
  export default url;
}

  • 安装 typescript 相关包
yarn add typescript @babel/preset-typescript @types/node  @types/react-dom @types/react-D
    • 其中@types/react-dom @types/react 为 react、react-dom 的类型文件
  • 此时的目录结构
my-code-editor
├─ node_modules
├─ package.json
├─ yarn.lock
├─ .babelrc
├─ tsconfig.json
├─ webpack.config.js

编辑组件

  • 安装需要用到的插件包
yarn add antd codemirror @ant-design/icons @types/codemirror  -D

  • 创建 src 目录(以下我只罗列了我组件中一部分,这样更加便于理解)
  • 目录
my-code-editor
├─ node_modules
├─ package.json
├─ yarn.lock
├─ .babelrc
├─ tsconfig.json
├─ webpack.config.js
├─ src
    ├─ index.tsx
    ├─ sqlEditor
        ├─ index.tsx
        ├─ index.less

src/index.tsx

import React, { FC } from "react";
import SqlEditor from "./sqlEditor";

interface ICodeEditor {
  mode: "text/x-mysql" | "text/x-python";
  value?: string;
  busiId?: string;
  onChange?: (code?: string) => void;
  log?: string;
  hideTopBar?: boolean;
  hideFootBar?: boolean;
  onSave?: (code?: string) => void;
  onRun?: (code?: string) => void;
}
const CodeEditor: FC<ICodeEditor> = ({ mode, ...props }) => {
  if (mode === "text/x-mysql") return <SqlEditor {...props} />;
  return null;
};
export default CodeEditor;

src/sqlEditor/index.tsx

import React, { createRef, FC, useCallback, useEffect, useRef } from "react";
import { Button } from "antd";
import CodeMirror from "codemirror";
import "codemirror/lib/codemirror.css";
import "codemirror/mode/sql/sql";
import "codemirror/mode/shell/shell";
import "codemirror/addon/display/placeholder";
import "codemirror/addon/hint/show-hint.css"; // 用来做代码提示
import "codemirror/addon/hint/show-hint.js"; // 用来做代码提示
import "codemirror/addon/hint/sql-hint.js"; // 用来做代码提示
import "codemirror/addon/lint/lint.css";
import "codemirror/addon/selection/active-line";
import "codemirror/addon/lint/lint";

import {
  PlayCircleOutlined,
  SaveOutlined,
  FolderOpenOutlined,
} from "@ant-design/icons";
import "./index.less";

interface ISqlEditor {
  value?: string;
  onChange?: (code?: string) => void;
  onCatCodeList?: () => void;
  hideTopBar?: boolean;
  hideFootBar?: boolean;
  onSave?: (code?: string) => void;
  onRun?: (code?: string) => void;
}
const SqlEditor: FC<ISqlEditor> = ({
  value,
  onCatCodeList,
  hideTopBar,
  onChange,
  onSave,
  onRun,
}) => {
  const textareaRef = createRef<HTMLTextAreaElement>();
  const codeMirrorInstance = useRef<CodeMirror.EditorFromTextArea>();
  const curValue = useRef<string>();

  const completeAfter = (editor: any) => {
    var spaces = Array(editor.getOption("indentUnit")).join(";"); //分号;监听执行完后,就不会再执行inputRead输入监听了
    editor.replaceSelection(spaces);
  };
  useEffect(() => {
    codeMirrorInstance.current = CodeMirror.fromTextArea(
      textareaRef.current as HTMLTextAreaElement,
      {
        mode: "text/x-mysql",
        cursorHeight: 1,
        extraKeys: {
          "';'": completeAfter, //添加;号监听
          Ctrl: "autocomplete",
        },
        hintOptions: {
          completeSingle: false,
          tables: {
            users: ["name", "score", "birthDate"],
            countries: ["name", "population", "size"],
          },
        },
        styleActiveLine: true,
        indentWithTabs: true,
        smartIndent: true, // 自动缩进是否开启
        lineNumbers: true, // 是否使用行号
      }
    );
    codeMirrorInstance.current.on("inputRead", (instance) => {
      instance.showHint();
    });
    codeMirrorInstance.current.on("change", () => {
      const code = codeMirrorInstance.current?.getValue();
      curValue.current = code;
      onChange?.(code);
    });
  }, []);
  useEffect(() => {
    if (curValue.current !== value) {
      codeMirrorInstance.current?.setValue(value || "");
      curValue.current = value;
    }
  }, [value]);
  const handleRun = useCallback(() => {
    const code = codeMirrorInstance.current?.getValue();
    onRun?.(code);
  }, []);
  const handleSave = useCallback(() => {
    const code = codeMirrorInstance.current?.getValue();
    onSave?.(code);
  }, []);
  return (
    <div className="custom_sql_editor">
      {!hideTopBar && (
        <Button.Group className="btn_group">
          <Button onClick={handleRun}>
            运行 <PlayCircleOutlined />
          </Button>
          <Button onClick={handleSave}>
            保存 <SaveOutlined />
          </Button>

          <Button onClick={onCatCodeList}>
            我的代码 <FolderOpenOutlined />
          </Button>
        </Button.Group>
      )}
      <textarea ref={textareaRef}></textarea>
    </div>
  );
};
export default SqlEditor;

src/sqlEditor/index.less

.custom_sql_editor {
  .btn_group {
    margin-bottom: 10px;
  }
}

组件类型文件

  • 创建 index.d.ts 文件,将组件的类型在文件中声明
interface ICodeEditor {
  mode: "text/x-mysql" | "text/x-python";
  value?: string;
  busiId?: string;
  onChange?: (code?: string) => void;
  log?: string;
  hideTopBar?: boolean;
  hideFootBar?: boolean;
  onSave?: (code?: string) => void;
  onRun?: (code?: string) => void;
}
export declare const CodeEditor: React.FC<ICodeEditor>;
export default CodeEditor;

  • 在 package.json 中添加"typings": "index.d.ts",

完善一下 package.json

{
  "name": "my-code-editor",
  "version": "1.0.0",
  "description": "全站通用的code editor",
  "main": "dist/main.js",
  "typings": "index.d.ts",
  "scripts": {
    "build:tsc": "tsc",
    "build": "webpack --config webpack.config.js"
  },
  "devDependencies": {
    "@ant-design/icons": "^4.7.0",
    "@babel/core": "^7.18.5",
    "@babel/preset-env": "^7.18.2",
    "@babel/preset-react": "^7.17.12",
    "@babel/preset-typescript": "^7.17.12",
    "@types/codemirror": "^5.60.5",
    "@types/node": "^18.0.0",
    "@types/react": "^18.0.14",
    "@types/react-dom": "^18.0.5",
    "antd": "^4.21.3",
    "babel-loader": "^8.2.5",
    "codemirror": "^5.65.6",
    "css-loader": "^6.7.1",
    "mini-css-extract-plugin": "^2.6.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "style-loader": "^3.3.1",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
  },
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "keywords": [],
  "author": "name",
  "license": "ISC"
}

打包

npm run build

发布

  • 登录 npm login
  • 发布 yarn publish