Автор: Сяо Лэй
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
Для запуска локальной службы разработки примерный процесс выглядит следующим образом:
Сначала взгляните на службу ввода запуска 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/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, см.официальная документация.