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

以個人小經驗來看,軟件萌新寫出來的代碼大多「沒法直視」。具體現象包括空格和換行符亂用、文件夾和變量的命名多使用拼音等。坐不住的我,便想到了經過 ESLint 配置文件來規範實驗室的 JavaScript 代碼規範的 Idea。css

因而巧遇前實驗室畢業學長曾經發布的 npm 包——creatshare-project-quick-init。安裝好這個包,咱們即可以在空文件夾下生成一個項目的基礎骨架。html

dist  //發佈目錄,用於生產環境
src   //開發目錄,開發時所需資源
|----dist  //測試環境目錄
|     |----static
|             |----css  //編譯打包後的css資源
|             |----js   //打包壓縮後的js資源
|             |----imgs //測試環境圖片資源
|----less  //開發所需less代碼
|----js    //開發所需js代碼
|    |----lib //庫或框架資源
|----imgs  //開發所需圖片資源
index.html    //開發頁面
gulpfile.js
package.json
README.md

What a good idea~!前端

在學長的這個包中,主要構建了 gulp 配置,less 和測試文件的骨架。雖然再無更多內容,但這份構建基礎骨架的靈感仍是被我愉快的收走了——學前端的人不少,但大多都太缺工程化意識了。因而,這個靈感成爲了避免錯突破口。node

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

0

經過這篇文章,你能瞭解到:git

  • 如何用 NodeJS 編寫命令行工具?
  • 如何發佈本身的 npm 包?
  • 筆者與 creatshare-app-init 的故事?

在本文中,或多或少出現過如下關鍵字,個人解釋是:github

  • 輪子:該詞在前端開發平常用語中,表示一個基於原生代碼實現,但並無對前端行業產生積極意義的模塊。雖然它的出現方便了一些人的使用,但更多的加大了咱們的學習成本。
  • 項目:該詞在前端領域常指一個服務於用戶的軟件立項。
  • 模塊:creatshare-app-init 就是一個模塊,是開發前端項目中的一個子集。正如汽車的各個部件同樣,多個模塊合理組裝起來纔是一輛汽車。

1

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

"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 聲明瞭模塊上線時的依賴,devDependencies 聲明瞭模塊開發時的依賴。該模塊在上線時,即 npm 包被用戶用到時,只須要 commander 庫。commander 庫是 NodeJS 命令行接口開發的優選解決方案,受啓發於 Ruby 的 commander。在解析 bin/index.js 源碼時將詳細拓展。npm

"name": "creatshare-app-init",
"version": "2.1.0",
"description": "CreatShare 實驗室前端項目初始化工具",
"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 最開頭的內容,字段詳情以下:json

  • name 字段:聲明模塊名稱。特殊注意該字段不容許大寫字母及空格的出現,且其與 version 字段造成了 npm 模塊的惟一標識符。
  • version 字段:聲明模塊當前版本號。這裏每當使用 npm publish 將模塊發佈到 npm 倉庫中時,版本號都須要手動自增。
  • description 字段:對模塊進行描述,同時有助於被檢索。
  • bin 字段:npm 自己是經過 bin 屬性配置一個或多個可解析到 PATH 路徑下的可執行模塊。模塊若被全局安裝,則 npm 會爲 bin 中配置的文件在 bin 目錄下建立一個軟鏈接;模塊若被局部安裝,軟鏈接會配置在項目內的 ./node_modules/.bin/目錄下。
  • script 字段:定義模塊的腳本配置。如,當咱們在模塊目錄下使用 npm run compile 時,將自動執行 babel src/ -d lib/ 命令,進行 ECMAScript6 代碼的轉譯。

2

剛剛提到 package.json 配置文件下的 bin 字段聲明瞭 npm 在生成軟鏈接時的配置。這就即是用戶在安裝好這個目錄後,能夠隨時使用 cs 命令的出處。

咱們又提到了該模塊在非開發環境下只需用到 commander 模塊,這個模塊是 NodeJS 命令行接口開發的優選解決方案。

基於這倆點,咱們就從 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 互聯網實驗室前端 Web App 項目腳手架')
  .option('-e, --enjoy')

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

program.parse(process.argv)

就這麼二十來行。由於咱們要寫的模塊是要運行在命令行下的,就須要 #!/usr/bin/env node 語句來告訴系統使用 node 環境來運行咱們的文件,必不可少。

在引入 commander 並將其賦值給 program 變量後,咱們對其使用了以下方法:

  • .allowUnknownOption() 方法:
  • .version() 方法:用於設置命令程序的版本號。
  • .description() 方法:用於設置命令的描述。能夠綁定在跟命令下,這裏是 cs 命令;或綁定在子命令下,如 cs create <dir> 命令。
  • .option() 方法:定義命令的具體選項。
  • .command() 方法:定義命令的子命令,這裏是 cs create <dir> 命令。
  • .action() 方法:用於設置命令執行的相關回調。這裏綁定在 cs create <dir> 命令上,在使用該命令時觸發執行回調函數。

代碼最後的 process 爲進程對象,是 NodeJS 運行時存在的衆多全局變量之一。process 對象中的 argv 屬性用來捕獲命令行參數。

3

剛剛在 bin/index.js 裏說明的 .action 回調函數綁定在 cs create <dir> 命令下。當咱們使用該命令時,會觸發 cs.create() 語句的執行,這就要說起咱們引入的 lib/cs.js 文件了。

打住,第一節裏展現的 package.json 中,script字段裏有這麼一條語句:"compile": "babel src/ -d lib/"。這是說明 lib/ 文件夾下的代碼是經過 src/ 文件夾下的代碼轉譯過來的,真正咱們須要去關注的是 src/cs.js 文件。

