Vue-cli 原理實現


背景

在平時工作中會有遇到許多以相同模板定制的小程序,因此想自己建立一個生成模板的腳手架工具,以模板為基礎構建對應的小程序,而平時的小程序都是用mpvue框架來寫的,因此首先先參考一下Vue-cli的原理。知道原理之后,再定制自己的模板腳手架肯定是事半功倍的。


在說代碼之前我們首先回顧一下Vue-cli的使用,我們通常使用的是webpack模板包,輸入的是以下代碼。

vue init webpack [project-name]

在執行這段代碼之后,系統會自動下載模板包,隨后會詢問我們一些問題,比如模板名稱,作者,是否需要使用eslint,使用npm或者yarn進行構建等等,當所有問題我們回答之后,就開始生成腳手架項目。

我們將源碼下載下來,源碼倉庫點擊這里,平時用的腳手架還是2.0版本,要注意,默認的分支是在dev上,dev上是3.0版本。

我們首先看一下package.json,在文件當中有這么一段話

  1.  
    {
  2.  
    "bin": {
  3.  
    "vue": "bin/vue",
  4.  
    "vue-init": "bin/vue-init",
  5.  
    "vue-list": "bin/vue-list"
  6.  
    }
  7.  
    }

由此可見,我們使用的命令 vue init,應該是來自bin/vue-init這個文件,我們接下來看一下這個文件中的內容


bin/vue-init

  1.  
    const download = require('download-git-repo')
  2.  
    const program = require('commander')
  3.  
    const exists = require('fs').existsSync
  4.  
    const path = require('path')
  5.  
    const ora = require('ora')
  6.  
    const home = require('user-home')
  7.  
    const tildify = require('tildify')
  8.  
    const chalk = require('chalk')
  9.  
    const inquirer = require('inquirer')
  10.  
    const rm = require('rimraf').sync
  11.  
    const logger = require('../lib/logger')
  12.  
    const generate = require('../lib/generate')
  13.  
    const checkVersion = require('../lib/check-version')
  14.  
    const warnings = require('../lib/warnings')
  15.  
    const localPath = require('../lib/local-path')

download-git-repo 一個用於下載git倉庫的項目的模塊
commander 可以將文字輸出到終端當中
fs 是node的文件讀寫的模塊
path 模塊提供了一些工具函數,用於處理文件與目錄的路徑
ora 這個模塊用於在終端里有顯示載入動畫
user-home 獲取用戶主目錄的路徑
tildify 將絕對路徑轉換為波形路徑 比如/Users/sindresorhus/dev → ~/dev
inquirer 是一個命令行的回答的模塊,你可以自己設定終端的問題,然后對這些回答給出相應的處理
rimraf 是一個可以使用 UNIX 命令 rm -rf的模塊
剩下的本地路徑的模塊其實都是一些工具類,等用到的時候我們再來講


  1.  
    // 是否為本地路徑的方法 主要是判斷模板路徑當中是否存在 `./`
  2.  
    const isLocalPath = localPath.isLocalPath
  3.  
    // 獲取模板路徑的方法 如果路徑參數是絕對路徑 則直接返回 如果是相對的 則根據當前路徑拼接
  4.  
    const getTemplatePath = localPath.getTemplatePath
  1.  
    /**
  2.  
    * Usage.
  3.  
    */
  4.  
     
  5.  
    program
  6.  
    .usage(' <template-name> [project-name]')
  7.  
    .option('-c, --clone', 'use git clone')
  8.  
    .option('--offline', 'use cached template')
  9.  
     
  10.  
    /**
  11.  
    * Help.
  12.  
    */
  13.  
     
  14.  
    program.on('--help', () => {
  15.  
    console.log(' Examples:')
  16.  
    console.log()
  17.  
    console.log(chalk.gray(' # create a new project with an official template'))
  18.  
    console.log(' $ vue init webpack my-project')
  19.  
    console.log()
  20.  
    console.log(chalk.gray(' # create a new project straight from a github template'))
  21.  
    console.log(' $ vue init username/repo my-project')
  22.  
    console.log()
  23.  
    })
  24.  
     
  25.  
    /**
  26.  
    * Help.
  27.  
    */
  28.  
    function help () {
  29.  
    program.parse(process.argv)
  30.  
    if (program.args.length < 1) return program.help()
  31.  
    }
  32.  
    help()

