Menambahkan SSR Ke Aplikasi Vue JS - CRUDPRO

Menambahkan SSR Ke Aplikasi Vue JS

Render sisi server memungkinkan pengembang memuat aplikasi sisi klien mereka di server alih-alih mengunduh seluruh aplikasi dan memuat semuanya sekaligus. Ini meningkatkan kinerja aplikasi dengan hanya memuat kode yang perlu dilihat oleh pengguna. Selain itu, perayap mesin telusur dapat mengindeks aplikasi yang dirender sisi server dengan lebih mudah karena tidak melibatkan pemuatan JavaScript pada klien karena rendering sudah dilakukan di sisi server. Oleh karena itu, meningkatkan pengoptimalan mesin telusur juga merupakan satu hal untuk meningkatkan peluang peringkat aplikasi.

Vue.js mendukung rendering sisi server. Namun, itu tidak ditambahkan secara default. Jika Anda menggunakan Vue CLI untuk merancah aplikasi Anda, kemungkinan Anda akan mengalami masalah saat mengonversi aplikasi Vue.js yang dimuat di sisi klien biasa ke sisi server yang dirender menggunakan instruksi yang ada di https://ssr.vuejs.org /

Dalam cerita ini, kami akan membuat aplikasi yang menampilkan data dari New York Times API. Anda dapat mendaftar untuk kunci API di https://developer.nytimes.com/. Setelah itu kita bisa mulai membuat aplikasi. Selain membangun aplikasi, kami akan menambahkan render sisi server agar kami dapat merender aplikasi kami di sisi server.

Untuk mulai membangun aplikasi, kita harus menginstal Vue CLI. Kami melakukan ini dengan menjalankan:

npm install -g @vue/cli

Node.js 8.9 atau lebih baru diperlukan untuk menjalankan Vue CLI. Saya tidak berhasil menjalankan Vue CLI dengan Node.js versi Windows. Ubuntu tidak memiliki masalah menjalankan Vue CLI untuk saya. Lalu kita jalankan:

vue create vue-material-nyt-app

untuk membuat folder proyek dan membuat file. Di wizard, alih-alih menggunakan opsi default, kami memilih 'Pilih fitur secara manual'. Kemudian kita pilih Babel, Router, dan Vuex dari daftar pilihan dengan menekan spasi pada masing-masing pilihan. Jika berwarna hijau, itu berarti mereka dipilih.

Selanjutnya, kami menambahkan penyaji sisi server kami ke aplikasi kami. Kami menggunakan Plugin Vue CLI SSR, yang memungkinkan kami mengonversi aplikasi kami yang dibuat dengan Vue CLI ke aplikasi yang dirender sisi server hampir secara instan. Untuk melakukan ini, kami menjalankan:

vue add @akryum/ssr

Perintah ini akan menjalankan semua skrip yang diperlukan untuk mengonversi aplikasi Vue.js yang dibuat dengan Vue CLI 3.x ke aplikasi yang dirender sisi server. Untuk menjalankan aplikasi kami, kami menjalankan:

npm run ssr:serve

untuk menjalankan versi yang dirender sisi server dari aplikasi kita. Perhatikan bahwa tidak seperti aplikasi Vue CLI pada umumnya, Anda mungkin harus menekan F5 untuk menyegarkan dan melihat keluaran baru Anda ditampilkan.

Sekarang kita perlu menginstal beberapa perpustakaan. Kita perlu menginstal klien HTTP, pustaka untuk memformat tanggal, satu untuk menghasilkan string kueri GET dari objek, dan satu lagi untuk validasi formulir. Kita juga perlu menginstal pustaka Vue Material itu sendiri. Kami melakukan ini dengan menjalankan:

npm i axios moment querystring vee-validate vue-material

axios adalah klien HTTP kami, moment untuk memanipulasi tanggal, querystring adalah untuk menghasilkan string kueri dari objek vee-validate adalah paket tambahan untuk Vue.js untuk melakukan validasi. vue-material adalah perpustakaan Desain Material kami.

