造輪子之 npm i -g creatshare-app-init 源碼淺析

以我的小經(jīng)驗(yàn)來看拍柒,軟件萌新寫出來的代碼大多“無法直視”。具體現(xiàn)象包括空格和換行符亂用屈暗、文件夾和變量的命名多使用拼音等拆讯。坐不住的我,便想到了通過 ESLint 配置文件來規(guī)范實(shí)驗(yàn)室的 JavaScript 代碼規(guī)范的 Idea恐锦。

于是巧遇前實(shí)驗(yàn)室畢業(yè)學(xué)長曾經(jīng)發(fā)布的 npm 包——creatshare-project-quick-init往果。安裝好這個包,我們便可以在空文件夾下生成一個項(xiàng)目的基礎(chǔ)骨架一铅。

dist  //發(fā)布目錄陕贮,用于生產(chǎn)環(huán)境
src   //開發(fā)目錄,開發(fā)時(shí)所需資源
|----dist  //測試環(huán)境目錄
|     |----static
|           |----css  //編譯打包后的css資源
|           |----js   //打包壓縮后的js資源
|           |----imgs //測試環(huán)境圖片資源
|----less  //開發(fā)所需less代碼
|----js    //開發(fā)所需js代碼
|    |----lib //庫或框架資源
|----imgs  //開發(fā)所需圖片資源
index.html    //開發(fā)頁面
gulpfile.js
package.json
README.md

What a good idea~!

在學(xué)長的這個包中潘飘,主要構(gòu)建了 gulp 配置肮之,less 和測試文件的骨架。雖然再無更多內(nèi)容卜录,但這份構(gòu)建基礎(chǔ)骨架的靈感還是被我愉快的收走了——學(xué)前端的人很多戈擒,但大多都太缺工程化意識了。于是艰毒,這個靈感成為了不錯突破口筐高。

creatshare-app-init 腳手架孕育而生。

0

通過這篇文章丑瞧,你能了解到:

  • 如何用 NodeJS 編寫命令行工具柑土?
  • 如何發(fā)布自己的 npm 包?
  • 筆者與 creatshare-app-init 的故事绊汹?

在本文中稽屏,或多或少出現(xiàn)過以下關(guān)鍵字,我的解釋是:

  • 輪子:該詞在前端開發(fā)日常用語中西乖,表示一個基于原生代碼實(shí)現(xiàn)狐榔,但并沒有對前端行業(yè)產(chǎn)生積極意義的模塊。雖然它的出現(xiàn)方便了一些人的使用获雕,但更多的加大了我們的學(xué)習(xí)成本薄腻。
  • 項(xiàng)目:該詞在前端領(lǐng)域常指一個服務(wù)于用戶的軟件立項(xiàng)。
  • 模塊:creatshare-app-init 就是一個模塊届案,是開發(fā)前端項(xiàng)目中的一個子集被廓。正如汽車的各個部件一樣,多個模塊合理組裝起來才是一輛汽車萝玷。

1

嘗試解析源碼嫁乘,第一步昆婿,從模塊根目錄下的 package.json 來看。

"dependencies": {
    "commander": "^2.11.0"
},
"devDependencies": {
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "babel-runtime": "^6.26.0",
    "eslint-config-standard": "^10.2.1",
    "eslint-plugin-import": "^2.8.0",
    "eslint-plugin-node": "^5.2.1",
    "eslint-plugin-promise": "^3.6.0",
    "eslint-plugin-standard": "^3.0.1"
}

如上蜓斧,dependencies 聲明了模塊上線時(shí)的依賴仓蛆,devDependencies 聲明了模塊開發(fā)時(shí)的依賴。該模塊在上線時(shí)挎春,即 npm 包被用戶用到時(shí)看疙,只需要 commander 庫。commander 庫是 NodeJS 命令行接口開發(fā)的優(yōu)選解決方案直奋,受啟發(fā)于 Ruby 的 commander能庆。在解析 bin/index.js 源碼時(shí)將詳細(xì)拓展。

"name": "creatshare-app-init",
"version": "2.1.0",
"description": "CreatShare 實(shí)驗(yàn)室前端項(xiàng)目初始化工具",
"bin": {
  "cs": "bin/index.js"
},
"scripts": {
  "compile": "babel src/ -d lib/",
  "prepublish": "npm run compile",
  "eslint": "eslint src bin",
  "test": "echo \"Error: no test specified\" && exit 1"
},

