npm package.json scripts

weixin_34146805發表於2018-09-07

參考
知乎 王仕軍 關於 npm script 我寫了本掘金小冊,還配了視訊
《使用 npm script 打造超溜前端工作流》
阮一峰 npm scripts 使用指南

一、前言

網際網路大潮和前端社群的蓬勃發展讓現代前端專案的複雜性比 5 年前翻了好多倍,前端工作流中也出現了越來越多工程化的環節,比如程式碼風格檢查、自動化測試、自動化構建、自動化部署、服務監控、依賴管理等。大多數前端工程師的工作流可能都離不開 gulp、grunt、webpack 這樣的重量級構建工具,而是否能熟練運用這些工具將重複任務自動化也是工程師素質的重要體現,我本人也是這些自動化工具的忠實粉絲,因為它們確實能幫我解決問題。但幾番折騰之後,你可能已經像我一樣感受到明顯的痛點:比如對外掛依賴嚴重(開發者的自由度受限),外掛和底層工具文件脫節,除錯變的更復雜等,在這點上,我們並不孤獨,社群已經有人對上面的問題作出總結並寫了文章:我為何放棄Gulp與Grunt,轉投npm scripts(上)我為何放棄Gulp與Grunt,轉投npm scripts(中)我為何放棄Gulp與Grunt,轉投npm scripts(下)

就我自己的親身經歷,我曾接手維護過使用了 39 個 gulp 外掛的專案,因為專案起步較早,部分外掛所依賴的基礎工具版本都比較老,當這些外掛所依賴的基礎工具升級之後,gulp 外掛本身並沒有更新的那麼快,我不得不 fork 原倉庫去維護內部的版本,而當 gulp 釋出了新版本之後,升級外掛更是一場艱苦的持久戰。冷靜思考下來,上面這種複雜性其實並沒有必要,在軟體工程裡面有個重要的原則,就是簡單性,越是簡單的東西越是可靠,從概率論的角度,任何系統環節越多穩定性越差。

相比而言,直接使用 npm 內建的 script 機制已經被無數開發者證明是更好的選擇,它能減輕甚至消除上面的痛點:你可以直接使用海量的 npm 包來完成你的任務、不需要在外掛文件和基礎工具文件間來回切換,最重要的點,不使用 grunt 之類的構建工具能讓你的技術棧相對更簡單,而我在做技術選擇是遵循的基本原則是簡單化,簡單才有可能容易讓別人上手。

二、

1.npm run xxx(npm run-script xxx)流程
使用npm init得到預設的package.json裡有一個scripts標籤:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },

在終端中執行 npm run test,能看到 Error: no test specified 的輸出。npm run test 可以簡寫為 npm test,或更簡單的 npm t,得到的結果是幾乎相同的。npm test 顧名思義,就是執行專案測試,實際用法在實戰環節會有介紹。和 test 類似,start 也是 npm 內建支援的命令,但是需要先在 scripts 欄位中宣告該指令碼的實際內容,如果沒宣告就執行 npm start,會直接報錯。那麼,npm 是如何管理和執行各種 scripts 的呢?作為 npm 內建的核心功能之一,npm run 實際上是 npm run-script 命令的簡寫。當我們執行 npm run xxx 時,基本步驟如下:

  • 從 package.json 檔案中讀取 scripts 物件裡面的全部配置;
  • 以傳給 npm run 的第一個引數作為鍵,本例中為 xxx,在 scripts 物件裡面獲取對應的值作為接下來要執行的命令,如果沒找到直接報錯;
  • 在系統預設的 shell 中執行上述命令,系統預設 shell 通常是 bash,windows 環境下可能略有不同,稍後再講。

注意,上面這是簡化的流程,更復雜的鉤子機制後面章節單獨介紹。

2.指定 script 之前會把 node_modules/.bin 加到環境變數 $PATH 的前面
舉例來說,如果 package.json 檔案內容如下:

{
  "name": "hello-npm-script",
  "devDependencies": {
    "eslint": "latest"
  },
  "scripts": {
    "eslint": "eslint **.js"
  }
}

如果不帶任何引數執行 npm run,它會列出可執行的所有命令,比如下面這樣:

Available scripts in the myproject package:
  eslint
    eslint **.js

如果執行 npm run eslint,npm 會在 shell 中執行 eslint **.js。有沒有好奇過上面的 eslint 命令是從哪裡來的?其實,npm 在執行指定 script 之前會把 node_modules/.bin 加到環境變數 $PATH 的前面,執行結束後,再將PATH變數恢復原樣。這意味著任何內含可執行檔案的 npm 依賴都可以在 npm script 中直接呼叫,換句話說,你不需要在 npm script 中加上可執行檔案的完整路徑,比如 ./node_modules/.bin/eslint **.js。

3.萬用字元
由於 npm 指令碼就是 Shell 指令碼,因為可以使用 Shell 萬用字元。

"lint": "jshint *.js"
"lint": "jshint **/*.js"

上面程式碼中,表示任意檔名,*表示任意一層子目錄。如果要將萬用字元傳入原始命令,防止被 Shell 轉義,要將星號轉義。

"test": "tap test/\*.js"

4.傳參
向 npm 指令碼傳入引數,要使用--標明。"lint": "jshint **.js"向上面的npm run lint命令傳入引數,必須寫成下面這樣。

$ npm run lint --reporter checkstyle > checkstyle.xml

也可以在package.json裡面再封裝一個命令。

"lint": "jshint **.js",
"lint:checkstyle": "npm run lint -- --reporter checkstyle > checkstyle.xml"

