使用 webpack 开发 React 组件库

使用 Webpack 开发 React 组件库

组件库设计要求

  1. 使用 webpack 做模块开发以及打包
  2. 打包后的格式为 umd 模块
  3. Code Splitting 代码分割
  4. 第三方包排除打包 react、antd 、lodash 等
  5. 样式文件抽离
  6. 借助 babel-plugin-import 实现组件的按需引入

主要文件结构

├── package.json
├── script # 一些脚本
│   ├── babel.config.js # 由于 plugins 的配置需要用到函数,所以这里定义的是 js
│   ├── config.js # 一些路径常量的定义
│   └── url-loader # 对 url-loader 进行了修改,增加了对 图片来源的判断
├── src # 源码
│   ├── components # 组件库
│   ├── index.js # 总出口
│   ├── static # css\imgs 等静态文件
│   └── styles # 全局的样式
└── webpack.config.js # webpack 配置

主要功能实现

1. 编写组件库开发环境的配置

使用 webpack 开发组件库可以按照以下配置定义


// 定义一个函数,用来将组件库目录下的文件名称及路径便利出来
const fs = require('fs');
const path = require('path');
const rootDir = path.resolve(__dirname, '../');
const componentDir = 'src/components';
const cModuleNames = fs.readdirSync(path.resolve(componentDir));

const cModuleMap = cModuleNames.reduce((prev, name) => {
  prev[name] = path.join(rootDir, `${componentDir}/${name}/index.js`);

  return prev;
}, {});

// wenpack 配置
module.exports = {
  // 入口处设置为多入口,即每一个组件都作为一个入口,这样输出的可以是拆分后的组件
  entry: {
    index: config.entryFile,
    ...config.cModuleMap // 组件的名称及位置
  },
  output: {
    path: config.outputDir, // 要输出多文件这里就要配置输出目录而不是当个文件
    filename: '[name]/index.js',
    // output.library 和 output.libraryTarget 一起使用 对外暴露 library 及定义输出组件库格式
    library: ['xxx-components', '[name]'], 
    libraryTarget: 'umd',
    publicPath: '/'
  },
}

更多 webpack 组件库打包配置可以查看官方文档

2. 如何排除,不打包第三方包

借助 webpackConfig.externals 实现第三方包排除,可以将第三方包外部化,externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法

排除前需要找到第三方包,在组件库开发中可以约定第三方包名称,或者将真个 package.json 中的 dependencies 设定为需要排除的第三方包

/*
  externals 的值可以是一个数组,数组的每一项都是一个函数,函数接收三个参数(如下)
  其中 request 是当前处理的 模块引用路径
*/ 
module.exports = {
  externals: Object.keys(pkg.dependencies).map(pkgName => (context, request, callback) => {
    // 逻辑:以模块名 pkgName 开始的引用都将视为外部模块
    request.indexOf(pkgName) === 0 ? callback(null, request) : callback();
  })
}

