Использование компонентов Vue.js в приложениях PHP

Практическое руководство по объединению компонентов Vue.js с веб-пакетом для использования в приложениях PHP.

Часто думают, что Vue.js используется только в больших одностраничных приложениях, полностью написанных на JavaScript. Однако это не единственный способ его использования. Вы можете отображать свои страницы с помощью PHP и внедрять только интерактивные компоненты в Vue.js, так же, как вы использовали бы jQuery несколько лет назад.

Такое гибридное решение зачастую проще и эффективнее. Он хорошо работает, особенно на мобильных устройствах, где загрузка и запуск больших приложений JavaScript может быть медленным. Это также отличный способ улучшить существующее приложение PHP или постепенно перенести его на Vue.js.

Объединение скриптов с помощью webpack

Чтобы использовать Vue.js, нам нужно настроить весь процесс сборки для преобразования файлов .vue, таблиц стилей и всех зависимостей во что-то, что браузер может обработать.

Шаблон по умолчанию для нового проекта Vue.js поставляется с кучей сценариев сборки и файлов конфигурации, которые делают webpack похожим на черную магию вуду, особенно если вы новичок в этих технологиях, как я.

Но на самом деле не так сложно понять весь процесс объединения и настроить его вручную с нуля. Я постараюсь объяснить это шаг за шагом, чтобы вы могли лучше понять, как это работает.

Прежде чем мы начнем, имейте в виду, что не существует единственного «правильного» способа настройки webpack. Все зависит от того, чего вы хотите достичь. Для упрощения сделаю несколько предположений:

  • Все упаковано в один файл .js и один файл .css. Это хорошее начало, и вы можете добавить разделение кода по мере роста вашего пакета.
  • Наш пакет состоит из файлов .js, компонентов .vue, таблиц стилей и изображений.
  • Таблицы стилей могут быть написаны с использованием Less или другого процессора стилей, но это необязательно.
  • Большая часть приложения написана на PHP, поэтому нам нужен способ вставки связанных файлов .js и .css в страницы, сгенерированные PHP.
  • Мы передадим данные из PHP в скрипт, чтобы создать и настроить наши компоненты.
  • Мы хотим использовать сервер разработки веб-пакетов для компиляции на лету и горячей перезагрузки.

Структура проекта

Прежде чем мы начнем настраивать веб-пакет, нам нужно понять структуру папок в нашем проекте. В моем примере это выглядит так:

Исходники нашего бандла находятся в папке src/. Главный файл, который включает в себя все остальные файлы, называется main.js. Мы разместим компоненты Vue.js во вложенной папке src/components/, таблицы стилей - в src/styles/, а изображения - в src/images/.

Обратите внимание, что здесь нужно размещать только изображения, которые напрямую используются нашим пакетом. Это включает, например, фоновые изображения, используемые таблицами стилей. Изображения, которые используются только приложением PHP, должны быть размещены в другом месте, чтобы они не смешивались вместе.

Файл конфигурации webpack помещается в src/build/, поэтому он не загромождает корневую папку нашего приложения.

Сгенерированный пакет помещается в папку assets/. Он содержит свернутый скрипт и таблицу стилей, а также все изображения, скопированные из src/images/. Вы можете изменить расположение пакета, но вам необходимо соответствующим образом изменить конфигурацию примера.

Другие файлы и папки, например app/ и lib/, являются частью приложения PHP. Их имена могут быть разными, но это не очень важно, потому что webpack их не тронет.

Помните, что при развертывании приложения на производственном сервере не включайте папки src/ и node_modules/. Все, что вам нужно развернуть, будет помещено в папку assets/.

Добавление зависимостей

Вам необходимо создать package.json файл в корневой папке приложения. Команда npm init может сделать это за вас. Я предполагаю, что у вас уже есть базовые представления о Node.js и npm.

Добавьте в package.json следующие модули:

  "dependencies": {
    "vue": "^2.3.3"
  },
  "devDependencies": {
    "assets-webpack-plugin": "^3.5.1",
    "babel-core": "^6.0.0",
    "babel-loader": "^6.0.0",
    "babel-preset-env": "^1.6.0",
    "css-loader": "^0.25.0",
    "extract-text-webpack-plugin": "^2.1.2",
    "file-loader": "^0.9.0",
    "less": "^2.7.2",
    "less-loader": "^4.0.4",
    "vue-loader": "^12.1.0",
    "vue-template-compiler": "^2.3.3",
    "webpack": "^2.6.1",
    "webpack-dev-server": "^2.5.0"
  }

