梦回苍石居  |
Cangshi Live

NPM 包的开发指南

TYPESCRIPTJAVASCRIPTPUBLISHPACKAGENPMNODEJS
苍石 发表于:2021-06-24 18:13:30  最后编辑于:3 年前 1804 Views

NPM 包的开发指南

NPM, 全称 Node Package Manager,用过 Javascript 做开发的朋友应该都知道这个东西,其实严格上来说,NPM 是随同 NodeJS 一起安装的包管理工具,能解决 NodeJS 代码部署上的很多问题。

无论是前端开发,还是 Node 服务器的搭建,我们都会不可避免地使用到 NPM,通过 NPM 我们可以引用第三方的库/框架到自己的项目中,并且在使用起来特别简洁,只需要一行代码,以常用的库来说:

npm install vue express

以上命令便在当前项目中很快地安装了前端使用的框架 Vue 和服务器使用的框架 Express

不仅如此,通过使用 NPM ,在协同开发的时候,我们不必将任何依赖上传到代码库中,其中一个人引用的依赖,别人在 clone 了目录结构后,只需要运行 npm install 便可以将所有依赖自动下载到本地。

再比如说,NPM 提供了一系列 script 可以快捷运行常用的命令

还有很多很多方便的地方这里就不一一介绍了。

总而言之,Javascript 的开发越来越离不开 NPM

那你有没有在使用别人的包或框架时,有没有相关发布一个自己的包,让全网的人去使用呢?

我相信肯定是有的,下面我们就来看看怎样在 NPM 上发布属于自己的包,并且还会谈一谈相关的项目结构,工程自动化以及简单的优化、打包等内容

发布包之前的准备工作,以及发布一个最简单的包

  1. 首先需要去官网注册一个 NPM 的账号 npmjs.com

    整体上的注册流程十分简单,主要是提供一个邮箱账号,并且验证通过

  2. 安装 NodeJS 环境(此处略过100字)

  3. 在本地登录并配置 NPM 账号(让 NPM 知道上传的包是属于谁的)

    配置很简单,命令输入 npm adduser 然后根据提示输入之前的注册信息便可。(如果不是第一次发布将命令行换成 npm login)

最基本的准备工作就完成啦,紧接着就是发布了,从最简单的来说,发包流程大致如下

  1. 首先需要准备想要发布的代码(简单起见,这里的代码没有任何依赖)
  2. 新建一个文件夹,在文件夹内运行命令 npm init,这里会命令行会问你包的一些基础信息,比如包名什么的
  3. 将你的代码复制到这个文件夹内,比如 demo.js
  4. 这时你会看到文件夹内有两个文件 => demo.js, package.json(刚刚你填的基础信息会放在这里,后面会详细讲讲里面的具体属性和具体作用)
  5. 打开 package.json 修改 main 属性值为 demo.js (指定你这个包的入口文件)
  6. 好了,再次打开命令行运行 npm publish --access public 便成功地部署到了 NPM 官方服务器上(--access public 是发布一个公有包的意思,emmmm,私有的包是要给钱的)

通过上面六个步骤,我们便发布了一个最简单的包到服务器上,这时候在任意一个项目里运行 npm install [你的包名] 便可以使用了

package.json 里常用的属性

从前面的发布流程我们不难看出,package.json 这个文件便是 NPM 的核心文件,没有之一。

想必即便是没有发布包的人,也常用到它,因为它可以管理当前项目的依赖,并且可以通过 script 自定义一些快捷命令,十分简单便捷。

