Изоморфизм Vue (1): быстро начать

сервер браузер Vue.js Webpack
Изоморфизм Vue (1): быстро начать

предисловие

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

Изоморфный (рендеринг на сервере)

Изоморфизм Vue — это то, что мы часто называем рендерингом на стороне сервера.Рендеринг на стороне сервера сегодня не является чем-то новым.React и Vue имеют свои собственные решения для рендеринга на сервере.Многие мелкие партнеры могут нужный? У таких фреймворков, как Vue и React, есть особенность, заключающаяся в том, что все они относятся к рендерингу в браузере, например, самый простой пример:   

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

   Мы видим, что шаблон, который мы получили с сервера, на самом деле не соответствует тому интерфейсу, который мы ожидаемhtmlструктура, есть только один корневой элемент для монтирования приложения, а клиентский браузер выполняет загрузкуJavaScriptСоответствующая структура DOM создается только при выполнении кода. Однако рендеринг в браузере на самом деле имеет два очевидных недостатка:

  • Не подходит для поисковой оптимизации (SEO: Search Engine Optimization), каждая поисковая система на самом деле является веб-страницей.htmlСтруктура и синхронизацияJavascriptкод для индексации, поэтому рендеринг на стороне клиента может помешать правильной индексации ваших страниц поисковыми системами.
  • TTC (время поступления контента: Time-To-Conten) слишком велико.Представьте, что если сеть устройства плохая или скорость выполнения кода устройства низкая, пользователю нужно долго ждать, чтобы увидеть контент. Это все белый экран или другое состояние загрузки веб-страницы, что, безусловно, плохо для пользователя.

   К счастью, появление Node прояснило все это, JavaScript можно выполнять не только в браузере, но и в бэкэнд-среде. Таким образом, мы можем визуализировать пользовательский интерфейс в виде строки HTML на сервере, затем передать его в браузер, чтобы пользователь получил интерфейс с возможностью предварительного просмотра, и, наконец, «смешать» статическую разметку в полностью интерактивное приложение на стороне клиента. весь процесс рендеринга завершен.

Простейший пример

  Визуализация сервера Vue использует официальную библиотекуvue-server-renderer,из-заExpressЭто более интуитивно понятно, мы используемExpressВ качестве бэкенд-сервера сначала приведем простейший пример:

// server.js
const Vue = require('vue')
const server = require('express')()
// 创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()

