
Интеграция в компонент
Пошаговое руководство по интеграции Vue 3 + PrimeVue в компонент MODX 3 с использованием VueTools.
Настройка Vite
В vite.config.js укажите внешние зависимости, которые не должны включаться в бандл:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import prefixSelector from 'postcss-prefix-selector'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
// Эти модули НЕ бандлятся — берутся из Import Map
external: [
'vue',
'pinia',
'primevue',
'@vuetools/useApi',
'@vuetools/useLexicon',
'@vuetools/useModx',
'@vuetools/usePermission'
],
output: {
format: 'es',
entryFileNames: '[name].min.js',
chunkFileNames: '[name].min.js'
}
}
},
// Изоляция стилей от ExtJS
css: {
postcss: {
plugins: [
prefixSelector({
prefix: '.vueApp',
exclude: [/^:root/, /^\.p-/, /^\.pi/, /^\[data-p-/]
})
]
}
}
})Ключевой момент
Массив external указывает Vite НЕ включать эти зависимости в бандл. Браузер загрузит их из Import Map, зарегистрированного VueTools.
Установка зависимостей
npm install postcss-prefix-selector --save-devЗагрузка скриптов в PHP контроллере
Базовый подход
<?php
class MyComponentManagerController extends modExtraManagerController
{
public function loadCustomCssJs()
{
$assetsUrl = $this->myComponent->config['assetsUrl'];
// CSS вашего компонента (идёт в <head>)
$this->addCss($assetsUrl . 'css/mgr/vue-dist/my-widget.min.css');
// ES modules ОБЯЗАТЕЛЬНО через regClientStartupHTMLBlock
$this->modx->regClientStartupHTMLBlock(
'<script type="module" src="' . $assetsUrl . 'js/mgr/vue-dist/my-widget.min.js"></script>'
);
}
}Критично
- Используйте
regClientStartupHTMLBlock()для<script type="module"> - НЕ используйте
addJavascript()илиaddLastJavascript()для ES modules — они не поддерживаютtype="module"
Правильная регистрация нескольких скриптов
// ✅ ПРАВИЛЬНО — отдельные вызовы
$this->modx->regClientStartupHTMLBlock(
'<script type="module" src="' . $assetsUrl . 'js/mgr/vue-dist/widget1.min.js"></script>'
);
$this->modx->regClientStartupHTMLBlock(
'<script type="module" src="' . $assetsUrl . 'js/mgr/vue-dist/widget2.min.js"></script>'
);
// ❌ НЕПРАВИЛЬНО — multiline строка с несколькими тегами
$this->modx->regClientStartupHTMLBlock('
<script type="module" src="' . $assetsUrl . 'js/mgr/vue-dist/widget1.min.js"></script>
<script type="module" src="' . $assetsUrl . 'js/mgr/vue-dist/widget2.min.js"></script>
');Проверка наличия VueTools
При отсутствии VueTools на сайте Vue модули не загрузятся, а в консоли появятся ошибки. Рекомендуется реализовать проверку и показывать понятное сообщение пользователю.
Метод addVueModule()
Создайте метод в базовом контроллере:
<?php
class MyComponentManagerController extends modExtraManagerController
{
/**
* Флаг регистрации скрипта проверки (один раз на страницу)
*/
protected static $vueCoreCheckRegistered = false;
/**
* Регистрация Vue ES module с проверкой зависимости VueTools
*
* @param string $src URL скрипта модуля
*/
public function addVueModule(string $src): void
{
// Регистрируем скрипт проверки только один раз на страницу
if (!self::$vueCoreCheckRegistered) {
$this->registerVueCoreCheck();
self::$vueCoreCheckRegistered = true;
}
// Добавляем версию для сброса кэша
$src = $src . '?v=' . $this->myComponent->version;
// Регистрируем модуль с атрибутом data-vue-module
$this->modx->regClientStartupHTMLBlock(
'<script type="module" data-vue-module src="' . $src . '"></script>'
);
}
/**
* Регистрация inline скрипта проверки Import Map
*/
protected function registerVueCoreCheck(): void
{
$alertTitle = $this->modx->lexicon('mycomponent_error') ?: 'Error';
$alertMessage = $this->modx->lexicon('mycomponent_vuetools_required')
?: 'VueTools package is required. Please install it from Package Manager.';
$script = <<<JS
<script>
(function() {
var importMap = document.querySelector('script[type="importmap"]');
var hasVueCore = false;
if (importMap) {
try {
var mapContent = JSON.parse(importMap.textContent);
hasVueCore = mapContent.imports && mapContent.imports.vue;
} catch (e) {
hasVueCore = false;
}
}
if (!hasVueCore) {
// Удаляем все скрипты с атрибутом data-vue-module
document.querySelectorAll('script[type="module"][data-vue-module]').forEach(function(el) {
el.remove();
});
// Показываем MODX алерт
if (typeof Ext !== 'undefined') {
Ext.onReady(function() {
if (typeof MODx !== 'undefined' && MODx.msg) {
MODx.msg.alert('{$alertTitle}', '{$alertMessage}');
} else {
alert('{$alertMessage}');
}
});
} else {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
if (typeof MODx !== 'undefined' && MODx.msg) {
MODx.msg.alert('{$alertTitle}', '{$alertMessage}');
} else {
alert('{$alertMessage}');
}
}, 500);
});
}
window.MY_COMPONENT_VUE_CORE_MISSING = true;
}
})();
</script>
JS;
$this->modx->regClientStartupHTMLBlock($script);
}
}Использование
public function loadCustomCssJs()
{
$assetsUrl = $this->myComponent->config['assetsUrl'];
// CSS (как обычно)
$this->addCss($assetsUrl . 'css/mgr/vue-dist/my-widget.min.css');
// ✅ С проверкой зависимости
$this->addVueModule($assetsUrl . 'js/mgr/vue-dist/my-widget.min.js');
$this->addVueModule($assetsUrl . 'js/mgr/vue-dist/another-widget.min.js');
}Лексиконы
Добавьте лексиконы для сообщения об ошибке:
// lexicon/ru/default.inc.php
$_lang['mycomponent_error'] = 'Ошибка';
$_lang['mycomponent_vuetools_required'] = 'Для работы требуется пакет VueTools. Установите его через Менеджер пакетов.';
// lexicon/en/default.inc.php
$_lang['mycomponent_error'] = 'Error';
$_lang['mycomponent_vuetools_required'] = 'VueTools package is required. Please install it via Package Manager.';Результат
| Без проверки | С проверкой |
|---|---|
Ошибки Failed to resolve module specifier "vue" в консоли | Чистая консоль |
| Vue виджеты не работают | Понятный MODX алерт с инструкцией |
| Пользователь не понимает проблему | Пользователь знает что делать |
Использование в Vue компонентах
<script setup>
// Vue импортируется из Import Map (не бандлится)
import { ref, computed, onMounted } from 'vue'
// Pinia из Import Map
import { createPinia } from 'pinia'
// PrimeVue компоненты из Import Map
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
// Composables из VueTools
import { useLexicon } from '@vuetools/useLexicon'
import { useModx } from '@vuetools/useModx'
import { usePermission } from '@vuetools/usePermission'
const { _ } = useLexicon()
const { modx, config } = useModx()
const { hasPermission } = usePermission()
// Ваш код компонента
const items = ref([])
const canEdit = computed(() => hasPermission('my_component_edit'))
</script>
<template>
<div class="my-component">
<h1>{{ _('my_component_title') }}</h1>
<Button
v-if="canEdit"
:label="_('my_component_add')"
icon="pi pi-plus"
/>
<DataTable :value="items">
<Column field="name" :header="_('my_component_name')" />
</DataTable>
</div>
</template>Entry Point (точка входа)
Создайте entry point для инициализации Vue приложения:
// src/entries/my-widget.js
import '../scss/styles.scss'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import Aura from '@primevue/themes/aura'
import ToastService from 'primevue/toastservice'
import ConfirmationService from 'primevue/confirmationservice'
import MyWidget from '../components/MyWidget.vue'
let appInstance = null
function createVueApp(props = {}) {
const app = createApp(MyWidget, props)
app.use(createPinia())
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
darkModeSelector: 'none'
}
}
})
app.use(ToastService)
app.use(ConfirmationService)
return app
}
export function init(selector = '#my-vue-widget', props = {}) {
const el = document.querySelector(selector)
if (!el) {
console.warn(`[MyWidget] Element ${selector} not found`)
return null
}
// Предотвращаем повторную инициализацию
if (el.dataset.vApp === 'true') {
return appInstance
}
appInstance = createVueApp(props)
appInstance.mount(selector)
el.dataset.vApp = 'true'
return appInstance
}
export function destroy() {
if (appInstance) {
appInstance.unmount()
appInstance = null
}
}
// Export для глобального доступа из ExtJS
window.MyComponentWidget = { init, destroy }Интеграция в ExtJS вкладку
// В ExtJS панели
{
title: _('my_tab_title'),
id: 'my-vue-tab',
html: '<div id="my-vue-widget" class="vueApp"></div>',
listeners: {
activate: function() {
// Инициализация при активации вкладки
if (window.MyComponentWidget) {
const el = document.querySelector('#my-vue-widget')
if (el && el.dataset.vApp !== 'true') {
window.MyComponentWidget.init('#my-vue-widget', {
someId: config.record.id
})
}
}
}
}
}Важно
Не забудьте добавить класс vueApp к контейнеру — без него стили PrimeVue не применятся.
Собственный API клиент
Если ваш компонент использует собственный роутер (не стандартный MODX connector), создайте локальный request.js:
// src/request.js
class Request {
getConnectorUrl() {
return window.myComponent?.config?.connector_url
|| '/assets/components/mycomponent/connector.php'
}
getModAuthToken() {
return window.MODx?.siteId || null
}
buildUrl(route, params = {}) {
const url = new URL(this.getConnectorUrl(), window.location.origin)
// Ваш процессор-роутер
url.searchParams.set('action', 'MyComponent\\Processors\\Api\\Index')
url.searchParams.set('route', route)
const token = this.getModAuthToken()
if (token) {
url.searchParams.set('HTTP_MODAUTH', token)
}
Object.entries(params).forEach(([key, value]) => {
if (value != null) url.searchParams.set(key, value)
})
return url.toString()
}
async request(method, route, data = null) {
const options = {
method,
headers: { 'Accept': 'application/json' },
credentials: 'same-origin'
}
let url
if (method === 'GET' && data) {
url = this.buildUrl(route, data)
} else {
url = this.buildUrl(route)
if (data) {
options.headers['Content-Type'] = 'application/json'
options.body = JSON.stringify(data)
}
}
const response = await fetch(url, options)
const result = await response.json()
if (!result.success) {
throw new Error(result.message || 'Request failed')
}
return result.object || result.data || result
}
get(route, params) { return this.request('GET', route, params) }
post(route, data) { return this.request('POST', route, data) }
put(route, data) { return this.request('PUT', route, data) }
delete(route, data) { return this.request('DELETE', route, data) }
}
export default new Request()Использование:
// Вместо useApi из VueTools
import request from '../request.js'
const products = await request.get('/api/products', { limit: 20 })
await request.post('/api/products', { name: 'New Product' })Чеклист интеграции
- [ ] Добавить
vuetoolsв зависимости пакета (setup options) - [ ] Настроить
externalв vite.config.js - [ ] Настроить postcss prefix selector для изоляции стилей
- [ ] Реализовать
addVueModule()с проверкой зависимости - [ ] Добавить лексиконы для сообщения об ошибке
- [ ] Использовать
addVueModule()вместоregClientStartupHTMLBlock() - [ ] Добавить
class="vueApp"к контейнерам Vue - [ ] Загрузить топики лексиконов в контроллере
- [ ] Создать локальный
request.jsесли используете собственный роутер
Примеры
- MiniShop3 — полная интеграция с собственным роутером
