Передача конфигурации в Vue.js

Как передать конфигурацию и начальное состояние приложения с сервера в приложение Vue.js?

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

Напоминаем, что вот последний файл main.js из этой статьи:

import Vue from 'vue'
import makeI18n from './i18n'
import makeAjax from './services/ajax'
import makeStore from './store'
import App from './components/App.vue'
const i18n = makeI18n( 'en' );
const ajax = makeAjax( 'http://example.com' );
const store = makeStore( i18n, ajax );
new Vue( {
  el: '#app',
  i18n,
  ajax,
  store,
  render: h => h( App )
} );

Как видите, мы передаем локаль в модуль i18n, а адрес сервера - в модуль ajax. Однако эти параметры по-прежнему жестко запрограммированы в сценарии.

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

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

Внедрение конфигурации на страницу

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

Это можно сделать разными способами, например, используя атрибуты данных HTML, но наиболее гибкий способ - встроить данные JSON в отдельный тег <script>:

<script id="config" type="application/json">{"locale":"en","baseURL":"http:\/\/example.com"}
</script>

У него есть атрибут id, чтобы его было легко отличить от других тегов скрипта на той же странице. Также установлен тип содержимого application/json, чтобы браузер не интерпретировал данные как код JavaScript. Когда браузер встречает такой тег скрипта, он никоим образом не выполняет его, но этот тег и его содержимое доступны в дереве DOM, поэтому это отличное решение для хранения данных любого типа.

Обратите внимание, что символы косой черты в строке URL экранируются обратной косой чертой. Это может показаться ненужным, потому что косая черта не имеет особого значения в контексте строки JSON. Чтобы понять, почему это важно, представьте этот сценарий, в котором некоторый пользовательский ввод становится частью данных JSON:

<script id="config" type="application/json">
{"locale":"en","userName":"</script><h1>Hello!</h1><script>"}
</script>

Анализатор HTML интерпретирует тег </script> как конец скрипта, даже если он находится внутри строки, потому что он ничего не знает о синтаксисе JSON. Затем он интерпретирует следующий тег <h1> как часть тела документа. Итак, с точки зрения браузера указанная выше разметка интерпретируется следующим образом:

<script id="config" type="application/json">
{"locale":"en","userName":"
</script>
<h1>Hello!</h1>
<script>"}</script>

Это может привести к потенциально опасным инъекциям. Таким образом, мы пользуемся преимуществом того факта, что парсер JSON интерпретирует последовательность \/ внутри строки как обычную косую черту, и следующие данные интерпретируются правильно (и безопасно):

<script id="config" type="application/json">
{"locale":"en","userName":"<\/script><h1>Hello!<\/h1><script>"}
</script>

Функция json_encode() в PHP уже по умолчанию избегает таких косых черт, поэтому ее можно использовать.

Вот пример кода PHP, который вводит данные конфигурации на страницу:

<?php
$code[ 'locale' ] = get_user_locale();
$code[ 'baseURL' ] = get_server_base_url();
?>
<script id="config" type="application/json">
<?php echo json_encode( $config ) ?>
</script>

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

const configElement = document.getElementById( 'config' );
const config = JSON.parse( configElement.innerHTML );
const i18n = makeI18n( config.locale );
const ajax = makeAjax( config.baseURL );

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

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

Явная передача конфигурации

Немного другой подход - изменить скрипт main.js так, чтобы он экспортировал функцию:

import Vue from 'vue'
import makeI18n from './i18n'
import makeAjax from './services/ajax'
import makeStore from './store'
import App from './components/App.vue'
export function main( { locale, baseURL } ) {
  const i18n = makeI18n( locale );
  const ajax = makeAjax( baseURL );
  const store = makeStore( i18n, ajax );
  new Vue( {
    el: '#app',
    i18n,
    ajax,
    store,
    render: h => h( App )
  } );
}

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

output: {
  path: path.resolve( __dirname, './assets' ),
  filename: 'js/myapp.js',
  library: 'MyApp'
}

Это делает экспортированные символы доступными как свойства объекта, назначенного глобальной переменной MyApp. Итак, функцию main() в нашем скрипте можно вызвать так:

<script>
MyApp.main({"locale":"en","baseURL":"http:\/\/example.com"});
</script>

Для этого требуется лишь небольшое изменение кода на стороне сервера:

<?php
$code[ 'locale' ] = get_user_locale();
$code[ 'baseURL' ] = get_server_base_url();
?>
<script>
MyApp.main(<?php echo json_encode( $config ) ?>);
</script>

У этого решения есть два преимущества. Тег script, который загружает клиентский код, теперь может быть включен в заголовок страницы, поэтому он может быть загружен и скомпилирован браузером до полной загрузки страницы. Только вызов main() должен быть включен в конец тела страницы.

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

Начальное состояние Vuex

Очень часто приложению Vue.js также необходимо загрузить некоторые начальные данные при запуске, например начальное состояние хранилища Vuex. Загрузка этих данных с помощью запроса AJAX добавляет задержку до того, как приложение будет отрисовано и готово к использованию.

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

Изменим main.js следующим образом:

import Vue from 'vue'
import makeI18n from './i18n'
import makeAjax from './services/ajax'
import makeStore from './store'
import App from './components/App.vue'
export function main( { locale, baseURL, ...initialState } ) {
  const i18n = makeI18n( locale );
  const ajax = makeAjax( baseURL );
  const store = makeStore( i18n, ajax, initialState );
  new Vue( {
    el: '#app',
    i18n,
    ajax,
    store,
    render: h => h( App )
  } );
}

Вы можете видеть, что теперь все параметры, кроме locale и baseURL, назначены объекту initialState и переданы функции makeStore().

Изменим соответственно store / index.js:

import Vue from 'vue'
import Vuex from 'vuex'
import makeModule1 from './modules/module1'
Vue.use( Vuex );
export default function makeStore( i18n, ajax, initialState ) {
  return new Vuex.Store( {
    state: makeState( initialState ),
    actions: makeActions( i18n, ajax ),
    modules: {
      module1: makeModule1( i18n, ajax, initialState )
    }
  } );
}
function makeState( initialState ) {
  return {
    ...
  };
}
function makeActions( i18n, ajax ) {
  return {
    ...
  };
}

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

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

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