在項目比較多並且雜的環境下,有時候咱們想統一一下各個項目技術棧或者一些插件/組件的封裝習慣,可是每次從零開發一個新項目的時候,老是會重複作一些相似於複製粘貼的工做,這是一個很頭疼的事情,因此各類各樣的腳手架應用而生。
腳手架也就是爲了方便咱們作一些重複的事情,快速搭建一個基本的完整的項目結構。例如:vue-cli, react-cli, express-generator前端
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/vue-init/vue init這些命令開始,爲何能夠在終端中直接使用這些命令,這些命令的使用說明是怎麼打印出來的,還有vue-cli是怎樣在輸入vue init webpack demo1命令後,成功的在當前目錄建立出一個項目骨架的,這些項目是怎麼來的。git
//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文件,這裏只分析下第一個和第二個文件。
#!/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]
/** * 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) }) }) }
至此,還差最後一步,即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 }
按照以前所說的,若是要使用命令必需要在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 login 最後發佈, npm publish