三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从零到一发布高质量npm包:实战指南与核心要点解析

从零到一发布高质量npm包:实战指南与核心要点解析

1. 从使用者到创造者:理解npm生态的核心价值

如果你是一名前端开发者,那么“npm install”这个命令对你来说,就像呼吸一样自然。每天,我们都在享受着npm仓库里海量开源包带来的便利,从React、Vue这样的框架,到lodash、axios这样的工具库,它们构成了现代前端开发的基石。但不知道你有没有想过,这些包是怎么来的?它们是如何被创建、打包,并最终出现在那个庞大的仓库里,供全球开发者使用的?今天,我们就来聊聊这个话题——如何从一个npm包的“消费者”,转变为一个“生产者”,亲手打造并发布自己的npm包。

这不仅仅是一个技术操作,更是一种思维方式的转变。当你开始思考如何设计一个包时,你会被迫去考虑API的简洁性、模块的边界、依赖的管理以及版本的控制。这些思考,会让你对前端工程化的理解提升一个维度。无论你是想封装团队内部的工具函数,还是有一个绝妙的创意想分享给社区,掌握发布npm包的完整流程,都是一项极具价值的能力。接下来,我将以一个实战者的角度,带你走通从零到一发布一个npm包的全过程,并分享其中那些文档里不会写的“坑”和技巧。

2. 项目整体设计与思路拆解

在动手写第一行代码之前,清晰的顶层设计是避免后续返工的关键。一个成功的npm包,不仅仅是能运行的代码,更是一个考虑周全的“产品”。

2.1 核心需求与定位分析

首先,你需要明确你的包要解决什么问题,以及它的目标用户是谁。这决定了包的功能范围、API设计和复杂度。

  • 工具类库:例如,封装一组常用的数据处理函数(如日期格式化、URL参数解析)。这类包的核心是无副作用功能纯粹,API设计追求原子化和可组合性。依赖应尽可能少,甚至零依赖(Zero Dependency),以减小体积,避免依赖冲突。
  • UI组件/插件:例如,一个基于Vue/React的特定业务组件,或一个给富文本编辑器用的插件。这类包的核心是与宿主框架/环境良好集成。你需要明确支持的框架版本,并处理好样式(是内联、导出CSS文件,还是支持按需引入)。
  • 脚手架/构建工具:例如,一个快速生成项目模板的CLI工具。这类包的核心是交互体验和可配置性。你需要处理命令行参数、文件读写、模板渲染等,对Node.js环境操作要求更高。

以我最近封装的一个“智能裁剪并上传图片”的工具包为例。我的核心需求是:在Web前端,用户选择图片后,能先进行本地裁剪预览,然后压缩并上传。它的定位是一个轻量级、无UI框架依赖的工具函数集合。因此,我决定不依赖任何具体的UI库(如Element UI、Ant Design),而是输出纯JavaScript函数和基于原生DOM的示例,让使用者可以自由集成到自己的UI中。

2.2 技术选型与架构考量

