Open worldzhao opened 3 years ago
我加了配置以后,修改src组件代码,Example得代码不会更新,up有遇到这种情况吗?
我加了配置以后,修改src组件代码,Example得代码不会更新,up有遇到这种情况吗?
tsdx 感觉很久没有维护了... 我两年前处理是没有问题 🤣
这个库2年没维护了,现在以用,挺多坑得
我加了配置以后,修改src组件代码,Example得代码不会更新,up有遇到这种情况吗?
tsdx 感觉很久没有维护了... 我两年前处理是没有问题 🤣
推荐用 microbundle 打包,storybook 开发。
前言
在组件库系列文章中介绍了如何从 0 到 1 搭建一个 React 组件库架子,但为了一两个组件去搭建组件库未免显得大材小用。
这次以移动端一个常用组件
Popup
为例,以最方便快捷的形式发布一个完整的 npm 包。本文包含以下内容:
Popup
组件的开发;README.md
文件。本文不会和组件库那篇文章一般死扣打包细节,因为单个组件和组件库的打包有本质上的区别:
项目初始化
tsdx 内置三种项目模板:
模板还内置了
start
、build
、test
以及lint
等 npm scripts,的确是零配置开箱即用(大误)。为了方便讲解,此处选择
react
模板。执行
npx tsdx create react-easy-popup
,选择react
完成项目创建后进入项目目录。配置 tsdx
由于
tsdx
没有提供样式文件打包支持,使用css in js
方案会带来额外的依赖以及运行时消耗,所以需要简单配置一下tsdx
以支持 less 样式。参照customization-tsdx这一小节进行配置。
安装相关依赖:
新建
tsdx.config.js
,写入以下内容:tsdx.config.js
在
package.json
中配置browserslist
字段。package.json
清空
src
目录,新建index.tsx
、index.less
。src/index.tsx
src/index.less
example/index.tsx
进入项目根目录,执行以下命令:
现在
src
目录下的内容的变更会被实时监听,在根目录下生成的dist
文件夹包含打包后的内容。开发时调试的文件夹为
example
,另起一个终端。执行以下命令:在
localhost:1234
可以发现项目启动啦,样式生效且有浏览器前缀。需要注意的是
example
的入口文件index.tsx
引入的是我们打包后的文件,即dist/index.js
。但是引入路径却为
'../.'
,这是因为tsdx
使用了parcel
的 aliasing。同时,观察根目录下的
dist
文件夹:dist
也可以很轻易地在
package.json
中找到main
、module
以及typings
相关配置。实现 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 目录,新建以下文件:
在编写代码之前,需要确定好
Portal
组件的 API。与
ReactDOM.createPortal
方法接受的参数基本一致:指定的挂载节点以及内容。唯一的区别是:Portal
在未传入指定的挂载节点时,会创建一个节点以供使用。在
type.ts
中写入Portal
的Props
类型定义。src/type.ts
现在开始编写代码:
代码实现比较简单,就是调用了一下
ReactDOM.createPortal
,没有考虑到使用者未传入node
的情况:需要内部创建,组件销毁时销毁该node
。同时为了让非 ts 用户能够享受到良好的运行时错误提示,需要安装
prop-types
。src/portal.tsx
这样就完成了
Portal
组件的编写,在入口文件进行导出。src/index.ts
example/index.ts
中引入Portal
,进行测试。example/index.tsx
在网页中看到预期的
DOM
结构。实现 Popup
API 梳理
老规矩,先规划 API,写好类型定义,再动手写代码。
我写这个组件的时候参考了Popup-cube-ui。
最终确定 API 如下:
src/type.ts
编写
Popup
的基本结构。src/popup.tsx
在入口文件进行导出。
src/index.ts
前置 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
,元素成功隐藏。预设动画样式
借助react-transition-group完成动画效果,需要内置一些动画样式。
新建
animation.less
,写入以下动画样式。展开查看代码
```less @animationDuration: 300ms; .react-easy-popup { /* Fade */ &-fade-enter, &-fade-appear, &-fade-exit-done { visibility: hidden; opacity: 0; } &-fade-appear-active, &-fade-enter-active { visibility: visible; opacity: 1; transition: opacity @animationDuration, visibility @animationDuration; } &-fade-exit, &-fade-enter-done { visibility: visible; opacity: 1; } &-fade-exit-active { visibility: hidden; opacity: 0; transition: opacity @animationDuration, visibility @animationDuration; } /* SlideUp */ &-slide-up-enter, &-slide-up-appear, &-slide-up-exit-done { transform: translate(0, 100%); } &-slide-up-enter-active, &-slide-up-appear-active { transform: translate(0, 0); transition: transform @animationDuration; } &-slide-up-exit, &-slide-up-enter-done { transform: translate(0, 0); } &-slide-up-exit-active { transform: translate(0, 100%); transition: transform @animationDuration; } /* SlideDown */ &-slide-down-enter, &-slide-down-appear, &-slide-down-exit-done { transform: translate(0, -100%); } &-slide-down-enter-active, &-slide-down-appear-active { transform: translate(0, 0); transition: transform @animationDuration; } &-slide-down-exit, &-slide-down-enter-done { transform: translate(0, 0); } &-slide-down-exit-active { transform: translate(0, -100%); transition: transform @animationDuration; } /* SlideLeft */ &-slide-left-enter, &-slide-left-appear, &-slide-left-exit-done { transform: translate(100%, 0); } &-slide-left-enter-active, &-slide-left-appear-active { transform: translate(0, 0); transition: transform @animationDuration; } &-slide-left-exit, &-slide-left-enter-done { transform: translate(0, 0); } &-slide-left-exit-active { transform: translate(100%, 0); transition: transform @animationDuration; } /* SlideRight */ &-slide-right-enter, &-slide-right-appear, &-slide-right-exit-done { transform: translate(-100%, 0); } &-slide-right-enter-active, &-slide-right-appear-active { transform: translate(0, 0); transition: transform @animationDuration; } &-slide-right-exit, &-slide-right-enter-done { transform: translate(0, 0); } &-slide-right-exit-active { transform: translate(-100%, 0); transition: transform @animationDuration; } } ```完成基本逻辑
安装相关依赖。
Portal
即可;CSSTransition
组件的in
属性,控制蒙层以及内容的过渡显隐;CSSTransition
组件的unmountOnExit
属性,决定隐藏时是否卸载内容节点;className
;className
,从而控制蒙层有无;用过
antd
的同学都知道,antd
的modal
在首次visible === true
之前,内容节点是不会被挂载的,只有首次visible === true
,内容节点才挂载,而后都是样式上隐藏,而不会去卸载内容节点,除非手动设置destroyOnClose
属性,我们也顺带实现这个特点。展开查看逻辑代码
```jsx // 类名前缀 const prefixCls = "react-easy-popup"; // 动画时长 const duration = 300; // 位置与动画的映射 const animations: { [key in Position]: string } = { bottom: `${prefixCls}-slide-up`, right: `${prefixCls}-slide-left`, left: `${prefixCls}-slide-right`, top: `${prefixCls}-slide-down`, center: `${prefixCls}-fade`, }; const Popup = (props: PopupProps) => { const firstRenderRef = React.useRef(false); const { visible } = props; // 在首次visible === true之前 都返回null if (!firstRenderRef.current && !visible) return null; if (!firstRenderRef.current) { firstRenderRef.current = true; } const { node, mask, maskClosable, onClose, wrapClassName, position, destroyOnClose, children, } = props; // 蒙层点击事件 const onMaskClick = () => { if (maskClosable) { onClose(); } }; // 拼接容器节点类名 const rootCls = classnames( prefixCls, wrapClassName, `${prefixCls}__${position}` ); // 拼接蒙层节点类名 const maskCls = classnames(`${prefixCls}-mask`, { [`${prefixCls}-mask__visible`]: mask, }); // 拼接内容节点类名 const contentCls = classnames( `${prefixCls}-content`, `${prefixCls}-content__${position}` ); // 内容过渡动画 const contentAnimation = animations[position]; return (展开查看样式代码
```less @import './animation.less'; @popupPrefix: react-easy-popup; .@{popupPrefix} { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1999; pointer-events: none; // 特别注意:为none时可以产生点透的效果 可以理解为容器节点压根不存在 .@{popupPrefix}-mask { position: absolute; top: 0; left: 0; display: none; // mask默认隐藏 width: 100%; height: 100%; overflow: hidden; background-color: rgba(0, 0, 0, 0.72); pointer-events: auto; &__visible { display: block; // 展示mask } // fix some android webview opacity render bug &::before { display: block; width: 1px; height: 1px; margin-left: -10px; background-color: rgba(0, 0, 0, 0.1); content: '.'; } } /* position为center时 使用flex居中 */ &__center { display: flex; align-items: center; justify-content: center; } .@{popupPrefix}-content { position: relative; width: 100%; color: rgba(113, 113, 113, 1); pointer-events: auto; -webkit-overflow-scrolling: touch; /* ios5+ */ ::-webkit-scrollbar { display: none; } &__top { position: absolute; left: 0; top: 0; } &__bottom { position: absolute; left: 0; bottom: 0; } &__left { position: absolute; width: auto; max-width: 100%; height: 100%; } &__right { position: absolute; right: 0; width: auto; max-width: 100%; height: 100%; } &__center { width: auto; max-width: 100%; } } } ```组件编写完毕,接下来在
example/index.ts
中编写相关示例测试功能即可。example/index.ts
部署 github pages
相信大多数人使用一个 npm 包会先看示例再看文档。
接下来将
example
中的示例项目打包,并部署到 github pages 上。安装
gh-pages
。package.json 新增脚本。
package.json
由于 gh-pages 默认部署在
https://username.github.io/repo
下,而非根路径。为了能够正确引用到静态资源,还需要修改打包的public-url
。修改 example 的 package.json 中的打包命令:
在根目录下执行
yarn deploy
,等脚本执行完再去看看吧。编写 README.md
一份规范的 README 会显得作者很专业,此处使用
readme-md-generator
生成基本框架,向里面填充内容即可。readme-md-generator:📄 CLI that generates beautiful README.md files
README.md
使用 np 发包
在上一篇文章中,专门编写了一个脚本来处理以下六点内容:
生成 CHANGELOG这次就不生成 CHANGELOG 文件了,其他五点配合
np
,操作十分简单。np:A better
npm publish
package.json
--no-yarn
: 不使用yarn
。发包时出现 npm 与 yarn 之间的一些问题;--no-tests
:测试用例暂时还未编写,先跳过;--no-cleanup
:发包时不要重新安装 node_modules;更多配置请查看官方文档。