Краткий анализ системы плагинов Vue-cli@3.0

Командная строка JavaScript Vue.js Webpack

Автор: Сяо Лэй

Vue-cli@3.0 — это новая платформа для проектов Vue. В отличие от каркаса на основе шаблонов 1.x/2.x, Vue-cli@3.0 использует архитектуру на основе подключаемых модулей, которая объединяет некоторые основные функции в интерфейсе командной строки и предоставляет разработчикам расширяемый API для разработки. Пользователь может гибко расширять и настраивать функции CLI. Далее давайте посмотрим, как эта архитектура плагина разработана с помощью исходного кода Vue-cli@3.0.

Вся система плагинов содержит 2 важных компонента:

  • @vue/cli предоставляет службы команд cli, такие какvue createсоздать новый проект;
  • @vue/cli-service предоставляет локальную службу сборки для разработки.

@vue/cli-service

когда вы используетеvue create <project-name>Создайте новый проект Vue, вы обнаружите, что сгенерированный проект сильно изменился по сравнению с шаблоном, полученным с удаленного компьютера, когда проект инициализируется в 1.x/2.x, в котором конфигурация, связанная с веб-пакетом и скриптом npm, all Вместо того, чтобы открываться непосредственно в шаблоне, предоставляется новый скрипт npm:

// package.json
"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint"
}

Первые две команды сценария представляют собой локальные службы разработки/сборки, упакованные на основе веб-пакета и связанных с ним плагинов, предоставляемых @vue/cli-service, установленным локально в проекте. @vue/cli-service объединяет функции, предоставляемые веб-пакетом и соответствующими плагинами, в @vue/cli-service для достижения.

Эти две команды соответствуют serve.js и build/index.js в node_modules/@vue/cli-service/lib/commands.

Функция и свойство defaultModes доступны внутри serve.js и build/index.js соответственно для внешнего использования.На самом деле оба они доступны как встроенные плагины для использования vue-cli-service..

Сказав это, давайте посмотрим, как вся система плагинов построена внутри @vue/cli-service. просто прими казньnpm run serveДля запуска локальной службы разработки примерный процесс выглядит следующим образом:

run-serve 流程图

Сначала взгляните на службу ввода запуска cli, предоставляемую @vue/cli-service (@vue/cli-service/bin/vue-cli-service.js):

#!/usr/bin/env node

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

const Service = require('../lib/Service')   // 引入 Service 基类
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())   // 实例化 service

const rawArgv = process.argv.slice(2)
const args = require('minimist')(rawArgv)
const command = args._[0]

service.run(command, args, rawArgv).catch(err => {  // 开始执行对应的 service 服务
  error(err)
  process.exit(1)
})

Увидев это, вы обнаружите, что в корзине нет сервиса, связанного с локальным сервисом разработки, а сервисы, предоставляемые локально установленным @vue/cli-сервисом в проекте, будь то встроенный или подключаемый модуль, являются динамическими.Завершите регистрацию соответствующей службы CLI.

Основной класс Service определен внутри lib/Service.js, который существует как служба времени выполнения @vue/cli. в исполненииnpm run serveПосле этого сначала выполните создание экземпляра службы:

class Service {
  constructor(context) {
    ...
    this.webpackChainFns = []  // 数组内部每项为一个fn
    this.webpackRawConfigFns = []  // 数组内部每项为一个 fn 或 webpack 对象字面量配置项
    this.devServerConfigFns = []
    this.commands = {}  // 缓存动态注册 CLI 命令

    ...
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)   // 完成插件的加载
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {   // 缓存不同 CLI 命令执行时所对应的mode值
      return Object.assign(modes, defaultModes)
    }, {})   
  }
}

В процессе создания экземпляра Сервиса выполняются еще две важные задачи:

  • Загрузить плагин
  • Кэшируйте режим, используемый различными службами команд, предоставляемыми плагином.

Когда служба создана, вызовитеrunметод для запуска службы, предоставляемой соответствующей командой CLI.

async run (name, args = {}, rawArgv = []) {
  const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])

  // load env variables, load user config, apply plugins
  // 执行所有被加载进来的插件
  this.init(mode)

  ...
  const { fn } = command
  return fn(args, rawArgv)  // 开始执行对应的 cli 命令服务
}