确定了定位,接下来就要选择实现的技术栈和规划项目结构。这不是简单地堆砌技术,而是基于可维护性、开发体验和用户体感做出的权衡。

  1. 开发语言与语法:现代前端包普遍采用ES6+语法编写。但为了兼容性,我们需要通过构建工具(如Rollup、Webpack)将其编译为ES5(或ES2015)的通用模块格式。我强烈推荐使用TypeScript进行开发。即使你的包本身是JavaScript,TypeScript提供的类型提示、接口定义也能极大提升开发体验,并且最终可以生成.d.ts类型声明文件,让使用你的包的其他开发者也能获得完美的代码提示。
  2. 模块系统:你需要决定包输出哪些模块格式。目前主流的有:
    • ES Module (ESM):使用import/export语法,是现代构建工具和浏览器原生模块的首选,支持静态分析和Tree Shaking(摇树优化,移除未使用代码)。
    • CommonJS (CJS):使用require/module.exports语法,是Node.js环境的传统标准。
    • UMD:一种兼容性格式,试图同时支持AMD、CJS和全局变量模式,适用于直接在浏览器<script>标签中引入。 一个成熟的包通常会同时输出ESMCJS两种格式,并在package.json中通过“main”(指定CJS入口)、“module”“exports”(指定ESM入口)字段来声明,让打包器能自动选择最优格式。
  3. 构建工具选择:这是核心决策点。Webpack功能强大但配置复杂,更适合应用构建。对于库/包开发,RollupVite是更优的选择,它们天生为打包库而设计,能生成更干净、更小体积的包。
    • Rollup:是库打包的事实标准,对ESM支持极好,生成的代码非常简洁,易于实现Tree Shaking。社区插件生态成熟。
    • Vite:基于ESBuild,开发体验极快。其库模式(build.lib)配置简单,也能输出高质量的包。如果你同时需要优秀的开发服务器和打包能力,Vite是个不错的选择。 在我的项目中,我选择了Rollup,因为它输出更可控,并且有丰富的插件来处理TypeScript、Babel转换、压缩等。
  4. 代码质量与规范:从一开始就集成代码检查工具能省去后期大量整理时间。
    • ESLint:检查JavaScript/TypeScript代码质量和风格。
    • Prettier:代码自动格式化工具,与ESLint配合使用。
    • Husky + lint-staged:在Git提交前自动运行lint和格式化,确保仓库代码一致。
  5. 测试框架:一个没有测试的包就像没有保险的汽车。至少应包含单元测试。Jest是目前最流行的选择,它开箱即用,支持快照测试、覆盖率报告。为你的核心函数编写测试,不仅能保证代码质量,也是你后续迭代重构的“安全网”。

基于以上考量,我项目的技术栈最终定为:TypeScript + Rollup + Jest + ESLint & Prettier。这是一个在功能、体验和社区支持上非常平衡的组合。

3. 核心细节解析与实操要点

有了蓝图,我们开始深入每个环节的细节。这里有很多决定成败的“魔鬼”。

3.1 package.json:包的身份证与说明书

package.json是npm包的灵魂文件,它定义了包的一切元信息。很多新手会忽略其中字段的精确含义,导致包发布后问题频出。

  • name(包名):这是全局唯一的标识。发布前,务必去 npm官网 搜索一下你想用的名字是否已被占用。命名应遵循kebab-case(短横线分隔),如my-awesome-utils。如果你想发布到公司私有仓库,可能需要配置作用域(scoped),如@my-org/my-package
  • version(版本):必须遵循 语义化版本规范(SemVer) 。格式为主版本号.次版本号.修订号MAJOR.MINOR.PATCH)。
    • PATCH:向后兼容的问题修复,递增修订号,如1.0.0 -> 1.0.1
    • MINOR:向后兼容的功能性新增,递增次版本号,如1.0.1 -> 1.1.0
    • MAJOR:不兼容的API变更,递增主版本号,如1.1.0 -> 2.0.0。 严格遵守此规范,是作为包作者对使用者的基本尊重。
  • main,module,exports(入口文件)
    • main: 定义CommonJS入口,通常是dist/index.cjs.jslib/index.js
    • module: (非官方标准,但被广泛支持)定义ES Module入口,通常是dist/index.esm.jses/index.js
    • exports: Node.js 12+引入的现代入口定义方式,功能更强大,可以条件导出不同环境下的入口,并替代mainmodule。例如:
      "exports": { ".": { "import": "./dist/index.esm.js", "require": "./dist/index.cjs.js", "types": "./dist/index.d.ts" }, "./style.css": "./dist/style.css" }
  • files(发布包含的文件):一个数组,指明哪些文件会被打包发布到npm。通常只包含构建产物(dist/lib/)、README.mdLICENSEpackage.json本身。千万不要把src/源码、node_modules/、测试文件等无关内容发布上去,这会让你的包体积臃肿。
  • dependenciesvsdevDependenciesvspeerDependencies(依赖管理)
    • dependencies:你的包运行时必须依赖的第三方包。用户安装你的包时,这些也会被自动安装。
    • devDependencies:仅在开发阶段需要的包,如构建工具、测试框架、TypeScript。这些不会被打包进最终产物,用户安装你的包时也不会安装它们。
    • peerDependencies:声明你的包期望宿主环境已经安装的包,但你不直接依赖它。常见于插件、组件库。例如,一个Vue 3组件库会在peerDependencies中声明"vue": "^3.2.0",意思是“我需要Vue 3.2+的环境,但我不打包Vue,请使用者自己安装”。这能避免同一个Vue被多次打包,引发冲突。
  • types/typings(类型声明):如果你用TypeScript开发,构建后会生成.d.ts文件。通过此字段指向它(如"types": "./dist/index.d.ts"),能为使用者的TypeScript项目提供完美的类型提示。