5.執行順序
如果 npm 指令碼里面需要執行多個任務,那麼需要明確它們的執行順序。
如果是並行執行(即同時的平行執行),可以使用&符號。

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

如果是繼發執行(即只有前一個任務成功,才執行下一個任務),可以使用&&符號。

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

這兩個符號是 Bash 的功能。此外,還可以使用 node 的任務管理模組:script-runnernpm-run-allredrun

6.預設值
一般來說,npm 指令碼由使用者提供。但是,npm 對兩個指令碼提供了預設值。也就是說,這兩個指令碼不用定義,就可以直接使用。

"start": "node server.js",
"install": "node-gyp rebuild"

上面程式碼中,npm run start的預設值是node server.js,前提是專案根目錄下有server.js這個指令碼;npm run install的預設值是node-gyp rebuild,前提是專案根目錄下有binding.gyp檔案。

7.鉤子
npm 指令碼有pre和post兩個鉤子。舉例來說,build指令碼命令的鉤子就是prebuild和postbuild。

"prebuild": "echo I run before the build script",
"build": "cross-env NODE_ENV=production webpack",
"postbuild": "echo I run after the build script"

使用者執行npm run build的時候,會自動按照下面的順序執行。

npm run prebuild && npm run build && npm run postbuild

因此,可以在這兩個鉤子裡面,完成一些準備工作和清理工作。下面是一個例子。

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

npm 預設提供下面這些鉤子。

prepublish,postpublish
preinstall,postinstall
preuninstall,postuninstall
preversion,postversion
pretest,posttest
prestop,poststop
prestart,poststart
prerestart,postrestart

自定義的指令碼命令也可以加上pre和post鉤子。比如,myscript這個指令碼命令,也有premyscript和postmyscript鉤子。不過,雙重的pre和post無效,比如prepretest和postposttest是無效的。

npm 提供一個npm_lifecycle_event變數,返回當前正在執行的指令碼名稱,比如pretest、test、posttest等等。所以,可以利用這個變數,在同一個指令碼檔案裡面,為不同的npm scripts命令編寫程式碼。請看下面的例子。

const TARGET = process.env.npm_lifecycle_event;

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

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

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

注意,prepublish這個鉤子不僅會在npm publish命令之前執行,還會在npm install(不帶任何引數)命令之前執行。這種行為很容易讓使用者感到困惑,所以 npm 4 引入了一個新的鉤子prepare,行為等同於prepublish,而從 npm 5 開始,prepublish將只在npm publish命令之前執行。

8.簡寫形式
四個常用的 npm 指令碼有簡寫形式。

npm start是npm run start
npm stop是npm run stop的簡寫
npm test是npm run test的簡寫
npm restart是npm run stop && npm run restart && npm run start的簡寫

npm start、npm stop和npm restart都比較好理解,而npm restart是一個複合命令,實際上會執行三個指令碼命令:stop、restart、start。具體的執行順序如下。

prerestart
prestop
stop
poststop
restart
prestart
start
poststart
postrestart

9.變數
npm 指令碼有一個非常強大的功能,就是可以使用 npm 的內部變數。

首先,通過npm_package_字首,npm 指令碼可以拿到package.json裡面的欄位。比如,下面是一個package.json。

{
  "name": "foo", 
  "version": "1.2.5",
  "scripts": {
    "view": "node view.js"
  }
}

那麼,變數npm_package_name返回foo,變數npm_package_version返回1.2.5。

// view.js
console.log(process.env.npm_package_name); // foo
console.log(process.env.npm_package_version); // 1.2.5

上面程式碼中,我們通過環境變數process.env物件,拿到package.json的欄位值。如果是 Bash 指令碼,可以用$npm_package_name$npm_package_version取到這兩個值。

npm_package_字首也支援巢狀的package.json欄位。

  "repository": {
    "type": "git",
    "url": "xxx"
  },
  scripts: {
    "view": "echo $npm_package_repository_type"
  }

上面程式碼中,repository欄位的type屬性,可以通過npm_package_repository_type取到。

下面是另外一個例子。

"scripts": {
  "install": "foo.js"
}

上面程式碼中,npm_package_scripts_install變數的值等於foo.js。

然後,npm 指令碼還可以通過npm_config_字首,拿到 npm 的配置變數,即npm config get xxx命令返回的值。比如,當前模組的發行標籤,可以通過npm_config_tag取到。

"view": "echo $npm_config_tag",

注意,package.json裡面的config物件,可以被環境變數覆蓋。

{ 
  "name" : "foo",
  "config" : { "port" : "8080" },
  "scripts" : { "start" : "node server.js" }
}

上面程式碼中,npm_package_config_port變數返回的是8080。這個值可以用下面的方法覆蓋。

$ npm config set foo:port 80

最後,env命令可以列出所有環境變數。

"env": "env"

10.常用指令碼示例

// 刪除目錄
"clean": "rimraf dist/*",

// 本地搭建一個 HTTP 服務
"serve": "http-server -p 9090 dist/",

// 開啟瀏覽器
"open:dev": "opener http://localhost:9090",

// 實時重新整理
 "livereload": "live-reload --port 9091 dist/",

// 構建 HTML 檔案
"build:html": "jade index.jade > dist/index.html",

// 只要 CSS 檔案有變動,就重新執行構建
"watch:css": "watch 'npm run build:css' assets/styles/",

// 只要 HTML 檔案有變動,就重新執行構建
"watch:html": "watch 'npm run build:html' assets/html",

// 部署到 Amazon S3
"deploy:prod": "s3-cli sync ./dist/ s3://example-com/prod-site/",

// 構建 favicon
"build:favicon": "node scripts/favicon.js",

相關文章