祖传项目封装组件库

1,236 阅读10分钟

前言

随着企业内部开发的项目逐渐增多,组件的维护工作变得愈益困难.

前端同学通常会面临的困境之一.开发完了A项目,在A项目下封装了大量的公共组件.当B项目启动时,由于设计风格相似,以至于A项目的很多组件可以直接复制到B项目下使用.

AB项目开发完毕,后续的CD项目又启动.每当有新项目启动时,手工复制就要重复一遍,这样的工作不仅乏味而且低效.

如果仅仅只是在新项目启动时,将旧项目的组件代码复制一遍还能勉强接受.

倘若有一天,产品经理要求D项目下的某个公共组件做一下样式的调整,并且ABC项目下的对应的公共组件同步更新,那么这样的机械劳动会随着项目数量的增多变得无休无止.

本文以企业已产出的项目为视角,将之前开发好的react组件抽离出来封装成业务型组件库,而不是打造一款通用型的组件库,抽离出来的业务型组件库和通用型组件库有哪些区别呢?

  • 我们打造的组件库以业务组件为主,服务的对象是企业内部.因而我们开发的组件库可能会包含很多业务图片,比如公司的logo.
  • 一般而言,通用型组件库的所有组件都从0实现.但业务型组件不一样,它很多功能都是依赖于第三方组件库(比如antdElement UI)做的二次开发.因此业务型的组件库很可能集成了第三方组件库依赖.

源代码在文章末尾,可下载启动运行.

实现

项目搭建

首先本地搭建一个空项目,里面创建文件夹src/components,需要封装的组件都可以复制到components下面(如下图所示);

1.png

上图案例封装了四个组件,另外还单独创建了一个入口文件index.ts.

index.ts内部分别将components下的所有组件导入并暴露给外部调用.

// index.ts文件

export { default as Button } from "./Button";

export { default as Icon } from "./Icon";

export { default as Logo } from "./Logo";

export { default as Empty } from "./Empty";

我们看下components/Button组件的文件结构(如下图).

2.png

Button组件包含两个文件,一个是less样式文件,另一个是逻辑代码index.tsx.

Button组件的index.tsx内容很简单,它引入同级的less样式,并编写了组件逻辑,最后导出提供给外部使用.

import React from 'react';
import './index.less';


type defaultProps = {
  text: string; //按钮的文案
  onClick?: any; //点击事件
};

const Button = (props: defaultProps) => {
  return (
    <div onClick={props.onClick}>
      {props.text}
    </div>
  );
};

export default Button;

文件结构介绍完毕,接下来编写webpack的配置文件将组件库代码打包输出(代码如下).

webpack配置文件的思路很简单,入口entry设置为组件库源代码src/components下对外暴露的入口文件index.ts,输出output设置成dist文件夹下.

rules里面主要处理tsxless文件.tsx文件是所有待封装组件的逻辑代码,因为组件的代码是使用ts语法编写的,因而先使用ts-loaderts语法转换后js,再使用babel去编译(具体配置细节可在源代码中查看).

less文件要先通过less-loaderless语法编译成css,再使用postcss使用插件给css加上满足各大浏览器兼容性的前缀.

postcss处理完传递给css-loader处理,最后使用MiniCssExtractPlugin插件将所有样式文件抽离出来放入app.css中.

// webpack.config.js文件

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
    entry:path.resolve(__dirname,"src/components/index.ts"), //入口
    mode:"development",
    output: { //输出
      library: 'ui-design-demo',
      libraryTarget: 'umd',
      filename: 'bundle.js',
      path:path.resolve(__dirname,"dist")
    },
    resolve: {
      extensions: ['.tsx', '.ts', '.js'],
    },
    module:{
      rules: [
        {
          test: /\.tsx?$/,
          use: ['babel-loader','ts-loader'],
          exclude: /node_modules/,
        },
        {
            test: /\.(le|c)ss$/i,
            use: [
              {
                loader:MiniCssExtractPlugin.loader
              },
              {
                loader: "css-loader"
              },
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    plugins: ['autoprefixer'],
                  }
                },
              },
              // 将 less 编译成 CSS
              'less-loader'
            ],
        }
      ]
    }, 
    plugins:[
      new MiniCssExtractPlugin({
        filename:"app.css"
      })
    ]
};

webpack最终打包生成的文件如下图所示.所有组件的js代码都封装到bundle.js,而样式文件被单独抽离放入app.css.

3.png

如果将dist目录发布到npm仓库,其他项目该如何引入该组件库呢?

调试引入

为了让其他项目能够引入当前封装的组件库,首先要在组件库根目录下的package.json修改两个字段(如下图所示)

4.png

name是自定义组件库的名称,其他项目通过import或者require引入组件库中的组件时,name对应的就是被引入的组件库名称.

main设定了当前组件库的入口文件地址.

组件库的代码并不是开发完后就一成不变的,它可能随时会依据现实情况做出功能或样式的调整.

如果修改了组件库的代码,发布到npm仓库再让其他项目安装使用,过程过于繁琐.

npm link可以简化这一流程直接在本地调试组件库的代码.