实操心得:在第一次发布前,用npm pack命令可以生成一个.tgz压缩包,模拟发布后的内容。解压这个包,检查里面的文件是否和你预期一致,这是避免误发布无用文件的最佳实践。

3.2 源码组织与模块设计

好的代码结构让开发和维护都事半功倍。一个典型的库项目结构如下:

my-npm-package/ ├── src/ # 源代码目录 │ ├── index.ts # 主入口文件,统一导出所有模块 │ ├── core/ # 核心逻辑模块 │ ├── utils/ # 内部工具函数 │ └── types/ # TypeScript类型定义 ├── dist/ # 构建输出目录(由Rollup生成) ├── tests/ # 测试文件目录 ├── .eslintrc.js # ESLint配置 ├── .prettierrc # Prettier配置 ├── rollup.config.js # Rollup配置 ├── tsconfig.json # TypeScript配置 ├── jest.config.js # Jest配置 └── package.json

src/index.ts中,你应该清晰地导出所有希望对外提供的API。避免导出内部模块,保持API的简洁和稳定。

// src/index.ts export { default as ImageCropper } from './core/ImageCropper'; export { compressImage, formatFileSize } from './utils/image-utils'; export type { CropArea, CompressOptions } from './types';

3.3 Rollup配置详解

Rollup的配置是打包的核心。下面是一个支持TypeScript、生成多种格式、压缩并生成类型文件的配置示例:

// rollup.config.js import typescript from '@rollup/plugin-typescript'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import { babel } from '@rollup/plugin-babel'; import { terser } from 'rollup-plugin-terser'; import dts from 'rollup-plugin-dts'; export default [ // 主打包配置,生成ESM和CJS { input: 'src/index.ts', output: [ { file: 'dist/index.esm.js', format: 'esm', // ES Module格式 sourcemap: true, // 生成sourcemap,方便调试 }, { file: 'dist/index.cjs.js', format: 'cjs', // CommonJS格式 sourcemap: true, }, ], plugins: [ nodeResolve(), // 解析node_modules中的第三方模块 commonjs(), // 将CommonJS模块转换为ES6 typescript({ tsconfig: './tsconfig.json' }), // 编译TypeScript babel({ babelHelpers: 'bundled', // 处理ES6+语法转换 exclude: 'node_modules/**', }), terser(), // 代码压缩 ], external: ['lodash-es'], // 将lodash-es声明为外部依赖,不打包进来 }, // 单独打包类型声明文件.d.ts { input: 'src/index.ts', output: [{ file: 'dist/index.d.ts', format: 'es' }], plugins: [dts()], }, ];
  • external选项至关重要:这里列出了你希望作为“外部依赖”的包名。Rollup不会打包这些依赖的代码,而是保留import语句,让用户环境去提供。这能显著减小你的包体积,并避免与用户项目的依赖发生冲突。通常,像vuereactlodash这样的大型库都应该被external

4. 实操过程与核心环节实现

让我们一步步走完从编码到发布的完整流程。假设我们的包名是smart-image-uploader

4.1 初始化项目与环境搭建

首先,创建一个新目录并初始化项目。

mkdir smart-image-uploader cd smart-image-uploader npm init -y

编辑生成的package.json,填入核心信息:

