前端掃盲-之打造一個Node命令行工具
Node 給前端開發帶來了很大的改變,促進了前端開發的自動化,我們可以簡化開發工作,然后利用各種工具包生成生產環境。如運行sass src/sass/main.scss dist/css/main.css即可編譯 Sass 文件。在實際的開發過程中,我們可能會有自己的特定需求,那么我們得學會如何創建一個Node命令行工具。
命令行接口:Cmmand Line Interface,簡稱 CLI,是 Node 提供的一個用于命令行交互的工具,本質是基于 Node 引擎運行的。
在前面的文章 前端掃盲-之打造一個自動化的前端項目 中,給大家留了一個問題,就是如何通過執行一條命令就生成我們需要的項目結構。今天我就帶著大家一步一步解答這個問題。
我們的初步設想是,在指定目錄下執行一個命令(假設為autogo)
autogo demo
就會生成一個目錄名為demo的項目,里面包含有我們所需的基礎文件結構。
開始
1、首先咱們創建一個程序包清單(package.json文件)包含了該命令包的相關信息:
npm init
然后根據提示輸出對應的信息,也可以一路回車,待生成好package.json文件后再作修改。
2、創建一個用于運行命令的腳本bin/autogo.js:
#! /usr/bin/env node console.log("hello")
然后我們執行
node bin/autogo.js
能夠看到輸出了hello,當然這不是我們想要的結果,我們是要直接運行autogo命令。
3、告訴 npm 你的命令腳本文件是哪一個,這里我們需要給package.json添加一個bin字段:
{ ... "bin": { "autogo": "./bin/autogo.js" } ... }
這里我們指定autogo命令的執行文件為./bin/autogo.js。
4、啟用命令行:
npm link
這里我們通過npm link在本地安裝了這個包用于測試,然后就可以通過
autogo
來運行命令了。
可能遇到的問題
1、有的同學可能在執行autogo命令后會報下面的錯誤:
-bash: /usr/local/bin/autogo: /usr/local/bin/node^M: bad interpreter: No such file or directory
之所以出現這個錯誤是因為bin/autogo.js文件是在 windows 下做的編輯,windows 下默認的換行是\n\r,而 linux 下默認的換行是\n,所以文件后的\r在 linux 下是不會別識別的,顯示成了^M。
要解決這個問題的辦法就是改變文件的編碼,這里我們需要用到 dos2unix 這個包。
首先安裝
sudo apt-get install dos2unix
然后
sudo dos2unix bin/autogo.js
問題就解決了。
2、還有的同學可能會遇到下面這個報錯
: No such file or directory
報這個錯是因為#! /usr/bin/env node沒能識別出你的node的路徑,需要將你的 node 安裝路徑(如/usr/local/bin/)加入到系統的 PATH 中。
其實你可以在測試環境中將這個標識換成#!/usr/local/bin/node,再運行就沒問題了。但是我們之所以用#! /usr/bin/env node是因為這可以動態檢測出不同用戶各自的 node 路徑,而不是寫死的,畢竟不是所有用戶的 node 命令都是在/usr/local/bin/下。
到此,一個本地的 npm 命令行工具就已經成功運行了,(可參見 官方文檔 )接下來我們就來完善具體的功能。
創建項目結構
咱們需要的項目結構大致如下,包含了所需的文件夾和文件( 詳見 )。
要創建上面的結構,我們可以通過程序來創建么個文件和文件夾,但是對于這么多文件,而且每個文件里或許還有更多內容,所以我們應該用一個更簡便的方法。
實際上我們可以先創建一個完整的結構,然后再在執行命令時,通過程序把這些文件和文件夾整個復制到目標項目文件夾中去,最后再對某些文件做一些修改即可。
按照這個思路,我們根據上面的結構,將這些文件和文件夾創建到structure下,然后咱們創建一個生成結構的方法lib/generateStructure.js(這里咱們將功能模塊放在了lib/目錄下)
var Promise = require("bluebird"), fs = Promise.promisifyAll(require('fs-extra')); function generateStructure(project){ return fs.copyAsync('structure', project,{clobber: true}) .then(function(err){ if (err) return console.error(err) }) } module.exports = generateStructure;
上面的代碼就是通過fs-extra這個包( 查看文檔 ) 將structure目錄下的內容復制到了project參數的目標文件夾中。fs-extra是對fs包的一個擴展,方便我們對文件的 操作。
這里咱們用到了bluebird( 查看文檔 ),這是一個實現 Promise 的庫,因為這里牽涉到了對文件的操作,所以會有異步方法,而 Promise 就是專門解決這些異步操作嵌套回調的,能將其扁平化。
自然,我們應該安裝這兩個包:
npm install bluebird --save npm install fs-extra --save
這里加上--save參數是為了在安裝后就自動將該依賴加入到package.json中。然后咱們改造一下bin/autogo.js
#!/usr/bin/env node var gs = require('../lib/generateStructure'); gs("demo");
然后執行
autogo
可以看到當前目錄下生成了一個demo文件夾,里面包含了和structure相同的文件結構。
我們的目標已經初步達成了,接下來我們就來細化該命令。
命名參數
上面的命令中,我們執行autogo時,是生成了一個固定的demo項目,實際上這個名字是不能寫死的,而是應該通過命令中的參數傳進去。像下面這樣:
autogo demo
因此,我們得在bin/autogo.js中去接收參數了。為了方便起見,我們這里直接使用一個專門用于處理命令行工具的包 commander ( 文檔 )。
同樣,首先安裝
$ npm install commander --save
然后改造bin/autogo.js為:
#!/usr/bin/env node var program = require('commander'), gs = require('../lib/generateStructure'); program .version(require('../package.json').version) .usage('[options] [project name]') .parse(process.argv); var pname = program.args[0] gs(pname);
這里的.version()意思是返回該命令包的版本號,即運行
autogo --version //- 返回1.0.0
會返回package.json中定義的版本號。
.usage()顯示基本使用方法 執行
autogo --help
會輸出:
Usage: autogo [options] [project name] Options: -h, --help output usage information -V, --version output the version number
可以看到 Commander 幫我們做好了用法(Usage) 信息,以及兩個參數(Options)-h, --help和-V, --version。
.parse(process.argv);是將接收到的參數加入 Commander 的處理管道。
program.args是獲取到命令后的參數,注意這里是一個數組
autogo //- 返回 [] autogo demo //-返回 ['demo'] autogo demo hello //-返回 ['demo','hello']
這里咱們取第一個參數作為項目名,然后調用
var pname = program.args[0] gs(pname);
現在我們執行:
autogo demo2
就可以看到新的項目demo2生成了,看上去我們已經完成工作了,只要運行autogo <項目名>就可以生成一個新的項目結構,里面包含了處理 Sass、coffee、jade 的 gulp 構建工具。
如果我們直接運行autogo是會報錯的,因為沒有傳入項目名,實際上我們在運行一個命令而不傳入任何參數時,可以直接返回幫助信息:
... var pname = program.args[0] if (!pname) program.help(); ...
上面我們判斷是否存在參數,如果不存在就調用program.help()方法,這是 commander 為我們提供的顯示幫助信息的方法,可以直接調用。
那有的同學要說了,我不想用jade,就喜歡寫原生的 HTML,很明顯我們做了多余的事,而且整個結構就不那么合理了,我們需要的是一個干凈的項目結構。
這個時候我們就需要把與jade相關的文件都刪掉(這里不是刪 structure 目錄下的文件,而是新項目下的指定文件)。與jade有關的文件有:
- /structure/views/下的index.jade和layouts/layout.jade
- /structure/gulpfile.js中的templates任務代碼
因此,咱們得把上面這些文件和代碼干掉。
移除指定模塊
首先,咱們創建一個lib/jadeWithout.js用來移除 jade:
var Promise = require("bluebird"), fs = Promise.promisifyAll(require('fs-extra')), del = require('../lib/delFile'); var files = ['/views/layouts/layout.jade','/views/index.jade']; function jadeWithout(project){ return Promise.all([del(project,files)]) .then(function(){ return console.log('remove jade success'); }) } module.exports = jadeWithout;
這里咱們將指定的files數組中的文件都刪除了,這里我用了一個公共的刪除文件模塊/lib/delFile.js:
var Promise = require("bluebird"), fs = Promise.promisifyAll(require('fs-extra')); function del(project,files){ return files.map(function(item){ return fs.removeAsync(project + item) }) } function delFile(project,files){ return Promise.all([del(project,files)]) } module.exports = delFile;
因為我們這里不光有 jade ,還有 sass 和 coffee 可以被移除,所以我們創建一個公共入口withoutFile.js:
var Promise = require("bluebird"); function deal(project,outs){ return outs.map(function(item){ var action = require('../lib/'+item+'Without'); return action(project) }) } function withoutFile(project,outs){ return Promise.all([deal(project,outs)]) } module.exports = withoutFile;
這里我們需要傳入一個要移除的列表(如['sass','jade']),然后對每個模塊進行刪除。
最后,我們將withoutFile引入到bin/autogo.js中:
... var gs = require('../lib/generateStructure'), wf = require('../lib/withoutFile'); ... Promise.all([gs(pname)]) .then(function(){ return wf(pname,["jade",'sass']) })
然后我們再次執行
autogo demo
可看到控制臺依次輸出了
generate project success remove jade success remove sass success
而且目標項目中相關文件已經被刪除了。
這里咱們是wf(pname,["jade",'sass'])寫死了 outs 參數作為測試,實際上是要再傳入一個數組,那么這個數組從哪兒來呢?很明顯,得從命令行參數中獲取。
我們希望的是這樣:
autogo --without jade demo
option
commander 為我們提供了一個option管道來配置命令參數,修改bin/autogo.js:
... program .version(require('../package.json').version) .usage('[options] [project name]') .option('-W, --without <str | array>', 'generate project without some models(value can be `sass`、`coffee`、`jade`)') .parse(process.argv); ...
這里咱們添加了option,其格式為.option('-<大寫標識>, --<小寫全稱> <可取參數類型>', '數功能描述')
接著處理without參數:
... var outs = program.without ? [program.without] : [] Promise.all([gs(pname)]) .then(function(){ return wf(pname,outs) }) ...
然后咱們再運行
autogo --without jade demo
可以看到這里只移除了 jade 模塊,那如果我想移除多個呢?是不是可以這樣:
autogo --without [jade,sass] demo
注意,這樣是會報錯的,因為獲取到的program.without是一個字符串'[jade,sass]'而不是數組,所以咱們可以這樣:
autogo --without jade,sass demo
program.without則為'jade,sass'然后再
program.without.split(',')
既可以獲取到一個數組了,因此咱們的代碼就變成了:
... var outs = program.without ? program.without.split(',') : [] Promise.all([gs(pname)]) .then(function(){ return wf(pname,outs) }) ...
這下我們就可以這樣來運行了:
autogo demo --without sass,jade
發布
到目前為止,我們開發的 autogo 還是在本地的,現在就該將其發布到 npm 上了。
1、首先咱們得 注冊一個賬號 。
2、回到項目中,執行
npm login
輸入用戶名、密碼和郵箱便可將本地機器與 npm 連接起來了。
3、執行
npm publish
然后回到你的 npm 個人主頁,就可以看到我們發布成功了 https://www.npmjs.com/package/autogo
從包的路徑規則來看,是沒有包含用戶名的,由此可知,同名的包是不會被允許的,所以大家在跟著做的時候要給項目取一個不同的名字。
然后咱們來測試一下剛剛發布的包
首先刪除本地開發做的 autogo 鏈接
sudo npm unlink
然后
npm install autogo -g
注意這里需要帶上-g參數,因為命令行是應該安裝在全局環境中。安裝成功后,我們切換到另外一個目錄下,執行:
autogo demo
然而結果并非我們想象的那樣:
Unhandled rejection Error: ENOENT, lstat 'structure' at Error (native)
意思是找不到structure,這是怎么回事呢?
實際上當我們執行npm install autogo -g的時候,實際上是將命令包安裝在了/usr/local/lib/node_modules/autogo下面,所以在執行命令的目錄下是找不到structure文件夾的。
那該怎么辦呢?我們能想到的就是,得在程序中去獲取這個包安裝的實際路徑。
幸運的是 Node 給我們提供了__dirname這個變量用于獲取當前執行文件的路徑。 我們在lib/generateStructure.js下console.log(__dirname)會輸出/usr/local/lib/node_modules/autogo/lib,然后我們把后面的lib去掉就是根目錄了:
var root = __dirname.replace(/autogo\/lib/,'autogo/') function generateStructure(project,outs){ return fs.copyAsync(root + 'structure', project) .then(function(err){ return err ? console.error(err) : console.log('generate project success'); }) } ...
修改后,我們按照下面的方式更新,重新安裝,然后
autogo demo cd demo npm install gulp watch
OK 一個新的項目誕生了,準備開發吧...
更新
首先修改package.json配置文件中的version字段,比如這里我從0.1.0改成0.1.1(只能大于當前版本),然后再次
npm publish
即可成功發布新版本。
想將該項目從 npm 中移除嗎?執行 :
npm unpublish autogo --force