爲什麼寫這篇文章

很多 Node.js 開發者,都知道有 package.json 這個文件,也多少都瞭解一些 npm 知識,但是可能沒有系統的學習過,這部分的知識對於開發一個   cli 工具,發佈自己的 npm 包都很常用,開發中也會經常用到 npm script 內容,系統的學習一下確實會有所幫助,上面三個場景如果你都用不到,配置上節約時間,知其所以然也是有必要的!

本文你能學到哪些?

Package.json 知識全覽圖

文章比較長,希望耐心看完!

寫文本的目標:希望再遇到 package.json , npm 相關的問題,不用再去搜索,一篇文章全覆蓋,不一定全掌握,知道我這裏都有,需要時候能直接來查就好了,也爲了方便自己後面查閱。(本文主要講的部分是 npm script ,個人認爲很常用,一些文章都沒有講)

好了,開始我們的正文學習吧。

package.json 如何產生的

npm init

npm init 命令用來初始化一個簡單的 package.json 文件,執行該命令後終端會依次詢問 name , version , description 等字段。

npm init 默認執行行爲

我們在執行 npm init 的時候,會有一個初始化 pacakge.json 過程,然後一路回車,其實可以直接使用 npm init --yes 在命令後追加 --yes 參數即可,其作用與一路回車相同,這樣生成的文件中就包含 package.json 文件

自定義 npm init 行爲

npm init 命令的原理並不複雜,調用 shell 腳本,輸出一個初始化的 package.json 文件。所以相應地,自定義 npm init 命令的實現方式也很簡單,在電腦 npmStudy 目錄創建一個 .npm-init.js 即可,該文件的 module.exports 即爲 package.json 配置內容,需要獲取用戶輸入時候,使用 prompt() 方法即可。例如編寫這樣的 ~/.npm-init.js

const desc = prompt('description?', 'A new package...')
const bar = prompt('bar?', '')
const count = prompt('count?', '42')

module.exports = {
  key: 'value',
  foo: {
    bar: bar,
    count: count
  },
  name: prompt('name?', process.cwd().split('/').pop()),
  version: prompt('version?', '0.1.0'),
  description: desc,
  main: 'index.js',
}

然後在 /npmStudy 目錄下執行 npm init 會出現下圖中對一系列操作

然後生成 package.json 文件。到這裏一個 npm init 簡單自定義過程結束,知道了兩種生成 pacakge.json 的方式

package.json 中的常規屬性

對於常規屬性都知道的可以忽略,繼續往下看 npm script 主要想講的部分。

npm 中的依賴包

這裏只說我們常用的兩個依賴包 dependenicesdevDependenices ,其它的一些依賴包只有作爲包的發佈者纔會用到,需要的小夥伴自行查看文檔。

dependenices

通過命令 npm install/i packageName -S/--save 把包裝在此依賴項裏。如果沒有指定版本,直接寫一個包的名字,則安裝當前 npm 倉庫中這個包的最新版本。如果要指定版本的,可以把版本號寫在包名後面,比如 npm i [email protected] -S

npm 5.x 開始,可以不用手動添加 -S/--save 指令,直接執行 npm i packageName 把依賴包添加到 dependencies 中去。

 "dependencies": {
      "lodash": "^4.17.13",
      "moment": "^2.24.0",
 }

devDependenices

有一些包有可能你只是在開發環境中用到,例如你用於檢測代碼規範的 eslint ,用於進行測試的 jest ,用戶使用你的包時即使不安裝這些依賴也可以正常運行,反而安裝他們會耗費更多的時間和資源,所以你可以把這些依賴添加到 devDependencies 中,這些依賴照樣會在你本地進行 npm install 時被安裝和管理,但是不會被安裝到生產環境:

 "devDependencies": {
      "jest": "^24.3.1",
      "eslint": "^6.1.0",
 }

二者簡單對比

  • devDependencies 主要是存放用於本地開發的
  • dependencies 會在我們開發的時候帶到線上
  • -D
    devDependencies
    -S
    dependencies
    
  • --save-dev 也會添加到 devDependencies
  • --save 會添加到 dependencies
  • npm 5.x 開始,如果什麼參數都不帶,那麼默認添加到 dependencies
  # 添加到 devDependencies
  npm install -D xxxx
  # 添加到 dependencies
  npm install -S xxxx

bin

 "bin": {
    "vm2": "./bin/vm2"
  },