那作为想要发布一个包的开发者而言,我们有必要详细聊聊里面常用的属性

  • nameversion
    • 这两个属性如果是在通常的项目里,随便乱填好像也没啥毛病,但对于一个包的作者而言是十分重要的
    • name 表示你这个包的唯一的名字(不能和其它已发布的包冲突),如果别人使用你的包就得通过这个名字来引入
    • version 很简单就是你这个包的版本,每次发布一个新的版本必须是一个递增的版本号(别人使用你的包也可以通过带版本号的标示来安装指定版本)
  • main, module
    • 这两个属性通常作为包的入口来使用,通过定义这两个属性告诉 NPM 你的入口位置,如设置为 "main": "index.js",那么在引用这个包的时候,引用包名会自动映射到你的入口文件。
    • mainmodule 的区别:一般来说,如果指定了 module 属性即告诉 NPM 我这个包支持模块化,这里的模块化是指 ES Module,而 module 指定的入口便是模块化的入口,当指定了 module 入口,很多打包工具便会自动识别出来,在打包的时候通过 tree-shaking(摇树优化)就可以按需打包,只打包引用到的内容,后面会详细讲讲 Javascript 中的模块化
  • dependencies, devDependencies
    • 这两个属性作为最常用到的属性用来引入依赖,对于日常非发布包需求的项目来说使用起来没什么不同
    • 然而对于要发布一个包的我们而言,还是有一定区别的,就从单词拼写而言,一个是依赖,一个是开发依赖,看起来好像没多大区别,然而实际上在发包的时候,作为 dependencies 引入的依赖会在别人引入你的包的时候自动下载,而 devDependencies 引入的依赖只有在你开发包的时候用到,当别人使用你的包的时候,是不会下载这些依赖的。
  • 其它的常用属性
    • license
      • NPM 上发布一个公共的包,也可以认为是发布一个开源工具,所以需要规定当前包的开源使用许可,通常情况下还需要在根目录存放一个命名为 LICENSE 的文件用以详细描述,常用的有 MIT, GPL
    • scripts
      • 这个属性肯定很多人都用过,可以快速的运行一些自定义的命令行,一般可用于开发调试,测试,打包,部署等
    • files
      • 在默认情况下,在发布包的时候会将目录下除了 node_modules 的所有文件上传到 NPM 服务器,如果想指定只上传某些文件可以通过 files 属性来指定
    • description
      • 设置关于这个包的简单描述,如果要详尽地描述包的内容,需要在根目录添加 readme.md 用作详细的介绍
    • types
      • 可以设置 ***.d.ts 作为该包的类型声明,一般来说 IDE 会自动识别
    • author, contributors
      • 这两个属性可以声明当前包的作者以及贡献者名单
    • repository
      • 这个属性可以指定当前包所在的远程代码库的位置,例如如果是依托在 github 上,可以在这里填入对应的地址
    • keywords
      • 用以列出当前包的关键字

Javascript 中的模块化

Javascript 发展至今已经很多年了,模块化作为项目开发不可或缺的一部分,它的模块化也已经有了很多版本,用的稍多的大体上有以下几种规范:

  1. CommonJS 规范 (NodeJS 所采用的规范,针对于服务端)

    // 加载模块
    const a = require('xxx');
    // 定义模块
    module.exports = xxxx; 
    
  2. AMDCMD 规范

    都是针对于 web 端开发的模块化规范,分别对应 require.jssea.js 两种模块化框架(其中 sea.js 基本上凉了,很少有人用)

  3. ES Module 规范

    在 ES6 中引入的模块化规范,浏览器和服务器通用的模块解决方案,完全可以取代 CommonJS 和 AMD 规范(不过目前无论是 NodeJS 还是 Web 端都不能直接使用,需要开启一些选项或者编译为对应的语法)

    // 加载模块
    import xxx from 'xxx'
    // 定义模块
    export xxx
    
  4. UMD 规范

    实际上 UMD 规范的目的是让代码在服务器和 web 端都能运行,与其说是一种模块标准,不如说它是一组模块形式的集合更准确,它在 ES Module 之前提出可以兼容 CommonJSAMD 等其它规范

(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS之类的
        module.exports = factory(require('jquery'));
    } else {
        // 浏览器全局变量(root 即 window)
        root.returnExports = factory(root.jQuery);
    }
}(this, function($) {
    // 方法
    function myFunc() {};

    // 暴露公共方法
    return myFunc;

}));

包的分类

上面讲了 Javascript 的模块化规范,有了这个基础,我们可以为我们的包做一个简单的分类

  1. 服务端使用的包,开发时使用 CommonJSES ModuleUMD 规范
  2. Web 端所引用的包,开发时使用 ES ModuleUMDAMDCMD 规范
  3. 通用引用的包(服务端+Web端),使用 UMD 规范
  • NodeJS 中原生地使用 ES Module 需要更改后缀为 .mjs 还要在配置中开启 --experimental-modules 选项
  • Web 端原生地使用 ES Module 需要在 <script> 标签上加入一个属性 type="module"<script type="module"></script>
  • 再或者是将 ES Module 编译为相对应的模块化规范