init (mode = process.env.VUE_CLI_MODE) {
  ...
  // 执行plugins
  // apply plugins.
  this.plugins.forEach(({ id, apply }) => {
    // 传入一个实例化的PluginAPI实例,插件名作为插件的id标识,在插件内部完成注册 cli 命令服务和 webpack 配置的更新的工作
    apply(new PluginAPI(id, this), this.projectOptions)
  })

  ...
  // apply webpack configs from project config file
  if (this.projectOptions.chainWebpack) {
    this.webpackChainFns.push(this.projectOptions.chainWebpack)
  }
  if (this.projectOptions.configureWebpack) {
    this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
  }
}

Далее давайте взглянем на процесс инстанцирования Сервиса в @vue/cli-service: завершим загрузку плагина через метод resolvePlugins:

 resolvePlugins(inlinePlugins, useBuiltIn) {
    const idToPlugin = id => ({
      id: id.replace(/^.\//, 'built-in:'),
      apply: require(id)    // 加载对应的插件
    })

    let plugins

    // @vue/cli-service内部提供的插件
    const builtInPlugins = [
      './commands/serve',
      './commands/build',
      './commands/inspect',
      './commands/help',
      // config plugins are order sensitive
      './config/base',
      './config/css',
      './config/dev',
      './config/prod',
      './config/app'
    ].map(idToPlugin)

    if (inlinePlugins) {
      plugins = useBuiltIn !== false
        ? builtInPlugins.concat(inlinePlugins)
        : inlinePlugins
    } else {
      // 加载项目当中使用的插件
      const projectPlugins = Object.keys(this.pkg.devDependencies || {})
        .concat(Object.keys(this.pkg.dependencies || {}))
        .filter(isPlugin)
        .map(idToPlugin)
      plugins = builtInPlugins.concat(projectPlugins)
    }

    // Local plugins
    if (this.pkg.vuePlugins && this.pkg.vuePlugins.service) {
      const files = this.pkg.vuePlugins.service
      if (!Array.isArray(files)) {
        throw new Error(`Invalid type for option 'vuePlugins.service', expected 'array' but got ${typeof files}.`)
      }
      plugins = plugins.concat(files.map(file => ({
        id: `local:${file}`,
        apply: loadModule(file, this.pkgContext)
      })))
    }

    return plugins
 }

В этом методе resolvePlugins в основном завершается загрузка плагинов, предоставляемых @vue/cli-service, и плагинов, которые необходимо использовать в приложении проекта (package.json), а соответствующие плагины кэшируются. Внутренние плагины, которые он предоставляет, делятся на две категории:

'./commands/serve'
'./commands/build'
'./commands/inspect'
'./commands/help'

Этот тип подключаемого модуля динамически регистрирует новые команды CLI внутри, и разработчики могут запускать соответствующую службу команд CLI в виде сценария npm.

'./config/base'
'./config/css'
'./config/dev'
'./config/prod'
'./config/app'

Этот тип подключаемого модуля в основном завершает различные связанные конфигурации, когда веб-пакет компилируется и собирается локально. @vue/cli-service объединяет функции разработки и сборки веб-пакета внутри компании.

Плагин загружен, начинаем звонитьservice.runметод, внутри этого метода начните выполнение всех загруженных плагинов:

this.plugins.forEach(({ id, apply }) => {
    apply(new PluginAPI(id, this), this.projectOptions)
  })

Во время выполнения каждого плагина первым полученным параметром является экземпляр PluginAPI, который также является основным базовым классом всего сервиса @vue/cli-service:

class PluginAPI {
  constructor (id, service) {
    this.id = id            // 对应这个插件名
    this.service = service  // 对应 Service 类的实例(单例)
  }
  ...
  registerCommand (name, opts, fn) {  // 注册自定义 cli 命令
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}
  }
  chainWebpack (fn) {     // 缓存变更的 webpack 配置
    this.service.webpackChainFns.push(fn)
  }
  configureWebpack (fn) {   // 缓存变更的 webpack 配置
    this.service.webpackRawConfigFns.push(fn)
  }
  ...
}

Каждый экземпляр API, созданный PluginAPI, предоставляет:

  • Зарегистрируйте службу команд cli (api.registerCommand)
  • Обновите конфигурацию веб-пакета через форму API (api.chainWebpack)
  • Обновите конфигурацию веб-пакета через необработанную конфигурацию (api.configureWebpack),а такжеapi.chainWebpackТо, как API цепочки доступен в конфигурации WEBPACK,api.configureWebpackНеобработанная форма конфигурации приемлема, а конфигурация веб-пакета объединяется с помощью веб-слияния.
  • разрешить конфигурацию wepack (api.resolveWebpackConfig), вызовите изменения в конфигурации веб-пакета, ранее сделанные с помощью chainWebpack и configureWebpack, и сгенерируйте окончательную конфигурацию веб-пакета.
  • ...

