Open WangShuXian6 opened 1 month ago
React hooks 是强大的、可重用的函数,可以以简单的一行代码的形式嵌入到任何你选择的 React 组件中。
精心设计的自定义 React hooks 可以将许多复杂性抽象到组件之外。
结合 TypeScript 的类型功能,你的 React hooks 可以变得更加强大且易于使用。
在本课程中,我们将从一个完全空白的画布开始,逐步重建 ReactUsePleaseStay,
这是我发布为 npm 包的一个 React hook,它可以同时为网页的标题和 favicon 添加动画效果。
它通常用作一个有趣的视觉提醒,吸引访问者回到你的网站。
在详细的逐步演示课程中,我们将构建 ReactUsePleaseStay 的所有有趣功能,
还将添加开发者体验改进、创建一个交互式演示页面,并将这个 hook 发布到 npm。
最终,我们将获得一个面向终端用户和开发者都能使用的最先进的包。
在开始构建 hooks 项目之前,首先要确保大家的环境设置相同。
环境主要由 Node 和 NPM 组成。
NVM 管理不同的 Node.js 版本。
这个工具可以让我们轻松切换不同版本的 Node.js。
我们将使用 NVM 来安装最新的长期支持 (LTS) 版本的 Node.js。
根据 NVM 安装文档,NVM 可以通过 curl 或 wget 安装。
NVM 会提示你关闭并重新打开终端,
但因为它已经将内容追加到 shell 的配置文件中,我们可以直接运行以下命令来重新加载配置文件。
我使用的是 zsh,所以我会运行 source,然后是波浪号表示主目录,接着是 .zshrc。
source ~/.zshrc
如果你使用的是 bash,你可能需要 source .bashrc 文件,或者甚至是 .bash_profile。
你可以通过运行以下命令来检查 NVM 是否已正确安装并加载:
nvm list
你应该会看到已安装的 Node.js 版本列表,默认情况下至少会安装一个版本。
可以使用以下命令安装这个版本的 Node.js:
nvm install 16.18.1
由于我们在接下来的课程中都会使用这个版本,所以让我们将其设置为 Node.js 的默认版本。
我们可以通过
nvm alias default 16.18.1
来设置。
这意味着每次你打开新的终端时,NVM 都会自动为你选择 Node.js 版本 16.18.1。
为了确认 NVM 是否正常工作,我们可以简单地打开一个新终端并运行
node -v
我们当然得到了预期的输出:16.18.1。
从零开始重建 react-use-please-stay。
首先,打开终端,创建一个名为 react-use-please-stay 的新目录。
然后,进入该目录。
接着,我们使用 npm init 初始化项目。
按回车键接受默认的包名称和版本号。
在描述提示符中,我写上 "一个有趣的 React hook,可以在页面失去焦点时为文档标题和 favicon 添加动画效果"。
对于入口点,index.js 是可以接受的默认选项。
测试命令可以暂时留空。
接下来,我们到 GitHub 上准备一个代码仓库。
在我的情况下,我已经有一个名为 react-use-please-stay 的仓库。
https://github.com/WangShuXian6/react-use-please-stay.git
对于描述,我们可以直接复制 package.json 中的描述并粘贴到这里。
创建仓库后,复制该 URL。
返回 npm init,将这个 URL 粘贴进去。
对于关键词,我写上 react、typescript、react hook、react hooks。
接着填写我的名字。
选择 MIT 许可证。
我喜欢使用 MIT 许可证,因为它比 ISC 许可证更加明确和详细。
确认输入 "yes" 后,一切看起来正常。
如果我们打开该文件夹中的代码,可以看到 package.json
文件已正确生成。
{
"name": "react-use-please-stay",
"version": "1.0.0",
"description": "一个有趣的 React hook,可以在页面失去焦点时为文档标题和 favicon 添加动画效果",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WangShuXian6/react-use-please-stay.git"
},
"keywords": [
"react、typescript、react",
"hook、react",
"hooks"
],
"author": "wangshuxian",
"license": "MIT",
"bugs": {
"url": "https://github.com/WangShuXian6/react-use-please-stay/issues"
},
"homepage": "https://github.com/WangShuXian6/react-use-please-stay#readme"
}
目前项目文件夹中只有这个文件。
现在让我们开始安装该 hook 所需的初始依赖项。
首先是 react。
npm i react -S
本课程的重要部分之一是如何在 TypeScript 中使用 React hooks 的模式。
安装 TypeScript,作为开发依赖项。
npm i typescript -D
安装 React 的类型定义包,同样作为开发依赖项。
npm i @types/react -D
https://github.com/WangShuXian6/blog/issues/67#issuecomment-2382273766 在 package.json 中,我们应该将 react 从 dependencies 列表中移到 peerDependencies 列表中。
表明这些包必须由使用该项目的宿主项目来提供
不幸的是,npm 或 node 没有自动从命令行执行此操作的命令。
因此我们需要手动进行。
我会简单地添加 "peer" 并将 dependencies 中的 "D" 改为大写。
{
"name": "react-use-please-stay",
"version": "1.0.0",
"description": "一个有趣的 React hook,可以在页面失去焦点时为文档标题和 favicon 添加动画效果",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WangShuXian6/react-use-please-stay.git"
},
"keywords": [
"react、typescript、react",
"hook、react",
"hooks"
],
"author": "wangshuxian",
"license": "MIT",
"bugs": {
"url": "https://github.com/WangShuXian6/react-use-please-stay/issues"
},
"homepage": "https://github.com/WangShuXian6/react-use-please-stay#readme",
"dependencies": {
},
"devDependencies": {
"@types/react": "^18.3.10",
"typescript": "^5.6.2"
},
"peerDependencies ": {
"react": "^18.3.1"
}
}
现在我们创建一个 tsconfig.json 文件。
在 tsconfig.json 中,我们将设置编译器选项。
例如,将输出目录设置为 dist。
JSX 设置为 react。
我们还会跳过库检查。
{
"compilerOptions": {
"outDir": "dist",
"jsx":"react",
"skipLibCheck": false
}
}
接着创建一个 src 文件夹。
在 src 中,我们创建一个名为 hooks 的文件夹。
然后我们创建主角文件 usePleaseStay.ts
注意,由于这是一个 React hook,我预计我们不需要使用任何 JS 语法。
因此我们使用 .ts 文件扩展名,而不是 .tsx。
目前在 usepleasestay 文件中,我们只导出一个名为 usepleasestay 的空函数。
export const usePleaseStay = () => {};
所以我们现在有一个非常基础的 TypeScript React hook。
让我们在 package.json 中创建一个构建脚本来确保一切正常运行。
在 package.json 中,添加一个 build 脚本。
对于我们来说,目前构建命令就是 TypeScript 编译器,命令为 tsc。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc"
},
保存 package.json 文件后,我们可以通过运行 npm run build 来执行构建脚本。
看起来我们忘记在 tsconfig 中设置正确的 lib 值,因此我们现在添加它。
根据 TypeScript 的建议,我们将这个值设置为 es2015 或更高版本。
lib 属性是一个数组,因此我们会按如下方式添加。
{
"compilerOptions": {
"outDir": "dist",
"jsx": "react",
"skipLibCheck": false,
"lib": ["ES2015"]
}
}
保存后,重新运行构建脚本。
修复后,TypeScript 编译器不应再报告任何错误。
如果我们查看 dist 文件夹,可以看到 usepleasestay.js 文件已经生成。
接下来为项目添加一个 readme 文件。
这个文件将在我们添加关于如何使用该 hook 的文档时变得更加重要。
但现在我们可以先把包的名称写进去。
使用单个 "#" 符号表示标题,我们写上包的名称 React use please stay。
我们还可以从 package.json 中复制描述并粘贴到这里。
这作为初始创建 README 文件的默认内容已经很不错了。
最后,我们需要创建一个 gitignore 文件。
目前我们只需要忽略 node_modules 文件夹。
dist/
deploy_versions/
.temp/
.rn_temp/
node_modules/*
.DS_Store
.swc
project.private.config.json
.vscode
yarn-error.log
.idea
.env.development.local
.env.production.local
.cloudbase/
package-lock.json
yarn.lock
现在,我们已经准备好开始实现和构建我们的 hook。
总结一下,我们通过 npm init 创建了一个新项目,生成了 package.json 文件。
我们为项目创建了一个 GitHub 仓库。
添加了 React 作为依赖项。
还添加了 React 类型定义和 TypeScript 包作为开发依赖项。
创建了一个带有基础设置的 TypeScript 配置文件。
编写了一个空函数来存放 hook 的主要逻辑。
最后,我们构建了项目,测试了 TypeScript 工具是否配置正确。
在下一课中,我们将使用 create React app 构建一个简单的 React 应用程序,以便在构建过程中测试我们的 hook。
在继续之前,我们将创建一个示例应用程序,用于调用我们的 hook。
我们将使用 create-react-app 包并添加 TypeScript 模板。
首先,创建一个名为 example 的文件夹。
命令为 "makedir example"。
接着进入该文件夹。
现在我们将使用带有 TypeScript 模板的 create-react-app 进行项目构建。
具体命令是 "npx create-react-app --template TypeScript"。
我希望在当前目录中进行构建,因此使用了句点来表示在 example 目录中进行操作。
项目构建完成后,我们将立即从 src 文件夹中删除以下文件:
app.css、app.test.tsx、index.css、logo.svg 和 setup_tests。
我们还会删除 index.tsx 和 app.tsx 中对这些文件的引用。
因此可以移除对 logo 和 app.css 的引用。
同样,在 index.tsx 中移除 index.css 的引用。
现在保存 index.tsx,所有内容看起来都正常,没有任何 TypeScript 警告或错误。
不过在 app.tsx 中,由于我们删除了 logo,系统会给出一个警告。
实际上,我们并不需要 app.tsx 中的任何标记,因此我将返回一个 fragment。
现在我们需要从根目录的 src 文件夹中导入我们的 hook。
让我们在 example 的 src 文件夹中创建一个 hooks 文件夹,用于存放我们的 hook。
不幸的是,create_react_app 不允许我们从其自身的 src 文件夹外部导入文件。
目前,我们将编写一个 bash 脚本来复制我们的 hook。
稍后我们将学习如何使用 npm link 将 react_use_please_stay 包直接用于我们的示例 React 应用程序中。
目前,使用 bash 脚本是一个适合初步测试的解决方案。
在示例应用程序的 package.json 中,我们将创建一个名为 copy_hook 的新脚本。
该脚本使用 cp 命令来复制文件。
我们只需要将 hook 从根目录的 src 文件夹复制到该项目中的 src_hooks 文件夹。
确保我们处于示例项目的根目录后,可以运行此脚本。
运行命令 npm run copy_hook。
现在我们可以在示例 src_hooks 文件夹中看到 use_please_stay 的副本。
最后,我们可以将刚刚复制的 hook 导入 app.tsx 中,看看是否一切正常运行。
接着我们可以运行 npm run start 启动应用程序。
看起来一切编译成功,没有任何警告或问题。
当然,目前我们的应用非常简单。
因为我们从 app.tsx 中移除了所有标记,页面上现在只有一个空白页面。
不过,目标已经达成。
我们已经成功复制了 hook,并准备好开始构建和测试它。
总结一下,我们使用 create_react_app 构建了一个带有 TypeScript 的 React 应用程序。
删除了应用程序中所有不必要的文件。
创建了一个自定义的 bash 脚本,将 hook 复制到这个示例应用程序中。
将 hook 导入到 app.tsx 中,并启动了应用程序以确保一切正常运行。
在下一节课中,我们将开始为我们的 hook 编写第一个功能。
让我们最终开始实现我们 Hook 的第一个功能。
首先,我们将重点放在能够显示并循环显示传递给 Hook 的标题数组上。
打开 use-please-stay
文件,确保这是位于根目录的 src
文件夹中的文件,而不是示例文件夹中的文件。
现在,让我们允许我们的 Hook 接收参数 titles
,其类型将为字符串数组。
接下来,当标签页失去焦点时,我们希望文档标题能够进行动画显示。
幸运的是,JavaScript 提供了一个 visibilityChange
事件,我们可以添加一个监听器。
一旦我们的 Hook 挂载,我们就会开始监听此事件。
我们可以使用 React 的 useEffect
Hook 来完成此操作,并确保提供空的依赖数组,这样它只会在挂载时触发。
我们将这个事件监听器函数称为 handleVisibilityChange
。
正如我提到的,因为我们只希望它在挂载时运行,所以我们将空的依赖列表传递给 useEffect
Hook。
我们遇到的第一个 TypeScript 错误是 TypeScript 找不到文档的类型,因为我们在 lib
编译选项中没有包含 DOM。
因此,让我们进入根目录的 tsconfig.json
文件,并将 DOM 库添加到库列表中。
在 React Hook 卸载时,最好移除监听器函数。
这可以在 useEffect
Hook 中通过返回一个函数来实现。
此返回的函数中的内容会在卸载过程中被调用。
我们可以使用这个函数来移除我们的事件监听器。
因此,返回一个箭头函数:document.removeEventListener("visibilityChange", handleVisibilityChange)
。
现在,我们需要编写我们的 handleVisibilityChange
函数。
当 document.visibilityState
的值不是 visible
时,我们应该开始遍历提供给 Hook 的标题。
否则,我们应该停止遍历这些标题。
我们可以使用 React 状态变量来跟踪是否应遍历这些标题。
让我们创建一个状态变量并使用 React 的 useState
Hook 设置成对变量。
我将它们命名为 shouldIterateTitles
和 setShouldIterateTitles
。
最初将其设置为 false
。
我们可以将其导入。
我们的 handleVisibilityChange
函数会根据 document.visibilityState
的值来调用 setShouldIterateTitles
,并在其不等于 visible
时更改状态。
现在我们有了一个在页面可见性变化时改变的状态变量。
这很好,但我们还需要添加功能以实际修改文档标题。
为此,我们将利用 JavaScript 的 setInterval
函数。
然而,我们不能立即直接使用 setInterval
。
直接在 React Hook 中使用 setInterval
并不是一个好主意。
就像我们的 documentChange
函数一样,我们还需要考虑清理以及我们设置的任何间隔以防止内存泄漏。
在 React 中做到这一点有些棘手,但完全可以实现。
让我们从编写一个新的自定义 React Hook useInterval
开始。
因此,我们将在 usePleaseStay
旁边创建一个新文件,命名为 useInterval
。
这个将导出为 const useInterval
。
传递给这个 Hook 的参数至少需要一个回调,或者是我们希望在给定间隔内运行的内容
以及间隔本身,将是一个数字。
我们要做的是存储传入回调的引用,并使用 React Hook useRef
完成此操作。
然后,我们需要确保每次回调发生更改时,我们正在更新 callbackRef
的当前值。
因此,再次需要使用 useEffect
。
每当回调发生变化时,我们会更新 callbackRef
的当前值。
最后,我们可以使用 JavaScript 内置的 setInterval
函数。
因此,我们将创建另一个 useEffect
,该函数将在间隔发生变化时触发。
我们将存储 setInterval
的 intervalID
,并调用 current.callbackRef
。
当然,也是在提供的间隔内。正如所提到的,我们创建这个自定义 Hook 的原因是能够正确地清理 setInterval
。
因此,就像我们在 usePleaseStay
中所做的那样,我们将在 useEffect
中返回一个函数来清理这个 ID。
可以通过调用 JavaScript 的 clearInterval
函数并传入间隔 ID 来实现。
我们还应有一种方法来启动和停止 setInterval
调用。
让我们添加一个布尔类型的 shouldRun
参数到我们的 Hook 中。
我们可以将 shouldRun
添加到第二个 useEffect
Hook 的依赖数组中。
当 shouldRun
切换为 false
时,我们可以直接返回这个 effect。
现在我们有了一个可以在 React 生命周期中安全使用 setInterval
的方法。
让我们回到 usePleaseStay
并开始使用 useInterval
。
如我们所说,遍历标题的触发条件是 shouldIterateTitles
。
所以现在我将它放入我们的 useInterval
函数中,暂时使用一个空函数。
间隔设为 500 毫秒,并使用 shouldIterateTitle
状态变量作为 shouldRun
触发器。
在这个函数体内,将包含更改文档标题的实际逻辑。
让我们先创建另一个状态变量,用于跟踪 titles
数组的当前索引。
我称它为 titleIndex
。
初始值设为 0
。
在间隔函数中,首先创建一个名为 nextIndex
的常量以提高可读性,
titleIndex + 1
。为了安全地设置我们的 titleIndex
,我们将使用三元运算符。
通过检查 nextIndex
是否已达到 titles.length
,如果是,则重新开始。
否则,我们可以安全地将 titleIndex
设置为 nextIndex
。
现在我们终于实现了 Hook 的用户功能,即设置 document.title
的值。
我们可以在此处使用另一个 useEffect
,
依赖项为 titles
本身以及 titleIndex
。
由于我们连接到了 titleIndex
,每当这个 useInterval
Hook 触发并更新 titleIndex
时,
我们只需要将 document.title
设置为 titles[titleIndex]
。
我要清理一下这个代码,保存,然后让我们试用一下我们的 Hook,看看它是否按预期工作。
首先,确保我们在示例项目根目录中。我将进入示例文件夹。
在运行自定义脚本以复制 Hook 之前,我们还需要复制 useInterval
,因为它被 usePleaseStay
使用。
因此,我们将在示例 package.json
中扩展脚本以执行此操作。
我将保留这个命令,同时复制 useInterval
Hook 到示例项目的文件夹中。
现在我们可以运行复制 Hook 的脚本。
在 app.tsx
中调用 usePleaseStay
时,我们需要添加我们刚刚要求的标题字符串数组。
可以看到 TypeScript 已经在提示我们传入 titles
数组。
所以我们传入 title1
,title2
和 title3
。
现在我们可以用 npm start
启动我们的示例应用。
在浏览器中,如果我们聚焦这个 React 应用程序,应该会看到 title1
。
如预期的那样,因为当前标签页在焦点中,标题不会循环。
然而,一旦打开另一个浏览器标签,我们将看到标题按预期循环显示。
总结一下,我们创建了一个在挂载时使用的 useEffect
Hook 来监听 visibilityChange
事件触发的时刻。
我们还添加了清理步骤,以在 Hook 被卸载时移除事件监听器。
并设置了一个状态变量,根据 document.visibilityState
的值进行设置。
接下来我们编写了一个自定义 useInterval
Hook,当 shouldIterateTitles
为 true
时用于运行更改标题的功能。
最后,我们连接到标题和标题索引,每次标题索引更改时,
我们根据该标题索引在标题数组中的值设置文档标题。
我们已经完成了一个完全可以使用的 React Hook。
在下一节课程中,我们将学习如何使用 Rollup 将 Hook 打包为 npm 模块。
我们代码库的大小已经在扩展,可以想象不断修改和运行我们在示例项目中的复制 hook 脚本将会有多麻烦。
让我们将代码打包为 npm 模块,这样我们就可以通过 npm link 直接在示例应用中使用它。
我们现在将安装三个开发依赖包。
第一个是 rollup,第二个是 rollup 的标准 TypeScript 插件,第三个是 tslib。
这就是 npm install --save-dev rollup @rollup/plugin-typescript tslib
。
接下来,我们将在项目根目录创建一个 rollup.config.js
文件。
在这个文件中,我们首先导入 TypeScript 插件。
将包作为 pkg
从 package.json
导入。
我们不能直接使用 package
这个词,因为它是保留字,因此我们还需要导入 json
文件。
然后,我们将设置我们的配置。所以我们将导出默认配置。
首先,我们需要为 rollup 定义一个输入点。将会是 index.ts
文件(我们尚未创建)。
文件的输出将使用 package.json
中的 main
键。
输出格式设为模块化,我们希望生成 source maps,将严格模式设置为 false
。
接下来,我们需要引入 TypeScript 插件,这是一个函数。
我们使用的外部库是 React。
然后我们需要更新构建脚本。
将原来的 TSC 替换为 rollup -c
。
-c
标志告诉 rollup 使用我们指定的 rollup.config
文件进行打包。
最后,我们需要更新构建脚本,并添加我们要打包的文件。
在这个例子中,我们打包的是 usePleaseStay.ts
文件。
我们将创建一个新的 index.ts
文件,将其放在 src
根目录中。
从这里,我们导出 usePleaseStay
。
我们还需要允许合成默认导入以使 rollup 正常工作,因为 React 本身是合成的默认导入。
因此,我们将其添加到 tsconfig
中,并将声明设为 true
。
这意味着 TypeScript 会在 rollup 构建时为我们创建类型声明文件。
在编译器选项外,我们还应定义 TypeScript 编译器应包含哪些文件,并排除那些在构建过程中我们不想包含的文件。
我们要包含 src
文件夹,并确保排除 example
文件夹。
因为 usePleaseStay
hook 即将成为 npm 模块,我们需要在 package.json
中通知 npm。
可以通过在 JSON 中添加 "type": "module"
来实现。
另外,在 package.json
中,我们应更新 main
值。
如你所记得的,我们在配置中使用它来指定输出文件。
所以我们可以将其更新为 dist/index.js
。
现在,让我们尝试使用新的构建命令来打包我们的 hook。
看来构建工作正常,为了确认,我们可以查看 dist
文件夹。
在其中应包含以下文件和文件夹。
在 hooks
中,我们应该有 useInterval
和 usePleaseStay
的类型声明。
在 dist
根目录中,我们应该有三个文件:类型声明、实际的可分发 hook 文件和 source map 文件。
如果该文件夹中还有 usePleaseStay.js
文件,应将其删除。
要能够使用 npm link
,我们需要在 package.json
中添加一些属性。
首先,因为这是一个带有类型的模块(感谢 TypeScript 的类型声明文件),我们应指定 types
属性为 dist/index.d.ts
。
然后,我们应提供 files
属性。
这告诉 npm 我们包的所有相关文件。
在我们的例子中,就是 dist
文件夹,其中 rollup 放置了所有打包文件。
npm 期望这是一个数组,因此我们将其作为数组传递。
完成后,现在可以运行 npm link
。
在将此链接包用于示例应用之前,我们需要确保链接到单一版本的 React。
在这里,我们希望链接到示例文件夹中使用的 React 版本。
确保你还在根目录中,我们可以链接到 example
项目 node_modules
文件夹中的 React 版本。
这将是 npm link example/node_modules/react
。
现在,我们可以进入 example
文件夹并链接我们的包。
为确保链接成功,可以输入 npm ls --location=global --depth=0 --link=true
。
现在让我们运行示例应用,看看是否一切正常。
应用启动后,我们可以看到标题切换效果仍然与前一课中一致。
看起来一切正常。
在清理方面,我们可以删除示例应用的 package.json
中的复制 hook 脚本,并删除项目中的 hooks 文件夹。
总结一下,我们安装了 rollup、typescript 插件和 tslib。
创建了一个 rollup 配置文件,以便 rollup 打包我们的 hook。
我们创建了一个 index.ts
文件来导出我们的 hook 文件。
修改了 package.json
文件,并设置了打包所需的重要属性。
我们将包链接到示例项目。
最后,我们从链接的包中直接导入了我们的 hook,并运行了示例项目以确保一切正常。
在下一节课程中,我们将清理并重构我们已构建的内容。
借助 npm link
测试我们的 hook,现在可以更轻松地对源码进行重构和清理。
我们描述的 hook 功能正常运行,但查看 usePleaseStay
的源码时,逻辑有些难以跟踪。
代码相当简洁,但为了完全理解它,我们需要在 useEffect
和 useInterval
这些 hooks 以及状态之间来回跳转,以了解其逻辑链如何协同工作。
接下来我们将进一步清理自定义 hook,将某些逻辑部分封装到小型、易于理解的文件中。
让我们从上到下浏览 usePleaseStay
并进行重构。
第一个重构将是与可见性变化事件监听器和副作用相关的部分。
我们可以创建一个新 hook,称为 useListenToVisibilityChangeOnMount
。
我们将导出 const useListenToVisibilityChangeOnMount
。
我们可以剪切并粘贴 handleVisibilityChange
函数以及 onMountUseEffect
hook 到我们的新 hook 中。
我们现在只需将 setShouldIterateTitle
状态设置函数传递给我们的 hook。
确保传入 useEffect
自 React。
这就完成了清理。
现在我们用 useListenToVisibilityChangeOnMount
替换了根 hook 中的逻辑,确保传递状态设置函数。
为了引导后续阅读此 hook 的开发人员,我们可以留一个小注释,描述该 hook 内部正在发生的事情。
我会写:“当页面可见性丢失时设置 shouldIterateTitles 值”。
还可以添加:“处理事件监听器的清理”。
同样地,我们可以将底部的两个效果封装到一个新的自定义 hook 中,我将其命名为 useTitleChangeEffect
。
重构之后,我们可以看到 titleIndex
和 setTitleIndex
未被使用。
这对我们来说非常合适,我们可以将整个状态部分移到新 hook 中。
现在我们添加所需的参数 titles
和 shouldIterateTitles
,并导入 useState
、useInterval
和 useEffect
。
接着清理一下。
回到 usePleaseStay
中,确保正确使用 useTitleChangeEffect
。
我们需要传递 titles
和 shouldIterateTitles
。
如同在 useListenToVisibilityChangeOnMount
中所做的那样,我在这里也会留下一个小注释,帮助开发者理解 useTitleChangeEffect
的内容。
可以写:“当 shouldIterateTitles 为 true 时,修改页面的 document.title”。
然后删除这些未使用的导入。
现在一切就绪。
我们现在的 hook 代码更加清晰易读。
在 usePleaseStay
入口文件中,我们可以很好地了解该 hook 各个主要部分的工作原理。
如果我们需要更深入的细节或以后添加功能,可以进入 useListenToVisibilityChangeOnMount
或 useTitleChangeEffect
。
为了双重检查,让我们重新构建 hook,启动示例应用,并检查一切是否正常。
结果符合预期,我们依然看到相同的功能。
当我回到页面时,迭代自然停止。
总结一下,我们创建了一个名为 useListenToVisibilityChangeOnMount
的新 hook,并将整个可见性变化事件监听逻辑移入其中。
接着创建了一个新 hook useTitleChangeEffect
,并将相应的 useInterval
和 useEffect
逻辑移入其中。
然后在 usePleaseStay
中导入并使用了这两个 hook。
在这两个 hook 中,我们都留下了简明的注释,帮助未来阅读此 hook 源码的开发人员。
本课程模块一到此为止。
在模块二中,我们将开始为 usePleaseStay
构建更多高级和有趣的功能。
在模块一中,我们为 hook 设置了基本功能,完成了一些清理和重构。
现在我们有了一个干净的基础,让我们开始为 hook 添加更多高级功能。
第一个将是设计一个逐字母级联显示的功能。
例如,如果标题是 "a cool title",我们将看到文档标题中的字母逐个呈现动画。
首先看到 "a",然后是 "a co",然后是 "a co",以此类推。
这个新的级联字母功能将与当前的标题循环功能截然不同。
为了保持 hooks 代码整洁,定义一个枚举以区分我们想要运行的动画类型是合理的。
让我们创建一个新的枚举,称为 "animation type"。
首先,在 "src" 文件夹下新建一个文件夹,命名为 "enums"。
在此文件夹中创建一个新文件,称为 "animation type.ts"。
这将是 export enum animation type
。
目前,我们只有两种动画类型。
循环标题功能,我们将其命名为 "loop",
和新的级联字母功能,命名为 "cascade"。
我们将使用字符串枚举,因为它们在调试和日志输出时比数值枚举更易理解。
现在我们有了新的动画类型枚举,可以为 usePleaseStay
添加一个新参数。
这个新参数将是 "animation type",类型为 animation type
。
我们还会将这个动画类型参数传递给 useTitleChangeEffect
hook。
在 useTitleChangeEffect
中,让我们添加新引入的参数。
在直接使用它之前,我们会对该 hook 进行一些小的重构,以便更好地在未来扩展它。
首先,我将现有的迭代逻辑提取到一个新函数中,称为 runLoopIterationLogic
。
将其移出并放到这里。然后我们将为级联编写新的迭代逻辑。
runCascadeIterationLogic
这将与之前的逻辑类似,但只使用标题数组的第一个标题。
现在在 useInterval
函数中,我们可以利用这个枚举。
我们会对动画类型进行 switch 判断。
如果类型是 "cascade",当然要调用 runCascadeIterationLogic
。
如果类型是 "loop",我们则运行循环迭代逻辑。
使用枚举时,非常重要的是要添加默认情况。
由于这是我们的原始实现,我们将利用 JavaScript 的 switch 语句中的“贯通”功能作为默认情况。
这样,如果动画类型被遗忘或拼写错误,我们将始终运行循环迭代逻辑。
现在我们需要在第二个 useEffect
中做同样的事情。
首先,我会将现有逻辑提取出来,创建一个新函数,称为 runLoopTitleLogic
。
我们还将为我们的 "cascade" 功能编写新逻辑。
这个函数将被称为 runCascadeTitleLogic
。
再次强调,逻辑非常相似,但我们将使用标题数组的第一个索引。
为了遍历第一个标题中的字母,我们将使用 substring
函数。
从第一个标题的起始位置,到 titleIndex
的位置。
同样地,在 useEffect
中的 switch 判断中,我们也会使用标题逻辑函数。
由于动画类型枚举作为公开 hook 的一个公共参数类型,我们也应该将此类型公开。
接下来进入 index.ts
并导出 animation type
枚举。
这在 "enums" 文件夹中的 "animation type" 文件里。
这样就完成了添加新的级联功能所需的所有步骤。
现在我们重建 hook。
并进入示例项目。
在 app.tsx
中,让我们首先添加 "cascade" 类型的动画。
由于我们知道在实现中,它只会取第一个标题,所以我们可以用 "I am a cascading title" 来测试。
保存后,用 npm start
启动应用程序。
我们可以看到,当页面失去焦点时,级联效果开始运行。
它会遍历到标题的末尾并重复。
我们成功地为 usePleaseStay
添加了一个不错的级联字母功能。
总结一下,我们创建了一个名为 animation type
的新枚举。
我们为 hook 添加了一个新的名为 animation type
的参数。
我们将该动画类型参数传递给 useTitleChangeEffect
。
在 useTitleChangeEffect
中,我们进行了小幅重构,将迭代逻辑和标题逻辑重新封装到各自的函数中。
在 useInterval
和 useEffect
hooks 中使用了该枚举类型的 switch 判断。
最后,我们重建了 hook 并在示例应用中测试,确认新功能正常工作。
在下一节课中,我们将实现一个跑马灯效果,标题会从左到右移动,然后循环在标签的左侧重新出现。
现在让我们构建标题的跑马灯效果。
我们将有一个字母数组,并需要在浏览器标签页中从左到右滚动它们。
与标题循环和字母级联功能类似,我们需要在 useTitleChangeEffect
中编写一个迭代函数和一个标题修改函数。
在 Chrome 中,标签页中能显示的最大字母数约为 30,而 Firefox 稍微长一些,大约可以显示 35 个字符。
我们将使用较短的长度来运行跑马灯效果,以确保其在 Firefox 中也兼容。
首先,我们将在 src
下创建一个新的 constants
文件夹,并创建一个 TypeScript 文件 constants.ts
。
我们定义一个常量 tabCharacterCount
,设为 30。
在 useTitleChangeEffect
中,现在让我们编写迭代和标题逻辑函数。
我要创建一个名为 runMarqueeIterationLogic
的新函数。
逻辑非常简单,为了提高可读性,我将 nextIndex
保存为常量。
对于跑马灯效果,我们希望检查是否达到了标签页的字符数上限(我们刚才在常量文件中定义的值)。
如果达到了,我们会从头开始。
否则,我们可以继续下一个索引。
然后可以开始编写跑马灯标题逻辑函数。
称为 runMarqueeTitleLogic
。
在 runMarqueeTitleLogic
中,我们首先需要检查超出字符数限制的字符数。
我将其命名为 carryOverCount
。
这就是当前的 titleIndex
加上标题的总长度减去标签页字符数。
如果这个值大于零,我们需要对标题进行一些额外的计算。
首先创建一个用于分隔跑马灯开头和结尾的空格文本。
可以通过重复空格字符来计算。
这个值是标签页字符数减去标题长度。
计算好后,我们可以设置文档标题。
文档标题为当前标题从 carryOverCount
开始,一直到标题的末尾。
然后是空格文本,接着是剩余的标题部分。
如果 carryOverCount
不大于零,那么我们只需在标题前设置初始空格。
首先存储一个偏移值,使用 Unicode 空格字符,重复当前 titleIndex
次。
偏移值就是我们要偏移的字符数,在字符串开头有相应数量的空格字符。
标题变成了这个偏移量加上标题本身。
清理一下代码,跑马灯标题逻辑函数就完成了。
在将这两个新函数添加到 switch case 之前,我们进入 animation type
并添加新动画类型:marquee。
回到 useTitleChangeEffect
,我们添加新情况。
当类型为 marquee
时,迭代逻辑调用 runMarqueeIterationLogic
。
同样地,对于标题逻辑,当动画类型为 marquee
时,我们调用 runMarqueeTitleLogic
。
跑马灯功能现在就设置好了。
重建 hook,执行 npm run build
。
接着进入示例文件夹。
在 app.tsx
中,设置一个新的跑马灯标题。
我将其更改为 "I am a marquee"。
运行示例应用。
打开浏览器,失去焦点后,可以看到偏移功能在正常运行。
当我们到达标签标题的末尾时,可以看到字符接续。
跑马灯效果运转良好。
总结一下,我们首先创建了 runMarqueeIterationLogic
函数和 runMarqueeTitleLogic
函数。
我们添加了新的动画类型 marquee
,并在 useTitleChangeEffect
的两个 switch case 中利用了这个新类型。
最后,我们重建并测试了 hook,确保所有功能正常。
下一课中,我们将学习如何通过 hook 实现 favicon 的更改。
让我们让 hook 通过 favicon 进行迭代。
创建一个新的 hook,称为 useFaviconChangeEffect
。
这个 hook 将与 useTitleChangeEffect
非常相似。
我们将导出 const useFaviconChangeEffect
。
这个 hook 将接收一个名为 faviconLinks
的参数,类型为字符串数组。
还将接收 shouldIterateFavicons
,类型为布尔值。
我们首先需要一个状态变量来跟踪当前的 favicon 索引。
称其为 faviconIndex
,初始值为零。
我们还将为 favicon 设置一个 ref,因为每次使用 favicon 时,在整个 DOM 中查找它并不高效。
称其为 faviconRef
,使用 useRef
。
其类型为 HTMLLinkElement
,初始值为 null
。
与 useTitleChangeEffect
类似,我们将使用 interval,通过递增当前 faviconIndex
计算下一个索引。
如果下一个索引已达到 faviconLinks
数组的末尾,那么将从零开始。
否则,继续下一个索引。
我们将使用 500 毫秒的间隔,并传递 shouldIterateFavicons
参数,以在需要时启用或禁用 interval。
favicon 可以以多种方式和格式存储。
我们将创建一个自定义函数以尝试在 DOM 中找到 favicon。
因此,在新的 utils
文件夹中创建一个新函数,称为 getFavicon.ts
。
该函数期望返回一个 HTMLLinkElement
,如果未找到则返回 null
。
我们将期望 favicon 是一个 link 标签,且其 rel
属性为 icon
或 shortcut icon
。
首先获取所有类型为 link 的节点。
然后循环检查当前查看的 link 标签是否具有 rel
属性,并且其值是否为 icon
或 shortcut icon
。
如果满足条件,则返回该元素。
如果没有找到,我们返回 null
。
现在回到 useFaviconChangeEffect
,我们可以用 getFavicon
函数替代 null
作为 faviconRef
的初始值。
接着编写 useEffect
hook 实际更新 favicon。
这个 useEffect
hook 将在 faviconIndex
变化时触发,
我们将更新 faviconRef.current
,并设置为 faviconLinks
中当前索引对应的值。
现在让我们将 useFaviconChangeEffect
添加到 usePleaseStay
中。
与其他自定义 hooks 一样,我们添加注释:“Modifies the favicon of the page whenever shouldIterateTitles is true”。
使用 useFaviconChangeEffect(faviconLinks, shouldIterateTitles)
。
我们还需要在 usePleaseStay
中添加 faviconLinks
参数,类型为字符串数组。
接着我们可以在示例应用中测试此新功能。
首先重建项目。
进入示例应用。
为了测试清晰,我们将动画类型设为 loop。
用 WeHeartReact
和 WeHeartNewLine
切换。
对应的 favicons,一个来自 ReactJS.org,另一个是 newline favicon。
调整格式以提高可读性。
启动示例应用。
我们设置了默认标题,并期望在打开新标签时标题和 favicon 同步变化。
结果正如预期。
总结,我们创建了一个新的 useFaviconChangeEffect
,与 useTitleChangeEffect
格式类似。
我们创建了一个 getFavicon
自定义函数,用于帮助在 DOM 中找到 favicon。
将 useFaviconChangeEffect
添加到 usePleaseStay
主函数,并暴露了新的 faviconLinks
字符串数组参数。
重建并测试了 hook,确认其功能如预期运行。
下一课中,我们将允许将间隔时间作为 hook 的参数添加。
目前 useTitleChangeEffect
和 useFaviconChangeEffect
中的 useInterval
调用使用的是硬编码的 500 毫秒间隔。
我们可以将其作为参数传入 hook。
首先将其添加到 usePleaseStay
的参数中,称为 interval
,类型为 number
。
然后将该参数传递给 useTitleChangeEffect
和 useFaviconChangeEffect
。
替换原来的 500 为 interval
。
保存后,在 useFaviconChangeEffect
中也执行相同操作。
快速检查后,重建 hook。
进入示例文件夹。
在 app.tsx
中保留上节课的配置,但传入一个 3000 毫秒或 3 秒的间隔。
运行示例。
现在我们可以看到,每次打开标签页时,变化速度变慢了,不再是半秒,而是每隔 3 秒变化一次。
看来新添加的间隔参数正常工作。
总结,我们向 usePleaseStay
添加了新的 interval
参数。
我们将该参数传递给了 useTitleChangeEffect
和 useFaviconChangeEffect
。在这两个 hooks 中,用传入的 interval
替换了硬编码的 500 毫秒。
在下节课中,我们将添加另一个参数,允许用户始终播放动画,而不仅仅是在页面失去焦点时播放。
有时 hook 的用户可能希望动画始终播放,而不仅仅是在窗口不在焦点时播放。
我们可以通过向 usePleaseStay
hook 添加一个布尔参数 shouldAlwaysPlay
来实现这一点。
将 shouldAlwaysPlay
添加到参数列表中,类型为 boolean
。
接着将其传递给 useListenToVisibilityChangeOnMount
hook。
在 useListenToVisibilityChangeOnMount
中,我们也添加这个布尔参数。
然后将它添加到现有的 useEffect
中。
唯一的修改是在 shouldAlwaysPlay
为 true
时调用 setShouldIterateTitles(true)
,然后返回。
保存所有更改并重建 hook。
进入示例项目的 app.tsx
文件。
将 shouldAlwaysPlay
设置为 true
。
启动应用程序后,即使当前标签页获得焦点,动画也会持续播放。
我们可以看到,不论标签页是否失去焦点,每隔三秒标题和 favicon 都会发生变化,这符合预期。
总结一下,我们为 usePleaseStay
hook 添加了一个新的布尔参数 shouldAlwaysPlay
。
将该参数传递给 useListenToVisibilityChangeOnMount
hook,并将其添加到 useEffect
的依赖数组中。
当 shouldAlwaysPlay
为 true
时,立即调用 setShouldIterateTitles(true)
,并返回。
模块二到此结束。
在模块三中,我们将进行最终优化和改进,以提升开发者体验和 hook 的易用性,然后发布到 npm。
当前,我们传递给 hook 的参数列表较长,阅读起来有些困难。
为了简化,我们将创建一个类型,用来定义这些参数,然后可以为一些可选参数添加默认值。
首先在 src
文件夹下创建一个名为 Types
的文件夹。
定义一个类型 UsePleaseStayOptions
,导出它以便使用。
在这个类型中包含以下字段:
titles
:必需项,类型为字符串数组。animationType
:可选项,类型为 AnimationType
。faviconLinks
:可选项,类型为字符串数组。interval
:可选项,类型为 number
。shouldAlwaysPlay
:可选项,类型为 boolean
。这个类型的结构能消除参数顺序的困惑,因为顺序并不重要。
在 UsePleaseStayOptions
类型中,titles
是必需项,但可以通过传递空数组来绕过约束,这在 useTitleChangeEffect
中可能导致崩溃。我们创建一个类型 ArrayOfOneOrMore
以确保数组至少包含一个元素。
接着,在 types
文件夹中定义 ArrayOfOneOrMore
:
export type ArrayOfOneOrMore<T> = [T, ...T[]];
然后将 titles
类型修改为 ArrayOfOneOrMore<string>
。
对于 faviconLinks
,也添加了类似约束,但要求至少包含两个值,因此我们创建 ArrayOfTwoOrMore
:
export type ArrayOfTwoOrMore<T> = [T, T, ...T[]];
将 faviconLinks
类型修改为 ArrayOfTwoOrMore<string>
。
在 usePleaseStay
中导入该类型,并将参数包装在一个对象中以简化默认值设置。所有其他可选参数都设置为合理的默认值,例如:
animationType
默认为 loop
interval
默认为 500 毫秒shouldAlwaysPlay
默认为 false
接着,确保在 useFaviconChangeEffect
中对 faviconLinks
进行检查,以避免在其未定义时尝试访问数组索引。
接着,我们在 README 中文档化了所有参数,使用 Markdown 表格提供参数的名称、类型、默认值和描述,并在 GitHub 中链接到 src/types/UsePleaseStayOptions
源文件。
检查并运行示例应用:
app.tsx
中配置对象,并设置 titles
和 faviconLinks
等参数。总结:
UsePleaseStayOptions
来定义 hook 参数。ArrayOfOneOrMore
和 ArrayOfTwoOrMore
工具类型,确保数组不为空。usePleaseStay
中应用合理的默认值。下一课中,我们将实现一个仅在开发时触发的警告日志器,以在参数无效时提醒开发者。
为了确保我们的 hook 能为开发者提供高质量的体验,我们将添加一个在开发模式下显示的警告消息功能。
我们会使用 nodeEnv
环境变量来检测是否处于开发环境,如果是,则显示警告消息。
首先在 README 中添加说明,表明如果 nodeEnv
变量设置为 development
,hook 会显示一些警告消息,并且这些消息仅在开发环境下显示,不会在生产环境中显示。
在 utils
文件夹中创建一个名为 issueWarningMessage.ts
的新工具函数文件:
export const issueWarningMessage = (message: string): void => {
if (process.env.NODE_ENV === 'development') {
console.warn(`[use-please-stay]: ${message} (shown in development only)`);
}
};
然后,在 utils
文件夹中创建另一个名为 validateParameters.ts
的文件,用于检查参数的边界情况,并在不合适时发出警告。检查标题数组是否为空、动画类型为 cascade
或 marquee
时标题数组中只有一个元素等情况。然后调用 issueWarningMessage
来显示相应的警告信息。
例如:
import { issueWarningMessage } from './issueWarningMessage';
import { AnimationType } from '../types';
export const validateParameters = (titles: string[], animationType: AnimationType): void => {
if (titles.length === 1 && titles[0] === '') {
issueWarningMessage("You have passed an empty string as the title. This will result in no text being displayed, regardless of animation type chosen.");
}
if (titles.length > 1 && (animationType === AnimationType.Cascade || animationType === AnimationType.Marquee)) {
issueWarningMessage(`You have passed more than one title, but have specified animation type ${animationType}. Only the first title in the titles array will be used.`);
}
};
在 usePleaseStay
中,添加 validateParameters
作为第一行调用,将 titles
和 animationType
传入。
接下来,测试该功能。在 app.tsx
中引发一个警告场景,运行应用后,可以在浏览器控制台中看到警告消息。
最后,我们通过构建生产版本来验证这些消息仅在开发环境中显示。使用 npm run build
构建生产版本,然后用 serve -s build
启动生产服务器,确认不再看到任何警告消息。
总结:
console.warn
消息的 issueWarningMessage
函数。validateParameters
函数验证 usePleaseStay
参数的边界情况,并在必要时发出警告。validateParameters
添加到 usePleaseStay
的第一个调用行,以确保传入的参数在开发过程中得到验证。下一节课中,我们将进一步优化,添加另一种警告消息,帮助开发者防止 hook 与本地存储变量并发调用的情况。
由于 usePleaseStay
会按时间间隔修改文档标题,因此它应该只被调用一次。如果在同一个 React 组件中调用 usePleaseStay
两次,可能会导致间隔时间变得不稳定或奇怪的行为。
为了防止开发者在同一应用中多次调用 usePleaseStay
,我们将利用浏览器的本地存储 API 创建并检查全局日期变量。如果发现不同的日期记录,则意味着存在多次调用。我们将使用上节课的 issueWarningMessage
函数来警告用户。
首先,在 src/hooks
文件夹中创建一个名为 useMultipleInstanceCheck.ts
的文件:
import { useEffect } from 'react';
import { issueWarningMessage } from '../utils/issueWarningMessage';
export const useMultipleInstanceCheck = (): void => {
useEffect(() => {
const date = new Date().toISOString();
localStorage.setItem('usePleaseStay', date);
return () => {
localStorage.removeItem('usePleaseStay');
};
}, []);
const existingDate = localStorage.getItem('usePleaseStay');
if (existingDate) {
issueWarningMessage(`Use Please Stay should be mounted only once in an application.
Doing otherwise could lead to strange behavior.
Use Please Stay was last mounted at ${existingDate}.
Please check your code for multiple Use Please Stay usages.
Due to React's Strict Mode in development, this message is valid only if you see different dates.`);
}
};
在 usePleaseStay
文件中,将 useMultipleInstanceCheck
添加为调用的第一行,以确保在参数验证之后立即执行多实例检查。
接着,测试这一功能:在开发模式下运行应用程序,并观察控制台中的警告信息。如果检测到多次调用,我们会看到带有不同日期的警告消息。
在总结中,我们通过以下步骤确保开发者避免多次调用:
useMultipleInstanceCheck
自定义 hook,使用本地存储记录调用时间。issueWarningMessage
向开发者发出警告,如果不同日期出现,则可能是多次调用了 usePleaseStay
。usePleaseStay
的最前面,确保任何参数验证之前就进行实例检查。在下一节课中,我们将学习如何在恢复焦点或卸载 usePleaseStay
时,重置原本的标题和 favicon。
到目前为止,我们主要忽略了恢复焦点或卸载 usePleaseStay
时的处理,这样会导致当前状态的文档标题或 favicon 保持不变,这对开发者体验并不友好。
为了解决这个问题,我们可以通过 useRef
钩子存储标题和 favicon 的原始值。当页面重新获得焦点或 usePleaseStay
被卸载时,我们可以恢复这些原始值,从而实现完整的“无痕”生命周期。这些更改将确保开发者可以轻松地挂载和卸载我们的钩子。
保存原始值:首先,我们将 shouldAlwaysPlay
传递给 useTitleChangeEffect
和 useFaviconChangeEffect
函数,并在这两个函数中添加一个 useRef
用于保存 document.title
和原始 favicon。
标题恢复:在 useTitleChangeEffect
中,添加 originalTitleRef
,存储初始标题,并在 useEffect
的清理函数中恢复 document.title
。然后,检查是否 shouldAlwaysPlay
为 false,若为 false 则恢复标题。
favicon 恢复:在 useFaviconChangeEffect
中,存储原始 favicon 的 href 值,在清理函数中恢复这个 href。对焦点恢复的情况,使用 useEffect
检查 shouldIterateFaveIcons
和 shouldAlwaysPlay
,在特定条件下恢复原始 favicon。
在 app.tsx
中配置示例,以观察焦点恢复的行为。设置 shouldAlwaysPlay
为 true
,确保焦点丢失时标题和 favicon 仍持续切换;设置为 false
时,则在焦点恢复后恢复原始标题和 favicon。这样,可以确保 shouldAlwaysPlay
参数的行为符合预期。
shouldAlwaysPlay
参数传递给 useTitleChangeEffect
和 useFaviconChangeEffect
。useTitleChangeEffect
中,通过 useRef
保存初始标题,并在卸载或焦点恢复时还原标题。useFaviconChangeEffect
中,通过 useRef
保存原始 favicon 的 href,并在适当情况下恢复 href 值。在接下来的课程中,我们将学习如何将这个钩子发布到 NPM 上。
我们的钩子已经实现得非常简洁,有详细的文档,还包含许多增强开发者体验的额外功能。现在是时候将我们的钩子发布到 NPM 上了。
注册 NPM 账号:如果你还没有 NPM 账号,可以访问 npmjs.com/signup 注册一个。
安装 npmrc 工具:通过全局安装 npmrc
来管理多个 NPM 账号。
npm install -g npmrc
登录 NPM:确保使用 npmlogin
登录到正确的账号。如果启用了两步验证,请输入验证码。
设置包的作用域:打开 package.json
,在包名之前添加 @your-username/
,例如 @FullStackCraftLLC/react-use-please-stay
,以便在 NPM 上发布为作用域包。
发布包:最后,运行以下命令来发布包。
npm publish --access public
验证发布:在浏览器中访问 npmjs.com/package/@your-username/react-use-please-stay 检查是否成功发布。可以在另一个项目中安装并测试刚发布的包,确保它运行正常。
npmrc
管理多个账号。package.json
中定义包名。npm publish
命令将钩子发布到 NPM。通过这些步骤,你已经成功发布了第一个 React 钩子到 NPM。接下来,我们将在最后一个模块中,将示例应用程序转变为完整的演示页面,以便开发者实时实验钩子的功能。
到目前为止,我们的示例应用主要用于内部测试。不过,通过一些额外的开发,我们可以将其转变为一个交互式示例页面,并使用 Bootstrap 来美化页面。以下是将应用转变为互动文档页面的步骤:
index.tsx
文件中引入 Bootstrap 的根样式表。
npm install bootstrap
// 在 index.tsx 文件中引入
import 'bootstrap/dist/css/bootstrap.min.css';
在 App.tsx
文件中为每个 usePleaseStay
钩子的参数创建状态变量,这些变量将用于跟踪 UI 表单中的输入值。
const [titles, setTitles] = useState(["Newline", "TypeScript"]);
const [interval, setInterval] = useState(1000);
const [animationType, setAnimationType] = useState(AnimationType.Loop);
const [faviconLinks, setFaviconLinks] = useState(["/favicon1.ico", "/favicon2.ico"]);
const [shouldAlwaysPlay, setShouldAlwaysPlay] = useState(true);
对于每个状态变量,创建相应的表单输入字段。比如,可以用 <textarea>
来接受 titles
,并用复选框来控制 shouldAlwaysPlay
,从而让用户可以实时更新这些值。
<div className="form-group">
<label htmlFor="titles">Titles (comma separated):</label>
<textarea
id="titles"
value={titles.join(", ")}
onChange={(e) => setTitles(e.target.value.split(",").map((title) => title.trim()))}
className="form-control"
/>
</div>
使用 Prism React Renderer
库来高亮显示代码示例:
npm install prism-react-renderer
import Highlight, { defaultProps } from "prism-react-renderer";
import theme from "prism-react-renderer/themes/dracula";
const CodeHighlighter = ({ code }) => (
<Highlight {...defaultProps} code={code} language="jsx" theme={theme}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={style}>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
);
在代码高亮组件中添加一个复制代码的按钮。可以使用 navigator.clipboard.writeText
来实现:
<button onClick={() => navigator.clipboard.writeText(code)}>
Copy Code
</button>
为页面的各个部分添加 Bootstrap 类,比如 .container
、.mt-4
,这样可以使页面更美观且响应式。在页面顶部展示包的标题和简介,在表单下方展示生成的代码并提供复制功能。
确保代码展示和复制功能工作正常后,可以将示例页面发布到 GitHub Pages 上以供用户使用。
通过上述步骤,你成功地将一个基本的 React 应用转换成了一个交互式的文档页面。它展示了 usePleaseStay
钩子的功能,并提供了使用示例,使开发者可以直接在页面上体验和配置该钩子。
在下一课中,我们将把这个示例页面发布到 GitHub Pages,以便其他开发者可以访问和使用这个钩子。
现在我们可以将示例应用发布到 GitHub Pages 上,使其他人能够访问并查看该钩子的互动示例页面。以下是将应用发布到 GitHub Pages 的步骤:
在根目录下的 package.json
文件中更新 "homepage"
字段,使其指向 GitHub Pages 的 URL。这通常是:
https://<your-username>.github.io/<repository-name>
确保 URL 格式正确,并在示例应用的 package.json
中同样设置这个 homepage
。
gh-pages
库在根目录中安装 gh-pages
作为开发依赖项,以便能够轻松地将应用部署到 GitHub Pages。
npm install --save-dev gh-pages
在 package.json
中添加两个脚本来自动化发布过程:
"scripts": {
"predeploy": "cd example && npm run build",
"deploy": "gh-pages -d example/build"
}
predeploy
: 这个脚本会在 npm run deploy
之前自动运行,进入 example
目录并构建应用。deploy
: 使用 gh-pages
库将构建后的内容(example/build
文件夹)发布到 GitHub Pages。更新项目的 README.md
文件,在文件顶部添加一个指向示例页面的链接,以便用户能直接访问:
## Example App
An interactive example app using this hook can be found [here](https://<your-username>.github.io/<repository-name>).
运行以下命令将示例页面发布到 GitHub Pages:
npm run deploy
发布完成后,访问 GitHub Pages 的 URL(https://<your-username>.github.io/<repository-name>
)来查看示例页面。请注意,首次发布时可能会看到 404 错误,这是因为 GitHub Pages 需要一点时间来处理新发布的内容。稍等片刻后刷新页面即可。
通过上述步骤,我们:
package.json
中添加了主页 URL。gh-pages
库以简化发布过程。predeploy
和 deploy
脚本来自动化构建和发布。npm run deploy
命令成功将示例应用发布到了 GitHub Pages。恭喜你完成了整个过程!
掌握使用TypeScript编写自定义 React Hooks | Master Custom React Hooks with TypeScript
使用TypeScript构建高级React Hook,你将从一个空白编辑器开始,最终完成并发布一个完整的开源npm包。
如何使用TypeScript创建自定义React Hooks React Hooks全生命周期(挂载、卸载等)的重要性 如何将TypeScript类型集成到自定义React Hooks中 使用TypeScript解决React Hooks中的哪些问题 如何创建易于使用的社区友好型自定义React Hook包 如何添加基本功能,包括标题循环、使用Rollup打包模块、清理与重构等 如何添加高级功能,包括级联字母、跑马灯效果、图标修改、将间隔时间作为Hook参数、始终播放作为Hook参数等 如何实现优化,例如自定义类型、仅在开发环境中记录警告、防止Hook并发使用、重新聚焦和卸载时恢复原始值等
01 课程介绍 02 使用 nvm 进行环境搭建 03 使用 npm init 构建一个 React Hook 库的脚手架 04 构建一个用于测试 Hook 的基础 create-react-app 项目 05 使用 useEffect Hook 实现标题循环功能 06 使用 Rollup 将 react-use-please-stay 打包成一个 npm 模块 07 清理并重构 react-use-please-stay 08 实现级联字母功能 09 实现跑马灯功能 10 实现 favicon 修改功能 11 添加 Hook 参数中的间隔时间 12 添加 Hook 参数中的始终播放选项 13 使用自定义类型来定义 Hook 参数 14 添加仅在开发环境中使用的警告日志记录器 15 防止 Hook 同时被多次使用 16 在重新聚焦和卸载时恢复原始值 17 将 Hook 发布到 npm 18 准备示例应用作为公共示例页面 19 将示例应用发布到 GitHub Pages