{ "name": "smart-image-uploader", "version": "0.1.0", "description": "A lightweight utility for cropping, compressing and uploading images in browser.", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts", "files": ["dist"], "scripts": { "build": "rollup -c", "dev": "rollup -c -w", "test": "jest", "lint": "eslint src --ext .ts", "format": "prettier --write \"src/**/*.ts\"", "prepublishOnly": "npm run lint && npm run test && npm run build" }, "keywords": ["image", "upload", "crop", "compress", "frontend"], "author": "Your Name", "license": "MIT", "devDependencies": { "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.6", "@types/jest": "^29.5.12", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "jest": "^29.7.0", "prettier": "^3.2.5", "rollup": "^4.12.0", "rollup-plugin-dts": "^6.1.0", "rollup-plugin-terser": "^7.0.2", "ts-jest": "^29.1.2", "tslib": "^2.6.2", "typescript": "^5.4.3" }, "peerDependencies": { "cropperjs": "^1.6.1" } }

注意scripts里的prepublishOnly钩子:这是一个npm生命周期脚本,在npm publish执行之前自动运行。我们在这里串联了代码检查、测试和构建,确保每次发布的内容都是经过检验的。

然后,安装所有开发依赖(这是一个长命令,耐心执行):

npm install --save-dev @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript @types/jest @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier jest prettier rollup rollup-plugin-dts rollup-plugin-terser ts-jest tslib typescript

同时,因为我们声明了cropperjspeerDependencies,也需要安装它(但作为普通依赖,因为开发时我们需要它):

npm install cropperjs

接着,配置TypeScript (tsconfig.json)、ESLint (.eslintrc.js)、Prettier (.prettierrc) 和 Jest (jest.config.js)。这些配置有较多样板内容,你可以在项目初始化后从成熟的开源项目中参考或使用npx tsc --init等命令生成基础配置,再根据项目调整。

4.2 编写核心功能与单元测试

src/目录下编写你的业务代码。以我们的图片压缩工具函数为例:

// src/utils/image-utils.ts export interface CompressOptions { maxWidth?: number; maxHeight?: number; quality?: number; // 0.1 - 1.0 mimeType?: string; // e.g., 'image/jpeg', 'image/png' } export async function compressImage( file: File, options: CompressOptions = {} ): Promise<Blob> { const { maxWidth = 1920, maxHeight = 1080, quality = 0.8, mimeType = 'image/jpeg' } = options; return new Promise((resolve, reject) => { const img = new Image(); const reader = new FileReader(); reader.onload = (e) => { img.src = e.target?.result as string; }; img.onload = () => { const canvas = document.createElement('canvas'); let { width, height } = img; // 计算等比例缩放后的尺寸 if (width > maxWidth || height > maxHeight) { const ratio = Math.min(maxWidth / width, maxHeight / height); width *= ratio; height *= ratio; } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('Failed to get canvas context')); return; } ctx.drawImage(img, 0, 0, width, height); canvas.toBlob( (blob) => { if (blob) { resolve(blob); } else { reject(new Error('Canvas toBlob failed')); } }, mimeType, quality ); }; reader.onerror = () => reject(new Error('FileReader failed')); img.onerror = () => reject(new Error('Image loading failed')); reader.readAsDataURL(file); }); } export function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }

紧接着,为这个函数编写单元测试。测试是信心的来源。

// tests/image-utils.test.ts import { compressImage, formatFileSize } from '../src/utils/image-utils'; // 注意:由于compressImage依赖DOM API(Canvas, Image), // 我们需要在Jest中配置相应的测试环境(如jsdom) describe('image-utils', () => { describe('formatFileSize', () => { it('should format bytes correctly', () => { expect(formatFileSize(0)).toBe('0 B'); expect(formatFileSize(1024)).toBe('1 KB'); expect(formatFileSize(1048576)).toBe('1 MB'); expect(formatFileSize(1073741824)).toBe('1 GB'); expect(formatFileSize(1500)).toBe('1.46 KB'); }); }); // 对于compressImage,我们可以模拟一个File对象进行测试 // 这是一个简化的示例,实际测试可能需要更复杂的Mock describe('compressImage', () => { it('should return a Blob', async () => { // 创建一个模拟的图片文件(这里简化,实际可用jest.createMockFromModule等) const mockFile = new File(['dummy'], 'test.jpg', { type: 'image/jpeg' }); // 由于涉及Canvas,在Node环境下测试较复杂,可能需要跳过或在特定环境运行 // 这里仅示意测试结构 console.log('compressImage test requires browser environment'); }); }); });