Во-первых, давайте взглянем на плагин, предоставленный @vue/cli-service для динамической регистрации сервисов CLI, возьмем сервис serve (./commands/serve) для:

// commands/serve
module.exports = (api, options) => {
  api.registerCommand(
    'serve',
    {
      description: 'start development server',
      usage: 'vue-cli-service serve [options] [entry]',
      options: {
        '--open': `open browser on server start`,
        '--copy': `copy url to clipboard on server start`,
        '--mode': `specify env mode (default: development)`,
        '--host': `specify host (default: ${defaults.host})`,
        '--port': `specify port (default: ${defaults.port})`,
        '--https': `use https (default: ${defaults.https})`,
        '--public': `specify the public network URL for the HMR client`
      }
    },
    async function serve(args) {
      // do something
    }
  )
}

./commands/serveПредоставьте функцию внешнему миру, получите первый параметр API-интерфейса экземпляра PluginAPI и завершите регистрацию команд CLI (т. е. обслуживание службы) с помощью метода registerCommand, предоставленного API.

Давайте посмотрим на плагины, предоставленные внутри @vue/cli-service для настройки веб-пакета (./config/base):

module.exports = (api, options) => {
  api.chainWebpack(webpackConfig => {
    webpackConfig.module
      .rule('vue')
      .test(/\.vue$/)
      .use('cache-loader')
      .loader('cache-loader')
      .options(vueLoaderCacheConfig)
      .end()
      .use('vue-loader')
      .loader('vue-loader')
      .options(
        Object.assign(
          {
            compilerOptions: {
              preserveWhitespace: false
            }
          },
          vueLoaderCacheConfig
        )
      )
  })
}

Этот плагин дополняет базовое содержимое конфигурации веб-пакета, такое как вход, выход и конфигурация загрузчика для загрузки файлов разных типов.В отличие от ранее использовавшегося веб-пакета в стиле конфигурации, @vue/cli-service по умолчанию использует цепочку веб-пакетов (ссылка, пожалуйста, нажмите на меня), чтобы завершить изменение конфигурации веб-пакета. Этот метод также делает настройку веб-пакета более гибкой.При переносе вашего проекта на @vue/cli@3.0 используемый подключаемый модуль веб-пакета также должен использовать конфигурацию в стиле API.В то же время подключаемый модуль должен не только предоставлять функции самого подключаемого модуля, но также необходимо помочь вызывающему абоненту завершить регистрацию подключаемого модуля и другую работу.

@Vue / CLI-Service сойдется с внутренней конфигурацией построения WebPack для реализации, когда у вас нет особых требований к разработке и сборке, можно использовать внутреннюю конфигурацию, разработчикам не нужно заботиться о некоторых деталях. Конечно, в реальной командной разработке внутренняя конфигурация определенно неудовлетворительна, благодаря дизайну сборки плагинов @ Vue-CLI @ 3.0 разработчикам не нужно извлекать внутреннюю конфигурацию, а напрямую использовать @ Vue / CLI-Service Exposed. API-интерфейсы удовлетворили потребность в специальных разработках и строительстве.

Вышеуказанное введение в несколько основных модулей в системе плагина @ vuue / cli-service, а именно:

Service.js предоставляет базовый класс для сервисов, обеспечивает локальную разработку и построение в экосистеме @vue/cli: загрузку плагинов (включая внутренние плагины и плагины приложений проекта), инициализацию плагина, а его синглтон используется всеми плагинами. использует свой синглтон для обновления веб-пакета.

PluginAPI.js предоставляет объектный интерфейс, используемый подключаемыми модулями, который имеет прямое соответствие с подключаемыми модулями. Все локально разработанные плагины для использования @vue/cli-service получают экземпляр PluginAPI в качестве первого аргумента (api), плагин использует этот экземпляр для завершения регистрации команд CLI, выполнения соответствующих сервисов, обновления конфигурации веб-пакета и т. д.

Выше приведен простой анализ системы плагинов @vue/cli-service Заинтересованные студенты могут подробно прочитать соответствующий исходный код (ссылка, пожалуйста, нажмите на меня) учиться.

@vue/cli

В отличие от предыдущих версий 1.x/2.x инструменты vue-cli основаны на удаленных шаблонах для завершения инициализации проекта. может потребоваться для преобразования vue-cli на уровне исходного кода или для помощи разработчикам в инициализации всех файлов конфигурации в удаленном шаблоне. И @vue/cli@3.0 в основном основан на генераторе на основе плагинов для завершения инициализации проекта.Он разбирает исходный большой и всеобъемлющий шаблон на текущий рабочий метод, основанный на системе плагинов, и каждый плагин завершает свое собственное приложение. к проекту работа над расширением шаблона.