bin 字段指定了各個內部命令對應的 可執行文件 的位置。如果全局安裝模塊報, npm 會使用符號鏈接把可執行文件鏈接到 /usr/local/bin ,如果項目中安裝,會鏈接到 ./node_modules/.bin/

上面的這種當你的包安裝到全局時:npm 會在 /usr/local/bin 下創建一個以 vm2 爲名字的軟鏈接,指向全局安裝下來的 vm2 包下面的 "./bin/index.js" 。這時你在命令行執行 vm2 則會調用鏈接到的這個 js 文件。

main

一個常用的npm包

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

main 屬性指定程序的主入口文件,其他項目在引用這個 npm 包時,實際上引入的是 lib/index 中暴露出去的模塊。

npm script(本文重點)

npm script 工作中應用到的一個場景,決定看一下原理。

什麼是 npm script 腳本?

在生成的 package.json 文件中,有一個 scripts 對象,在這個對象中, npm 允許使用 scripts 字段定義腳本命令。

"scripts": {
    "test": "test.js"
    "build": "tsc",
  },

scripts 對象中每一個屬性,對應一段腳本。比如, test 命令對應的腳本是 node test.js

命令行下使用 npm run 命令,就可以執行這段腳本。

查看當前項目的所有 npm 腳本命令 ,可以使用不帶任何參數的 npm run 命令。

原理

我們每次在運行 scripts 中的一個屬性時候( npm run ),**實際系統都會自動新建一個shell(一般是Bash),在這個shell裏面執行指定的腳本命令。因此 凡是能在 shell 中允許的腳本,都可以寫在npm scripts中。

特別的點, npm run 新建的 shell ,會在當前目錄的 node_modules/.bin 子目錄加入到 PATH 變量,執行結束後,再將 PATH 變量恢復原樣。也就是說,當前項目目錄 node——modules/.bin 子目錄中所有的腳本,都可以直接用腳本名稱調用,不需要增加路徑.(簡單總結:通過 npm 啓動的腳本,會默認把 node_modules/.bin 加到 PATH 環境變量中。)

例子

當前項目的依賴裏面有 Mocha,只要直接寫 mocha test 就可以了。

"test": "mocha test"

而不用寫成下面這樣。

"test": "./node_modules/.bin/mocha test"

然後我們就可以直接執行 npm run test 了。 npm 腳本的退出碼,也遵守 Shell 腳本規則。如果退出碼不是0,npm 就認爲這個腳本執行失敗。

這裏有的小夥伴可能會有疑問, node_modules目錄下的.bin文件是哪裏來的 ?我之前也有這樣的疑問,打開了一個 .bin/tsc ,裏面的內容是這樣的

#!/usr/bin/env node
require('../lib/tsc.js')

npm install 安裝的某個模塊,如果模塊在 package.json 中配置了 bin 屬性,在安裝時候會自動軟鏈接到 node_modules/.bin 中,舉個例子:如 mocha 源碼 配置了:

{
    "name":"mocha",
    "bin":{
        "mocha":"./bin/mocha"
    }
}

腳本默認值

正常情況下, npm 腳本是用戶自己定義。但是 npm 本身對兩個腳本提供了默認值,這兩個腳本不用在 script 屬性中定義,可以直接使用

"start": "node server.js"
"install": "node-gyp rebuild"
  • npm run start
    node server.js
    server.js
    
  • npm run install
    node-gyp rebuild
    binding.gyp
    

擴展小知識,本文不重點說, node-gyp 是什麼, binding.gyp 文件是什麼?GYP 是一種構建自動化工具。

  • node
    gyp
    node-gyp
    C++
    C++
    node-gyp
    Node
    
  • Node.js
    C++
    binging.gyp
    .gyp
    Python
    鍵-值對
    

看一段簡單的 .gyp 文件,應該好理解一些。

{
    "targets": [
        {
            "target_name": "nodecat",
            "sources": [
                "src/nodecat.cc",
            ],
            "include_dirs": [
                "include"
            ],
            "libraries": [
                "-lcatclient"
            ]
        }
    ]
}

想了解更多詳細的可以看下面的文檔。

binging.gyp 參數說明書/文檔/指南(國外的一篇文檔抄錄,棒!)binging.gyp 參數說明書/文檔/指南

關於 node-gypbinding.gyp 後面會單獨寫一篇文章,這裏先簡單介紹,小夥伴們瞭解下。

鉤子(生命週期)