Затем запустите npm update, чтобы загрузить все модули и при необходимости обновить их до последней версии.

Обратите внимание, что в этом примере я использую Less, но вы можете использовать другой обработчик стилей, например Sass, или просто простой CSS. Вместо этого просто установите соответствующие модули.

Настройка webpack

Начнем с очень простого webpack.config.js файла:

const path = require( 'path' );
const webpack = require( 'webpack' );
module.exports = function( env = {} ) {
  if ( env.production )
    process.env.NODE_ENV = 'production';
  return {
    entry: './src/main.js',
    output: {
      path: path.resolve( __dirname, '../../assets' ),
      filename: env.production ? 'js/main.min.js' : 'js/main.js'
    },
    plugins: env.production ? [
      new webpack.DefinePlugin( {
        'process.env': {
          NODE_ENV: '"production"'
        }
      } ),
      new webpack.optimize.UglifyJsPlugin( {
        compress: {
          warnings: false
        }
      } ),
    ] : [
    ],
    devtool: env.production ? false
      : '#cheap-module-eval-source-map'
  };
};

Вместо того, чтобы разделять конфигурацию производства и разработки на отдельные файлы, мы можем использовать один файл конфигурации, который экспортирует функцию. Он сгенерирует правильную конфигурацию в зависимости от среды.

Мы говорим webpack принять src/main.js файл в качестве входных и создать выходной файл с именем main.min.js в производственном режиме и main.js в режиме разработки.

Поскольку сам файл конфигурации находится в папке src/build/, нам нужно разрешить путь к папке assets/, используя относительный путь.

В производственном режиме мы используем UglifyJsPlugin, чтобы минимизировать сгенерированный скрипт. Мы отключаем warnings, который может быть сгенерирован кодом, вставленным веб-пакетом.

Параметр devtool указывает, что исходные карты будут создаваться только для разработки и будут отключены в производственной среде. Вы можете изменить это, если вам нужны производственные исходные карты.

Благоприятная производственная среда

В производственном режиме мы устанавливаем переменную NODE_ENV на "production". Это позволяет сделать некоторые оптимизации в vue-loader, которые мы добавим позже.

Мы также передаем эту переменную в DefinePlugin, чтобы удалить ненужный код отладки из Vue.js.

Наши скрипты также могут использовать такой отладочный код:

if ( process.env.NODE_ENV != 'production' )
  console.log( 'some debugging information' );

При сборке производственного пакета DefinePlugin заменит process.env.NODE_ENV на "production", условие будет оцениваться как ложное, а отладочный код будет полностью удален во время минификации.

Запуск веб-пакета

Чтобы запустить webpack в режиме разработки, вы можете использовать следующую команду:

webpack-dev-server --config src/build/webpack.config.js

Чтобы собрать производственный пакет, запустите эту команду:

webpack --config src/build/webpack.config.js --env.production

Флаг --env.production передается в качестве аргумента нашей функции в файле конфигурации. При необходимости вы можете передать дополнительные параметры.

Вы можете разместить эти команды в разделе scripts файла package.json. Тогда вы сможете быстро запустить их с помощью команды npm run.

Настройка загрузчиков веб-пакетов

Загрузчики - это дополнительные модули, которые сообщают webpack, что делать с файлами определенного типа. Начнем с загрузчика файлов .vue:

module: {
  rules: [
    {
      test: /\.vue$/,
      loader: 'vue-loader',
      options: {
        loaders: {
          css: makeStyleLoader(),
          less: makeStyleLoader( 'less' )
        }
      }
    }
  ]
}

Это указывает webpack обрабатывать файлы .vue с помощью vue-loader. Он автоматически преобразует HTML-шаблоны в JavaScript и обрабатывает встроенные таблицы стилей.

Нам нужно настроить внутренние загрузчики, которые будут указывать vue-loader, как обрабатывать эти таблицы стилей. Эта конфигурация позволяет использовать таблицы стилей CSS и Less в файлах .vue. Вы можете использовать другой обработчик стилей или удалить его, если вам нужен только CSS. О функции makeStyleLoader() я расскажу позже.

Другое правило скажет webpack обрабатывать файлы .js с помощью Babel:

{
  test: /\.js$/,
  loader: 'babel-loader',
  exclude: /node_modules/
}

Это позволит нам использовать синтаксис ES6 в наших файлах .js. Обратите внимание, что vue-loader также использует babel-loader под капотом, поэтому мы можем использовать ES6 и в файлах .vue.