這部分代碼聲明了vue init用法,如果在終端當中 輸入 vue init --help或者跟在vue init 后面的參數長度小於1,也會輸出下面的描述

  1.  
    Usage: vue-init <template-name> [project-name]
  2.  
     
  3.  
    Options:
  4.  
     
  5.  
    -c, --clone use git clone
  6.  
    --offline use cached template
  7.  
    -h, --help output usage information
  8.  
    Examples:
  9.  
     
  10.  
    # create a new project with an official template
  11.  
    $ vue init webpack my-project
  12.  
     
  13.  
    # create a new project straight from a github template
  14.  
    $ vue init username/repo my-project

接下來是一些變量的獲取

  1.  
    /**
  2.  
    * Settings.
  3.  
    */
  4.  
    // 模板路徑
  5.  
    let template = program.args[0]
  6.  
    const hasSlash = template.indexOf('/') > -1
  7.  
    // 項目名稱
  8.  
    const rawName = program.args[1]
  9.  
    const inPlace = !rawName || rawName === '.'
  10.  
    // 如果不存在項目名稱或項目名稱輸入的'.' 則name取的是 當前文件夾的名稱
  11.  
    const name = inPlace ? path.relative('../', process.cwd()) : rawName
  12.  
    // 輸出路徑
  13.  
    const to = path.resolve(rawName || '.')
  14.  
    // 是否需要用到 git clone
  15.  
    const clone = program.clone || false
  16.  
     
  17.  
    // tmp為本地模板路徑 如果 是離線狀態 那么模板路徑取本地的
  18.  
    const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
  19.  
    if (program.offline) {
  20.  
    console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
  21.  
    template = tmp
  22.  
    }

接下來主要是根據模板名稱,來下載並生產模板,如果是本地的模板路徑,就直接生成。

  1.  
    /**
  2.  
    * Check, download and generate the project.
  3.  
    */
  4.  
     
  5.  
    function run () {
  6.  
    // 判斷是否是本地模板路徑
  7.  
    if (isLocalPath(template)) {
  8.  
    // 獲取模板地址
  9.  
    const templatePath = getTemplatePath(template)
  10.  
    // 如果本地模板路徑存在 則開始生成模板
  11.  
    if (exists(templatePath)) {
  12.  
    generate(name, templatePath, to, err => {
  13.  
    if (err) logger.fatal(err)
  14.  
    console.log()
  15.  
    logger.success('Generated "%s".', name)
  16.  
    })
  17.  
    } else {
  18.  
    logger.fatal('Local template "%s" not found.', template)
  19.  
    }
  20.  
    } else {
  21.  
    // 非本地模板路徑 則先檢查版本
  22.  
    checkVersion(() => {
  23.  
    // 路徑中是否 包含'/'
  24.  
    // 如果沒有 則進入這個邏輯
  25.  
    if (!hasSlash) {
  26.  
    // 拼接路徑 'vuejs-tempalte'下的都是官方的模板包
  27.  
    const officialTemplate = 'vuejs-templates/' + template
  28.  
    // 如果路徑當中存在 '#'則直接下載
  29.  
    if (template.indexOf('#') !== -1) {
  30.  
    downloadAndGenerate(officialTemplate)
  31.  
    } else {
  32.  
    // 如果不存在 -2.0的字符串 則會輸出 模板廢棄的相關提示
  33.  
    if (template.indexOf('-2.0') !== -1) {
  34.  
    warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
  35.  
    return
  36.  
    }
  37.  
     
  38.  
    // 下載並生產模板
  39.  
    downloadAndGenerate(officialTemplate)
  40.  
    }
  41.  
    } else {
  42.  
    // 下載並生生成模板
  43.  
    downloadAndGenerate(template)
  44.  
    }
  45.  
    })
  46.  
    }
  47.  
    }