爲何須要轉譯?src 裏的 JavaScript 代碼或多或少的使用到了 ECMAScript6 新特性,有些用戶的 Node 環境並不必定能獲得較好的解析。

src/cs.js 主要代碼片斷爲:

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

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

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

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

4

精彩的來了。都說 ECMAScript6 的指定振奮人心,JavaScript 的魅力愈來愈大,這裏即是一次體驗 JavaScript 在 NodeJS 上的新玩法有趣之旅。

src/create.js 文件中,主要用到了 NodeJS 自帶的 fs 文件模塊,來生成新項目的基礎架構。文件最後暴露出的 init 方法源碼以下。

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

init 方法獲取了 path 參數、dist 參數和 rootDir 參數。在該方法中,咱們先將 rootDir 參數傳入 createRootDir() 函數中建立項目根目錄。

在哪裏建立項目根目錄呢?就在執行 cs 命令時的當前目錄下:

const createRootDir = (rootDir) => {
  fs.access(process.cwd(), function (err) {
    if (err) {
      // 目錄不存在時建立目錄
      fs.mkdirSync(rootDir)
    }
  })
}

有了項目根目錄,就要將模塊下 dist/ 文件夾裏的全部文件遞歸拷貝到根目錄下。一個參數用來指向 dist/ 文件夾,另外一個參數用來指向根目錄,即可以開始遞歸複製。

/**
 * [初始化靜態資源]
 * @param  {[type]} src  [初始化資源路徑]
 * @param  {[type]} dist [當前終端所在目錄]
 * @return {[type]}      [description]
 */
const copyDir = (src, dist) => {
  fs.access(dist, function (err) {
    if (err) {
      // 目錄不存在時建立目錄
      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()) {
            // 當是目錄是,遞歸複製
            copyDir(_src, _dist)
          }
        })
      })
    })
  }
}

fs 文件模塊的具體內容推薦閱讀阮一峯的開源電子書——《JavaScript 標準參考教程》中的「NodeJS」章節,來深刻淺出 fs 模塊的用法。

完美,這時咱們就能夠發佈咱們的腳手架包了。

5

如何發佈一個 npm 包到 npm 倉庫中,供其餘人使用?當咱們照着第一步,將 package.json 配置好後,其實模塊的準備工做已經作好了。

尚未作的就是在域名爲 npmjs.com 的官網上註冊一個帳號。這樣,當咱們直接在模塊根目錄使用 npm publish 命令的時候,輸入正確的 npmjs.com 帳號、密碼,就能成功發佈你的開源包了!

縱然讀博文是一個有趣的體驗,但也能夠親自動手試一試哦。

6

也就是說,酷炫的生成新項目骨架的來源,只是簡單的遞歸複製該模塊下的 dist/ 文件夾到新項目中。但咱們須要關注的重點在於,dist/ 文件夾下,到底裝了什麼?

「初級 Web App 項目初始化工具」一說,也就名歸有主了。dist/ 模板,也就是新項目的骨架以下。

.
├── .babelrc             # ES6 代碼轉義規則配置
├── .eslint.js           # JavaScript 代碼規範
├── .gitignore           # Git 不跟蹤的特殊文件
├── LICENSE              # 開源協議
├── README.md            # 項目介紹
├── material             # README.md 引用的圖片庫
├── package.json         # 項目配置文件
├── src                  # 源碼開發目錄
│   ├── favicon.ico      # 網頁標題小圖標
│   ├── html             # HTML 頁面模板目錄
│   ├── image            # 圖片資源目錄
│   ├── manifest.json    # 網絡應用清單
│   ├── script           # 腳本文件資源目錄
│   └── style            # 樣式文件資源目錄
├── webpack.config.js    # Webpack 多文件打包基礎配置
├── webpack.dev.js       # Webpack 開發環境配置
├── webpack.prod.js      # Webpack 發佈上線配置
└── yarn.lock            # yarn 包管理器的依賴說明

新項目骨架中默認推薦了:

  • 使用 Webpack 來打包多頁面;
  • 使用 ESLint 來規範本身項目的 JavaScript 代碼;
  • 使用 Babel 來編譯使用 ECMAScript 新特性的 JavaScript 代碼。
  • 使用 MIT 開源協議;
  • 源代碼都放在 src/ 目錄下;
  • src/ 目錄要對不一樣的代碼進行合理的分層。

End

如今的不足,是將來的暢想。

這個模塊並不完美,一個健壯的命令還應該能支持足夠多的參數,運行足夠有意義的子命令。好比咱們經常使用 man 命令來看另外一個命令的使用手冊,那要讓用戶能用到 man cs 命令,還須要咱們在代碼中加入 man 字段等等。。

我又爲何,這麼熱衷於分享這個輪子?

記得有一個前端羣裏曾有人問過:

「怎麼沒有 VueJS 的源碼解析?」

時,我說過:

「大牛很忙,關注的是前端前沿,不寫這些源碼解析博文是個好事。

「當咱們想有一個源碼解析教程的時候,這是一個打開新世界的契機——何嘗不使咱們親自來寫,經過分享走向學習效率金字塔的最高層?」

這樣的能力並非人人都能具有,也沒必要要讓人人都具有。我曾在大一傲氣的說過「作最好的本身,影響該影響的人」,如今想起來除了有馬上找地洞鑽進去的衝動外,反而仍是以爲有必定的道理(笑。這時候容許我自稱爲一次「教主」,咱們的理念是:

讀文檔,讀文檔,讀文檔。
寫博客,寫博客,寫博客。

相關文章
相關標籤/搜索