Изоморфизм Vue (2): маршрутизация и разделение кода

внешний интерфейс сервер браузер Vue.js
Изоморфизм Vue (2): маршрутизация и разделение кода

предисловие

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

Предыдущий постИзоморфизм Вуэ (1)Мы рассказали, как использовать изоморфизм Vue для рендеринга простого компонента на стороне сервера и активации его на стороне сервера. Соответствующий код был загружен наGithub. В этой статье мы представляем знания, связанные с маршрутизацией в изоморфизме Vue.

маршрутизация

  Пишем здесь, давайте сначала обсудим, зачем нам нужна внешняя маршрутизация и зачем нам нужно внедрять Vue-Router в нашу программу? Фактически, самые ранние веб-сайты отображались на сервере, а не на браузерном. Каждый раз, когда вы вводите соответствующий URL-адрес на панели навигации браузера или щелкаете ссылку текущей страницы, браузер получает соответствующий URL-адрес и отображает HTML-страницу. Проблема в том, что каждое действие означает обновление страницы. Все это решает появление асинхронных запросов, мы можем динамически запрашивать данные через XMLHTTPRequest вместо того, чтобы каждый раз обновлять соответствующий интерфейс, реализуя взаимодействие страниц без фонового обновления. Позже появление одностраничного приложения (SPA: Single Page Web Application) продвинуло эту концепцию на шаг вперед: не только взаимодействие со страницей не требует обновления страницы, но даже переход на страницу не требует обновления текущей страницы. Когда переход на страницу не требует обновления текущей страницы, мы должны решить проблему переключения компонентов по разным URL-адресам, что и делает фронтенд-роутинг.

Концепция маршрутизатора на самом деле исходит из фона и отвечает за сопоставление URL-адресов с функциями. Например:

/user         ->    getAllUsers()
/user/count   ->    getUserCount()

Каждое из правил сопоставления URL-адреса с функцией, которое мы называемroute,а такжеrouterэквивалентно управлениюrouteконтейнер. Концепция внешней маршрутизации аналогична, за исключением того, что URL-адрес сопоставляет внешние компоненты. Например:

/user         ->    User组件

Маршрутизация рендеринга на стороне клиента

Благодаря элегантному дизайну Vue комбинация Vue и Vue Router очень проста: сначала нужно настроить правила маршрутизации и создать экземпляры маршрутизации, затем передать экземпляр маршрутизации корневому элементу Vue, чтобы добавить его, и, наконец, использоватьrouter-viewкомпонент, чтобы сообщить Vue Router, где выполнять рендеринг.

<div id="app">
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
</div>
//引入路由组件
import Home from '../components/Home.vue'
import About from '../components/About.vue'

//路由配置
const routes = [
  { path: '/', component: Home },
  { path: '/home', component: Home },
  { path: '/about', component: About }
]

//创建Vue Router实例
var router = new VueRouter({
    routes
})

//引入vue-router
const app = new Vue({
  router,
  //......
})

серверная маршрутизация

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

Vue Router фактически имеет два режима:hashузор иhistoryмодель.hashРежим — это режим по умолчанию для Vue Router. Чтобы прояснить эти два режима, мы должны упомянуть различную логику реализации, соответствующую этим двум режимам.

хэш-режим

   На самом деле можно подумать, что браузер нельзя обновлять в процессе переключения фронтенд-роутинга, иначе это нарушит правила взаимодействия SPA-роутинга. Прежде всего, мы нацелились на идентификатор фрагмента (якорь) в URL-адресе, как полный URL-адрес, формат следующий

пользователь:pass@www.example.com:80/dir/index.h…

Часть #ch1 - это то, что мы называемидентификатор фрагмента, который обычно можно использовать для обозначения подресурсов в полученном ресурсе. Изменение идентификатора фрагмента не приведет к обновлению браузера, поэтомуhashШаблон — это идентификатор фрагмента, используемый в качестве основы для внешней маршрутизации. Во внешней маршрутизации мы называем идентификатор фрагмента хеш-частью, хэш-часть — это только состояние клиента, а хеш-часть не будет получена сервером. мы можем пройтиwindow.onhashchagngeсобытие для прослушивания URLhashЧасть изменения, которая также является основой маршрутизации на основе хэшей. Например:

window.addEventListener('hashchange', function(e){
  console.log('hashchange', e);
})

