React + TypeScript 从零开发Popup组件并发布到 npm
0 条评论在组件库系列文章中介绍了如何从 0 到 1 搭建一个 React 组件库架子,但为了一两个组件去搭建组件库未免显得大材小用。
这次以移动端一个常用组件 Popup
为例,以最方便快捷的形式发布一个完整的 npm 包。
本文包含以下内容:
Popup
组件的开发;一些工具的使用
- tsdx :项目初始化、开发以及打包大管家;
- np:一键发布 npm 包;
- gh-pages:部署示例 demo ;
- readme-md-generator:生成一份规范的
README.md
文件。
本文不会和组件库那篇文章一般死扣打包细节,因为单个组件和组件库的打包有本质上的区别:
- 组件库需要提供按需引入的能力,所以对组件仅仅是进行了语法上的编译(以及比较绕的样式处理),故选择了 gulp 管理打包流程;
- 单组件一般不需要提供按需引入的能力,只需要打包出一个 js bundle 和 css bundle 即可,webpack 与 rollup 就更适用于此类场景。
项目初始化
tsdx 内置三种项目模板:
- basic => 工具包模板
- react => React 组件模板,使用 parcel 用作 example 调试
- react-with-storybook => 同上,使用 storybook 编写文档以及 example 调试
模板还内置了start
、build
、test
以及lint
等 npm scripts,的确是零配置开箱即用(大误)。
为了方便讲解,此处选择react
模板。
执行npx tsdx create react-easy-popup
,选择react
完成项目创建后进入项目目录。
配置 tsdx
由于tsdx
没有提供样式文件打包支持,使用css in js
方案会带来额外的依赖以及运行时消耗,所以需要简单配置一下tsdx
以支持 less 样式。
参照customization-tsdx这一小节进行配置。
安装相关依赖:
1 | yarn add rollup-plugin-postcss autoprefixer cssnano less --dev |
新建 tsdx.config.js
,写入以下内容:
tsdx.config.js
1 | const postcss = require('rollup-plugin-postcss'); |
在 package.json
中配置browserslist
字段。
package.json
1 | // ... |
清空src
目录,新建index.tsx
、index.less
。
src/index.tsx
1 | import * as React from 'react'; |
src/index.less
1 | .react-easy-popup { |
example/index.tsx
1 | import 'react-app-polyfill/ie11'; |
进入项目根目录,执行以下命令:
1 | yarn start |
现在 src
目录下的内容的变更会被实时监听,在根目录下生成的dist
文件夹包含打包后的内容。
开发时调试的文件夹为example
,另起一个终端。执行以下命令:
1 | cd example |
在localhost:1234
可以发现项目启动啦,样式生效且有浏览器前缀。
若 example 启动后网页报错,删除 example 下的.cache 以及 dist 目录重新 start
需要注意的是 example
的入口文件index.tsx
引入的是我们打包后的文件,即dist/index.js
。
但是引入路径却为'../.'
,这是因为 tsdx
使用了 parcel
的 aliasing。
同时,观察根目录下的dist
文件夹:
dist
1 | ├── index.d.ts # 组件声明文件 |
也可以很轻易地在package.json
中找到main
、module
以及typings
相关配置。
基于 rollup 手动搭一个组件模板并不困难,但是社区已经提供了方便的轮子,就不要重复造轮子啦。既要有造轮子的能力,也要有不造轮子的觉悟。似乎我们正在造轮子?
实现 Portal
Popup
在移动端场景下极其常见,其内部基于Portal
实现,自身又可以作为Toast
和Modal
等组件的下层组件。
要实现Popup
,就要先基于ReactDOM.createPortal实现一个Portal
。
此处结合官方文档做一个简单总结。
- 什么是传送门?
Portal
是一种将子节点渲染到存在于父组件以外的DOM
节点的优秀的方案。 - 为什么需要传送门?父组件有
overflow: hidden
或z-index
样式,我们又需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。
同时还有很重要的一点:portal
与普通的 React
子节点行为一致,仍存在于React
树,所以Context
依旧可以触及。有一些弹层组件会提供xxx.show()
的 API 形式进行弹出,这种调用形式较为方便,虽然底层也是基于Portal
,但是内部重新执行了ReactDOM.render
,脱离了当前主应用的React
树,自然也无法获取到Context
。
清空 src 目录,新建以下文件:
1 | ├── index.less # 样式文件 |
在编写代码之前,需要确定好Portal
组件的 API。
与ReactDOM.createPortal
方法接受的参数基本一致:指定的挂载节点以及内容。唯一的区别是:Portal
在未传入指定的挂载节点时,会创建一个节点以供使用。
属性 说明 类型 默认值
node 可选,自定义容器节点 HTMLElement -
children 需要传送的内容 ReactNode -
在type.ts
中写入Portal
的Props
类型定义。
src/type.ts
1 | export type PortalProps = React.PropsWithChildren<{ |
现在开始编写代码:
1 | import * as React from 'react'; |
注意:此处没有使用 React.FC 去进行声明 react-typescript-cheatsheet:Section 2: Getting Started => Function Components => What about
React.FC
/React.FunctionComponent
?
代码实现比较简单,就是调用了一下ReactDOM.createPortal
,没有考虑到使用者未传入node
的情况:需要内部创建,组件销毁时销毁该node
。
1 | import * as React from "react"; |
同时为了让非 ts 用户能够享受到良好的运行时错误提示,需要安装prop-types
。
1 | yarn add prop-types |
src/portal.tsx
1 | // ... |
这样就完成了 Portal
组件的编写,在入口文件进行导出。
src/index.ts
1 | export { default as Portal } from './portal'; |
example/index.ts
中引入Portal
,进行测试。
example/index.tsx
1 | import "react-app-polyfill/ie11"; |
在网页中看到预期的DOM
结构。
实现 Popup
API 梳理
老规矩,先规划 API,写好类型定义,再动手写代码。
我写这个组件的时候参考了Popup-cube-ui。
最终确定 API 如下:
属性 说明 类型 默认值
visible 可选,控制 popup 显隐 boolean false
position 可选,内容定位 ‘center’ / ‘top’ / ‘bottom’ / ‘left’ / ‘right’ ‘center’
mask 可选,控制蒙层显隐 boolean true
maskClosable 可选,点击蒙层是否可以关闭 boolean false
onClose 可选,关闭函数,若 maskClosable 为 true,点击蒙层调用该函数 function ()=>{}
node 可选,元素挂载节点 HTMLElement -
destroyOnClose 可选,关闭是否卸载内部元素 boolean false
wrapClassName 可选,自定义 Popup 外层容器类名 string ‘’
src/type.ts
1 | export type Position = 'top' | 'right' | 'bottom' | 'left' | 'center'; |
编写 Popup
的基本结构。
src/popup.tsx
1 | import * as React from 'react'; |
在入口文件进行导出。
src/index.ts
1 | + export { default as Popup } from './popup'; |
前置 CSS 知识
在正式开发逻辑之前,先明确一点:
蒙层 Mask 以及内容 Content 入场以及出场均有动画效果。具体表现为:蒙层为 Fade 动画,内容则取决于当前 position,比如内容在中间(position === ‘center’),则其动画效果为 Fade,如果在左边(position === ‘left’),则其动画效果为 SlideRight,其他 position 以此类推。
再回顾张鑫旭大大的一篇文章:小 tip: transition 与 visibility
划重点:
opacity
的值在0
与1
之间相互过渡(transition
)可以实现 Fade 动画。然而元素即使透明度变成 0,肉眼看不见,在页面上却依旧点击,还是可以覆盖其他元素的,我们希望元素淡出动画结束后,元素可以自动隐藏;- 元素隐藏很容易想到
display:none
。而display:none
无法应用transition
效果,甚至是破坏作用; visibility:hidden
可以看成visibility:0
;visibility:visible
可以看成visibility:1
。实际上,只要visibility
的值大于0
就是显示的。
总结一下:我们想用opacity
实现淡入淡出的 Fade 动画,但是希望元素淡出后,能够隐藏,而不仅仅是透明度为 0
,覆盖在其他元素上。所以需要配置 visibility
属性,淡出动画结束时,visibility
值也由visible
变为了hidden
,元素成功隐藏。
如果蒙层淡出动画结束后仅仅是透明度变为 0,却未隐藏,那么蒙层在视觉上虽然消失了,实际还是覆盖在页面上,就无法触发页面上的事件。
预设动画样式
借助react-transition-group完成动画效果,需要内置一些动画样式。
新建animation.less
,写入以下动画样式。
展开查看代码
完成基本逻辑
安装相关依赖。
1 | yarn add react-transition-group classnames |
- node: 透传给
Portal
即可; - visible: 将该属性赋值给蒙层以及内容外层
CSSTransition
组件的in
属性,控制蒙层以及内容的过渡显隐; - destroyOnClose: 将该属性赋值给内容外层
CSSTransition
组件的unmountOnExit
属性,决定隐藏时是否卸载内容节点; - wrapClassName: 拼接在外层容器节点的
className
; - position: 1)用于获取内容节点的对应动画名称;2)决定容器节点以及内容节点类名,配合样式决定内容节点位置;
- mask: 决定蒙层节点的
className
,从而控制蒙层有无; - maskClose: 决定点击蒙层是否触发 onClose 函数。
用过 antd
的同学都知道,antd
的modal
在首次visible === true
之前,内容节点是不会被挂载的,只有首次 visible === true
,内容节点才挂载,而后都是样式上隐藏,而不会去卸载内容节点,除非手动设置 destroyOnClose
属性,我们也顺带实现这个特点。
代码逻辑比较简单,在拼接类名时注意配合样式文件一起阅读,重要的点都有注释标出。
展开查看逻辑代码
展开查看样式代码
组件编写完毕,接下来在example/index.ts
中编写相关示例测试功能即可。
部署 github pages
相信大多数人使用一个 npm 包会先看示例再看文档。
接下来将 example
中的示例项目打包,并部署到 github pages 上。
安装gh-pages
。
1 | yarn add gh-pages --dev |
package.json 新增脚本。
package.json
1 | { |
由于 gh-pages 默认部署在https://username.github.io/repo
下,而非根路径。为了能够正确引用到静态资源,还需要修改打包的 public-url
。
修改 example 的 package.json 中的打包命令:
1 | { |
https://username.github.io/repo
记得换成你自己的哦。
在根目录下执行 yarn deploy
,等脚本执行完再去看看吧。
编写 README.md
一份规范的 README 会显得作者很专业,此处使用readme-md-generator
生成基本框架,向里面填充内容即可。
readme-md-generator:📄 CLI that generates beautiful README.md files
1 | npx readme-md-generator -y |
使用 np 发包
在上一篇文章中,专门编写了一个脚本来处理以下六点内容:
- 版本更新
生成 CHANGELOG- 推送至 git 仓库
- 组件打包
- 发布至 npm
- 打 tag 并推送至 git
这次就不生成 CHANGELOG 文件了,其他五点配合np
,操作十分简单。
np:A better npm publish
1 | yarn add np --dev |
package.json
1 | { |
1 | npm login |
--no-yarn
: 不使用yarn
。发包时出现 npm 与 yarn 之间的一些问题;--no-tests
:测试用例暂时还未编写,先跳过;--no-cleanup
:发包时不要重新安装 node_modules;- 首次发布新包时可能会报错,因为 np 进行了 npm 双因素认证,但依旧可以发布成功,等后续更新。
更多配置请查看官方文档。
本文转载自
https://github.com/worldzhao/blog/issues/2
- 本文链接:https://xuehuayu.cn/article/624d1918.html
- 版权声明:① 标为原创的文章为博主原创,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接。② 标为转载的文章来自网络,已标明出处,侵删。