WTQ
2291 文字
11 分
基于React及TypeScript封装组件库之(一)项目搭建与封装AntD

在工作过程中开发低代码平台时,涉及到封装组件库给引擎引入。从零封装一套组件库是一个费力不讨好的工作,因此考虑使用 AntD 封装。

目标#

  1. React 组件库,取名 x-components,导出 x-components.umd.js、x-components.umd.css。
  2. 组件逻辑使用 TypeScript 开发。
  3. 组件样式使用 less 开发。
  4. 组件使用 webpack 作为打包工具。
  5. React 及 React-Dom 模块使用外部引入。

项目构建#

整体技术选型#

  1. 使用 yarn 作为包管理工具
  2. 使用 webpack 作为打包工具
  3. 使用 babel 处理 TypeScript 代码 3.使用 less-loader、css-loader 等处理样式代码,使用 MiniCssExtractPlugin 分离 CSS

项目初始化#

初始化 x-components 项目#

可参考前端工程化系列

目录结构

x-components\src
│  index.ts

├─components
│  ├─x-button
│  │      index.tsx
│  │
│  └─x-input
│          index.tsx
│   ....

安装 webpack#

在本地安装 webpack,接着安装 webpack-cli(此工具用于在命令行中运行 webpack):

yarn add -D webpack webpack-cli

添加配置文件 webpack.config.js#

const path = require('path');

module.exports = {
  entry: './src/index.ts',
  output: {
    filename: 'x-components.umd.js', // 打包后的文件名
    path: path.resolve(__dirname, 'dist'), // 打包后的文件目录:根目录/dist/
    library: 'xcomponents', // 导出的UMD js会在window挂xcomponents,即可以访问window.xcomponents
    libraryTarget: 'umd', // 导出库为UMD形式
  },
  resolve: {
    // webpack 默认只处理js、jsx等js代码
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },
};

引入 Babel#

引入 babel-loader 以及相关 babel 库

yarn add -D babel-loader @babel/core @babel/preset-env @babel/preset-typescript @babel/preset-react @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
# diff --git a/package.json b/package.json
@@ -11,6 +12,13 @@
     "prepack": "npmignore --auto"
   },
   "devDependencies": {
+    "@babel/core": "^7.26.7",
+    "@babel/plugin-proposal-class-properties": "^7.18.6",
+    "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
+    "@babel/preset-env": "^7.26.7",
+    "@babel/preset-react": "^7.26.3",
+    "@babel/preset-typescript": "^7.26.0",
+    "babel-loader": "^9.2.1",
     "@eslint/js": "^9.18.0",
     "eslint": "^9.18.0",
     "husky": "^9.1.7",

Babel 原理#

看到上面一长串依赖,一开始可能会有点手足无措,不知道这么多东西是干啥的。可以通过这篇文章了解到 babel 到底是做什么的。文章比较长,这里简单做个总结。

babel 总共分为三个阶段:解析,转换,生成。
babel 本身不具有任何转化功能,它把转化的功能都分解到一个个 plugin 里面。因此当我们不配置任何插件时,经过 babel 的代码和输入是相同的。
插件总共分为两种:

  • 语法插件:用于解析这一步,将代码转换为 AST。当我们添加 语法插件 之后,在解析这一步就使得 babel 能够解析更多的语法。(顺带一提,babel 内部使用的解析类库叫做 babylon,并非 babel 自行开发)

举个简单的例子,当我们定义或者调用方法时,最后一个参数之后是不允许增加逗号的,如 callFoo(param1, param2,) 就是非法的。如果源码是这种写法,经过 babel 之后就会提示语法错误。但最近的 JS 提案中已经允许了这种新的写法(让代码 diff 更加清晰)。为了避免 babel 报错,就需要增加语法插件 babel-plugin-syntax-trailing-function-commas。

  • 转译插件:用于转换这一步,将 AST 转换为能够执行的目标代码。当我们添加 转译插件 之后,在转换这一步把源码转换并输出。这也是我们使用 babel 最本质的需求。

比起语法插件,转译插件其实更好理解,比如箭头函数 (a) => a 就会转化为 function (a) {return a}。完成这个工作的插件叫做 babel-plugin-transform-es2015-arrow-functions。

同一类语法可能同时存在 语法插件 版本和 转译插件 版本,只需要选其一即可。

因为 babel 的插件处理的力度很细,所以 babel 提出,将一堆插件组合成一个 preset(预置插件包),这样,我们只需要引入一个插件组合包,就能处理代码的各种语法、语义。

  • @babel/core,babel 的核心模块,实现了上述的流程运转以及代码语法、语义分析的功能。

  • @babel/plugin-proposal-class-properties(允许类具有属性)和@babel/plugin-proposal-object-rest-spread(对象展开),plugin 开头代表插件,其实这两个插件的功能都已经并入了 ES 规范,且包含在 @babel/preset-env 中,不需要再额外引入了,这里引入只是为了作为例子。

  • @babel/preset-typescript,会处理所有 ts 的代码的语法和语义规则,并转换为 js 代码;

  • @babel/preset-react,故名思义,可以帮助处理使用 React 相关特性,例如 JSX 标签语法的转译等。

webpack 的基于 babel-loader 的处理流程#

我们解释了这么多包,还剩一个 babel-loader 还没有说。这个包就是连接 webpack 和 babel 的桥梁。我们通过配置 webpack.config.js,即可通过 webpack 使用 babel。

diff --git a/webpack.config.js b/webpack.config.js
index 2d68725..8931a9f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -12,4 +12,13 @@ module.exports = {
     // webpack 默认只处理js、jsx等js代码
     extensions: ['.js', '.jsx', '.ts', '.tsx'],
   },
+  module: {
+    rules: [
+      {
+        test: /\.tsx?$/,
+        use: 'babel-loader',
+        exclude: /node_modules/,
+      },
+    ],
+  },
 };