Если хеш-часть браузера изменится, функция прослушивателя немедленно вызовет соответствующее событие.

режим истории

HTML5 представляет новые API для изменения URL-адресов без обновления текущей страницы. Существует два соответствующих метода:

  • pushState(state, title, url)
  • replaceState(state, title, url)

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

  • состояние (объект состояния): состояние объекта состояния является объектом JavaScript, и соответствующее состояние события можно прочитать после изменения URL-адреса. Может использоваться для восстановления состояния страницы.
  • title (название): этот параметр в настоящее время игнорируется, но может быть использован в будущем. Можно передать пустую строку
  • URL: Этот параметр определяет новую историческую запись URL.

а такжеpushStateа такжеreplaceStateОн используется сonpopstateсобытия, важно отметить, что вызовhistory.pushState()илиhistory.replaceState()не сработаетpopstateмероприятие. Это событие запускается только тогда, когда выполняется действие браузера, например, когда пользователь нажимает кнопку «Назад» в браузере (или вызывает в коде Javascripthistory.back())

Например:

window.addEventListener('popstate', function(){
	console.log("location: " + document.location + ", state: " +JSON.stringify(event.state));
})

history.pushState({page: 1}, "title 1", "?page=1");
history.back(); //"location: http://example.com/example.html?page=1, state: {"page":1}"

  Два режима мы закончили,historyв сравнении сhashВ плане урла должно быть красиво, но нужна поддержка фонового сервера, т.к.historyЯ больше всего боюсь, что браузер обновится, например, у нас front-end роутинг начинается с/homeизменить на/about, это только изменение внешнего URL-адреса, и оно не будет обновлять текущую страницу, а также включение браузера назад и вперед не обновит браузер. Но если после обновления браузер действительно запросит текущий URL-адрес, например/about. В настоящее время, если браузер не распознает URL-адрес, он может не найти текущую страницу.

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

   Сказав так много, какой режим нам нужно использовать для рендеринга сервера? То, что мы используем,historyРежим, это единственный вариант Ответ на самом деле был упомянут выше, потому что хэш-часть является только состоянием клиента и не будет получена сервером. Теперь предположим, что наше текущее приложение имеет два маршрута:

/             ->    Home
/about        ->    About

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

// router/index.js
import Vue from 'vue'
import Router from 'vue-router'

import Home from '../components/Home.vue'
import About from '../components/About.vue'

Vue.use(Router)

export function createRouter() {
    return new Router({
        mode: "history",
        routes: [{
            path: '/', component: Home
        }, {
            path: "/about", component: About
        }]
    })
}

  createRouterКаждый раз, когда вызывается функция, создается экземпляр маршрутизации, и конфигурация в экземпляре маршрутизацииhistoryрежим и правила маршрутизации настроены.

Далее посмотримHomeКомпоненты:

<template>
    <div>
        <div>当前位置: About</div>
        <router-link to="/home">前往Home</router-link>
        <button @click="directHome">按钮: 前往Home</button>
    </div>
</template>

<script>
    export default {
        name: "about",
        methods: {
            directHome: function () {
                this.$router.push('/');
            }
        }
    }
</script>

Причина, по которой я использую этот компонентrouter-linkтакже используется в случаеbutton, главным образом, чтобы доказать, что клиент был активирован.Aboutкомпоненты иHomeКомпоненты идентичны, за исключением имен и адресов ссылок, и больше не перечислены.

мы в корневом компонентеAppВизуализация компонентов сопоставления маршрутов в

//App.Vue
<template>
    <div id="app">
        <router-view></router-view>
    </div>
</template>

   Далее нам нужно продолжить трансформациюapp.js, мы уже представили сервер в предыдущей статьеapp.jsОсновная задача открыта за пределами заводской функции, логическая конкретная клиентская и боковая сторона браузера, соответственно, были переданы на клиентский файл ввода и сторона браузераentry-client.jsа такжеentry-server.js

import Vue from 'vue'
import App from './components/App.vue'
import {createRouter} from './router'

export function createApp() {
    const router = createRouter()

    const app =  new Vue({
        router,
        render: h => h(App)
    })

    return {
        app,
        router
    }
}

createAppОтличие от предыдущего заключается в том, что экземпляр Vue внедряется каждый раз при его создании.router. И вернул созданный экземпляр Vue и экземпляр Vue Router.

Логика рендеринга на стороне сервера сосредоточена вentry-server.js:

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

