Nuxtjs服務端渲染實踐,搭建一個blog

關於SSR的簡介

SSR,即服務端渲染,這其實是舊事重提的一個概念,我們常見的服務端渲染,一般見於後端語言生成的一段前端腳本,如:php後端生成html+jsscript內容傳遞給瀏覽器展現,nodejs在後端生成頁面模板供瀏覽器呈現,java生成jsp等等。

Vuejs、Reactjs、AngularJs這些js框架,原本都是開發web單頁應用(SPA)的,單頁應用的好處就是只需要初次加載完所有靜態資源便可在本地運行,此後頁面渲染都只在本地發生,只有獲取後端數據才需要發起新的請求到後端服務器;且因爲單頁應用是純js編寫,運行較爲流暢,體驗也稍好,故而和本地原生應用結合很緊密,有些對頁面響應流暢度要求不是特別苛刻的頁面,用js寫便可,大大降低了app開發成本。

然而單頁應用並不支持良好的SEO,因爲對於搜索引擎的爬蟲而言,抓取的單頁應用頁面源碼基本上沒有什麼變化,所以會認爲這個應用只有一個頁面,試想一下,一個博客網站,如果所有文章被搜索引擎認爲只有一個頁面,那麼你辛辛苦苦寫的大多數文章都不會被收錄在裏面的。

SSR首先解決的就是這個問題,讓人既能使用Vuejs、Reactjs來進行開發,又能保證有良好的SEO,且技術路線基本都是屬於前端開發棧序列,語言語法沒有多大變化,而搭載在Nodejs服務器上的服務端渲染又可以有效提高併發性能,一舉多得,何樂而不爲?

ps:當然,目前某些比較先進的搜索引擎爬蟲已經支持抓取單頁應用頁面了,比如谷歌。但並不意味着SSR就沒用了,針對於資源安全性要求比較高的場景,搭載在服務器上的SSR有着天然的優勢。

關於Nuxtjs

這裏是官方介紹,Nuxtjs是誕生於社區的一套SSR解決方案,是一個比較完備的Vuejs服務端渲染框架,包含了異步數據加載、中間件支持、佈局支持等功能。

關於nuxtjs,你必須要掌握以下幾點知識:

  1. vuejs、vue-router、vuex等
  2. nodejs編程
  3. webpack構建前端工程
  4. babel-loader
如果想使用進程管理工具,推薦使用pm2管理nodejs進程,安裝方式爲:npm install -g pm2

搭建一個blog

準備好工具

推薦下載

這裏iview將作爲一個插件在nuxtjs項目中使用。

注意幾個配置:
nux.config.js

module.exports = {
  /*
  ** Headers of the page
  */
  head: {
    title: '{{ name }}',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '{{escape description }}' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  plugins: [
      {src: '~plugins/iview', ssr: true}
  ],
  /*
  ** Customize the progress bar color
  */
  loading: { color: '#3B8070' },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  }
}

plugins文件夾下,加入iview.js

import Vue from 'vue';
import iView from 'iview';

Vue.use(iView);
import 'iview/dist/styles/iview.css';

如果你想要加入其它的配置,可以在nuxt.config.js的plugins配置項中加入,同時在plugins文件夾下加入引入邏輯。例如:
nuxt.config.js

{src: '~plugins/vuetify', ssr: true}

plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify'
Vue.use(Vuetify)

import 'vuetify/dist/vuetify.min.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'

配置很方便。

開始寫頁面

頁面佈局

