基於vue的腳手架開發與發佈到npm倉庫

什麼是腳手架

在項目比較多並且雜的環境下,有時候咱們想統一一下各個項目技術棧或者一些插件/組件的封裝習慣,可是每次從零開發一個新項目的時候,老是會重複作一些相似於複製粘貼的工做,這是一個很頭疼的事情,因此各類各樣的腳手架應用而生。
腳手架也就是爲了方便咱們作一些重複的事情,快速搭建一個基本的完整的項目結構。例如:vue-cli, react-cli, express-generator前端

以vue-cli爲例

1.全局安裝vue-clivue

npm install vue-cli -g

2.而後在終端中鍵入vue,vue init或者vue-init命令,會出現如下的命令說明:node

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 demo1react

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的文件夾,這就是腳手架生成的項目骨架,到目前爲止,已經成功的使用腳手架工具建立出了一個咱們半自定義(一些自定義配置選項是咱們在剛開始選擇的那些,腳手架會將這些選項應用到初始化項目中)的項目。webpack

vue-cli的原理分析

對於vue-cli的原理分析,其實無外乎有幾個大點,首先從我剛開始在終端中輸入vue/vue-init/vue init這些命令開始,爲何能夠在終端中直接使用這些命令,這些命令的使用說明是怎麼打印出來的,還有vue-cli是怎樣在輸入vue init webpack demo1命令後,成功的在當前目錄建立出一個項目骨架的,這些項目是怎麼來的。git

  1. 可執行的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目錄。github

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等命令。web

2.vue-cli源碼分析vue-router

找到vue-cli源碼進行分析,有兩種方法,能夠直接去找剛剛安裝的腳手架的位置,這裏是全局安裝的,mac會定位到/usr/local/lib/node_modules/vue-cli,或者直接看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

相關文章
相關標籤/搜索