Sekarang kita telah menginstal semua perpustakaan, kita dapat mulai membangun aplikasi kita. Pertama kita buat beberapa komponen. Di folder views, kita buat Home.vue dan Search.vue. Itu adalah file kode untuk halaman kami. Buat folder mixins dan buat file bernama nytMixin.js. Mixin adalah cuplikan kode yang dapat dimasukkan langsung ke dalam komponen Vue.js kami dan digunakan seolah-olah mereka berada langsung di dalam komponen. Kemudian kami menambahkan beberapa filter. Filter adalah kode Vue.js yang memetakan dari satu hal ke hal lainnya. Kami membuat folder filter dan menambahkan capitalize.js dan formatDate.js . Kemudian di folder komponen, kami membuat file bernama SearchResults.vue . Folder komponen berisi komponen Vue.js yang bukan halaman.

Untuk membuat pengiriman data antar komponen lebih mudah dan lebih terorganisir, kami menggunakan Vuex untuk manajemen status. Karena kami memilih Vuex ketika kami menjalankan vue create , kami harus memiliki store.js di folder proyek kami. Jika tidak, buatlah. Di store.js , kami menempatkan:

impor Vue dari 'vue'
impor Vuex dari 'vuex'
Vue.use(Vuex)
fungsi ekspor createStore() {
   kembalikan Vuex.Store baru({
     negara() {
       kembali {
         Hasil Pencarian: []
       }
     },
     mutasi: {
       setSearchResults(status, muatan) {
         state.searchResults = muatan;
       }
     },
     tindakan: {
     }
   })
}

Objek negara adalah tempat negara disimpan. objek mutasi adalah tempat kita dapat memanipulasi keadaan kita. Ketika kita memanggil this.$store.commit(“setSearchResults”, searchResults) dalam kode kita mengingat searchResults telah didefinisikan, maka state.searchResults akan disetel ke searchResults . Kami kemudian bisa mendapatkan hasilnya dengan menggunakan this.$store.state.searchResults . Perhatikan bahwa kami membungkus toko Vuex di createStore() . Ini untuk membuat instance baru toko saat dimuat untuk setiap pengguna sehingga setiap pengguna aplikasi menggunakan toko mereka sendiri, mencegah pencemaran status toko untuk satu pengguna oleh pengguna lain. Kami membutuhkan ini untuk aplikasi yang dirender sisi server karena pengguna tidak perlu mengunduh salinan aplikasi mereka sendiri lagi karena semua rendering dilakukan di sisi server.

Kita perlu menambahkan beberapa kode boilerplate ke aplikasi kita. Pertama kita tambahkan filter kita. Di capitalize.js , kami menempatkan:

export const capitalize = (str) => {
    if (typeof str == 'string') {
        if (str == 'realestate') {
            return 'Real Estate';
        }
        if (str == 'sundayreview') {
            return 'Sunday Review';
        }
if (str == 'tmagazine') {
            return 'T Magazine';
        }
        return `${str[0].toUpperCase()}${str.slice(1)}`;
    }
}
import * as moment from 'moment';
export const formatDate = (date) => {
    if (date) {
        return moment(date).format('YYYY-MM-DD hh:mm A');
    }
}

untuk memformat tanggal kita ke dalam format yang dapat dibaca manusia.

Di main.js , kami menempatkan:

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import VueMaterial from 'vue-material';
import VeeValidate from 'vee-validate';
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'
import { formatDate } from './filters/formatDate';
import { capitalize } from './filters/capitalize';
Vue.config.productionTip = false;
Vue.use(VueMaterial);
Vue.use(VeeValidate);
Vue.filter('formatDate', formatDate);
Vue.filter('capitalize', capitalize);
export async function createApp({
  beforeApp = () => { },
  afterApp = () => { }
} = {}) {
  const router = createRouter()
  const store = createStore()
  await beforeApp({
    router,
    store,
  })
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })
  const result = {
    app,
    router,
    store,
  }
  await afterApp(result)
  return result
}

