[Vue CLI 3]源碼系列之 invoke

vue invoke 在官方文檔中提到的內容不多,不少同窗應該會對它比較陌生javascript

輸入以下命令:前端

vue invoke pwa

以後,它到底作了啥呢?vue

先拋一句命令行的 descriptionjava

invoke the generator of a plugin in an already created project

咱們來從源碼設計角度看看它多作了什麼?node

首先它也是 vue 的一個子命令,在 @vue/cli/bin/vue.jsgit

這裏是咱們屢次提到的命令行必備工具包 commandergithub

const program = require('commander')

設置 commanddescriptionallowUnknownOptionactionvue-cli

program
.command('invoke <plugin> [pluginOptions]')
.description('invoke the generator of a plugin in an already created project')
.allowUnknownOption()
.action((plugin) => {
require('../lib/invoke')(plugin, minimist(process.argv.slice(3)))
})

咱們看一下 @vue/cli/lib/invoke.jsjson

接受 3 個參數微信

  • pluginName
  • options 默認 {}
  • context 默認 process.cwd()
async function invoke (pluginName, options = {},
context = process.cwd()) {
}

先查看項目根目錄下的 package.json 中是否認義了插件依賴

const pkg = getPkg(context)

getPkg 的源碼以下:

function getPkg (context) {}

這裏的 fs 是來自工具包 fs-extra

const fs = require('fs-extra')

先獲取 package.json 文件路徑:

const path = require('path')
const pkgPath = path.resolve(context, 'package.json')

文件不存在(fs.existsSync)會拋錯,代碼以下

if (!fs.existsSync(pkgPath)) {
throw new Error( package.json not found in ${chalk.yellow(context)})
}

核心經過 fs.readJsonSync 讀取對應文件路徑:

const pkg = fs.readJsonSync(pkgPath)

同時會查看 vuePlugins.resolveFrom

if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
return getPkg(path.resolve(context, pkg.vuePlugins.resolveFrom))
}
return pkg

經過函數 findPlugin 從 devDependencies 和 dependencies 中查找:

const id = findPlugin(pkg.devDependencies) ||
findPlugin(pkg.dependencies)

這裏返回的 id 爲:@vue/cli-plugin-pwa

findPlugin 的源碼設計,接受一個函數

const findPlugin = deps => {}

先找官方的 plugin:

if (!deps) return
let name
// official
if (deps[(name = @vue/cli-plugin-${pluginName})]) {
return name
}

// full id, scoped short, or default short
if (deps[(name = resolvePluginId(pluginName))]) {
return name
}

resolvePluginId 處理 3 種狀況:

const {
resolvePluginId
} = require('@vue/cli-shared-utils')

內容以下:定義一個正則:

const pluginRE = /^(@vue/|vue-|@[w-]+/vue-)cli-plugin-/
exports.resolvePluginId = id => {}

一、處理全的 id,如:vue-cli-plugin-foo、@vue/cli-plugin-fo 以及 @bar/vue-cli-plugin-foo

// already full id
// e.g. vue-cli-plugin-foo, @vue/cli-plugin-foo, @bar/vue-cli-plugin-foo
if (pluginRE.test(id)) {
return id
}

二、charAt 判斷以 @開頭的

定義一個正則:

const scopeRE = /^@[w-]+//

具體以下:

// scoped short
  // e.g. @vue/foo, @bar/foo
  if (id.charAt(0) === '@') {}
const scopeMatch = id.match(scopeRE)
    if (scopeMatch) {
      const scope = scopeMatch[0]
      const shortId = id.replace(scopeRE, '')
      return `${scope}${scope === '@vue/' ? `` : `vue-`}cli-plugin-${shortId}`
    }

三、默認狀況

// default short
// e.g. foo
return `vue-cli-plugin-${id}`

發現沒有定義就會提示

if (!id) {
    throw new Error(
      `Cannot resolve plugin ${chalk.yellow(pluginName)} from package.json. ` +
        `Did you forget to install it?`)
}

因此咱們在 package.json 定義了 devDependencies:

"devDependencies": {
"@vue/cli-plugin-pwa": "latest"
}

後面會查找它的 generator 若是不存在也會拋錯

const pluginGenerator = loadModule(`${id}/generator`, context)

  if (!pluginGenerator) {
    throw new Error(`Plugin ${id} does not have a generator.`)
  }

咱們看一下 @vue/cli-plugin-pwa 的目錄:

cli-plugin-pwa

generator

template

看看 loadModule,代碼目錄以下: @vue/cli-shared-utils/lib/module.js

const {
  loadModule
} = require('@vue/cli-shared-utils')

接受 3 個參數:

  • request
  • context
  • force 默認 false