@vue/cli предоставляет команды vue в терминале, например:

  • vue create <project>Создайте новый vue-проект
  • vue uiОткройте визуальную конфигурацию vue-cli
  • ...

Когда вам нужно преобразовать vue-cli и настроить скаффолдинг, который соответствует вашим собственным требованиям разработки, вам нужно пройтиРазрабатывайте плагины vue-cli для расширения услуг, предоставляемых vue-cli, для удовлетворения соответствующих требований.. Плагин vue-cli всегда включает в себя плагин службы в качестве основного экспорта, а также, при необходимости, генератор и файл подсказки. Я не буду вдаваться в подробности разработки плагина vue-cli, если вам интересно, то можете прочитатьvue-cli-plugin-eslint

Это в основном для того, чтобы увидеть, как vue-cli проектирует всю систему плагинов и как работает вся система плагинов.

Метод установки плагина, предоставленный @vue/cli@3.0, представляет собой службу cli:vue add <plugin>:

install a plugin and invoke its generator in an already created project

После выполнения этой команды @vue/cli поможет вам загрузить, установить и запустить генератор, предоставленный плагином. Последовательность выполнения всего процесса можно резюмировать следующей блок-схемой:

vue-cli add 流程图

Давайте посмотрим на конкретную логику кода:

// @vue/cli/lib/add.js

async function add (pluginName, options = {}, context = process.cwd()) {

  ...

  const packageManager = loadOptions().packageManager || (hasProjectYarn(context) ? 'yarn' : 'npm')
  // 开始安装这个插件
  await installPackage(context, packageManager, null, packageName)

  log(`${chalk.green('✔')}  Successfully installed plugin: ${chalk.cyan(packageName)}`)
  log()

  // 判断插件是否提供了 generator 
  const generatorPath = resolveModule(`${packageName}/generator`, context)
  if (generatorPath) {
    invoke(pluginName, options, context)
  } else {
    log(`Plugin ${packageName} does not have a generator to invoke`)
  }
}

Сначала подключаемый модуль будет установлен внутри cli, и будет оцениваться, предоставляет ли подключаемый модуль генератор, и если да, то соответствующий генератор будет выполнен.

// @vue/cli/lib/invoke.js

async function invoke (pluginName, options = {}, context = process.cwd()) {
  const pkg = getPkg(context)

  ...
  // 从项目应用package.json中获取插件名
  const id = findPlugin(pkg.devDependencies) || findPlugin(pkg.dependencies)

  ...

  // 加载对应插件提供的generator方法
  const pluginGenerator = loadModule(`${id}/generator`, context)

  ...
  const plugin = {
    id,
    apply: pluginGenerator,
    options
  }

  // 开始执行generator方法
  await runGenerator(context, plugin, pkg)
}

async function runGenerator (context, plugin, pkg = getPkg(context)) {
  ...
  // 实例化一个Generator实例
  const generator = new Generator(context, {
    pkg
    plugins: [plugin],    // 插件提供的generator方法
    files: await readFiles(context),  // 将项目当中的文件读取为字符串的形式保存到内存当中,被读取的文件规则具体见readFiles方法
    completeCbs: createCompleteCbs,
    invoking: true
  })

  ...
  // resolveFiles 将内存当中的所有缓存的 files 输出到文件当中
  await generator.generate({
    extractConfigFiles: true,
    checkExisting: true
  })
}

Подобно @vue/cli-service, внутри @vue/cli также есть базовый класс.Generator, каждый@vue/cliПлагин соответствуетGeneratorпример. в инстанцированииGeneratorВ процессе метода завершается выполнение генератора, предоставленного плагином.

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

module.exports = class Generator {
  constructor (context, {
    pkg = {},
    plugins = [],
    completeCbs = [],
    files = {},
    invoking = false
  } = {}) {
    this.context = context
    this.plugins = plugins
    this.originalPkg = pkg
    this.pkg = Object.assign({}, pkg)
    this.imports = {}
    this.rootOptions = {}
    ...
    this.invoking = invoking
    // for conflict resolution
    this.depSources = {}
    // virtual file tree
    this.files = files
    this.fileMiddlewares = []
    this.postProcessFilesCbs = []

    ...
    const cliService = plugins.find(p => p.id === '@vue/cli-service')
    const rootOptions = cliService
      ? cliService.options
      : inferRootOptions(pkg)
    // apply generators from plugins
    // 每个插件对应生成一个 GeneratorAPI 实例,并将实例 api 传入插件暴露出来的 generator 函数
    plugins.forEach(({ id, apply, options }) => {
      const api = new GeneratorAPI(id, this, options, rootOptions)
      apply(api, options, rootOptions, invoking)
    })
  }
}