Мы создадим простой .babelrc файл, чтобы сообщить Babel использовать babel-env-preset и оставить обработку модуля webpack:

{
  "presets": [
    [ "env", { "modules": false } ]
  ]
}

Возможно, вам потребуется настроить эту конфигурацию, но это тема для другой статьи.

Чтобы иметь возможность импортировать файлы .css и .less из файлов .js, мы добавим еще один набор правил:

{
  test: /\.css$/,
  use: makeStyleLoader()
},
{
  test: /\.less$/,
  use: makeStyleLoader( 'less' )
}

Наконец, давайте добавим правило для изображений:

{
  test: /\.(png|jpg)$/,
  loader: 'file-loader',
  options: {
    name: 'images/[name].[ext]'
  }
}

Таким образом изображения будут скопированы в папку assets/images/ при создании производственного пакета. Их первоначальные имена будут сохранены.

Чтобы ссылаться на изображения из ваших таблиц стилей, вы можете просто использовать относительный путь, например:

#my-logo {
  background-image: url('../images/my-logo.png');
}

Вы также можете ссылаться на изображения из тегов <img> в своих компонентах Vue.js.

Чтобы гарантировать создание правильных путей в сгенерированных файлах CSS, нам нужно будет настроить параметр publicPath, но я вернусь к этому через некоторое время.

Разрешение путей

Раздел resolve сообщает webpack, как разрешать пути при импорте файлов.

Давайте посмотрим на этот пример:

resolve: {
  extensions: [ '.js', '.vue', '.json' ],
  alias: {
    '@': path.resolve( __dirname, '..' )
  }
}

Массив extensions содержит расширения файлов, которые можно не указывать.

Параметр alias очень мощный, но особенно полезный трюк - это определение @ как псевдонима папки src/. Таким образом, вам не придется использовать относительные пути с большим количеством ../.

Например, чтобы импортировать компонент Vue.js, вы можете просто написать:

import PageComponent from '@/components/PageComponent'

Вы также можете импортировать таблицы стилей в JavaScript:

import '@/styles/global.less'

В таблице стилей Less вы можете импортировать другие файлы CSS или Less, используя следующий синтаксис:

@import "~@/styles/variables.less";

~ указывает less-loader разрешить путь с помощью правил веб-пакетов, а не рассматривать его как относительный путь.

Тильду также можно использовать для импорта файлов из другого модуля, например:

@import "~bootstrap/less/bootstrap.less";

Также предыдущий пример ссылки на изображение можно записать так:

#my-logo {
  background-image: url('~@/images/my-logo.png');
}

Конфигурация разработки

Сборка всего пакета каждый раз, когда вы вносите простое изменение, займет много времени, поэтому во время разработки вам следует использовать сервер разработки веб-пакетов, который сделает его намного быстрее за счет перекомпиляции только тех файлов, которые вы изменяете.

Но вы уже используете Apache или IIS для размещения PHP-приложения, так как же эти два сервера могут работать вместе?

Во-первых, давайте определим publicPath, где будут обслуживаться наши ресурсы:

output: {
  // ... path and file name ...
  publicPath: 'http://localhost:8080/'
}

URL нашего скрипта будет http://localhost:8080/js/main.js. Обратите внимание, что сервер разработки webpack по умолчанию использует порт 8080.

Также давайте добавим раздел конфигурации devServer:

devServer: {
  contentBase: false,
  hot: true,
  headers: {
    'Access-Control-Allow-Origin': '*'
  }
}

Отключение contentBase указывает серверу разработки веб-пакетов обслуживать только те файлы, которые являются частью нашего пакета. Все остальное будет обслуживаться Apache или IIS.

Приятная особенность сервера разработки webpack - горячая замена модулей. Это позволяет вам сразу увидеть ваши изменения, не обновляя страницу. Чтобы он заработал, мы должны сначала включить опцию hot.

Мы также устанавливаем заголовок Access-Control-Allow-Origin на *. Это необходимо, потому что наше приложение PHP работает на другом порту, чем сервер разработки веб-пакетов. Без него браузер блокировал бы запросы AJAX, которые извлекают обновленные скрипты.

Наконец, мы должны добавить HotModuleReplacementPlugin при запуске webpack в режиме разработки:

plugins: env.production ? [
  // ... production plugins ...
] : [
  new webpack.HotModuleReplacementPlugin()
]

Теперь, когда вы изменяете исходный файл, он не только на лету перекомпилируется с помощью webpack, но и внедряется на вашу веб-страницу. Потрясающие! Попробуйте сделать это с помощью jQuery… 😉.