运行npm test来确保你的测试通过。对于依赖浏览器环境的函数,你可能需要配置Jest使用jest-environment-jsdom,或者在构建后于真实浏览器中进行集成测试。

4.3 构建、本地测试与发布前检查

代码和测试都准备好了,接下来进行构建和本地验证。

  1. 执行构建:运行npm run build。这会在dist/目录下生成打包后的文件(index.esm.js,index.cjs.js,index.d.ts)以及可选的sourcemap。
  2. 本地链接测试(关键步骤):这是发布前最重要的一步,模拟用户安装和使用你的包。
    • 在你的包项目根目录,运行npm link。这会在全局创建一个符号链接,指向你的本地项目。
    • 然后,新建一个测试项目(或者在你的另一个前端项目里),进入其目录,运行npm link smart-image-uploader。这会把全局链接的包安装到当前项目的node_modules中。
    • 在测试项目中,像正常使用npm包一样import你的包,并调用其API。全面测试所有功能,确保在真实环境下一切正常。这是发现API设计问题、依赖缺失或构建错误的最佳时机。
  3. 版本号更新:根据语义化版本规范,决定此次发布是patchminor还是major更新。使用命令更新package.json中的版本号:
    npm version patch # 0.1.0 -> 0.1.1 # 或 npm version minor # 0.1.1 -> 0.2.0 # 或 npm version major # 0.2.0 -> 1.0.0
    这个命令会自动修改package.jsonversion字段,并创建一个对应的Git tag。

4.4 发布到npm仓库

终于到了发布的时刻。如果你是第一次发布,需要先登录npm。

  1. 登录npm:在终端运行npm login。你会被提示输入用户名、密码和邮箱(如果是公开仓库)以及一次性密码(如果开启了双重验证)。确保你使用的npm源是官方源(https://registry.npmjs.org/)。如果你使用了淘宝镜像等国内源,需要先切换回来:
    npm config set registry https://registry.npmjs.org/
  2. 执行发布:运行npm publish。如果你的包名是作用域包(如@my-org/xxx)且想公开发布,需要加上--access public参数:npm publish --access public

    重要提示npm publish命令会触发我们在package.json中定义的prepublishOnly脚本。它会自动执行代码检查、运行测试和构建。只有所有步骤都通过,发布才会继续。这是一个非常重要的质量关卡。

  3. 发布成功:如果一切顺利,终端会显示成功信息,并给出包的访问链接,例如+ smart-image-uploader@0.1.0。你可以立即在npm官网搜索到你的包了。
  4. 设置npm源(针对国内开发者):发布完成后,如果你个人需要从国内源加速安装其他包,可以将registry切回国内镜像,但记住下次发布前要切回官方源。
    npm config set registry https://registry.npmmirror.com/

5. 常见问题与排查技巧实录

即使流程清晰,实操中依然会遇到各种“坑”。下面是我总结的一些高频问题和解决方案。

5.1 发布失败与权限错误

问题现象可能原因解决方案
npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/... - You do not have permission to publish "xxx".1. 包名已被他人占用。
2. 你尝试发布一个作用域包(如@my-org/xxx)但没有指定公开访问权限。
1. 在npm官网搜索包名,确认是否可用。如果被占,需要修改package.json中的name字段。
2. 对于作用域包,使用npm publish --access public发布。
npm ERR! 401 401 Unauthorized - PUT ...1. 未登录或登录状态已过期。
2. 使用了错误的registry(如还在淘宝源)。
1. 运行npm whoami检查当前登录用户。运行npm login重新登录。
2. 运行npm config get registry确认当前registry是https://registry.npmjs.org/
npm ERR! 402 Payment Required尝试发布一个私有包,但你的npm账户没有付费订阅。package.json中的private字段设为false,或升级npm账户套餐。对于开源包,确保privatefalse或未设置。

5.2 包安装后无法使用或报错

问题现象可能原因解决方案
Module not found: Error: Can't resolve 'your-package'1.package.jsonmainmodule字段指向的入口文件路径错误或不存在。
2.files字段未包含构建产物目录。
1. 检查package.json的入口配置,确保路径正确,且文件在发布包内存在(用npm pack验证)。
2. 确保files字段包含了distlib目录。
Uncaught TypeError: yourPackage.default is not a function模块导出方式不一致。你的包可能是默认导出(export default),但使用者以命名导入方式引用(import { something } from ...),或者相反。统一导出方式。库推荐使用命名导出export { funcA, funcB }),这样使用者可以按需引入,利于Tree Shaking。在主入口index.ts做好统一导出。
引入包后,项目打包体积激增未正确配置external,导致第三方依赖(如lodash)被打包进了你的库,然后又被打包进用户项目,造成重复。在Rollup配置中,将明确的、应该由用户提供的依赖列入external数组。并在package.json中用peerDependenciesdependencies声明。
TypeScript项目中使用时没有类型提示1. 未生成.d.ts类型声明文件。
2.package.jsontypes字段未配置或指向错误。
1. 确保TypeScript编译配置tsconfig.json"declaration": true,或使用rollup-plugin-dts
2. 检查package.jsontypes字段是否正确指向生成的.d.ts文件。