本地桌面上重新创建一个新的react测试项目,完成以下三步可直接在测试项目中引入组件库.

  • 在组件库的根目录下执行npm link命令,将当前组件库链接到全局
  • 组件库的根目录下执行命令链接到测试项目下的react(不执行这一步可能会报错),比如npm link D:\react-ui-test\node_modules\react
  • 在测试项目react-ui-test根目录下,执行命令npm link ui-design-demo链接组件库,ui-design-demo就是上面定义的name

执行完上述三步后,测试项目就可以直接引入组件库中的组件进行测试了(代码如下).

// App.js 文件

import React from "react";
import { Button } from "ui-design-demo";
import "ui-design-demo/dist/app.css";

function App() {

  const clickEvent = () => {
    console.log(123);
  }

  return (
    <div className="App">
       Hello world
       <Button call="按钮" onClick={clickEvent}/>
    </div>
  );
}

export default App;

由于组件库的css代码被单独抽离成了app.css,因此测试项目想使用组件库一般要在项目入口文件index.js处将app.css给引入进来(上面代码为了方便看就在App组件引入了css).

测试项目启动后运行效果图如下.

5.png

有了npm link的赋能,组件库的调试变得容易.测试项目可以是临时创建的新项目,也可能是已经存在的真实项目.

它们都可以在本地直接启动引入组件库,如果发现组件库的组件有需要调整的地方,直接在组件库里修改编译,测试项目这边可以实时刷新观察效果.

图标处理

一般而言,组件内不可能只包含jscss.我们日常开发的很多组件都会用到图标,图标的用途主要有以下两方面.

  • 组件库内部的组件需要使用图标
  • 外部项目引用组件库时,需要使用组件库对外暴露的图标

为了同时满足上述两点要求,我们在组件库源代码src/components下新建一个组件Icon(如下图所示)

6.png

Icon下的less文件代码如下,直接引入从iconfont下载的字体图标样式.

// index.less文件
@import url('../../fonts/iconfont.css');

Icon下的index.tsx文件代码如下,封装了一个Icon组件提供给外部使用.

import React from 'react';
import './index.less';

interface defaultProps {
  name:string;
  size?:number | string;
}


const Icon = (props: defaultProps)=>{
  return <i className={`iconfont ${props.name}`} style={{fontSize:`${props.size?props.size:12}px`}}></i>;
}

export default Icon;

紧接着源代码src/components下的入口文件index.ts要将Icon导出,最后修改一下webpack的配置文件.

配置文件webpack.config.js要在原来的基础添加上对字体文件的处理,配置完后打包生成的dist目录下会多出一个fonts文件夹,专门用来存放字体文件.

 // webpack.config.js 文件

 ... //省略
 
 module:{
      rules: [
         ...,
          {
              test:/\.(ttf|woff|woff2)$/,
              loader:"file-loader",
              options: {
                name: '[name].[ext]',
                outputPath: 'fonts'
              }
        }
      ]
      
 }    
 ...

上述工作完成后,测试项目中可以直接引入字体图标进行使用,代码如下.

import React from "react";
import {Button,Icon} from "ui-design-demo";
import "ui-design-demo/dist/app.css";

function App() {

  const clickEvent = () => {
    console.log(123);
  }

  return (
    <div className="App">
       Hello world
       <Button call="按钮" onClick={clickEvent}/>
       <Icon name="icon-zengjia" size="50"/>
    </div>
  );
}

export default App;

效果图如下:

7.png

图片处理

组件库现在需要增加一个业务组件,用于展现公司的Logo,这时候需要组件库将图片集成进来.

组件库源代码src/components下新建组件Logo,文件结构如下图所示.

8.png

Logoindex.tsx和样式代码如下.类名为Logodiv需要展现一张图片,index.less使用背景图的方式引入了图片.

这里为什么不在index.tsx文件内直接使用import将图片引入进来?因为组件库使用webpack编译打包后图片路径发生了改变,测试项目引入该组件时可能会因为图片路径不对找不到图片.

less通过background加载背景图的方式可以避免图片路径出错的问题.

这是由于css在组件库中虽然被webpack编译了一次,但测试项目的入口文件会引入组件库全局css,那时测试项目的webpack还会对css编译一次,这样就保证了图片路径不会出错.

// index.tsx文件
import React from 'react';
import "./index.less";

const Logo = () => {
  return (
    <div className="Logo">
    </div>
  );
};

export default Logo;


------------------------------------

// index.less文件

.Logo {
  width: 133px;
  height: 114px;
  background: url(../../images/logo.png) no-repeat;
}

另外webpack的配置文件需要增加对图片的处理(代码如下).

// webpack.config.js文件
 ...
 
 module:{
      rules: [
         ... // 省略
        {
          test:/\.(gif|jpg|png)$/,
          loader:"file-loader",
          options: {
            name: '[name].[ext]',
            outputPath: 'images'
          }
        }
        ...
      ]
    }

上述配置做完后,最终打包生成的dist目录多出一个images文件夹,专门用来存放组件库下的业务图片(如下图).

9.png