Конфигурация производства

Давайте настроим конфигурацию нашего пакета для производственных сборок.

Я уже упоминал UglifyJsPlugin, который минимизирует сгенерированный файл JavaScript.

Нам также необходимо извлечь все стили в отдельный файл CSS. Мы делаем это только в производственном режиме. Во время разработки стили вводятся сценарием непосредственно в заголовок страницы. Таким образом, горячая перезагрузка также может работать для таблиц стилей.

Сначала нам нужно загрузить extract-text-webpack-plugin, потому что он не включен в веб-пакет:

const ExtractTextPlugin = require( 'extract-text-webpack-plugin' );

Мы добавляем этот плагин в список рабочих плагинов и указываем имя сгенерированного файла CSS:

new ExtractTextPlugin( {
  filename: 'css/style.min.css'
} )

Теперь нам нужно использовать этот плагин с загрузчиками стилей. Это обрабатывается функцией makeStyleLoader(), о которой я упоминал ранее:

function makeStyleLoader( type ) {
  const cssLoader = {
    loader: 'css-loader',
    options: {
      minimize: env.production
    }
  };
  const loaders = [ cssLoader ];
  if ( type )
    loaders.push( type + '-loader' );
  if ( env.production ) {
    return ExtractTextPlugin.extract( {
      use: loaders,
      fallback: 'vue-style-loader'
    } );
  } else {
    return [ 'vue-style-loader' ].concat( loaders );
  }
}

Обратите внимание, что эта функция должна быть помещена внутри основной функции, экспортированной из файла конфигурации веб-пакета, чтобы она могла получить доступ к аргументу env.

Параметр minimize для css-loader включен в рабочем режиме, поэтому извлеченный файл CSS будет минимизирован.

Функция связывает дополнительный загрузчик стилей, если указан аргумент type. Таким образом, мы можем справиться с процессором Less или другого стиля.

В производственном режиме мы передаем эти загрузчики плагину извлечения текста.

В режиме разработки мы используем vue-style-loader, который вставляет стили в заголовок страницы и обрабатывает горячую перезагрузку.

Использование скрипта в приложении PHP

В режиме разработки мы можем просто добавить следующий тег в конце тела страницы, который генерируется PHP для загрузки скрипта:

<script src="http://localhost:8080/js/main.js"></script>

Сервер разработки веб-пакетов также будет обслуживать любые изображения, которые включены в наши компоненты или таблицы стилей. В предыдущем примере изображение было указано с использованием относительного пути, например '../images/my-logo.png'. Этот путь автоматически переводится с использованием указанного publicPath, поэтому он станет следующим:

#my-logo {
  background-image: url('http:/localhost:8080/images/my-logo.png');
}

Перевод также произойдет, если вы используете тег <img> в шаблоне Vue.js.

Загрузка производственного пакета

Когда мы развертываем наше PHP-приложение в производственной среде, мы загружаем связанные стили и скрипт из папки assets/, например:

<link href="http://example.com/assets/css/style.min.css"
      rel="stylesheet">
<script src="http://example.com/assets/js/main.min.js"></script>

Мы также должны изменить publicPath в производственном режиме. В противном случае пути к изображениям в таблице стилей будут относиться к серверу разработки.

Использование абсолютного URL-адреса - не лучший вариант, поскольку мы не всегда знаем URL-адрес сервера, на котором будет установлен наш код. Вместо этого мы будем использовать относительный путь:

publicPath: env.production ? '../' : 'http://localhost:8080/'

Таким образом, переведенный путь будет таким же, как и исходный, например '../images/my-logo.png'. Это будет работать, потому что наша таблица стилей находится в assets/css/, а изображения - в assets/images/.

Однако такие относительные пути в тегах <img> работать не будут, потому что браузер попытается разрешить их на основе URL-адреса текущей страницы.

К счастью, общий путь можно изменить во время выполнения с помощью специальной переменной __webpack_public_path__. Мы можем установить его в начале нашего скрипта, например:

if ( process.env.NODE_ENV == 'production' )
  __webpack_public_path__ = 'http://example.com/assets/';

Мы делаем это только в производственном режиме, потому что в процессе разработки публичный путь уже правильно установлен на http://localhost:8080/.

Но это по-прежнему не решает проблему, мы не можем жестко запрограммировать публичный путь в скрипте, если мы его не знаем. Мы можем определить его автоматически, например, на основе window.location, или использовать код PHP для передачи базового URL-адреса в качестве параметра скрипту. Я покажу вам, как передавать параметры из PHP.

