vue-cli的簡單實現(xiàn)

這個demo我是模仿Vue-CLI 2.0寫的一個簡單的構建工具,3.0的源碼還沒去看蜒秤,所以會有不同的地方衅疙。

已經(jīng)上傳到github上了

先安裝開發(fā)依賴的工具

npm i commander handlebars inquirer metalsmith -D

commander:用來處理命令行參數(shù)

handlerbars:一個簡單高效的語義化模板構建引擎膝擂,比如我們用vue-cli構建項目后命令行會有一些交互行為虑啤,讓你選擇要安裝的包什么的等等,而Handlerbars.js會根據(jù)你的這些選擇回答去渲染模版猿挚。

inquirer:會根據(jù)模版里面的meta.js或者meta.json文件中的設置咐旧,與用戶進行一些簡單的交互以確定項目的一些細節(jié)。

metalsmith:一個非常簡單的可插拔的靜態(tài)網(wǎng)站生成器绩蜻,通過添加一些插件對要構建的模版文件進行處理铣墨。

安裝完后就能在package.json中看到如下的依賴

依賴

項目目錄結構

image.png

其中template-demo里面包含了本次要構建的項目模版templae,和meta.js文件

代碼編寫

1.bin/dg.js之后在命令行下面運行

node bin/dg.js xxx xxx

就可以構建項目了办绝。
兩個 xxx的地方 第一個是項目的模版伊约,第二個是要輸入到哪個目錄下也就是要構建的項目名稱

// dg.js
const program = require('commander')
const path = require('path')
const chalk = require('chalk')  // 終端字體顏色
const inquirer = require('inquirer')
const exists = require('fs').existsSync // 判斷 路徑是否存在
const generate = require('./lib/generate')

/**
 * 注冊一個help的命令
 * 當在終端輸入 dg --help 或者沒有跟參數(shù)的話
 * 會輸出提示
 */
program.on('--help', () => {{
  console.log('  Examples:')
  console.log()
  console.log(chalk.gray('    # create a new project with an template')) // 會以灰色字體顯示
  console.log('    $ dg dgtemplate my-project')
}})

/**
 * 判斷參數(shù)是否為空
 * 如果為空調用上面注冊的 help命令
 * 輸出提示
 */
function help () {
  program.parse(process.argv)  //commander 用來處理 命令行里面的參數(shù), 這邊的process是node的一個全局變量不明白的可以查一下資料
  if (program.args.length < 1) return program.help()
}
help()

/**
 * 獲取命令行參數(shù)
 */
let template = program.args[0] // 命令行第一個參數(shù) 模版的名字
const rawName = program.args[1] // 第二個參數(shù) 項目目錄

/**
 * 獲取項目和模版的完整路徑
 */
const to = path.resolve(rawName) // 構建的項目的 絕對路徑
const tem = path.join(process.cwd(), template) //模版的路徑  cwd是當前運行的腳本是在哪個路徑下運行

/**
 * 判斷這個項目路徑是否存在也就是是否存在相同的項目名
 * 如果存在提示 是否繼續(xù)然后運行 run
 * 如果不存在 則直接運行 run 最后會創(chuàng)建一個項目目錄
 */
if (exists(to)) {
  inquirer.prompt([  // 這邊就用到了與終端交互的inquirer了
    {
      type: 'confirm',
      message: 'Continue?',
      name: 'ok'
    }
  ]).then(answers => {
    if (answers.ok) {
      run ()
    }
  })
} else {
  run ()
}

/**
 * run函數(shù)則是用來調用generate來構建項目
 */
function run () {
  if (exists(tem)) {
    generate(rawName, tem, to, (err) => {
      if (err) console.log(err)  // 如果構建失敗就調用的回調函數(shù)
    })
  }
}

注釋說明 都在代碼里面了孕蝉。

2.接下來就是很重要的lib/generate.js文件了

// generate.js
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const path = require('path')
const chalk = require('chalk')
const getOptions = require('./options')
const ask = require('./ask')


/**
 * 把generate 導出去給dg.js使用
 * opts是通過getOptions()函數(shù)用來獲取 meta.js中的配置
 * metalsmith是通過metalsmith.js獲取模版的元數(shù)據(jù)
 * metalmith可以讓我們編寫一些插件來對項目下面的文件進行配置
 * 其中第一個use的第一個插件就是用來在終端中輸入一些問題一些選項讓我們設置一些模版中的細節(jié)
 * 而這些問題就是 放在meta.js中
 * 第二個use的插件這是渲染模版屡律,這里就是用了handebars.js來渲染模版
 * 
 */
module.exports = function generate (name, tem, dest, done) {
  const opts = getOptions(name, tem)
  const metalsmith = Metalsmith(path.join(tem, 'template'))
  const data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd()
  })
  metalsmith.use(askQuestions(opts.prompts)).use(renderTemplateFiles())  // 這兩個插件在下面的代碼中
  // 在構建前執(zhí)行一些函數(shù)
  metalsmith.clean(false)
    .source('.') // 默認的source路徑是 ./src 所以這邊要改成整個 template 這個根據(jù)自己要輸出的需求配置
    .destination(dest)  // 要輸出到哪個路徑下 這里就是 我們的項目地址
    .build((err, files) => {  // 最后進行構建項目
      done(err) // 執(zhí)行 回掉函數(shù)
      if (typeof opts.complete === 'function') {
        const helpers = { chalk }
        opts.complete(data, helpers)  // 判斷meta.js中是否定義了構建完成后要執(zhí)行的函數(shù) 這里是判斷是否執(zhí)行自動安裝依賴
      } else {
        console.log('complete is not a function')
      }
    })
}


/**
 * 這里通過這個函數(shù)返回一個metalsmith的符合metalsmith插件格式的函數(shù)
 * 第一個參數(shù)fils就是 這個模版下面的全部文件
 * 第二個參數(shù)ms就是元數(shù)據(jù)這里我們的問題以及回答會已鍵值對的形式存放在里面用于第二個插件渲染模版
 * 第三個參數(shù)就是類似 next的用法了 調用done后才能移交給下一個插件運行
 * ask函數(shù)則在另外一個js文件中
 */
function askQuestions (prompts) {
  return (fils, ms, done) => {
    ask(prompts, ms.metadata(), done)
  }
}

/**
 * render函數(shù)則是通過我們第一個插件收集這些問題以及回答后
 * 然后渲染我們的模版
 */
function renderTemplateFiles () {
  return (files, ms, done) => {
    const keys = Object.keys(files)  // 獲取模版下的所有文件名
    keys.forEach(key => {  // 遍歷對每個文件使用handlerbars渲染
      const str = files[key].contents.toString()
      let t = Handlebars.compile(str)
      let html = t(ms.metadata())
      files[key].contents = new Buffer.from(html)  // 渲染后重新寫入到文件中
    })
    done() // 移交給下個插件
  }
}

其實generate.js功能就是用來收集我們在命令行下交互的問題的答案用來渲染模版,只不過我這邊只是簡單的實現(xiàn)降淮,在vue-cli 2.0中還有對文件的過濾超埋,跳過不符合使用handlebars渲染文件,添加一些handlebars的helpers來制定文件渲染的規(guī)則等等

  1. lib/options.js
// options.js
const path = require('path')

/**
 * 這里的options內容比較簡單
 * 就是用于用來獲取 meta.js 里面的配置
 */
module.exports = function options (name, dir) {
  const metaPath = path.join(dir, 'meta.js')
  const req = require(metaPath)
  let opts = {}
  opts = req
  return opts
}

options我也是簡單的實現(xiàn)佳鳖,有興趣的話可以查看vue-cli的源碼

  1. lib/ask.js
// ask.js
const async = require('async')  // 這是node下一個異步處理的工具
const inquirer = require('inquirer')

const promptMapping = {
  string: 'input'
}

/**
 * 這個函數(shù)就是 根據(jù)meta.js里面定義的prompts來與用戶進行交互
 * 然后收集用戶的交互信息存放在metadate 也就是metalsmith元數(shù)據(jù)中
 * 用于渲染模版使用
 */
module.exports = function ask (prompts, metadate, done) {
  async.eachSeries(Object.keys(prompts), (key, next) => {  // 這里不能簡單的使用數(shù)組的 foreach方法 否則只直接跳到最后一個問題
    inquirer.prompt([{
      type: promptMapping[prompts[key].type] || prompts[key].type,
      name: key,
      message: prompts[key].message,
      choices: prompts[key].choices || [],
    }]).then(answers => {
      if (typeof answers[key] === 'string') {
        metadate[key] = answers[key].replace(/"/g, '\\"')
      } else {
        metadate[key] = answers[key]
      }
      next()
    }).catch(done)
  }, done) // 全部回答完 調用 done移交給下一個插件
}

收集問題的答案用于渲染模版

下面是用于渲染模版的配置中的代碼

為了方便 我把要渲染的模版霍殴,直接跟 構建工具 項目放到了同個文件夾下面,就是上面我截圖的項目結構的 template-demo 里面包含了要渲染的模版 放在 template-demo/template下面了系吩,還包含了渲染模版的配置文件meta.js来庭。

// meta.js
const { installDependencies } = require('./utils')
const path = require('path')


/***
 * 要交互的問題都放在 prompts中 
 * when是當什么情況下 用來判斷是否 顯示這個問題
 * type是提問的類型
 * message就是要顯示的問題
 */
module.exports = {
  prompts: {
    name: {
      when: 'ismeta',
      type: 'string',
      message: '項目名稱:'
    },
    description: {
      when: 'ismeta',
      type: 'string',
      message: '項目介紹:'
    },
    author: {
      when: 'ismeta',
      type: 'string',
      message: '項目作者:'
    },
    email: {
      when: 'ismeta',
      type: 'string',
      message: '郵箱:'
    },
    dgtable: {
      when: 'ismeta',
      type: 'confirm',
      message: '是否安裝dg-table(筆者編寫的基于elementui二次開發(fā)的強大的表格)',
    },
    genius: {
      when: 'ismeta',
      type: 'list',
      message: '想看想看?',
      choices: [
        {
          name: '想',
          value: '想',
          short: '想'
        },
        {
          name: '很想',
          value: '很想',
          short: '很想'
        }
      ]
    },
    autoInstall: {
      when: 'ismeta',
      type: 'confirm',
      message: '是否自動執(zhí)行npm install 安裝依賴穿挨?',
    },
  },
  complete: function(data, { chalk }) {
    /**
     * 用于判斷是否執(zhí)行自動安裝依賴
     */
    const green = chalk.green // 取綠色
    const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName)
    if (data.autoInstall) {
      installDependencies(cwd, 'npm', green) // 這里使用npm安裝
        .then(() => {
          console.log('依賴安裝完成')
        })
        .catch(e => {
          console.log(chalk.red('Error:'), e)
        })
    } else {
      // printMessage(data, chalk)
    }
  }
}

主要是用于配置交互的問題月弛,和再項目構建完成后執(zhí)行的 complete 函數(shù),這里就是 判斷用戶是否 選擇了 自動安裝依賴科盛,如果autoInstall為true就自動安裝依賴

const spawn = require('child_process').spawn  // 一個node的子線程

/**
 * 安裝依賴
 */
exports.installDependencies = function installDependencies(
  cwd,
  executable = 'npm',
  color
) {
  console.log(`\n\n# ${color('正在安裝項目依賴 ...')}`)
  console.log('# ========================\n')
  return runCommand(executable, ['install'], {
    cwd,
  })
}


function runCommand(cmd, args, options) {
  return new Promise((resolve, reject) => {
    /**
     * 如果不清楚spaw的話可以上網(wǎng)查一下
     * 這里就是 在項目目錄下執(zhí)行 npm install
     */
    const spwan = spawn(
      cmd,
      args,
      Object.assign(
        {
          cwd: process.cwd(),
          stdio: 'inherit',
          shell: true, // 在shell下執(zhí)行
        },
        options
      )
    )
    spwan.on('exit', () => {
      resolve()
    })
  })
}

執(zhí)行安裝的具體實現(xiàn)函數(shù)些楣。

最后你就可以在構建工具的根目錄下 執(zhí)行

node bin/dg.js template-demo demo

來構建項目啦老速。
如果把dg.js添加到$PATH中 就可以 直接使用dg template-demo demo來構建項目棒掠。

參數(shù)為空或者--helpe

Boolean類型

多選類型

自動安裝依賴

依賴安裝完成

demo就是我們構建的項目

demo/package.json

最后我們可以看到我們在命令行回答的問題被渲染到了這里面來了濒憋,根據(jù)是否安裝 dg-table讓這個插件出現(xiàn)在了依賴列表里面,當然包括模版中的index.html也被渲染了。這里圖片就不貼出來了冀泻。這個模版只不過是為了演示沒有其他意義了。

主要是我比較懶蜡饵,挺多功能沒實現(xiàn)弹渔,還有vue-cli可以自動從github上面拉取模版,const download = require('download-git-repo') //用于下載遠程倉庫至本地 支持GitHub溯祸、GitLab肢专、Bitbucket

如果想更清楚的了解內部實現(xiàn)最好還是看下Vue-cli2.0的源碼焦辅。

已經(jīng)上傳到github上了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末博杖,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子筷登,更是在濱河造成了極大的恐慌剃根,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件前方,死亡現(xiàn)場離奇詭異狈醉,居然都是意外死亡,警方通過查閱死者的電腦和手機惠险,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門苗傅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人班巩,你說我怎么就攤上這事渣慕。” “怎么了抱慌?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵逊桦,是天一觀的道長。 經(jīng)常有香客問我遥缕,道長卫袒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任单匣,我火速辦了婚禮夕凝,結果婚禮上,老公的妹妹穿的比我還像新娘户秤。我一直安慰自己码秉,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布鸡号。 她就那樣靜靜地躺著转砖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上府蔗,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天晋控,我揣著相機與錄音,去河邊找鬼姓赤。 笑死赡译,一個胖子當著我的面吹牛,可吹牛的內容都是我干的不铆。 我是一名探鬼主播蝌焚,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼誓斥!你這毒婦竟也來了只洒?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤劳坑,失蹤者是張志新(化名)和其女友劉穎毕谴,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體距芬,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡析珊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蔑穴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忠寻。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖存和,靈堂內的尸體忽然破棺而出奕剃,到底是詐尸還是另有隱情,我是刑警寧澤捐腿,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布纵朋,位于F島的核電站,受9級特大地震影響茄袖,放射性物質發(fā)生泄漏操软。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一宪祥、第九天 我趴在偏房一處隱蔽的房頂上張望聂薪。 院中可真熱鬧,春花似錦蝗羊、人聲如沸藏澳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翔悠。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蓄愁,已是汗流浹背双炕。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留撮抓,地道東北人雄家。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像胀滚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子乱投,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容

  • 目錄 UI組件 開發(fā)框架 實用庫 服務端 輔助工具 應用實例 Demo示例 UI組件 element★13489 ...
    余生社會閱讀 19,729評論 7 233
  • 地產(chǎn)的根本戚炫,要看背后的數(shù)據(jù)剑刑。比如當初深圳房價開始怪獸式不可理喻的暴漲,那么很簡單双肤,你有興趣又有數(shù)據(jù)和途徑的話施掏,就去...
    思想家如懷閱讀 161評論 0 0
  • 記得以前,也就一兩年之前吧茅糜,那時候不管怎么曬太陽七芭,皮膚曬黑了,過一段時間就會自己恢復蔑赘,會白回來狸驳,現(xiàn)如今不經(jīng)曬了,一...
    Kasonn閱讀 363評論 0 0
  • 最近每天早上必喝一杯咖啡缩赛,才開始一天的生活耙箍。以前只是偶爾喝一杯,我是屬于敏感體質酥馍,有時...
    喜風SanPedroSula閱讀 358評論 0 0
  • 重新認知“裂變” ——《流量池》第4章讀書筆記 一辩昆、內容概要 第4章主要講解了“裂變營銷”相關的基本理論、方法模型...
    WendaoSolemn閱讀 304評論 0 0