SSR,即服務端渲染,這實際上是舊事重提的一個概念,咱們常見的服務端渲染,通常見於後端語言生成的一段前端腳本,如:php後端生成html+jsscript內容傳遞給瀏覽器展示,nodejs在後端生成頁面模板供瀏覽器呈現,java生成jsp等等。php
Vuejs、Reactjs、AngularJs這些js框架,本來都是開發web單頁應用(SPA)的,單頁應用的好處就是隻須要初次加載完全部靜態資源即可在本地運行,此後頁面渲染都只在本地發生,只有獲取後端數據才須要發起新的請求到後端服務器;且由於單頁應用是純js編寫,運行較爲流暢,體驗也稍好,故而和本地原生應用結合很緊密,有些對頁面響應流暢度要求不是特別苛刻的頁面,用js寫即可,大大下降了app開發成本。css
然而單頁應用並不支持良好的SEO,由於對於搜索引擎的爬蟲而言,抓取的單頁應用頁面源碼基本上沒有什麼變化,因此會認爲這個應用只有一個頁面,試想一下,一個博客網站,若是全部文章被搜索引擎認爲只有一個頁面,那麼你辛辛苦苦寫的大多數文章都不會被收錄在裏面的。html
SSR首先解決的就是這個問題,讓人既能使用Vuejs、Reactjs來進行開發,又能保證有良好的SEO,且技術路線基本都是屬於前端開發棧序列,語言語法沒有多大變化,而搭載在Nodejs服務器上的服務端渲染又能夠有效提升併發性能,一舉多得,何樂而不爲?前端
ps:固然,目前某些比較先進的搜索引擎爬蟲已經支持抓取單頁應用頁面了,好比谷歌。但並不意味着SSR就沒用了,針對於資源安全性要求比較高的場景,搭載在服務器上的SSR有着自然的優點。
這裏是官方介紹,Nuxtjs是誕生於社區的一套SSR解決方案,是一個比較完備的Vuejs服務端渲染框架,包含了異步數據加載、中間件支持、佈局支持等功能。vue
關於nuxtjs,你必需要掌握如下幾點知識:java
若是想使用進程管理工具,推薦使用pm2管理nodejs進程,安裝方式爲:
npm install -g pm2
推薦下載node
這裏iview將做爲一個插件在nuxtjs項目中使用。webpack
注意幾個配置:
nux.config.jsios
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.jslaravel
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;"> 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="輸入博客內容"--> <!--></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=""> <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=""> <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:thundervsflash@qq.com" nuxt> <v-icon>contact_mail</v-icon> </v-btn> <span>mailto:thundervsflash@qq.com</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"--> <!-->--> <!--<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 > ©2017-{{(new Date()).getFullYear()}} <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插件。
寫法與單頁應用相似,但要注意幾個不一樣點:
<router-view></router-view>
而在nuxt中,要寫成
<nuxt/>
<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 dev
或npm run start
即可。