好多語言或者框架我們學的時候都會考慮到生命週期,其實 package.json 中的 script 也是有生命週期的。 npm 腳本有兩個鉤子, prepost ,當我們執行 start 腳本時候, start 的鉤子就是 prestartpoststart

當我們執行 npm run start 的時候,npm 會自動按照下面的順序執行

npm run prestart && npm run start && npm run poststart

那這個鉤子有什麼用呢,在實際開發中,我們可以做一些準備或者清理工作,下面是個例子(引用的阮一峯老師文章中的例子)

"clean": "rimraf ./dist && mkdir dist",
"prebuild": "npm run clean",
"build": "cross-env NODE_ENV=production webpack"

鉤子好用,但是不可亂用,舉個開發過程中遇到的坑,有一次想設置運行時的環境變量,當時想優雅一點,就在 prestart 裏面設置了一個環境變量,但是在項目 start 的時候,無法拿到設置的環境變量,因爲 script 的屬性運行的時候都會新啓動一個 shell ,所以在 prestart 中設置的環境變量只對應了那個 shell 的運行時。

env 環境變量

我們在執行 npm run 腳本時候, npm 會設置一些特殊的env環境變量。其中 package.json 中的所有字段,都會被設置爲以 npm_package_ 開頭的環境變量。看個簡單的例子

{
  "name": "npm-demo",
  "version": "1.0.0",
  "script": {
    "build": "webpack --mode=production"
  },
  "files": ["src"]
}

可以得到 npm_package_name、npm_package_version、npm_package_script_build、npm_package_files_0 等變量。注意上面 package.json 中對象和數組中每個字段都會有對應的環境變量。

同時, npm 相關的所有配置也會被設置爲以 npm_config_ 開頭的環境變量。此外,還會設置一個比較特殊的環境變量 npm_lifecycle_event ,表示正在運行的腳本名稱。比如執行 npm run serve 的時候, process.env.npm_lifecycle_event 值爲 serve ,通過判斷這個變量,可以將一個腳本使用在不同的 npm scripts 中。這裏還要提一下上面說的鉤子, npm_lifecycle_event 可以和鉤子配合使用,利用這個變量,在同一個腳本文件裏面,爲不同的 npm scripts 命令編寫代碼。請看下面的例子。

const TARGET = process.env.npm_lifecycle_event;

if (TARGET === 'service') {
  console.log(`Running the service task!`);
}

if (TARGET === 'preservice') {
  console.log(`Running the preservice task!`);
}

if (TARGET === 'postservice') {
  console.log(`Running the postservice task!`);
}

強調:這些環境變量只能在 npm run 的腳本執行環境內拿到,正常執行的 node 腳本是獲取不到的。所以,不能直接通過 env $npm_package_name 的形式訪問,但可以在 scripts 中定義腳本 "scripts": {"bundle": "echo $npm_package_name"} 來訪問。

環境變量常用小技巧

  1. env 命令可以列出所有環境變量

npm run env
  1. 在shell腳本中輸出環境變量

echo PATH
  1. 在 shell 腳本設置環境變量

echo PATH = /usr/local/lib

有的時候我們需要設置的環境變量是相對項目的 再補充一個shell腳本中設置環境變量時候如何拼接相對路徑

echo PATH = ${pwd}/lib/include  //使用${},也可以直接使用雙引號

腳本傳入參數

說到腳本傳入參數,需要再次提到前面說的 pacakge.json 中的 bin 字段, bin 字段指定了各個內部命令對應的可執行文件的位置。前面已經說了 bin 文件的產生,有了 bin 字段,在安裝這個模塊的時候, node_modules 下面的 .bin/文件夾 下會有對應模塊的文件,和模塊中的文件相同,然後我們就可以通過調用這個文件腳本中的方法傳入參數了。

在我的 node_module 中找到一個簡單 .bin/文件 下的腳本,大家感受一下。

#!/usr/bin/env node
'use strict';
var pkg = require('./package.json');
var osName = require('./');
var argv = process.argv;

function help() {
 console.log([
  '',
  '  ' + pkg.description,
  '',
  '  Example',
  '    os-name',
  '    OS X Mavericks'
 ].join('\n'));
}

if (argv.indexOf('--help') !== -1) {
 help();
 return;
}

if (argv.indexOf('--version') !== -1) {
 console.log(pkg.version);
 return;
}

console.log(osName());

node 處理 scripts 中的參數,除了 屬性後面的第一個命令,以空格分割的任何字符串(除特別shell語法)都是參數 ,並且都能通過 process.argv 屬性訪問。