我們來看下 downloadAndGenerate這個方法

  1.  
    /**
  2.  
    * Download a generate from a template repo.
  3.  
    *
  4.  
    * @param {String} template
  5.  
    */
  6.  
     
  7.  
    function downloadAndGenerate (template) {
  8.  
    // 執行加載動畫
  9.  
    const spinner = ora('downloading template')
  10.  
    spinner.start()
  11.  
    // Remove if local template exists
  12.  
    // 刪除本地存在的模板
  13.  
    if (exists(tmp)) rm(tmp)
  14.  
    // template參數為目標地址 tmp為下載地址 clone參數代表是否需要clone
  15.  
    download(template, tmp, { clone }, err => {
  16.  
    // 結束加載動畫
  17.  
    spinner.stop()
  18.  
    // 如果下載出錯 輸出日志
  19.  
    if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
  20.  
    // 模板下載成功之后進入生產模板的方法中 這里我們再進一步講
  21.  
    generate(name, tmp, to, err => {
  22.  
    if (err) logger.fatal(err)
  23.  
    console.log()
  24.  
    logger.success('Generated "%s".', name)
  25.  
    })
  26.  
    })
  27.  
    }

到這里為止,bin/vue-init就講完了,該文件做的最主要的一件事情,就是根據模板名稱,來下載生成模板,但是具體下載和生成的模板的方法並不在里面。

下載模板

下載模板用的download方法是屬於download-git-repo模塊的。

最基礎的用法為如下用法,這里的參數很好理解,第一個參數為倉庫地址,第二個為輸出地址,第三個是否需要 git clone,帶四個為回調參數

  1.  
    download( 'flipxfx/download-git-repo-fixture', 'test/tmp',{ clone: true }, function (err) {
  2.  
    console.log(err ? 'Error' : 'Success')
  3.  
    })

在上面的run方法中有提到一個#的字符串實際就是這個模塊下載分支模塊的用法

  1.  
    download( 'bitbucket:flipxfx/download-git-repo-fixture#my-branch', 'test/tmp', { clone: true }, function (err) {
  2.  
    console.log(err ? 'Error' : 'Success')
  3.  
    })

生成模板

模板生成generate方法在generate.js當中,我們繼續來看一下


generate.js

  1.  
    const chalk = require('chalk')
  2.  
    const Metalsmith = require('metalsmith')
  3.  
    const Handlebars = require('handlebars')
  4.  
    const async = require('async')
  5.  
    const render = require('consolidate').handlebars.render
  6.  
    const path = require('path')
  7.  
    const multimatch = require('multimatch')
  8.  
    const getOptions = require('./options')
  9.  
    const ask = require('./ask')
  10.  
    const filter = require('./filter')
  11.  
    const logger = require('./logger')
  12.  
     

chalk 是一個可以讓終端輸出內容變色的模塊
Metalsmith是一個靜態網站(博客,項目)的生成庫
handlerbars 是一個模板編譯器,通過templatejson,輸出一個html
async 異步處理模塊,有點類似讓方法變成一個線程
consolidate 模板引擎整合庫
multimatch 一個字符串數組匹配的庫
options 是一個自己定義的配置項文件

隨后注冊了2個渲染器,類似於vue中的 vif velse的條件渲染

  1.  
    // register handlebars helper
  2.  
    Handlebars.registerHelper( 'if_eq', function (a, b, opts) {
  3.  
    return a === b
  4.  
    ? opts.fn( this)
  5.  
    : opts.inverse( this)
  6.  
    })
  7.  
     
  8.  
    Handlebars.registerHelper( 'unless_eq', function (a, b, opts) {
  9.  
    return a === b
  10.  
    ? opts.inverse( this)
  11.  
    : opts.fn( this)
  12.  
    })