上面一段是 package.json 最開頭的內(nèi)容脚线,字段詳情如下:

  • name 字段:聲明模塊名稱搁胆。特殊注意該字段不允許大寫字母及空格的出現(xiàn),且其與 version 字段形成了 npm 模塊的唯一標(biāo)識符邮绿。
  • version 字段:聲明模塊當(dāng)前版本號渠旁。這里每當(dāng)使用 npm publish 將模塊發(fā)布到 npm 倉庫中時(shí),版本號都需要手動自增船逮。
  • description 字段:對模塊進(jìn)行描述顾腊,同時(shí)有助于被檢索。
  • bin 字段:npm 本身是通過 bin 屬性配置一個或多個可解析到 PATH 路徑下的可執(zhí)行模塊挖胃。模塊若被全局安裝杂靶,則 npm 會為 bin 中配置的文件在 bin 目錄下創(chuàng)建一個軟連接;模塊若被局部安裝酱鸭,軟連接會配置在項(xiàng)目內(nèi)的 ./node_modules/.bin/目錄下伪煤。
  • script 字段:定義模塊的腳本配置。如凛辣,當(dāng)我們在模塊目錄下使用 npm run compile 時(shí),將自動執(zhí)行 babel src/ -d lib/ 命令职烧,進(jìn)行 ECMAScript6 代碼的轉(zhuǎn)譯扁誓。

2

剛剛提到 package.json 配置文件下的 bin 字段聲明了 npm 在生成軟連接時(shí)的配置。這就便是用戶在安裝好這個目錄后蚀之,可以隨時(shí)使用 cs 命令的出處蝗敢。

我們又提到了該模塊在非開發(fā)環(huán)境下只需用到 commander 模塊,這個模塊是 NodeJS 命令行接口開發(fā)的優(yōu)選解決方案足删。

基于這倆點(diǎn)寿谴,我們就從 bin 字段所指向的 bin/index.js 聊起。

#!/usr/bin/env node

var program = require('commander')
var cs = require('../lib/cs')

program
  .allowUnknownOption()
  .version('2.1.1')
  .description('CreatShare 互聯(lián)網(wǎng)實(shí)驗(yàn)室前端 Web App 項(xiàng)目腳手架')
  .option('-e, --enjoy')

program.
  .command('create <dir>')
  .description('創(chuàng)建一個新的 Web App 項(xiàng)目骨架')
  .action(function (rootDir) {
    cs.create(rootDir)
})

program.parse(process.argv)

就這么二十來行失受。因?yàn)槲覀円獙懙哪K是要運(yùn)行在命令行下的讶泰,就需要 #!/usr/bin/env node 語句來告訴系統(tǒng)使用 node 環(huán)境來運(yùn)行我們的文件咏瑟,必不可少。

在引入 commander 并將其賦值給 program 變量后痪署,我們對其使用了如下方法:

  • .allowUnknownOption() 方法:
  • .version() 方法:用于設(shè)置命令程序的版本號码泞。
  • .description() 方法:用于設(shè)置命令的描述±欠福可以綁定在跟命令下余寥,這里是 cs 命令;或綁定在子命令下悯森,如 cs create <dir> 命令宋舷。
  • .option() 方法:定義命令的具體選項(xiàng)。
  • .command() 方法:定義命令的子命令瓢姻,這里是 cs create <dir> 命令祝蝠。
  • .action() 方法:用于設(shè)置命令執(zhí)行的相關(guān)回調(diào)。這里綁定在 cs create <dir> 命令上汹来,在使用該命令時(shí)觸發(fā)執(zhí)行回調(diào)函數(shù)续膳。

代碼最后的 process 為進(jìn)程對象,是 NodeJS 運(yùn)行時(shí)存在的眾多全局變量之一收班。process 對象中的 argv 屬性用來捕獲命令行參數(shù)坟岔。

3

剛剛在 bin/index.js 里說明的 .action 回調(diào)函數(shù)綁定在 cs create <dir> 命令下。當(dāng)我們使用該命令時(shí)摔桦,會觸發(fā) cs.create() 語句的執(zhí)行社付,這就要提及我們引入的 lib/cs.js 文件了。

打住邻耕,第一節(jié)里展示的 package.json 中鸥咖,script字段里有這么一條語句:"compile": "babel src/ -d lib/"。這是說明 lib/ 文件夾下的代碼是通過 src/ 文件夾下的代碼轉(zhuǎn)譯過來的兄世,真正我們需要去關(guān)注的是 src/cs.js 文件啼辣。

為什么需要轉(zhuǎn)譯?src 里的 JavaScript 代碼或多或少的使用到了 ECMAScript6 新特性御滩,有些用戶的 Node 環(huán)境并不一定能得到較好的解析鸥拧。

src/cs.js 主要代碼片段為:

let create = require('./create')
let path = require('path')
let distPath = path.join(__dirname, '/../dist')
let dist = process.cwd() + '/'

/**
* [運(yùn)行 create 命令]
* @return {[type]} [description]
*/
exports.create = (rootDir) => {
  console.log('\n項(xiàng)目目錄開始創(chuàng)建\n')
  create.init(distPath, dist, rootDir)
  helpGuide()
}

