Понять принцип Vue SSR и построить структуру проекта

Vue.js
Понять принцип Vue SSR и построить структуру проекта

1. Зачем использовать SSR?

В традиционном одностраничном приложении Vue рендеринг страницы выполняется с помощью js, как показано на следующем рисунке, в html-файле, возвращаемом сервером.bodyтолько один изdivэтикетка иscriptтеги, остальная часть DOM-структуры страницы будетbundle.jsстроить, затем монтировать<div id="app"></div>начальство. Это делает невозможным для поисковых роботов сканирование содержимого страницы, и если SEO важно для вашего сайта, вам может потребоваться рендеринг на стороне сервера (SSR) для решения этой проблемы.

image.pngВ дополнение к SEO, используйте SSR для ускорения скорости предшественника первого экрана, поскольку сервер напрямую возвращает страницу рендеринга HTML, вам не нужно JS, чтобы увидеть полную страницу рендеринга. Эта часть кода невелик по сравнению с приложением одной страницы, эта часть кода невелика, поэтому время прибытия первого экрана будет быстрее, а белый экран короче.

Конечно, использование SSR имеет некоторые ограничения: во-первых, условия разработки ограничены, в серверном рендеринге не доступен хук жизненного цикла, кроме Created и BeforeCreate. Во-вторых, большая загрузка на стороне сервера, рендеринг полного приложения на сервере, явно больше занятых ресурсов ЦП, чем только последовательные файлы. Кроме того, SSR предъявляет дополнительные требования к развертыванию. В отличие от полностью статического одностраничного приложения (SPA), которое можно развернуть на любом статическом файловом сервере, сервер отображает приложение, которое должно работать в node.js. Поэтому при выборе технологии SSR необходимо учитывать ее преимущества и недостатки, чтобы понять, нужна ли она.

Во-вторых, реализация основных функций

Суть SSR заключается в том, что сервер возвращает обработанный HTML-документ. Сначала мы запускаем сервер в корневом каталоге проекта, а затем возвращаем html-документ. Здесь мы используем koa в качестве серверной среды.

//server.js
const Koa = require('koa')
const router = require('koa-router')()

const koa = new Koa()
koa.use(router.routes())

router.get('/',(ctx)=>{
  ctx.body = `<!DOCTYPE html>      //要返回给客户端的html
  <html lang="en">
    <head><title>Vue SSR</title></head>
    <body>
      <div>This is a server render page</div>
    </body>
  </html>`
})

koa.listen(9000, () => {
  console.log('server is listening in 9000');
})

Запустите сервер в командной строке:node server.js, а затем перейдите в браузереhttp://localhost:9000/, сервер возвращает следующее содержимое, и браузер отображает страницу в соответствии с этим html.

image.png

vue-server-renderer

Конечно, возвращаемая строка html может быть сгенерирована шаблоном vue, что требует использованияvue-server-renderer,Так и будетСгенерировать html-строку на основе экземпляра Vue, является ядром Vue SSR. надserver.jsНемного изменено:

const Koa = require('koa')
const router = require('koa-router')()

const koa = new Koa()
koa.use(router.routes())

const Vue = require('Vue')     //导入Vue,用于创建Vue实例
const renderer = require('vue-server-renderer').createRenderer()  //创建一个 renderer 实例
const app = new Vue({          //创建Vue实例
  template: `<div>{{msg}}</div>`,
  data(){
    return {
      msg: 'This is renderred by vue-server-renderer'
    }
  }
})

router.get('/',(ctx)=>{
  //调用renderer实例的renderToString方法,将Vue实例渲染成字符串
  //该方法接受两个参数,第一个是Vue实例,第二个是一个回调函数,在渲染完成后执行
  renderer.renderToString(app, (err, html) => {   //渲染得到的字符串作为回调函数的第二个参数传入
    ctx.body = `<!DOCTYPE html>
    <html lang="en">
      <head><title>Vue SSR</title></head>
      <body>
        ${html}    //将渲染得到的字符串拼接到要返回的结果中
      </body>
    </html>`
  })
})

koa.listen(9000, () => {
  console.log('server is listening in 9000');
})

Перезапустите сервер и зайдите снова:

image.png

Таким образом, мы завершили очень простой Vue SSR. Однако это не очень практично, мы не можем написать это в реальной разработке проекта, мы будем строить проект модульно, а затем упаковывать его в один или несколько js-файлов через инструмент упаковки.

Официальный момент использования

Проект по созданию модульного vue

Мы строим простой проект vue модульно, используяvue-routerУправлять маршрутизацией.