接下來看關鍵的generate方法

  1.  
    module.exports = function generate (name, src, dest, done) {
  2.  
    // 讀取了src目錄下的 配置文件信息, 同時將 name auther(當前git用戶) 賦值到了 opts 當中
  3.  
    const opts = getOptions(name, src)
  4.  
    // 拼接了目錄 src/{template} 要在這個目錄下生產靜態文件
  5.  
    const metalsmith = Metalsmith(path.join(src, 'template'))
  6.  
    // 將metalsmitch中的meta 與 三個屬性合並起來 形成 data
  7.  
    const data = Object.assign(metalsmith.metadata(), {
  8.  
    destDirName: name,
  9.  
    inPlace: dest === process.cwd(),
  10.  
    noEscape: true
  11.  
    })
  12.  
    // 遍歷 meta.js元數據中的helpers對象,注冊渲染模板數據
  13.  
    // 分別指定了 if_or 和 template_version內容
  14.  
    opts.helpers && Object.keys(opts.helpers).map(key => {
  15.  
    Handlebars.registerHelper(key, opts.helpers[key])
  16.  
    })
  17.  
     
  18.  
    const helpers = { chalk, logger }
  19.  
     
  20.  
    // 將metalsmith metadata 數據 和 { isNotTest, isTest 合並 }
  21.  
    if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
  22.  
    opts.metalsmith.before(metalsmith, opts, helpers)
  23.  
    }
  24.  
     
  25.  
    // askQuestions是會在終端里詢問一些問題
  26.  
    // 名稱 描述 作者 是要什么構建 在meta.js 的opts.prompts當中
  27.  
    // filterFiles 是用來過濾文件
  28.  
    // renderTemplateFiles 是一個渲染插件
  29.  
    metalsmith.use(askQuestions(opts.prompts))
  30.  
    .use(filterFiles(opts.filters))
  31.  
    .use(renderTemplateFiles(opts.skipInterpolation))
  32.  
     
  33.  
    if (typeof opts.metalsmith === 'function') {
  34.  
    opts.metalsmith(metalsmith, opts, helpers)
  35.  
    } else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
  36.  
    opts.metalsmith.after(metalsmith, opts, helpers)
  37.  
    }
  38.  
     
  39.  
    // clean方法是設置在寫入之前是否刪除原先目標目錄 默認為true
  40.  
    // source方法是設置原路徑
  41.  
    // destination方法就是設置輸出的目錄
  42.  
    // build方法執行構建
  43.  
    metalsmith.clean(false)
  44.  
    .source('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
  45.  
    .destination(dest)
  46.  
    .build((err, files) => {
  47.  
    done(err)
  48.  
    if (typeof opts.complete === 'function') {
  49.  
    // 當生成完畢之后執行 meta.js當中的 opts.complete方法
  50.  
    const helpers = { chalk, logger, files }
  51.  
    opts.complete(data, helpers)
  52.  
    } else {
  53.  
    logMessage(opts.completeMessage, data)
  54.  
    }
  55.  
    })
  56.  
     
  57.  
    return data
  58.  
    }

meta.js

接下來看以下complete方法

  1.  
    complete: function(data, { chalk }) {
  2.  
    const green = chalk.green
  3.  
    // 會將已有的packagejoson 依賴聲明重新排序
  4.  
    sortDependencies(data, green)
  5.  
     
  6.  
    const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName)
  7.  
    // 是否需要自動安裝 這個在之前構建前的詢問當中 是我們自己選擇的
  8.  
    if (data.autoInstall) {
  9.  
    // 在終端中執行 install 命令
  10.  
    installDependencies(cwd, data.autoInstall, green)
  11.  
    .then(() => {
  12.  
    return runLintFix(cwd, data, green)
  13.  
    })
  14.  
    .then(() => {
  15.  
    printMessage(data, green)
  16.  
    })
  17.  
    .catch(e => {
  18.  
    console.log(chalk.red('Error:'), e)
  19.  
    })
  20.  
    } else {
  21.  
    printMessage(data, chalk)
  22.  
    }
  23.  
    }

構建自定義模板

在看完vue-init命令的原理之后,其實定制自定義的模板是很簡單的事情,我們只要做2件事

  • 首先我們需要有一個自己模板項目
  • 如果需要自定義一些變量,就需要在模板的meta.js當中定制

由於下載模塊使用的是download-git-repo模塊,它本身是支持在github,gitlab,bitucket上下載的,到時候我們只需要將定制好的模板項目放到git遠程倉庫上即可。

由於我需要定義的是小程序的開發模板,mpvue本身也有一個quickstart的模板,那么我們就在它的基礎上進行定制,首先我們將它fork下來,新建一個custom分支,在這個分支上進行定制。

