當你使用vue-cli時, 你有沒有想過什麼?一塊兒來實現一個精簡版吧

前言

最近在研究前端工程化的一些東西, 那就得有本身的一套腳手架, 因而瞄上了vue-cli, 也看了下create-react-app, 感受vue-cli更符合個人預期, 因而擼了遍源碼, 寫了個小demo腳手架。前端

獻上源碼地址: 源碼vue

體驗方法:react

$ npm i masoneast-cli -g
$ masoneast init my-project
複製代碼

如今, 咱們一塊兒來了解下vue-cli到底幫咱們作了什麼,讓咱們能夠一行命令就能夠生成一個工程吧!webpack

總體流程

咱們先了解下如何使用vue-cli, 再詳細講解每一步的實現。git

vue-cli提供了多種模板, 咱們這裏以webpack模板爲例。github

  • 安裝: npm install vue-cli -gweb

  • 使用:vue-cli

    1. 直接下載使用: vue init webpack my-project
    2. 離線使用: vue init webpack my-projiect --offline
    3. clone使用: vue init webpack my-projiect --clone

這樣, 咱們就能在當前目錄下獲得一個vue的初始工程了。npm

當咱們使用vue-cli時, 其實依賴了兩個東西: 一個是vue-cli命令行, 一個是vue-template模板, 用於生成工程。json

流程:

  1. 當咱們全局安裝了vue-cli, 會註冊環境變量,生成軟鏈接, 這樣咱們在命令行中任意路徑就可使用該命令了。
  2. 當咱們敲下vue init webpack my-projectvue-cli會提示你正在下載模板。

此時, vue-cli就是從github託管的代碼中download對應的webpack模板。 對應的webpack模板的git地址在這裏: webpack模板

拼接url代碼是這段:

function getUrl (repo, clone) {
    var url

    // Get origin with protocol and add trailing slash or colon (for ssh)
    var origin = addProtocol(repo.origin, clone)
    if (/^git\@/i.test(origin))
        origin = origin + ':'
    else
        origin = origin + '/'

    // Build url
    if (clone) {
        url = origin + repo.owner + '/' + repo.name + '.git'
    } else {
        if (repo.type === 'github')
            url = origin + repo.owner + '/' + repo.name + '/archive/' + repo.checkout + '.zip'
        else if (repo.type === 'gitlab')
            url = origin + repo.owner + '/' + repo.name + '/repository/archive.zip?ref=' + repo.checkout
        else if (repo.type === 'bitbucket')
            url = origin + repo.owner + '/' + repo.name + '/get/' + repo.checkout + '.zip'
    }

    return url
}
複製代碼
  1. 當模板下載完畢後vue-cli會將它放在你的本地,方便你之後離線使用它生成項目, 路徑是/Users/xxx/.vue-templates, 若是你以前有使用vue-cli生成過項目, 應該在你的管理員路徑下能找到對應的.vue-templates文件夾。裏面的webpack文件就和上面git地址裏的代碼如出一轍。

注意: .開頭的文件夾默認是隱藏的, 你須要讓它展現出來才能看到。

  1. 詢問交互

接下, vue-cli會問你一堆問題, 你回答的這些問題它會將它們的答案存起來, 在接下來的生成中, 會根據你的答案來渲染生成對應的文件。

  1. 文件篩選

在你回答完問題後, vue-cli就會根據你的需求從webpack模板中篩選出無用的文件, 並刪除, 它不是從你本地刪除, 只是在給你生成的項目中刪除這些文件。

  1. 模板渲染

在模板中, 你的src/App.vue長這樣:

<template>
  <div id="app">
    <img src="./assets/logo.png">
    {{#router}}
    <router-view/>
    {{else}}
    <HelloWorld/>
    {{/router}}
  </div>
</template>

<script>
{{#unless router}}
import HelloWorld from './components/HelloWorld'

{{/unless}}
export default {
  name: 'App'{{#router}}{{else}},
  components: {
    HelloWorld
  }{{/router}}
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
複製代碼

若是在選擇是否須要路由, 你選是,那最後生成在你的項目的App.vue長這樣:

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
複製代碼

它會根據你的要求,渲染出不一樣的文件給你。

  1. 文件生成

在完成渲染後, 接下來就會在你當前目錄下生成對應的文件了, 至此, vue-cli的工做就完成了。

動手實現

搞明白了vue-cli的工做原理, 咱們徹底能夠本身作一個簡單點的cli出來了。

命令註冊

經過npm init生成你的package.json文件, 在裏面加入bin

"bin": {
    "xxx": "bin/index.js"
  },
複製代碼

這樣, 當你全局裝包的時候纔會把你xxx命令註冊到環境變量中。

接下來就是bin/index.js的事了。

使用commander完成命令行中的命令

program
   .command('init [project-name]')
   .description('create a project')
   .option("-c, --clone", `it will clone from ${tmpUrl}`)
   .option('--offline', 'use cached template')
   .action(function (name, options) {
       console.log('we are try to create "%s"....', name);
       downloadAndGenerate(name, options)
   }).on('--help', function () {
       console.log('');
       console.log('Examples:');
       console.log('');
       console.log(' $ masoneast init my-project');
       console.log(` $ path: ${home}`);
   });

program.parse(process.argv)
複製代碼

經過上面代碼, 你就有了init命令, 和clone, offline參數了, 此時你就有了:

$ masoneast init my-project
$ masoneast init my-project --clone
$ masoneast init my-project --offline
複製代碼

關於commander包的具體使用, 能夠看這裏: commander

實現下載和clone模板

這裏你須要有有個模板的地址供你下載和clone, 若是你只是玩玩的話也能夠直接使用vue提供的模板地址, 或者個人模板地址: 模板

下載實現代碼:

這裏依賴了兩個庫: git-clonedownload

function download (name, clone, fn) {
    if (clone) {
        gitclone(tmpUrl, tmpPath, err => {
            if (err) fn(err)
            rm(tmpPath + '/.git')
            fn()
        })
    } else {
        const url = tmpUrl.replace(/\.git*/, '') + '/archive/master.zip'
        console.log(url)
        downloadUrl(url, tmpPath, { extract: true, strip: 1, mode: '666', headers: { accept: 'application/zip' } })
            .then(function (data) {
                fn()
            })
            .catch(function (err) {
                fn(err)
            })
    }
}
複製代碼

實現詢問交互

交互的實現, 主要依賴了inquirer庫。

function askQuestion (prompts) {                    //詢問交互
    return (files, metalsmith, done) => {
        async.eachSeries(Object.keys(prompts), (key, next) => {
            prompt(metalsmith.metadata(), key, prompts[key], next)
        }, done)
    }
}
複製代碼

將詢問獲得的答案存貯起來, 留給後面渲染使用

function prompt (data, key, prompt, done) {                    //將用戶操做存儲到metaData中
    inquirer.prompt([{
        type: prompt.type,
        name: key,
        message: prompt.message || prompt.label || key,
        default: prompt.default,
        choices: prompt.choices || [],
        validate: prompt.validate || (() => true)
    }]).then(answers => {
        if (Array.isArray(answers[key])) {
            data[key] = {}
            answers[key].forEach(multiChoiceAnswer => {
                data[key][multiChoiceAnswer] = true
            })
        } else if (typeof answers[key] === 'string') {
            data[key] = answers[key].replace(/"/g, '\\"')
        } else {
            data[key] = answers[key]
        }
        done()
    }).catch(done)
}
複製代碼

實現模板渲染

模板渲染, 依賴了前端模板引擎handlebar和解析模板引擎的consolidate庫。 上面看到的vue-template模板裏的{{#router}}其實就是handlebar的語法。

function renderTemplateFiles () {

    return (files, metalsmith, done) => {

        const keys = Object.keys(files)
        const metalsmithMetadata = metalsmith.metadata()            //以前用戶操做後的數據存在這裏面
        async.each(keys, (file, next) => {                          //對模板進行遍歷, 找到須要渲染內容的文件
            const str = files[file].contents.toString()
            if (!/{{([^{}]+)}}/g.test(str)) {                       //正則匹配文件內容, 若是沒有就不須要修改文件, 直接去往下一個
                return next()
            }
            render(str, metalsmithMetadata, (err, res) => {
                if (err) {
                    err.message = `[${file}] ${err.message}`
                    return next(err)
                }
                files[file].contents = new Buffer(res)
                next()
            })
        }, done)
    }
}
複製代碼

實現將文件從本地寫到你的項目目錄中

這裏用到了一個核心庫: metalsmith。它主要功能就是讀取你的文件, 並經過一系列的中間件對你的文件進行處理, 而後寫到你想要的路徑中去。就是經過這個庫, 將咱們的各個流程串聯起來, 實現對模板的改造, 寫出你想要的項目。

metalsmith.use(askQuestion(options.prompts))                            //這一段是generator的精華, 經過各類中間件對用戶選擇的模板進行處理
        .use(filterFiles(options.filters))                                  //文件篩選過濾
        .use(renderTemplateFiles())                                         //模板內部變量渲染
        .source('.')
        .destination(projectPath)                                            //項目建立的路徑
        .build((err, files) => {
            if (err) console.log(err)

        })
複製代碼

後話

我這裏實現的demo就是vue-cli的精簡版, 主要功能有:

    1. 從git上download和clone項目模板
    1. 保存模板到本地,方便離線使用
    1. 詢問問題, 按用戶需求定製模板

vue-cli還有有不少的容錯判斷, 以及其餘模板, 下載源等的切換我這裏都沒有作處理了。

這個masoneast-cli就是我閱讀vue-cli源碼的學習成果, 這裏作一個總結。 若是 對你們有幫助, 隨手給個star✨唄。

相關文章
相關標籤/搜索