[Vue CLI 3]源码系列之 invoke

vue invoke 在官方文档中提到的内容很少,很多同学应该会对它比较陌生

输入如下命令:

vue invoke pwa

之后,它到底做了啥呢?

先抛一句命令行的 description

invoke the generator of a plugin in an already created project

我们来从源码设计角度看看它多做了什么?

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

这里是我们多次提到的命令行必备工具包 commander

const program = require('commander')

设置 commanddescriptionallowUnknownOptionaction

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.js

接受 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 => {}

1、处理全的 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
}

2、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}`
    }

3、默认情况

// 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-cli/blob/dev/packages/%40vue/cli-plugin-eslint/prompts.js

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))
}))

本文来自微信公众号:前端新视野

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容

  • 在官方文档中,我们可以看到在新版本的 Vue CLI 中去掉了我们熟悉的 `config` 目录 比如之前在 pr...
    dailyvuejs阅读 3,639评论 0 2
  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 2,887评论 1 4
  • 用惯老版本 Vue CLI 的同学一般多会选择使用如下命令来创建模板项目: vue init webpack de...
    dailyvuejs阅读 3,366评论 0 1
  • 今天看方励的演讲,发现自己的三观竟然和方励出奇地一致。当然,我是后来才悟出来的“关于人要怎样过一生的话题”。 ...
    纪梵希阅读 665评论 0 1
  • 清明 闲花方次第,风雨落香尘。 本是无根客,谁怜有限身。 荒丘难觅路,野草自争春。 最惧团圆日,觥筹少一人。 中秋...
    杨花点点点点点阅读 227评论 2 1