开发模式的选择

可能你们已经注意到了,上面提到了"编译"二字,所谓"编译",就和我们的开发模式有着息息相关的联系。

对于开发模式而言,我这里大致把它分为两类:

  1. 非工程化

    所谓"非工程化",便是在开发过程中直接写原生的代码,是可以直接被 NodeJSWeb 端所直接运行的。

    优点:开箱即用,方便快捷 缺点:

    1. 首先对于 Web 端而言,在单次引用的情况下,你开发的代码必须放在一个文件里,并且还不能有任何的依赖,基本不适用
    2. 无论是 Web 还是 Node,由于 Javascript 的语法变化很快,你的代码不能向前兼容,这意味着如果老的 Javascript 环境将会无法运行你的代码
  2. 工程化

    工程化是一个很广泛的概念,我这里理解为通过使用一些开发者工具,让你的项目能够实现编译、打包、测试等开发所需要的步骤 优点:灵活性相当高,基本上可以弥补非工程化项目的所有缺点,可以根据需要选择第三方的工具,实现想要的效果,比如语法编译兼容,比如多文件打包,自动测试等等 缺点:任何第三方开发工具都是有学习成本的,项目结构的架构也是需要设计的,所以项目的准备时间会拉长

总体上而言,工程化对于一个项目而言,可以降低其开发维护成本,在不是必要的情况下都推荐将项目工程化

Javascript 工程化中的语法编译

工程化的项目在开发的过程中,编译是很有必要的,那为什么要编译呢?主要有以下三个目的:

  1. 模块的转化

    这个很好理解,就是将各种模块化规范相互转换,比如 ES Module => CommonJS

  2. 兼容老的运行环境

    在开发的时候总不能面面俱到,但我们又需要考虑代码在低版本的运行时中是否能运行,这时我们可以通过编译工具将代码转换 ES5 甚至 ES3 的代码

  3. 支持一些新的语法,甚至语法的变种

    Javascript 有很多语法提案,比如装饰器,比如管道操作符等等,但很多提案并没有通过还没有应用到运行环境上,这时,我们可以通过第三方编译工具将使用了这些提案的代码转化为当前版本的代码。除此之外,一些 Javascript 的变种语言,如 Typescript 或者 Coffeescript 都可以用过编译工具转化为原生的 JS 代码。

为了实现以上需求,有很多编译器可以使用,我这里推荐两个:

  1. typescript 的官方编译器,支持将 typescript 转化为原生语法,还有很多与 typescript 配套的功能,比如语法检查等等
  2. babelbabel 是官方称之为下一代的 Javascript 编译器,基本上支持你能想到的所有的编译案例,强大的插件功能让它也支持 typescriptcoffeescript

Javascript 工程化中的测试

一个健壮的包,开发过程中的测试是少不了的,测试分为很多种,其中在开发环节比较重要的主要是单元测试。

对于单元测试而言,我们需要编写相应的测试用例,并且使用第三方的测试工具来实现自动测试。

比较推荐的单元测试框架有以下两个:

  1. jest 支持自动化测试,可以配合 babel 实现不同语法的测试用例撰写(我常用的)
  2. mocha 是官方称之为简单, 灵活, 有趣的测试框架,可以使异步测试变得简单有趣(很少用到,不过应该值得尝试)

除了单元测试,某些项目可能还会用到端对端测试(由于用的少,这里就简单列一下常用的第三方框架,感兴趣的可以自行了解)

  1. CasperJS
  2. Protractor
  3. Nightwatch.js
  4. TestCafe
  5. CodeceptJS

Javascript 工程化中的打包

一般来说,如果是作为可以在 Web 端使用的包,都是要进行打包的,打包可以将你使用到的依赖以及你自己的代码打包到单个文件中,这样便可以在 Web 端直接引用这个打包好的文件即可。