不難理解,create 變量指向 cs create <dir> 所要執(zhí)行的源代碼削解;path 是 NodeJS 自帶模塊富弦,提供文件目錄解析功能。

最終 src/index.js 使用 exports.create 語句向外部暴露出 create 方法氛驮。bin/index.js 便可以將該方法通過 .action() 綁定到 cs create <dir> 命令上了腕柜。

4

精彩的來了。都說 ECMAScript6 的指定振奮人心,JavaScript 的魅力越來越大盏缤,這里便是一次體驗(yàn) JavaScript 在 NodeJS 上的新玩法有趣之旅砰蠢。

src/create.js 文件中,主要用到了 NodeJS 自帶的 fs 文件模塊蛾找,來生成新項(xiàng)目的基礎(chǔ)架構(gòu)娩脾。文件最后暴露出的 init 方法源碼如下。

exports.init = (path, dist, rootDir) => {
  createRootDir(rootDir)
  // 從新目錄開始新建項(xiàng)目
  dist = dist + rootDir
  copyDir(path, dist)
}

init 方法獲取了 path 參數(shù)打毛、dist 參數(shù)和 rootDir 參數(shù)柿赊。在該方法中,我們先將 rootDir 參數(shù)傳入 createRootDir() 函數(shù)中創(chuàng)建項(xiàng)目根目錄幻枉。

在哪里創(chuàng)建項(xiàng)目根目錄呢碰声?就在執(zhí)行 cs 命令時(shí)的當(dāng)前目錄下:

const createRootDir = (rootDir) => {
  fs.access(process.cwd(), function (err) {
    if (err) {
      // 目錄不存在時(shí)創(chuàng)建目錄
      fs.mkdirSync(rootDir)
    }
  })
}

有了項(xiàng)目根目錄,就要將模塊下 dist/ 文件夾里的所有文件遞歸拷貝到根目錄下熬甫。一個參數(shù)用來指向 dist/ 文件夾胰挑,另一個參數(shù)用來指向根目錄,便可以開始遞歸復(fù)制椿肩。

/**
 * [初始化靜態(tài)資源]
 * @param  {[type]} src  [初始化資源路徑]
 * @param  {[type]} dist [當(dāng)前終端所在目錄]
 * @return {[type]}      [description]
 */
const copyDir = (src, dist) => {
  fs.access(dist, function (err) {
    if (err) {
      // 目錄不存在時(shí)創(chuàng)建目錄
      fs.mkdirSync(dist)
    }
    _copy(null, src, dist)
  })

  function _copy (err, src, dist) {
    if (err) { throw err }
    fs.readdir(src, function (err, files) {
      if (err) { throw err }
      // 過濾不生成的文件
      miscFiles.forEach(function (v) {
        if (!files.includes(v)) return
        files = files.filter(function (k) {
          return k !== v
        })
      })
      // 遍歷目錄中的文件
      files.forEach(function (path) {
        var _src = src + '/' + path
        var _dist = dist + '/' + path
        fs.stat(_src, function (err, st) {
          if (err) { throw err }
          // 判斷是文件還是目錄
          if (st.isFile()) {
            fs.writeFileSync(_dist, fs.readFileSync(_src))
          } else if (st.isDirectory()) {
            // 當(dāng)是目錄是瞻颂,遞歸復(fù)制
            copyDir(_src, _dist)
          }
        })
      })
    })
  }
}

fs 文件模塊的具體內(nèi)容推薦閱讀阮一峰的開源電子書——《JavaScript 標(biāo)準(zhǔn)參考教程》中的“NodeJS”章節(jié),來深入淺出 fs 模塊的用法郑象。

完美贡这,這時(shí)我們就可以發(fā)布我們的腳手架包了。

5

如何發(fā)布一個 npm 包到 npm 倉庫中厂榛,供其他人使用盖矫?當(dāng)我們照著第一步,將 package.json 配置好后击奶,其實(shí)模塊的準(zhǔn)備工作已經(jīng)做好了辈双。

還沒有做的就是在域名為 npmjs.com 的官網(wǎng)上注冊一個賬號。這樣柜砾,當(dāng)我們直接在模塊根目錄使用 npm publish 命令的時(shí)候湃望,輸入正確的 npmjs.com 賬號、密碼痰驱,就能成功發(fā)布你的開源包了证芭!

縱然讀博文是一個有趣的體驗(yàn),但也可以親自動手試一試哦萄唇。

6

也就是說,酷炫的生成新項(xiàng)目骨架的來源术幔,只是簡單的遞歸復(fù)制該模塊下的 dist/ 文件夾到新項(xiàng)目中另萤。但我們需要關(guān)注的重點(diǎn)在于,dist/ 文件夾下,到底裝了什么四敞?

