vue-cli3腳手架實現淺析

前言

本身平時喜歡一些工具類的小玩意,因此打算搞一個本身玩的cli!
比較了下仍是vue-cli比較合適,因此本文以vue-cli爲模板進行拆分.
網上查閱了大量資料,但資料大部分都是vue-cli2的,簡單的看了下,比較簡單,就不打算深究了!
本文着重vue-cli3的實現思路
複製代碼
  • 地址github,歡迎star | fork

依賴工具包

semver  // 語義化版本控制規範
    
    fs-extra  // 二次封裝的fs模塊
    
    didyoumean //一個簡單的JavaScript匹配引擎
    
    execa // 是能夠調用shell和本地外部程序的javascript封裝。
    
    commander.js,能夠自動的解析命令和參數,用於處理用戶輸入的命令。富有表現力和強大的命令行框架.
    
    download-git-repo,下載並提取 git 倉庫,用於下載項目模板。
    
    Inquirer.js,通用的命令行用戶界面集合,用於和用戶進行交互。
    
    handlebars.js,模板引擎,將用戶提交的信息動態填充到文件中。
    
    ora,下載過程久的話,能夠用於顯示下載中的動畫效果。
    
    chalk,能夠給終端的字體加上顏色。
    
    log-symbols,能夠在終端上顯示出 √ 或 × 等的圖標。
    
    loglevel JavaScript的最小輕量級簡單日誌記錄
    
    prompt  //一個漂亮的命令行提示符
    
    slash  //用於轉換 Windows 反斜槓路徑轉換爲正斜槓路徑 \ => /
    
    minimist // 輕量級的命令行參數解析引擎
    
    validate-npm-package-name  // 驗證npm包名
複製代碼

正文

  • 先來入口文件
// vue.js
...
program
     .command('create <app-name>')  // app-name 必需輸入
     .description('create a new project')
     .action((name, cmd) => {
       // console.log(cmd)
       const options = cleanArgs(cmd)

       if (minimist(process.argv.slice(3))._.length > 1) {
         console.log(chalk.red('\n 信息:您提供了多個參數。第一個將用做應用程序的名稱,其他的將被忽略。'))
       }
       // --git makes commander to default git to true
       if (process.argv.includes('-g') || process.argv.includes('--git')) {
         options.forceGit = true
       }
       require('../lib/create')(name, options) // 這裏去讀取了create文件
     })
...

// 解析參數很是重要
program.parse(process.argv) 
複製代碼
  • create.js
//  create.js
async function create (projectName, options) {
  if (options.proxy) {
    process.env.HTTP_PROXY = options.proxy
  }
  // 當前工做目錄
  const cwd = options.cwd ||process.cwd()
  
  //  是否相等
  const inCurrent = projectName === '.'
  // 
  const name = inCurrent ? path.relative('../', cwd) : projectName
  // 目標路徑
  const targetDir = path.resolve(cwd, projectName || '.')
  
  // 獲取包名返回結果
  // 爲true 說明不能註冊,反之--
  const result = validateProjectName(name)
  if (!result.validForNewPackages) {
    console.error(chalk.red(`Invalid project name: "${name}"`))
    result.errors && result.errors.forEach(err => {
      console.error(chalk.red.dim('Error: ' + err))
    })
    result.warnings && result.warnings.forEach(warn => {
      console.error(chalk.red.dim('Warning: ' + warn))
    })
    // process.exit(1)
  }
  
  // 若是路徑存在,進行如下操做
  if(fs.existsSync(targetDir)){
    // 若是存在 -f 參數 ,把當前路徑刪除
    if(options.force) {
      await fs.remove(targetDir)
    }else {
        ...
        ...
      if(!inCurrent) {
        const { action } = await inquirer.prompt([
          {
            name: 'action',
            type: 'list',
            message: `目標目錄 ${chalk.cyan(targetDir)} 已經存在,請選擇一個選項:`,
            choices: [
              { name: '覆蓋', value: 'overwrite' },
              { name: '合併', value: 'merge' },
              { name: '取消', value: false }
            ]
          }
        ])
        if (!action) {
          return
        } else if (action === 'overwrite') {
          console.log(`\n正在刪除 ${chalk.cyan(targetDir)}`)
          await fs.remove(targetDir)
        }
      }
    }
  }

  const creator = new Creator(name, targetDir, getPromptModules())  // 這裏調用了Creator函數 // 
                                //  getPromptModules() 這個函數讀取了promptModules這個目錄下的每一個模塊
  
  
  await creator.create(options)
}
複製代碼
  • getPromptModules.js
exports.getPromptModules = () => {
  return [
    'babel',
    'typescript',
    'pwa',
    'router',
    'vuex',
    'cssPreprocessors',
    'linter',
    'unit',
    'e2e'
  ].map(file => require(`../promptModules/${file}`))
}
複製代碼
  • Creator.js
