腳手架的做用:爲減小重複性工做而作的重複性工做javascript
即爲了開發中的:編譯 es6,js 模塊化,壓縮代碼,熱更新等功能,咱們使用webpack
等打包工具,可是又帶來了新的問題:初始化工程的麻煩,複雜的webpack
配置,以及各類配置文件,因此就有了一鍵生成項目,0 配置開發的腳手架html
本文項目代碼地址前端
本系列分 3 篇,詳細介紹如何實現一個腳手架:vue
首先說一下我的的開發習慣java
在寫功能前我會先把調用方式寫出了,而後一步一步的從使用者的角度寫,現將基礎功能寫好後,慢慢完善node
例如一鍵初始化項目功能react
我指望的就是 在命令行執行輸入 my-cli create text-project
,回車後直接建立項目並生成模板,還會把依賴都下載好webpack
咱們下面就從命令行開始入手git
建立項目 my-cli
,執行 npm init -y
快速初始化es6
my-cli
:
在 package.json
中加入:
{ "bin": { "my-cli": "bin.js" } }
bin.js
:
#!/usr/bin/env node console.log(process.argv);
#!/usr/bin/env node
,這一行是必須加的,就是讓系統動態的去PATH
目錄中查找node
來執行你的腳本文件。
命令行執行 npm link
,建立軟連接至全局,這樣咱們就能夠全局使用my-cli
命令了,在開發 npm
包的前期都會使用link
方式在其餘項目中測試來開發,後期再發布到npm
上
命令行執行 my-cli 1 2 3
輸出:[ '/usr/local/bin/node', '/usr/local/bin/my-cli', '1', '2', '3' ]
這樣咱們就能夠獲取到用戶的輸入參數
例如my-cli create test-project
咱們就能夠經過數組第 [2] 位判斷命令類型create
,經過第 [3] 位拿到項目名稱test-project
node
的命令行解析最經常使用的就是commander
庫,來簡化複雜cli
參數操做
(咱們如今的參數簡單能夠不使用commander
,直接用process.argv[3]
獲取名稱,可是爲了以後會複雜的命令行,這裏也先使用commander
)
#!/usr/bin/env node const program = require("commander"); const version = require("./package.json").version; program.version(version, "-v, --version"); program .command("create <app-name>") .description("使用 my-cli 建立一個新的項目") .option("-d --dir <dir>", "建立目錄") .action((name, command) => { const create = require("./create/index"); create(name, command); }); program.parse(process.argv);
commander
解析完成後會觸發action
回調方法
命令行執行:my-cli -v
輸出:1.0.0
命令行執行: my-cli create test-project
輸出:test-project
拿到了用戶傳入的名稱,就能夠用這麼名字建立項目
咱們的代碼儘可能保持bin.js
整潔,不將接下來的代碼寫在bin.js
裏,建立create
文件夾,建立index.js
文件
create/index.js
中:
const path = require("path"); const mkdirp = require("mkdirp"); module.exports = function(name) { mkdirp(path.join(process.cwd(), name), function(err) { if (err) console.error("建立失敗"); else console.log("建立成功"); }); };
process.cwd()
獲取工做區目錄,和用戶傳入項目名稱拼接起來
(建立文件夾咱們使用mkdirp
包,能夠避免咱們一級一級的建立目錄)
修改bin.js
的action
方法:
// bin.js .action(name => { const create = require("./create") create(name) });
命令行執行: my-cli create test-project
輸出:建立成功
並在命令行所在目錄建立了一個test-project
文件夾
首先須要先列出咱們的模板包含哪些文件
一個最基礎版的vue
項目模板:
|- src |- main.js |- App.vue |- components |- HelloWorld.vue |- index.html |- package.json
這些文件就不一一介紹了
咱們須要的就是生成這些文件,並寫入到目錄中去
模板的寫法後不少種,下面是個人寫法:
模板目錄:
|- generator |- index-html.js |- package-json.js |- main.js |- App-vue.js |- HelloWorld-vue.js
generator/index-html.js
模板示例:
module.exports = function(name) { const template = ` { "name": "${name}", "version": "1.0.0", "description": "", "main": "index.js", "scripts": {}, "devDependencies": { }, "author": "", "license": "ISC", "dependencies": { "vue": "^2.6.10" } } `; return { template, dir: "", name: "package.json" }; };
dir
就是目錄,例如main.js
的dir
就是src
create/index.js
在mkdirp
中新增:
const path = require("path"); const mkdirp = require("mkdirp"); const fs = require("fs"); module.exports = function(name) { const projectDir = path.join(process.cwd(), name); mkdirp(projectDir, function(err) { if (err) console.error("建立失敗"); else { console.log(`建立${name}文件夾成功`); const { template, dir, name: fileName } = require("../generator/package")(name); fs.writeFile(path.join(projectDir, dir, fileName), template.trim(), function(err) { if (err) console.error(`建立${fileName}文件失敗`); else { console.log(`建立${fileName}文件成功`); } }); } }); };
這裏只寫了一個模板的建立,咱們能夠用readdir
來獲取目錄下全部文件來遍歷執行
咱們日常下載npm
包都是使用命令行 npm install / yarn install
這時就須要用到 node
的 child_process.spawn
api 來調用系統命令
由於考慮到跨平臺兼容處理,因此使用 cross-spawn 庫,來幫咱們兼容的操做命令
咱們建立utils
文件夾,建立install.js
utils/install.js
:
const spawn = require("cross-spawn"); module.exports = function install(options) { const cwd = options.cwd || process.cwd(); return new Promise((resolve, reject) => { const command = options.isYarn ? "yarn" : "npm"; const args = ["install", "--save", "--save-exact", "--loglevel", "error"]; const child = spawn(command, args, { cwd, stdio: ["pipe", process.stdout, process.stderr] }); child.once("close", code => { if (code !== 0) { reject({ command: `${command} ${args.join(" ")}` }); return; } resolve(); }); child.once("error", reject); }); };
而後咱們就能夠在建立完模板後調用install
方法下載依賴
install({ cwd: projectDir });
要知道工做區爲咱們項目的目錄
至此,解析 cli,建立目錄,建立模板,下載依賴一套流程已經完成
基本功能都跑通以後下面就是要填充剩餘代碼和優化
當代碼寫的多了以後,咱們看上面create
方法內的回調嵌套回調會很是難受
node 7
已經支持async,await
,因此咱們將上面代碼改爲Promise
在utils
目錄下建立,promisify.js
:
module.exports = function promisify(fn) { return function(...args) { return new Promise(function(resolve, reject) { fn(...args, function(err, ...res) { if (err) return reject(err); if (res.length === 1) return resolve(res[0]); resolve(res); }); }); }; };
這個方法幫咱們把回調形式的Function
改爲Promise
在utils
目錄下建立,fs.js
:
const fs = require(fs); const promisify = require("./promisify"); const mkdirp = require("mkdirp"); exports.writeFile = promisify(fs.writeFile); exports.readdir = promisify(fs.readdir); exports.mkdirp = promisify(mkdirp);
將fs
和mkdirp
方法改形成promise
改造後的create.js
:
const path = require("path"); const fs = require("../utils/fs-promise"); const install = require("../utils/install"); module.exports = async function(name) { const projectDir = path.join(process.cwd(), name); await fs.mkdirp(projectDir); console.log(`建立${name}文件夾成功`); const { template, dir, name: fileName } = require("../generator/package")(name); await fs.writeFile(path.join(projectDir, dir, fileName), template.trim()); console.log(`建立${fileName}文件成功`); install({ cwd: projectDir }); };
關於進一步優化:
chalk
和ora
優化log
,給用戶更好的反饋inquirer
問詢用戶獲得更多的選擇:模板vue-router
,vuex
等更多初始化模板功能,eslint
更多的功能:
其實要學會善用第三方庫,你會發現咱們上面的每一個模塊都有第三方庫的身影,咱們只是將這些功能組裝起來,再結合咱們的想法進一步封裝
雖然有vue-cli
,create-react-app
這些已有的腳手架,可是咱們仍是可能在某些狀況下須要本身實現腳手架部分功能,根據公司的業務來封裝,減小重複性工做,或者瞭解一下內部原理
【青團社】招聘前端方面: 高級/資深/技術專家,歡迎投遞 lishixuan@qtshe.com