如何編寫一個健壯的 npm 包 | 京東雲技術團隊

無腦發佈 npm

比如老王我,用npm init新建一個包,改把改把,然後來個npm publish,so easy ✌️!

Too young too naive, baby 👶!

請容我講述一些發佈過程中踩過的坑。

首先,算了也可以之後有空再說,我們需要通讀npm的配置文檔。

package.json doc

通用性👷

指定發佈文件

利用package.jsonfiles字段精簡發佈體積。

{
  "files": ["dist", "lib", "module"]
}

若不指定files,每次發佈會把所有不以.開頭的文件都發布出去,導致發佈體積過大(node_modules默認也不會被髮布)。

README.md作爲主文檔,加不加都會發布,package.json也是。

指定源代碼

{
  "source": "src/index.ts",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourname/yourproject.git"
  }
}

通常來說我是不在npm發佈中包括源代碼的,因此都沒有加過source字段,只是用repository來告知一下git倉庫地址即可。

如果倉庫是內部倉庫或私人倉庫並不對外,則source字段就有用了,將源代碼發佈後可讓人幫忙debug找問題。

注意如果有source,則files也要加上souce對應的文件或文件夾。

發佈sourcemap

一般來說我們發佈的都是經過編譯的代碼,爲了給使用者方便調試,只要不是源碼,都要有對應的sourcemap文件,例如發佈了一個dist/index.js則也需要一個dist/index.js.map文件與之配套。

指定安裝源

如果你從來不用私有源,可跳過該項。

利用.npmrc指定安裝源,用於當前項目與你的全局配置區分開。

否則當前項目很可能指定的內部npm源,導致外部用戶無法利用lock文件安裝。

例如

registry=https://registry.npmjs.org/

精確指定dependenciesdevDependenciespeerDependencies

dependencies要儘量少,只有在運行時確實用到才放進去。

依賴的版本號要清晰指明,如"react": "16.x || 17.x"

否則,如果指定了"react": "17.0.0",則在使用了react 16的項目中,會引入兩份react,造成一些莫名其妙的問題。

這種情況,react應放到peerDependencies中。

指定發佈目標

如果你從來不在私有源發佈,可跳過該項。

package.json中指定發佈地址,在當前包與全局配置不一致時非常必要。

{
  "publishConfig": {
    "registry": "https://registry.npmjs.org"
  }
}

sideEffects

對應配置:

{ "sideEffects": false }

作用:在打包時進行treeshake可根據是否使用而優化相關的代碼。

如果sideEffectstrue,則一旦引入,不管是否調用都不能被treeshake掉。

專用性🥷

類型配套

無論針對哪個環境,目前自帶類型已經是既成事實的標配。

記得生成類型的.d.ts文件,並在package.json中指定。

{
  "types": "type/index.d.ts",
  "typings": "type/index.d.ts"
}

我一般會用一個專用的tsconfig.declaration.json來專門生成類型:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": false,
    "emitDeclarationOnly": true,
    "declaration": true,
    "outDir": "types"
  }
}

作爲後端庫

package.json中指定main字段。

編譯結果需要在nodejs環境中運行,輸出commonjs格式模塊。

爲了兼容最新與將來,同時也要輸出esmodule格式模塊。

相關配置:

{
  "main": "lib/index.js",
  "module": "module/index.js",
  "jsnext:main": "module/index.js"
}

modulejsnext:main都是指esmodule格式,只是爲了兼容某些特殊環境的別名。可能還有其他別名單我暫時就見過這倆。

其中module中的文件推薦使用特定的後綴名,例如.esm.js.mjs,但在一些工程相關工具中是否會有未知爲題,不好說。

未來已來,現在大部分前端工程工具都會優先使用module指定的文件,單如果沒有指定module,也會爲了兼容去加載main

作爲前端庫

前端庫其實要求比後端庫更高,爲啥?

因爲現代前端開發環境要求支持所有後端環境,並延伸出前端環境的額外支持。也就是說後端庫要求一般是前端庫要求的子集。

需要擴展的是純前端環境的運行格式,老格式amd已經被淘汰可以不用考慮,現在基本都被umd格式統一。

{
  "main": "lib/index.js",
  "module": "module/index.js",
  "unpkg": "dist/index.js",
  "umd:main": "dist/index.js",
  "jsdelivr": "dist/index.umd.production.min.js"
}

其中unpkgumd:mainjsdelivr都是爲了更廣泛兼容的指向瀏覽器環境運行的同一個目標別名。

通常來說commonjsesmoduleumd都不會將其依賴的其他包包括進去,只是在運行時才加載。

還有一種情況,可能只有我自己用到過,就是發佈包中有些東西與外部環境有衝突,因此除了這些通用模式之外我又加了一個independent(取名叫standalong也比較合適)格式,將這個包的所有依賴都封裝進去,可以不依賴外部環境獨立使用。

例如mobx-value的獨立運行文件。

mobx-value independent

注意瀏覽器環境輸出的都是優化後的.production.min格式,也必須同時輸出.development後綴的開發模式,爲了方便使用者調試方便。

因爲最大的使用者,往往就是我們自己,不要連自己都糊弄了事~