process.argv 屬性返回一個數組,數組包含了啓動 node 進程時的命令行參數。第一個元素爲啓動 node 進程的可執行文件的絕對路徑名 process.execPath ,第二個元素爲當前執行的 jacascript 文件路徑。 剩餘的元素爲其他命令行參數。

如下 script 例子

"scripts":{
  "serve": "vue-cli-service serve --mode=dev --mobile -config build/example.js"
}

當我們執行 npm run server 命令的時候, process.argv 的具體內容爲:

[ '/usr/local/Cellar/node/12.14.1/bin/node',
  '/Users/mac/Vue-projects/hao-cli/node_modules/.bin/vue-cli-service',
  'serve',
  '--mode=dev',
  '--mobile',
  '-config',
  'build/example.js']

再列舉幾個傳參可能有的方式

npm run serve --params  // 參數params將轉化成process.env.npm_config_params = true
npm run serve --params=123 // 參數params將轉化成process.env.npm_config_params = 123
npm run serve -params  // 等同於--params參數

npm run serve -- --params  // 將--params參數添加到process.env.argv數組中
npm run serve params  // 將params參數添加到process.env.argv數組中
npm run serve -- params  // 將params參數添加到process.env.argv數組中

對比下 npm install koa2 --save 是不是知道了bin腳本中接收到的 process.env.npm_config_save = true; 我想是這樣的,有興趣的小夥伴去看源碼驗證下。

執行順序

npm 腳本執行多任務分爲兩種情況

  • 並行任務(同時的平行執行),使用&符號

$ npm run script1.js & npm run script2.js
  • 串行任務(前一個任務成功,才執行下一個任務),使用 && 符號

$ npm run script1.js && npm run script2.js

任意腳本

我們配置的腳本命令,如 "start": "node test.js"node test.js 會當做一行代碼傳遞給系統的 shell 去解釋執行。實際使用的 shell 可能會根據系統平臺而不同,類 UNIX 系統裏,如 macOSlinux 中指代的是 /bin/sh , 在 windows 中使用的是 cmd.exe 。原理我們也看了,因爲交給 shell 去解釋執行的,說明配置的腳本可以是任意能夠在 shell 中運行的命令,而不僅僅是 node 腳本或者 js 代碼。如果你的系統裏安裝了 python (或者說 系統變量 PATH 裏能找到 python 命令),你也可以將 scripts 配置爲 "myscript": "python xxx.py"

npm 配置

npm 的配置操作可以幫助我們預先設定好 npm 對項目的行爲動作,也可以讓我們預先定義好一些配置項以供項目中使用。所以瞭解 npm 的配置機制也是很有必要。

npm config

npm cli 提供了 npm config 命令進行 npm 相關配置,通過 npm config ls -l 可查看 npm 的所有配置,包括默認配置。npm 文檔頁爲每個配置項提供了詳細的說明 https://docs.npmjs.com/misc/config . 修改配置的命令爲 npm config set , 我們使用相關的常見重要配置:

  • proxy, https-proxy : 指定 npm 使用的代理
  • registry
    npm
    https://registry.npmjs.org/
    Registry
    
  • package-lock 指定是否默認生成 package-lock 文件,建議保持默認 true
  • save true/false
    npm install
    dependencies
    npm 5
    true
    

刪除指定的配置項命令爲 npm config delete <key> .

這裏最常見的一個操作是 npm 太慢,設置淘寶鏡像

npm config set registry https://registry.npm.taobao.org

恢復使用之前的 npm

npm config set registry https://registry.npmjs.org

env 環境變量

如果 env 環境變量中存在以 npm_config_ 爲前綴的環境變量,則會被識別爲 npm 的配置屬性。比如在 env 環境變量中設置 npm_config_package_lock 變量:

export npm_config_package_lock=false //修改的是內存中的變量,只對當前終端有效

這時候執行 npm installnpm 會從環境變量中讀取到這個配置項,從而不會生成 package-lock.json 文件。

查看某個環境變量:echo $NODE_ENV 刪除某個環境變量:unset NODE_ENV

npmrc 文件

除了使用 CLInpm config 命令顯示更改 npm 配置,還可以通過 npmrc 文件直接修改配置。

這樣的 npmrc 文件優先級由高到低包括:

  • 工程內配置文件: /path/to/my/project/.npmrc
  • 用戶級配置文件: ~/.npmrc
  • 全局配置文件: $PREFIX/etc/npmrc (即npm config get globalconfig 輸出的路徑)
  • npm內置配置文件: /path/to/npm/npmrc

