vue-cli原始碼分析(試探篇)

飯妖精發表於2019-03-04

引言:近期在研究怎麼通過node命令生成自己想要的資料夾結構,因為大佬說vue-cli的原理和這個很像,所以抽時間研究了一下,並分享自己的研究心得。希望我的初學經驗對你有用。


1.vue init命令是怎麼實現的

vue-cli可以通過命令vue init webpack my-project執行

拿到一個專案配置,首先看的是package.json

"bin": {
    "vue": "bin/vue",
    "vue-init": "bin/vue-init",
    "vue-list": "bin/vue-list",
    "vue-build": "bin/vue-build"
}複製程式碼

bin項用來指定各個內部命令對應的可執行檔案的位置。所以,vue可以執行檔案bin/vue,讓我們看看bin/vue裡面是什麼?

// file: ./bin/vue
require(`commander`)
  //版本號
  .version(require(`../package`).version)
  .usage(`<command> [options]`)
  .command(`init`, `generate a new project from a template`)
  .command(`list`, `list available official templates`)
  .command(`build`, `prototype a new project`)
  .parse(process.argv)複製程式碼

於是,找到了commander,8000多star,github地址請點這裡

正常來說,command的用法有三個引數,如下

program
   .command(`aaa`)
   .description(`do something`)
   //aaa命令呼叫是回撥action函式
   .action(function(env) {
     console.log(`deploying "%s"`, env);
   });複製程式碼

而vue-cli裡面並沒有顯示呼叫action(fn)時,則將會使用子命令模式。Commander 將嘗試在入口指令碼的目錄中搜尋可執行檔案,(像./bin/vue)與名稱 program-command,像 vue-init,vue-build。

.command(`init`)命令則會找到同資料夾的vue-init,所以會呼叫檔案”bin/vue-init”並執行。

然後vue init就可以解釋了,那麼vue init webpack my-project 怎麼執行?往下看

2.vue-init檔案解析

頭部模組
#!/usr/bin/env node

//從倉庫下載並提取git儲存庫(GitHub,GitLab,Bitbucket)。
var download = require(`download-git-repo`)
//主要用於建立子命令和切割命令列引數並執行
var program = require(`commander`)
//檢查檔案是否存在
var exists = require(`fs`).existsSync
//路徑模組提供用於處理檔案和目錄路徑的實用程式。 比如路徑分割,檔案路徑格式化,json格式化等
var path = require(`path`)
//漂亮的loding
var ora = require(`ora`)
//獲取使用者主目錄的路徑
var home = require(`user-home`)
//絕對路徑轉換為相對路徑
var tildify = require(`tildify`)
//美化
var chalk = require(`chalk`)
//常用的互動式命令列使用者介面的集合。表現是控制檯輸出提問
var inquirer = require(`inquirer`)
var rm = require(`rimraf`).sync
var logger = require(`../lib/logger`)
//輸出資訊
var generate = require(`../lib/generate`)
var checkVersion = require(`../lib/check-version`)
var warnings = require(`../lib/warnings`)
var localPath = require(`../lib/local-path`)

var isLocalPath = localPath.isLocalPath
var getTemplatePath = localPath.getTemplatePath複製程式碼
主體部分
//help部分太簡單跳過
/***
    用法舉例:
    1.path.relative(`/data/orandea/test/aaa`, `/data/orandea/impl/bbb`);
    Returns: `../../impl/bbb`

    2.path.resolve(`wwwroot`, `static_files/png/`, `../gif/image.gif`);
    如果當前路徑是/home/myself/node,
    Returns: `/home/myself/node/wwwroot/static_files/gif/image.gif`

    3.path.join(`/foo`, `bar`, `baz/asdf`, `quux`, `..`);
    Returns: `/foo/bar/baz/asdf`
*/

/**
 * 配置
 */
