git clone https://github.com/WdBly/my-blog-WdBly.git
cd server && composer install 配置.env文件 包含了SESSION_DOMAIN APP_KEY DATABASE的相關信息 配置目錄權限 配置nginx 參考下方配置
cd web && npm install npm run build-client && npm run build-server
node ssr
npm install pm2 -g pm2 start ssr
幾個問題:使用服務端渲染解決了什麼問題?,技術上如何實現? 通過服務端渲染改造的項目和改造前的單頁的區別?css
場景:已有基於vue-cli的單頁博客項目,前端使用 vue+vue-router+vuex+axios+elementui+webpack,後臺使用laravel + mysql
,服務器阿里雲 Ubuntu 16.04,web服務器nginx。html
面臨的問題。1:單頁應用首屏加載過慢;2:沒法被搜索引擎抓取;3:首屏白屏時間過長(重要);前端
解決一:首屏加載過慢。
通過分析,頁面首屏慢主要是首次須要加載的js文件過大。vue
1:對webpack打包過程進行優化,採用多入口將項目的vender依賴分割,對不須要變更的文件作緩存,同時對路由作異步加載。java
//多個入口 entry: { "app": path.join(projectRoot, 'entry-client.js'), "vendor": ['vue', 'vue-router', 'vuex', 'element-ui'] } //對多入口的文件 單獨打包 而且不開啓hash保證緩存 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'client/[name].bundle.js', }) //js壓縮 縮減文件大小 new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false } }, sourceMap: true, parallel: true }) //異步的路由 使得首屏加載的代碼儘可能小 const Home = ()=> import('@/components/Home.vue');
2:服務端nginx開啓gzip壓縮node
gzip on; gzip_disable "msie6"; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
通過驗證 開啓gzip壓縮的js文件大小大概能縮減爲源文件的1/5
經過前面兩部的優化,首屏加載快了很多,但仍是有點慢。mysql
解決二三:SEO和白屏的處理webpack
首先咱們來捋一捋實現流程,傳統的單頁應用的流程爲前端將文件打包後生成了index.html文件和其餘依賴文件,index.html文件中引入了一些js文件和css文件。以下:ios
<!DOCTYPE html> <html> <head> <meta charset=utf-8> <meta http-equiv=X-UA-Compatible content="IE=edge"> <link href=b12acbf1c.css rel=stylesheet> </head> <body> <div id=app></div> <script type=text/javascript src=982.js></script> </body> </html>
nginx中配置相應的server_name和root字段,兩個路由分別對應前端頁面和後臺接口。
//前端頁面的路由 server { root /www/wwwroot/myblog-WdBly/web/dist server_name www.wddsss.com ... } //後臺接口路由 server { root /www/wwwroot/myblog-WdBly/server/public server_name api.wddsss.com ... }
用戶訪問前端路由www.wddsss.com時 會返回dist目錄下的index.html文件給瀏覽器。剩下的全部工做都由瀏覽器完成。
如今咱們須要在nginx服務端和瀏覽器之間開啓一個node中間層用於服務端渲染。
理想狀態是 當用戶訪問 www.wddsss.com時,nginx經過proxy_pass將訪問流量代理到node中間層監聽的端口,而不是直接返回一個index.html文件,nginx將後面的返回頁面的工做交給了node。nginx代理配置:
//node監聽了 5006端口,注意咱們並不對外部暴露5006端口,也就是說經過 //www.wddsss.com:5006的訪問是會失敗的。 upstream z.com { server 127.0.0.1:5006; } //proxy_pass 將訪問轉移到 127.0.0.1:5006 server { server_name www.wddsss.com; access_log /var/log/nginx/blog.api.access.log; error_log /var/log/nginx/blog.api.error.log; location / { proxy_pass http://z.com; } }
如今的任務清晰了很多,咱們須要開啓一個node服務,監聽一個端口,當有用戶訪問的時候完成一大波事情,最後須要返回一個充滿數據的html文件。
那麼咱們開始實現這個任務吧!,首先分析,用戶訪問www.wddsss.com/app/home這個路由時,咱們在node中監聽到訪問,必然須要將此路由對應的組件,以及組件中須要的數據獲取並整合造成html文件。
vue-server-renderer 提供了一個renderToString方法,此方法接受一個Vue組件,返回一段對應的html代碼。這不就解決了咱們的問題嘛。
從新整理思路,node在監聽到某個路由被訪問時,會去查找前端路由表,並找到對應的組件。對於某些須要ssr的組件,咱們手動爲其添加了一個asyncData()方法,在node加載這些組件同時會去執行asyncData()方法,拿到組件內的數據渲染到組件中。最後將這個組件傳入renderToString方法。這樣一個簡陋無比的ssr就作好了!部分代碼以下(刪減版,完整的請前往github查看)
const { createRenderer } = require('vue-server-renderer') const createApp = require('./dist/bundle.server.js')['default'] const renderer = createRenderer({ template: require('fs').readFileSync('ssr/view/index.template.html', 'utf-8') }) const data = { script: ` <script src="/client/vendor.bundle.js"></script> <script src="/client/app.client.js"></script> `, state: `` } express.get('*', (req, res) => { const context = {url: req.url}; createApp(context).then(app => { var state = JSON.stringify(context.state); data.state = `<script>window.__INITIAL_STATE__ = ${state}</script>` renderer.renderToString(app, data, (err, html) => { res.end(html) }) }) })
1:咱們能夠看到createRenderer方法能夠接受一個html模板文件,由於renderToString方法最終生成的html片斷時不帶head頭等內容的,須要咱們自定義一個模板,將renderToString方法生成的html插入到次模板html文件便可。
2:createApp變量來自一個bundle.server.js,這個js文件是經過特定的配置對項目打包後生成的入口文件,它接受一個context用於根據路由尋找對應組件並將最終的state添加至context,後面會說到。
3:createApp自己是一個異步過程,由於在這個函數中可能會存在數據的獲取,當數據獲取且組件加載完畢後,執行then中的renderToString,renderToString的第二個data參數便是傳入模板html文件的參數。
webpack.server.js部分配置
target: 'node', entry: [path.join(projectRoot, 'entry-server.js')], output: { libraryTarget: 'commonjs2', path: path.join(projectRoot, 'dist'), filename: 'bundle.server.js', chunkFilename: '[name].bundle.js', publicPath: "/" }
這個比較簡陋的ssr存在不少問題,主要是客戶端拿到的只是一個單獨的html頁面其中,咱們綁定的事件,通通是不生效的。
正確的流程是當客戶端拿到首屏渲染好的頁面時,會在瀏覽器後臺執行一次重繪,生成一系列的虛擬Node,而且和從服務端獲取的真實dom節點進行比對,如果不匹配,會執行重繪(使用瀏覽器端生成的頁面),而瀏覽器在後臺生成虛擬Node依賴於頁面中的數據,而咱們又不可能在瀏覽器再次發送ajax請求來獲取頁面數據(浪費),全部在上方代碼中咱們能夠看到window.__INITIAL_STATE__這一句,當服務端獲取到組件的數據時,會將state放在script標籤的一個變量中,在客戶端執行重繪時採用的便是這裏的數據。
具體過程參考下圖:
在整個過程當中產生的一些問題總結
在 app.js 中咱們引入了 'element-ui/lib/theme-chalk/index.css'的css文件,咱們必需要清楚 app.js自己會在服務端執行,因此咱們必須在webpack.server.js中配置處理css文件的loader
{ test:/\.css$/, use:['vue-style-loader', 'css-loader'], }
由於咱們的項目是要在服務端執行,同時也會在客戶端執行,到時服務端不支持某些客戶端對象 如window對象,因此在咱們的代碼中若是有使用到window,document等瀏覽器API的地方須要對當前的執行環境進行判斷
咱們使用的方式是webpack的插件
new webpack.DefinePlugin({ 'process.env.VUE_ENV': '"client"', 'process.env.NODE_ENV': '"production"', }), new webpack.DefinePlugin({ 'process.env.VUE_ENV': '"server"', 'process.env.NODE_ENV': '"production"', }),
對於某些頁面,咱們可能會在頁面中顯示當前登錄用戶的信息,並將這個信息存入了localStorage中,但結合上個問題咱們能夠看出來,在服務端渲染中咱們並不能獲取到這個localStorage這個對象。那麼最終渲染出的頁面在和瀏覽器重繪的頁面進行對比時必然會出現不匹配的錯誤。
處理方法,在node層爲登錄用戶設置cookie,當用戶請求時,如果判斷出當前的執行環境爲node,則從cookie中讀取信息載入頁面,不然從localStorage讀取數據。
除了上述狀況可能致使兩個渲染不匹配,還有從服務端返給客戶端的__INITIAL_STATE__不存在或者__INITIAL_STATE__的內容有誤時,都會致使客戶端獲取不到初始__INITIAL_STATE__而發生不匹配的錯誤(這裏__INITIAL_STATE__不存在的狀況有不少種),
1:如果直接將 <script>window.__INITIAL_STATE__ = ${state}</script>作爲參數傳入index.template.html,那麼須要使用{{{ }}}的語法解析。
2:在index.template.html 引入state的標籤須要在引入build.client.js的標籤以前引入,由於build.client.js須要依賴初始state。
3:如果state中存在標籤(好比mackdown語法生成的dom結構)須要使用不轉義插值{{{}}}。
4: 如果state中有某些特殊字符 :: 回車等特殊字符,須要使用{{}}進行轉移,不然在渲染頁面時這部分state會直接跑到頁面上去。
laravel設置的cookie配置 .env 文件 SESSION_DOMAIN=.wddsss.com
這樣咱們的node層(www.wddsss.com)才能獲取到laravel的cookie,並在客戶端將登錄時將cookie保存下來,同時返回給客戶端。這樣用戶在刷新頁面時,node會經過axios並帶上cookie請求nginx服務器獲取數據。
咱們的首頁圖片加載能夠明細看到很慢(畢竟1M的小服務器),開啓七牛cdn,具體流程就不說了,使用的是laravel itbdw/laravel-storage-qiniu包。
到這個時候咱們發現網站被標記爲不安全了,https走一波,推薦一個免費證書申請機構。