exports.loadModule = function (request, context, force = false) {}

函數內部:

const resolvedPath = exports.resolveModule(request, context)

  if (resolvedPath) {
    if (force) {
      clearRequireCache(resolvedPath)
    }
    return require(resolvedPath)
  }

當命令行沒有傳入參數的時候,會默認作處理:

if (!Object.keys(options).length) {}

看一下 prompts 目錄,目前官方的 plugin 中 cli-plugin-eslint

有 prompts.js 文件

地址:https://github.com/vuejs/vue-...

let pluginPrompts = loadModule( ${id}/prompts, context)

若是像 eslint 同樣存在,判斷對應的類型,最後調用工具包 inquirer

const inquirer = require('inquirer')
if (pluginPrompts) {
      if (typeof pluginPrompts === 'function') {
        pluginPrompts = pluginPrompts(pkg)
      }

      if (typeof pluginPrompts.getPrompts === 'function') {
        pluginPrompts = pluginPrompts.getPrompts(pkg)
      }

     options = await inquirer.prompt(pluginPrompts)
    }

-------------- 重點關注 vue add 的部分會重點關注 -----------------

runGenerator 函數:接受 3 個參數:

  • context
  • plugin
  • pkg 默認 getPkg(context)

函數結構以下:

async function runGenerator (context, plugin, pkg = getPkg(context)) {}

依賴 @vue/cli/lib/Generator.js

const Generator = require('./Generator')

傳入 5 個參數:

  • pkg
  • plugins
  • files
  • completeCbs
  • invoking
const generator = new Generator(context, {
    pkg,
    plugins: [plugin],
    files: await readFiles(context),
    completeCbs: createCompleteCbs,
    invoking: true
  })

依賴 readFiles 函數,代碼以下:這裏加載了工具包 globby

const globby = require('globby')

readFiles 函數結構:

async function readFiles (context) {}

readFiles 函數內部:

const files = await globby(['**'], {
    cwd: context,
    onlyFiles: true,
    gitignore: true,
    ignore: ['**/node_modules/**', '**/.git/**'],
    dot: true
  })

建立一個對象,同時對應 key 的值經過 fs.readFileSync 讀取內容

const res = {}
  for (const file of files) {
    const name = path.resolve(context, file)
    res[file] = isBinary.sync(name)
      ? fs.readFileSync(name)
      : fs.readFileSync(name, 'utf-8')
  }
  return normalizeFilePaths(res)

這裏的 isBinary 使用了工具包 isbinaryfile:

const isBinary = require('isbinaryfile')

normalizeFilePaths 函數來自 util/normalizeFilePaths.js:

const normalizeFilePaths = require('./util/normalizeFilePaths')

調用工具包 slash

const slash = require('slash')

依賴的 normalizeFilePaths 函數以下:接受一個參數

module.exports = function normalizeFilePaths (files) {}

經過 Object.keys 進行循環

Object.keys(files).forEach(file => {
     const normalized = slash(file)
     if (file !== normalized) {
       files[normalized] = files[file]
       delete files[file]
     }
   })
   return files

-------------- 重點關注 --------------

調用 generate 函數,接受 2 個參數:

  • extractConfigFiles 默認 false
  • checkExisting 默認 false
await generator.generate({
    extractConfigFiles: true,
    checkExisting: true
 })

定義一個 class Generator

module.exports = class Generator {
  async generate ({
  } = {}) {
  }
}

重置 package.json:

this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + 'n'

writeFileTree 函數

const writeFileTree = require('./util/writeFileTree')

依賴的工具包:

const fs = require('fs-extra')
const path = require('path')

源碼結構以下:接受 3 個參數

module.exports = async function writeFileTree (dir, files, previousFiles) {}
if (previousFiles) {
    await deleteRemovedFiles(dir, files, previousFiles) 
}

核心是 fs.ensureDirfs.writeFile

return Promise.all(Object.keys(files).map(async (name) => {
    const filePath = path.join(dir, name)
    await fs.ensureDir(path.dirname(filePath))
    await fs.writeFile(filePath, files[name])
  }))

deleteRemovedFiles 函數結構以下:

function deleteRemovedFiles (directory, newFiles, previousFiles) {}

經過 Object.keys 找出要刪除的文件 filesToDelete

// get all files that are not in the new filesystem and are still existing
const filesToDelete = Object.keys(previousFiles)
  .filter(filename => !newFiles[filename])

經過 fs.unlink 刪除

// delete each of these files
return Promise.all(filesToDelete.map(filename => {
  return fs.unlink(path.join(directory, filename))
}))
本文來自微信公衆號:前端新視野
相關文章
相關標籤/搜索