Подобно подключаемому модулю, используемому @vue/cli-service, генератор, предоставляемый подключаемым модулем @vue/cli, также предоставляет функцию, получает первый параметр API, а затем использует методы, предоставляемые API, для завершения работы по расширению приложения.

Разработчики используют этот экземпляр API для завершения расширения приложения проекта.Этот экземпляр API обеспечивает:

  • Расширьте метод конфигурации package.json (api.extendPackage)
  • Способ рендеринга файлов шаблонов с помощью ejs (api.render)
  • Функция обратного вызова после того, как все строки файла, хранящиеся в памяти, будут записаны в файл (api.onCreateComplete)
  • вставить в файлimportграмматические приемы (api.injectImports)
  • ...

Например, метод генератора плагина @vue/cli-plugin-eslint в основном дорабатывается: добавление сервисных команд vue-cli-service cli lint, добавление зависимостей родственных стандартных библиотек lint и т. д.:

module.exports = (api, { config, lintOn = [] }, _, invoking) => {
  if (typeof lintOn === 'string') {
    lintOn = lintOn.split(',')
  }

  const eslintConfig = require('./eslintOptions').config(api)

  const pkg = {
    scripts: {
      lint: 'vue-cli-service lint'
    },
    eslintConfig,
    devDependencies: {}
  }

  if (config === 'airbnb') {
    eslintConfig.extends.push('@vue/airbnb')
    Object.assign(pkg.devDependencies, {
      '@vue/eslint-config-airbnb': '^3.0.0-rc.10'
    })
  } else if (config === 'standard') {
    eslintConfig.extends.push('@vue/standard')
    Object.assign(pkg.devDependencies, {
      '@vue/eslint-config-standard': '^3.0.0-rc.10'
    })
  } else if (config === 'prettier') {
    eslintConfig.extends.push('@vue/prettier')
    Object.assign(pkg.devDependencies, {
      '@vue/eslint-config-prettier': '^3.0.0-rc.10'
    })
  } else {
    // default
    eslintConfig.extends.push('eslint:recommended')
  }

  ...

  api.extendPackage(pkg)

  ...

  // lint & fix after create to ensure files adhere to chosen config
  if (config && config !== 'base') {
    api.onCreateComplete(() => {
      require('./lint')({ silent: true }, api)
    })
  }
}

Выше представлено несколько основных модулей, связанных с @vue/cli и системой плагинов, а именно:

add.js предоставляет службу команд cli и функции установки для загрузки плагинов;

invoke.js завершает загрузку генератора и выполняет метод, предоставленный подключаемым модулем, в то время как строка элементов преобразуется из кэш-файла в память;

Generator.js связан с плагинами.Каждый раз, когда @vue/cli добавляет плагин, он создает соответствующий ему экземпляр Generator;

GeneratorAPI.js имеет прямое соответствие с плагинами Это объект API, предоставляемый @vue/cli для плагинов, и предоставляет множество расширений для приложений проекта.


Суммировать

Выше приведен краткий анализ двух основных частей системы плагинов Vue-cli@3.0: @vue/cli и @vue/cli-service.

  • @vue/cli предоставляет команды vue cli, которые отвечают за настройки, создание шаблонов и установку зависимостей плагинов, таких какvue create <projectName>,vue add <pluginName>
  • @vue/cli-service, как внутренний основной плагин во всей системе плагинов @vue/cli, предоставляет обновления конфигурации веб-пакета, локальные службы разработки и сборки.

Первый в основном выполняет управление зависимостями подключаемых модулей, расширение шаблонов проектов и т. д. Второй в основном предоставляет услуги для локальной разработки и построения во время выполнения, а последний также существует как внутренний основной подключаемый модуль во всем подключаемом модуле. в системе @vue/cli. Основные функции также разбираются плагином в системе плагинов, например базовая конфигурация веб-пакета, встроенная в @vue/cli-service, команды скрипта npm и т. д. Оба используют традиционный метод, чтобы предоставить разработчикам возможности расширения плагинов.Подробнее о том, как разрабатывать плагины @vue/cli, см.официальная документация.