Kode di atas akan memuat instance baru untuk setiap pengguna, sehingga setiap pengguna akan memiliki instance aplikasinya sendiri. Ini karena pemrosesan dilakukan di sisi server sehingga tidak ada pengguna yang akan mengunduh instance aplikasi yang benar-benar baru. Saat beberapa pengguna menggunakan aplikasi yang sama, tindakan mereka kemungkinan akan bertentangan dengan menimpa tindakan orang lain di aplikasi, menyebabkan pencemaran status aplikasi oleh pengguna yang berbeda melakukan hal yang berbeda pada instance yang sama dari aplikasi yang dirender sisi server.

Perhatikan pada file di atas, kita harus mendaftarkan perpustakaan yang kita gunakan dengan Vue.js dengan memanggil Vue.use pada mereka sehingga mereka dapat digunakan di template aplikasi kita. Kami memanggil Vue.filter pada fungsi filter kami sehingga kami dapat menggunakannya di template kami dengan menambahkan pipa dan nama filter di sebelah kanan variabel kami.

Kemudian di router.js , kami menempatkan:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue';
import Search from './views/Search.vue';
Vue.use(Router)
export function createRouter() {
  return new Router({
     mode: 'history',
      base: process.env.BASE_URL,
      routes: [
      {
        path: '/',
        name: 'home',
        component: Home
      },
      {
        path: '/search',
        name: 'search',
        component: Search
      }
    ]
  })
}

sehingga kita bisa pergi ke halaman ketika kita memasukkan URL yang terdaftar. mode: 'history' berarti kita tidak akan memiliki tanda hash antara URL dasar dan rute kita. Perhatikan bahwa kami membungkus objek Router di dalam suatu fungsi sehingga kami membuat instance baru dari setiap pengguna sehingga navigasi satu pengguna tidak dapat memengaruhi navigasi pengguna lain.

Jika kita menggunakan aplikasi kita maka kita perlu mengonfigurasi server web kita sehingga semua permintaan akan dialihkan ke index.html sehingga kita tidak akan mengalami kesalahan saat memuat ulang aplikasi. Misalnya, di Apache, kami melakukan:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

dan di Nginx, kami menempatkan:

location / {
  try_files $uri $uri/ /index.html;
}

Lihat dokumentasi server web Anda untuk melihat cara melakukan hal yang sama di server web Anda.

Sekarang kita menulis kode untuk komponen kita. Di SearchResult.vue , kami menempatkan:

<template>
  <div id="search-results">
    <md-card v-for="s in searchResults" :key="s.id">
      <md-card-header>
        <div class="md-title title">{{s.headline.main}}</div>
      </md-card-header>
<md-card-content>
        <md-list>
          <md-list-item>Date: {{s.pub_date | formatDate}}</md-list-item>
          <md-list-item>
            <a :href="s.web_url">Link</a>
          </md-list-item>
          <md-list-item v-if="s.byline.original">{{s.byline.original}}</md-list-item>
          <md-list-item>{{s.lead_paragraph}}</md-list-item>
          <md-list-item>{{s.snippet}}</md-list-item>
        </md-list>
      </md-card-content>
    </md-card>
  </div>
</template>
<script>
export default {
  computed: {
    searchResults() {
      return this.$store.state.searchResults;
    }
  }
};
</script>
<style scoped>
.title {
  margin: 0 15px !important;
}
#search-results {
  margin: 0 auto;
  width: 95vw;
}
.md-title.title {
  color: rgba(0, 0, 0, 0.87) !important;
}
</style>

Di sinilah mendapatkan hasil pencarian kami dari toko Vuex dan menampilkannya. Kami mengembalikan this.$store.state.searchResults dalam fungsi di properti yang dihitung di aplikasi kami sehingga hasil pencarian akan secara otomatis disegarkan saat status SearchResults toko diperbarui.

