衆所周知,Vue SPA單頁面應用對SEO不友好,固然也有相應的解決方案。 服務端渲染 (SSR) 就是經常使用的一種。 SSR 有利於 搜索引擎優化(SEO, Search Engine Optimization) ,而且 內容到達時間(time-to-content) (或稱之爲首屏渲染時長)也有很大的優化空間。css
Nuxt.js 是一個基於 Vue.js
的輕量級應用框架,可用來建立 服務端渲染 (SSR)
應用,也可充當靜態站點引擎生成靜態站點應用,具備優雅的代碼結構分層和熱加載等特性。html
項目地址:明麼的博客vue
經過 Nuxt 官方提供的腳手架工具 create-nuxt-app 初始化項目:node
$ npx create-nuxt-app <項目名>
// 或者ios
$ yarn create nuxt-app <項目名>
項目建立的時候會讓你進行一些配置的選擇,可根據本身須要進行選擇。nginx
運行完後,它將安裝全部依賴項,下一步是啓動項目:git
$ cd <project-name> $ yarn dev
在瀏覽器中,打開 http://localhost:3000github
. ├── assets // 用於組織未編譯的靜態資源 ├── components // 用於組織應用的 Vue.js 組件 ├── layouts // 用於組織應用的佈局組件 ├── middleware // 用於存放應用的中間件 ├── node_modules ├── pages // 用於組織應用的路由及視圖 ├── plugins // 組織插件。 ├── static // 用於存放應用的靜態文件 ├── store // 狀態管理 ├── nuxt.config.js // 配置文件 ├── package.json ├── jsconfig.json ├── stylelint.config.js ├── README.md └── yarn.lock
項目啓動以後,咱們就能夠進行開發階段了。vue-router
在pages
建立頁面文件:npm
pages/ └── article/ ├── index.vue ├── _category/ │ └── index.vue └── detail/ └── _articleId.vue
Nuxt.js 預設了利用 Vue.js 開發服務端渲染的應用所須要的各類配置。因此不須要再安裝 vue-router
了,他會依據 pages
目錄結構自動生成 vue-router
模塊的路由配置。頁面之間使用路由,官方推薦使用 <nuxt-link>
標籤,與 <router-link>
的使用方式是同樣的。上面建立的目錄結構將會生成對應的路由配置表:
router: { routes: [ { name: 'article', path: '/article', component: 'pages/article/index.vue' }, { name: "article-category" path: "/article/:category", component: 'pages/article/_category/index.vue', }, { name: "article-detail-articleId" path: "/article/detail/:articleId", component: 'pages/article/detail/_articleId.vue' } ] }
組件這一塊劃分爲base
、framework
、page
三個目錄:
components/ ├── base 基本組件 ├── framework 佈局相關組件 └── page/ 各個頁面下的組件 ├── home └── ...
這裏須要注意在開發 VUE SPA
應用時咱們有時候會把頁面組件放在 pages
下,我將頁面下的組件所有放到了components
下,由於 Nuxt.js
框架會讀取 pages
目錄下全部的 .vue
文件並自動生成對應的路由配置。
官方介紹的很詳細,資源的存放有兩個目錄:static
、assets
static
: 用於存放應用的靜態文件,此類文件不會調用 Webpack
進行構建編譯處理。服務器啓動的時候,該目錄下的文件會映射至應用的根路徑 /
下。
舉個例子: /static/banner.png
映射至 /banner.png
assets
: 用於組織未編譯的靜態資源如 LESS
、SASS
或 JS
。
別名 | 目錄 |
---|---|
~ 或 @ | srcDir |
~~ 或 @@ | rootDir |
爲了方便引用,nuxt 提供了兩個別名,若是你須要引入 assets
或者 static
目錄, 使用 ~/assets/your_image.png
和 ~/static/your_image.png
方式。
這裏我選用 LESS
預處理語言,安裝:
$ yarn add less less-loader -D
在 assets/css/
建立 .less
文件, 經過一個文件引入:
// assets/css/index.less @import './normalize.less'; @import './reset.less'; @import './variables.less'; @import './common.less'
在 nuxt.config.js
中引入
export default { ... css: ['~/assets/css/index.less'], ... }
在使用預處理語言的時候,咱們確定會使用到變量,以方便統一管理顏色、字體大小等。
首先定義好變量文件 variables.less
/* ===== 主題色配置 ===== */ @colorPrimary: #6bc30d; @colorAssist: #2db7f5; @colorSuccess: #67c23a; @colorWarning: #e6a23c; @colorError: #f56c6c; @colorInfo: #909399;
安裝:
$ yarn add @nuxtjs/style-resources -D
在 nuxt.config.js
中增長配置:
export default { ... modules: [ // https://go.nuxtjs.dev/axios '@nuxtjs/axios', '@nuxtjs/style-resources', ], styleResources: { // your settings here // sass: [], // scss: [], // stylus: [], less: ['~/assets/css/variables.less'], }, ... }
個人博客大概分爲這幾種佈局方式:
在這裏我建立了三種佈局組件:
layouts/ ├── admin.vue // 上圖第四個 ├── default.vue // 上圖第一個和第三個只包含nav和footer └── user.vue //上圖第二個
admin.vue
: 後臺管理模塊的佈局user.vue
: 我的中心模塊的佈局default.vue
: 默認的佈局
拿 default.vue
舉例,我把 導航 和 頁腳 放到了一個組件 AppLayout
中:
<!-- layouts/default.vue --> <template> <app-layout> <nuxt /> </app-layout> </template> <script> import AppLayout from '@/components/framework/AppLayout/AppLayout' export default { name: 'AppLayoutDefault', components: { AppLayout } } </script>
而後在頁面中使用:
<!-- pages/index.vue --> <template> <!-- Your template --> </template> <script> export default { layout: 'default' // 指定佈局,不指定的話將會使用默認佈局: layouts/default.vue // 其實我這裏指不指定均可以哈哈。 } </script>
關於頁面上路由的跳轉,官方推薦使用 <nuxt-link>
,這裏 <nuxt-link>
和 <a>
仍是有區別的,nuxt-link
走的是 vue-router 的路由,即頁面爲單頁面,瀏覽器不會重定向。而 <a>
標籤走的是 window.location.href
,每次點擊a
標籤後頁面,都會進行一次服務端渲染。
在 plugins/
目錄下,新建 filters.js
,好比說咱們要對時間進行一個格式化處理 :
Day.js :一個輕量的處理時間和日期的 JavaScript 庫
$ yarn add dayjs
import Vue from 'vue' import dayjs from 'dayjs' // 時間格式化 export function dateFormatFilter(date, fmt) { if (!date) { return '' } else { return dayjs(date).format(fmt) } } const filters = { dateFormatFilter } Object.keys(filters).forEach((key) => { Vue.filter(key, filters[key]) }) export default filters
而後,在 nuxt.config.js
中配置,
export default { ... plugins: ['~/plugins/filters.js'] ... }
在 plugins/directive/focus
目錄下,添加 index.js
:
import Vue from 'vue'; const focus = Vue.directive('focus', { inserted(el) { el.focus(); }, }); export default focus;
自定義指令和全局過濾器同樣,都須要在 nuxt.config.js
添加配置:
export default { ... plugins: [ '~/plugins/filters.js', { src: '~/plugins/directive/focus/index.js', ssr: false }, ], ... }
經過使用 head
方法設置當前頁面的頭部標籤。
<template> <h1>{{ title }}</h1></template> <script> export default { ... head() { return { title: '明麼的博客', meta: [ { hid: 'description', name: 'description', content: 'My custom description' } ] } } } </script>
注意:爲了不子組件中的 meta 標籤不能正確覆蓋父組件中相同的標籤而產生重複的現象,建議利用 hid 鍵爲 meta 標籤配一個惟一的標識編號。
若是頁面比較多的話,每一個頁面都須要寫 head 對象,就會有些的繁瑣。能夠藉助 nuxt
的 plugin
機制,將其封裝成一個函數,並注入到每個頁面當中:
// plugins/head.js import Vue from 'vue' Vue.mixin({ methods: { $seo(title, content) { return { title, meta: [{ hid: 'description', name: 'description', content }] } } } })
在 nuxt.config.js
中增長配置:
export default { ... plugins: [ '~/plugins/filters.js', { src: '~/plugins/directive/focus/index.js', ssr: false }, '~/plugins/head.js' ], ... }
在頁面中使用:
head() { return this.$seo(this.detail.title, this.detail.summary) }
請求數據,在初始化項目的時候已經選擇了Axios,就不須要再另行安裝了,能夠查看 nuxt.config.js
中已經配置好了:
export default { ... modules: [ // https://go.nuxtjs.dev/axios '@nuxtjs/axios', ... ], ... }
頁面中經過 this.$axios.$get
來獲取數據,不須要在每一個頁面都單獨引入 axios
.
可是通常來講咱們會對 axios
作一下封裝,集中處理一些數據或者是錯誤信息。
在 plugins
目錄下新建 axios.js
和 api-repositories.js
,下面是個人一些簡單的配置:
// plugins/axios.js import qs from 'qs' export default function(ctx) { const { $axios, store, app } = ctx // $axios.defaults.timeout = 0; $axios.transformRequest = [ (data, header) => { if (header['Content-Type'] && header['Content-Type'].includes('json')) { return JSON.stringify(data) } return qs.stringify(data, { arrayFormat: 'repeat' }) } ] $axios.onRequest((config) => { const token = store.getters.token if (token) { config.headers.Authorization = `Bearer ${token}` } // 若是是 get 請求,參數序列化 if (config.method === 'get') { config.paramsSerializer = function(params) { return qs.stringify(params, { arrayFormat: 'repeat' }) // params是數組類型如arr=[1,2],則轉換成arr=1&arr=2 } } return config }) $axios.onRequestError((error) => { console.log('onRequestError', error) }) $axios.onResponse((res) => { // ["data", "status", "statusText", "headers", "config", "request"] // 若是 後端返回的碼正常 則 將 res.data 返回 if (res && res.data) { if (res.headers['content-type'] === 'text/html') { return res } if (res.data.code === 'success') { return res } else { return Promise.reject(res.data) } } }) $axios.onResponseError((error) => { console.log('onResponseError', error) }) $axios.onError((error) => { console.log('onError', error) if (error && error.message.indexOf('401') > 1) { app.$toast.error('登陸過時了,請從新登陸!') sessionStorage.clear() store.dispatch('changeUserInfo', null) store.dispatch('changeToken', '') } else { app.$toast.show(error.message) } }) }
// plugins/api-repositories.js export default ({ $axios }, inject) => { const repositories = { GetCategory: (params, options) => $axios.get('/categories', params, options), PostCategory: (params, options) => $axios.post('/categories', params, options), PutCategory: (params, options) => $axios.put(`/categories/${params.categoryId}`, params, options), DeleteCategory: (params, options) => $axios.delete(`/categories/${params.categoryId}`, params, options) ... } inject('myApi', repositories) }
而後在 nuxt.config.js
中增長配置:
export default { ... plugins: [ ... { src: '~/plugins/axios.js', ssr: true }, { src: '~/plugins/api-repositories.js', ssr: true }, ], /* ** Axios module configuration ** See https://axios.nuxtjs.org/options */ axios: { baseURL: 'http://localhost:5000/' }, }
這樣就能夠直接在頁面中使用了:
this.$myApi.GetCategory()
使用 proxy 解決跨域問題:
$ yarn add @nuxtjs/proxy
在 nuxt.config.js
中增長配置,下面是個人配置:
export default { ... modules: [ ... '@nuxtjs/proxy', ... ], axios: { proxy: true, headers: { 'Access-Control-Allow-Origin': '*', 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json; charset=UTF-8' }, prefix: '/api', credentials: true }, /* ** 配置代理 */ proxy: { '/api': { target: process.env.NODE_ENV === 'development' ? 'http://localhost:5000/' : 'http://localhost:5000/', changeOrigin: true, pathRewrite: { '^/api': '' } }, '/douban/': { target: 'http://api.douban.com/v2', changeOrigin: true, pathRewrite: { '^/douban': '' } }, ... }, }
在單頁面開發中,打包發佈上線還須要 nginx
代理才能實現跨域,在 nuxt
中,打包發佈上線以後,請求是在服務端發起的,不存在跨域問題,因此不須要在另外再作 nginx
代理。
該方法是 Nuxt
一大賣點, asyncData
方法會在組件(限於頁面組件)每次加載以前被調用。它能夠在服務端或路由更新以前被調用,服務端渲染的能力就在這裏。
注意:因爲asyncData
方法是在組件 初始化 前被調用的,因此在方法內是沒有辦法經過this
來引用組件的實例對象。
另外說起一點,當 asyncData
在服務端執行時,是沒有 document
和 window
對象的。
asyncData
第一個參數被設定爲當前頁面的上下文對象,能夠利用 asyncData
方法來獲取數據,Nuxt.js
會將 asyncData
返回的數據融合組件 data
方法返回的數據一併返回給當前組件。
export default { asyncData (ctx) { ctx.app // 根實例 ctx.route // 路由實例 ctx.params //路由參數 ctx.query // 路由問號後面的參數 ctx.error // 錯誤處理方法 } }
服務端渲染:
export default { data () { return { categoryList: [] }; }, async asyncData({ app }) { const res = await app.$myApi.GetCategory(); return { categoryList: res.result.list }; }, }
在使用 asyncData
時可能因爲服務器錯誤或api錯誤致使頁面沒法渲染,針對這種狀況的出現,咱們還須要作一下處理。nuxt
提供了 context.error
方法用於錯誤處理,在 asyncData
中調用該方法便可跳轉到錯誤頁面。
export default { async asyncData({ app, error}) { app.$myApi.GetCategory() .then(res => { return { categoryList: res.result.list } }) .catch(e => { error({ statusCode: 500, message: '服務器出錯了啦~' }) }) }, }
當出現異常時會跳轉到默認的錯誤頁,錯誤頁面能夠經過 /layout/error.vue
自定義。
context.error
的參數必須是相似{ statusCode: 500, message: '服務器開了個小差~' }
,statusCode
必須是http
狀態碼
爲了方便,全局統一處理錯誤方法,在 plugins
目錄下建立 ctx-inject.js
:
// plugins/ctx-inject.js export default (ctx, inject) => { ctx.$errorHandler = (err) => { try { const res = err.data if (res) { // 因爲nuxt的錯誤頁面只能識別http的狀態碼,所以statusCode統一傳500,表示服務器異常。 ctx.error({ statusCode: 500, message: res.resultInfo }) } else { ctx.error({ statusCode: 500, message: '服務器出錯了啦~' }) } } catch { ctx.error({ statusCode: 500, message: '服務器出錯了啦~' }) } } }
而後,在 nuxt.config.js
中增長配置:
export default { ... plugins: [ ... '~/plugins/ctx-inject.js', ... ], ... }
在頁面中使用:
export default { data() { return { categoryList: [] } }, async asyncData(ctx) { const { app } = ctx // 儘可能使用try catch的寫法,將全部異常都捕捉到 try { const res = await app.$myApi.GetCategory() return { categoryList: res.result.list, } } catch (err) { ctx.$errorHandler(err) } }, }
fetch
方法用於在渲染頁面前填充應用的狀態樹(store)數據, 與 asyncData
方法相似,不一樣的是它不會設置組件的數據。它會在組件每次加載前被調用(在服務端或切換至目標路由以前)。和 asyncData
同樣,第一個參數也是頁面的上下文對象,一樣沒法在內部使用 this
來獲取組件實例。
<template> ... </template> <script> export default { async fetch({ app, store, params }) { let res = await app.$myApi.GetCategory() store.commit('setCategory', res.result.list) } } </script>
在 nuxt
中使用狀態管理,只須要在 store/
目錄下建立文件便可。
store/ ├── actions.js ├── getters.js ├── index.js ├── mutations.js └── state.js
// store/actions.js const actions = { changeToken({ commit }, token) { commit('setToken', token) }, ... } export default actions // store/getters.js export const token = (state) => state.token export const userInfo = (state) => state.userInfo ... // store/mutations.js const mutations = { setToken(state, token) { state.token = token }, ... } export default mutations // store/state.js const state = () => ({ token: '', userInfo: null, ... }) export default state // store/index.js import state from './state' import * as getters from './getters' import actions from './actions' import mutations from './mutations' export default { state, getters, actions, mutations }
不管使用那種模式,您的state
的值應該始終是function
,爲了不返回引用類型,會致使多個實例相互影響。
開發完畢後,就能夠進行打包部署了,通常來講先在本地測試一下:
$ yarn build $ yarn start
而後,雲服務器安裝 node
環境 和 pm2
。
增長pm2
配置,在 server/
目錄下,新建 pm2.config.json
文件:
{ "apps": [ { "name": "my-blog", "script": "./server/index.js", "instances": 0, "watch": false, "exec_mode": "cluster_mode" } ] }
而後,在 package.json
中 scripts
配置命令:
{ "scripts": { ... "pm2": "cross-env NODE_ENV=production pm2 start ./server/pm2.config.json", } }
把咱們項目中 .nuxt
, static
, package.json
, nuxt.config.js
, yarn.lock
或者是 package.lock
上傳到服務器。進入上傳的服務器目錄,安裝依賴:
$ yarn install
而後,運行:
$ npm run pm2
在設置服務器開放 3000 端口後,就能夠經過端口訪問了。後面加個端口號總歸是不合適,還須要使用 nginx
代理到默認端口 80(http) 或 433(https)。
記錄一個小問題:3000 端口沒問題,項目啓動也正常,經過http://60.***.***.110:3000
就是訪問不了。在nuxt.config.js
增長:
{ ... server: { port: 3000, host: '0.0.0.0' } }
從新啓動項目便可。