<template>
  <div data-app>
    <v-app>
      <!-- header -->
      <v-toolbar dark color="primary" fixed v-show="showToolbar">
        <!--<v-toolbar-side-icon @click="drawer = !drawer"></v-toolbar-side-icon>-->

        <v-toolbar-title class="white--text"><a href="/" style="text-decoration-line:none;line-height: 40px;height: 40px;font-size:1.2em;color:white;">&nbsp;Blog</a></v-toolbar-title>

        <v-spacer></v-spacer>
        <v-spacer></v-spacer>
        <v-spacer></v-spacer>
        <!--<v-btn icon @click="showSearch">-->
          <!--<v-icon>search</v-icon>-->
        <!--</v-btn>-->
        <!--<v-text-field-->
          <!--hide-details-->
          <!--prepend-icon="search"-->
          <!--single-line-->
          <!--clearable-->
          <!--color="yellow"-->
          <!--placeholder="輸入博客內容"-->
        <!--&gt;</v-text-field>-->
        <v-autocomplete
          v-model="searching"
          :items="articles"
          item-text="name"
          item-value="id"
          color="red"
          prepend-icon="search"
          placeholder="輸入搜索內容"
          hide-no-data
          hide-selected
          :loading="isLoading"
          browser-autocomplete
          clearable
          :search-input.sync="changeSearch"
        >
          <template slot="selection" slot-scope="data">
            {{data.item.name}}
          </template>
          <template slot="item" slot-scope="data">
            <v-list-tile-content @click="toDetail(data.item.id)">
              <v-list-tile-title v-html="data.item.name"></v-list-tile-title>
              <v-list-tile-sub-title v-html="data.item.group"></v-list-tile-sub-title>
            </v-list-tile-content>
          </template>
        </v-autocomplete>

        <v-menu bottom transition="slide-y-transition" offset-y open-on-hover left>
          <v-btn icon
          slot="activator"
          >
            <img :src="languageChoice" alt="" width="26px">
          </v-btn>
          <v-list>
            <v-list-tile @click="languageChoice = '/imgs/cn.webp'">
              <img src="/imgs/cn.webp" alt="">&nbsp; <v-list-tile-title>簡體中文</v-list-tile-title>
            </v-list-tile>
            <v-list-tile @click="languageChoice = '/imgs/us.webp'">
              <img src="/imgs/us.webp" alt="">&nbsp;<v-list-tile-title>English</v-list-tile-title>
            </v-list-tile>
          </v-list>
        </v-menu>
        <v-tooltip bottom >
          <v-btn icon slot="activator" href="mailto:[email protected]" nuxt>
            <v-icon>contact_mail</v-icon>
          </v-btn>
          <span>mailto:[email protected]</span>
        </v-tooltip>
        <v-menu bottom left transition="slide-y-transition" offset-y open-on-hover>
          <v-btn
            slot="activator"
            dark
            icon
          >
            <v-icon>more_vert</v-icon>
          </v-btn>

          <!--<v-list style="width:150px">-->
            <!--<v-list-tile-->
              <!--href="/about"-->
              <!--target="_blank"-->
            <!--&gt;-->
              <!--<v-avatar size="30px" color="lime">-->
                <!--<v-icon dark small>account_circle</v-icon>-->
              <!--</v-avatar>-->
              <!--<v-spacer></v-spacer>-->
              <!--<v-list-tile-title style="text-align: end"><span style="margin-right:10px;">關於我</span></v-list-tile-title>-->
            <!--</v-list-tile>-->
          <!--</v-list>-->
        </v-menu>
      </v-toolbar>






      <!-- content -->
      <v-content :style="contentStyle">
          <nuxt/>
      </v-content>


        <v-btn
          fab
          color="red"
          bottom
          right
          style="bottom:20%"
          fixed
          @click="toAdd"
        >
          <v-icon color="white">add</v-icon>
        </v-btn>


      <!-- footer -->
      <v-footer style="margin-top:25px;">
        <v-layout
          justify-center
          row
          wrap
        >
          <v-flex xs12 text-xs-center indigo darken-4 white--text py-2>
            Site's built by <a href="https://vuejs.org">vuejs</a>/<a href="https://vuetifyjs.com">vuetifyjs</a>/<a
            href="https://nuxtjs.org">nuxtjs</a>/<a href="https://lumen.laravel.com">lumen</a>/<a href="https://github.com/hhxsv5/laravel-s">laravel-swoole</a>/<a
            href="https://wiki.swoole.com/">swoole</a> etc.
          </v-flex>
          <v-flex
            indigo darken-4
            py-3
            text-xs-center
            white--text
            xs12
          >
            &copy;2017-{{(new Date()).getFullYear()}}&nbsp;<strong><a href="/">Rainbow-blog</a> by Henry. All rights reserved.</strong>
          </v-flex>
        </v-layout>
      </v-footer>
      <!-- back to top -->
      <v-fab-transition>
        <v-btn
          v-show="showUp"
          color="red"
          v-model="fab"
          dark
          fab
          fixed
          bottom
          right
          @click="$vuetify.goTo(target, options)"
        >
          <v-icon>keyboard_arrow_up</v-icon>
        </v-btn>
      </v-fab-transition>
      <v-snackbar
        v-model="snackbar"
        color="info"
        :timeout="3000"
        :vertical="true"
        top
        right
      >
        {{ location }}
        <v-btn
          dark
          flat
          @click="snackbar = false"
        >
          Close
        </v-btn>
      </v-snackbar>
    </v-app>
  </div>
