用 Nuxt.js 搭建一個(gè)服務(wù)端渲染(SSR)應(yīng)用
客戶端渲染(CSR)的含義
客戶端渲染模式下,服務(wù)端把渲染的靜態(tài)文件給到客戶端,客戶端拿到服務(wù)端發(fā)送過來(lái)的文件自己跑一遍 JS,根據(jù) JS運(yùn)行結(jié)果,生成相應(yīng) DOM,然后渲染給用戶。
前端渲染的方式起源于 JavaScript 的興起,ajax 的大熱更是讓前端渲染更加成熟,前端渲染真正意義上的實(shí)現(xiàn)了前后端分離,前端只專注于 UI 的開發(fā),后端只專注于邏輯的開發(fā),前后端交互只通過約定好的API來(lái)交互,后端提供 json 數(shù)據(jù),前端循環(huán) json 生成 DOM 插入到頁(yè)面中去。
大多數(shù)平臺(tái)采用的是客戶端渲染,查看首頁(yè)的源代碼會(huì)發(fā)現(xiàn)代碼里的 html 結(jié)構(gòu)只有簡(jiǎn)單的幾句。當(dāng)請(qǐng)求首頁(yè)面時(shí),返回的 body 為空,之后執(zhí)行 js 將 html 結(jié)構(gòu)注入到 body 里,結(jié)合 css 顯示出來(lái);
- <body>
- <div id=app></div>
- <script type=text/javascript src=/static/js/manifest.9476fbe0d0f0fe7c5038.js></script>
- </body>
客戶端渲染(CSR)的優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):網(wǎng)絡(luò)傳輸數(shù)據(jù)量小、減少了服務(wù)器壓力、前后端分離、局部刷新,無(wú)需每次請(qǐng)求完整頁(yè)面、交互好可實(shí)現(xiàn)各種效果
- 缺點(diǎn):不利于SEO、爬蟲看不到完整的程序源碼、首屏渲染慢(渲染前需要下載一堆js和css等)
服務(wù)端渲染(SSR)的含義
服務(wù)端渲染: 當(dāng)用戶第一次請(qǐng)求頁(yè)面時(shí),由服務(wù)器把需要的組件或頁(yè)面渲染成 HTML 字符串,然后把它返回給客戶端。客戶端拿到手的,是可以直接渲染然后呈現(xiàn)給用戶的 HTML 內(nèi)容,不需要為了生成 DOM 內(nèi)容自己再去跑一遍 JS 代碼。使用服務(wù)端渲染的網(wǎng)站,可以說(shuō)是“所見即所得”,頁(yè)面上呈現(xiàn)的內(nèi)容,我們?cè)?html 源文件里也能找到。如下,我們查看網(wǎng)頁(yè)源碼的時(shí)候,可以看到全部?jī)?nèi)容。
服務(wù)端渲染(SSR)的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):首屏渲染快、利于SEO、可以生成緩存片段,生成靜態(tài)化文件、節(jié)能(對(duì)比客戶端渲染的耗電)
缺點(diǎn):服務(wù)端壓力較大
什么情況下使用服務(wù)端渲染
通過服務(wù)端渲染的概念以及它的兩個(gè)特點(diǎn):首屏加載速度快、SEO優(yōu)化。我們知道,服務(wù)端渲染其實(shí)就是由瀏覽器做的一些事情,我們放到了服務(wù)端去做。關(guān)于在 server 端還是在 browser 端渲染的選擇,更多的是要看業(yè)務(wù)場(chǎng)景。
常用框架介紹
服務(wù)端渲染框架應(yīng)用有Nuxt.js 、Beidou(北斗) 等。
Nuxt.js 是一個(gè)基于 Vue.js 的輕量級(jí)應(yīng)用框架,可用來(lái)創(chuàng)建服務(wù)端渲染 (SSR) 應(yīng)用,也可充當(dāng)靜態(tài)站點(diǎn)引擎生成靜態(tài)站點(diǎn)應(yīng)用,具有優(yōu)雅的代碼結(jié)構(gòu)分層和熱加載等特性。
Beidou(北斗) 是 NodeJS & React 同構(gòu)框架,基于Egg.js開發(fā)。
嘗試了這兩個(gè)框架,對(duì)比覺得Nuxt.js更簡(jiǎn)單易上手,下面就用Nuxt.js搭建一個(gè)服務(wù)端渲染應(yīng)用來(lái)介紹下 Nuxt.js 的用法。
創(chuàng)建一個(gè) SSR 項(xiàng)目
為了快速入門,Nuxt.js團(tuán)隊(duì)創(chuàng)建了腳手架工具 create-nuxt-app。
- npx create-nuxt-app nuxtdemo
它會(huì)讓你進(jìn)行一些選擇,比如集成的服務(wù)器端框架、喜歡的UI框架、測(cè)試框架、添加 axios、Eslint、 Prettier 等。根據(jù)項(xiàng)目需求進(jìn)行選擇就好了。這里以服務(wù)器框架選擇None (Nuxt默認(rèn)服務(wù)器),UI框架選擇Element UI為例進(jìn)行講解。
勾選完畢后,它將安裝所有依賴項(xiàng),因此下一步是直接啟動(dòng)項(xiàng)目:
- cd nuxtdemo
- npm run dev
這時(shí)候我們可以看到一個(gè)默認(rèn)簡(jiǎn)易的項(xiàng)目搭建完成啦,如下所示:
接下來(lái),我們來(lái)看下整個(gè)項(xiàng)目的目錄結(jié)構(gòu):
- ├── assets 未編譯的靜態(tài)資源如 LESS、SASS 或 JavaScript
- ├── components 組件,不會(huì)像頁(yè)面組件那樣有 asyncData 方法的特性
- ├── layouts 布局目錄 layouts 用于組織應(yīng)用的布局組件
- ├── middleware 用于存放應(yīng)用的中間件
- ├── nuxt.config.js 用于組織Nuxt.js 應(yīng)用的個(gè)性化配置,以便覆蓋默認(rèn)配置
- ├── package.json 用于描述應(yīng)用的依賴關(guān)系和對(duì)外暴露的腳本接口
- ├── pages 用于組織應(yīng)用的路由及視圖
- ├── plugins 存放需要在根vue.js應(yīng)用實(shí)例化之前需要運(yùn)行的JS插件
- ├── static 用于存放應(yīng)用的靜態(tài)文件(不會(huì)被webpack編譯處理)
- ├── store 應(yīng)用的 Vuex 狀態(tài)樹
了解了每個(gè)文件的作用,我們來(lái)用Nuxt.js搭一個(gè)簡(jiǎn)單的網(wǎng)站吧。用一個(gè)簡(jiǎn)單的網(wǎng)站,講解下 Nuxt.js 的基礎(chǔ)用法。
Nuxt.js 入門
我們用 Nuxt.js 來(lái)搭一個(gè)常用的網(wǎng)頁(yè)框架,包括公共頭部、底部、動(dòng)態(tài)路由、嵌套路由,錯(cuò)誤頁(yè)面,以及在 Nuxt.js 框架下如何引用公共樣式、公共方法、路由校驗(yàn)等。先放上網(wǎng)站成品圖:
下載鏈接:
- git clone git@code.aliyun.com:echomaps/nuxtdemo.git
這是個(gè)簡(jiǎn)易的網(wǎng)站,包括公共頭部跟尾部。首頁(yè)是一個(gè)文章列表,采用了動(dòng)態(tài)路由,點(diǎn)進(jìn)去可以跳到對(duì)應(yīng)的文章。人員介紹頁(yè)面采用了嵌套路由。在左側(cè)點(diǎn)擊人員,右側(cè)可以相應(yīng)出來(lái)人員的信息。好,讓我們來(lái)開始吧。
布局
一般網(wǎng)站都有公共的頭部、底部。在之前的項(xiàng)目中,我們都得手動(dòng)去引入頭部、尾部組件。如下:
- import header from '@/publicResource/components/header.vue'
- import footer from '@/publicResource/components/footer.vue'
- export default {
- components: {
- 'v-header': header,
- 'v-footer': footer
- }
- } :
但在 Nuxt.js 中就不用這么麻煩。我們直接在 layout 目錄下創(chuàng)建自定義的布局。修改 layouts/default.vue 文件來(lái)擴(kuò)展應(yīng)用的默認(rèn)布局:
- <template>
- <div>
- <v-header></v-header>
- <nuxt />
- <v-footer></v-footer>
- </div>
- </template>
- <script>
- import Header from '~/components/Header.vue'
- import Footer from '~/components/Footer.vue'
- export default {
- components: {
- 'v-header': Header,
- 'v-footer': Footer
- },
- data () {
- return { }
- }
- }
- </script>
<nuxt/> 組件用于顯示頁(yè)面的主體內(nèi)容。這樣所有的頁(yè)面都會(huì)自動(dòng)帶上頭部、尾部,不用特意聲明與引入。如果有些頁(yè)面布局不需要頭部、尾部,這也很簡(jiǎn)單,我們只需要告訴頁(yè)面使用哪個(gè)自定義布局即可。
- <template>
- <!-- Your template -->
- </template>
- <script>
- export default {
- layout: 'blog'
- // page component definitions
- }
- </script>
錯(cuò)誤頁(yè)面
我們也可以通過編輯 layouts/error.vue 文件來(lái)定制化錯(cuò)誤頁(yè)面。這個(gè)布局文件不需要包含 <nuxt/> 標(biāo)簽。可以把這個(gè)布局文件當(dāng)成是顯示應(yīng)用錯(cuò)誤(404,500等)的組件。
- <template>
- <div class="error-wrap">
- <p v-if="error.statusCode === 404" class="info">頁(yè)面不存在</p>
- <p class="info" v-else>應(yīng)用發(fā)生錯(cuò)誤異常</p>
- <p><nuxt-link to="/">首 頁(yè)</nuxt-link></p>
- </div>
- </template>
- <script>
- export default {
- props: ['error'],
- }
- </script>
基礎(chǔ)路由
Nuxt.js中不用編寫路由配置文件,只需要按照API規(guī)定命名與存放文件,即可自動(dòng)生成路由配置文件。例如,我們需要新增一個(gè)人員介紹頁(yè)面users. 只需要在pages下新增users頁(yè)面,就可以自動(dòng)生成路由。假設(shè) pages 的目錄結(jié)構(gòu)如下:
- pages/
- --| users.vue
- --| index.vue
那么,Nuxt.js 自動(dòng)生成的路由配置如下:
- router: {
- routes: [
- {
- name: 'index',
- path: '/',
- component: 'pages/index.vue'
- },
- {
- name: 'users',
- path: '/users',
- component: 'pages/users.vue'
- }
- ]
- }
其它頁(yè)面引用的時(shí)候,直接用nuxt-link即可。
- <nuxt-link to="/users">人員介紹</nuxt-link>
同樣地,我們也可以通過框架規(guī)定的命名、存放文件。無(wú)需配置路由,可生成動(dòng)態(tài)路由、嵌套路由的配置文件。
動(dòng)態(tài)路由
在 Nuxt.js 里面定義帶參數(shù)的動(dòng)態(tài)路由,需要?jiǎng)?chuàng)建對(duì)應(yīng)的以下劃線作為前綴的 Vue 文件 或 目錄。如下所示:
- ├── pages
- ├────── blogs
- │ └─── _blog.vue 博客的詳情頁(yè)
- ├────── index.vue 首頁(yè)
假如我們?cè)趇ndex.vue中編寫一個(gè)文章列表并鏈接到對(duì)應(yīng)的文章頁(yè)面,如下:
- <template>
- <div class="container">
- <div class="bm-sider">
- {{content}}
- </div>
- <div class="bm-con">
- <ul>
- <li><nuxt-link to="blogs/1">這是文章1</nuxt-link></li>
- <li><nuxt-link to="blogs/2">這是文章2</nuxt-link></li>
- <li><nuxt-link to="blogs/3">這是文章3</nuxt-link></li>
- </ul>
- </div>
- </div>
- </template>
pages/blogs/_blog.vue:
- <template>
- <div class="container">
- 這是內(nèi)容{{$route.params.blog}}
- </div>
- </template>
- <script>
- export default {
- components: {},
- data () {
- return { }
- },
- validate ({ params }) {
- return !isNaN(+params.blog)
- }
- }
- </script>
這樣,默認(rèn)首頁(yè)的展示如下:
當(dāng)點(diǎn)擊具體文章時(shí)候,展示如下:
我們還可以添加 validate 配置一個(gè)校驗(yàn)方法用于校驗(yàn)動(dòng)態(tài)路由參數(shù)的有效性。如果校驗(yàn)方法返回的值不為 true 或 Promise 中 resolve 解析為 false 或拋出 Error , Nuxt.js 將自動(dòng)加載顯示 404 錯(cuò)誤頁(yè)面或 500 錯(cuò)誤頁(yè)面。這里我們?cè)O(shè)置只有數(shù)字可以正常訪問,其它路由將跳到錯(cuò)誤頁(yè)面。如下所示:
嵌套路由
創(chuàng)建內(nèi)嵌子路由,需要添加一個(gè) Vue 文件,同時(shí)添加一個(gè)與該文件同名的目錄用來(lái)存放子視圖組件。在父組件(.vue文件) 內(nèi)增加用于顯示子視圖內(nèi)容。
人員介紹頁(yè)面采用了嵌套路由。點(diǎn)擊左側(cè)的人員名單,將出現(xiàn)對(duì)應(yīng)的人員信息,效果如下:
實(shí)現(xiàn)這一效果,我們需要在 pages下添加人員介紹頁(yè)面 users.vue:
- <template>
- <div class="container">
- <div class="bm-sider">
- <ul class="players">
- <li v-for="user in users" :key="user.id">
- <NuxtLink :to="'/users/'+user.id">
- {{ user.name }}
- </NuxtLink>
- </li>
- </ul>
- </div>
- <div class="bm-con">
- <NuxtChild :key="$route.params.id" />
- </div>
- </div>
- </template>
同時(shí)添加一個(gè)與該文件同名的目錄用來(lái)存放子視圖組件。文件如下命名:
- ├── users
- │ ├── _id.vue 點(diǎn)擊人員后對(duì)應(yīng)的人員信息組件
- │ └── index.vue 默認(rèn)的視圖組件
- └── users.vue 人員介紹頁(yè)面
users/index.vue:
- <template>
- <h2>Please select an user.</h2>
- </template>
users/_id.vue:
- <template>
- <div class="player">
- <h1>#{{ number }}</h1>
- <h2>{{ name }}</h2>
- </div>
- </template>
- <script>
- export default {
- validate ({ params }) {
- return !isNaN(+params.id)
- },
- asyncData ({ params, env, error }) {
- const user = env.users.find(user => String(user.id) === params.id)
- if (!user) {
- return error({ message: 'User not found', statusCode: 404 })
- }
- return user
- },
- head () {
- return {
- title: this.name
- }
- }
- }
- </script>
這樣,當(dāng)我們未點(diǎn)擊人員時(shí)候,人員介紹默認(rèn)頁(yè)面是這樣的:
點(diǎn)擊人員后,人員介紹頁(yè)面將展示對(duì)應(yīng)的人員信息內(nèi)容:
全局 css
在 Nuxt 中添加全局 css 也是非常簡(jiǎn)單的。我們?cè)?assets 下新建一個(gè) css 文件 base.css 。然后在 nuxt.config.js 中引用即可。
- css: [
- '~assets/base.css',
- ],
全局方法
將內(nèi)容注入 Vue 實(shí)例,避免重復(fù)引入,在 Vue 原型上掛載注入一個(gè)函數(shù),所有組件內(nèi)都可以訪問。
- import Vue from 'vue'
- Vue.prototype.$myInjectedFunction = (string) => console.log("This is an example", string)
這樣,我們就可以在所有Vue組件中使用該函數(shù)。
- export default {
- mounted(){
- this.$myInjectedFunction('test')
- }
- }
總結(jié)
Nuxt.js 是使用 Webpack 和 Node.js 進(jìn)行封裝的基于 Vue 的 SSR 框架,使用它,你可以不需要自己搭建一套 SSR 程序,而是通過其約定好的文件結(jié)構(gòu)和API就可以實(shí)現(xiàn)一個(gè)首屏渲染的 Web 應(yīng)用。整體上,Nuxt.js 通過各個(gè)文件夾和配置文件的約束來(lái)管理我們的程序,而又不失擴(kuò)展性。