server.get('*', (req, res) => {
  // 创建一个 Vue 实例
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>访问的 URL 是: {{ url }}</div>`
  })

  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    // html就是Vue实例app渲染的html
    res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
  })
})

server.listen(8080)

   затем начнитеnode server.js, и в доступе к браузеру, например.http://localhost:8080/app, интерфейс браузера отобразит:

Доступ к URL-адресу: /app

   В это время обратите внимание, что возвращаемое значение запроса:

   Мы нашли возвращенноеhtmlЭлементы DOM были отображены в . Таким образом, нам не нужно ждать, чтобы сразу увидеть содержимое страницы. Логика приведенного выше кода также очень проста: когда http-сервер получает запрос на получение, он создает экземпляр Vue, который находится в vue-server-renderer.createRendererсоздатьRendererпример,RendererсерединаrenderToStringОн используется для преобразования экземпляра Vue в соответствующую строку HTML.Следует отметить, что нам нужно обернуть созданную строку вhtmlВернуться вместе. Конечно, вы можете разделить их в виде шаблонов страниц:

<!DOCTYPE html>
<html lang="en">
  <head><title>Hello</title></head>
  <body>
    <!--vue-ssr-outlet-->
    <!--这里将是应用程序 HTML 标记注入的地方>
  </body>
</html>
// renderer中包含了模板
const renderer = createRenderer({
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})

renderer.renderToString(app, (err, html) => {
  res.end(html)
})

   Конечно, это только самый простой пример, браузер только получает html-код, соответствующий экземпляру Vue, и не активирует его, поэтому он не интерактивен.

Процесс рендеринга в браузере

   Для рендеринга в браузере мы предпочитаем упаковывать код в Webpack.Общий процесс можно пояснить на следующем рисунке:

   Для приложения Vue уровень исходного кода фактически включает три аспекта: компоненты, маршрутизацию и управление состоянием. Мы считаем эту часть кода общим кодом, который может выполняться как на стороне сервера, так и на стороне браузера.В Webpack есть две записи:server entryа такжеclient entry, которые используются для упаковки кода, выполняемого на стороне сервера, и кода, выполняемого на стороне браузера, соответственно.Server BundleПоскольку код упаковывается и выполняется на стороне сервера, он отвечает за создание соответствующего HTML иClinet BundleПоскольку код выполняется на стороне браузера, основная ответственность заключается в активации приложения.

Ниже мы приводим соответствующую конфигурацию вебпака.Для удобства начала работы мы приводим только самую простую конфигурацию, чтобы можно было запустить код.Конфигурация состоит из трех частей:base,client,serverbaseявляется общей частью между ними,clientЭто конфигурация упаковки соответствующего браузера,serverэто конфигурация упаковки на стороне сервера, черезwebpack-merge(Это можно легко понять какObject.assign), чтобы подключить его:

// webpack.base.config.js
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/dist/',
        filename: '[name].[chunkhash].js'
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
}

  Выше приведена самая простая общая конфигурация в webpack, которая определяет три части:

  • output: Как файл пакета сохраняет выходные данные и где их хранить
  • module: Выполняем соответствующий загрузчик для файла js и файла vue
  • plugins: VueLoaderPluginПлагины необходимы, и их роль заключается в копировании и применении других правил, определенных вами, к соответствующему языковому блоку в файле .vue. Например, код JavaScript, соответствующий тегу script в файле vue, и код CSS, соответствующий тегу stype.
// webpack.server.config.js
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(base, {
    target: 'node',
    entry: './src/entry-server.js',
    output: {
        libraryTarget: 'commonjs2'
    },
    plugins: [
        new VueSSRServerPlugin()
    ]
})

  Приведенная выше конфигурация используется для упаковки комплекта серверной стойки:

  • target: используется для указания цели сборки, узел указывает, что веб-пакет будет скомпилирован для использования в среде, подобной Node.js.
  • entry: Файл записи упаковки сервера
  • libraryTarget: Поскольку он используется в среде Node, мы выбираемcommonjs2
  • VueSSRServerPlugin: используется для упаковки сгенерированного пакета на стороне сервера, и, наконец, все файлы могут быть упакованы в одинjsonфайл и, наконец, отправлен на серверrendererиспользовать.
// webpack.client.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

module.exports = merge(base, {
    entry: {
        app: './src/entry-client.js'
    },
    plugins: [
        // extract vendor chunks for better caching
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: function (module) {
                // a module is extracted into the vendor chunk if...
                return (
                    // it's inside node_modules
                    /node_modules/.test(module.context)
                )
            }
        }),
        // extract webpack runtime & manifest to avoid vendor chunk hash changing
        // on every build.
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest'
        }),
        new VueSSRClientPlugin()
    ]
})
  • entry: Файл записи упаковки браузера.
  • VueSSRClientPlugin: похожий наVueSSRServerPluginОсновная функция плагина — упаковать внешний код вbundle.json, а затем передать значение вrenderer, который может автоматически выводить и вставлять директивы предварительной загрузки/предварительной выборки и теги сценария в отображаемый HTML.

оCommonsChunkPluginПлагины, по сути, не нужны для самого простого приложения, но они добавляются, потому что помогают повысить производительность. Когда я впервые изучил Webpack, весь код каждый раз упаковывался в один и тот же файл, напримерapp.[hash].js, по фактуapp.[hash].jsОн содержит две части кода, одна часть — это код бизнес-логики, который меняется каждый раз, а другая часть — это код библиотеки классов (например, исходный код Vue), который почти не меняется. Сейчас эта ситуация на самом деле очень неблагоприятна для кеширования браузера, потому что каждый раз, когда бизнес-код меняется,app.[hash].jsдолжны измениться, поэтому браузер должен повторно запросить, иapp.[hash].jsОбъем кода может исчисляться терабайтами. Таким образом, мы можем отделить бизнес-код от кода библиотеки классов в приведенном выше примере:

new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: function (module) {
    // a module is extracted into the vendor chunk if...
        return (
        // it's inside node_modules
        /node_modules/.test(module.context)
        )
    }
}),

   Мы процитируемnode_modulesКод упакован вvendor.[hash].js, который содержит указанную библиотеку классов, которая является относительно неизменной частью кода. Но если у вас есть только вышеуказанная часть, вы обнаружите, что каждый раз, когда изменяется логический код,vendor.[hash].jsизhashЗначение тоже меняется, почему так? Поскольку каждый раз, когда Webpack упаковывается и запускается, он по-прежнему будет генерировать некоторый код, связанный с текущей операцией Webpack, что повлияет на текущее значение пакета, поэтомуvendor.[hash].jsКаждый раз пакет все равно меняется, по сути, браузер не может его корректно кэшировать. Итак, мы используем:

new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest'
})

Нам нужно извлечь среду выполнения в отдельныйmanifestфайл, вот такvendorизhashне изменится, браузер можетvendorправильно кэшируется,mainfestизhashХотя он меняется каждый раз, он очень мал, по сравнению сvendorВлияние изменения незначительно.

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

компоненты

   Прежде всего, мы используем Vue для написания простого счетного компонента, нажмите «+», чтобы увеличить количество, нажмите «-», чтобы уменьшить количество.

// App.vue
<template>
    <div id="app">
        <span>times: {{times}}</span>
        <button @click="add">+</button>
        <button @click="sub">-</button>
    </div>
</template>

<script>
    export default {
        name: "app",
        data: function () {
            return {
                times: 0
            }
        },
        methods: {
            add: function () {
                this.times = this.times + 1;
            },
            sub: function () {
                this.times = this.times - 1;
            }
        }
    }
</script>
<style scoped>
</style>

   Приведенная выше часть представляет собой очень простой компонент Vue, а также общий код для рендеринга на стороне сервера и на стороне клиента. В чисто клиентской программе рендеринга будетapp.jsИспользуется для создания экземпляра Vue и его монтирования на соответствующий дом, например:

// 客户端渲染 app.js
import App from './App.vue'

new Vue({
  el: '#app',
  components: { App },
  template: '<App/>',
})

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

import Vue from 'vue'
import App from './components/App.vue'

export function createApp() {
    return new Vue({
        render: h => h(App)
    })
}

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

   Затем посмотрите на файл записи упаковки на стороне браузера:

// entry-server.js
import { createApp } from './app'

export default context => {
    const app = createApp()
    return app
}

  entry-server.jsПредоставляет функцию для создания текущего экземпляра компонента. Затем посмотрите на файл записи клиентского пакета:

// client-server.js
import { createApp } from './app'

var app = createApp();

app.$mount('#app')

Логика    тоже очень проста, мы создаем экземпляр Vue и монтируем его вidдляappв структуре DOM.

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

Мы видим, чтоapp.[hash].jsпредставляет собой упакованный бизнес-код,vendor.[hash].jsЭто код соответствующей библиотеки (например, исходный код Vue),manifest.[hash].jsявляетсяCommonsChunkPluginСгенерировать файл манифеста. а такжеvue-ssr-client-manifest.jsonявляетсяVueSSRClientPluginгенерируется в соответствии с клиентомbundle,а такжеvue-ssr-server-bundle.jsonявляетсяVueSSRServerPluginПлагин сгенерирован на стороне сервераbundle. Имея указанный выше файл пакета, мы можем обработать запрос:

//server.js
const fs = require("fs")
const express = require("express")
const { createBundleRenderer } = require('vue-server-renderer')

const template = fs.readFileSync("./src/index.template.html", "utf-8")
const bundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')


const app = express();
app.use("/dist", express.static("dist"))

const renderer = createBundleRenderer(bundle, {
    template,
    clientManifest
})


app.get('*', (req, res) => {
    renderer.renderToString({}, function (err, html) {
        res.end(html);
    });
})

app.listen(8080, function () {
    console.log("server start and listen port 8080")
})

   В этот раз мы не использовалиvue-server-rendererсерединаcreateRendererфункционировать, но использоватьcreateBundleRendererфункция, мыserver.jsбыли введены вserver-bundle.jsonа такжеclient-manifest.jsonс шаблономtemplate.html, а затем передать егоcreateBundleRendererгенерация функцийrenderer, а затем в каждом запросе вызыватьrendererизrenderToStringметод, сгенерируйте соответствующий HTML-код, а затем верните клиенту.renderToStringПервый параметр - это, по сути, контекстcontextобъект, с одной стороныcontextИспользуется для обработки файлов шаблонов, например существующих в файлах шаблонов.

<title>{{title}}</title>

а такжеcontextсуществуют вtitle: 'SSR', файлы в шаблоне будут интерполированы. Другая часть, входной файл клиентаserver-entry.jsСредняя функция также получитcontext, который можно использовать для передачи связанных параметров.

  Почему мы используемexpress.staticправильноdistПричина, по которой файлы в папке предоставляют службы статических ресурсов, заключается в том, что соответствующий код будет внедрен в код клиента.JavaScriptфайл (например,app.[hash].js), чтобы обеспечить возможность запроса соответствующих ресурсов.

   Затем запускаем команду:

node server.js

   и зайдите на http://localhost:8080 в браузере. Вы обнаружите, что простая программа счетчика уже запущена, и она работает, и нажатие кнопки вызовет соответствующее событие.

   Это соответствует принятой структуре html:

   Мы обнаружили, что возвращенный html-код имеет структуру DOM, соответствующую нашему экземпляру Vue, которая отличается от обычной структуры на стороне клиента и существует в корневом элементе.data-server-renderedАтрибут, указывающий, что структура является узлом, соответствующим рендерингу сервером. В режиме разработки Vue сравнивает визуализированный виртуальный DOM с текущей структурой DOM. Если они равны, текущая структура будет повторно использоваться. В противном случае визуализированный виртуальный DOM будет отброшен.Хорошая структура, вместо повторного рендеринга на стороне клиента. В производственном режиме шаг обнаружения будет пропущен и повторно использован напрямую, чтобы избежать потери производительности.

   При серверном рендеринге компонент проходит толькоbeforeCreateа такжеcreatedдва срока службы, а остальные, например.beforeMountЖизненный цикл ожидания не выполняется на стороне сервера, поэтому следуетbeforeCreateа такжеcreatedКод жизненного цикла, создающий глобальные побочные эффекты, напримерbeforeCreateа такжеcreatedиспользуется вsetIntervalнастраиватьtimer, пока вbeforeDestroyилиdestroyedУничтожить его при жизни, что вызываетtimerникогда не будет отменен.

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