加载第三方组件库

如果组件库依赖了第三方组件库,比如ant design,首先需要在组件库的根目录下使用npm install antd --save安装依赖库.

antd安装完后,就可以在组件库下直接使用了.比如在源代码src/components下新建组件Empty(代码如下).

// Empty/index.tsx文件

import React from 'react';
import { Empty as Ant_Empty } from 'antd';

const Empty = () => {
  return (
    <Ant_Empty image={Ant_Empty.PRESENTED_IMAGE_SIMPLE} />
  );
};

export default Empty;

Empty组件是我们基于antd做的二次封装,我们要在src/components下的入口文件index.ts导出.另外入口文件还需要引入antd的样式文件,并在文件末尾将antd内部的组件全部导出.

import 'antd/dist/antd.css';

export { default as Button } from "./Button";

export { default as Icon } from "./Icon";

export { default as Logo } from "./Logo";

export { default as Empty } from "./Empty";

export * from "antd";

上述修改完后,组件库的配置文件webpack.config.js还需要添加一项,将reactantd排除,这样我们打包生成bundle.js体积变得轻量.

module.exports = {
    ...
    externals: {
      antd: "antd",
      react: "react"
    }
    ...

通过上述配置后,测试项目引入组件库后不再需要安装antd,测试项目可以使用组件库的自定义组件,也可以直接使用antd下的组件(代码如下).

// App.js文件(测试项目)

import React from "react";
import {Button,Icon,Logo,Switch} from "ui-design-demo";
import "ui-design-demo/dist/app.css";

function App() {

  const clickEvent = () => {
    console.log(123);
  }

  return (
    <div className="App">
       Hello world
       <Button call="按钮" onClick={clickEvent}/>
       <Icon name="icon-zengjia" size="50"/>
       <Logo/>
       <Switch/>
    </div>
  );
}

测试项目可直接从ui-design-demo中引用antd下定义的组件Switch使用,但要注意ui-design-demo下的自定义组件不要和antd下的组件重名(效果图如下).

10.png

按需加载

以上介绍的都是组件库的全量加载,这一小节我们探讨一下按需加载的具体实现.

如果在测试项目中使用按需加载的方式引入组件库该如何书写呢?(代码如下)

按需加载的含义就是用到哪个组件就只加载该组件的对应的jscss即可,下面代码手动引入了Button组件下的jscss.

这样一来组件库只有Button组件的代码被测试项目引用,测试项目打包后体积会因此变得小很多.

有的同学会说,为什么antd的按需加载不用写的这么麻烦,它的书写方式和全量加载一样的,根本不需要手动引入css?

这是由于babel-plugin-import这个插件发挥了功能,即使你按照全量加载的方式书写,babel-plugin-import会自动将语法编译成下面类似的形式.

import React from "react";
import Button from "ui-design-demo/es/Button";
import "ui-design-demo/es/Button/style.css";

function App() {

  const clickEvent = () => {
    console.log(123);
  }

  return (
    <div className="App">
       Hello world
       <Button call="按钮" onClick={clickEvent}/>
    </div>
  );
}

export default App;

现在理解了按需加载的运行机制,那么组件库打包后应该为每一个组件都要创建一个单独的文件夹,并将它们的jscss放在一起(如下图所示).

11.png

webpack想要根据组件的个数生成多个目录,入口entry需要动态生成.output也要根据组件名称创建单独的文件夹,MiniCssExtractPlugin插件将每个组件的css代码抽离出来放入该组件对应的文件夹下.

// webpack.lazy.config.js文件

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const glob = require("glob");

module.exports = (async function () {

  const entries = await getEntries();

  return {
    entry: entries,
    mode: "development",
    externals: {
      antd: "antd",
      react: "react"
    },
    output: {
      library: '[name]',
      libraryTarget: 'umd',
      filename: '[name]/index.js',
      path: path.resolve(__dirname, "es")
    }
    module:{
      ... //省略
    },
    plugins: [
      new MiniCssExtractPlugin({
        filename: "[name]/style.css"
      })
    ]
  };

}())


/**
 * 获取按需加载的入口
 */
function getEntries () {
  return new Promise((resolve) => {
    const module = {};
    glob("./src/components/**/*.tsx", (err, files) => {
      files.forEach((file) => {
        const array = file.split("/");
        const name = array[array.length - 2];
        module[name] = file;
      })
      console.log(module);
      resolve(module);
    })
  })
}

getEntries();

组件文档

组件库的文档可以采用docz自动生成,doczgithup地址如下:

 https://github.com/doczjs/docz

根据docz官方文档的说明,按照要求一步步做好相关的配置,最后在每一个组件目录下新建一个组件描述文件(代码如下).

// src/components/Button/Button.mdx 文件

---
name: 按钮
route: /Button
menu: 业务组件
---

import { Playground } from 'docz'
import Button from './index.tsx'
import './index.less'

# Button 按钮

button 按钮

## 基本用法

<Playground>
  <Button call="按钮" />
</Playground>

输入命令启动docz就可以查看组件库的在线文档了,如下图所示.

12.png

源代码

源代码