Очистка кеша

Есть еще одна вещь, которую мы должны учитывать при развертывании нашего приложения в производственной среде. Браузер сохранит связанные файлы в локальном кеше, поэтому ему не придется каждый раз загружать их с сервера. Но даже если мы опубликуем новую версию на сервере, браузер все равно может использовать кешированную старую версию.

Простое решение этой проблемы называется «очистка кеша»: мы включаем версию файла в URL-адрес, например main.min.js?v=1.

Нам не нужно обновлять версию вручную, потому что webpack может справиться с этим за нас с помощью нескольких простых изменений. Он будет использовать хэши на основе содержимого файла, чтобы гарантировать, что URL-адрес изменяется при каждом обновлении файла.

Мы можем включить хеш в имя файла выходного скрипта:

filename: env.production ? 'js/main.min.js?[chunkhash]'
 : 'js/main.js'

Мы также можем передать хэш извлеченному имени файла CSS:

new ExtractTextPlugin( {
  filename: 'css/style.min.css?[contenthash]'
} ),

Также file-loader поддерживает эту опцию:

{
  test: /\.(png|jpg)$/,
  loader: 'file-loader',
  options: {
    name: 'images/[name].[ext]?[hash]'
  }
}

Также можно включить хэш в имя файла вместо использования строки запроса, например js/main-[chunkhash].min.js. Оба решения имеют свои преимущества и недостатки, поэтому выбирайте то, что лучше всего подходит для вас.

Но как мы узнаем эти хэши, когда мы включаем на страницу файлы таблиц стилей и скриптов?

Мы можем использовать assets-webpack-plugin, чтобы извлечь их для нас:

const AssetsPlugin = require( 'assets-webpack-plugin' );

Давайте добавим его в конфигурацию нашего веб-пакета в производственном режиме и настроим следующим образом:

new AssetsPlugin( {
  filename: 'assets.json',
  path: path.resolve( __dirname, '../../assets' ),
  fullPath: false
} )

Плагин создаст файл с именем assets.json, который выглядит так:

{
  "main": {
    "js": "js/main.min.js?6973f571ac0276126690",
    "css": "css/style.min.css?ea9c3ce9b0e3a48ab3837671690b9e39"
  }
}

Теперь нам просто нужно загрузить этот файл в код PHP и вуаля! Мы можем сгенерировать правильные URL-адреса, которые включают эти хэши.

Передача данных в скрипт

Если мы хотим встроить компоненты Vue.js в приложение PHP, нам нужно контролировать, какие компоненты создаются и где они размещаются. Нам также может потребоваться передать дополнительные параметры для их настройки.

Одно из решений - передать всю информацию с помощью настраиваемых атрибутов HTML:

<input name="date" data-component="DatePicker" data-format="...">

Сценарий должен сканировать документ, искать эти атрибуты и создавать соответствующие компоненты.

Другой подход - создать компоненты явно следующим образом:

<input id="date-field" name="date">
<script>
  MyLibrary.createDatePicker( '#date-field', { format: "..." } );
</script>

Это проще, поэтому я покажу вам, как это можно сделать.

Во-первых, наш main.js скрипт должен экспортировать некоторые функции, например:

import Vue from 'vue'
import DatePicker from './components/DatePicker.vue'
export function createDatePicker( selector, options ) {
  new Vue( {
    el: selector,
    render( h ) {
      return h( DatePicker, { props: { format: options.format } } );
    }
  } );
}

Мы должны сообщить webpack, что хотим, чтобы экспортируемые функции были доступны в глобальной переменной. Для этого добавьте параметр library в раздел output и передайте имя глобальной переменной:

output: {
  // ... paths and file name ...
  library: 'MyLibrary'
}

Это позволит вам использовать компоненты Vue.js где угодно, как если бы вы использовали компоненты jQuery.

Я не показываю вам окончательный файл конфигурации, потому что я намеревался объяснить, как настроить webpack, а не предоставить еще один шаблонный файл, который вы можете скопировать и вставить в свое приложение. Ваш вариант использования может отличаться от моего, поэтому прочтите документацию по веб-пакету, она действительно хорошо написана, и проведите собственное исследование.

Кроме того, я ни в коем случае не являюсь экспертом в Vue.js и webpack. Я только изучаю эти технологии и хочу поделиться с вами тем, что узнал на данный момент. Но я надеюсь, что это хорошая отправная точка, и вы тоже кое-чему научитесь.