对于打包的过程我们需要注意以下几点:

  1. 一般来说,打包工具可以自定义打包后的模块化规范,针对于前后端都可能使用到的包,推荐使用 UMD 规范

  2. 如果我们的包是可能会被其它包引用的,或者说别人打包他们的项目时会用到了你的包作为依赖,这种情况下,由于你自己的入口也是打包了的文件,会导致别人打包时就算用了你这个包的一个小功能也会将你的包整个地打包到别人打包后的文件,造成文件过大。

    怎么解决呢,这里我们首先需要在打包的同时,生成一个以 ES Module 为规范的的打包文件,并将这个文件的路径放到 package.json 中的 module 属性(前面有提到哦),这样在使用支持 tree-shaking 的打包工具进行打包时,会按需的打包使用到的内容。

这里列举几个常用的打包工具:

  1. Webpack 一般用于前端项目的打包,也可用于 NPM 中库的打包
  2. RollupJS 一般用于 NPM 中库的打包,通过 ES Module 的特性,能够按需地打包所依赖的内容,生成的代码十分简洁
  3. Vite - Vue 作者新推出的打包工具,性能优秀,但生态较弱
  4. Parcel 等等

值得一提的是,webpack 更多的用于前端资源的打包,可以将 css, js甚至是图片都进行打包,而 rollup.js 更多用于框架的打包,生成的代码十分简洁

Javascript 工程化中的文档生成

在你完成了包的开发,准备上传到 NPM 时,问问自己,如果别人来使用你的包,他能不能正常使用呢?换言之,我们应该提供比较完善的文档以供参考。

一般来说,我们需要在发布之前提供以下内容:

  1. readme.md 最基础的介绍,讲讲这个包解决了什么问题,可以在里面提供一个简单的例子,如果包相对简单还可以将 API 接口写着这里面

  2. index.d.ts 配置在 package.jsontypes 属性

    这是 typescript 中提供的,称之做声明文件,虽然是 typescript 文件,但也可以用在非 typescript 项目中,通过这个文件我们可以规定我们包所提供的接口,甚至方法参数,在引用这个包时,IDE 会自动根据这个文件来提示方法名或参数。

    关于 index.d.ts 的撰写,如果不是 typescript 的项目,可以通过手动写,也可以通过 tsc(typescript 自带的 cli 工具)通过你代码中的 jsdoc 注释来自动生成

    • 使用 javascript 可以通过 tsc 运行命令 tsc main/index.js --declaration --allowJs --emitDeclarationOnly --outDir types
    • 使用 typescript 可以在配置文件中配置 "declaration": true 便可以在编译时自动生成
  3. API 文档

    关于 API 文档,如果简单可以手动撰写,如果比较复杂,我们可以通过 jsdoc 在开发的时候写在代码注释里,然后通过自动生成工具生成

    • 使用 javascript 可以通过 jsdoc2md 运行命令 jsdoc2md "main/**/*.js" > docs/readme.md
    • 使用 typescript 可以通过 typedoctypedoc-plugin-markdown (用于生成 markdown 格式) 运行命令 typedoc --out docs main/**/*.ts

上面无论是类型的声明还是文档的生成,都提到了一个叫做 jsdoc 的东西,这个是 Javascript 中的文档规范,可以通过这个规范来告诉别人某个方法的调用方式:

/**
 * @typedef  {Object} Person
 * @property {number} age
 * @property {string} gender
 */

/**
 * Using to get person by age and gender
 * @param {number} age 
 * @param {string} gender 
 * @return Person
 */
function getPerson(age, gender) {
    return {age, gender}
}

以上代码展示了一个简单的 jsdoc 例子,在这个例子里我们可以指定方法参数的类型,返回值的类型以及一些文档说明,更多用法见 jsdoc 官网

Javascript 工程化中的其它工具

除了上述用于参与开发流程的编译,测试,打包,文档生成等功能,在工程化的项目结构中还可以加入一些其它的功能,比如语法检查:jslinttslint,比如代码风格统一:prettier 等等

总结

综上而言,想要发布一个包到 NPM 上是一件很简单的事,同时想要发布一个高质量的包也是一件不容易的事。这篇文章主要讲了 Javascript 项目中的工程化,我们可以从管中窥豹,灵活运用,举一反三,无论是在任何项目中,做一个框架也好,后台项目也好,前端项目也好,学习工程化从而使得开发更加灵活,更加优雅。

文章评论 ( 0 )

Person name
未登录用户可以发表匿名评论