├── 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 配置
使用 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 组件库打包配置可以查看官方文档
借助
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();
})
}
开发过程中发现,有些组件库中的组件,还引用了同一组件库中的其他组件,这个时候被引用的其他组件也会被打包,使得打包体积变大,因此找到了如下方法排除这个情况的打包
首先约定 在组件代码中如果要引用当前组件库中的组件就可以通过 import {组件名} from 'component';
来引入了
在 webpack 配置中增减 resolve.alias: {components: path.resolve(__dirname, 'src/components/'}
将组件中通过 ‘component/组件名’ 引入的组件定位到正确的位置
增减如下的 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();
})),
}
module.exports = {
'plugins': [
[
'import', [
{'libraryName': 'antd', 'libraryDirectory': 'es', 'style': true},
{
'libraryName': 'components',
'camel2DashComponentName': false,
'customName': name => `../${name}`, // 这里的customName可以是一个函数,定义如何转化文件路径
'style': true,
},
]
]
]
};
import {组件名} from 'component';
这种方式,经过 webpack 编译后,组件中只有当前组件的代码,被引用的组件会被分解为 ../组件名 | ../组件名/style
;两个引用,使用组件时会动态的引用借助
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 进行按需加载
})
参考了 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
开发中发现静态文件的处理是一个问题,一般使用 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 这个图片,解决方案有一下三个
url-loader 的配置项不设置 limit 即可,但是这样一来组件的体积会很大
{
loader: 'url-loader',
options: {
limit: 1024,
name: 'static/imgs/[name].[ext]'
}
}
将静态文件拷贝到项目的 static 目录下并且需要与报错中请求资源一样的路径,这个时候就能找到文件了
// 项目的 webpack 配置中增加
plugins: [
new CopyWebpackPlugin([
{ from: 'node_modules/xxx-component/dist/static', to: 'dist/static' }, // 路径看情况而定
], options)
]
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 login # 输入用户名 密码
npm publish # 需要确保 package.json version 与上一个版本不一样
有一些 字段需要注意
{
"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/"
}
}