Доброго времени суток, уважаемые Хабровчане! С недавнего времени, мы, в нашей команде начали использовать фреймворк 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.
Спасибо за внимание!
Автор: Станислав