我們在使用 vue-cli
、create-react-app
的時候,只要執行一個簡單的命令 vue init app
或是 create-react-app app
就是快速建立出一個可直接使用的專案模板,極大地提高了開發效率。
本文提供了一個開發簡易腳手架的過程。
準備工作
第三方工具
- comander:
tj
大神出品的nodejs
命令列解決方案,用於捕獲控制檯輸入的命令; - chalk:命令列文字配色工具;
- cross-spawn:跨平臺的
node
spawn/spawnSync 解決方案; - fs-extra:
nodejs
fs
的加強版,新增了API的同時,也包含了原fs
的API
; - handlebars:一個字串模板工具,可以將資訊填充到模板的指定位置;
- inquirer:互動式命令列使用者介面集合,用於使用者補充資訊或是選擇操作;
- log-symbols:不同日誌級別的彩色符號標誌,包含了
info
、success
、warning
和error
四級; - ora:動態載入操作符號;
初始化專案
首先,這仍然是一個 nodejs
的工程專案,所以我們新建一個名為 scaffold-demo
的資料夾,並使用 npm init
來初始化專案。此時,專案中只有一個 package.json
檔案,內容如下:
{
"name": "scaffold-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
複製程式碼
然後我們刪除 "main": "index.js"
,加入 "private": false
。
main
:是程式的主要入口點,就是說如果有其他使用者install
並requrie
這個包,那麼將返回該檔案export
出來的物件。
private
:是為了保護私有庫的手段,當你的庫是私有庫的時候,加入"private": true
,那麼npm
將會拒絕釋出這個庫。
我們在使用其他腳手架時,在控制檯中輸入一段簡短的命令就能快速建立一個專案模板,那麼他們是如何使用命令列來操作執行專案的呢,答案就在 npm
的 package.json
的 bin
欄位值中。
bin
欄位接受一個 k-v 的Map
,其中key
表示命令名稱,value
表示命令執行的入口檔案。當設定了bin
欄位後,一旦安裝了你的package
,npm
將會這個命令註冊到全域性中,並連結對應的檔案,然後使用者就可以直接使用該命令了。詳見:npm bin 官方文件
我們需要在 package.json
檔案中加入以下內容,其中這個 sd
就是我們命令:
"bin": {
"sd": "./main.js"
},
複製程式碼
然後在專案中新建 main.js
檔案,內容如下:
#!/usr/bin/env node
console.log('Hello Bin')
複製程式碼
其中
#!/usr/bin/env node
的作用就是這行程式碼是當系統執行到這一行的時候,去env
中查詢node
配置,並且呼叫對應的直譯器來執行之後的node
程式。
然後我們執行命令 npm link
或是 npm install -g
,這樣將本專案的命令註冊到了全域性中,然後在命令列中執行 sd
就能看到結果 Hello Bin
。
npm link
:將當前package
連結到全域性執行環境。
npm install -g
:將當前 package 全域性安裝到本地。對應的解除命令為:
npm unlink
或是npm uninstall -g
正式開始
現在我們已經能夠完成最基礎的命令列操作了,繼續構建我們簡易腳手架。
1. 捕獲命令資訊
在上文,我們設定了bin資訊,但是隻有一個命令名稱資訊,但是在其他腳手架中,我們可以輸入多個欄位,如 create-react-app app
中 create-reate-app
表示命令,app
表示建立的專案的名稱。而這種捕獲命令列的操作我們可以藉助 comander 來完成。
實際上,
vue
和react
的腳手架也是藉助 comander 完成的。
我們將 main.js
做如下修改:
#!/usr/bin/env node
const program = require('commander')
program
.command('init <name>')
.description('初始化模板')
.action(name => {
console.log('Hello ' + name)
})
program.parse(process.argv)
複製程式碼
然後在命令列輸入 sd init firstApp
,就能看到返回 Hello firstApp
了。
在上述程式碼中,command
函式表示當前命令的一個子命令,可以設定多個,緊隨的 description
用於描述該命令,action
表示輸入命令後需要執行的操作。其中 command
中的尖括號(<>
)表示該引數為必須輸入的,中括號([]
)表示為可選的。 program.parse(process.argv)
必須要,如果沒有則不會起作用。
更詳細例子參考官網的例子:github.com/tj/commande…
2. 複製專案模板至指定目錄
在本文中我們採用的本地專案模板複製的方式,即本腳手架中包含了所需要初始專案的模板檔案,位於Template
資料夾下(這個目錄開發者可以隨意修改)。
如果想使用線上模板的方式,可以藉助工具
download-git-repo
,將 copy 換成下載即可。本文的
template
內容見文末的程式碼倉庫。
然後我們將 action
中的邏輯替換成如下內容:
action(async name => {
// 判斷使用者是否輸入應用名稱,如果沒有設定為 myApp
const projectName = name || 'myApp'
// 獲取 template 資料夾路徑
const sourceProjectPath = __dirname + '/template'
// 獲取命令所在資料夾路徑
// path.resolve(name) == process.cwd() + '/' + name
const targetProjectPath = path.resolve(projectName)
// 建立一個空的資料夾
fs.emptyDirSync(targetProjectPath)
try {
// 將模板資料夾中的內容複製到目標資料夾(目標資料夾為命令輸入所在資料夾)
fs.copySync(sourceProjectPath, targetProjectPath)
console.log('已經成功拷貝 Template 資料夾下所有檔案!')
} catch (err) {
console.error('專案初始化失敗,已退出!')
return
}
}
複製程式碼
3. 確認目標資料夾是否存在(命令列互動)
我們已經完成了最基礎簡單的目標檔案複製的過程,但是在實際過程中,很有可能存在使用者輸入的資料夾已經存在了的情況,所以我們需要詢問使用者是要覆蓋原資料夾內容還是退出重新操作。這一塊的操作我們使用 inquirer 來完成,inquirer 可以提供命令列的使用者互動功能。
我們在建立空資料夾之前加入一下判斷檔案是否存在的程式碼。
// 判斷資料夾是否存在
if (fs.existsSync(targetProjectPath)) {
console.log(`資料夾 ${projectName} 已經存在!`)
try {
// 若存在,則詢問使用者是否覆蓋當前資料夾的內容,yes 則覆蓋,no 則退出。
const { isCover } = await inquirer.prompt([
{ name: 'isCover', message: '是否要覆蓋當前資料夾的內容', type: 'confirm' }
])
if (!isCover) {
return
}
} catch (error) {
console.log('專案初始化失敗,已退出!')
return
}
}
複製程式碼
請注意這裡使用了
async - await
。
4. 美化命令列 console
現在的命令列都是單調的白色字,我們使用 chalk 和 log-symbols 來實現命令列的美化。主要程式碼如下:
主要改了
console
部分的程式碼,使用 log-symbols 新增輸出標識, chalk 改變文字顏色。
action(async name => {
// 判斷使用者是否輸入應用名稱,如果沒有設定為 myApp
const projectName = name || 'myApp'
// 獲取 template 資料夾路徑
const sourceProjectPath = __dirname + '/template'
// 獲取命令所在資料夾路徑
// path.resolve(name) == process.cwd() + '/' + name
const targetProjectPath = path.resolve(projectName)
// 判斷資料夾是否存在及其後續邏輯
if (fs.existsSync(targetProjectPath)) {
console.log(symbols.info, chalk.blue(`資料夾 ${projectName} 已經存在!`))
try {
const { isCover } = await inquirer.prompt([
{ name: 'isCover', message: '是否要覆蓋當前資料夾的內容', type: 'confirm' }
])
if (!isCover) {
return
}
} catch (error) {
console.log(symbols.fail, chalk.red('專案初始化失敗,已退出!'))
return
}
}
// 建立一個空的資料夾
fs.emptyDirSync(targetProjectPath)
try {
// 將模板資料夾中的內容複製到目標資料夾(目標資料夾為命令輸入所在資料夾)
fs.copySync(sourceProjectPath, targetProjectPath)
console.log(symbols.success, chalk.green('已經成功拷貝 Template 資料夾下所有檔案!'))
} catch (err) {
console.error(symbols.fail, chalk.red('專案初始化失敗,已退出!'))
return
}
})
複製程式碼
美化前:
美化後:
5. 修改 package.json
有些時候,我們需要根據使用者輸入來修改填充 package.json
,就像 npm init
的時候輸入的資訊。在這裡我們使用 inquirer 獲取使用者輸入,使用 handlebars 來將使用者輸入填充到 package.json
中去。
在拷貝資料夾後加入以下程式碼:
// 獲取專案的描述及作者名稱等資訊
const { projectDescription, projectAuthor } = await inquirer.prompt([
{ name: 'projectDescription', message: '請輸入專案描述' },
{ name: 'projectAuthor', message: '請輸入作者名字' }
])
const meta = {
projectAuthor,
projectDescription,
projectName
}
// 獲取拷貝後的模板專案中的 `package.json`
const targetPackageFile = targetProjectPath + '/package.json'
if (fs.pathExistsSync(targetPackageFile)) {
// 讀取檔案,並轉換成字串模板
const content = fs.readFileSync(targetPackageFile).toString()
// 利用 handlebars 將需要的內容寫入到模板中
const result = handlebars.compile(content)(meta)
fs.writeFileSync(targetPackageFile, result)
} else {
console.log('package.json 檔案不存在:' + targetPackageFile)
}
複製程式碼
至此,我們的簡易腳手架已經基本搭建完成了,能夠在指定資料夾生成專案模板檔案。但是,我們如果使用 create-react-app
的話,就會發現只要你一執行命令就會它幫你自動安裝依賴,而且也會自動初始化 Git
。
現在我們就來完成這兩個功能。
6. 安裝依賴
// 通過執行命令 yarn --version 的方式,來判斷本機是否已經安裝了 yarn
// 如果安裝了,後續就使用yarn,否則就使用 npm;
function canUseYarn() {
try {
spawn('yarnpkg', ['--version'])
return true
} catch (error) {
return false
}
}
function tryYarn(root) {
return new Promise((resolve, reject) => {
let child
const isUseYarn = canUseYarn()
if (isUseYarn) {
// 這裡就相當於命令列中執行 `yarn`
child = spawn('yarnpkg', ['--cwd', root], { stdio: 'inherit' })
} else {
// 這裡就相當於命令列中執行 `npm install`
child = spawn('npm', ['install'], { cwd: root, stdio: 'inherit' })
}
// 當命令執行完成的時候,判斷是否執行成功,並輸出相應的輸出。
child.on('close', code => {
if (code !== 0) {
reject(console.log(symbols.error, chalk.red(isUseYarn ? 'yarn' : 'npm' + ' 依賴安裝失敗...')))
return
}
resolve(console.log(symbols.success, chalk.green(isUseYarn ? 'yarn' : 'npm' + ' 依賴安裝完成!')))
})
})
}
複製程式碼
這裡需要注意的是執行命令語句
spawn('yarnpkg', ['--cwd', root], { stdio: 'inherit' })
。上述語句相當於命令列中執行
yarn
,但是我們必須加上'--cwd'
來將其執行路徑修改為命令所在的目錄,因為spawn
預設執行目錄是腳手架目錄。同時又因為spawn
是開了一個子執行緒,所以如果你不使用{ stdio: 'inherit' }
,那麼你將看不到yarn
安裝的過程。參考部落格:Node.js child_process模組解讀
stdio
選項用於配置父程式和子程式之間建立的管道,由於stdio
管道有三個(stdin
,stdout
,stderr
)因此stdio
的三個可能的值其實是陣列的一種簡寫
pipe
相當於['pipe', 'pipe', 'pipe']
(預設值)ignore
相當於['ignore', 'ignore', 'ignore']
inherit
相當於[process.stdin, process.stdout, process.stderr]
然後在修改 package.json
程式碼後面新增以下程式碼即可。
// 安裝依賴
await tryYarn(targetProjectPath)
複製程式碼
7. 初始化 Git
然後我們進行git的初始化,即執行 git init
。
function tryInitGit(root) {
// 原本模板中,我們就存放了 gitignore 模板檔案,需要將其內容複製到新建的 .gitignore 檔案中
try {
// 如果專案中存在了 .gitignore 檔案,那麼這個 API 會執行失敗,跳入 catch 分支進行合併操作
fs.moveSync(path.join(root, 'gitignore'), path.join(root, '.gitignore'))
} catch (error) {
const content = fs.readFileSync(path.join(root, 'gitignore'))
fs.appendFileSync(path.join(root, '.gitignore'), content)
} finally {
// 移除 gitignore 模板檔案
fs.removeSync(path.join(root, 'gitignore'))
}
try {
spawn('git', ['init'], { cwd: root })
spawn('git', ['add .'], { cwd: root })
spawn('git', ['commit', '-m', 'Initial commit from New App'], { cwd: root })
console.log(symbols.success, chalk.green('Git 初始化完成!'))
} catch (error) {
console.log(symbols.error, chalk.red('Git 初始化失敗...'))
}
}
複製程式碼
然後在安裝依賴之後加入以下程式碼:
// 初始化 git
tryInitGit(targetProjectPath)
複製程式碼
總結
本文程式碼倉庫:github.com/Huanqiang/s…
本文總結了個人在搭建簡易腳手架的過程,功能過於簡單,算是一個小小的開端吧。
最後不由感嘆 nodejs
還是非常之強悍的!