</template>
<script>
  export default {
    head: {

    },
    data() {
        return {
          location: "",
          snackbar: false,
          languageChoice: "/imgs/cn.webp",
          contentStyle: {
            marginTop:"64px"
          },
          showToolbar: true,
          drawer: null,
          items: [
            { title: 'Home', icon: 'dashboard' },
            { title: 'About', icon: 'question_answer' }
          ],
          mini: false,
          right: null,
          showUp: false,
          fab: true,
          target: 0,
          options: {
            duration: 300,
            offset: 0,
            easing: 'easeInOutCubic'
          },
          articles: [
          ],
          searching: "",
          isLoading: false,
          changeSearch: ""
        }
    },
    watch: {
      changeSearch(newV, oldV) {
        if (newV == 'undefined' || !newV) {
          return ;
        }
        this.isLoading = true
        // Lazily load input items
        this.$axios.get('https://api.hhhhhhhhhh.com/blog/index?'+ '_kw='+newV)
          .then(res => {
            this.articles = res.data
            console.log(this.articles);
          })
          .catch(err => {
            console.log(err)
          })
          .finally(() => (this.isLoading = false))
        console.log(this.articles);
      },
      languageChoice(value) {
        this.$axios.post('https://api.hhhhhhhh.com/hhhhh').then(res => {
          this.snackbar = true;
          this.location = res.data.location;
        });
      }
    },
    mounted() {
      window.addEventListener('scroll', () => {
        if (window.pageYOffset > 80) {
          this.showUp = true;
          if (this.$route.fullPath == '/') {
            this.showToolbar = true;
          }
        } else {
          this.showUp = false;
          if (this.$route.fullPath == '/') {
            this.showToolbar = false;
          }
        }
      });
      if (this.$route.fullPath == '/') {
        this.contentStyle.marginTop = "0px";
        this.showToolbar = false;
      }
    },
    methods: {
      toAdd() {
        location.href = '/hhhhh'
      },
      getHighlight(originStr) {
        if (!this.searching) {
          return originStr;
        }
        let ind = originStr.indexOf(this.searching);
        let len = this.searching.length;
        return originStr.substr(0, ind) + "<code>" + this.searching + "</code>" + originStr.substr(ind + len);
      },
      toDetail(id) {
        location.href = "/blog/"+id;
      },
    }
  }
</script>
<style>
html {
  font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 16px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: border-box;
  margin: 0;
}

.button--green {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #3b8070;
  color: #3b8070;
  text-decoration: none;
  padding: 10px 30px;
}

.button--green:hover {
  color: #fff;
  background-color: #3b8070;
}

.button--grey {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #35495e;
  color: #35495e;
  text-decoration: none;
  padding: 10px 30px;
  margin-left: 15px;
}

.button--grey:hover {
  color: #fff;
  background-color: #35495e;
}
div.v-image__image--cover {
  filter: blur(5px) !important;
}
.v-btn--floating .v-icon {
  height: auto !important;
}
  code {
    box-shadow: none !important;
    -webkit-box-shadow: none !important;
  }
</style>

所有頁面都寫在page/文件夾之下,例如新建一個index.vue頁面

