從零實現(xiàn)命令行腳手架工具——自動初始化項目工程以及發(fā)布到NPM
前言
這篇文章將帶你從零實現(xiàn)一款命令行腳手架工具用于初始化項目以及如何發(fā)布到NPM。首先,我們所熟知的VueCLI就是采用命令行工具快速生成項目工程目錄的,這樣,我們每次開發(fā)項目前只需要在命令行中輸入命令,然后就可以快速生成項目工程,非常方便。那么,這么方便的命令行工具是怎么實現(xiàn)的呢?下面我們就開始進入實戰(zhàn)環(huán)節(jié)。
實戰(zhàn)
我將會使用之前自己開發(fā)的一款腳手架工具strview-cli來介紹如何實現(xiàn)它的。這個腳手架工具的源碼地址如下:
- https://github.com/maomincoding/strview-cli
第一步
首先,我們姑且創(chuàng)建一個文件夾名字叫strviewCli。
- mkdir strviewCli
然后,在文件夾中初始化項目
- npm init
之后,自動生成一個package.json文件。
第二步
我們再創(chuàng)建一個文件夾,即bin。
- mkdir bin
接著,在文件夾中我們創(chuàng)建一個index.js文件、config文件夾、utils文件夾。
- touch index.js
- mkdir config
- mkdir utils
最后,在config 文件中創(chuàng)建一個index.js文件,在utils文件夾中創(chuàng)建一個checkDire.js。
- touch index.js
- touch checkDire.js
目前,我們文件目錄結(jié)構(gòu)為
- - bin
- -- config
- --- index.js
- -- utils
- --- checkDire.js
- -- index.js
- - package.json
最后的最后,我們在根目錄下,創(chuàng)建一個.gitignore文件,以及README.md。
最后,命令行工具項目文件目錄如下:
- - bin
- -- config
- --- index.js
- -- utils
- --- checkDire.js
- -- index.js
- - package.json
- - README.md
- - .gitignore
第三步
上面兩步,我們給我們的命令行工具奠定了基本結(jié)構(gòu)。下面,我們將在一一為每個文件注入靈魂~
首先,.gitignore文件和README.md,這里就不過多闡述了,可以根據(jù)自己的需要進行增添內(nèi)容。
其次,詳細介紹的就是package.json文件。以下是我自己寫的package.json,這里需要注意幾個重要的字段。
- name :項目名
- version:版本號
- description:項目描述
- main:入口文件
- author:作者
- keywords:關(guān)鍵詞
- bin:腳本執(zhí)行入口
- repository:代碼倉庫
- license:許可證
- private:私有
- dependencies:依賴
- browserslist:指定了項目的目標瀏覽器的范圍
- {
- "name": "strview-cli",
- "version": "1.1.1",
- "description": "Strview.js project scaffolding tools.",
- "main": "index.js",
- "author": {
- "name": "maomincoding",
- "email": "17864296568@163.com",
- "url": "https://www.maomin.club"
- },
- "keywords": [
- "strview",
- "strview.js",
- "strview-app",
- "strview-cli"
- ],
- "bin": {
- "strview-cli": "./bin/index.js"
- },
- "repository": {
- "type": "git",
- "url": "https://github.com/maomincoding/strview-cli.git"
- },
- "license": "MIT",
- "private": false,
- "dependencies": {
- "chalk": "^4.0.0",
- "commander": "^5.0.0",
- "fs-extra": "^9.0.0",
- "inquirer": "^7.1.0"
- },
- "browserslist": [
- "> 1%",
- "last 2 versions"
- ]
- }
上面的package.json中幾個屬性如:name、version、description、main、author、keywords、repository、license可以根據(jù)自己的需求來定義。
你可能會看到dependencies屬性中有幾個依賴,分別是chalk、commander、fs-extra、inquirer。這里先提一下,下面會詳細介紹它們。不過,需要注意的是fs-extra模塊是添加了本機fs模塊中不包含的文件系統(tǒng)方法,并向fs方法添加了promise支持。它還使用優(yōu)美的fs來防止EMFILE錯誤。它應(yīng)該是fs的替代品。
第四步
接下來,我們將進入bin文件夾中,然后,我們首先需要編輯config\index.js文件。
- module.exports = {
- npmUrl: 'https://registry.npmjs.org/strview-cli',
- promptTypeList: [
- {
- type: 'list',
- message: 'Please select the template type to pull:',
- name: 'type',
- choices: [
- {
- name: 'strview-app',
- value: {
- url: 'https://github.com/maomincoding/strview-app.git',
- gitName: 'strview-app',
- val: 'strview-app',
- },
- },
- ],
- },
- ],
- };
以上代碼中導(dǎo)出一個對象,對象中有兩個屬性:npmUrl和promptTypeList。
npmUrl屬性是命令行工具提交到NPM的地址。怎么得到這個地址呢?你需要按照下面步驟:
登錄NPM
- npm login
依次輸入你的用戶名、密碼、郵箱。
發(fā)布到NPM
- npm publish
發(fā)布成功后,會顯示版本號。記住,每次發(fā)布都要更改版本號,否則會出錯。
正常發(fā)布之后,你可以打開NPM網(wǎng)址,搜索你的命令行工具的名稱。比如我的命令行工具strview-cli。網(wǎng)址即:https://registry.npmjs.org/strview-cli。
promptTypeList屬性中有一個choices屬性,它是一個數(shù)組,你可以配置你遠程項目工程倉庫。數(shù)組每個元素是一個對象,比如這里的
- {
- name: 'strview-app',
- value: {
- url: 'https://github.com/maomincoding/strview-app.git',
- gitName: 'strview-app',
- val: 'strview-app',
- },
- }
name屬性是你的項目工程名稱,value屬性又是一個對象,里面有三個屬性:url、gitName、val分別表示遠程倉庫地址、倉庫名、值(一般與倉庫名一致)。你可以根據(jù)你的需要進行配置,這里是我配置的自己的strview-app。
以上就是config\index.js文件的配置。
第五步
下面,我們還在bin文件夾中,我們接下來編輯utils\checkDire.js文件。
- const fs = require("fs");
- const chalk = require("chalk");
- module.exports = function (dir, name) {
- let isExists = fs.existsSync(dir);
- if (isExists) {
- console.log(
- chalk.red(
- `The ${name} project already exists in directory. Please try to use another projectName`
- )
- );
- process.exit(1);
- }
- };
這個文件沒有自定義的部分,你只需要直接用即可。這里我們看到引入兩個模塊,分別是fs、chalk。Node.js內(nèi)置的fs模塊就是文件系統(tǒng)模塊,負責(zé)讀寫文件。chalk模塊是美化命令行輸出樣式,使輸出命令不再單調(diào)。
我們看到這里導(dǎo)出一個函數(shù),函數(shù)有兩個參數(shù):分別是dir和name。我們這里先暫且不看這個函數(shù),先只知道需要傳兩個參數(shù)就可以。
第六步
下面我們先分析bin\index.js文件,這個文件是命令行工具的入口文件,非常重要。同樣,這里不需要自定義,直接用就可以。
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const chalk = require('chalk');
- const commander = require('commander');
- const inquirer = require('inquirer');
- const checkDire = require('./utils/checkDire.js');
- const { exec } = require('child_process');
- const { version } = require('../package.json');
- const { promptTypeList } = require('./config');
- commander
- .version(version, '-v, --version')
- .command('init <projectName>')
- .alias('i')
- .description('Enter the project name and initialize the project template')
- .action(async (projectName) => {
- await checkDire(path.join(process.cwd(), projectName), projectName);
- inquirer.prompt(promptTypeList).then((result) => {
- const { url, gitName, val } = result.type;
- console.log(
- 'The template type information you selected is as follows:' + val
- );
- console.log('Project initialization copy acquisition...');
- if (!url) {
- console.log(
- chalk.red(`${val} This type is not supported at the moment...`)
- );
- process.exit(1);
- }
- exec('git clone ' + url, function (error, stdout, stderr) {
- if (error !== null) {
- console.log(chalk.red(`clone fail,${error}`));
- return;
- }
- fs.rename(gitName, projectName, (err) => {
- if (err) {
- exec('rm -rf ' + gitName, function (err, out) { });
- console.log(
- chalk.red(`The ${projectName} project template already exist`)
- );
- } else {
- if (fs.existsSync(`./${projectName}/.git/config`)) {
- exec('git remote rm origin', { cwd: `./${projectName}` });
- console.log(
- chalk.green(
- `✔ The ${projectName} project template successfully create`
- )
- );
- }
- }
- });
- });
- });
- });
- commander.parse(process.argv);
我們從頭開始看,我們會看到引入了fs、path、chalk、commander、inquirer、child_process。
path 模塊提供了用于處理文件和目錄的路徑的實用工具。
commander是完整的 node.js 命令行解決方案,它有很多用法,具體你可以參照Commander中文文檔:
- https://github.com/tj/commander.js/blob/master/Readme_zh-CN.md
inquirer是通用交互式命令行用戶界面的集合。開始通過npm init創(chuàng)建package.json文件的時候就有大量與用戶的交互,而現(xiàn)在大多數(shù)工程都是通過腳手架來創(chuàng)建的,使用腳手架的時候最明顯的就是與命令行的交互,如果想自己做一個腳手架或者在某些時候要與用戶進行交互,這個時候就不得不提到inquirer.js了。
child_process模塊是nodejs的一個子進程模塊,可以用來創(chuàng)建一個子進程,并執(zhí)行一些任務(wù)。比如說就可以直接在js里面調(diào)用shell命令。
介紹完引入的模塊,然后再介紹下面的代碼。你會看到下面的代碼大部分都用到了commander的方法。
首先,commander.version(version, '-v, --version'),.version()方法可以設(shè)置版本,之后就可以使用-v或--version命令查看版本。
通過 .command('init
.description('Enter the project name and initialize the project template')這行代碼中.description方法則是對上面初始化配置項目名的描述。
.action((projectName, cmd) => {...})這行代碼中.action方法是定義命令的回調(diào)函數(shù),我們發(fā)現(xiàn)它使用了async/await這組關(guān)鍵字用來處理異步。首先,await checkDire(path.join(process.cwd(), projectName), projectName);傳入兩個參數(shù)分別為項目所在的目錄、項目名稱。我們這里先分析checkDire方法,也就是之前utils\checkDire.js文件內(nèi)的方法。
- module.exports = function (dir, name) {
- let isExists = fs.existsSync(dir);
- if (isExists) {
- console.log(
- chalk.red(
- `The ${name} project already exists in directory. Please try to use another projectName`
- )
- );
- process.exit(1);
- }
- };
fs.existsSync(dir)以同步的方法檢測目錄是否存在。如果目錄存在 返回 true ,如果目錄不存在 返回false。如果存在了,就執(zhí)行下面的提示并退出終止進程。
接著,我們又回到bin\index.js文件。接著往下執(zhí)行,到了inquirer.prompt()這個方法,這個方法的作用主要是啟動提示界面(查詢會話),第一個參數(shù)是包含問題對象的問題(數(shù)組)(使用反應(yīng)式接口,還可以傳遞一個Rx.Observable實例),這里我們傳入的是config\index.js文件中的promptTypeList屬性,它是一個數(shù)組。
- promptTypeList: [
- {
- type: 'list',
- message: 'Please select the template type to pull:',
- name: 'type',
- choices: [
- {
- name: 'strview-app',
- value: {
- url: 'https://github.com/maomincoding/strview-app.git',
- gitName: 'strview-app',
- val: 'strview-app',
- },
- },
- ],
- },
- ],
inquirer.prompt()這個方法返回一個Promise對象,所以這里可以then()方法來獲取返回的數(shù)據(jù)。然后我們通過解構(gòu)來分別獲取到url、gitName、val這三個屬性值。
- const { url, gitName, val } = result.type;
然后,下面就是輸出命令以及執(zhí)行命令的環(huán)節(jié)了。我分為兩部分來分析剩余代碼,第一部分如下:
- console.log('The template type information you selected is as follows:' + val);
- console.log('Project initialization copy acquisition...');
- if (!url) {
- console.log(chalk.red(`${val} This type is not supported at the moment...`));
- process.exit(1);
- }
我們這里有一個判斷語句,就是判斷遠程倉庫地址是否是假值,如果是假值的話,就執(zhí)行提示并退出終止進程。
接著,我們分析第二部分:
- exec('git clone ' + url, function (error, stdout, stderr) {
- if (error !== null) {
- console.log(chalk.red(`clone fail,${error}`));
- return;
- }
- fs.rename(gitName, projectName, (err) => {
- if (err) {
- exec('rm -rf ' + gitName, function (err, out) { });
- console.log(chalk.red(`The ${projectName} project template already exist`));
- } else {
- if (fs.existsSync(`./${projectName}/.git/config`)) {
- exec('git remote rm origin', { cwd: `./${projectName}` });
- console.log(
- chalk.green(`✔ The ${projectName} project template successfully create`)
- );
- }
- }
- });
- }
這部分主要是執(zhí)行命令,也是最關(guān)鍵的部分。這部分首先使用了exec()方法,第一個參數(shù)是要執(zhí)行的命令,第二個參數(shù)是回調(diào)函數(shù)。
首先,我們執(zhí)行exec('git clone ' + url)來下載遠程項目,接著我們進入回調(diào)函數(shù),如果有錯誤,就輸出提示并退出。否則,將使用fs.rename()方法將文件重命名。因為我們下載的遠程倉庫名不一定跟我們初始化配置的名字一樣。
如果錯誤,就刪除遠程項目工程目錄。否則,就判斷./${projectName}/.git/config文件是否存在,如果存在就執(zhí)行exec('git remote rm origin', { cwd:./${projectName}});命令刪除遠程倉庫地址。這是因為需要自定義配置倉庫地址,而不是直接使用下載的倉庫地址。最后,提示創(chuàng)建成功。
最后一行。commander.parse(process.argv);這行代碼中.parse()的第一個參數(shù)是要解析的字符串數(shù)組,也可以省略參數(shù)而使用process.argv。指明,按 node 約定。
第七步
到了這一步,所有配置文件都配置完成了。如果你想開源的話,你可以參照線上自己生成一個LICENSE文件。這個文件是軟件許可證,可以去github去自動生成這個文件。
最后,我們就要發(fā)布我們這個命令行工具了。注意,在發(fā)布之前,需要改一下你的版本號。 如之前是1.0.0,現(xiàn)在可以改成2.0.0。具體這三個數(shù)字怎么定義,也有說法。第一部分為主版本號,變化了表示有了一個不兼容上個版本的大更改。第二部分為次版本號,變化了表示增加了新功能,并且可以向后兼容。第三部分為修訂版本號,變化了表示有bug修復(fù),并且可以向后兼容。
- npm publish
發(fā)布成功。
第八步
這里以strview-cli為例。
你可以全局安裝你的腳手架。
- npm i strview-cli -g
安裝完成之后,你可以查看版本。
- strview-cli -v
最后,就是初始化項目了,
- strview-cli init <projectName>
or
- strview-cli i <projectName>
結(jié)語
謝謝閱讀,希望沒有耽誤你的時間。
你可以自己封裝一個常用的項目工程,可以通過這種方式來初始化你的項目。這樣,才會顯得有逼格!!!哈哈哈~
本文轉(zhuǎn)載自微信公眾號「前端歷劫之路」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系前端歷劫之路公眾號。