这一步配置的意思是,在遇到以.ts、.tsx 结尾的文件时,将代码交给 babel-loader 解析。babel-loader 将代码交给内部引用的 @babel/core 进行相关处理。同时,为防止 webpack 解析依赖库,将 node_modules 排除。

而 @babel/core 是通过什么知道,我们想要的解析配置呢。和许多其他工具类似,配置文件:ESLint (.eslintrc)、Prettier (.prettierrc)。这里就需要通过 babel 的配置文件获得。

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript",
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-proposal-object-rest-spread"
  ]
}

总结#

webpack 中 babel 的工作流程:

  1. **.ts(x)文件交给 webpack 打包
  2. webpack 将**.ts(x)文件交给 babel-loader
  3. babel-loader 将其交给@babel/core
  4. @babel/core 根据配置文件进行解析转换为 js 代码

React 通过外部引入#

那么为什么需要将 React 通过外部引入呢?

这是因为,作为一个组件库,通常需要在另一个项目中被引用,而在这个项目中可能已经引用过 React 库,因此为避免代码重复打包,影响页面性能,因此将 React 排除。

首先通过 webpack 配置 externals#

diff --git a/webpack.config.js b/webpack.config.js
index 8931a9f..9c43162 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -12,6 +12,11 @@ module.exports = {
     // webpack 默认只处理js、jsx等js代码
     extensions: ['.js', '.jsx', '.ts', '.tsx'],
   },
