從 vue-cli 源碼學習如何寫模板

Miguel0563 7年前發布 | 23K 次閱讀 Vue.js Vue.js開發

vue-cli vuejs 官方提供的基于 vuejs 的項目腳手架工具, 可以很快的幫助 vuejs 開發者搭建一個 startup 項目, 免去環境配置的繁瑣, 開箱即用. 今天就來看下 vue-cli 的實現.

vue-cli 的版本是 2.8.2

vue-init

vue init 是基于第三方模板生成項目的命令. 先看下其整體流程:

首先, vue cli 獲取到輸入的參數:

# vue-cli/bin/vue-init
// ...
var template = program.args[0]
var hasSlash = template.indexOf('/') > -1
var rawName = program.args[1]
// ...

之后, 會先判斷用戶是否輸入了 offline 選項. 如果有, 則會使用之前緩存的模板:

# vue-cli/bin/vue-init
// ...
var tmp = path.join(home, '.vue-templates', template.replace(/\//g, '-'))
if (program.offline) {
  console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
  template = tmp
}
// ...

如果沒有, 則判斷將會生成的項目目錄是否存在. 若存在, 則會向用戶確認是否在當前目錄生成項目( 代碼在這 ); 若不存在, 之后就會生成一個新的目錄.

然后, 會去判斷使用的模板是否是本地的, 是本地且存在則使用本地模板生成項目, 反之使用線上模板生成項目( 代碼在這 ).

在判斷是使用線上的模板之后, 會根據模板名是否帶 / 判斷是使用官方提供的模板還是使用第三方模板( 代碼在這 ).

最后會調用 downloadAndGenerate 去下載官方模板或第三方模板來生成項目( 代碼在這 ). vue cli 對模板的下載依賴于 download-git-repo , 所以使用第三方模板時, 對指定模板的輸入要求可以見 download .

模板下載成功之后, vue cli 會調用 generate 來生成模板, 這是 cli 的核心模塊, 其源碼在 lib/generate.js 中. 接下來就具體分析 generate 模塊.

generate 模塊導出之前, 會先在 handlebars 中注冊兩個輔助函數: if_eq 和 unless_eq , 用于模板中的表達式判斷:

# vue-cli/lib/generate.js

//...

// register handlebars helper
Handlebars.registerHelper('if_eq', function (a, b, opts) {
  return a === b
    ? opts.fn(this)
    : opts.inverse(this)
})

Handlebars.registerHelper('unless_eq', function (a, b, opts) {
  return a === b
    ? opts.inverse(this)
    : opts.fn(this)
})

導出的 generate 函數接收四個參數: 項目目錄名、下載的模板的臨時路徑、項目目錄路徑和一個回調函數. 回調函數用于項目生成之后在終端輸出一些提示信息. 在 generate 函數內, 首先會讀取模板的 meta 信息, 讀取的 meta 信息來自于模板目錄下的 meta.{js,json} 文件 :

# vue-cli/lib/options.js
// ...
// dir 是模板下載成功之后的臨時路徑
var json = path.join(dir, 'meta.json')
var js = path.join(dir, 'meta.js')
var opts = {}

// ...

具體實現 戳此 . 之后會讀取用戶的 git 昵稱和郵箱用于設置 meta 信息的一些默認屬性.

得到基本的 meta 信息之后, 會利用 metalsmith 讀取 template 內容:

# vue-cli/lib/generate.js
// ...
// src 是模板下載成功之后的臨時路徑
var opts = getOptions(name, src)  
var metalsmith = Metalsmith(path.join(src, 'template'))

// ...

需要注意的是, 讀取的內容是模板的 tempalte 目錄. metalsmith 會返回文件路徑和文件內容相映射的對象, 這樣會方便 metalsmith 的中間件對文件進行處理.

之后, vue cli 使用了三個中間件來處理模板:

//vue-cli/lib/generate.js#L53-L55

metalsmith.use(askQuestions(opts.prompts))
    .use(filterFiles(opts.filters))
    .use(renderTemplateFiles(opts.skipInterpolation))

askQuestions

中間件 askQuestions 用于讀取用戶輸入:

function askQuestions (prompts) {
  return function (files, metalsmith, done) {
    ask(prompts, metalsmith.metadata(), done)
  }
}

ask 的源碼在 vue-cli/lib/ask.js 中, 其會遍歷 prompts , 在終端交互式的讀取用戶輸入, 并將數據保存在 global metadata 中, 便于后續依賴 global metadata 的中間件對模板進行進一步處理. prompts 是一個對象, 每個 prompt 都是一個 Inquirer.js question object . 示例如下:

// meta.{js,json}
{
    "prompts": {
        "name": {
            "type": "string",
            "required": true,
           "message" : "Project name"
        },
        "version": {
           "type": "input",
           "message": "project's version",
           "default": "1.0.0"
        }
    }
}

在 ask 中, 對 meta 信息中的 prompt 會有條件的咨詢用戶:

// vue-cli/lib/ask.js#prompt

inquirer.prompt([{
    type: prompt.type,
    message: prompt.message,
    default: prompt.default
    //...
}], function(answers) {
    // 保存用戶的輸入
})

經過 askQuestions 中間件處理之后, global metadata 是一個以 prompt 中的 key 為 key, 用戶的輸入為 value 的對象:

// global metadata
{
    name: 'test',
    version: '0.1.1'
    // ...
}

filterFiles

中間件 filterFiles 會根據 meta 信息中的 filters 都文件進行過濾:

function filterFiles (filters) {
  return function (files, metalsmith, done) {
    filter(files, filters, metalsmith.metadata(), done)
  }
}

filter 的源碼在 vue-cli/lib/filter.js 中:

module.exports = function (files, filters, data, done) {
  // 沒有 filters 直接返回
  if (!filters) {
    return done()
  }

  // 獲取所有的文件名(即路徑, eg: test/**)
  var fileNames = Object.keys(files)

  // 遍歷 filters
  Object.keys(filters).forEach(function (glob) {
    fileNames.forEach(function (file) {
      if (match(file, glob, { dot: true })) {
        // 獲取到匹配的值
        var condition = filters[glob]
        if (!evaluate(condition, data)) {
          // 刪除文件
          delete files[file]
        }
      }
    })
  })
  done()
}

evaluate 用于執行 js 表達式, 關鍵定義如下:

// vue-cli/lib/eval.js

var fn = new Function('data', 'with (data) { return ' + exp + '}')

所以在 filters 中, 可以將某些 key 的 value 定義為一個 js 表達式.

renderTemplateFiles

根據用戶的輸入過濾掉不需要的文件之后, 就可以利用 renderTemplateFiles 中間件來渲染模板了:

// vue-cli/lib/generate.js#renderTemplateFiles

// ...
var render = require('consolidate').handlebars.render
var async = require('async')
// ...

function renderTemplateFiles(//...){
    return function (files, metalsmith, done) {
        var keys = Object.keys(files)
        var metalsmithMetadata = metalsmith.metadata()

        // 遍歷 keys
        async.each(keys, function(file, next){
            // 讀取文件內容
            var str = files[file].contents.toString()

            // 不渲染不含mustaches表達式的文件
            if (!/{{([^{}]+)}}/g.test(str)) {
                return next()
            }

            // 調用 handlebars 渲染文件
            render(/* 渲染文件 */)
             })
    }
}

渲染完成之后, metalsmith 會將最終結果 build 的 dest 目錄. 若失敗, 則將 err 傳給回調輸出; 反之, 如果 meta 信息有 complete (函數) 或者 completeMessage (字符串), 則會進行調用或輸出:

// vue-cli/lib/generate.js

// ...
var opts = getOptions(name, src)

// ...

if (typeof opts.complete === 'function') {
    var helpers = {chalk, logger, files}
    opts.complete(data, helpers)
} else {
    logMessage(opts.completeMessage, data)
}

// ...

vue-list

vue list 命令用于查看官方提供的模板列表, 源碼在 vue-cli/bin/vue-list 中, 關鍵代碼如下:

// ...
var request = require('request')

//...

request({
    url: 'https://api.github.com/users/vuejs-templates/repos',
   headers: {
     'User-Agent': 'vue-cli'
   }
}, function(err, res, body) {
    // 在終端輸出列表
})

需要注意的是, Github Api 對未認證的請求是有請求數限制的, 超過限制則會報錯, 但可以通過 BA 認證的方式來提高請求數限制, 具體可以 戳此 .

這是個潛在的問題, 已經有 vue-cli 的用戶碰到過認證失敗的問題: #368 . vue-cli 的下一個版本可能會解決這個問題, 已經有社區用戶提出 PR .

怎么自己寫模板呢

從上述的分析可以知道, 模板是有特定的目錄結構的:

  • 模板倉庫的根目錄下必須有 template 目錄, 在該目錄下定義你的模板文件
  • 模板倉庫的根目錄下必須有 meta.{js,json} 文件, 該文件必須導出為一個對象, 用于定義模板的 meta 信息

對于 meta.{js,json} 文件, 目前可定義的字段如下:

  • prompts<Object> : 收集用戶自定義數據
  • filters<Object> : 根據條件過濾文件
  • completeMessage<String> : 模板渲染完成后給予的提示信息, 支持 handlebars 的 mustaches 表達式
  • complete<Function> : 模板渲染完成后的回調函數, 優先于 completeMessage
  • helpers<Object> : 自定義的 Handlebars 輔助函數

prompts

prompts 是一個對象, 每個 prompt 都是一個 Inquirer.js question object . 示例如下:

// meta.{js,json}
{
    "prompts": {
        "name": {
            "type": "string",
            "required": true,
           "message" : "Project name"
        },
        "test": {
            "type": "confirm",
           "message" : "Unit test?"
        },
        "version": {
           "type": "input",
           "message": "project's version",
           "default": "1.0.0"
        }
    }
}

所有的用戶輸入完成之后, template 目錄下的所有文件將會用 Handlebars 進行渲染. 用戶輸入的數據會作為模板渲染時的使用數據:

// template/package.json

{{#test}}
"test": "npm run test"
{{/test}}

在上述示例中, 只有用戶在 test 中的回答值是 yes 時, test 腳本才會在 package.json 文件中生成.

prompt 可以添加一個 when 字段, 該字段表示此 prompt 會根據 when 的值來判斷是否出現在終端提示用戶進行輸入. 在 vue-cli 中, 其會根據 when 進行 eval 運算:

// ...

if (prompt.when && !evaluate(prompt.when, data)) {
    return done()
}

//...

帶 when 的 prompt 示例:

{
  "prompts": {
    "lint": {
        "type": "confirm",
        "message": ""Use ESLint to lint your code?"
    },
    "eslint": {
      "when": "lint",
      "type": "list",
      "message": "Pick a lint config",
      "choices": [
        "standard",
        "airbnb",
        "none"
      ]
    }
  }
}

在上述示例中, 只有用戶在 lint 中的回答值是 yes 時, eslint 才會被觸發, 在終端顯示讓用戶選擇 eslint 的配置規范.

filters

filters 字段是一個包含文件過濾規則的對象, 鍵用于定義符合 minimatch glob pattern 規則的過濾器, 鍵值是 prompts 中用戶的輸入值或者表達式. 例如:

{
  "prompts": {
      "unit": {
          "type": "confirm",
          "message": "Setup unit tests with Mocha?"
      }
  },  
  "filters": {
    "test/*": "unit"
  }
}

在上述示例中, template 目錄下 test 目錄只有用戶在 unit 中的回答值是 yes 時才會生成, 反之會被刪除.

如果要匹配以 . 開頭的文件, 則需要將 minimatch 的 dot 選項設置成 true .

helpers

helpers 字段是一個包含自定義的 Handlebars 輔助函數的對象, 自定義的函數可以在 template 中使用:

{
    "helpers": {
        "if_or": function (v1, v2, options) {
          if (v1 || v2) {
            return options.fn(this);
          }

          return options.inverse(this);
        }
    },
}

在 template 的文件使用該 if_or :

{{#if_or val1 val2}}
// 當 val1 或者 val2 為 true 時, 這里才會被渲染
{{/if_or}}

complete

在渲染完成后的 complete 回調:

{
    "complete": function(data, helpers) {}
}

data 和 helpers 由 vue cli 傳入:

// vue-cli/lib/generate.js

// ...
var data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd(),
    noEscape: true
})

// ...

// files 是 metalsmith build 之后的文件對象
var helpers = {chalk, logger, files}

// ...

如果 complete 有定義, 則調用 complete , 反之會輸出 completeMessage .

總結

vue-cli 的源碼還是很好分析的, 參考 vue-cli , 寫了一個簡化的腳手架工具 chare , 其新加了三個功能:

  • token 設置, 用于 Github Api 的 BA 認證
  • init project 時可以關聯一個遠程倉庫
  • 支持 prompt filter

自己針對日常使用的 vuejs 和 react 框架寫了一些 startup, 歡迎指正:

 

來自:https://github.com/dwqs/blog/issues/56

 

 本文由用戶 Miguel0563 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!