md-card adalah widget kartu untuk menampilkan data dalam sebuah kotak. v-for adalah untuk mengulang entri array dan menampilkan semuanya. md-list adalah widget daftar untuk menampilkan item dalam daftar dengan rapi di halaman. {{s.pub_date | formatDate}} adalah tempat filter formatDate kami diterapkan.

Selanjutnya kita menulis mixin kita. Kami akan menambahkan kode untuk panggilan HTTP kami di mixin kami. Di nytMixin.js , kami menempatkan:

const axios = require('axios');
const querystring = require('querystring');
const apiUrl = 'https://api.nytimes.com/svc';
const apikey = 'your api key';
export const nytMixin = {
    methods: {
        getArticles(section) {
            return axios.get(`${apiUrl}/topstories/v2/${section}.json?api-key=${apikey}`);
        },
searchArticles(data) {
            let params = Object.assign({}, data);
            params['api-key'] = apikey;
            Object.keys(params).forEach(key => {
                if (!params[key]) {
                    delete params[key];
                }
            })
            const queryString = querystring.stringify(params);
            return axios.get(`${apiUrl}/search/v2/articlesearch.json?${queryString}`);
        }
    }
}

Kami mengembalikan janji untuk permintaan HTTP kami untuk mendapatkan artikel di setiap fungsi. Dalam fungsi searchArticles, kami memijat objek yang kami masukkan ke dalam string kueri yang kami berikan ke permintaan kami. Pastikan Anda memasukkan kunci API ke dalam aplikasi Anda ke dalam konstanta apiKey dan menghapus apa pun yang tidak terdefinisi dengan:

Object.keys(params).forEach(key => {
  if (!params[key]) {
     delete params[key];
  }
})

Selanjutnya di Home.vue , kami menempatkan:

<template>
  <div>
    <div class="center">
      <h1>{{selectedSection | capitalize}}</h1>
      <br />
      <md-menu>
        <md-button class="md-raised" md-menu-trigger>Sections</md-button>
<md-menu-content>
          <md-menu-item v-for="s in sections" :key="s" @click="selectSection(s)">{{s | capitalize}}</md-menu-item>
        </md-menu-content>
      </md-menu>
    </div>
    <br />
<md-card v-for="a in articles" :key="a.id">
      <md-card-header>
        <div class="md-title title">{{a.title}}</div>
      </md-card-header>
<md-card-content>
        <md-list>
          <md-list-item>Date: {{a.published_date | formatDate}}</md-list-item>
          <md-list-item>
            <a :href="a.url">Link</a>
          </md-list-item>
          <md-list-item v-if="a.byline">{{a.byline}}</md-list-item>
          <md-list-item>{{a.abstract}}</md-list-item>
        </md-list>
        <img
          v-if="a.multimedia[a.multimedia.length - 1]"
          :src="a.multimedia[a.multimedia.length - 1].url"
          :alt="a.multimedia[a.multimedia.length - 1].caption"
          class="image"
        />
      </md-card-content>
    </md-card>
  </div>
</template>
<script>
import { nytMixin } from "../mixins/nytMixin";
export default {
  name: "home",
  mixins: [nytMixin],
  computed: {},
data() {
    return {
      selectedSection: "home",
      articles: [],
      sections: `arts, automobiles, books, business, fashion, food, health,
    home, insider, magazine, movies, national, nyregion, obituaries,
    opinion, politics, realestate, science, sports, sundayreview,
    technology, theater, tmagazine, travel, upshot, world`
        .replace(/ /g, "")
        .split(",")
    };
  },
beforeMount() {
    this.getNewsArticles(this.selectedSection);
  },
methods: {
    async getNewsArticles(section) {
      const response = await this.getArticles(section);
      this.articles = response.data.results;
    },
selectSection(section) {
      this.selectedSection = section;
      this.getNewsArticles(section);
    }
  }
};
</script>
<style scoped>
.image {
  width: 100%;
}
.title {
  color: rgba(0, 0, 0, 0.87) !important;
  margin: 0 15px !important;
}
</style>