我們需要定制的地方有用到的依賴庫,需要額外用到less以及wxparse
因此我們在 template/package.json當中進行添加

  1.  
    {
  2.  
    // ... 部分省略
  3.  
    "dependencies": {
  4.  
    "mpvue": "^1.0.11"{{#vuex}},
  5.  
    "vuex": "^3.0.1"{{/vuex}}
  6.  
    },
  7.  
    "devDependencies": {
  8.  
    // ... 省略
  9.  
    // 這是添加的包
  10.  
    "less": "^3.0.4",
  11.  
    "less-loader": "^4.1.0",
  12.  
    "mpvue-wxparse": "^0.6.5"
  13.  
    }
  14.  
    }

除此之外,我們還需要定制一下eslint規則,由於只用到standard,因此我們在meta.js當中 可以將 airbnb風格的提問刪除

  1.  
    "lintConfig": {
  2.  
    "when": "lint",
  3.  
    "type": "list",
  4.  
    "message": "Pick an ESLint preset",
  5.  
    "choices": [
  6.  
    {
  7.  
    "name": "Standard (https://github.com/feross/standard)",
  8.  
    "value": "standard",
  9.  
    "short": "Standard"
  10.  
    },
  11.  
    {
  12.  
    "name": "none (configure it yourself)",
  13.  
    "value": "none",
  14.  
    "short": "none"
  15.  
    }
  16.  
    ]
  17.  
    }

.eslinttrc.js

  1.  
    'rules': {
  2.  
    {{#if_eq lintConfig "standard"}}
  3.  
    "camelcase": 0,
  4.  
    // allow paren-less arrow functions
  5.  
    "arrow-parens": 0,
  6.  
    "space-before-function-paren": 0,
  7.  
    // allow async-await
  8.  
    "generator-star-spacing": 0,
  9.  
    {{/if_eq}}
  10.  
    {{#if_eq lintConfig "airbnb"}}
  11.  
    // don't require .vue extension when importing
  12.  
    'import/extensions': ['error', 'always', {
  13.  
    'js': 'never',
  14.  
    'vue': 'never'
  15.  
    }],
  16.  
    // allow optionalDependencies
  17.  
    'import/no-extraneous-dependencies': ['error', {
  18.  
    'optionalDependencies': ['test/unit/index.js']
  19.  
    }],
  20.  
    {{/if_eq}}
  21.  
    // allow debugger during development
  22.  
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  23.  
    }

最后我們在構建時的提問當中,再設置一個小程序名稱的提問,而這個名稱會設置到導航的標題當中。
提問是在meta.js當中添加

  1.  
    "prompts": {
  2.  
    "name": {
  3.  
    "type": "string",
  4.  
    "required": true,
  5.  
    "message": "Project name"
  6.  
    },
  7.  
    // 新增提問
  8.  
    "appName": {
  9.  
    "type": "string",
  10.  
    "required": true,
  11.  
    "message": "App name"
  12.  
    }
  13.  
    }

main.json

  1.  
    {
  2.  
    "pages": [
  3.  
    "pages/index/main",
  4.  
    "pages/counter/main",
  5.  
    "pages/logs/main"
  6.  
    ],
  7.  
    "window": {
  8.  
    "backgroundTextStyle": "light",
  9.  
    "navigationBarBackgroundColor": "#fff",
  10.  
    // 根據提問設置標題
  11.  
    "navigationBarTitleText": "{{appName}}",
  12.  
    "navigationBarTextStyle": "black"
  13.  
    }
  14.  
    }
  15.  
     

最后我們來嘗試一下我們自己的模板

vue init Baifann/mpvue-quickstart#custom min-app-project 

image_1cj0ikq141je51ii31eek25t18il19.png-31.4kB

 

總結

以上模板的定制是十分簡單的,在實際項目上肯定更為復雜,但是按照這個思路應該都是可行的。比如說將一些自行封裝的組件也放置到項目當中等等,這里就不再細說。原理解析都是基於vue-cli 2.0的,但實際上 3.0也已經整裝待發,如果后續有機會,深入了解之后,再和大家分享,謝謝大家。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM