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

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

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

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

npm i commander handlebars inquirer metalsmith -D

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

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

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

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

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

依賴

項目目錄結(jié)構(gòu)

image.png

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

代碼編寫

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

node bin/dg.js xxx xxx

就可以構(gòu)建項目了。
兩個 xxx的地方 第一個是項目的模版,第二個是要輸入到哪個目錄下也就是要構(gòu)建的項目名稱

// 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ù)是否為空
 * 如果為空調(diào)用上面注冊的 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) // 構(gòu)建的項目的 絕對路徑
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ù)則是用來調(diào)用generate來構(gòu)建項目
 */
function run () {
  if (exists(tem)) {
    generate(rawName, tem, to, (err) => {
      if (err) console.log(err)  // 如果構(gòu)建失敗就調(diào)用的回調(diào)函數(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的第一個插件就是用來在終端中輸入一些問題一些選項讓我們設(shè)置一些模版中的細節(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())  // 這兩個插件在下面的代碼中
  // 在構(gòu)建前執(zhí)行一些函數(shù)
  metalsmith.clean(false)
    .source('.') // 默認的source路徑是 ./src 所以這邊要改成整個 template 這個根據(jù)自己要輸出的需求配置
    .destination(dest)  // 要輸出到哪個路徑下 這里就是 我們的項目地址
    .build((err, files) => {  // 最后進行構(gòu)建項目
      done(err) // 執(zhí)行 回掉函數(shù)
      if (typeof opts.complete === 'function') {
        const helpers = { chalk }
        opts.complete(data, helpers)  // 判斷meta.js中是否定義了構(gòu)建完成后要執(zhí)行的函數(shù) 這里是判斷是否執(zhí)行自動安裝依賴
      } else {
        console.log('complete is not a function')
      }
    })
}


/**
 * 這里通過這個函數(shù)返回一個metalsmith的符合metalsmith插件格式的函數(shù)
 * 第一個參數(shù)fils就是 這個模版下面的全部文件
 * 第二個參數(shù)ms就是元數(shù)據(jù)這里我們的問題以及回答會已鍵值對的形式存放在里面用于第二個插件渲染模版
 * 第三個參數(shù)就是類似 next的用法了 調(diào)用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內(nèi)容比較簡單
 * 就是用于用來獲取 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) // 全部回答完 調(diào)用 done移交給下一個插件
}

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

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

為了方便 我把要渲染的模版,直接跟 構(gòu)建工具 項目放到了同個文件夾下面,就是上面我截圖的項目結(jié)構(gòu)的 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)
    }
  }
}

主要是用于配置交互的問題,和再項目構(gòu)建完成后執(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ù)。

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

node bin/dg.js template-demo demo

來構(gòu)建項目啦。
如果把dg.js添加到$PATH中 就可以 直接使用dg template-demo demo來構(gòu)建項目。

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

Boolean類型

多選類型

自動安裝依賴

依賴安裝完成

demo就是我們構(gòu)建的項目

demo/package.json

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

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容