module.exports = class Creator extends EventEmitter {
    constructor (name, targetDir, promptModules) {
        super()
        this.name = name
        
        this.context = process.env.VUE_CLI_CONTEXT = targetDir
        
        const { presetPrompt, featurePrompt } = this.resolveIntroPrompts() // 這個函數解析命令行交互(1)
        this.presetPrompt = presetPrompt
        this.featurePrompt = featurePrompt
        this.outroPrompts = this.resolveOutroPrompts() // 一些配置暫時無論
        this.injectedPrompts = []
        
        this.promptCompleteCbs = []
        this.createCompleteCbs = []
        this.run = this.run.bind(this)
        const promptAPI = new PromptModuleAPI(this) // 這個函數是一些操做模塊的方法(2)
        promptModules.forEach(m => m(promptAPI))    // 把promptAPI模塊傳進去,進行操做(3) 
    }
    async promptAndResolvePreset (answers = null) {
        ...
        ...
    }
    
    ...
    ...
}
複製代碼
  • resolveIntroPrompts.js (上面1)
// 解析命令行交互
  resolveIntroPrompts () {
    const presetPrompt = {
      name: 'preset',
      type: 'list',
      message: `Please pick a preset:`,
      choices: [
        // ...presetChoices,
        {
          name: 'Manually select features',
          value: '__manual__'
        }
      ]
    }

    const featurePrompt = {
      name: 'features',
      when: isManualMode,
      type: 'checkbox',
      message: 'Check the features needed for your project:',
      choices: [],  // 這裏規則在(3)進行了替換(見 PromptModuleAPI.js)  
      pageSize: 10
    }
    return {
      presetPrompt,
      featurePrompt
    }
  }
複製代碼
  • PromptModuleAPI.js (上面2)
module.exports = class PromptModuleAPI {
constructor (creator) {
  this.creator = creator
}

injectFeature (feature) {
  this.creator.featurePrompt.choices.push(feature)
}

injectPrompt (prompt) {
  this.creator.injectedPrompts.push(prompt)
}

injectOptionForPrompt (name, option) {
  this.creator.injectedPrompts.find(f => {
    return f.name === name
  }).choices.push(option)
}

onPromptComplete (cb) {
  this.creator.promptCompleteCbs.push(cb)
}
}
複製代碼
  • (上面3 ,在promptModules目錄下,這裏列舉一個Babel.js)
module.exports = cli => {
  cli.injectFeature({
    name: 'Babel',
    value: 'babel',
    short: 'Babel',
    description: 'Transpile modern JavaScript to older versions (for compatibility)',
    link: 'https://babeljs.io/',
    checked: true
  })

  cli.onPromptComplete((answers, options) => {
    if (answers.features.includes('ts')) {
      if (!answers.useTsWithBabel) {
        return
      }
    } else if (!answers.features.includes('babel')) {
      return
    }
    options.plugins['@vue/cli-plugin-babel'] = {}
  })
}
複製代碼

  • 回過頭來看create.js
//  create.js

  const creator = new Creator(name, targetDir, getPromptModules())
  
  await creator.create(options) // 這裏調用了Creator類下面的create方法
  
複製代碼
  • create()
// Creator.js