Komponen halaman ini adalah tempat mendapatkan artikel untuk bagian yang dipilih, default ke bagian home. Kami juga memiliki menu untuk memilih bagian yang ingin kami lihat dengan menambahkan:

<md-menu>
  <md-button class="md-raised" md-menu-trigger>Sections</md-button>
<md-menu-content>
     <md-menu-item v-for="s in sections" :key="s" @click="selectSection(s)">{{s | capitalize}}
     </md-menu-item>
  </md-menu-content>
</md-menu>

Perhatikan bahwa kami menggunakan async dan menunggu kata kunci dalam kode janji kami alih-alih menggunakan then . Ini jauh lebih pendek dan fungsionalitas antara saat itu dan menunggu dan async setara. Namun, itu tidak didukung di Internet Explorer. Di blok beforeMount, kami menjalankan this.getNewsArticles untuk mendapatkan artikel saat halaman dimuat.

Kemudian di Search.vue , kami keluar:

<template>
  <div>
    <div class="center">
      <h1>Search</h1>
    </div>
    <form @submit="search" novalidate>
      <md-field :class="{ 'md-invalid': errors.has('keyword') }">
        <label for="keyword">Keyword</label>
        <md-input type="text" name="keyword" v-model="searchData.keyword" v-validate="'required'"></md-input>
        <span class="md-error" v-if="errors.has('keyword')">{{errors.first('keyword')}}</span>
      </md-field>
<div>
        <md-datepicker v-model="searchData.beginDate" :md-disabled-dates="disabledDates">
          <label>Begin Date</label>
        </md-datepicker>
      </div>
<div>
        <md-datepicker v-model="searchData.endDate" :md-disabled-dates="disabledDates">
          <label>End Date</label>
        </md-datepicker>
      </div>
<md-field>
        <label for="movie">Sort By</label>
        <md-select v-model="searchData.sort">
          <md-option value="newest">Newest</md-option>
          <md-option value="oldest">Oldest</md-option>
          <md-option value="relevance">Relevance</md-option>
        </md-select>
      </md-field>
<md-button class="md-raised" type="submit">Search</md-button>
    </form>
    <SearchResults />
  </div>
</template>
<script>
import { nytMixin } from "../mixins/nytMixin";
import SearchResults from "@/components/SearchResults.vue";
import * as moment from "moment";
export default {
  name: "search",
  mixins: [nytMixin],
  components: {
    SearchResults
  },
  computed: {
    isFormDirty() {
      return Object.keys(this.fields).some(key => this.fields[key].dirty);
    }
  },
  data: () => {
    return {
      searchData: {
        sort: "newest"
      },
      disabledDates: date => {
        return +date >= +new Date();
      }
    };
  },
  methods: {
    async search(evt) {
      evt.preventDefault();
      if (!this.isFormDirty || this.errors.items.length > 0) {
        return;
      }
      const data = {
        q: this.searchData.keyword,
        begin_date: moment(this.searchData.beginDate).format("YYYYMMDD"),
        end_date: moment(this.searchData.endDate).format("YYYYMMDD"),
        sort: this.searchData.sort
      };
      const response = await this.searchArticles(data);
      this.$store.commit("setSearchResults", response.data.response.docs);
    }
  }
};
</script>

Di sinilah kita memiliki formulir untuk mencari artikel. Kami juga memiliki 2 datepicker untuk memberi label kepada pengguna yang mengatur tanggal mulai dan berakhir. Kami hanya membatasi tanggal hingga hari ini dan sebelumnya sehingga kueri penelusuran masuk akal. Di blok ini:

<md-field :class="{ 'md-invalid': errors.has('keyword') }">
  <label for="keyword">Keyword</label>
  <md-input type="text" name="keyword" v-model="searchData.keyword" v-validate="'required'"></md-input>
  <span class="md-error" v-if="errors.has('keyword')">{{errors.first('keyword')}}</span>
</md-field>

Kami menggunakan vee-validate untuk memeriksa apakah bidang kata kunci pencarian yang diperlukan diisi. Jika tidak, itu akan menampilkan pesan kesalahan dan mencegah kueri melanjutkan. Kami juga menyarangkan komponen SearchResults kami ke dalam komponen halaman Pencarian dengan memasukkan:

components: {
  SearchResults
}

antara tag skrip dan <SearchResults /> di template.

Terakhir, kami menambahkan bilah dan menu teratas kami dengan meletakkan yang berikut di App.vue :

<template>
  <div id="app">
    <md-toolbar>
      <md-button class="md-icon-button" @click="showNavigation = true">
        <md-icon>menu</md-icon>
      </md-button>
      <h3 class="md-title">New York Times App</h3>
    </md-toolbar>
    <md-drawer :md-active.sync="showNavigation" md-swipeable>
      <md-toolbar class="md-transparent" md-elevation="0">
        <span class="md-title">New York Times App</span>
      </md-toolbar>
<md-list>
        <md-list-item>
          <router-link to="/">
            <span class="md-list-item-text">Home</span>
          </router-link>
        </md-list-item>
<md-list-item>
          <router-link to="/search">
            <span class="md-list-item-text">Search</span>
          </router-link>
        </md-list-item>
      </md-list>
    </md-drawer>
<router-view />
  </div>
</template>
<script>
export default {
  name: "app",
  data: () => {
    return {
      showNavigation: false
    };
  }
};
</script>
<style>
.center {
  text-align: center;
}
form {
  width: 95vw;
  margin: 0 auto;
}
.md-toolbar.md-theme-default {
  background: #009688 !important;
  height: 60px;
}
.md-title,
.md-toolbar.md-theme-default .md-icon {
  color: #fff !important;
}
</style>

Jika Anda menginginkan bilah atas dengan laci navigasi kiri, Anda harus mengikuti struktur kode di atas dengan tepat.

Di entry-server.js , kami menempatkan:

import { createApp } from './main'
export default context => {
  return new Promise(async (resolve, reject) => {
    const {
      app,
      router,
      store,
    } = await createApp()
  router.push(context.url)
router.onReady(() => {
      context.rendered = () => {
        // After all preFetch hooks are resolved, our store is now
        // filled with the state needed to render the app.
        // When we attach the state to the context, and the `template` option
        // is used for the renderer, the state will automatically be
        // serialized and injected into the HTML as `window.__INITIAL_STATE__`.
        context.state = store.state
        context.title = 'New York Times App'
      }
      resolve(app)
    }, reject)
  })
}

Kami menambahkan context.title = 'Aplikasi New York Times' untuk mengubah judul aplikasi, dan di index.html , kami menempatkan:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <title>{{ title }}</title>
  <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons">
  {{{ renderResourceHints() }}}
  {{{ renderStyles() }}}
</head>
<body>
  <!--vue-ssr-outlet-->
  {{{ renderState() }}}
  {{{ renderState({ contextKey: 'apolloState', windowKey: '__APOLLO_STATE__' }) }}}
  {{{ renderScripts() }}}
</body>
</html>

untuk memuat font Roboto dan Ikon Material. Bidang dalam objek konteks di entry-server.js dimuat di index.html . Misalnya, context.title di entry-server.js terikat dengan {{ title }} di index.html .

Setelah semua kode ditulis, kami memiliki yang berikut:

Menambahkan SSR Ke Aplikasi Vue JS
Menambahkan SSR Ke Aplikasi Vue JS
Menambahkan SSR Ke Aplikasi Vue JS
Menambahkan SSR Ke Aplikasi Vue JS