构建并发布您的第一个 NPM 包
你创建了一段可复用的代码,想与大家分享,或者你只是有个想法,可以在不同的项目中派上用场。但你完全不知道如何开始编写代码、创建 npm 包,甚至不知道如何发布已有的代码。
我曾经创建过一些小型包,例如ICollections、Ngx-indexed-db、React-indexed-db,现在我想帮助你创建并发布你的第一个包。本教程将专注于如何创建一个简单的包,我不会涉及一些对项目有益的内容,例如使用 TypeScript、语义发布、CI 等等。
我们将共同构建一个在日常应用中非常有用的自定义 React Hook,实现一个简单的切换状态。如果您不熟悉 React Hooks,请查看此链接:React Hooks 文档。
这个想法是能够通过 NPM 运行来安装包
npm install useToggle
然后在任何项目中使用它,如下面的代码所示:
import React from 'react';
import useToggle from 'useToggle';
const App = () => {
const [isLoading, toggleLoading] = useToggle(true);
return (
<div>
<button onClick={toggleLoading}>Toggle</button>
{isLoading ? <div>loading...</div> : <div>Content</div>}
</div>
);
};
export default App;
让我们开始创建一个我将命名为的文件夹useToggle
,导航到文件夹内部并将其初始化为 npm 包。
在控制台中运行以下命令:
mkdir useToggle // to create the folder
cd useToggle // to navigate inside the folder
npm init // to initialize the the npm inside the folder
当我们运行时,npm init
我们必须回答一些应该很简单的问题。这是我最后一条命令的最终结果:
{
"name": "usetoggle",
"version": "1.0.0",
"description": "React hook to facilitate the state toggle",
"main": "lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"react",
"hooks",
"toggle"
],
"author": "Charles Assuncao",
"license": "ISC"
}
安装依赖项
我们需要一些东西来创建项目,让我们通过 npm 安装它:
我们需要useState
React 来让包工作,所以让我们将其作为常规依赖项进行安装
npm install react
我们将在这里使用 babel 来转换和压缩最终代码:
npm install --save-dev @babel/core @babel/cli @babel/preset-env babel-preset-minify
请注意,这次我们传递了标志--save-dev
来表示该依赖项仅用于开发我们的代码,但它不是包工作的依赖项。
我们需要测试代码,确保一切按预期运行。记住:未经测试的代码就是有问题的代码!由于我们要创建自定义钩子,因此需要 React Hooks 测试库。
npm install --save-dev jest @testing-library/react-hooks react-test-renderer
动手,让我们编码吧!
编写测试
让我们开始编写测试,并更加关注代码的预期运行方式。测试驱动开发有几个优点,我强烈建议你深入阅读它。
创建我们要保存代码的文件夹:
mkdir src
在此文件夹中创建三个新文件:
index.js
useToggle.js
useToggle.spec.js
我们的项目现在基本上是这样的:
├── package-lock.json
├── package.json
├── node_modules
├── src
│ ├── index.js
│ ├── useToggle.js
│ ├── useToggle.spec.js
由于我们安装了 jest 来运行测试,我们现在需要在我们的package.json
"scripts": {
"test": "jest"
}
我喜欢 Jest 的这种简洁,无需任何配置。现在我们可以运行npm run test
并执行我们的 specs 文件了。接下来,让我们创建第一个测试:
//useToggle.spec.js
import { renderHook } from '@testing-library/react-hooks';
import useToggle from './useToggle';
describe('useToggle Hook', () => {
test('Should initiate with false as default', () => {
const { result } = renderHook(() => useToggle());
expect(result.current[0]).toBe(false);
});
});
这里发生了什么?
我们为我们的钩子“useToggle Hook”创建了一个测试套件,第一个测试是检查钩子中初始化的默认值。renderHook
执行钩子并返回一个包含钩子返回值的对象,该值保存在 result.current 中。在我们的例子中,钩子将返回一个包含状态值的数组和一个用于修改状态的函数。所以基本上:
result.current[0] // is our state, by default false
result.current[1] // is the toggleState function
如果我们现在运行npm run test
测试,结果会是红色。因为文件里什么都没有useToggle.js
。所以,我们来创建一个简单的函数,让测试结果变成绿色:
//useToggle.js
export default function useToggle(initialState = false) {
return [initialState];
}
现在运行测试,看到绿色代表幸福
我们的函数已经返回了默认初始值为 false 的数组。让我们思考并创建一些测试来测试我们的钩子如何工作:
//useToggle.spec.js
import { renderHook, act } from '@testing-library/react-hooks';
import useToggle from './useToggle';
describe('useToggle Hook', () => {
test('Should initiate with false as default', () => {
const { result } = renderHook(() => useToggle());
expect(result.current[0]).toBe(false);
});
test('Should initiate with the provided value', () => {
const { result } = renderHook(() => useToggle(true));
expect(result.current[0]).toBe(true);
});
test('Should toggle the value from false to true', () => {
const { result } = renderHook(() => useToggle());
act(() => {
result.current[1]();
});
expect(result.current[0]).toBe(true);
});
});
前两个测试会通过,我们的useToggle
函数返回的是满足前两个测试要求的状态模拟。但我们的钩子实际上并没有执行任何操作。所以让我们改变一下,让测试再次顺利通过。
import { useState } from 'react';
export default function useToggle(initialState = false) {
const [state, setState] = useState(initialState);
function toggleState() {
setState(!state);
}
return [state, toggleState];
}
我们从 react 中导入useState
,并使用它来保存我们的初始值并通过函数来改变它,setState
但不是返回setState
函数,而是创建一个闭包来切换状态值,使其按照我们的预期表现。
现在运行测试,所有测试都通过后,控制台会闪闪发光。不过,我们再创建一些测试,只是为了好玩。最终的测试文件将如下所示:
import { renderHook, act } from '@testing-library/react-hooks';
import useToggle from './useToggle';
describe('useToggle Hook', () => {
test('Should initiate with false as default', () => {
const { result } = renderHook(() => useToggle());
expect(result.current[0]).toBe(false);
});
test('Should initiate with the provided value', () => {
const { result } = renderHook(() => useToggle(true));
expect(result.current[0]).toBe(true);
});
test('Should toggle the value from false to true', () => {
const { result } = renderHook(() => useToggle());
act(() => {
result.current[1]();
});
expect(result.current[0]).toBe(true);
});
test('Should toggle the value from true to false', () => {
const { result } = renderHook(() => useToggle(true));
act(() => {
result.current[1]();
});
expect(result.current[0]).toBe(false);
});
test('Should execute multiple toggles', () => {
const { result } = renderHook(() => useToggle()); //init false
// false -> true
act(() => {
result.current[1]();
});
// true -> false
act(() => {
result.current[1]();
});
// false -> true
act(() => {
result.current[1]();
});
// true -> false
act(() => {
result.current[1]();
});
expect(result.current[0]).toBe(false);
});
});
最后但同样重要的是,我们应该从入口点导出钩子index.js
。只需一行代码即可完成:
// index.js
export { default } from './useToggle';
建筑
让我们配置构建脚本,我们需要用到 Babel。所以,我们创建一个 Babel 配置文件 (babel.config.js)。我们的配置应该非常简单:
//babel.config.js
module.exports = {
presets: ['@babel/preset-env', 'minify'],
};
并在我们的 package.json 中创建一个构建脚本:
"scripts": {
"test": "jest",
"build": "babel src --out-dir lib"
}
现在我们可以运行npm run build
并且它将生成lib/index.js
文件。
发布
为了发布它,我们需要对项目进行一些小的修改package.json
。让我们配置一下应该包含在包中的文件,以及一个每次尝试发布包时都会运行的特殊脚本。此外,我们将把 React 依赖项更改为 peerDependency,因为我们期望使用我们包的项目已经有自己的 React 版本:
"files": [
"lib"
],
"scripts": {
...
"prepublish": "npm run build"
},
. . .
"peerDependencies": {
"react": "^16.9.0"
},
运行npm login
并使用我们之前在 npm 站点创建的凭证。成功登录后,您可以运行 now npm publish
。现在,您的软件包已经存在于 npm 软件包的世界中,任何人都可以使用它,只需运行npm install useToggle