export default function (context) {
    return new Promise((resolve, reject) => {
        const {app, router} = createApp()
        router.push(context.url)
        router.onReady(() => {
            // Promise 应该 resolve 应用程序实例,以便它可以渲染
            resolve(app)
        }, reject)

    })
}

  entry-server.jsКак запись рендеринга на стороне сервера, она упакована как соответствующийbundleвходящийcreateBundleRendererгенерироватьrenderer,передачаrenderer.renderToStringможно пройти вcontext, который может содержать текущий маршрутurl. пока мыentry-server.jsв функции, которая принимаетcontextЗатем объект может получить информацию о маршрутизации.

   В отличие от статьи выше, мы не возвращали непосредственно экземпляр Vue, а возвращалиPromise,существуетPromiseСначала мы звонимcreateAppПолучить экземпляр Vueappи экземпляр Vue Routerrouter, то звонимpushФункция перемещает текущий маршрут к целевому URL-адресу. Затем мы звоним вrouter.onReadyфункция, которая гарантирует, что все ожидающие маршрутаАсинхронная функция ловушкиа такжеАсинхронные компонентыПосле загрузкиresolveТекущий экземпляр Vue.

а такжеentry-server.jsАналогично, упакованный входной файл клиентаentry-client.jsЕго также необходимо вызвать перед установкой приложения.router.onReady:

import { createApp } from './app'

const {app, router} = createApp();

router.onReady(() => {
    app.$mount('#app')
})

   Теперь перейдем к нашемуexpressКод сервера в основном такой же, как и при последнем рендеринге, но нам нужно датьrenderToStringпередатьcontextобъект, который содержит текущийurlстоимость.

//server.js
//省略......

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

//省略......

Теперь мы упаковываем серверную и браузерную частиbundleи запустите сервер:

   Теперь думаем о проблеме, если задать в качестве маршрутизацииrouterЕсли защита установлена ​​в , будет ли она выполняться в браузере или на сервере? Для проверки этой задачи даемrouterДобавить глобальную защитуbeforeEachа такжеafterEach:

export function createApp() {
    const router = createRouter()

    router.beforeEach((to, from, next) => {
        console.log("beforeEach---start");
        console.log('to: ', to.path, ' from: ', from.path);
        console.log("beforeEach---end");
        next();
    })

    router.afterEach((to, from) => {
        console.log("afterEach---start");
        console.log('to: ', to.path, ' from: ', from.path);
        console.log("afterEach---end");
    })
    // 省略......
}

  Мы посещаем напрямую/Маршрутизация, мы видим, что результаты вывода сервера и клиента выглядят следующим образом:

服务端

客户端

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

разделение кода

   Прежде всего, мы можем рассмотреть проблему Основная цель введения изоморфизма Vue состоит в том, чтобы ускорить скорость отображения первого экрана, тогда мы можем рассмотреть, если мы посетим/При маршрутизации вам нужно только загрузитьHomeКомпонент в порядке, его не нужно загружатьAboutкомпоненты. Мы можем загрузить его снова, когда нам это нужно.Aboutкомпонентов, чтобы мы могли уменьшить размер ресурсов, загружаемых при первоначальном рендеринге, и ускорить интерактивное время. Здесь мы можем рассмотреть возможность разделения кода.

  Разделение кода на самом деле является функцией, поддерживаемой Webpack, которая может упаковывать разные коды в разныеbundle, а затем загружать файл по запросу.

  WebpackПростейшее разбиение кода не более чем ручная работа, можно настроить несколькоentryЧтобы добиться, но есть много проблем в ручном режиме, таких как несколькоbundleоба ссылаются на один и тот же модуль, то каждыйbundleЕсть дублирующий код. Эта проблема хорошо решена, мы можем использоватьSplitChunksPluginплагин для решения этой проблемы. Но вручную все равно не очень удобно, поэтому Webpack предоставляет более удобныйдинамический импорт.

  Функция динамического импорта рекомендуется использовать предложение ECMAScriptimport()грамматика,import()Вы можете указать расположение загружаемого модуля, а затем динамически загружать модуль во время выполнения и возвращатьPromise. Скажем, мы хотим динамически загружать модульlodashМодули, которые мы можем сначала добавить в файл конфигурации Webpack:

output: {
    chunkFilename: '[name].bundle.js',
},