作爲命令行工具

多配置兼容

命令行工具一般需要很多參數,例如tsc,當參數過多時沒人願意每次都輸入長長的參數,因此需要配置文件的支持。

那麼選哪種配置格式呢?

此時cosmiconfig隆重登場!以一句名言形容,小孩子才做選擇,成年人全都要!

兼容各種配置,各種位置,詳情參見其api

還有一點,如果需要讀取一些周邊的json配置,不要用原生的JSON.parse,很多json是帶註釋的或者編寫不規範,用json5讀取兼容好。

還有一個精簡版:lilconfig,功能差不多,我下次打算試試。

配置文件類型校驗

剛入門typescript時,我嘗試用typescript作爲配置文件,然後在運行時利用類型機制達到校驗配置的目的。

但這樣會丟失很多靈活性,限制死了配置文件的來源與格式,並由於庫的typescript環境與應用所在的typescript環境不一致,也導致了很多工程問題(對我說的就是ts-gear)。

後來發現通過註釋文檔的方式,js文件中也同樣可以校驗類型,而且js文件對運行時更友好。

例如webpack.config.js這樣配置

/**
 * @type {import('webpack').Configuration}
 * */
const config = {...}
export default config

配置文件運行時校驗

我們的程序要讀配置,但配置是使用者提供的,誰知道用戶會寫些什麼,即使有上面那步提到的類型校驗把關,也會有很多邊界問題類型根本管不了。

因此,運行時配置數據校驗就是必備環節。

不光是校驗不通過時終止運行,還必須給出一個合理且精準的錯誤提示。

推薦一個協議、兩個校驗工具與一個漂亮的格式化提示工具。

協議是json schema,校驗工具爲joiajv,提示輸出工具爲chalk

指定可運行文件

package.json中指定bin

{
  "bin": "bin/run.js"
}

對於大部分js腳本,都要在運行文件頭部指定運行環境。

#! /usr/bin/env node

然後別忘了在發佈前添加可執行屬性,務必整合在自動化發佈腳本中。

chmod +x bin/run.js

可調用api

例如babel,我們不光能使用@babel/cli在命令行使用,也可以在自己的程序裏import babel from 'babel'來調用其api

一個命令行工具通常也是一個第三方庫,方便集成到調用者自身的腳本與環境中。

其他特定環境

例如針對react-native,這個我就見過,沒實際用過。

{
  "react-native": "dist/index.esm.js"
}

最後不論什麼格式,都記得輸出配套sourcemap.map文件。

健壯性🏋

指定運行環境:engine與os

尤其對於命令行工具,這倆點很重要,不然很容易就換個人換個電腦就莫名報錯。

{
  "engine": "node>=14",
  "os": ["linux", "darwin"]
}

有否配套測試用例

  • 有可運行的配套測試用例。
  • README.md上有可見的測試覆蓋率統計,讓人可以放心使用。

測試用例放在哪?

最初我習慣按照jest推薦的模式,將所有測試用例放在__tests__文件夾內。

最近兩年看了好多別的語言的單測用例,我現在更傾向於將測試文件與源文件放在一起。因爲測試用例,就是源代碼的一部分!

比如以下這種目錄結構

src/setter.ts
src/setter.test.ts

測試運行時機

npm prepublishOnly的鉤子一定要加上運行測試用例。

有餘力的情況,可以再配置個額外的流水線,github上有好多免費的配套流水線,自己折騰折騰。

代碼校驗配套

項目必須有一個較好的文檔規則校驗流程,大多數情況我使用eslint,然後配上airbnbprettier的校驗規則。

校驗有兩個重要作用,一個是真的能解決很多隱性bug,另一個是代碼漂亮,之後看你項目源碼的人也會覺得舒服,關鍵是面試時也能拿的出手。

如果有面試者給我看自己的開源作品,如果代碼風格都不行,立即就判定不行,也不用再看什麼邏輯能力了,招進來也是挖坑。

好的代碼風格必須依賴校驗工具,最好把校驗流程也集成到發佈的鉤子上。

推廣性🤹

文檔

使用.markdownlint配置規範自己的markdown文檔,否則很容易寫飛了。

要不人家一看文檔,項目質量很容易就露餡了不是🤭

配套展示用例

  • 一個方法是在項目中自帶一個可運行的樣例,讓人clone之後運行指定命令即可查看樣例。
  • 更好一些,部署一個可以在線查看的例子,並在主文檔上附上直達鏈接。
  • 更進一步,項目增大之後,需要說明的地方越來越多,一個README已經太長。使用docusaurus等類似的工具部署一個獨立的文檔站點。

有否自動化版本管理

Why?因爲版本號與兼容性是強相關的,具體參考semver規範。

  • 使用husky/yorkie等規範提交日誌。
  • 使用standard-version等自動生成CHANGELOG並根據規則自動提升版本號。

最後留個作業

  • 你有什麼npm發佈時的關鍵經驗這裏沒提到的,幫我補充下🤝
  • 當我們再一次運行npm publish,腦編譯一下,想想這期間都發生了些什麼,還少些什麼?

作者:京東零售 王凡

內容來源:京東雲開發者社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章