...
...
async create (cliOptions = {}, preset = null){
    
    const { run, name, context, createCompleteCbs } = this;
    const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG  // 判斷是不是測試或debug

    if (!preset) {
      if (cliOptions.preset) {

        preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
      } else if (cliOptions.default) {
        // vue create foo --default
      preset = defaults.presets.default
      } else if (cliOptions.inlinePreset) {
      // vue create foo --inlinePreset {...}
        try {
          preset = JSON.parse(cliOptions.inlinePreset)
        } catch (e) {
          error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
          exit(1)
        }
      } else {
        // 進入這個判斷
        preset = await this.promptAndResolvePreset()  // 調用全部選項函數 (4)
        
      }
    }

    // 給preset添加plugins
    preset.plugins['@vue/cli-service'] = Object.assign({
      projectName: name
    }, preset)
    if (cliOptions.bare) {
      preset.plugins['@vue/cli-service'].bare = true
    }

    const packageManager = (
      cliOptions.packageManager ||
      loadOptions().packageManager ||
      (hasYarn() ? 'yarn' : null) ||
      (hasPnpm3OrLater() ? 'pnpm' : 'npm')
    )
    
    // 這個是vue-cli的共享工具裏的函數,添加下載動畫
    logWithSpinner(`✨`, `建立的項目在 ${chalk.cyan(context)}.`)
    
    this.emit('creation', { event: 'creating' })

    // get latest CLI version
    const { latest } = await getVersions()  //  返回當前用戶.vuerc下的版本 || 檢查版本 (5)
    
    // semver包
    // semver包.major(v):返回主版本號。
    // semver包.minor(v):返回次要版本號。
    // 這裏個人.vuerc目錄下版本是3.9.3,這裏進行了拼接latestMinor
    const latestMinor = `${semver.major(latest)}.${semver.minor(latest)}.0`
    
    // generate package.json with plugin dependencies
    const pkg = {
      name,
      version: '0.1.0',
      private: true,
      devDependencies: {}
    }
    const deps = Object.keys(preset.plugins)
    deps.forEach(dep => {
      if (preset.plugins[dep]._isPreset) {
        return
      }
      
   
      pkg.devDependencies[dep] = (
        preset.plugins[dep].version ||
        ((/^@vue/.test(dep)) ? `^${latestMinor}` : `latest`)
      )
    })
    
    // 寫入 package.json
    await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)
    })

    // 在安裝開發以來以前,下載git倉庫,以便vue- clip -service能夠設置git掛鉤。
    const shouldInitGit = this.shouldInitGit(cliOptions)
    
    if (shouldInitGit) {
      
      logWithSpinner(`🗃`, `Initializing git repository...`)
      this.emit('creation', { event: 'git-init' })
      await run('git init')
    }
    
    // install plugins
    stopSpinner()
    log(`⚙  Installing CLI plugins. This might take a while...`)
    log()
    this.emit('creation', { event: 'plugins-install' })
    if (isTestOrDebug) {
      // in development, avoid installation process
      await require('./util/setupDevProject')(context)
    } else {
      // 進入這裏,這個函數是下載package依賴的方法    (3)
      
      await installDeps(context, packageManager, cliOptions.registry)
    }
    
    // run generator
    log(`🚀  Invoking generators...`)
    this.emit('creation', { event: 'invoking-generators' })
    const plugins = await this.resolvePlugins(preset.plugins)
    
    //   ??????  這個Generator比較複雜暫時沒搞懂 ,(腦瓜疼...)
    // const generator = new Generator(context, {
    //   pkg,
    //   plugins,
    //   completeCbs: createCompleteCbs
    // })
    // await generator.generate({
    //   extractConfigFiles: preset.useConfigFiles
    // })


    // install additional deps (injected by generators)
    log(`📦  Installing additional dependencies...`)
    this.emit('creation', { event: 'deps-install' })
    log()
    if (!isTestOrDebug) {
      await installDeps(context, packageManager, cliOptions.registry) // 到這裏就是下載新生成的package.json裏的依賴
    }

    // run complete cbs if any (injected by generators)
    logWithSpinner('⚓', `Running completion hooks...`)
    this.emit('creation', { event: 'completion-hooks' })
    ...
    ...
  }
複製代碼
  • promptAndResolvePreset() (上面4)
// 調用全部選項
  async promptAndResolvePreset (answers = null) {
    // prompt
    if (!answers) {
      answers = await inquirer.prompt(this.resolveFinalPrompts()) 交互選項  (6)
    }
    let preset
    if (answers.preset && answers.preset !== '__manual__') {
      preset = await this.resolvePreset(answers.preset)
    } else {
    // manual
      preset = {
        useConfigFiles: answers.useConfigFiles === 'files',
        plugins: {}
      }
      answers.features = answers.features || []
      // console.log(answers.features)
      // run cb registered by prompt modules to finalize the preset
      this.promptCompleteCbs.forEach(cb => cb(answers, preset))
    }
    // console.log(preset,'preset');
    
    return preset
  }
複製代碼
  • getVersions() (上面5)
...
...
module.exports = async function getVersions () {
  if (sessionCached) {
    return sessionCached
  }

  let latest
  const local = require(`../../package.json`).version
  if (process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG) {
    return (sessionCached = {
      current: local,
      latest: local
    })
  }
  
  const { latestVersion = local, lastChecked = 0 } = loadOptions()
  
  const cached = latestVersion
  const daysPassed = (Date.now() - lastChecked) / (60 * 60 * 1000 * 24)
  
  if (daysPassed > 1) {
    // 在繼續以前,若是咱們一天沒有檢查新版本,等待檢查
    latest = await getAndCacheLatestVersion(cached)
    
  } else {
    // Otherwise, do a check in the background. If the result was updated,
    // it will be used for the next 24 hours.
    
    getAndCacheLatestVersion(cached)
    latest = cached
  }
  
  return (sessionCached = {
    current: local,
    latest
  })
}
...
...
複製代碼
  • resolveFinalPrompts() (上面6)
// 交互全部選項
  resolveFinalPrompts () {
    
    this.injectedPrompts.forEach(prompt => {
      const originalWhen = prompt.when || (() => true)
      prompt.when = answers => {
        
        return isManualMode(answers) && originalWhen(answers)
      }
    })
    const prompts = [
      this.presetPrompt,
      this.featurePrompt,
      ...this.injectedPrompts,
      ...this.outroPrompts
    ]
    return prompts
    
  }
複製代碼

結尾

着陸是不可能的了,哈哈,本人技術有限,若有不對的地方,歡迎指出,敬請諒解!!,後續繼續補充 `Generator()`

最後也指望有大神,能出個比較全套的講解,讓我也學習一下,哈哈哈哈哈哈哈哈
複製代碼

Github地址 歡迎star | fork

相關文章
相關標籤/搜索