开发过程中发现,有些组件库中的组件,还引用了同一组件库中的其他组件,这个时候被引用的其他组件也会被打包,使得打包体积变大,因此找到了如下方法排除这个情况的打包

  1. 首先约定 在组件代码中如果要引用当前组件库中的组件就可以通过 import {组件名} from 'component'; 来引入了

  2. 在 webpack 配置中增减 resolve.alias: {components: path.resolve(__dirname, 'src/components/'} 将组件中通过 ‘component/组件名’ 引入的组件定位到正确的位置

  3. 增减如下的 externals 配置

/*
  Object.keys(config.cModuleMap) 拿到的时 组件库每个组件的名称
  以下的逻辑就是:以 `../${组件名称}` 或者 ‘component’ 开头的引用都将视为引用外部模块,排除打包
*/
module.exports = {
  externals: Object.keys(pkg.dependencies).concat(['components']).map(pkgName => (context, request, callback) => {
    request.indexOf(pkgName) === 0 ? callback(null, request) : callback();
  }).concat(Object.keys(config.cModuleMap).map(pkgName => (context, request, callback) => {
    request.indexOf(`../${pkgName}`) === 0 ? callback(null, request) : callback();
  })),
}
  1. 定义 babel.config.js 如下
module.exports = {
  'plugins': [
    [
      'import', [
      {'libraryName': 'antd', 'libraryDirectory': 'es', 'style': true},
      {
        'libraryName': 'components',
        'camel2DashComponentName': false,
        'customName': name => `../${name}`, // 这里的customName可以是一个函数,定义如何转化文件路径
        'style': true,
      },
    ]
    ]
  ]
};
  1. 通过以上配置 组件中引用其他组件就可以通过 import {组件名} from 'component'; 这种方式,经过 webpack 编译后,组件中只有当前组件的代码,被引用的组件会被分解为 ../组件名 | ../组件名/style ;两个引用,使用组件时会动态的引用

2. 样式文件抽离

借助 mini-css-extract-plugin webpack 插件实现样式提取

// loader 部分
[
  {loader: MiniCssExtractPlugin.loader, options: {publicPath: '../../'}},
  {loader: 'css-loader', options: {
      modules: true,
      sourceMap: true,
      localIdentName: '[folder]__[local]'
    }},
  {loader: 'postcss-loader', options: {config: {path: './.postcssrc.js'}}},
]

// plugin 部分
new MiniCssExtractPlugin({
  filename: '[name]/style/index.css', // 这种文件路径格式是为了方便 babel-plugin-import 进行按需加载
})

3. 按需引入组件实现

参考了 antd 的按需引入方式:通过 babel-plugin-import 插件,在 babel 运行时,将类似 import { ModuleName } from 'libiaryName'; 的代码转化为组件所在的路径,这样实际引用的就是这个组件的模块而不是整个 Library

// babel 前
import { ModuleName } from 'libiaryName';

// babel 后
import ModuleName from 'libiaryName/ModuleName/index.js';
import 'libiaryName/ModuleName/style/index.css';

由此,实现按需加载的关键就是保证编译后输出的组件有如下格式

├── LoadingButton
│   ├── index.js
│   └── style
│       └── index.css
├── TestButton
│   ├── index.js
│   └── style
│       └── index.css
├── TestLodash
│   └── index.js
├── TestWeb
│   ├── index.js
│   └── style
│       └── index.css

4. 静态文件的处理

开发中发现静态文件的处理是一个问题,一般使用 webpack 将项目时会将静态文件扔到 一个 static 目录下, 文件小的可以打包到组件代码中

但是在使用 webpack 开发组件库时,如果还是使用同样的方法处理图片等资源,将会发现css 中引用图片可以被正确找到,jsx 文件中 import 的文件不会被正确找到,原因如下案例

// 1. 组件 TempComponent 代码中使用图片
import NotFoundSvg from '../../static/imgs/notFound.svg';

<img alt="" src={NotFoundSvg} />

// 2. 使用 url-loader 打包后,由于图片较大,将会被输出为单个文件

// 3. 组件代码中保留了对这个资源的引用
/***/ "./src/static/imgs/fetchError.svg":
/*!****************************************!*\
  !*** ./src/static/imgs/fetchError.svg ***!
  \****************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("module.exports = __webpack_require__.p + \"static/imgs/fetchError.svg\";\n\n//# sourceURL=webpack://xxx-components.%5Bname%5D/./src/static/imgs/fetchError.svg?");

// 4. 在一个项目中使用这个组件库中的组件
import {TempComponent} from 'xxx-components';

class Workspace extends React.Component<Props> {
  render(): React.Element<any> {
    return (
      <div className="container" >
        <Button>ajsdio</Button>
        <TempComponent type={"NO_Found"}/>
      </div>
    )
  }
}

// 5. 运行后报错
// noData.svg:1 GET http://localhost:10243/static/imgs/noData.svg 404 (Not Found)

以上可见 static/imgs/noData.svg 中并没有 noData.svg 这个图片,解决方案有一下三个

1. 所有图片都打包到组件代码中,不再单独提取出来

url-loader 的配置项不设置 limit 即可,但是这样一来组件的体积会很大

{
  loader: 'url-loader',
  options: {
    limit: 1024,
    name: 'static/imgs/[name].[ext]'
  }
}

2. 使用 copy-webpack-plugin 将组件库中的静态文件拷贝到项目中

将静态文件拷贝到项目的 static 目录下并且需要与报错中请求资源一样的路径,这个时候就能找到文件了

  // 项目的 webpack 配置中增加
  plugins: [
    new CopyWebpackPlugin([
      { from: 'node_modules/xxx-component/dist/static', to: 'dist/static' }, // 路径看情况而定
    ], options)
  ]

3. 修改 url-loader 使得 css 中引入图片可以被提取,而 jsx 中引入的图片直接打包

url-loader 中没有现成的接口来判断,当前处理的静态资源文件是哪个文件来源,因此如下方案是一个临时方案

// 1. 首先将 url-loader 替换为我们改写后的 url-loader
{
  loader: path.join(config.scriptDir, 'url-loader'),
  options: {
    // name: '[folder]/imgs/[name].[ext]',
    defaultLimit: 2 * 1024,
    useInlineImageInJs: true,
    name: 'static/imgs/[name].[ext]'
  }
},

/*
  2. 在改写的 url-loader 中增加如下判断 
  主要是 判断 this._compilation.name 属性中是否包含 mini-css-extract-plugin 处理的样式, 含有则静态资源的来源是 style 否则是 script 
  如果是 stylle 来源则抽离否则将其打包到代码中
*/ 
function loader(src) {
  const options = (0, _loaderUtils.getOptions)(this) || {};
  
  /* ---------------- 新添加的代码 START ---------------------*/
  const isFromCss = /^mini-css-extract-plugin.+\.(less|css)$/.test(this._compilation.name);

  if (options.useInlineImageInJs) {
    if (!isFromCss) {
      options.limit = undefined;
    } else {
      options.limit = options.defaultLimit ? parseInt(options.defaultLimit, 10) : undefined;
    }
  }
  /* ---------------- 新添加的代码 END ---------------------*/
}

弊端:改写的 url-loader 需要在组件库项目中维;this._compilation.name 这个属性也不是官方推荐的

优势:很大程度的减少组件体积

npm 线上发布

1. 创建 npm 账户

创建地址

2. login

npm login # 输入用户名 密码

3. publish

npm publish # 需要确保 package.json version 与上一个版本不一样

4. package.json 说明

有一些 字段需要注意

{
  "name": "xxx-ui", // 包名
  "version": "0.1.4", // 版本,每次都要不一样
  "main": "es/index/index.js", // 规范的,定义程序入口文件
  "module": "es/index/index.js", // 新的,定义使用 es 模块的入口文件
  "files": [ // npm publish 中约定可上传的文件夹
    "dist",
    "src",
    "es"
  ],
  "directories": { 
    "es": "es"
  },
  "publishConfig": { // 设置模块发布时发布到的镜像仓库地址
    "registry": "http://localhost:8081/repository/xxx-ui/"
  }
}

你可能感兴趣的:(前端构建工具,webpack)