chunkFilenameИменно для конфигурации нужно определить имя незаходного чанка, а затем в коде:

import(/* webpackChunkName: "lodash" */ 'lodash').then(lodash => {
    //lodash便可以使用
})

  Код упаковки Мы можем обнаружить, что lodash упакован отдельно, потому что в комментарии мы присваиваем значение webpackChunkName для lodash, поэтому он называется lodash.bundle.js. Конечно этоchunkFilenameЭто не обязательно, по умолчанию будет названо[id].bundle.js.

Асинхронные компоненты

  Vue предоставляет концепцию асинхронных компонентов, позволяющую нам разбивать код на куски и загружать их по запросу. По сравнению с обычной регистрацией компонентов мы можем определить компоненты в виде фабричной функции, которая получитresolveОбратный вызов, эта функция обратного вызова будет вызываться, когда вы получите определение компонента с сервера. Или верните прямо в заводскую функциюPromise. мы знаемimport()Синтаксис возвращаетPromise, поэтому мы сопоставляем код до преобразования:

// router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export function createRouter() {
    return new Router({
        mode: "history",
        routes: [{
            path: '/',
            component: () => import('../components/Home.vue')
        }, {
            path: "/about",
            component: () => import('../components/About.vue')
        }]
    })
}

Затем упакуйте пакет клиента:

> vue-ssr-demo@1.0.0 build:client /Users/mr_wang/WebstormProjects/vue-ssr-demo
> cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules

Hash: 16fbba9bf008ec7ef466                                                            
Version: webpack 3.12.0
Time: 1158ms
                           Asset     Size  Chunks                    Chunk Names
       0.8ac6ad83b93d774d3817.js  5.04 kB       0  [emitted]         
       1.5967060b78729a4577f9.js  5.04 kB       1  [emitted]         
     app.1c160fc3e08eec3aed0f.js  7.37 kB       2  [emitted]         app
  vendor.f32c57c9ee5145002da1.js   296 kB       3  [emitted]  [big]  vendor
manifest.4b057fd51087adaec1f3.js  5.85 kB       4  [emitted]         manifest
    vue-ssr-client-manifest.json  1.48 kB          [emitted]     

   Мы обнаружили, что в выходном файле больше 0.[хэш].js и 1.[хэш].js, что соответствуетHomeКомпоненты иAboutкомпоненты. Конечно, если вы считаете, что этот модуль выглядит неясно, вы также можете передать параметр webpackChunkName, как указано выше, чтобы сделать проблемы с пакетами более идентифицируемыми:

component: import(/* webpackChunkName: "home" */'../components/Home.vue')
component: import(/* webpackChunkName: "about" */'../components/About.vue')

На данный момент файлы, упакованные Webpack:

Hash: aaf79995904c4786cadc                                                           
Version: webpack 3.12.0
Time: 976ms
                           Asset     Size  Chunks                    Chunk Names
                  home.bundle.js  5.04 kB       0  [emitted]         home
                 about.bundle.js  5.04 kB       1  [emitted]         about
     app.f22015420ff0db6ec4b0.js  7.37 kB       2  [emitted]         app
  vendor.f32c57c9ee5145002da1.js   296 kB       3  [emitted]  [big]  vendor
manifest.2a21c55e4a3e98ab252c.js  5.83 kB       4  [emitted]         manifest
    vue-ssr-client-manifest.json  1.44 kB          [emitted]         

Затем мы запускаем сервер, обращаемся к маршруту '/' и обнаруживаем, что запрос выглядит следующим образом:

Сначала мы смотрим в сеть, мы находим, что,0.[hash].jsсначала просил, потом просил1.[hash].js, и приоритеты загрузки у них разные,0.[hash].jsприоритет выше, чем1.[hash].js,Почему это? Давайте посмотрим на соответствующий html.

Мы видим, что0.[hash].jsЭто предварительная нагрузка при впрыскивании и1.[hash].jsКогда инжект это prefetch, есть ли разница между preload и prefetch?На самом деле и то, и другое можно написать в одной статье, но здесь мы кратко подытожим.

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

  предварительная загрузка — это указание браузеру загружать ресурсы, найденные позже, заранее. Некоторые ресурсы скрыты в CSS и JavaScript, браузер не знает, что странице вот-вот потребуются эти ресурсы, и когда они обнаружены, для загрузки уже слишком поздно, поэтому декларативная ранняя загрузка.

Суммировать

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