<template>
  <div>
    <v-parallax
      src="./bg2.jpg"
      :height="bgHeight"
    >
      <v-layout
        align-center
        column
        justify-center
      >
        <h1 class="display-2 mb-3" style="color:black;">Blog</h1>
        <h4 class="subheading" style="color:black;">hhhhhhhafadsjfjasdf</h4>
        <h4 class="subheading" style="color:black;">blabla的個人博客站,深挖網站編程藝術</h4>
      </v-layout>
    </v-parallax>
    <!-- the blog list -->
    <v-container>

      <v-layout row wrap>
        <v-flex d-flex xs12 sm6>
          <v-card>
            <v-toolbar color="primary" dark>
              <v-toolbar-title>博客列表</v-toolbar-title>
              <v-spacer></v-spacer>
              <v-btn icon fab flat small @click="pageMinus">
                <v-icon>keyboard_arrow_left</v-icon>
              </v-btn>
              <v-btn fab small dark flat>{{page}}</v-btn>
              <v-btn icon fab flat small @click="pagePlus">
                <v-icon>keyboard_arrow_right</v-icon>
              </v-btn>
            </v-toolbar>

            <v-list three-line :expand="true">
              <div v-for="(item, index) in items" :key="item.id">
                <v-list-tile
                  avatar
                  ripple
                  @click="toDetail(item.id)"
                >
                  <v-list-tile-content>
                    <v-list-tile-title><strong>{{ item.title }}</strong></v-list-tile-title>


                    <v-list-tile-sub-title class="text--primary">{{ item.headline }}</v-list-tile-sub-title>
                    <v-list-tile-sub-title>{{ item.subtitle }}</v-list-tile-sub-title>
                    <div>
                      <v-chip outline color="pink" text-color="red" small v-for="cate in item.categories" :key="cate.id">
                        {{cate}}
                      </v-chip>
                    </div>
                  </v-list-tile-content>

                  <v-list-tile-action>
                    <v-list-tile-action-text>{{ item.action }}</v-list-tile-action-text>


                    <v-icon
                      color="yellow darken-2"
                    >
                      keyboard_arrow_right
                    </v-icon>
                  </v-list-tile-action>

                </v-list-tile>
                <v-divider
                  v-if="index + 1 < items.length"
                ></v-divider>
              </div>
            </v-list>
          </v-card>
        </v-flex>


        <v-flex d-flex xs12 sm5 offset-sm1>
          <v-layout row wrap>
            <v-flex d-flex>
              <v-layout row wrap>
                <h2 style="margin-top:16px;">最新博文:</h2>
                <v-flex
                  d-flex
                  xs12
                  v-for="post in posts" :key="post.id"
                >
                    <v-card class="my-3" hover>
                      <v-img
                        v-if="post.imgUrl"
                        class="white--text"
                        height="150px"
                        :src="post.imgUrl"
                      >
                        <v-container fill-height fluid>
                          <v-layout>
                            <v-flex xs12 align-end d-flex>
                              <span class="caption">{{post.date}}</span>
                            </v-flex>
                          </v-layout>
                        </v-container>
                      </v-img>
                      <v-card-title class="headline"><strong>{{ post.title }}</strong></v-card-title>
                      <v-card-text>
                        {{ post.subtitle }}
                      </v-card-text>
                      <v-card-actions>
                        <v-chip outline color="pink" text-color="red" small v-for="cate in post.categories.slice(0,3)" :key="cate">
                          {{cate}}
                        </v-chip>
                        <v-spacer></v-spacer>
                        <v-btn @click="toDetail(post.id)" flat class="blue--text">查看博文</v-btn>
                      </v-card-actions>

                    </v-card>
                </v-flex>
              </v-layout>
            </v-flex>
            <v-dialog
              v-model="openLoader"
              hide-overlay
              persistent
              width="300"
            >
              <v-card
                color="primary"
                dark
              >
                <v-card-text>
                  請稍候
                  <v-progress-linear
                    indeterminate
                    color="white"
                    class="mb-0"
                  ></v-progress-linear>
                </v-card-text>
              </v-card>
            </v-dialog>
          </v-layout>
        </v-flex>
      </v-layout>
    </v-container>
  </div>
</template>
<script>
  import axios from 'axios';
  export default {
    head: {
      title: "博客 - 首頁",
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: 'blog description'
        },
        {name: 'keywords', content: '博客,代碼,技術,web開發'},
        
        {name:"baidu-site-verification", content: "nVF2mYh7tG"}
      ]
    },
    asyncData() {
      return axios.get('https://blabla.blabla.com/blog/index?page=1').then(res => {
        return axios.get('https://blabla.blabla.com/blog/index?page='+1).then(res1 => {
          return {
            items: res.data,
            posts: res1.data.splice(0, 4)
          };
        });
      });
    },
    data () {
      return {
        openLoader: true,
        bgHeight: "920",
        title: 'Your Logo',
        page: 1,
        posts: [

        ],
        items: [

        ],
        isMaxPage: false
      }
    },
    mounted() {
      this.openLoader = false;
      
    },
    methods: {
      toggle (index) {
        const i = this.selected.indexOf(index)
        if (i > -1) {
          this.selected.splice(i, 1)
        } else {
          this.selected.push(index)
        }
      },
      toDetail(id) {
        location.href = "/blog/"+id;
      },
      pageMinus() {
        if (this.page == 1) {
          return ;
        }
        this.page--;
      },
      pagePlus() {
        if (this.isMaxPage) {
          return ;
        }
        this.page++;
      }
    },
    watch: {
      page(val) {
        this.openLoader = true;
        this.$axios.get('https://blabla.blabla.com/blog/index?page='+val).then(res => {
          this.items = res.data;
          if (this.items.length < 7) {
            this.isMaxPage = true;
          } else {
            this.isMaxPage = false;
          }
          this.openLoader = false;
        });
      }
    }
  }
