之前用vue做了一個動態官網項目,后期客戶要求seo,百度上之前搜索不到官網地址,后來在項目的入口文件index.html頁面加上了,固定的meta標簽,加上name名為keywords、description的meta標簽。
例子:
<meta charset="utf-8"> //下面這個meta標簽 是ie8的專用標記,指定ie8瀏覽器器模擬特定版本的ie瀏覽器渲染方式,下面指定的是edge <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> //在ie、360等瀏覽器打開時,默認使用極速模式,然后是兼容模式,然后是標准模式 <meta name="renderer" content="webkit|ie-comp|ie-stand"> //seo相關,百度收錄時,的描述文字 <meta name="description" content="第十七屆xxxxx醫學大會"> //seo相關:百度搜索時的 關鍵字,就是用那些關鍵字可以搜索到此網站 <meta name="keywords" content="中國國際xx醫學大會,第十七屆,xx,醫學,國際">
以上做了個簡單的seo優化,這個項目有幾個官網,但是其中只有一個官網要求seo,也就是在百度能夠搜索到,當時為了應急,就寫死了,但是,其它的網站也就會受到干擾了,也就是對於一個項目對應幾個官網,寫死的meta標簽做seo是不科學的。
於是下面又來尋找更科學的seo優化方案,下面是一些相關連接,很感謝作者:
https://blog.csdn.net/weixin_41049850/article/details/81945201
文中提到了關於vue單頁項目的一些seo優化方案:
首先想vue、react、angular三大前端框架都有spa頁面,做起來seo就很麻煩,當然,不是必須要求用這三個框架就必須采用spa的開發模式,但是spa前后端分離的形式真的是太方便了,如果不采用spa方式進行開發,用古老的混合開發自然不會存在seo的問題,但是我們現在大多采用的是spa的形式開發的,
方案1:服務端渲染
上面這個截圖是vue作者,為解決seo優化所提出的一個方案,服務端渲染(ssr):官網鏈接:https://cn.vuejs.org/v2/guide/ssr.html
如果項目剛開始就考慮到seo,采用服務端渲染,那么就用服務端渲染就得了。
但是一般來講,項目做到后期才會考慮到seo的問題,這時再去搞服務端渲染,相當於重頭寫項目,非常耗費人力物力。
那么,不采用服務端渲染該如何最大程度解決seo問題呢?
方案2:預渲染
原文鏈接:https://www.cnblogs.com/kdcg/p/9606302.html
這比服務端渲染要簡單很多,而且可以配合 vue-meta-info 來生成title和meta標簽,基本可以滿足SEO的需求
TIPS: 使用預渲染vue-router必須使用history模式
// 安裝 npm install prerender-spa-plugin --save-dev
然后在webpack.prod.conf.js里面添加:cli3項目在vue.config.js文件中配置
// 頭部引入 const PrerenderSPAPlugin = require('prerender-spa-plugin') const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
在plugins里面添加:
config.js
// vue.config.js const webpack = require('webpack'); const path = require('path'); const CompressionPlugin = require('compression-webpack-plugin');//引入gzip壓縮插件 // 預渲染 const PrerenderSPAPlugin = require('prerender-spa-plugin') const Renderer = PrerenderSPAPlugin.PuppeteerRenderer function resolve(dir) { return path.join(__dirname, dir) } module.exports = { // 選項... //基本路徑 publicPath: process.env.NODE_ENV === 'production'? '/': '/',//部署服務器的路徑 默認在根路徑上(影響靜態資源的引用路徑) outputDir: 'customizationWeb', assetsDir: 'static', productionSourceMap:false,//打包時不要map文件 // filenameHashing: true, devServer: { port: 9522, proxy:{ '/qiantai':{ target:'http://139.xxx.xxx.xx:xxxx', changeOrigin: true, pathRewrite: { "^/qiantai": '' } } } }, configureWebpack:()=> { if (process.env.NODE_ENV === 'development'){ return { externals: {//如果不想影響開發環境,這里也要配置externals 沒用它的就不用在開發環境也配置一份了 'vue': 'Vue', 'vuex': 'Vuex', // 'vue-router': 'VueRouter', 'Axios':'axios' }, } }else{ return { externals: { 'vue': 'Vue', 'vuex': 'Vuex', // 'vue-router': 'VueRouter', 'Axios':'axios' }, plugins: [ new CompressionPlugin({//gzip壓縮配置 test:/\.js$|\.html$|\.css/,//匹配文件名 threshold:10240,//對超過10kb的數據進行壓縮 deleteOriginalAssets:false,//是否刪除原文件 }), new PrerenderSPAPlugin({ // 生成文件的路徑,也可以與webpakc打包的一致。 // 下面這句話非常重要!!! // 這個目錄只能有一級,如果目錄層次大於一級,在生成的時候不會有任何錯誤提示,在預渲染的時候只會卡着不動。 staticDir: path.join(__dirname, 'customizationWeb'), // 對應自己的路由文件,比如a有參數,就需要寫成 /a/param1。 routes: ['/'], // 這個很重要,如果沒有配置這段,也不會進行預編譯 renderer: new Renderer({ inject: { foo: 'bar' }, headless: false, // 在 main.js 中 document.dispatchEvent(new Event('render-event')),兩者的事件名稱要對應上。 renderAfterDocumentEvent: 'render-event' }) }) ] } } }, chainWebpack(config) { // set svg-sprite-loader config.module .rule('svg') .exclude.add(resolve('src/icons')) .end() config.module .rule('icons') .test(/\.svg$/) .include.add(resolve('src/icons')) .end() .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) .end() // 打包依賴分析 // config // .plugin('webpack-bundle-analyzer') // .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin) } }
綠色背景的代碼是 預渲染相關都二代碼
在main.js加上這點代碼:
new Vue({ router, store, render: h => h(App), mounted: () => document.dispatchEvent(new Event("x-app-rendered")), mounted() { document.dispatchEvent(new Event('render-event')) }, }).$mount("#app");
這種操作不需要你去添加任何一段代碼,直接npm run build
,dist文件中就有你寫的幾個html靜態文件。
然后運行npm run build
發現打包好的index.html中多了一大堆的代碼,部署到nginx上試試看看什么效果
下面的報錯是之前用另一種方式 vue add 什么什么的全自動做預渲染的,網上可以查查,打包后會出現這些坑,第二次直接按照以上的步驟就沒有下面的問題了
和其它人一樣,一直打包不成功,有錯誤:
然后和大佬們的一樣安裝
npm i puppeteer 太慢
直接用cnpm i puppeteer 就行分分鍾下載好
之后再執行npm run build
還是上面的報錯:
查一下這個報錯:
使用cnpm或者
npm config set puppeteer_download_host=https://npm.taobao.org/mirrors npm i puppeteer
然后下載成功了,也不慢,
之后打包npm run build
打包成功,index.html中多了好多代碼,但是本地運行項目出問題了,后續接着看
搞了半天,感覺對於純動態的頁面,預渲染好像並無卵用。因為動態獲取的內容,預渲染是預渲染不出來的,還的看服務端渲染,。。。
這是一位大佬的文章,付費的哦:https://gitbook.cn/books/5d7f8b5b84257d2371a8babb/index.html
想了想,我掏錢了,我的把大佬的文章偷來,給大家免費看,嘻嘻:復制粘貼走起:
-------------------------------------------------------------------------------------------------------------------------------------------------------
前言
如果開發需要復雜交互的 Web 應用,我們多半會選擇 SPA;如果要做提供內容資訊的網站,更有利於 SEO、加載速度更快的服務器端渲染(Server-Side Rendering,SSR)自然是大家的首選;那么,如果是一個 CMS 生成純靜態網頁呢?
前陣子公司官網升級,我嘗試用 Webpack 多頁配置,成功的升級了工具鏈,收獲了比較理想的效果。我還寫了一篇 Chat 分享這期間的收獲:《升級工具鏈吧!使用 Webpack 開發企業官網》。實際運行一陣子之后,我發現一些新問題:這套技術鏈對於非前端開發者來說,還真算不上簡單,開發環境搭建、不同文件的功用,對后端同事來說仍然顯得很復雜。於是,有時候雖然只是小小的文案錯誤,也要找我來改;或者“加入我們”頁面里的崗位信息,也需要熟悉代碼的前端負責增刪。
於是我就想,能否把各頁面以 Vue 組件的形式搭建起來,每個組件可以有“編輯/顯示”兩個狀態,在本地啟動開發環境,在瀏覽器里“編輯”內容,用戶可以獲得所見即所得的體驗。最后以“顯示”狀態渲染成靜態頁,繼續使用純靜態的方式部署。這就相當於,基於原先的項目開發一個所見即所得 CMS,把原先的靜態 HTML(或 Pug)頁面改造成 Vue 頁面,然后利用 Vue 的 SSR 機制發布成純靜態 HTML。感覺上是個不錯的想法。
我以前只是聽說過 SSR,一直沒有實操經驗,有了這個想法之后,就想研究下這個過程,搞個最小用例,將來官網升級 3.0 的時候可以考慮。我本以為只是舉手之勞,最多 1,2 個小時就搞定了,沒想到最后折騰了大半天。所以我覺得,很有必要再寫一篇 Chat,分享這個過程中的收獲。
本次分享大綱如下:
- 網頁形態的歷史
- Vue CMS 的產品形態
- 了解 Nuxt.js
- 生成靜態頁的相關配置
- 添加 SEO 關鍵信息
- 注入專有 JS
面向讀者
- 初中級開發者,熟悉 Vue
- 希望了解前端工具鏈
- 希望了解靜態化和 Nuxt.js
名詞及約定
我假定所有讀者都是有一定經驗的開發者,大家至少都具備:
- 能讀懂 JavaScript
- 了解 Vue,使用過 Vue 開發項目
- 知道 Webpack,了解前端工具鏈中各工具的角色和基礎用法
其它約定:
- 為節省時間,范例代碼中的 HTML 會以 pug 書寫,這種語言很容易閱讀,文中也用不到高級語法,應該問題不大。另外,如果你還在寫原生 HTML 或 CSS,我建議你盡快切換到新語言。
- 范例代碼以 ES6+ 為基礎,如果你對這些“新”語法不熟悉,附錄里有一些資源方便你學習。
名詞:
- SEO:搜索引擎優化。指改進網頁,讓搜索引擎更容易理解它的內容,提高頁面的排名。
- CMS:發布系統。沒有特指的話,特指本文中發布靜態頁的工具。
文中代碼的目標環境:
- Vue >= 2.6
- Vue-router >= 3.1
- Nuxt.js >= 2.8
作者介紹
大家好,我叫翟路佳,花名“肉山”,這個名字跟 Dota 沒關系,從高中起伴隨我到現在。
我熱愛編程,喜歡學習,喜歡分享,從業十余年,投入的比較多,學習積累到的也比較多,對前端方方面面都有所了解,希望能與大家分享。
我興趣愛好比較廣泛,尤其喜歡旅游,歡迎大家相互交流。
我目前就職於 OpenResty Inc.,定居廣州。
你可以在這里找到我:
或者通過 郵件 聯系我。
限於個人能力、知識視野、文字技術不足,文中難免有疏漏差錯,如果你有任何疑問,歡迎在任何時間通過任何形式向我提出,我一定盡快答復修改。
再次感謝大家。
網頁形態的發展
在正式開始之前,先介紹一下網頁形態發展的歷史,方便大家理解。
遠古時期:純靜態
發明 HTML 的目的是方便大家看論文文獻,所以早期的 HTML 都是靜態的,放在服務器上,直接映射本地文件目錄結構。
當然那個時候也沒多少網頁,很多服務並沒有網頁版本,比如論壇,是以 Telnet 形式提供服務的;文件共享,大多使用 FTP。
古典時期:動態網站
所謂“動態網站”,就是根據用戶請求,返回合適的內容。或者換用技術的說法,接受請求之后,從數據庫中讀取數據,生成頁面,返回給用戶。其實現在沒什么網站不是動態網站了,感覺這是個上個世紀的詞,比如去京東上搜“動態網站”,能搜到一大堆書,大多有着深刻的時代印記,比如《ASP+Dreamweaver動態網站開發》,簡直辣眼睛。
文藝復興:偽靜態
相比於純靜態網站而言,動態網站通常需要更長的響應時間,而且 URL 也經常難以閱讀。比如:/post.php?id=157
,沒人知道它是什么意思。這個時期的搜索引擎也比較弱,面對類似的網頁,會降低權重。所以網站運營方要想辦法改進 SEO,就用服務器 rewrite 的方式,把形似靜態頁的路徑指向動態路徑,提升搜索排名。
偽靜態常常和真靜態共同工作,這個階段 CDN 還不夠普及,所以生成靜態頁面並部署到終端服務器的操作也很常見。
近代:SPA
隨着各項技術的發展,瀏覽器在整個互聯網產品中的地位越來越重要,承擔的職責也越來越多,最終單頁應用(Single Page Application,簡稱 SPA)脫穎而出,成為最流行的產品形態。關於它的好處我就不一一敘說了,相信大家都了解。
而接下來,MVVM 框架橫空出世,一統江湖,更是大大提升了 SPA 的開發體驗,同時大大降低了入門門檻。然后,類似的產品如雨后春筍搬涌現。
現代:SPA + SSR
SEO 的問題在 SPA 這里更明顯:對於一些不思進取的搜索引擎爬蟲來說,SPA 應用里什么內容都沒有。而不思進取,是很多統治級公司、統治級產品的常態。所以沒辦法,它們不適配我們,我們就得去適配它們。然后,服務器端渲染(Server-Side Rendering,簡稱 SSR)就顯得非常必要。
SPA 的 SSR 和以前的偽靜態不同。偽靜態時期,業務邏輯都是通過后端完成的,前端主要用來收集數據;SPA 時期,大部分業務邏輯已經移到前端,后端只負責數據校驗和必要的存儲。此時 SSR 的目的是讓用戶盡快看到第一波需要的數據,接下來的操作仍然由 SPA 負責。所以我們就面臨兩種選擇:
- 前后端共用一套模板,渲染后的頁面擁有全部功能,可以繼續與用戶交互
- 部分頁面生成純靜態內容,可以部署在服務器上;其它重交互的部分保持 SPA,獨立工作
現代分支:在本地寫作的純靜態網站
隨着雲服務發展,現在很多網站都提供基礎的靜態網站托管服務,比如 GitHub Pages,我們可以使用一些工具在本地完成靜態頁面的批量創建工作,然后上傳到服務器,擁有自己的網站。
本文的工作實際應該算到這個分支。
Vue CMS 的產品形態
回顧完歷史,我們來看看業務需求。
看過我上一次 Chat 《升級工具鏈吧!使用 Webpack 開發企業官網》的同學應該知道,一切都源自我廠的官網要改版。
我廠官網 v1.0 采用的是前文中“遠古時期”的模式,由設計師設計、制作完網頁之后,直接把純靜態資源上傳到服務器,然后提供服務。這樣的好處是:
- 簡單好操作,對人員要求低
- 訪問速度快,對機器配置要求低
- 對搜索引擎友好
但是也有很多不足:
- 純靜態,不利於調整內容
- 不利於 i18n,沒有多語言版
這些問題,我在 v2.0 時進行了修正。首先,我引入 Gulp 做批處理,增加了“發布”環節,雖然最終還是部署純靜態內容,但是內容可以簡單調整,也通過 DOM 查找,實現了 i18n,同步提供中英兩個語言。
新版本上線一年半,效果不錯,但也有很多不足:
- 需要開發者手動管理所有資源,很累
- 發布腳本很多很復雜,不便於理解;Gulp 采用 stream 模式,沒搞過的新人很難接手
- 沒有好的開發環境
於是,當整個頁面內容都要更新時,我決定升級到 v3.0。這次使用 Webpack 工具鏈,采用多頁面模式,配合 Pug 的可編程特性,更好的實現了最終效果,還可以配合 webpack-dev-server 方便的在本地開發。新版本大大改善了開發效率。新同事打開項目看了看 Webpack 的配置文件,很快就搞明白它是怎么工作的,如果要做工作,應該從哪里入手。
不過正如《矛盾論》所說,當主要矛盾被解決,次要矛盾就會變成新的主要矛盾。此刻,新的問題出現:專業的前端開發能很快摸清項目結構,但對於后端同事來說,未知的新概念還是很多。教會他們理解所有框架、類庫、工具沒什么意義,如果能夠給他們一個簡單可操作的 UI,那才能真正提高效率。比如 npm run edit
,然后瀏覽器就打開一個頁面。他們按需編輯后,保存,提交倉庫,發布。這樣的流程才是真正要追求的流程。
所以,最合適的做法就是用 Vue 實現一些編輯器組件,它們有兩種形態:編輯,靜態。編輯態就不用解釋了;我們只要把靜態的 HTML 保存下來,生成靜態頁,即可。
好的,現在需求已經出來了:
- 已有一個 Vue 項目
- 這個項目大部分功能由 SPA 提供,不需要靜態化,不需要預渲染
- 這個項目部分路由需要生成靜態頁
- 部署的時候,只需要部署靜態頁和其引用的資源
看到這個需求,我想大多數關注前端的同學可能跟我一樣,第一反應就是:應該找一個 SSR 框架,添加到現有項目中。那么,就選最出名的 Nuxt.js 吧,這個框架的作者還跟 Vue 的作者一起看 NBA 呢。
了解 Nuxt.js
Nuxt.js 的官方網站在此:https://nuxtjs.org,大家可以先看一下。有中文版。不過需要注意,我看的時候,中文翻譯並不全,而且不是一半中文一半英文的那種不全,是少了一大部分內容。后來通過 Google 搜索找到了需要的內容,我才發現中文翻譯有缺失,浪費我不少時間。所以我建議,大家盡量看英文,或者,先看一遍英文,再看中文。
言歸正傳,接下來,我們來了解下 Nuxt.js。
定位
相信大家都用 Vue 開發過不止一個項目,各種組件庫、工具庫也都用過不少。那么 Nuxt.js 在整個 Vue 生態里,大約是個什么定位呢?
讀罷官方文檔的介紹,我們明白,Nuxt.js,定位於在服務器端提供 UI 渲染的通用框架。它的應用層級高於 Vue,並不打算對 Vue 的功能進行補強,而是利用 Vue 的數據雙向綁定,提供更強的 B/S 架構中服務器(Server)一端的開發環境。換句話說,它就是 Node.js 版的 Laravel。而靜態頁發布,也就是 nuxt generate
功能,只是服務器端渲染衍生出的一個子功能。
坦率的說,這並不是一個好消息。看過前面產品設計的同學應該明白,我想要的,實際上是類似 Vue-Router 或者 Webpack 插件一樣的東西,我只要引用它,然后啟動一個開關,然后 npm run build
,就能生成我需要到的靜態頁,還有靜態需要的各種靜態資源。
但是在服務器端實現 Vue 模板渲染太過復雜,為了這樣簡單的目的,開發 Nuxt.js 這種規模的工具,實在太不划算。所以無論是 Next.js 也好,Nuxt.js 也好,他們的團隊都渴望更有挑戰性更有成長空間的項目,比如挑戰 Laravel。(偷偷說一句,Nuxt.js 的網站上廣告真多……)
作為一般用戶,我只能去適應他們。
結構
Nuxt.js 里面集成了 Vue 全家桶和 Webpack 全家桶。好消息是這些組件我都常用,應該不會遭遇什么困難。Nuxt.js 團隊還提供了腳手架創建工具,方便初始化項目。不過我的項目是現成的,所以作用不大,這個部分跳過不看。
直接在項目根目錄安裝 Nuxt.js:npm i nuxt -D
,完成之后可以使用 nuxt -h
查看 nuxt
命令的各項參數。nuxt
命令和 vue-cli-service
一樣,提供啟動開發環境、build 等功能。nuxt
使用 nuxt.config.js
作為配置文件,它的地位和 vue.config.js
一樣,包含了 Webpack 的默認配置,和一大堆私有配置。
所以,使用 Vue CLI 創建項目時,最好選擇把所有工具的配置分散在各自的配置文件里,比如 babel 就是 .babelrc,這樣復用起來更加方便。
Nuxt.js 有一套默認目錄結構,使用這套目錄結構可以最大限度的利用 Nuxt.js 的工作機制,不過對於我們來說,暫時排不上用場,也不需要關注。
nuxt generate
這就是最終我們要使用的命令,其實只能算作 Nuxt.js 的衍生功能。大家可以先看一下它的文檔:靜態應用部署以及generate 屬性配置,后者需要寫在 nuxt.config.js
里。
使用這個命令,會在指定的文件夾里生成預渲染文件。它的過程大概是這樣:
- nuxt 啟動一個本地服務
- 根據配置中的預渲染路徑,生成靜態頁面
- 把頁面保存成 .html 文件,其中引用的靜態資源也都保存下來
- 預渲染的頁面當中,包含完整的 JS 功能代碼
然后我們就可以把這些靜態資源整個部署到服務器上。
接下來,我們先來重構下老項目。
重構現有項目
理解了 Nuxt.js 的定位,就不難判斷,如果我們想集成 Nuxt.js 到現有項目,必須進行一些重構。
生命周期鈎子
服務器端渲染沒有掛載(amount
)DOM 的過程,所以自然也不支持 beforeMount
和 mounted
鈎子。如果你跟我一樣,習慣把初始化代碼寫在 beforeMount
里,那么可能有不少地方要修改。所以,以后如果有對時機要求不嚴的操作,最好放在 created
甚至 beforeCreated
里。
如果頁面需要異步加載數據,就需要用 asyncData
函數。這個函數的用法和鈎子函數類似,它需要返回 Promise 實例,真正的模板渲染會在這個 Promise 完成之后才開始。
不過需要注意,正常來說,靜態頁面里會包含完整的 JS,即包含完整的頁面邏輯,所以鈎子函數在瀏覽器里會照常執行。所以要避免同樣的代碼在 asyncData
和鈎子函數里重復運行。
入口
一般來說,如果使用 Vue CLI 創建項目,入口文件是 src/main.js
,這個文件會 import
src/App.vue
,並且 mount
到 #app
元素上。
我們當然可以繼續使用這個入口,不過考慮到靜態頁面的依賴跟 CMS 頁面的需求不同,我認為重新定義一個入口比較好。在本項目中,我把兩者都需要的依賴放在 src/App.vue
里,比如 Bootstrap 的樣式;只有 CMS 需要的依賴放在 src/main.js
里,比如 CMS 路由和 Vuex。然后在 nuxt.config.js
里重新配置路由,以 src/App.vue
作為入口。
head
網頁的頭信息,包括 <title>
,關鍵詞(keywords)、描述(description)對 SEO 非常重要。即使不考慮 SEO,只是方便用戶使用瀏覽器前進后退,顯示正確的 <title>
也是應該的。在 Vue 項目中,我們使用 vue-meta 滿足這個需求(vue-meta 也是 Nuxt.js 團隊開發的)。
但是在 Nuxt.js 生成的靜態頁面里,我們需要使用 head
屬性。它的用法和 vue-meta
基本一致,等下我會在具體配置里詳細介紹。
生成靜態頁的相關配置
接下來講解配置項。其實配置本身可說的部分不多,之所以鋪墊這么久,正是因為我踩了很多坑。原本打算一兩個小時搞定的事情,最后花費5、6個小時才摸索出來。后來我想,如果這些知識點我以前就知道,那該多好。所以才有了前面的內容。
好吧,言歸正傳,最終的配置如下:
const path = require('path'); const { promises: { copyFile, }, } = require('fs'); const {DefinePlugin} = require('webpack'); const pkg = require('./package'); const {POST_TABLE} = require('./src/model/Post'); module.exports = { // 用來輸出 `<head>` 里面的信息 head: { titleTemplate: '%s - Meathill', meta: [ {charset: 'utf-8'}, {name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no'}, ], }, // build 配置,其實就是封裝起來的 webpack 配置 build: { extend(config) { config.resolve.alias['@'] = path.resolve(__dirname, 'src'); }, extractCSS: true, plugins: [ new DefinePlugin({ VERSION: JSON.stringify(pkg.version), }), ], }, // 重新構建路由。因為靜態頁對 URL 的需求和 CMS 完全不同,所以這里我只針對靜態頁添加了簡單的路由設定。 // 前文說過,這里我用 `src/App.vue` 作為入口 router: { extendRoutes(routes, resolve) { routes.push({ name: 'home', path: '/', component: resolve(__dirname, 'src/App.vue'), children: [ { name: 'page.view', path: ':path', component: resolve(__dirname, 'src/modules/page/page.vue'), }, ], }); }, }, // 渲染配置,因為是純靜態頁,所以我選擇不注入業務邏輯(即 CMS 里的 JS) // 那么也就不需要 prefetch render: { injectScripts: false, resourceHints: false, }, // `nuxt generate` 的配置,只有只作用於生成的配置才寫在這里,所以其實是遠遠不夠的 generate: { dir: 'static', fallback: false, async routes() { const posts = await getAllPages(); return result.map(item => `/${item.get('permanentLink')}`); }, }, // 鈎子函數,復制文件,后面會解釋 hooks: { generate: { async done() { const from = path.resolve(__dirname, 'src/footer.js'); const to = path.resolve(__dirname, 'static/footer.js'); await copyFile(from, to); console.log('[CMS : Nuxt] Static files copied.'); }, }, }, };
這里需要介紹一下 generate
屬性,它的詳細文檔在這里:Nuxt.js > Configuration > The generate Property。它里面的信息是專門為 nuxt generate
准備的,但是只配置這個屬性是不夠的。
它的 routes
屬性很重要,里面是所有要渲染的靜態頁。如我的例子所示,這里也可以使用異步函數,動態獲取要渲染的頁面列表,然后逐個渲染。Nuxt 還提供了一個優化方案,可以批量渲染頁面,而不需要每次都訪問數據源,不過我暫時沒有用到。
添加 SEO 關鍵信息
SEO,中文全稱“搜索引擎優化”,英文全稱 Search Engeine Optimization,簡寫即 SEO。SEO 雖然名為“搜索引擎優化”,但其實優化的並不是搜索引擎,而是我們自己的頁面。目的是提升我們的網頁在搜索引擎中的排名。
要知道,流量是很貴的,最便宜的也要好幾塊/人,那些消費能力強、消費欲望高的用戶,一個可能要賣300+。所以對於老板來說,如果能夠通過 SEO,讓我們的網頁排到更靠前的位置,引來更多的用戶,那么就等於省去了很大一筆費用。所以顯而易見,SEO 對他們來說吸引力很大。
對於我們前端來說,SEO 的需求不可避免,那么如何做呢?這需要我們對搜索引擎的原理有一些理解。我簡單介紹一下:
- 搜索引擎會抓取所有頁面
- 然后搜索引擎會分析每個頁面,對內容進行分詞,對每個詞進行打分
- 最終得到“所有網頁”里“所有內容”的評分
- 當用戶輸入搜索關鍵詞的時候,尋找評分最高的頁面倒序顯示
如果想了解更詳細的搜索引擎知識,我推薦大家閱讀吳軍博士的《數學之美》,雖然我覺得他的其它作品水平不佳,但這本書科學內容比較多,還是蠻值得看的。
換言之,SEO,就是要讓我們的網頁針對某個關鍵詞,得分更高。一般來說,有以下工作:
<title>
里應該包含關鍵詞<meta name="keywords">
里應該包含關鍵詞- 頁面內的標題,即
<h1>
、<h2>
等,應該包含關鍵詞 - 圖片等元素的
alt
屬性,應該包含關鍵詞 - 其它正文中,應保持一些關鍵詞密度,比如每一段,每一百個字等,都要出現至少一次關鍵詞
具體到網站生成工作中,(3)(4)(5)通常都由運營/編輯/內容團隊負責,我們真正能做的,就是(1)(2),也就是接下來的組件使用。
組件
首先,在頁面組件里,添加 head
屬性,用來返回頭信息。在本次項目中,它必須是個函數,根據頁面數據動態返回值:
export default { head() { if (!this.meta) { return; } return { title: this.meta.title, meta: [ { vmid: 'keywords', name: 'keywords', content: this.meta.keywords, }, { vmid: 'description', name: 'description', content: this.meta.description, }, ], script: [ { body: true, defer: true, src: 'https://unpkg.com/swiper@4.5.0/dist/js/swiper.min.js', }, { body: true, defer: true, scr: '/footer.js', }, ], }; }, }
有些時候,我們會在 nuxt.config.js
里配置默認的 meta 信息,為了避免頁面的 meta 信息和默認 meta 信息重復出現,所以要用到 vmid
(在組件里) 和 hid
(在配置里)。這樣同樣 id 的頭信息就只出現一個,權重當然是頁面更高。
接下來 script
的部分,可以通過 body
屬性控制 <script>
插入的位置,默認為 false
,插入 <head>
。這里當然應該放在 </body>
之前。靜態網頁不需要 Vue 那些很復雜的交互,所以在上一章中,我通過 render
屬性把它們去掉了。但是有一些其它交互要添加進來,比如頭圖切換用 swiper,還有統計代碼。所以要插入一個 footer.js
進去。
這里需要注意,Nuxt.js 並不會調用 webpack 去處理這里的 JS,所以我們需要人工控制它們的路徑。下一章你會看到,我是直接復制文件到 static
文件夾的,所以它的路徑也就寫成固定的 /footer.js
。如果你有 publicPath
之類的需求,還要自己處理一下哦。
配置文件
配置文件里的內容上一章展示過:
module.exports = { head: { titleTemplate: '%s - Meathill', meta: [ {charset: 'utf-8'}, {name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no'}, ], }, }
好像沒什么可說的……我暫時只用到標題模板,比如一個頁面標題是“今天晚上吃什么?”,就會渲染成:“今天晚上吃什么? - Meathill”。其它選項大家可以參考 Vue Meta > API > metaInfo properties。
渲染靜態頁的時候,vue-meta 似乎不是必須的;換言之,我一開始用了 vue-meta,沒有配 head
,也沒有輸出需要的 meta 信息。
利用鈎子注入 JS
Nuxt.js 提供了很多鈎子,方便我們在特定的環節進行定制化操作。這些鈎子跟不同的操作綁定,接受不同的參數,返回不同的結果,具體鈎子列表請參考 Nuxt.js > API > Hooks > List of hooks。
在本項目中,我的需求是生成靜態頁面需要的 JS。這個 JS 包含兩個功能:
- 初始化 swiper
- 啟動統計代碼
因為功能非常簡單,我暫時不打算用 webpack 打包,只想復制到目標文件夾里。啟動復制操作的時機並不敏感,因為和主要的生成靜態頁工作不存在相互依賴的關系。不過 Nuxt.js 默認會清理生成目錄,所以我覺得晚一些復制會比較好,最后選擇 generate:done
這個鈎子(基本是最后才會執行)。代碼如下:
const { promises: { copyFile, }, } = require('fs'); module.exports = { hooks: { generate: { async done() { const from = path.resolve(__dirname, 'src/footer.js'); const to = path.resolve(__dirname, 'static/footer.js'); await copyFile(from, to); console.log('[CMS : Nuxt] Static files copied.'); }, }, }, }
這段代碼應該很容易看懂吧,就是一個簡單的復制。不過需要注意,copyFile
函數是 v10 之后添加到 Node.js 里的,而預 promise 化的 fs.promises
是 v12 之后添加的。
后記 & 附錄
先回顧一下本文的主要觀點:
- Nuxt.js 的目標是覆蓋完整的網站開發場景,這個場景更有前(錢)途
- 要達成這個目標,支持 Vue 模板的服務器渲染是必經之路
- 所以生成靜態頁只是 Nuxt.js 的一個衍生功能
- 所以在現有項目中集成 Nuxt.js ,渲染靜態頁的成本比較高,很多文章也提供類似的觀點
- 但是如果有必要,這仍然是最好操作的方案
接下來,關於技術選型:
- 如果是新項目,必須 SSR,那么建議從開始就用 Nuxt.js 創建項目
- 如果是老項目,部分頁面需要靜態化,請參考本文
最后,如果要在現有項目中解成 Nuxt.js,我們應該:
- 重構現有項目,一般要重構入口和路由
- 新建
nuxt.config.js
,添加基礎配置 - 配置
generate
屬性,生成所有要靜態化的路徑 - 如果不需要復雜的交互,可以用
render
屬性移除老的 JS,然后手動添加其它的。
希望可以幫大家節省學習嘗試踩坑的時間。
,
。