很多時候我們在公司內網需要通過代理才能訪問 npm 源,通過這個機制,我們可以方便地在工程跟目錄創建一個 .npmrc 文件來共享需要在團隊間共享的 npm 運行相關配置。比如如果我們在公司內網環境下需通過代理纔可訪問 registry.npmjs.org 源,或需訪問內網的 registry , 就可以在工作項目下新增 .npmrc 文件並提交代碼庫。

proxy = http://proxy.example.com/
https-proxy = http://proxy.example.com/
registry = http://registry.example.com/

因爲項目級 .npmrc 文件的作用域只在本項目下,所以在非本目錄下,這些配置並不生效。對於使用筆記本工作的開發者,可以很好地隔離公司的工作項目、在家學習研究項目兩種不同的環境。

將這個功能與 ~/.npm-init.js 配置相結合,可以將特定配置的 .npmrc.gitignore , README 之類文件一起做到 npm init 腳手架中,進一步減少手動配置。

npm 包發佈

規範的 npm 模塊目錄

一個 node.js 模塊是基於 CommonJS 模塊化規範實現的,嚴格按照 CommonJS 規範,模塊目錄下除了必須包含包描述文件 package.json 以外,還需要包含以下目錄:

  • bin:存放可執行二進制文件的目錄

  • lib:存放js代碼的目錄

  • doc:存放文檔的目錄

  • test:存放單元測試用例代碼的目錄

如何寫好一個模塊的 README 文件

這裏不單獨寫,推薦一篇不錯的討論

https://www.zhihu.com/question/29100816

如何發佈自己的 npm 包

  1. 先去 npm 註冊個賬號,然後在命令行使用

npm adduser #根據提示輸入用戶名密碼即可
  1. 使用命令發佈你的包

在推送之前,可以通過配置一個 .npmignore 文件來排除一些文件, 防止大量的垃圾文件推送到 npm , 規則上和你用的 .gitignore 是一樣的。 .gitignore 文件也可以充當 .npmignore 文件

npm publish
  1. 發佈成功之後,你就可以像下載安裝其他包一樣使用你自己的開發工具了

npm install koalanpmstudy

關於 npm 包的更新

更新 npm 包也是使用 npm publish 命令發佈,不過必須更改 npm 包的版本號,即 package.json 的 version 字段,否則會報錯,同時我們應該遵 Semver (語義化版本號) 規範,npm 提供了 npm version 給我們升級版本

# 升級補丁版本號
$ npm version patch

# 升級小版本號
$ npm version minor

# 升級大版本號
$ npm version major

本地開發的 npm 包如何調試

在本地開發的模塊包的時候,可以使用 npm link 調試,將模塊鏈接到對應的運行項目中去,方便地對模塊進行調試和測試。具體使用步驟如下

  • 假如我的項目是 koalaNpmStudy ,假如我的 npm 模塊包名稱是 npm-ikoala
  • 進入到 模塊包 npm-ikoala 目錄中,執行 npm link
  • 在自己的項目 koalaNpmStudy 中創建連接執行 npm link npm-ikoala
  • 在自己項目的 node_module 中會看到鏈接過來的模塊包,然後就可以像使用其他的模塊包一樣使用它了。
  • 調試結束後可以使用 npm unlink 取消關聯

npm link 主要做了兩件事:

  1. npm
    node
    /usr/local/lib/node_modules/
    
  2. npm
    bin
    node
    /usr/local/bin/
    

了方便進行探討和交流,我爲大家建立了一個讀者羣,一起學習,一起進步。

:heart:愛心三連擊

1.看到這裏了就點個在看支持下吧,你的 「在看」 是我創作的動力。

2.關注公衆號 達達前端「每天爲您分享原創或精選文章」

3.特殊階段,帶好口罩,做好個人防護。

4.添加微信【xiaoda0423】,拉你進 技術交流羣 一起學習

掃碼關注公衆號,訂閱更多精彩內容。

好文章,我 在看

參考文章

  • https://juejin.im/post/5cb3f1ef5188256d917874ff

  • https://www.ruanyifeng.com/blog/2016/10/npm_scripts.html

  • https://zhuanlan.zhihu.com/p/23493436

  • https://juejin.im/post/5ab3f77df265da2392364341#heading-22

  • https://www.cnblogs.com/nanvann/p/3913880.html

相關文章