通过包装 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