предисловие
Недавно я взял на себя старый проект, типичный компонентный рендеринг интерфейса Vue, и последующая оптимизация бизнеса может быть в направлении SSR, поэтому мы должны сначала сделать некоторые технические резервы. Если вы совершенно не знакомы с Vue SSR, сначала прочитайтеофициальная документация.
идеи
Vue предоставляетОфициальная демоверсия, преимущество этой демонстрации в том, что функция большая и полная, недостаток в том, чтоНе дружелюбен к новичкам, это легко увидеть. Поэтому сегодня мы напишем более простую в использовании демоверсию.Всего три шага, шаг за шагом.
- Напишите простую демонстрацию рендеринга внешнего интерфейса (без данных Ajax);
- Изменить внешний рендеринг на внутренний рендеринг (по-прежнему нет данных Ajax);
- На основе внутреннего рендеринга плюс обработка данных Ajax;
Первый шаг: демонстрация рендеринга переднего плана
Эта часть относительно проста, то есть страница содержит два компонента: Foo и Bar.
<!-- index.html -->
<body>
<div id="app">
<app></app>
</div>
<script src="./dist/web.js"></script> <!--这是 app.js 打包出来的 JS 文件 -->
</body>
// app.js,也是 webpack 打包入口
import Vue from 'vue';
import App from './App.vue';
var app = new Vue({
el: '#app',
components: {
App
}
});
// App.vue
<template>
<div>
<foo></foo>
<bar></bar>
</div>
</template>
<script>
import Foo from './components/Foo.vue';
import Bar from './components/Bar.vue';
export default {
components:{
Foo,
Bar
}
}
</script>
// Foo.vue
<template>
<div class='foo'>
<h1>Foo</h1>
<p>Component </p>
</div>
</template>
<style>
.foo{
background: yellow;
}
</style>
// Bar.vue
<template>
<div class='bar'>
<h1>Bar</h1>
<p>Component </p>
</div>
</template>
<style>
.bar{
background: blue;
}
</style>
Окончательный результат рендеринга показан на рисунке ниже, пожалуйста, обратитесь к исходному кодуздесь.
Шаг 2: Бэкенд-рендеринг (без данных Ajax)
Первая демонстрация не содержит никаких данных Ajax, но даже в этом случае преобразовать ее в рендеринг на бэкенде непросто.С каких аспектов следует начать?
- Разделить запись JS;
- Раздельная конфигурация упаковки Webpack;
- Напишите основную логику рендеринга на стороне сервера.
1. Разделить запись JS
Во внешнем рендеринге требуется только одна записьapp.js
. Теперь, чтобы выполнить бэкенд-рендеринг, нам нужно два JS-файла:entry-client.js
а такжеentry-server.js
Как вход браузера и сервера соответственно.
Первый взглядentry-client.js
, это первый шагapp.js
Есть ли разница? →Разницы никакой, просто другое название, содержание то же.
посмотри сноваentry-server.js
, он просто возвращает экземпляр App.vue .
// entry-server.js
export default function createApp() {
const app = new Vue({
render: h => h(App)
});
return app;
};
entry-server.js
а такжеentry-client.js
Основные различия между двумя входами заключаются в следующем:
-
entry-client.js
Выполняется на стороне браузера, поэтому вам нужно указать el и явно вызвать метод $mount, чтобы начать рендеринг в браузере. -
entry-server.js
Он вызывается на стороне сервера, поэтому его нужно экспортировать как функцию.
2. Разделить конфигурацию связывания Webpack
На первом этапе, поскольку толькоapp.js
Требуется одна запись, требуется только один файл конфигурации Webpack. Теперь, когда есть две записи, естественно требуются два файла конфигурации Webpack:webpack.server.conf.js
а такжеwebpack.client.conf.js
, их открытые части абстрагируются вwebpack.base.conf.js
.
оwebpack.server.conf.js
, следует отметить два момента:
-
libraryTarget: 'commonjs2'
→ Так как сервер Node, он должен бытьУпакован в соответствии со спецификацией commonjsдля вызова сервером. -
target: 'node'
→ Укажите среду Node, чтобы избежать ошибок из-за API-интерфейсов, не относящихся к среде Node, таких как документ и т. д.
3. Напишите основную логику рендеринга на стороне сервера
Vue SSR зависит от пакета vue-server-render, который вызывает поддержкуДва формата записи: createRenderer и createBundleRenderer, первый использует в качестве записи компонент Vue, второй — упакованный JS-файл в качестве записи, а в этой статье используется последний.
// server.js 服务端渲染主体逻辑
// dist/server.js 就是以 entry-server.js 为入口打包出来的 JS
const bundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf-8')
});
server.get('/index', (req, res) => {
renderer.renderToString((err, html) => {
if (err) {
console.error(err);
res.status(500).end('服务器内部错误');
return;
}
res.end(html);
})
});
server.listen(8002, () => {
console.log('后端渲染服务器启动,端口号为:8002');
});
Окончательный эффект рендеринга этого шага показан на рисунке ниже.Из рисунка видно, что компонент был успешно отрендерен серверной частью. Пожалуйста, обратитесь к исходному кодуздесь.
Шаг 3: Бэкэнд-рендеринг (предварительная загрузка данных Ajax)
Это решающий шаг и самый трудный.
Если каждый компонент второго шага должен запрашивать данные Ajax, что с этим делать?официальная документацияНавел нас на мысль, которую я кратко резюмирую следующим образом:
- Перед началом рендеринга предварительно извлеките все необходимые данные Ajax (а затем сохраните их в Vuex Store);
- При рендеринге серверной части полученные данные Ajax вводятся в каждый компонент через Vuex;
- Купить все данные AJAX в окне.INITIAL_STATE, переданный в браузер через HTML;
- Со стороны браузера окно.INITIAL_STATEДанные Ajax внутри вводятся в каждый компонент соответственно.
Вот несколько ключевых моментов.
Мы знаем, что в обычном внешнем рендеринге Vue запрос компонента Ajax обычно записывается так: «Вызывается в смонтированномthis.fetchData
, а затем записать возвращенные данные в данные экземпляра в обратном вызове, что нормально. "
В SSR это невозможно, потому что сервер не выполняет смонтированный цикл. Итак, мы можем поставитьthis.fetchData
Перейти к созданному или выполнить эти два жизненных цикла перед созданием? То же не работает. Причина в следующем:this.fetchData
Это асинхронный запрос.После отправки запроса серверная часть уже отобразила данные до того, как данные будут возвращены, и данные, возвращенные Ajax, не могут быть обработаны вместе.
Поэтому мы должны заранее знать, какие компоненты имеют Ajax-запросы, и только после того, как эти Ajax-запросы вернули данные, мы можем начать отрисовку компонентов.
// store.js
function fetchBar() {
return new Promise(function (resolve, reject) {
resolve('bar ajax 返回数据');
});
}
export default function createStore() {
return new Vuex.Store({
state: {
bar: '',
},
actions: {
fetchBar({commit}) {
return fetchBar().then(msg => {
commit('setBar', {msg})
})
}
},
mutations:{
setBar(state, {msg}) {
Vue.set(state, 'bar', msg);
}
}
})
}
// Bar.uve
asyncData({store}) {
return store.dispatch('fetchBar');
},
computed: {
bar() {
return this.$store.state.bar;
}
}
Метод asyncData компонента определен, но как индексировать этот метод asyncData? Давайте сначала посмотрим, как написан мой корневой компонент App.vue.
// App.vue
<template>
<div>
<h1>App.vue</h1>
<p>vue with vue </p>
<hr>
<foo1 ref="foo_ref"></foo1>
<bar1 ref="bar_ref"></bar1>
<bar2 ref="bar_ref2"></bar2>
</div>
</template>
<script>
import Foo from './components/Foo.vue';
import Bar from './components/Bar.vue';
export default {
components: {
foo1: Foo,
bar1: Bar,
bar2: Bar
}
}
</script>
Из корневого компонента App.vue мы видим, что можем найти метод asyncData каждого компонента по очереди, анализируя поле его компонентов.
// entry-server.js
export default function (context) {
// context 是 vue-server-render 注入的参数
const store = createStore();
let app = new Vue({
store,
render: h => h(App)
});
// 找到所有 asyncData 方法
let components = App.components;
let prefetchFns = [];
for (let key in components) {
if (!components.hasOwnProperty(key)) continue;
let component = components[key];
if(component.asyncData) {
prefetchFns.push(component.asyncData({
store
}))
}
}
return Promise.all(prefetchFns).then((res) => {
// 在所有组件的 Ajax 都返回之后,才最终返回 app 进行渲染
context.state = store.state;
// context.state 赋值成什么,window.__INITIAL_STATE__ 就是什么
return app;
});
};
Есть еще несколько интересных вопросов:
- Должен ли я использовать vue-router? → Нет. Хотя vue-router используется в официальной демонстрации, это только потому, что официальная демонстрация представляет собой SPA с несколькими страницами. В общем случае необходимо использовать vue-router, т.к. разные маршруты соответствуют разным компонентам, и выполняется не каждый asyncData всех компонентов. Но есть исключения, например, в этом моем старом проекте всего одна страница (одна страница содержит много компонентов), поэтому vue-router вообще не нужен, а SSR все равно можно сделать. Основное отличие заключается в том, как найти asyncData, который должен быть выполнен. Метод: Официальная демонстрация проходит через vue-router, и я анализирую поле компонентов напрямую, вот и все.
- Должен ли я использовать Vuex? → Да, но нет, см.ответ Юды. Почему что-то вроде Vuex должно существовать? Давайте проанализируем это.
2.1 Когда возвращаются предварительно загруженные данные Ajax, компонент Vue не начал рендеринг. Итак, мы должны поставить Ajax где-то в первую очередь.
2.2 Когда компонент Vue начинает рендериться, данные Ajax должны быть извлечены и правильно переданы каждому компоненту.
2.3. Когда браузер отображает, он должен разбирать окно.INITIAL_STATEи передается каждому компоненту.
Следовательно, у нас должно быть такое место, независимое от представления, для хранения, управления и передачи данных, поэтому существует Vuex. - Серверная часть уже преобразовала данные Ajax в HTML, зачем вам передавать данные Ajax через window.INITIAL_STATEпереходить на передний план? → Потому что интерфейсному рендерингу все равно нужно знать эти данные. Например, вы пишете компонент, привязываете к нему событие щелчка и печатаете значение поля this.msg при нажатии. Теперь серверная часть отображает HTML-код компонента, но привязку события должен выполнять браузер.Если браузер не может получить те же данные, что и сервер, где найти поле msg при запуске события щелчка компонента?
На данный момент мы завершили внутренний рендеринг с данными Ajax. Этот шаг самый сложный и самый ответственный, требующий повторных размышлений и попыток. Конкретный эффект рендеринга выглядит следующим образом, пожалуйста, обратитесь к исходному кодуздесь.
Эффект
Вы закончили? еще нет. Люди говорят, что SSR может улучшить скорость рендеринга первого экрана, давайте сравним и посмотрим, так ли это. (также в условиях сети Fast 3G).
Вариант официального мышления
На этом демонстрация Vue SSR завершена. Ниже приведены некоторые вариации характеристик моего собственного проекта, читатели, которым это не интересно, могут их пропустить.
Есть ли недостатки у третьего шага официальной идеи? Я думаю, что есть:Для старых проектов стоимость ремонта относительно высока. Если вам нужно явно ввести vuex, вы должны пойти с действиями и мутациями, Будь то количество изменений кода или стоимость обучения новичков, это не мало.
есть ли способУменьшить количество изменений в старом проекте рендеринга интерфейса?? Я делаю это.
// store.js
// action,mutations 那些都不需要了,只定义一个空 state
export default function createStore() {
return new Vuex.Store({
state: {}
})
}
// Bar.vue
// tagName 是组件实例的名字,比如 bar1、bar2、foo1 等,由 entry-server.js 注入
export default {
prefetchData: function (tagName) {
return new Promise((resolve, reject) => {
resolve({
tagName,
data: 'Bar ajax 数据'
});
})
}
}
// entry-server.js
return Promise.all(prefetchFns).then((res) => {
// 拿到 Ajax 数据之后,手动将数据写入 state,不通过 action,mutation 那一套
// state 内部区分的 key 值就是 tagName,比如 bar1、bar2、foo1 等
res.forEach((item, key) => {
Vue.set(store.state, `${item.tagName}`, item.data);
});
context.state = store.state;
return app;
});
// ssrmixin.js
// 将每个组件都需要的 computed 抽象成一个 mixin,然后注入
export default {
computed: {
prefetchData () {
let componentTag = this.$options._componentTag; // bar1、bar2、foo1
return this.$store.state[componentTag];
}
}
}
На данный момент у нас есть вариант Vue SSR. Для разработчиков компонентов только оригиналthis.fetchData
Метод абстрагируется от метода prefetchData, который затем можно использовать в DOM.{{prefetchData}}
Получил данные. Пожалуйста, обратитесь к этой части кодаздесь.
Суммировать
Vue SSR действительно интересная вещь, ключ в том, чтобы использовать ее гибко. В этой демонстрации все еще есть оставшаяся проблема, которая не была решена: когда Ajax абстрагируется в prefetchData и превращается в SSR, исходный внешний рендеринг становится недействительным. Может ли один и тот же код поддерживать как внешний, так и внутренний рендеринг? Таким образом, когда возникает проблема с внутренним рендерингом, я могу вернуться к внешнему рендерингу в любое время, и у меня есть восходящее решение.
использованная литература
- Создание одной из серий vue-ssr с нуля: слова, написанные спереди, Говорящая рыба
- Запись рендеринга сервера vue SSR, By echo_numb
- Официальный документ Vue SSR, практика 1: смешивание передней и задней частей от нуля до грубого, By songlairui
- Предварительное исследование рендеринга на стороне сервера в Vue 2., По титульному листу
- Vue SSR наступает на яму, By ghosert
- Подробно объясните модуль виртуальной машины NodeJS., By dorsywang
----- над ------