什么是腳手架
在項目比較多而且雜的環境下,有時候我們想統一一下各個項目技術棧或者一些插件/組件的封裝習慣,但是每次從零開發一個新項目的時候,總是會重復做一些類似於復制粘貼的工作,這是一個很頭疼的事情,所以各種各樣的腳手架應用而生。
腳手架也就是為了方便我們做一些重復的事情,快速搭建一個基本的完整的項目結構。例如:vue-cli, react-cli, express-generator
以vue-cli為例
1.全局安裝vue-cli
npm install vue-cli -g
2.然后在終端中鍵入vue,vue init或者vue-init命令,會出現以下的命令說明:
xwkdeMacBook-Pro:bin xwk$ vue
Usage: vue <command> [options]
Options:
-V, --version output the version number
-h, --help output usage information
Commands:
init generate a new project from a template
list list available official templates
build prototype a new project
create (for v3 warning only)
help [cmd] display help for [cmd]
xwkdeMacBook-Pro:bin xwk$ vue init
Usage: vue-init <template-name> [project-name]
Options:
-c, --clone use git clone
--offline use cached template
-h, --help output usage information
Examples:
# create a new project with an official template
$ vue init webpack my-project
# create a new project straight from a github template
$ vue init username/repo my-project
可以根據這些命令說明,來快速生成一個項目骨架,例如:vue init webpack demo1
xwkdeMacBook-Pro:Practice xwk$ vue init webpack demo1
? Project name: demo1
? Project description: A Vue.js project
? Author xuweikang: <xuweikang@dajiazhongyi.com>
? Vue build standalone:
? Install vue-router? Yes
? Use ESLint to lint your code? No
? Set up unit tests? No
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recomme
nded) npm
vue-cli · Generated "demo1".
# Installing project dependencies ...
如上圖所示,在輸入vue init指令的時候,會有一些選項讓我們去選擇,選擇完成后在當前目錄就會多出一個demo1的文件夾,這就是腳手架生成的項目骨架,到目前為止,已經成功的使用腳手架工具創建出了一個我們半自定義(一些自定義配置選項是我們在剛開始選擇的那些,腳手架會將這些選項應用到初始化項目中)的項目。
vue-cli的原理分析
對於vue-cli的原理分析,其實無外乎有幾個大點,首先從我剛開始在終端中輸入vue/vue-init/vue init這些命令開始,為什么可以在終端中直接使用這些命令,這些命令的使用說明是怎么打印出來的,還有vue-cli是怎樣在輸入vue init webpack demo1命令后,成功的在當前目錄創建出一個項目骨架的,這些項目是怎么來的。
- 可執行的npm包
如果我們想讓一個模塊全局可執行,就需要把這個模塊配置到PATH路徑下,npm讓這個工作變得很簡單,通過在package.json文件里面配置bin屬性,這樣該模塊在安裝的時候,若是全局安裝,則npm會為bin里面的文件在PATH目錄下配置一個軟鏈接,若是局部安裝,則會在項目里面的node_modules/.bin目錄下創建一個軟鏈接,例如:
//package.json
{
"bin": {
"cvd": "bin/cvd"
}
}
當我們安裝這個模塊的時候,npm就會為bin下面的cvd文件在/usr/local/bin創建一個軟鏈接。在Mac系統下,usr/local/bin這個目錄,是一個已經包含在環境變量里的目錄,可以直接在終端中執行這里的文件。
注意:windows下bin目錄不一樣,如果是直接在本地項目中進行包調試,可以通過npm link命令,將本項目的bin目錄鏈接到全局目錄里,這里面也可以看到對應的bin目錄。
xwkdeMacBook-Pro:vue-cli-demo1 xwk$ npm link
npm WARN vue-cli-demo1@1.0.0 No description
npm WARN vue-cli-demo1@1.0.0 No repository field.
audited 1 package in 1.35s
found 0 vulnerabilities
/usr/local/bin/cvd -> /usr/local/lib/node_modules/vue-cli-demo1/bin/cvd
/usr/local/bin/cvd-init -> /usr/local/lib/node_modules/vue-cli-demo1/bin/cvd-init
/usr/local/lib/node_modules/vue-cli-demo1 -> /Users/admin/Project/vue-cli-demo1
到目前為止可以解釋了為什么我們在全局install了vue-cli后,可以直接使用vue/vue-init等命令。
2.vue-cli源碼分析
找到vue-cli源碼進行分析,有兩種方法,可以直接去找剛剛安裝的腳手架的位置,這里是全局安裝的,mac會定位到/usr/local/lib/node_modules/vue-cli,或者直接看vue-cli的倉庫源碼,點擊 這里
有了上面的分析,直接找到package.json,可以看到:
{
"bin": {
"vue": "bin/vue",
"vue-init": "bin/vue-init",
"vue-list": "bin/vue-list"
}
}
這里面定義了3個可執行文件命令,vue,vue-init和vue-list,分別對應到了bin目錄下的vue,vue-init,vue-lsit文件,這里只分析下第一個和第二個文件。
bin/vue
#!/usr/bin/env node //聲明下該文件要用node格式打開
const program = require('commander') //ti大神的nodejs命令行庫
program
.version(require('../package').version) //取包的版本為當前版本
.usage('<command> [options]') //定義使用方法
.command('init', 'generate a new project from a template') //有一個init方法,並且對其進行描述
.command('list', 'list available official templates') //有一個list方法,並且對其進行描述
.command('build', 'prototype a new project') //有一個build方法,並且對其進行描述
.command('create', '(for v3 warning only)') //有一個create方法,並且對其進行描述
program.parse(process.argv) //執行
效果如下:
xwkdeMacBook-Pro:bin xwk$ vue
Usage: vue <command> [options]
Options:
-V, --version output the version number
-h, --help output usage information
Commands:
init generate a new project from a template
list list available official templates
build prototype a new project
create (for v3 warning only)
help [cmd] display help for [cmd]
bin/vue-init
/**
* Usage.
*/
program
.usage('<template-name> [project-name]')
.option('-c, --clone', 'use git clone')
.option('--offline', 'use cached template')
/**
* Help.
*/
program.on('--help', () => {
console.log(' Examples:')
console.log()
console.log(chalk.gray(' # create a new project with an official template'))
console.log(' $ vue init webpack my-project')
console.log()
console.log(chalk.gray(' # create a new project straight from a github template'))
console.log(' $ vue init username/repo my-project')
console.log()
})
這部分主要是聲明一些命令和使用方法介紹,其中chalk 是一個可以讓終端輸出內容變色的模塊。
下面這部分主要是一些變量的獲取,定義項目名稱,輸出路徑,以及本地存放模板的路徑位置
/**
* Settings.
*/
//vue init 命令后的第一個參數,template路徑
let template = program.args[0]
//template中是否帶有路徑標識
const hasSlash = template.indexOf('/') > -1
//第二個參數是項目名稱,如果沒聲明的話或者是一個“.”,就取當前路徑的父目錄名字
const rawName = program.args[1]
const inPlace = !rawName || rawName === '.'
const name = inPlace ? path.relative('../', process.cwd()) : rawName
//輸出路徑
const to = path.resolve(rawName || '.')
const clone = program.clone || false
//存放template的地方,用戶主目錄/.vue-templates ,我這里是/Users/admin/.vue-templates/
const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
//如果是離線狀態,模板路徑就取本地的
if (program.offline) {
console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
template = tmp
}
下面是一些對於項目初始化簡單的問答提示,其中inquirer 是一個node在命令行中的問答模塊,你可以根據答案去做不同的處理
if (inPlace || exists(to)) {
inquirer.prompt([{
type: 'confirm',
message: inPlace
? 'Generate project in current directory?'
: 'Target directory exists. Continue?',
name: 'ok'
}]).then(answers => {
if (answers.ok) {
run()
}
}).catch(logger.fatal)
} else {
run()
}
接下來是下載模板的具體邏輯,如果是本地模板,則直接生成
function run () {
// check if template is local
if (isLocalPath(template)) {
//獲取模版地址
const templatePath = getTemplatePath(template)
if (exists(templatePath)) {
//開始生成模板
generate(name, templatePath, to, err => {
if (err) logger.fatal(err)
console.log()
logger.success('Generated "%s".', name)
})
} else {
logger.fatal('Local template "%s" not found.', template)
}
} else {
checkVersion(() => {
//路徑中是否包含 ‘/’,如果包含 ‘/’,則直接去指定模板路徑去下載
if (!hasSlash) {
// use official templates
//生產倉庫里面的模板路徑
const officialTemplate = 'vuejs-templates/' + template
if (template.indexOf('#') !== -1) {
//下載倉庫分支代碼
downloadAndGenerate(officialTemplate)
} else {
if (template.indexOf('-2.0') !== -1) {
//如果存在 ‘-2.0’ 標識,則會輸出模板廢棄的警告並退出
warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
return
}
// warnings.v2BranchIsNowDefault(template, inPlace ? '' : name)
//開始下載
downloadAndGenerate(officialTemplate)
}
} else {
downloadAndGenerate(template)
}
})
}
}
vue-init源碼的最后一部分,是對downloadAndGenerate方法的聲明,是下載並在本地生產項目的具體邏輯。
download是download-git-repo模塊的方法,是來做從git倉庫下載代碼的,
//第一個參數是倉庫地址,如果指定分支用“#”分開,第二個為輸出地址,第三個為是否clone,為flase的話就下載zip,
//第四個參數是回調
download('flipxfx/download-git-repo-fixture#develop', 'test/tmp',{ clone: true }, function (err) {
console.log(err ? 'Error' : 'Success')
})
function downloadAndGenerate (template) {
//ora庫在終端中顯示加載動畫
const spinner = ora('downloading template')
spinner.start()
// Remove if local template exists
//如果有相同文件夾,則覆蓋刪除
if (exists(tmp)) rm(tmp)
download(template, tmp, { clone }, err => {
spinner.stop()
if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
//生成個性化內容
generate(name, tmp, to, err => {
if (err) logger.fatal(err)
console.log()
logger.success('Generated "%s".', name)
})
})
}
lib/generate.js
至此,還差最后一步,即generate方法的定義,這個方法在lib/generate.js中,主要作用是用於模板生成
/**
* Generate a template given a `src` and `dest`.
*
* @param {String} name
* @param {String} src
* @param {String} dest
* @param {Function} done
*/
module.exports = function generate (name, src, dest, done) {
//設置meta.js/meta.json配置文件的name字段和auther字段為項目名和git用戶名,同時還設置了校驗npm包名的方法屬性
const opts = getOptions(name, src)
//指定Metalsmith的模板目錄路徑
const metalsmith = Metalsmith(path.join(src, 'template'))
//將metalsmith的默認metadata和新增的3個屬性合並起來
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd(),
noEscape: true
})
//注冊一些其他的渲染器,例如if_or/template_version
opts.helpers && Object.keys(opts.helpers).map(key => {
Handlebars.registerHelper(key, opts.helpers[key])
})
const helpers = { chalk, logger }
if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
opts.metalsmith.before(metalsmith, opts, helpers)
}
//metalsmith做渲染的時候定義了一些自定義插件
//askQuestions是調用inquirer庫詢問了一些問題,並把回答結果放到metalsmithData中
//生產靜態文件時刪除一些不需要的文件,meta文件的filters字段中進行條件設置
//開始生產文件
metalsmith.use(askQuestions(opts.prompts))
.use(filterFiles(opts.filters))
.use(renderTemplateFiles(opts.skipInterpolation))
if (typeof opts.metalsmith === 'function') {
opts.metalsmith(metalsmith, opts, helpers)
} else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
opts.metalsmith.after(metalsmith, opts, helpers)
}
metalsmith.clean(false)
.source('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
.destination(dest)
.build((err, files) => {
done(err)
if (typeof opts.complete === 'function') {
const helpers = { chalk, logger, files }
opts.complete(data, helpers)
} else {
logMessage(opts.completeMessage, data)
}
})
return data
}
為什么vue init命令也可以?
按照之前所說的,如果要使用命令必須要在package.json 里面的bin字段進行說明,看到只有vue,vue-init,vue-list,如果是vue init 是使用了vue的命令的話,那么init肯定是作為一個參數傳入的,bin/vue里面也並沒有關於對init參數的具體執行,只有一些簡單的參數說明。
也可以注意到,在命令行中敲入vue init和vue-init 是同樣的效果。其實,兩個命令是同一個邏輯,具體要看commander readme里面有這樣一段話:
When .command() is invoked with a description argument, no .action(callback) should be called to handle sub-commands, otherwise there will be an error. This tells commander that you're going to use separate executables for sub-commands, much like git(1) and other popular tools.
The commander will try to search the executables in the directory of the entry script (like ./examples/pm) with the name program-command, like pm-install, pm-search.
總結一下
前端腳手架的開發,依靠的是npm的全局包安裝,在package.json里面的bin字段指定命令名字和對應的腳本處理,以鍵值對的方式聲明。npm包在全局安裝的時候,npm會將bin里面的命令,在PATH目錄里面創建一個軟連接,使得可以直接在終端里使用這些指令。如果是本地開發的npm包,可以通過npm link手動鏈接到PATH目錄。
對於vue-cli,會在初始化的時候去模板倉庫下載對應的模板,然后通過收錄一些問題,把這些用戶定制化的信息更新到meta.js中,metalsmith做渲染的時候,拿到meta.js里面的配置數據,生成一個最終的靜態骨架。
發布到npm
在本地開發完腳手架后,需要把對應的包放到npm倉庫中供其他人下載使用。
首先去npm倉庫注冊一個npm賬號,
然后在本地包目錄下登陸npm, npm login
最后發布, npm publish