</script>
<style>
  .v-parallax__image {
    filter: blur(9px)
  }
  .v-list--three-line .v-list__tile {
    height: 175px;
  }
  .v-chip--small {
    height: 18px;
  }
</style>

對這一部分代碼的解讀:

由於博客站使用的是vuetify編寫的,故而引用了vuetify作爲網站的UI插件。

佈局

寫法與單頁應用類似,但要注意幾個不同點:

  • 單頁應用一般會用vue-router的寫法表示加載路由頁面內容的位置:
<router-view></router-view>

而在nuxt中,要寫成

<nuxt/>
  • created和data中的邏輯,是在服務端加載時處理的,並不是瀏覽器端,瀏覽器端的邏輯比如window或location等對象要在mounted中寫,否則會報錯。
  • head中定義一些元數據,這些元數據會被爬蟲抓取到,可以在每一個頁面中自定義。

頁面

  • 單文件組件中的模板的寫法與單頁應用並無而已,直接寫就好,只是記住不要在模板中寫js邏輯
  • vue實例中head中可以定義的變量就是指<head></head>中定義的參數,例如本例:
head: {
  title: "首頁",
  meta: [
    {
      hid: 'description',
      name: 'description',
      content: '我就是一個小站點!'
    },
    {name: 'keywords', content: '博客,代碼,技術,開發'},
    {name:"google-site-verification", content:"RHlJ7VR51QWbIQFsW_s5qQrbbQPNBkTwhVLCgbFu_6g"},
    {name:"baidu-site-verification", content: "nVF2mYh7tG"}
  ]
}

將會被node渲染爲如下html:

<head>
    <title>首頁</title>
    <meta hid='description' name='description' content= '我就是一個小站點' />
    <meta name='keywords' content='博客,代碼,技術,開發' />
    <meta name='google-site-verification' content='RHlJ7VR51QWbIQFsW_s5qQrbbQPNBkTwhVLCgbFu_6g' />
    <meta name='baidu-site-verification' content= 'nVF2mYh7tG' />
</head>

這也是SEO的一個關鍵點,請注意。

  • 在渲染頁面之前,如果有一些原始數據是需要從外部拿到後纔可以繼續的,使用asyncData異步獲取數據,這裏異步獲取數據會在數據完全獲取完畢後纔會去渲染頁面,例如本例:
asyncData() {
  return axios.get('https://api.fshkehfahsfua.com/blog/list?page=1').then(res => {
    return axios.get('https://api.blohfhsldfhl.com/blog/listpo2?page='+1).then(res1 => {
      return {
        items: res.data,
        posts: res1.data.splice(0, 4)
      };
    });
  });
},

這裏要注意一下:asyncData中定義的數據,最好在data中也定義一下,因爲asyncData的數據會覆蓋data。

data() {
    return {
        posts: [],
        items: [],
    }
}

哦對了,還有blog詳情頁_id.vue

_id.vue表示可以用形似blog/123來進行訪問,這是vuejs單文件組件的常用寫法,這裏不贅述。
<template>
    <div>
        哈哈哈這裏是詳情頁,敏感代碼不貼了~
    </div>
</template>

<script>
import axios from 'axios';
let initId = 0;
export default {
    validate({params}) {
      initId = params.id;
      return /^\d+$/.test(params.id)
    },
    head() {
      return {
        title: this.title,
        meta: [
          {hid: 'description', name: "description", content: this.descript},
          {name: "keywords", content: this.keywords},
        ],
      }
    },
    asyncData() {
    },
    data() {
    }
}
</script>
獲取那個傳過來的ID,就用validate()中的寫法,在下面用的時候,就直接使用initId便可

結尾

以上是一些源碼的解析,本地運行命令npm run devnpm run start便可。

資源鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章