“初級 Web App 項(xiàng)目初始化工具”一說泛源,也就名歸有主了。dist/ 模板忿危,也就是新項(xiàng)目的骨架如下达箍。

.
├── .babelrc             # ES6 代碼轉(zhuǎn)義規(guī)則配置
├── .eslint.js           # JavaScript 代碼規(guī)范
├── .gitignore           # Git 不跟蹤的特殊文件
├── LICENSE              # 開源協(xié)議
├── README.md            # 項(xiàng)目介紹
├── material             # README.md 引用的圖片庫
├── package.json         # 項(xiàng)目配置文件
├── src                  # 源碼開發(fā)目錄
│   ├── favicon.ico      # 網(wǎng)頁標(biāo)題小圖標(biāo)
│   ├── html             # HTML 頁面模板目錄
│   ├── image            # 圖片資源目錄
│   ├── manifest.json    # 網(wǎng)絡(luò)應(yīng)用清單
│   ├── script           # 腳本文件資源目錄
│   └── style            # 樣式文件資源目錄
├── webpack.config.js    # Webpack 多文件打包基礎(chǔ)配置
├── webpack.dev.js       # Webpack 開發(fā)環(huán)境配置
├── webpack.prod.js      # Webpack 發(fā)布上線配置
└── yarn.lock            # yarn 包管理器的依賴說明

新項(xiàng)目骨架中默認(rèn)推薦了:

  • 使用 Webpack 來打包多頁面;
  • 使用 ESLint 來規(guī)范自己項(xiàng)目的 JavaScript 代碼铺厨;
  • 使用 Babel 來編譯使用 ECMAScript 新特性的 JavaScript 代碼缎玫。
  • 使用 MIT 開源協(xié)議;
  • 源代碼都放在 src/ 目錄下解滓;
  • src/ 目錄要對不同的代碼進(jìn)行合理的分層赃磨。

End

現(xiàn)在的不足,是未來的暢想洼裤。

這個模塊并不完美邻辉,一個健壯的命令還應(yīng)該能支持足夠多的參數(shù),運(yùn)行足夠有意義的子命令腮鞍。比如我們常用 man 命令來看另一個命令的使用手冊值骇,那要讓用戶能用到 man cs 命令,還需要我們在代碼中加入 man 字段等等移国。吱瘩。

我又為什么,這么熱衷于分享這個輪子桥狡?

記得有一個前端群里曾有人問過:

“怎么沒有 VueJS 的源碼解析搅裙?”

時(shí),我說過:

“大牛很忙裹芝,關(guān)注的是前端前沿部逮,不寫這些源碼解析博文是個好事。

“當(dāng)我們想有一個源碼解析教程的時(shí)候嫂易,這是一個打開新世界的契機(jī)——未嘗不使我們親自來寫兄朋,通過分享走向?qū)W習(xí)效率金字塔的最高層?”

這樣的能力并不是人人都能具備怜械,也不必要讓人人都具備颅和。我曾在大一傲氣的說過“做最好的自己,影響該影響的人”缕允,現(xiàn)在想起來除了有立刻找地洞鉆進(jìn)去的沖動外峡扩,反而還是覺得有一定的道理(笑。這時(shí)候允許我自稱為一次“教主”障本,我們的理念是:

讀文檔教届,讀文檔响鹃,讀文檔。
寫博客案训,寫博客买置,寫博客。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末景馁,一起剝皮案震驚了整個濱河市板壮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌合住,老刑警劉巖绰精,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異透葛,居然都是意外死亡笨使,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門僚害,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硫椰,“玉大人,你說我怎么就攤上這事萨蚕“胁荩” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵岳遥,是天一觀的道長奕翔。 經(jīng)常有香客問我,道長浩蓉,這世上最難降的妖魔是什么派继? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任帮坚,我火速辦了婚禮,結(jié)果婚禮上互艾,老公的妹妹穿的比我還像新娘。我一直安慰自己讯泣,他們只是感情好纫普,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著好渠,像睡著了一般昨稼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上假栓,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機(jī)與錄音霍掺,去河邊找鬼匾荆。 笑死杆烁,一個胖子當(dāng)著我的面吹牛牙丽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播兔魂,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼析校!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起智玻,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤遂唧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后尚困,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蠢箩,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡事甜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了逻谦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡贱鼻,死狀恐怖宴卖,靈堂內(nèi)的尸體忽然破棺而出邻悬,到底是詐尸還是另有隱情症昏,我是刑警寧澤父丰,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站蛾扇,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏镀首。R本人自食惡果不足惜坟漱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一更哄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧成翩,春花似錦、人聲如沸捕传。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至聂示,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鱼喉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工扛禽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锋边,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓编曼,卻偏偏與公主長得像豆巨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掐场,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容