javascript

Динамическая загрузка шаблона Vue компонента

  • пятница, 29 декабря 2017 г. в 03:12:44
https://habrahabr.ru/post/345814/
  • VueJS
  • Node.JS
  • JavaScript


Доброго времени суток, уважаемые Хабровчане! С недавнего времени, мы, в нашей команде начали использовать фреймворк Vue.js включая серверный рендеринг, после чего столкнулись с рядом проблем, в частности для меня как программиста.

Любое изменение в верстке сайта, происходило через меня. Мне скидывали часть html кода, будь то изменение заголовка, или смена мест блоков, далее было необходимо вставить эту часть в требуемый компонент, подставить необходимые переменные и методы, запустить webpack, залить код на сервер.

Можно было бы использовать на сервере webpack в режиме наблюдения, или дать перечень необходимых команд своим коллегам, что для них оказывается несколько сложным.
Поэтому приняли решение сделать динамическую загрузку шаблона с помощью получения данных с сервера.

В качестве примера, рассмотрим упрощенный вариант такого подхода.

Структура следующая:

  • public/ — директория содержащая статические файлы
    • templates/ — директория содержащая шаблоны компонентов

  • server/ — код серверной части
  • app/ — код Vue приложения
  • client.js — точка входа клиентской части Vue приложения
  • serverEntry.js — точка входа серверной части Vue приложения

Что-бы использовать предзагрузку шаблона в компонент, необходимо дождаться получения этих данных с сервера, для чего прекрасно подойдут Promis'ы. В итоге у нас получилось две обертки над Vue компонентами.

wrapComponent — для глобальной регистрации Vue-компонета

// ./wrapComponent.js
import Vue from 'vue';
import axios from 'axios';

export default function wrapComponenet(name, template, component) {
    return () => {
        return new Promise((resolve, reject) => {
            axios.get(template).then((fetchData) => {
                const template = fetchData.data;

                Vue.component(name, {
                        ...component,
                        template,
                });
                resolve();
            });
        });
    };
}

wrapPageComponent — для возврата Vue-компонента.

// ./wrapPageComponent.js
import axios from 'axios';

export default function wrapPageComponent(name, template, component) {
    return () => {
        return new Promise((resolve, reject) => {
            axios.get(template).then((fetchData) => {
                const template = fetchData.data;

                resolve({
                        ...component,
                        template,
                });
            });
        });
    };
}

Большая часть кода используемая ниже в большей степени взята с официальной документации по серверному рендеру vue.js (ssr.vuejs.org), поэтому детально на этом останавливаться не стану.

// ./server/index.js
// Koa
import Koa from 'koa';
import staticFile from 'koa-static';
// Точка входа Vue-приложения
import createApp from '../serverEntry.js';
// Vue модуль
import { createRenderer } from 'vue-server-renderer';

const PORT = 4000;
const server = new Koa();

server.use(staticFile('public'));

server.use((ctx) => {
  // Дожидаемся создания приложения
  const app = await createApp();
  // Рендерим html - код
  const html = await renderer.renderToString(app);
  const page = `
<!DOCTYPE html>
<html lang="ru">
  <head>
    <title>Vue App</title>
    <base href="/">
    <meta charset="utf-8">
  </head>
  <body>
    <div id="root">${html}</div>
    <script src="js/app.js"></script>
  </body>
</html>
`);

  ctx.body = page;
});

server.listen(PORT, (err) => {
  if (err) console.log(err);
  console.log(`Server started on ${PORT} port`);
});

export default server;

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

Далее рассмотрим серверную точку входа Vue-приложения, в ней все просто, дожидаемся генерации приложения и возвращаем результат. В дальнейшем данная точка входа расширяется роутингом, хранилищами и т.д.

// ./serverEntry.js
import { createApp } from './app';

export default async (context) => {
    const { app } = await createApp();

    return app;
};

Примерно тоже самое происходит и в клиентской точке входа

// ./client.js
import { createApp } from './app';

createApp()
    .then(({ app }) => {
        app.$mount('#app');
    });

И наконец мы подобрались к самому Vue-приложению.

// ./app/index.js
// Компоненты Vue
import Vue from 'vue';
import VueAxios from 'vue-axios';
// Дополнительные библиотеки
import axios from 'axios';
// Приложение Vue
import App from './App';

// Компоненты сайта
import mainMenu from './Components/MainMenu';
import mainContent from './Components/MainContent';

Vue.use(VueAxios, axios);
axios.defaults.baseURL = 'http://localhost:4000/';

export async function createApp( context ) {
    const appComponent = await App();
    
    const app = new Vue({
        render: (h) => h(App),
    });

    return new Promise((resolve, reject) => {
         // Загружаем все компоненты с помощью Promise
        const allComponents = [
            mainMenu(),
            mainContent(),
        ];
        Promise.all(allComponents)
            .then(() => {
                resolve({ app, router });
            });
    });
}

И не посредственно сами Vue-компоненты обернуты нашими обертками (извините за тавтологию).

// ./app/App.js
import wrapPageComponenets from '../wrapPageComponents';

export default wrapAppComponenets('App', '/template/App.html', {
  name: 'App',
});

// ./app/Components/MainMenu.js
import wrapComponenets from '../../wrapComponents';

export default wrapComponenets('main-menu', '/templates/MainMenu.html', { 
  data() {
    return { title: 'VueJS App'};
  }
})


// ./app/Components/MainContent.js
import wrapComponenets from '../../wrapComponents';

export default wrapComponenets('main-component', '/templates/MainContent.html', { 
  data() {
    return { name: 'Привет Хабра!'};
  },
  methods: {
    clickHandle() {
      alert('И еще раз привет');
    }
  }
});

И соответствующие данным компонентам шаблоны которые находятся в public/templates/

<!-- ./public/templates/App.html -->
<div>
  <main-menu></main-menu>
  <main-content></main-content>
</div>

<!-- ./public/templates/MainMenu.html -->
<nav>
  <ul>
    <li class="logo">{{title}}</li>
  </ul>
</nav>

<!-- ./public/templates/MainContent.html -->
<div>
  <h1 @click="clickHandle()">{{name}}</h1>
</div>

Вот и все. Теперь все шаблоны подгружаются с сервера, и для своих коллег я могу дать список переменных и методов которые они могут подставлять в тот или иной шаблон и мое участие сводится лишь к добавлению новых методов и переменных и минимум работы с html — шаблонами. Так же оказалось гораздо проще объяснить использование директив v-show,v-if,v-for.

Спасибо за внимание!