前戏
我是16年入了前端的坑,17年知道了gulp
和rollup
这两个玩意儿。由于那时webpack
势头很猛,便一直没有正眼瞧过它一眼。
直到20年进了一家小公司,做了很多类似的小项目,相同的代码拷来拷去,出现一个bug一堆项目都要改,实在恶心到我了。
于是不得不开始考虑将一些公共的方法和组件提取出来,做成一个第三方库来维护。而在库的封装方面,rollup
相对于webpack
具有一定的优势。
在此,便和大家分享一下如何自己造轮子,封装一个前端库并发布到npm仓库中去。
源码
这是我自己封装的3个库,有兴趣可以看看:
https://github.com/moohng/dan;
https://github.com/moohng/dui;
https://github.com/moohng/tui;
第一个是工具函数库,后面两个是UI库,目前已全部使用TypeScript重写。dui
是基于Vue 3
封装的,tui
无任何依赖,通过tsx
语法实现的。
由于后面没多久就离职了,也就没有封装更多方法和组件进去了,但还是具有一定的学习参考价值,尤其对于新手朋友。
每一个库从目录结构、打包命令、导出方式、ts类型支持、单元测试、自动发布等,几乎所有细节知识都涵盖到了。
实操
浏览百遍,不如实操一遍(文章基于tui来做介绍)。
目录结构
另外,还有lib
和dist
两个打包输出的目录没有列出来。
从上到下:
.github
:GitHub自动打包发布的脚步配置目录;packages
:存放tui组件库的源码;src
:tui组件库演示demo目录;test
:测试相关代码;webpack
:暂时忽略,用于测试webpack打包用的;
后面的babel.config.json
、gulpfile.esm.js
、rollup.config.js
、tsconfig.json
分别是babel
、gulp
、rollup
、ts
的配置文件。
环境配置
tsconfig.json
配置如下:
{
"compilerOptions": {
"jsx": "preserve", // 支持jsx
"module": "esnext",
"target": "esnext",
"declaration": true, // 生成相应的 .d.ts文件
"declarationDir": "lib", // 生成声明文件的输出路径
"noEmitOnError": true,
// "strict": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"skipLibCheck": true,
"moduleResolution": "node"
},
"include": ["packages/**/*"],
}
在配置文件中,需要开启jsx
的支持,因为tui
的html是基于jsx
语法来实现的;同时启用生成.d.ts
文件功能,这个功能可以在ts编译的时候根据我们的源码自动生成.d.ts
类型声明文件。
babel
的配置相对要复杂一些,主要是对tsx
的支持上。
部分配置源码如下:
{
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"pragma": "Tan.createElement", // 类似于 React.createElement 方法
"pragmaFrag": "Tan.Fragment", // 类似于 React.Fragment 片段
"importSource": false,
"useSpread": true
}
],
[
"@babel/preset-typescript",
{
"isTSX": true,
"jsxPragma": "Tan",
"allExtensions": true
}
]
]
}
@babel/preset-react
:用于解析jsx
语法,生成Tan.createElement('div', {})
这种js函数结构;@babel/preset-typescript
:将ts
转成js
,替代tsc
命令;
组件的实现
以最简单的Toast
组件为例。
功能:
- 提供通用、成功、失败、通知、警告、加载中等几种状态;
- 支持上、下、居中等位置弹出;
- 支持普通调用和快捷调用多种调用方式;
组件结构设计(非完整代码):
// packages/Toast/index.tsx
/** 组件入参 */
interface ToastOptions {
text?: string; // 字符串 或 html模板
type?: 'success' | 'error' | 'info' | 'warn' | 'loading' | 'toast';
margin?: string;
duration?: number;
position?: 'top' | 'bottom' | 'center';
}
/** 组件对象(支持函数调用和快捷方式调用) */
interface ToastObject {
(options: string | ToastOptions): ToastHide;
info: ToastFunction;
success: ToastFunction;
error: ToastFunction;
warn: ToastFunction;
loading: ToastFunction;
}
interface ToastFunction {
(text: string): ToastHide;
}
interface ToastHide {
(): void;
}
/** 函数实现 */
const Toast: ToastObject = (options) => {
// ...
}
/** 组件导