5.3 版本管理与更新策略

发布后,如何优雅地迭代?

  • npm version是你的好帮手:如前所述,用npm version patch/minor/major来更新版本号并打Git tag,比手动修改package.json更规范。
  • 善用npm deprecate:如果你发布了一个有问题的版本,或者想引导用户升级到新版本,不要直接unpublish(下架包,npm有严格限制)。可以使用npm deprecate <pkg>@<version> "<message>"来标记某个版本为废弃,并给出提示信息。例如:npm deprecate smart-image-uploader@0.1.0 "This version has a critical bug, please upgrade to 0.1.1"
  • 谨慎使用npm unpublish:根据npm政策,在发布72小时后,只有满足特定条件(如法律原因、安全漏洞等)才能下架包。随意下架会严重影响依赖你的用户。最好的做法是发布一个修复版本,并废弃有问题的版本。
  • 使用.npmignore文件:如果你觉得files字段不够直观,可以创建一个.npmignore文件(类似于.gitignore),列出不希望发布到npm的文件和目录。npm会优先使用这个文件。但通常更推荐使用files字段的白名单机制,更安全。

5.4 关于依赖管理的深度思考

依赖管理是包作者最容易犯错的地方之一。

  • “零依赖”的诱惑:对于工具函数库,追求“零依赖”是高尚的目标,能最大程度避免依赖冲突和体积膨胀。这意味着你需要用原生JavaScript实现所有功能,或者非常谨慎地引入极小的、功能单一的包。
  • peerDependencies的陷阱:声明了peerDependencies后,npm 7+版本会默认自动安装它们,这有时可能不符合预期。你可以通过在用户的项目中配置package.jsonpeerDependenciesMeta来关闭警告,但这需要使用者配合。清晰的文档说明在此刻尤为重要。
  • 锁定开发依赖版本:在package.json中,对于构建工具链(如Rollup、Babel、TypeScript)的版本,建议使用波浪号(~)插入号(^)前缀来允许安装最新的补丁或次要版本,以获取安全更新和bug修复。但对于核心的、可能导致构建结果不一致的依赖,有时也可以考虑锁定精确版本。

发布自己的npm包,是将个人或团队代码资产化、标准化的重要一步。这个过程迫使你以更宏观、更严谨的视角去审视代码。从设计一个清晰的API,到管理好每一份依赖,再到用自动化的流水线保障质量,每一步都是对工程能力的锻炼。当你看到自己的包被下载计数一次次增加,或者收到来自陌生开发者的Issue甚至Pull Request时,那种成就感是单纯完成业务需求所无法比拟的。最后一个小建议:写好README.mdCHANGELOG.md,清晰的文档和更新日志,是你送给使用者最好的礼物。

← 返回列表