腳手架的做用:爲減小重複性工做而作的重複性工做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