//假設是vue init webpack my-project,第一個引數是webpack
var template = program.args[0]
var hasSlash = template.indexOf(`/`) > -1
var rawName = program.args[1]
var inPlace = !rawName || rawName === `.`

//path.relative()方法根據當前工作目錄返回相對路徑。 
//如果從每個解析到相同的路徑(在每個路徑上呼叫path.resolve()之後),返回零長度的字串。
var name = inPlace ? path.relative(`../`, process.cwd()) : rawName

//合併路徑
var to = path.resolve(rawName || `.`)
var clone = program.clone || false
//path.join()方法使用平臺特定的分隔符作為分隔符將所有給定的路徑段連線在一起,    
//然後對結果路徑進行規範化。
//home輸出舉例 => /Users/admin, tmp => /Users/admin/.vue-templates/webpack
var tmp = path.join(home, `.vue-templates`, template.replace(///g, `-`))
//如果是線下,則template直接取這個路徑,否則需要去線上倉庫下載
if (program.offline) {
  console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
  template = tmp
}


/**
 * Padding.
 */

console.log()
process.on(`exit`, function () {
  console.log()
})

if (exists(to)) {
    //詢問選擇
  inquirer.prompt([{
    type: `confirm`,
    message: inPlace
      ? `Generate project in current directory?`
      : `Target directory exists. Continue?`,
    name: `ok`
  }], function (answers) {
    if (answers.ok) {
      run()
    }
  })
} else {
  run()
}複製程式碼

講run函式之前,要先了解下generate命令的呼叫檔案–lib/generate檔案,裡面用到了metalsmith,github地址請點這裡

/**
 * Check, download and generate the project.
 */

function run () {
  // 檢查要下載的模版是否是本地路徑
  if (isLocalPath(template)) {
    var templatePath = getTemplatePath(template)
    //資訊輸出
    if (exists(templatePath)) {
      generate(name, templatePath, to, function (err) {
        if (err) logger.fatal(err)
        console.log()
        logger.success(`Generated "%s".`, name)
      })
    } else {
      logger.fatal(`Local template "%s" not found.`, template)
    }
  } else {
      //檢查版本並輸出資訊
    checkVersion(function () {
      if (!hasSlash) {
        // use official templates
        var officialTemplate = `vuejs-templates/` + template
        if (template.indexOf(`#`) !== -1) {
          downloadAndGenerate(officialTemplate)
        } else {
          if (template.indexOf(`-2.0`) !== -1) {
            warnings.v2SuffixTemplatesDeprecated(template, inPlace ? `` : name)
            return
          }

          // warnings.v2BranchIsNowDefault(template, inPlace ? `` : name)
          downloadAndGenerate(officialTemplate)
        }
      } else {
        downloadAndGenerate(template)
      }
    })
  }
}

/**
 * 從模版倉庫下載模版
 *
 * @param {String} template
 */

function downloadAndGenerate (template) {
  //啟動控制檯loading
  var spinner = ora(`downloading template`)
  spinner.start()
  // 如果存在本地模版則移除
  if (exists(tmp)) rm(tmp)
  //下載
  download(template, tmp, { clone: clone }, function (err) {
    spinner.stop()
    //日誌
    if (err) logger.fatal(`Failed to download repo ` + template + `: ` + err.message.trim())
    //控制檯列印出模版資訊,比如 Generated "my-project".
    generate(name, tmp, to, function (err) {
      if (err) logger.fatal(err)
      console.log()
      logger.success(`Generated "%s".`, name)
    })
  })
}複製程式碼

3.總結

vue init webpack my-project 的執行大致如下:

1.通過program.argv拿到引數[webpack,my-project]

2.根據引數通過拼接、path處理等操作,拿到下載模版路徑,然後根據本地和線上的區別,做不同的操作去下載即可。複製程式碼

以上有說得不對的地方,還請大家多多指教,共同進步。

參考資料:

【1】html-js.site/2017/05/26/…

相關文章