做爲一位 Vuer(vue開發者),若是還不會這個框架,那麼你的 Vue
技術棧還沒被點亮。css
Nuxt.js 官方介紹:html
Nuxt.js 是一個基於 Vue.js 的通用應用框架。
經過對客戶端/服務端基礎架構的抽象組織,Nuxt.js 主要關注的是應用的 UI渲染。
咱們的目標是建立一個靈活的應用框架,你能夠基於它初始化新項目的基礎結構代碼,或者在已有 Node.js 項目中使用 Nuxt.js。
Nuxt.js 預設了利用 Vue.js 開發服務端渲染的應用所須要的各類配置。前端
若是你熟悉 Vue.js
的使用,那你很快就能夠上手 Nuxt.js
。開發體驗也和 Vue.js
沒太大區別,至關於爲 Vue.js
擴展了一些配置。固然你對 Node.js
有基礎,那就再好不過了。vue
如今 Vue.js
大多數用於單頁面應用,隨着技術的發展,單頁面應用已不足以知足需求。而且一些缺點也成爲單頁面應用的通病,單頁面應用在訪問時會將全部的文件進行加載,首屏訪問須要等待一段時間,也就是常說的白屏,另一點是總所周知的 SEO
優化問題。node
Nuxt.js
的出現正好來解決這些問題,若是你的網站是偏向社區須要搜索引擎提供流量的項目,那就再合適不過了。android
我在空閒的時間也用 Nuxt.js
仿掘金 web
網站:webpack
nuxt-juejin-project
是一個使用 Nuxt.js
仿寫掘金的學習項目,主要使用 :nuxt
+ koa
+ vuex
+ axios
+ element-ui
。該項目全部數據與掘金同步,由於接口都是經過 koa
做爲中間層轉發。主要頁面數據經過服務端渲染完成。ios
在項目完成後的幾天,我將記錄的筆記整理一下,並加入一些經常使用的技術點,最後有了這篇文章,但願可以幫到正在學習的小夥伴。git
項目介紹裏有部分截圖,若是jio得能夠,請來個 star😜~github
項目地址:https://github.com/ChanWahFung/nuxt-juejin-project
項目的搭建參照官網指引,跑個項目相信難不到大家,這裏不贅述了。
🏃♀️跑起來 https://www.nuxtjs.cn/guide/installation
關於項目的配置,我選擇的是:
context 是從 Nuxt 額外提供的對象,在"asyncData"、"plugins"、"middlewares"、"modules" 和 "store/nuxtServerInit" 等特殊的 Nuxt 生命週期區域中都會使用到 context。
因此,想要使用 Nuxt.js
,咱們必需要熟知該對象的有哪些可用屬性。
context
官方文檔描述戳這裏 https://www.nuxtjs.cn/api/context
下面我列舉幾個在實際應用中比較重要且經常使用的屬性:
app
是 context
中最重要的屬性,就像咱們 Vue
中的 this
,全局方法和屬性都會掛載到它裏面。由於服務端渲染的特殊性,不少Nuxt
提供的生命週期都是運行在服務端,也就是說它們會先於 Vue
實例的建立。所以在這些生命週期中,咱們沒法經過 this
去獲取實例上的方法和屬性。使用 app
能夠來彌補這點,通常咱們會把全局的方法同時注入到 this
和 app
中,在服務端的生命週期中使用 app
去訪問該方法,而在客戶端中使用 this
,保證方法的共用。
舉個例子:
假設 $axios
已被同時注入,通常主要數據經過 asyncData
(該生命週期發起請求,將獲取到的數據交給服務端拼接成html返回) 去提早請求作服務端渲染,而次要數據經過客戶端的 mounted
去請求。
export default { async asyncData({ app }) { // 列表數據 let list = await app.$axios.getIndexList({ pageNum: 1, pageSize: 20 }).then(res => res.s === 1 ? res.d : []) return { list } }, data() { return { list: [], categories: [] } }, async mounted() { // 分類 let res = await this.$axios.getCategories() if (res.s === 1) { this.categories = res.d } } }
store
是 Vuex.Store
實例,在運行時 Nuxt.js
會嘗試找到是應用根目錄下的 store
目錄,若是該目錄存在,它會將模塊文件加到構建配置中。
因此咱們只須要在根目錄的 store
建立模塊js文件,便可使用。
/store/index.js
:
export const state = () => ({ list: [] }) export const mutations = { updateList(state, payload){ state.list = payload } }
並且 Nuxt.js
會很貼心的幫咱們將 store
同時注入,最後咱們能夠在組件這樣使用::
export default { async asyncData({ app, store }) { let list = await app.$axios.getIndexList({ pageNum: 1, pageSize: 20 }).then(res => res.s === 1 ? res.d : []) // 服務端使用 store.commit('updateList', list) return { list } }, methods: { updateList(list) { // 客戶端使用,固然你也可使用輔助函數 mapMutations 來完成 this.$store.commit('updateList', list) } } }
爲了明白 store
注入的過程,我翻閱 .nuxt/index.js
源碼(.nuxt
目錄是 Nuxt.js
在構建運行時自動生成的),大概知道了流程。首先在 .nuxt/store.js
中,對 store
模塊文件作出一系列的處理,並暴露 createStore
方法。而後在 .nuxt/index.js
中,createApp
方法會對其同時注入:
import { createStore } from './store.js' async function createApp (ssrContext) { const store = createStore(ssrContext) // ... // here we inject the router and store to all child components, // making them available everywhere as `this.$router` and `this.$store`. // 注入到this const app = { store // ... } // ... // Set context to app.context // 注入到context await setContext(app, { store // ... }) // ... return { store, app, router } }
除此以外,我還發現 Nuxt.js
會經過 inject
方法爲其掛載上 plugin
(plugin
是掛載全局方法的主要途徑,後面會講到,不知道能夠先忽略),也就是說在 store
裏,咱們能夠經過 this
訪問到全局方法:
export const mutations = { updateList(state, payload){ console.log(this.$axios) state.list = payload } }
params
和 query
分別是 route.params
和 route.query
的別名。它們都帶有路由參數的對象,使用方法也很簡單。這個沒什麼好說的,用就完事了。
export default { async asyncData({ app, params }) { let list = await app.$axios.getIndexList({ id: params.id, pageNum: 1, pageSize: 20 }).then(res => res.s === 1 ? res.d : []) return { list } } }
該方法重定向用戶請求到另外一個路由,一般會用在權限驗證。用法:redirect(params)
,params
參數包含 status
(狀態碼,默認爲302)、path
(路由路徑)、query
(參數),其中 status
和 query
是可選的。固然若是你只是單純的重定向路由,能夠傳入路徑字符串,就像 redirect('/index')
。
舉個例子:
假設咱們如今有個路由中間件,用於驗證登陸身份,邏輯是身份沒過時則不作任何事情,若身份過時重定向到登陸頁。
export default function ({ redirect }) { // ... if (!token) { redirect({ path: '/login', query: { isExpires: 1 } }) } }
該方法跳轉到錯誤頁。用法:error(params)
,params
參數應該包含 statusCode
和 message
字段。在實際場景中,總有一些不按常理的操做,頁面所以沒法展現真正想要的效果,使用該方法進行錯誤提示仍是有必要的。
舉個例子:
標籤詳情頁面請求數據依賴於 query.name
,當 query.name
不存在時,請求沒法返回可用的數據,此時跳轉到錯誤頁
export default { async asyncData({ app, query, error }) { const tagInfo = await app.$api.getTagDetail({ tagName: encodeURIComponent(query.name) }).then(res => { if (res.s === 1) { return res.d } else { error({ statusCode: 404, message: '標籤不存在' }) return } }) return { tagInfo } } }
你可能想要在服務器端獲取並渲染數據。Nuxt.js添加了asyncData方法使得你可以在渲染組件以前異步獲取數據。
asyncData
是最經常使用最重要的生命週期,同時也是服務端渲染的關鍵點。該生命週期只限於頁面組件調用,第一個參數爲 context
。它調用的時機在組件初始化以前,運做在服務端環境。因此在 asyncData
生命週期中,咱們沒法經過 this
引用當前的 Vue
實例,也沒有 window
對象和 document
對象,這些是咱們須要注意的。
通常在 asyncData
會對主要頁面數據進行預先請求,獲取到的數據會交由服務端拼接成 html
返回前端渲染,以此提升首屏加載速度和進行 seo
優化。
看下圖,在谷歌調試工具中,看不到主要數據接口發起請求,只有返回的 html
文檔,證實數據在服務端被渲染。
最後,須要將接口獲取到的數據返回:
export default { async asyncData({ app }) { let list = await app.$axios.getIndexList({ pageNum: 1, pageSize: 20 }).then(res => res.s === 1 ? res.d : []) // 返回數據 return { list } }, data() { return { list: [] } } }
值得一提的是,asyncData
只在首屏被執行,其它時候至關於 created
或 mounted
在客戶端渲染頁面。
什麼意思呢?舉個例子:
如今有兩個頁面,分別是首頁和詳情頁,它們都有設置 asyncData
。進入首頁時,asyncData
運行在服務端。渲染完成後,點擊文章進入詳情頁,此時詳情頁的 asyncData
並不會運行在服務端,而是在客戶端發起請求獲取數據渲染,由於詳情頁已經不是首屏。當咱們刷新詳情頁,這時候詳情頁的 asyncData
纔會運行在服務端。因此,不要走進這個誤區(誒,不是說服務端渲染嗎,怎麼還會發起請求?)。
fetch 方法用於在渲染頁面前填充應用的狀態樹(store)數據, 與 asyncData 方法相似,不一樣的是它不會設置組件的數據。
查看官方的說明,能夠得知該生命週期用於填充 Vuex
狀態樹,與 asyncData
一樣,它在組件初始化前調用,第一個參數爲 context
。
爲了讓獲取過程能夠異步,你須要返回一個 Promise
,Nuxt.js
會等這個 promise
完成後再渲染組件。
export default { fetch ({ store, params }) { return axios.get('http://my-api/stars') .then((res) => { store.commit('setStars', res.data) }) } }
你也可使用 async 或 await 的模式簡化代碼以下:
export default { async fetch ({ store, params }) { let { data } = await axios.get('http://my-api/stars') store.commit('setStars', data) } }
但這並非說咱們只能在 fetch
中填充狀態樹,在 asyncData
中一樣能夠。
Nuxt.js 可讓你在動態路由對應的頁面組件中配置一個校驗方法用於校驗動態路由參數的有效性。
在驗證路由參數合法性時,它可以幫助咱們,第一個參數爲 context
。與上面有點不一樣的是,咱們可以訪問實例上的方法 this.methods.xxx
。
打印 this
以下:
生命週期能夠返回一個 Boolean
,爲真則進入路由,爲假則中止渲染當前頁面並顯示錯誤頁面:
export default { validate({ params, query }) { return this.methods.validateParam(params.type) }, methods: { validateParam(type){ let typeWhiteList = ['backend', 'frontend', 'android'] return typeWhiteList.includes(type) } } }
或者返回一個Promise:
export default { validate({ params, query, store }) { return new Promise((resolve) => setTimeout(() => resolve())) } }
還能夠在驗證函數執行期間拋出預期或意外錯誤:
export default { async validate ({ params, store }) { // 使用自定義消息觸發內部服務器500錯誤 throw new Error('Under Construction!') } }
監聽參數字符串更改並在更改時執行組件方法 (asyncData, fetch, validate, layout, ...)
watchQuery
可設置 Boolean
或 Array
(默認: [])。使用 watchQuery
屬性能夠監聽參數字符串的更改。 若是定義的字符串發生變化,將調用全部組件方法(asyncData
, fetch
, validate
, layout
, ...)。 爲了提升性能,默認狀況下禁用。
在 nuxt-juejin-project
項目的搜索頁中,我也用到了這個配置:
<template> <div class="search-container"> <div class="list__header"> <ul class="list__types"> <li v-for="item in types" :key="item.title" @click="search({type: item.type})">{{ item.title }}</li> </ul> <ul class="list__periods"> <li v-for="item in periods" :key="item.title" @click="search({period: item.period})">{{ item.title }}</li> </ul> </div> </div> </template>
export default { async asyncData({ app, query }) { let res = await app.$api.searchList({ after: 0, first: 20, type: query.type ? query.type.toUpperCase() : 'ALL', keyword: query.keyword, period: query.period ? query.period.toUpperCase() : 'ALL' }).then(res => res.s == 1 ? res.d : {}) return { pageInfo: res.pageInfo || {}, searchList: res.edges || [] } }, watchQuery: ['keyword', 'type', 'period'], methods: { search(item) { // 更新路由參數,觸發 watchQuery,執行 asyncData 從新獲取數據 this.$router.push({ name: 'search', query: { type: item.type || this.type, keyword: this.keyword, period: item.period || this.period } }) } } }
使用 watchQuery
有點好處就是,當咱們使用瀏覽器後退按鈕或前進按鈕時,頁面數據會刷新,由於參數字符串發生了變化。
Nuxt.js 使用了 vue-meta 更新應用的 頭部標籤(Head) 和 html 屬性。
使用 head
方法設置當前頁面的頭部標籤,該方法裏能經過 this
獲取組件的數據。除了好看之外,正確的設置 meta
標籤,還能有利於頁面被搜索引擎查找,進行 seo
優化。通常都會設置 description
(簡介) 和 keyword
(關鍵詞)。
title:
meta:
export default { head () { return { title: this.articInfo.title, meta: [ { hid: 'description', name: 'description', content: this.articInfo.content } ] } } }
爲了不子組件中的 meta
標籤不能正確覆蓋父組件中相同的標籤而產生重複的現象,建議利用 hid
鍵爲 meta
標籤配一個惟一的標識編號。
在 nuxt.config.js
中,咱們還能夠設置全局的 head
:
module.exports = { head: { title: '掘金', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width,initial-scale=1,user-scalable=no,viewport-fit=cover' }, { name: 'referrer', content: 'never'}, { hid: 'keywords', name: 'keywords', content: '掘金,稀土,Vue.js,微信小程序,Kotlin,RxJava,React Native,Wireshark,敏捷開發,Bootstrap,OKHttp,正則表達式,WebGL,Webpack,Docker,MVVM'}, { hid: 'description', name: 'description', content: '掘金是一個幫助開發者成長的社區,是給開發者用的 Hacker News,給設計師用的 Designer News,和給產品經理用的 Medium。掘金的技術文章由稀土上彙集的技術大牛和極客共同編輯爲你篩選出最優質的乾貨,其中包括:Android、iOS、前端、後端等方面的內容。用戶天天均可以在這裏找到技術世界的頭條內容。與此同時,掘金內還有沸點、掘金翻譯計劃、線下活動、專欄文章等內容。即便你是 GitHub、StackOverflow、開源中國的用戶,咱們相信你也能夠在這裏有所收穫。'} ], } }
下面是這些生命週期的調用順序,某些時候可能會對咱們有幫助。
validate => asyncData => fetch => head
如下二者均可以配置啓動端口,但我我的更喜歡第一種在 nuxt.config.js
配置,這比較符合正常的邏輯。
nuxt.config.js
:
module.exports = { server: { port: 8000, host: '127.0.0.1' } }
package.json
:
"config": { "nuxt": { "port": "8000", "host": "127.0.0.1" } },
nuxt.config.js
:
module.exports = { head: { link: [ { rel: 'stylesheet', href: '//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/styles/atom-one-light.min.css' }, ], script: [ { src: '//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/highlight.min.js' } ] } }
組件內可在 head
配置,head
能夠接受 object
或 function
。官方例子使用的是 object
類型,使用 function
類型一樣生效。
export default { head () { return { link: [ { rel: 'stylesheet', href: '//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/styles/atom-one-light.min.css' }, ], script: [ { src: '//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/highlight.min.js' } ] } } }
nuxt.config.js
提供env
選項進行配置環境變量。但此前我嘗試過根目錄建立 .env 文件管理環境變量,發現是無效的。
nuxt.config.js
:
module.exports = { env: { baseUrl: process.env.NODE_ENV === 'production' ? 'http://test.com' : 'http://127.0.0.1:8000' }, }
以上配置咱們建立了一個 baseUrl
環境變量,經過 process.env.NODE_ENV
判斷環境來匹配對應的地址
咱們能夠經過如下兩種方式來使用 baseUrl
變量:
process.env.baseUrl
context.env.baseUrl
舉個例子, 咱們能夠利用它來配置 axios
的自定義實例。
/plugins/axios.js
:
export default function (context) { $axios.defaults.baseURL = process.env.baseUrl // 或者 $axios.defaults.baseURL = context.env.baseUrl $axios.defaults.timeout = 30000 $axios.interceptors.request.use(config => { return config }) $axios.interceptors.response.use(response => { return response.data }) }
plugins
做爲全局注入的主要途徑,關於一些使用的方式是必需要掌握的。有時你但願在整個應用程序中使用某個函數或屬性值,此時,你須要將它們注入到 Vue
實例(客戶端), context
(服務器端)甚至 store(Vuex)
。
plugin
通常向外暴露一個函數,該函數接收兩個參數分別是 context
和 inject
context: 上下文對象,該對象存儲不少有用的屬性。好比經常使用的 app
屬性,包含全部插件的 Vue
根實例。例如:在使用 axios
的時候,你想獲取 $axios
能夠直接經過 context.app.$axios
來獲取。
inject: 該方法能夠將 plugin
同時注入到 context
, Vue
實例, Vuex
中。
例如:
export default function (context, inject) {}
plugins/vue-inject.js
:
import Vue from 'vue' Vue.prototype.$myInjectedFunction = string => console.log('This is an example', string)
nuxt.config.js
:
export default { plugins: ['~/plugins/vue-inject.js'] }
這樣在全部 Vue
組件中均可以使用該函數
export default { mounted() { this.$myInjectedFunction('test') } }
context
注入方式和在其它 vue
應用程序中注入相似。
plugins/ctx-inject.js
:
export default ({ app }) => { app.myInjectedFunction = string => console.log('Okay, another function', string) }
nuxt.config.js
:
export default { plugins: ['~/plugins/ctx-inject.js'] }
如今,只要你得到 context
,你就可使用該函數(例如在 asyncData
和 fetch
中)
export default { asyncData(context) { context.app.myInjectedFunction('ctx!') } }
若是須要同時在 context
, Vue
實例,甚至 Vuex
中同時注入,可使用 inject
方法,它是 plugin
導出函數的第二個參數。系統會默認將 $
做爲方法名的前綴。
plugins/combined-inject.js
:
export default ({ app }, inject) => { inject('myInjectedFunction', string => console.log('That was easy!', string)) }
nuxt.config.js
:
export default { plugins: ['~/plugins/combined-inject.js'] }
如今你就能夠在 context
,或者 Vue
實例中的 this
,或者 Vuex
的 actions
/ mutations
方法中的 this
來調用 myInjectedFunction
方法
export default { mounted() { this.$myInjectedFunction('works in mounted') }, asyncData(context) { context.app.$myInjectedFunction('works with context') } }
store/index.js
:
export const state = () => ({ someValue: '' }) export const mutations = { changeSomeValue(state, newValue) { this.$myInjectedFunction('accessible in mutations') state.someValue = newValue } } export const actions = { setSomeValueToWhatever({ commit }) { this.$myInjectedFunction('accessible in actions') const newValue = 'whatever' commit('changeSomeValue', newValue) } }
當 plugin
依賴於其餘的 plugin
調用時,咱們能夠訪問 context
來獲取,前提是 plugin
須要使用 context
注入。
舉個例子:如今已存在 request
請求的 plugin
,有另外一個 plugin
須要調用 request
plugins/request.js
:
export default ({ app: { $axios } }, inject) => { inject('request', { get (url, params) { return $axios({ method: 'get', url, params }) } }) }
plugins/api.js
:
export default ({ app: { $request } }, inject) => { inject('api', { getIndexList(params) { return $request.get('/list/indexList', params) } }) }
值得一提的是,在注入 plugin
時要注意順序,就上面的例子來看, request
的注入順序要在 api
以前
module.exports = { plugins: [ './plugins/axios.js', './plugins/request.js', './plugins/api.js', ] }
在Nuxt.js
中,路由是基於文件結構自動生成,無需配置。自動生成的路由配置可在 .nuxt/router.js
中查看。
在 Vue
中是這樣配置動態路由的
const router = new VueRouter({ routes: [ { path: '/users/:id', name: 'user', component: User } ] })
Nuxt.js
中須要建立對應的如下劃線做爲前綴的 Vue
文件 或 目錄
如下面目錄爲例:
pages/ --| users/ -----| _id.vue --| index.vue
自動生成的路由配置以下:
router:{ routes: [ { name: 'index', path: '/', component: 'pages/index.vue' }, { name: 'users-id', path: '/users/:id?', component: 'pages/users/_id.vue' } ] }
如下面目錄爲例, 咱們須要一級頁面的 vue
文件,以及和該文件同名的文件夾(用於存放子頁面)
pages/ --| users/ -----| _id.vue -----| index.vue --| users.vue
自動生成的路由配置以下:
router: { routes: [ { path: '/users', component: 'pages/users.vue', children: [ { path: '', component: 'pages/users/index.vue', name: 'users' }, { path: ':id', component: 'pages/users/_id.vue', name: 'users-id' } ] } ] }
而後在一級頁面中使用 nuxt-child
來顯示子頁面,就像使用 router-view
同樣
<template> <div> <nuxt-child></nuxt-child> </div> </template>
除了基於文件結構生成路由外,還能夠經過修改 nuxt.config.js
文件的 router
選項來自定義,這些配置會被添加到 Nuxt.js
的路由配置中。
下面例子是對路由添加劇定向的配置:
module.exports = { router: { extendRoutes (routes, resolve) { routes.push({ path: '/', redirect: { name: 'timeline-title' } }) } } }
Nuxt
已爲咱們集成好 @nuxtjs/axios
,若是你在建立項目時選擇了 axios
,這步能夠忽略。
npm i @nuxtjs/axios --save
nuxt.config.js
:
module.exports = { modules: [ '@nuxtjs/axios' ], }
服務器端獲取並渲染數據, asyncData
方法能夠在渲染組件以前異步獲取數據,並把獲取的數據返回給當前組件。
export default { async asyncData(context) { let data = await context.app.$axios.get("/test") return { list: data }; }, data() { return { list: [] } } }
這種使用方式就和咱們日常同樣,訪問 this
進行調用
export default { data() { return { list: [] } }, async created() { let data = await this.$axios.get("/test") this.list = data }, }
大多時候,咱們都須要對 axios
作自定義配置(baseUrl、攔截器),這時能夠經過配置 plugins
來引入。
/plugins/axios.js
:
export default function({ app: { $axios } }) { $axios.defaults.baseURL = 'http://127.0.0.1:8000/' $axios.interceptors.request.use(config => { return config }) $axios.interceptors.response.use(response => { if (/^[4|5]/.test(response.status)) { return Promise.reject(response.statusText) } return response.data }) }
nuxt.config.js
:
module.exports = { plugins: [ './plugins/axios.js' ], }
完成後,使用方式也和上面的同樣。
以 scss
爲例子
npm i node-sass sass-loader scss-loader --save--dev
無需配置,模板內直接使用
<style lang="scss" scoped> .box{ color: $theme; } </style>
在編寫佈局樣式時,會有不少相同共用的樣式,此時咱們能夠將這些樣式提取出來,須要用到時只須要添加一個類名便可。
global.scss
:
.shadow{ box-shadow: 0 1px 2px 0 rgba(0,0,0,.05); } .ellipsis{ text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .main{ width: 960px; margin: 0 auto; margin-top: 20px; }
nuxt.config.js
:
module.exports = { css: [ '~/assets/scss/global.scss' ], }
爲頁面注入 變量 和 mixin
並且不用每次都去導入他們,可使用 @nuxtjs/style-resources
來實現。
npm i @nuxtjs/style-resources --save--dev
/assets/scss/variable.scss
:
$theme: #007fff; $success: #6cbd45; $success-2: #74ca46;
nuxt.config.js
:
module.exports = { modules: [ '@nuxtjs/style-resources' ], styleResources: { scss: [ './assets/scss/variable.scss' ] }, }
/assets/scss/element-variables.scss
:
/* 改變主題色變量 */ /* $theme 在上面的 scss 文件中定義並使用 */ $--color-primary: $theme; /* 改變 icon 字體路徑變量,必需 */ $--font-path: '~element-ui/lib/theme-chalk/fonts'; /* 組件樣式按需引入 */ @import "~element-ui/packages/theme-chalk/src/select"; @import "~element-ui/packages/theme-chalk/src/option"; @import "~element-ui/packages/theme-chalk/src/input"; @import "~element-ui/packages/theme-chalk/src/button"; @import "~element-ui/packages/theme-chalk/src/notification"; @import "~element-ui/packages/theme-chalk/src/message";
nuxt.config.js
:
module.exports = { modules: [ '@nuxtjs/style-resources' ], styleResources: { scss: [ /* * 這裏須要注意使用的順序,由於 element-variables.scss 裏用到 variable.scss 裏定義的變量 * 若是順序反過來,在啓動編譯時會致使變量找不到報錯 */ '~/assets/scss/variable.scss', '~/assets/scss/element-variables.scss' ] }, }
還有另外一個方法可使用,那就是 plugin
import Vue from 'vue' import myComponentsInstall from '~/components/myComponentsInstall' import eleComponentsInstall from '~/components/eleComponentsInstall' import '~/assets/scss/element-variables.scss' // elementUI 自定義主題色 Vue.use(myComponentsInstall) Vue.use(eleComponentsInstall)
看到這裏你應該能感受到 asyncData
的重要性,對於這種常常會使用到的生命週期,一些細節上的修改就顯得尤其重要。一般, asyncData
中不僅發起一個請求,多是不少個:
export default { async asyncData({ app }) { // 文章列表 let indexData = await app.$api.getIndexList({ first: 20, order: 'POPULAR', category: 1 }).then(res => res.s == 1 ? res.d : {}) // 推薦做者 let recommendAuthors = await app.$api.getRecommendAuthor({ limit: 5 }).then(res => res.s == 1 ? res.d : []) // 推薦小冊 let recommendBooks = await app.$api.getRecommendBook().then(res => res.s === 1 ? res.d.data : []) return { indexData, recommendAuthors, recommendBooks } } }
上面的操做看起來沒什麼問題,但其實有個細節能夠優化一下。如今來盤一盤,咱們都知道 async/await
會將異步任務去同步化執行,上一個異步任務沒結束以前,下一個異步任務處於等待狀態中。這樣須要等待3個異步任務,假設這些請求均耗時1秒,也就是說頁面至少要等待3秒後纔會出現內容。本來咱們想利用服務端渲染來優化首屏,如今卻由於等待請求而拖慢頁面渲染,豈不是得不償失。
最好的方案應該是多個請求同時發送,可能聰明的小夥伴已經想到 Promise.all
。沒錯,利用 Promise.all
將這些請求並行發送,就能解決上述的問題。Promise.all
接受一個 Promise
數組做爲參數,當所有 Promise
成功時會返回一個結果數組。最終的耗時會以最久的 Promise
爲準,因此說本來3秒的耗時能夠下降到1秒。須要注意的是,若是其中有一個請求失敗了,會返回最早被 reject
失敗狀態的值,致使獲取不到數據。在項目封裝基礎請求時我已經作了 catch
錯誤的處理,因此確保請求都不會被 reject
。
export default { asyncData() { // 數組解構得到對應請求的數據 let [indexData, recommendAuthors, recommendBooks] = await Promise.all([ // 文章列表 app.$api.getIndexList({ first: 20, order: 'POPULAR', category: 1 }).then(res => res.s == 1 ? res.d : {}), // 推薦做者 app.$api.getRecommendAuthor({ limit: 5 }).then(res => res.s == 1 ? res.d : []), // 推薦小冊 app.$api.getRecommendBook().then(res => res.s === 1 ? res.d.data : []), ]) return { indexData, recommendAuthors, recommendBooks } } }
一個應用必不可少的功能就是 token
驗證,一般咱們在登陸後把返回的驗證信息存儲起來,以後請求帶上 token
供後端驗證狀態。在先後端分離的項目中,通常都會存放到本地存儲中。但 Nuxt.js
不一樣,因爲服務端渲染的特色,部分請求在服務端發起,咱們沒法獲取 localStorage
或 sessionStorage
。
這時候,cookie
就派上了用場。cookie
不只能在客戶端供咱們操做,在請求時也會帶上發回給服務端。使用原生操做 cooike
是很是麻煩的,藉助 cookie-universal-nuxt
模塊(該模塊只是幫助咱們注入,主要實現依賴 cookie-universal
),咱們可以更方便的使用 cookie
。無論在服務端仍是客戶端,cookie-universal-nuxt
都爲咱們提供一致的 api
,它內部會幫咱們去適配對應的方法。
安裝 cookie-universal-nuxt
npm run cookie-universal-nuxt --save
nuxt.config.js
:
module.exports = { modules: [ 'cookie-universal-nuxt' ], }
一樣的, cookie-universal-nuxt
會同時注入,訪問 $cookies
進行使用。
服務端:
// 獲取 app.$cookies.get('name') // 設置 app.$cookies.set('name', 'value') // 刪除 app.$cookies.remove('name')
客戶端:
// 獲取 this.$cookies.get('name') // 設置 this.$cookies.set('name', 'value') // 刪除 this.$cookies.remove('name')
更多使用方法戳這裏 https://www.npmjs.com/package/cookie-universal-nuxt
像掘金的登陸,咱們在登陸後驗證信息會被長期存儲起來,而不是每次使用都要進行登陸。但 cookie
生命週期只存在於瀏覽器,當瀏覽器關閉後也會隨之銷燬,因此咱們須要爲其設置一個較長的過時時間。
在項目中我將設置身份信息封裝成工具方法,在登陸成功後會調用此方法:
/utils/utils.js
:
setAuthInfo(ctx, res) { let $cookies, $store // 客戶端 if (process.client) { $cookies = ctx.$cookies $store = ctx.$store } // 服務端 if (process.server) { $cookies = ctx.app.$cookies $store = ctx.store } if ($cookies && $store) { // 過時時長 new Date(Date.now() + 8.64e7 * 365 * 10) const expires = $store.state.auth.cookieMaxExpires // 設置cookie $cookies.set('userId', res.userId, { expires }) $cookies.set('clientId', res.clientId, { expires }) $cookies.set('token', res.token, { expires }) $cookies.set('userInfo', res.user, { expires }) // 設置vuex $store.commit('auth/UPDATE_USERINFO', res.user) $store.commit('auth/UPDATE_CLIENTID', res.clientId) $store.commit('auth/UPDATE_TOKEN', res.token) $store.commit('auth/UPDATE_USERID', res.userId) } }
以後須要改造下 axios
,讓它在請求時帶上驗證信息:
/plugins/axios.js
:
export default function ({ app: { $axios, $cookies } }) { $axios.defaults.baseURL = process.env.baseUrl $axios.defaults.timeout = 30000 $axios.interceptors.request.use(config => { // 頭部帶上驗證信息 config.headers['X-Token'] = $cookies.get('token') || '' config.headers['X-Device-Id'] = $cookies.get('clientId') || '' config.headers['X-Uid'] = $cookies.get('userId') || '' return config }) $axios.interceptors.response.use(response => { if (/^[4|5]/.test(response.status)) { return Promise.reject(response.statusText) } return response.data }) }
上面提到身份信息會設置一個長期的時間,接下來固然就須要驗證身份是否過時啦。這裏我會使用路由中間件來完成驗證功能,中間件運行在一個頁面或一組頁面渲染以前,就像路由守衛同樣。而每個中間件應放置在 middleware
目錄,文件名的名稱將成爲中間件名稱。中間件能夠異步執行,只須要返回 Promise
便可。
/middleware/auth.js
:
export default function (context) { const { app, store } = context const cookiesToken = app.$cookies.get('token') if (cookiesToken) { // 每次跳轉路由 驗證登陸狀態是否過時 return app.$api.isAuth().then(res => { if (res.s === 1) { if (res.d.isExpired) { // 過時 移除登錄驗證信息 app.$utils.removeAuthInfo(context) } else { // 未過時 從新設置存儲 const stateToken = store.state.auth.token if (cookiesToken && stateToken === '') { store.commit('auth/UPDATE_USERINFO', app.$cookies.get('userInfo')) store.commit('auth/UPDATE_USERID', app.$cookies.get('userId')) store.commit('auth/UPDATE_CLIENTID', app.$cookies.get('clientId')) store.commit('auth/UPDATE_TOKEN', app.$cookies.get('token')) } } } }) } }
上面 if (cookiesToken && stateToken === '')
中的處理,是由於一些頁面會新開標籤頁,致使 vuex
中的信息丟失,這裏須要判斷一下從新設置狀態樹。
nuxt.config.js
:
module.exports = { router: { middleware: ['auth'] }, }
這種中間件使用是注入到全局的每一個頁面中,若是你但願中間件只運行於某個頁面,能夠配置頁面的 middleware
選項:
export default { middleware: 'auth' }
路由中間件文檔戳這裏 https://www.nuxtjs.cn/guide/routing#%E4%B8%AD%E9%97%B4%E4%BB%B6
先來個最簡單的例子,在 plugins
文件夾下建立 vue-global.js
用於管理全局須要使用的組件或方法:
import Vue from 'vue' import utils from '~/utils' import myComponent from '~/components/myComponent.vue' Vue.prototype.$utils = utils Vue.use(myComponent)
nuxt.config.js
:
module.exports = { plugins: [ '~/plugins/vue-global.js' ], }
對於一些自定義全局共用組件,個人作法是將它們放入 /components/common
文件夾統一管理。這樣可使用 require.context
來自動化的引入組件,該方法是由 webpack
提供的,它可以讀取文件夾內全部文件。若是你不知道這個方法,真的很強烈你去了解並使用一下,它能大大提升你的編程效率。
/components/myComponentsInstall.js
:
export default { install(Vue) { const components = require.context('~/components/common', false, /\.vue$/) // components.keys() 獲取文件名數組 components.keys().map(path => { // 獲取組件文件名 const fileName = path.replace(/(.*\/)*([^.]+).*/ig, "$2") // components(path).default 獲取 ES6 規範暴露的內容,components(path) 獲取 Common.js 規範暴露的內容 Vue.component(fileName, components(path).default || components(path)) }) } }
/plugins/vue-global.js
:
import Vue from 'vue' import myComponentsInstall from '~/components/myComponentsInstall' Vue.use(myComponentsInstall)
通過上面的操做後,組件已在全局被註冊,咱們只要按短橫線命名使用便可。並且每新建一個組件都無需再去引入,真的是一勞永逸。一樣在其餘實際應用中,若是 api
文件是按功能分模塊,也可使用這個方法去自動化引入接口文件。
/plugins/vue-global.js
:
import Vue from 'vue' import elementUI from 'element-ui' Vue.use(elementUI)
nuxt.config.js
:
module.exports = { css: [ 'element-ui/lib/theme-chalk/index.css' ] }
藉助 babel-plugin-component
,咱們能夠只引入須要的組件,以達到減少項目體積的目的。
npm install babel-plugin-component -D
nuxt.config.js
:
module.exports = { build: { plugins: [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ], } }
接下來引入咱們須要的部分組件,一樣建立一個 eleComponentsInstall.js
管理 elementUI 的組件:
/components/eleComponentsInstall.js
:
import { Input, Button, Select, Option, Notification, Message } from 'element-ui' export default { install(Vue) { Vue.use(Input) Vue.use(Select) Vue.use(Option) Vue.use(Button) Vue.prototype.$message = Message Vue.prototype.$notify = Notification } }
/plugins/vue-global.js
:
import Vue from 'vue' import eleComponentsInstall from '~/components/eleComponentsInstall' Vue.use(eleComponentsInstall)
在咱們構建網站應用時,大多數頁面的佈局都會保持一致。但在某些需求中,可能須要更換另外一種佈局方式,這時頁面 layout
配置選項就可以幫助咱們完成。而每個佈局文件應放置在 layouts
目錄,文件名的名稱將成爲佈局名稱,默認佈局是 default
。下面的例子是更換頁面佈局的背景色。其實按照使用 Vue
的理解,感受就像切換 App.vue
。
/layouts/default.vue
:
<template> <div style="background-color: #f4f4f4;min-height: 100vh;"> <top-bar></top-bar> <main class="main"> <nuxt /> </main> <back-top></back-top> </div> </template>
/layouts/default-white.vue
:
<template> <div style="background-color: #ffffff;min-height: 100vh;"> <top-bar></top-bar> <main class="main"> <nuxt /> </main> <back-top></back-top> </div> </template>
頁面組件文件:
export default { layout: 'default-white', // 或 layout(context) { return 'default-white' } }
自定義的錯誤頁須要放在 layouts
目錄中,且文件名爲 error
。雖然此文件放在 layouts
目錄中, 但應該將它看做是一個頁面(page)。這個佈局文件不須要包含 <nuxt/>
標籤。你能夠把這個佈局文件當成是顯示應用錯誤(404,500等)的組件。
<template> <div class="error-page"> <div class="error"> <div class="where-is-panfish"> <img class="elem bg" src="https://b-gold-cdn.xitu.io/v3/static/img/bg.1f516b3.png"> <img class="elem panfish" src="https://b-gold-cdn.xitu.io/v3/static/img/panfish.9be67f5.png"> <img class="elem sea" src="https://b-gold-cdn.xitu.io/v3/static/img/sea.892cf5d.png"> <img class="elem spray" src="https://b-gold-cdn.xitu.io/v3/static/img/spray.bc638d2.png"> </div> <div class="title">{{statusCode}} - {{ message }}</div> <nuxt-link class="error-link" to="/">回到首頁</nuxt-link> </div> </div> </template>
export default { props: { error: { type: Object, default: null } }, computed: { statusCode () { return (this.error && this.error.statusCode) || 500 }, message () { return this.error.message || 'Error' } }, head () { return { title: `${this.statusCode === 404 ? '找不到頁面' : '呈現頁面出錯'} - 掘金`, meta: [ { name: 'viewport', content: 'width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no' } ] } } }
錯誤頁面中 props
接受一個 error
對象,該對象至少包含兩個屬性 statusCode
和 message
。
除了這兩個屬性,咱們還能夠傳過去其餘的屬性,這裏又要提及上面提到的 error
方法:
export default { async asyncData({ app, query, error }) { const tagInfo = await app.$api.getTagDetail({ tagName: encodeURIComponent(query.name) }).then(res => { if (res.s === 1) { return res.d } else { // 這樣咱們在 error 對象中又多了 query 屬性 error({ statusCode: 404, message: '標籤不存在', query }) return } }) return { tagInfo } } }
還有頁面的 validate
生命週期:
export default { async validate ({ params, store }) { throw new Error('頁面參數不正確') } }
這裏傳過去的 statusCode
爲 500,message
就是 new Error
中的內容。若是想傳對象過去的話,message
會轉爲字符串 [object Object]
,你可使用 JSON.stringify
傳過去,錯誤頁面再處理解析出來。
export default { async validate ({ params, store }) { throw new Error(JSON.stringify({ message: 'validate錯誤', params })) } }
項目中基本每一個頁面的都會有觸底事件,因此我將這塊邏輯抽離成 mixin
,須要的頁面引入使用便可。
/mixins/reachBottom.js
:
export default { data() { return { _scrollingElement: null, _isReachBottom: false, // 防止進入執行區域時 重複觸發 reachBottomDistance: 80 // 距離底部多遠觸發 } }, mounted() { this._scrollingElement = document.scrollingElement window.addEventListener('scroll', this._windowScrollHandler) }, beforeDestroy() { window.removeEventListener('scroll', this._windowScrollHandler) }, methods: { _windowScrollHandler() { let scrollHeight = this._scrollingElement.scrollHeight let currentHeight = this._scrollingElement.scrollTop + this._scrollingElement.clientHeight + this.reachBottomDistance if (currentHeight < scrollHeight && this._isReachBottom) { this._isReachBottom = false } if (this._isReachBottom) { return } // 觸底事件觸發 if (currentHeight >= scrollHeight) { this._isReachBottom = true typeof this.reachBottom === 'function' && this.reachBottom() } } }, }
實現的核心固然是觸發時機: scrollTop
(頁面滾動距離)+ clientHeight
(頁面可視高度)>= scrollHeight
(頁面總高度,包括滾動區域)。但這種須要徹底觸底才能觸發事件,因此在此基礎上,我添加 reachBottomDistance
用於控制觸發事件的距離。最終,觸發事件會調用頁面 methods
的 reachBottom
方法。
命令式組件是什麼?element-UI
的 Message
組件就是很好的例子,當咱們須要彈窗提示時,只須要調用一下 this.message()
,而不是經過 v-if
切換組件。這種的好處就是不用引入組件,使用起來便捷,哪裏須要調哪裏。
nuxt-juejin-project
項目中我也封裝了兩個公用的彈窗組件,登陸彈窗和預覽大圖彈窗,技術點是手動掛載組件。實現代碼並很少,幾行足矣。
/components/common/picturesModal/picturesModal.vue
:
export default { data() { return { url: '', // 當前圖片連接 urls: '' // 圖片連接數組 } }, methods: { show(cb) { this.cb = cb return new Promise((resolve, reject) => { document.body.style.overflow = 'hidden' this.resolve = resolve this.reject = reject }) }, // 銷燬彈窗 hideModal() { typeof this.cb === 'function' && this.cb() document.body.removeChild(this.$el) document.body.style.overflow = '' // 銷燬組件實例 this.$destroy() }, // 關閉彈窗 cancel() { this.reject() this.hideModal() }, } }
/components/common/picturesModal/index.js
import Vue from 'vue' import picturesModal from './picturesModal' let componentInstance = null // 構造子類 let ModalConstructor = Vue.extend(picturesModal) function createModal(options) { // 實例化組件 componentInstance = new ModalConstructor() // 合併選項 Object.assign(componentInstance, options) // $mount能夠傳入選擇器字符串,表示掛載到該選擇器 // 若是不傳入選擇器,將渲染爲文檔以外的的元素,你能夠想象成 document.createElement()在內存中生成dom // $el獲取的是dom元素 document.body.appendChild(componentInstance.$mount().$el) } function caller (options) { // 單例 全局只存在一個彈窗 if (!componentInstance) { createModal(options) // 調用組件內的show方法 傳入的callback在組件銷燬時調用 return componentInstance.show(() => { componentInstance = null }) } } export default { install(Vue) { // 註冊調起彈窗方法,方法返回Promise then爲登陸成功 catch爲關閉彈窗 Vue.prototype.$picturesModal = caller } }
/plugins/vue-global.js
:
import picturesModal from '~/components/common/picturesModal' Vue.use(picturesModal)
這裏傳入的對象,就是上面 createModal
接收到的 options
參數,最後合併覆蓋到組件的 data
。
this.$picturesModal({ url: 'b.jpg' urls: ['a.jpg', 'b.jpg', 'c.jpg'] })
中間層工做的大概流程是在前端發送請求到中間層,中間層在發送請求到後端獲取數據。這樣作的好處是在前端到後端的交互過程當中,咱們至關於得到了代理的控制權。利用這一權利,咱們能作的事情就更多。好比:
中間層的存在也讓先後端職責分離的更加完全,後端只須要管理數據和編寫接口,須要哪些數據都交給中間層去處理。
nuxt-juejin-project
項目中間層使用的是 koa
框架,中間層的 http
請求方法是基於 request
庫簡單封裝一下,代碼實如今 /server/request/index.js
。由於後面須要用到,這裏就提一下。
npm i koa-router koa-bodyparser --save
koa-router: 路由器中間件,能快速的定義路由以及管理路由
koa-bodyparser: 參數解析中間件,支持解析 json、表單類型,經常使用於解析 POST 請求
相關中間件的使用方法在 npm
上搜索,這裏就贅述怎麼使用了
正所謂無規矩不成方圓,路由設計的規範,我參考的是阮一峯老師的 RESTful API 設計指南。
路由文件我會存放在 /server/routes
目錄中,按照規範還須要一個規定 api
版本號的文件夾。最終路由文件存放在 /server/routes/v1
中。
在 RESTful 架構中,每一個網址表明一種資源(resource),因此網址中不能有動詞,只能有名詞,並且所用的名詞每每與數據庫的表格名對應。通常來講,數據庫中的表都是同種記錄的"集合"(collection),因此 API 中的名詞也應該使用複數。
例如:
articles
tag
pins
路由操做資源的具體類型,由 HTTP
動詞表示
下面是用戶專欄列表接口的例子
/server/router/articles.js
const Router = require('koa-router') const router = new Router() const request = require('../../request') const { toObject } = require('../../../utils') /** * 獲取用戶專欄文章 * @param {string} targetUid - 用戶id * @param {string} before - 最後一條的createdAt,下一頁傳入 * @param {number} limit - 條數 * @param {string} order - rankIndex:熱門、createdAt:最新 */ router.get('/userPost', async (ctx, next) => { // 頭部信息 const headers = ctx.headers const options = { url: 'https://timeline-merger-ms.juejin.im/v1/get_entry_by_self', method: "GET", params: { src: "web", uid: headers['x-uid'], device_id: headers['x-device-id'], token: headers['x-token'], targetUid: ctx.query.targetUid, type: ctx.query.type || 'post', limit: ctx.query.limit || 20, before: ctx.query.before, order: ctx.query.order || 'createdAt' } }; // 發起請求 let { body } = await request(options) // 請求後獲取到的數據爲 json,須要轉爲 object 進行操做 body = toObject(body) ctx.body = { s: body.s, d: body.d.entrylist || [] } }) module.exports = router
/server/index.js
是 Nuxt.js
爲咱們生成好的服務端的入口文件,咱們的中間件使用和路由註冊都須要在這個文件內編寫。下面的應用會忽略部分代碼,只展現主要的邏輯。
/server/index.js
:
const Koa = require('koa') const Router = require('koa-router') const bodyParser = require('koa-bodyparser') const app = new Koa() const router = new Router() // 使用中間件 function useMiddleware(){ app.use(bodyParser()) } // 註冊路由 function useRouter(){ let module = require('./routes/articles') router.use('/v1/articles', module.routes()) app.use(router.routes()).use(router.allowedMethods()) } function start () { useMiddleware() useRouter() app.listen(8000, '127.0.0.1') } start()
最後接口的調用地址是: http://127.0.0.1:8000/v1/articles/userPost
沒錯,它又來了。自動化就是香,一勞永逸能不香嗎。
const fs = require('fs') const Koa = require('koa') const Router = require('koa-router') const bodyParser = require('koa-bodyparser') const app = new Koa() const router = new Router() // 註冊路由 function useRouter(path){ path = path || __dirname + '/routes' // 獲取 routes 目錄下的全部文件名,urls爲文件名數組 let urls = fs.readdirSync(path) urls.forEach((element) => { const elementPath = path + '/' + element const stat = fs.lstatSync(elementPath); // 是否爲文件夾 const isDir = stat.isDirectory(); // 文件夾遞歸註冊路由 if (isDir) { useRouter(elementPath) } else { let module = require(elementPath) let routeRrefix = path.split('/routes')[1] || '' //routes裏的文件名做爲 路由名 router.use(routeRrefix + '/' + element.replace('.js', ''), module.routes()) } }) //使用路由 app.use(router.routes()).use(router.allowedMethods()) } function start () { useMiddleware() useRouter() app.listen(8000, '127.0.0.1') } start()
上面的代碼以 routes
做爲路由的主目錄,向下尋找 js
文件註冊路由,最終以 js
文件路徑做爲路由名。例如,/server/routes/v1/articles.js
中有個搜索接口 /search
,那麼該接口的調用地址爲 localhost:8000/v1/articles/search
。
參數驗證是接口中必定會有的功能,不正確的參數會致使程序意外錯誤。咱們應該提早對參數驗證,停止錯誤的查詢並告知使用者。項目中我基於 async-validator
封裝了一個路由中間件來驗證參數。若是你不知道 koa
中間件的工做流程,那有必要去了解下洋蔥模型。
/server/middleware/validator/js
:
const { default: Schema } = require('async-validator') module.exports = function (descriptor) { return async function (ctx, next) { let validator = new Schema(descriptor) let params = {} // 獲取參數 Object.keys(descriptor).forEach(key => { if (ctx.method === 'GET') { params[key] = ctx.query[key] } else if ( ctx.method === 'POST' || ctx.method === 'PUT' || ctx.method === 'DELETE' ) { params[key] = ctx.request.body[key] } }) // 驗證參數 const errors = await validator.validate(params) .then(() => null) .catch(err => err.errors) // 若是驗證不經過 則返回錯誤 if (errors) { ctx.body = { s: 0, errors } } else { await next() } } }
使用方法請參考 async-validator
const Router = require('koa-router') const router = new Router() const request = require('../../request') const validator = require('../../middleware/validator') const { toObject } = require('../../../utils') /** * 獲取用戶專欄文章 * @param {string} targetUid - 用戶id * @param {string} before - 最後一條的createdAt,下一頁傳入 * @param {number} limit - 條數 * @param {string} order - rankIndex:熱門、createdAt:最新 */ router.get('/userPost', validator({ targetUid: { type: 'string', required: true }, before: { type: 'string' }, limit: { type: 'string', required: true, validator: (rule, value) => Number(value) > 0, message: 'limit 需傳入正整數' }, order: { type: 'enum', enum: ['rankIndex', 'createdAt'] } }), async (ctx, next) => { const headers = ctx.headers const options = { url: 'https://timeline-merger-ms.juejin.im/v1/get_entry_by_self', method: "GET", params: { src: "web", uid: headers['x-uid'], device_id: headers['x-device-id'], token: headers['x-token'], targetUid: ctx.query.targetUid, type: ctx.query.type || 'post', limit: ctx.query.limit || 20, before: ctx.query.before, order: ctx.query.order || 'createdAt' } }; let { body } = await request(options) body = toObject(body) ctx.body = { s: body.s, d: body.d.entrylist || [] } }) module.exports = router
type
表明參數類型,required
表明是否必填。當 type
爲 enum
(枚舉)類型時,參數值只能爲 enum
數組中的某一項。
須要注意的是,number
類型在這裏是沒法驗證的,由於參數在傳輸過程當中會被轉變爲字符串類型。可是咱們能經過 validator
方法自定義驗證規則,就像上面的 limit
參數。
如下是當 limit
參數錯誤時接口返回的內容:
設置 cors
來驗證請求的安全合法性,可讓你的網站提升安全性。藉助 koa2-cors
可以幫助咱們更便捷的作到這些。koa2-cors
的源碼也很少,建議去看看,只要你有點基礎都能看懂,不只要懂得用也要知道實現過程。
npm install koa2-cors --save
/server/index.js
:
const cors = require('koa2-cors') function useMiddleware(){ app.use(helmet()) app.use(bodyParser()) //設置全局返回頭 app.use(cors({ // 容許跨域的域名 origin: function(ctx) { return 'http://localhost:8000'; }, exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'], maxAge: 86400, // 容許攜帶頭部驗證信息 credentials: true, // 容許的方法 allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'], // 容許的標頭 allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Token', 'X-Device-Id', 'X-Uid'], })) }
若是不符合請求的方式,或帶有未容許的標頭。發送請求時會直接失敗,瀏覽器拋出 cors
策略限制的錯誤。下面是帶有未容許標頭錯誤的例子:
koa-helmet
提供重要的安全標頭,使你的應用程序在默認狀況下更加安全。
npm install koa-helmet --save
const helmet = require('koa-helmet') function useMiddleware(){ app.use(helmet()) // ..... }
默認爲咱們作了如下安全設置:
DNS
預取。X-Powered-By
標頭,使攻擊者更難於查看使網站受到潛在威脅的技術。HTTPS
。Internet Explorer
在您的站點上下文中執行下載。nosniff
,有助於防止瀏覽器試圖猜想(「嗅探」)MIME
類型,這可能會帶來安全隱患。XSS
攻擊。更多說明和配置戳這裏 https://www.npmjs.com/package/koa-helmet
感受中間層的相關知識點仍是不夠全,能作的還有不少,仍是得繼續學習。項目後續還會更新一段時間,更多會靠近服務端這塊,好比緩存優化、異常捕獲這類的。
若是你有任何建議或改進,請告訴我~
😄看到這裏還不來個小星星嗎? https://github.com/ChanWahFung/nuxt-juejin-project
其它常見問題:https://www.nuxtjs.cn/faq
官方github文檔:https://github.com/nuxt/docs/tree/master/zh(裏面有全面配置和例子使用,部分在 Nuxt.js 文檔中沒有說起,很建議看下)