// 打包入口文件 src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
  el: '#app',
  router,
  render: h => h(App)
})
//  src/App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>
<style lang="less">
#app{
  margin: 0 auto;
  width: 700px;
  #nav{
    margin-bottom: 20px;
    text-align: center;
  }
}
</style>
//  src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]

export default new VueRouter({
  mode: 'history',
  routes
})
// src/views/Home.vue
<template>
  <div class="home">
    <h1>This is home page</h1>
  </div>
</template>
// src/views/About.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

кsrc/main.jsКак файл записи упаковки, он упаковывается как одна страница на стороне клиента, а затем открывается в браузере.Результат рендеринга выглядит следующим образом:

image.png

Превратите проект в рендеринг на стороне сервера

Далее мы преобразуем приведенную выше демонстрацию в рендеринг на стороне сервера.

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

Создайте новый файл записи, предназначенный для рендеринга на стороне сервера.entry.server.js:

import { createApp } from './main'

export default context => {  //生成Vue实例的工厂函数,
  return new Promise((resolve, reject) => {
    const app = createApp()
    const router = app.$router

    const { url } = context    //context包含服务端需要传递给Vue实例的一些数据,比如这里的路由
    const { fullPath } = router.resolve(url).route

    if(fullPath !== url){  //判断当前路由在Vue实例中是否存在
      return reject({
        url: fullPath
      })
    }

    router.push(url)      //设置Vue实例的当前路由

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()  //判断当前路由是否有对应组件
      if(!matchedComponents.length){
        return reject({
          code: 404
        })
      }
      resolve(app)    //返回Vue实例
    }, reject)
  })
}

Будуsrc/main.jsПреобразуйте следующим образом:

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

Vue.config.productionTip = false

export function createApp(){
  const router = createRouter()
  const app = new Vue({
    router,
    render: h => h(App)
  })
  return app
}

на основеentry.server.jsупакованныйwebpackКонфигурация, но также внести некоторые изменения:

target: 'node',
entry: './src/entry.server.js',
output: {
  path: path.join(__dirname, '../dist'),
  filename: 'bundle.server.js',
  libraryTarget: 'commonjs2'
},

Затем на стороне сервера мы можем передать упакованныйbundle.server.jsКонец обслужен.

