VueJS: Plugin vue-meta và cách sử dụng

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Mô tả

vue-meta là một plugin Vue 2.0 cho phép quản lý thông tin meta của app, nó giống như react-helmet của React. Tuy nhiên, thay vì cài đặt dữ liệu như là các props được truyền tới component duy nhất thì ta đơn giản là export nó như là một của dữ liệu của component sử dụng thuộc tính metaInfo.

Những thuộc tính này khi thiết lập cho component lồng thì sẽ ghi đè metaInfo của component cha, điều này sẽ cho phép tùy chỉnh thông tin cho mỗi view ở top-level giống như việc ghép thông tin trực tiếp cho các component con lồng sâu bên trong nhằm mục đích dễ bảo trì hơn.

Cài đặt

Yarn

$ yarn add vue-meta

NPM

$ npm install vue-meta --save

CDN

Nếu bạn sử dụng phiên bản thấp hơn bạn sử dụng link sau:

Không nén:

<script src="https://unpkg.com/vue-meta@1.5.8/lib/vue-meta.js"></script>

Bản nén:

<script src="https://unpkg.com/vue-meta@1.5.8/lib/vue-meta.min.js"></script>

Sử dụng

Bước 1: Chuẩn bị plugin

Bạn có thể bỏ qua bước này nếu bạn không cần SSR và Vue có sẵn như một biến global. vue-meta sẽ tự cài đặt trong trường hợp này.

Để sử dụng plugin thì trước tiên ta cần truyền nó tới Vue.use - nếu bạn không render trên server-side thì file JS đầu vào của bạn sẽ không cần sửa gì, ngược lại thì ta sẽ đặt nó trong file mà chạy cả ở server và client trước khi đối tượng root được mount. Nếu ta đang dùng vue-router thì file router.js sẽ trông như sau:

router.js:

import Vue from 'vue'
import Router from 'vue-router'
import Meta from 'vue-meta'
 
Vue.use(Router)
Vue.use(Meta)
 
export default new Router({
  ...
})

Các tùy chọn

vue-meta cho phép các tùy chọn sau:

Vue.use(Meta, {
  keyName: 'metaInfo', // tên tùy chọn component để vue-meta tìm thông tin meta.
  attribute: 'data-vue-meta', // tên thuộc tính để vue-meta thêm vào các tag mà nó giám sát
  ssrAttribute: 'data-vue-meta-server-rendered', // tên thuộc tính để vue-meta biết rằng thông tin meta cũng được server-render
  tagIDKeyName: 'vmid' // tên thuộc tính để vue-meta sử dụng nhằm xác định là ghi đè hay nạp cho tag
})

Nếu ta không quan tâm đến server-side rendering ta có thể bỏ qua và chuyển sang bước 3, ngược lại thì ta tiếp tục chuyển sang bước 2.

Bước 2: Server Rendering (tùy chọn)

Nếu ta có một webapp dạng isomorphic/universal thì ta cần render metadata trên server side. Cách thức như sau:

Bước 2.1: Hiển thị $meta ở bundleRenderer

Ta sẽ cần hiển thị các kết quả của phương thức $meta mà vue-meta đã thêm vào đối tượng Vue ở ngữ cảnh bundle render trước khi có thể bắt đầu đưa vào thông tin meta. Bạn cần làm điều này trong file server-entry:

server-entry.js:

import app from './app'
 
const router = app.$router
const meta = app.$meta() // ở đây
 
export default (context) => {
  router.push(context.url)
  context.meta = meta // tại đây
  return app
}

Bước 2.2: Điền thông tin meta bằng inject()

Điều tiếp theo bạn cần làm trước khi bắt đầu sử dụng các tùy chọn của metaInfo trong các component là hãy đảm bảo chúng làm việc trên server bằng cách nạp chúng để bạn có thể gọi text() trên mỗi item nhằm hiển thị những thông tin cần thiết. Ta có 2 phương thức sau đây:

Hiển thị đơn giản với renderToString()

Xét phương thức dễ nhất để gộp phần head nếu Vue server được hiển thị ở dạng chuỗi:

server.js:

app.get('*', (req, res) => {
  const context = { url: req.url }
  renderer.renderToString(context, (error, html) => {
    if (error) return res.send(error.stack)
    const bodyOpt = { body: true }
    const {
      title, htmlAttrs, headAttrs, bodyAttrs, link, style, script, noscript, meta 
    } = context.meta.inject()
    return res.send(`
      <!doctype html>
      <html data-vue-meta-server-rendered ${htmlAttrs.text()}>
        <head ${headAttrs.text()}>
          ${meta.text()}
          ${title.text()}
          ${link.text()}
          ${style.text()}
          ${script.text()}
          ${noscript.text()}
        </head>
        <body ${bodyAttrs.text()}>
          ${html}
          <script src="/assets/vendor.bundle.js"></script>
          <script src="/assets/client.bundle.js"></script>
          ${script.text(bodyOpt)}
        </body>
      </html>
    `)
  })
})

Nếu ta đang sử dụng file template riêng thì hãy sửa thẻ <head> thành:

<head>
  {{{ meta.inject().title.text() }}}
  {{{ meta.inject().meta.text() }}}
</head>

Lưu ý là sử dụng {{{ thay vì {{, hãy cực kỳ thận trọng khi sử dụng {{{ với __dangerouslyDisableSanitizers.

Truyền kết xuất với renderToStream()

Phương thức này phức tạp hơn một chút nhưng tốt hơn, đó là thay thế việc việc truyền response. Ở đây vue-meta hỗ trợ việc truyền không hề mất chi phí dựa vào ngữ cảnh bundleRenderer thông mình của Vue:

server.js

app.get('*', (req, res) => {
  const context = { url: req.url }
  const renderStream = renderer.renderToStream(context)
  renderStream.once('data', () => {
    const bodyOpt = { body: true }
    const {
      title, htmlAttrs, headAttrs, bodyAttrs, link, style, script, noscript, meta 
    } = context.meta.inject()
    res.write(`
      <!doctype html>
      <html data-vue-meta-server-rendered ${htmlAttrs.text()}>
        <head ${headAttrs.text()}>
          ${meta.text()}
          ${title.text()}
          ${link.text()}
          ${style.text()}
          ${script.text()}
          ${noscript.text()}
        </head>
        <body ${bodyAttrs.text()}>
    `)
  })
  renderStream.on('data', (chunk) => {
    res.write(chunk)
  })
  renderStream.on('end', () => {
    res.end(`
          <script src="/assets/vendor.bundle.js"></script>
          <script src="/assets/client.bundle.js"></script>
          ${script.text(bodyOpt)}
        </body>
      </html>
    `)
  })
  renderStream.on('error', (error) => res.status(500).end(`<pre>${error.stack}</pre>`))
})

Bước 3: Định nghĩa metaInfo

Trong bất kỳ component nào thì việc định nghĩa thuộc tính metaInfo sẽ có dạng như sau:

App.vue:

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>
 
<script>
  export default {
    name: 'App',
    metaInfo: {
      // nếu không component con nào có metaInfo.title, thì title này được dùng:
      title: 'Tiêu đề trang',
      // tất cả các title sẽ được sử dụng theo template sau:
      titleTemplate: '%s | My Awesome Webapp'
    }
  }
</script> 

Home.vue

<template>
  <div id="page">
    <h1>Home Page</h1>
  </div>
</template>
 
<script>
  export default {
    name: 'Home',
    metaInfo: {
      title: 'V1Study chẳng hạn',
      // ghi đè template cha và chỉ sử dụng title ở trên
      titleTemplate: null
    }
  }
</script> 

About.vue

<template>
  <div id="page">
    <h1>About Page</h1>
  </div>
</template>
 
<script>
  export default {
    name: 'About',
    metaInfo: {
      // title sẽ được đưa vào titleTemplate cha
      title: 'About Us'
    }
  }
</script> 

Các thuộc tính chuẩn của metaInfo

title (String)

Thuộc tính này sẽ điền giá trị text vào giữa thẻ mở và đóng của thẻ <title>.

{
  metaInfo: {
    title: 'Foo Bar'
  }
}

Kết quả: 

<title>Foo Bar</title>

titleTemplate (String | Function)

Giá trị của thuộc tính title ở trên sẽ được thế vào phần %s của titleTemplate trước khi được render. Tiêu đề gốc sẽ có sẵn ở metaInfo.titleChunk.

{
  metaInfo: {
    title: 'Foo Bar',
    titleTemplate: '%s - Baz'
  }
}

Kết quả: 

<title>Foo Bar - Baz</title>

Từ phiên bản v1.2.0 thì titleTemple cũng có thể là hàm:

titleTemplate: (titleChunk) => {
  // Nếu không định nghĩa hoặc trống thì không cần gạch nối
  return titleChunk ? `${titleChunk} - Site Title` : 'Site Title';
}

htmlAttrs (Object)

Mỗi cặp key:value sẽ tương ứng với attribute:value của phần tử <html>.

{
  metaInfo: {
    htmlAttrs: {
      foo: 'bar',
      amp: undefined
    }
  }
}

Kết quả: 

<html foo="bar" amp></html>

headAttrs (Object)

Mỗi cặp key:value sẽ tương ứng với attribute:value của phần tử <head>.

{
  metaInfo: {
    headAttrs: {
      foo: 'bar'
    }
  }
}

Kết quả: 

<head foo="bar"></head>

bodyAttrs (Object)

Mỗi cặp key:value sẽ tương ứng với attribute:value của phần tử <body>.

{
  metaInfo: {
    bodyAttrs: {
      bar: 'baz'
    }
  }
}

Kết quả: 

<body bar="baz">Foo Bar</body>

base (Object)

Tương ứng với thẻ <base>:

{
  metaInfo: {
    base: { target: '_blank', href: '/' }
  }
}

Kết quả: 

<base target="_blank" href="/">

meta ([Object])

Mỗi item trong mảng ứng với một phần tử <meta>:

{
  metaInfo: {
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' }
    ]
  }
}

Kết quả:

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

Từ phiên bản v1.5.0 ta có thể thiết lập template cho meta để nó có thể làm việc tương tự như titleTemplate:

{
  metaInfo: {
    meta: [
      { charset: 'utf-8' },
      {
        'property': 'og:title',
        'content': 'Test title',
        'template': chunk => `${chunk} - My page`, //or as string template: '%s - My page',
        'vmid': 'og:title'
      }
    ]
  }
}

Kết quả: 

<meta charset="utf-8">
<meta name="og:title" property="og:title" content="Test title - My page">

link ([Object])

Mỗi item trong mảng ứng với một thẻ <link>:

{
  metaInfo: {
    link: [
      { rel: 'stylesheet', href: '/css/index.css' },
      { rel: 'favicon', href: 'favicon.ico' }
    ]
  }
}

Kết quả: 

<link rel="stylesheet" href="/css/index.css">
<link rel="favicon" href="favicon.ico">

style ([Object])

Mỗi item trong mảng ứng với môt một phần tử <style>:

{
  metaInfo: {
    style: [
      { cssText: '.foo { color: red }', type: 'text/css' }
    ]
  }
}

Kết quả: 

<style type="text/css">.foo { color: red }</style> 

script ([Object])

Mỗi item trong mảng ứng với một phần tử <script>:

{
  metaInfo: {
    script: [
      { innerHTML: '{ "@context": "http://schema.org" }', type: 'application/ld+json' }
    ]
  }
}

Kết quả:

<script type="application/ld+json">{ "@context": "http://schema.org" }</script>

Nếu trình duyệt không hỗ trợ defer hay một lý do nào đó thì ta cần đặt <script> trước </body> sử dụng body.

{
  metaInfo: {
    script: [
      { innerHTML: 'console.log("I am in body");', type: 'text/javascript', body: true }
    ]
  }
}

noscript ([Object])

Mỗi item trong mảng ứng với một phần tử <noscript>:

{
  metaInfo: {
    noscript: [
      { innerHTML: 'This website requires JavaScript.' }
    ]
  }
}

Kết quả:

<noscript>This website requires JavaScript.</noscript>

__dangerouslyDisableSanitizers ([String])

Mặc định thì vue-meta sẽ đặt mỗi thành phần HTML với một thuộc tính. Ta có thể bỏ hành vi này bằng cách sử dụng __dangerouslyDisableSantizers. Hãy truyền cho nó các thuộc tính ta muốn thiết đặt:

{
  metaInfo: {
    title: '<I will be sanitized>',
    meta: [{ vmid: 'description', name: 'description', content: '& I will not be <sanitized>'}],
    __dangerouslyDisableSanitizers: ['meta']
  }
}

Kết quả: 

<title>&lt;I will be sanitized&gt;</title>
<meta vmid="description" name="description" content="& I will not be <sanitized>">

Lưu ý: Sử dụng tùy chọn này khi ta biết chính xác ta đang làm gì. Việc bỏ tính năng thiết đặt này sẽ làm tăng nguy cơ bị tấn công như SQL injection và Cross-Site Scripting (XSS).

__dangerouslyDisableSanitizersByTagID ({[String]})

Cung cấp tính năng giống như __dangerouslyDisableSanitizers nhưng có thể chỉ định thuộc tính nào của tagIDKeyName cần được disabled. Một đối tượng tạo ra cần có vmid là key và một mảng các thuộc tính:

{
  metaInfo: {
    title: '<I will be sanitized>',
    meta: [{ vmid: 'description', name: 'still-&-sanitized', content: '& I will not be <sanitized>'}],
    __dangerouslyDisableSanitizersByTagID: { description: ['content'] }
  }
}

Kết quả:

<title>&lt;I will be sanitized&gt;</title>
<meta vmid="description" name="still-&amp;-sanitized" content="& I will not be <sanitized>">

Lưu ý: Sử dụng tùy chọn này khi ta biết chính xác ta đang làm gì. Việc bỏ tính năng thiết đặt này sẽ làm tăng nguy cơ bị tấn công như SQL injection và Cross-Site Scripting (XSS).

changed (Function)

Được gọi khi metaInfo phía client thực hiện việc update hoặc change. Hàm nhận các tham số sau:

  • newInfo (Object) - Trạng thái mới của đối tượng metaInfo.
  • addedTags ([HTMLElement]) - một danh sách các phần tử được thêm vào.
  • removedTags ([HTMLElement]) - một danh sách các phần tử bị xóa bỏ.

Ngữ cảnh this là một đối tượng component changed được định nghĩa.

{
  metaInfo: {
    changed (newInfo, addedTags, removedTags) {
      console.log('Meta info was updated!')
    }
  }
}

Cách thực hiện metaInfo

Ta có thể định nghĩa metaInfo tại bất kỳ component nào. Các component con mà có metaInfo gộp đệ quy metaInfo của chúng vào ngữ cảnh cha và sẽ ghi đè bất kỳ thuộc tính giống nhau nào. Hãy xét cấu trúc component như sau:

<parent>
  <child></child>
</parent>

Nếu cả <parent>  <child> đều định nghĩa thuộc tính title trong metaInfo thì title được định nghĩa trong <child> sẽ được sử dụng.

Danh sách các thẻ

Khi chỉ định một mảng trong metaInfo thì giống như ví dụ dưới đây, hành vi mặc định sẽ đơn giản là nối danh sách.

Input:

// component cha
{
  metaInfo: {
    meta: [
      { charset: 'utf-8' },
      { name: 'description', content: 'foo' }
    ]
  }
}
// component con
{
  metaInfo: {
    meta: [
      { name: 'description', content: 'bar' }
    ]
  }
}

Output:

<meta charset="utf-8">
<meta name="description" content="foo">
<meta name="description" content="bar">

Đây không phải điều ta muốn vì thẻ meta description phải là thẻ duy nhất trong trang web. Ta chỉnh sửa điều này bằng cách sau đây:

Input:

// component cha
{
  metaInfo: {
    meta: [
      { charset: 'utf-8' },
      { vmid: 'description', name: 'description', content: 'foo' }
    ]
  }
}
// component con
{
  metaInfo: {
    meta: [
      { vmid: 'description', name: 'description', content: 'bar' }
    ]
  }
}

Output:

<meta charset="utf-8">
<meta vmid="description" name="description" content="bar">

Trong khi các giải pháp như là react-helmet quản lý thứ tự xảy ra và hợp nhất hành vi cho ta một cách tự động thì nó phát sinh nhiều code hơn và vì vậy dễ gây lỗi nhiều hơn, trong khi cách thức trên gần như là an toàn tuyệt đối vì tính linh hoạt của nó; với chi phí một lần đánh đổi là: các thuộc tính vmid này sẽ được render trong lần đánh dấu cuối cùng (vue-meta sử dụng trình khách này để ngăn cản việc sao chép hoặc ghi đè). Nếu ta đang phục vụ cho nội dung GZIP'ped của ta thì việc tăng nhẹ tải trọng HTTP sẽ là không đáng kể.

Hiệu năng

Ở phía trình khách, vue-meta phân tách các cập nhật DOM sử dụng requestAnimationFrame. Nó cần phải làm điều này vì nó đăng ký một Vue mixin để đăng ký tới vòng đời hook beforeMount trên tất cả các component theo hướng được thông báo rằng các render đã đang xảy ra và dữ liệu đang sẵn sàng. Nếu vue-meta không phân tách các cập nhật thì thông tin meta DOM sẽ được tính toán lại và sẽ được cập nhật cho mỗi component trên trang một cách liên tiếp.

Nhờ vào việc phân tách cập nhật mà việc cập nhật sẽ chỉ xảy ra một lần - ngay cả khi thông tin meta chính xác được biên dịch bởi server. Nếu bạn không muốn hành vi này thì hãy xem bên dưới.

Cách ngăn chặn việc update trên trang kết xuất ban đầu

thêm thuộc tính data-vue-meta-server-rendered vào thẻ <html> trên phái máy chủ:

<html data-vue-meta-server-rendered>
...

vue-meta sẽ kiểm tra thuộc tính này mỗi khi nó cố cập nhật DOM - nếu nó tồn tại thì vue-meta sẽ xóa nó và không cho update. Nếu nó không có sẵn thì vue-meta sẽ cho phép update.

Lưu ý: Trong khi điều này có thể là dài dòng thì đây là điều có chủ ý. vue-meta xử lý điều này cho ta một cách tự động sẽ giúp hạn chế khả năng tương tác với các ngôn ngữ lập trình server-side khác. Ví dụ, nếu bạn sử dụng PHP cho máy chủ thì bạn có thể có meta info được xử lý trên server và muốn ngăn chặn bản cập nhật không liên quan này.

FAQ

Dưới đây là một vài câu trả lời cho một số câu hỏi phổ biến.

Tôi sử dụng prop và data component trong metaInfo thế nào?

Trả lời: Thay vì định nghĩa metaInfo như là một đối tượng thì hãy định nghĩa nó như là một hàm và truy cập this bình thường:

Post.vue:

<template>
  <div>
    <h1>{{{ title }}}</h1>
  </div>
</template>
 
<script>
  export default {
    name: 'post',
    props: ['title'],
    data () {
      return {
        description: 'A blog post about some stuff'
      }
    },
    metaInfo () {
      return {
        title: this.title,
        meta: [
          { vmid: 'description', name: 'description', content: this.description }
        ]
      }
    }
  }
</script>

PostContainer.vue:

<template>
  <div>
    <post :title="title"></post>
  </div>
</template>
 
<script>
  import Post from './Post.vue'
 
  export default {
    name: 'post-container',
    components: { Post },
    data () {
      return {
        title: 'Example blog post'
      }
    }
  }
</script> 

Tôi xử lý metaInfo từ kết quả của một action bất đồng bộ thế nào?

vue-meta sẽ làm điều này cho bạn một cách tự động khi state component của bạn thay đổi.

Hãy đảm bảo rằng ban đang sử dụng dạng function của metaInfo:

{
  data () {
    return {
      title: 'Foo Bar Baz'
    }
  },
  metaInfo () {
    return {
      title: this.title
    }
  }
}

Vì sao vue-meta không hỗ trợ jsnext:main?

Ban đầu thì có, tuy nhiên nó phát sinh vấn đề. Về cơ bản thì Vue không hỗ trợ jsnext:main, và nó không hướng nội cho thuộc tính default thể hiện từ bạn ES2015, do đó nó phá bỏ độ phân giải module.

jsnext:main là một thuộc tính không chuẩn và nó sẽ bị bỏ, và vue-meta được đóng gói thành một file không có phần bên trong module động cũng như thực tế là nếu bạn đang sử dụng vue-meta thì khả năng 99.9% bạn không sử dụng nó có điều kiện, vì vậy nó hoàn toàn không được hỗ trợ.

Nếu đây không phải là điều bạn muốn thì bạn phải hướng Babel để chuyển các import default thành cấu trúc module phổ biến với một plugin, điều này không được lý tưởng vì nhiều người dùng Vue viết code bằng TypeScript mà không phải là Babel.

» Tiếp: Hướng dẫn SEO với Vue.js
« Trước: Ví dụ tổng quan về Vuex-CLI3
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!