+  externals: {
+    // 打包过程遇到以下依赖导入,不会打包对应库代码,而是调用window上的React和ReactDOM
+    react: 'React',
+    'react-dom': 'ReactDOM',
+  },
   module: {
     rules: [

Html 中添加 React 及 React-DOm 的 CDN 链接#

<script src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>

<!--需要注意文件路径-->
<script defer src="x-components.umd.js"></script>

项目的开发依赖中引入 React 及 React-Dom#

当我们将 react 排除打包了之后,只需要通过开发依赖引入 react 即可。

yarn add -D @types/react@18.3.1 @types/react-dom@18.3.1
diff --git a/package.json b/package.json
index d071181..b0489ab 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,8 @@
     "@babel/preset-env": "^7.26.7",
     "@babel/preset-react": "^7.26.3",
     "@babel/preset-typescript": "^7.26.0",
+    "@types/react": "18.3.1",
+    "@types/react-dom": "18.3.1",
     "babel-loader": "^9.2.1",
     "@eslint/js": "^9.18.0",
     "eslint": "^9.18.0",

现在我们完成了 typescript 及 webpack 的打包。

接下来我们开始编写组件

组件编写#

创建 Button 组件#

我们先手写一个 button 组件并导出,以测试 webpack

// x-components\src\components\x-button\index.tsx
import React from 'react';

const Button = (props) => {
  const { children, ...rest } = props;

  return <button {...rest}>{children}</button>;
};

export default Button;

再通过 src/index.ts 导出

export { default as Button } from './components/x-button';

在 html 中编写测试代码

<body>
  <div id="root"></div>
  <script>
    const onClick = () => {
      alert('按钮被点击');
    };

    const rootElement = document.getElementById('root');

    const root = ReactDOM.createRoot(rootElement);

    // 前面我们在webpack中用xcomponents名导出了组件,这里可以通过window直接使用
    const button = React.createElement(
      xcomponents.Button,
      { onClick },
      '这是一个按钮'
    );
    root.render(button);
  </script>
</body>

效果 alt text

导出样式#

现在我们有了自己的组件,但是还没有样式。

我们需要编写 less 样式,

// x-components\src\components\x-button\index.less

.button {
  color: red;
}

在 button 中引入#

diff --git a/src/components/x-button/index.tsx b/src/components/x-button/index.tsx
index 434e446..0cef070 100644
--- a/src/components/x-button/index.tsx
+++ b/src/components/x-button/index.tsx
@@ -1,9 +1,10 @@
 import React from 'react';
+import './index.less';

 const Button = props => {
   const { children, ...rest } = props;

-  return <button {...rest}>{children}</button>;
+  return <button {...rest} className='button'>{children}</button>;
 };

 export default Button;

通过 webpack 导出单独的样式文件。#

其主要有一下几个步骤:

  • 引入 less。用于编译 less 为 css。
  • 引入 less-loader。用于链接 webpack 和 less 编译器。
  • 引入 css-loader。要想在 JavaScript 模块中导入 css 文件,需要安装 css-loader。
  • 引入 mini-css-extract-plugin, 用于压缩 css 代码,并将其单独打包输出。
yarn add -D less less-loader css-loader mini-css-extract-plugin
diff --git a/package.json b/package.json
index 92beb57..4c49e00 100644
--- a/package.json
+++ b/package.json
@@ -22,9 +22,13 @@
     "@types/react": "18.3.1",
     "@types/react-dom": "18.3.1",
     "babel-loader": "^9.2.1",
+    "css-loader": "^7.1.2",
     "eslint": "^9.18.0",
     "husky": "^9.1.7",
+    "less": "^4.2.2",
+    "less-loader": "^12.2.0",
     "lint-staged": "^15.3.0",
+    "mini-css-extract-plugin": "^2.9.2",
     "npmignore": "^0.3.1",
     "prettier": "3.4.2",
     "webpack": "^5.97.1"

配置 webpack

diff --git a/webpack.config.js b/webpack.config.js
index 6ae56ad..0cb2d36 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,5 +1,7 @@
const path = require('path');

+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+
module.exports = {
  entry: './src/index.ts',
  output: {
@@ -24,6 +26,15 @@ module.exports = {
        use: 'babel-loader',
        exclude: /node_modules/,
      },
+      {
+        test: /\.less$/,
+        use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
+      },
    ],
  },

效果测试#

html 中引入 css 文件

<script src="../dist/x-components.umd.js"></script>
<link href="../dist/x-components.umd.css" rel="stylesheet" />

alt text

可以看到现在按钮已经有了样式

引入 AntD#

现在将 AntD 引入,并替换原来的组件。

这里已经不需要将 AntD 的样式单独引入了。

yarn add antd
diff --git a/src/components/x-button/index.tsx b/src/components/x-button/index.tsx
index 0cef070..2f8284c 100644
--- a/src/components/x-button/index.tsx
+++ b/src/components/x-button/index.tsx
@@ -1,10 +1,13 @@
 import React from 'react';
+import { default as AntDButton, ButtonProps as AntDButtonProps } from 'antd/es/button';
 import './index.less';

-const Button = props => {
+interface ButtonProps extends AntDButtonProps {}
+
+const Button: React.FC<ButtonProps> = props => {
   const { children, ...rest } = props;

-  return <button {...rest} className='button'>{children}</button>;
+  return <AntDButton {...rest}>{children}</AntDButton>;
 };

 export default Button;

效果测试#

重新打包后:

alt text

写在最后#

本文至此全部结束,我们基于 React、TypeScript,封装 AntD 组件库,并通过 Webpack、babel 等将其打包输出。
所有代码均已提交至 github

基于React及TypeScript封装组件库之(一)项目搭建与封装AntD
https://www.aklelouch.cn/posts/2025-01/20250123-01/基于react及typescript搭建组件库之项目搭建与封装antd/
作者
@CJT  &  @WTQ
公開日
2025-01-23
ライセンス
CC BY-NC-SA 4.0