//server.js作如下改变:
const renderer = require('vue-server-renderer').createRenderer({   //基于模板创建一个 renderer 实例
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
const app = require('./dist/bundle.server.js').default    //导入Vue实例工厂函数
router.get('/(.*)', async (ctx, next) => {
  const context = {                   //获取路由,用于传递给Vue实例
    url: ctx.url
  }
  let htmlStr
  await app(context).then( res => {    //生成Vue实例,并传递给renderer实例生成字符串
    renderer.renderToString(res, context, (err,html)=>{
      if(!err){
        htmlStr = html
      }
    })
  })
  ctx.body = htmlStr
});

image.pngКак видите, здесь мы завершили рендеринг на стороне сервера, и структура dom страницы появляется в html-документе, возвращаемом сервером.

Активация клиента

Мы уже видели начало использования SSR в реальных проектах, но это только первый шаг. Теперь каждый раз, когда вы нажимаете «Главная/О программе», с сервера запрашиваются html-ресурсы, и преимущество внешней маршрутизации одной страницы не используется. Далее мы добавим шаг активации на стороне клиента, чтобы веб-приложение одновременно имело преимущества одной страницы. Это также официальный процесс Vue SSR.image.png

Так называемая активация на стороне клиента относится к процессу, когда Vue принимает статический HTML, отправленный сервером на стороне браузера, и превращает его в динамический DOM, управляемый Vue.Справочник по принципу активацииОфициальный сайт.

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

создать новыйentry.client.js:

import { createApp } from './main'

const app = createApp()
const router = app.$router

router.onReady(() => {
  app.$mount('#app')      //服务端渲染默认会生成一个id为app的div
})

упакованныйwebpackКонфигурация:

entry: './src/entry.client.js',
output: {
  path: path.join(__dirname, '../dist'),
  filename: 'bundle.client.js'
},

После упаковки этоbundle.client.jsДобавлено в html, мы рендерили на основе шаблона раньше:

const renderer = require('vue-server-renderer').createRenderer({   //基于模板创建一个 renderer 实例
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})

Так что просто поставьbundle.client.jsдобавить вindex.template.htmlВот и все.

//index.template.html
<!DOCTYPE html>
<html lang="en">
  <head><title>Vue SSR</title></head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
  <script src="bundle.client.js"></script>
</html>

Перезапустите службу и зайдите снова.Вы можете видеть, что когда вы нажимаете Home/About для переключения маршрутов, вы больше не будете запрашивать html-документы с сервера.image.png

3. Запросить данные

В реальных проектах страница часто заполняется и рендерится данными, запрашиваемыми из интерфейса, далее мы будем использовать запрошенные данные для рендеринга страницы. Для удобства (избавления от проблем) вместо написания интерфейса данных давайте запросим 20 лучших данных о фильмах Дубана.

конкретные идеи,То есть, если компонент должен запросить данные, когда он отображается на стороне сервера, мы запрашиваем данные на стороне сервера, и когда клиент использует компонент в режиме переключателя маршрутизации SPA, мы также можем отправлять данные запроса AJAX на сторона клиента.

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

Запросить данные на стороне сервера

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

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

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

//Home.vue
<template>
  <div class="movie-list">
    <div v-for="(item, index) in list" class="movie">
      <img class="cover" :src="item.cover">
      <p>
        <span class="title">{{item.title}}</span>
        <span class="rate">{{item.rate}}</span>
      </p>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'MovieList',
    asyncData ({ store,route }) {    //自定义静态方法asyncData
      return store.dispatch('getTopList')     
    },
    
    /*****
    在这里,执行asyncData,就会调用getTopList方法去请求数据
    并将数据更新到vue实例的$store.state中
    actions: {            
      getTopList (store) {      
        return top20().then((res) => {
          store.commit('setTopList', res.data.subjects)
        })
      }
    }
    *****/
    
    computed: {
      list () {
        return this.$store.state.topList
      }
    },
    created () {
      if(!this.$store.state.topList){
        this.$store.dispatch('getTopList')
      }
    }
  }
</script>

существуетentry.server.js, мы получаем компоненты, соответствующие router.getMatchedComponents(), посредством маршрутизации, и этот метод вызывается, если компонент предоставляет asyncData. Затем нам нужно присоединить проанализированное состояние к контексту рендеринга.

import { createApp } from './main'

export default context => {
  return new Promise((resolve, reject) => {
    const app = createApp()
    const router = app.$router
    const store = app.$store
    ...
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if(!matchedComponents.length){
        return reject({
          code: 404
        })
      }
      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          //如果组件暴露出 asyncData,就调用这个方法
          //在本例中,就会去请求豆瓣数据,并把数据更新到app.$store.state
          return Component.asyncData({  
            store,
            route: router.currentRoute
          })
        }
      })).then(() => {
        context.state = store.state  //将app.$store.state赋值给渲染上下文context.state,后面同步数据到客户端的时候会用到。
        resolve(app)
      }).catch(reject)
    }, reject)
  })
  })
}

Когда данные обновляются до app.$store.state, в HTML-коде есть данные, отображаемые сервером. Но страница пуста и отправляется запрос ajax. Причина в том, что при активации клиента он фактически подвергается вторичному рендерингу, то есть при загрузке и выполнении bundle.client.js страница снова рендерится с помощью bundle.client.js, вообще говоря, результатом рендеринга будет так же, как и раньше, так что это не заметно.

image.png

Избегайте повторного запроса клиентом данных

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

При использовании шаблона context.state будет использоваться какwindow.__INITIAL_STATE__состояние, автоматически встроенное в окончательный HTML. И когда клиент активируется, перед подключением к приложению vm.$store клиента должен получитьwindow.__INITIAL_STATE__условие.

1. Вserver.jsв, дляrenderer.renderToStringметод добавляет второй параметрcontext, context.state будет использоваться какwindow.__INITIAL_STATE__состояние, автоматически встроенное в окончательный HTML.

router.get('/(.*)', async (ctx, next) => {
  const context = {
    url: ctx.url
  }
  let htmlStr
  await app(context).then( res => {
    renderer.renderToString(res, context, (err,html)=>{  //添加第二个参数context
      if(!err){
        htmlStr = html
      }
    })
  })
  ctx.body = htmlStr
});

image.png

  1. Исправлятьentry.client.js:
import { createApp } from './main'

const app = createApp()
const router = app.$router
const store = app.$store

if (window.__INITIAL_STATE__) {   //如果window.__INITIAL_STATE__有内容,就存到app.$store中
  store.replaceState(window.__INITIAL_STATE__)
}

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

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

image.png

Этот проект очень прост в создании, в основном для того, чтобы разобраться в принципе и процессе Vue SSR.Для фактической разработки вы можете выбрать относительно зрелую структуру, такую ​​​​как nuxt.js.

адрес проекта: GitHub.com/Alxa О Лала/V…

использованная литература

Расшифровать Vue SSR

Научит вас создавать SSR (vue/vue-cli + экспресс)

Vue SSR Guide