vue ssr服務(wù)端渲染(小白解惑)
>初學(xué)ssr入坑
初學(xué)vue服務(wù)端渲染疑惑非常多,我們大部分前端都是半路出家,上手都是前后端分離,對(duì)服務(wù)端并不了解,不說(shuō)java、php語(yǔ)言了,連node服務(wù)都還沒(méi)搞明白,理解服務(wù)端渲染還是有些困難的;
網(wǎng)上有非常多的vue服務(wù)渲染的入門案例,但看了很久,很多,還是一頭霧水,搞不明白這些文件和關(guān)鍵字的聯(lián)系和意思:
- server.js
- entrt-client.js
- server-js
- built-server-bundle.js
- vue-ssr-server-bundle.json
- vue-ssrclientmanifest.json
- createBundleRenderer
- clientManifest
這篇內(nèi)容會(huì)按照 基礎(chǔ)服務(wù)端渲染--vue實(shí)例渲染--加入vueRouter--加入vueX的順序入坑,后續(xù)應(yīng)該還有--開(kāi)發(fā)模式--seo優(yōu)化--部分渲染,這里先不挖那么多坑了;
>基礎(chǔ)服務(wù)端渲染
顧名思義,得啟個(gè)服務(wù):(建個(gè)新項(xiàng)目,不要用vue-cli)
//server.js const express = require('express'); const chalk = require('chalk');//加個(gè)chalk就是console好看點(diǎn)。。 const server = express(); server.get('*', (req, res) => { res.set('content-type', "text/html"); res.end(` <!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>你好</body> </html> `) }) server.listen(8080,function(){ let ip = getIPAdress(); console.log(`服務(wù)器開(kāi)在:http://${chalk.green(ip)}:${chalk.yellow(8080)}`) }) function getIPAdress(){//node下的os模塊可以拿到啟動(dòng)該文件的服務(wù)端的部分信息 var interfaces = require('os').networkInterfaces(); for (var devName in interfaces) { var iface = interfaces[devName]; for (var i = 0; i < iface.length; i++) { var alias = iface[i]; if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) { return alias.address; } } } }
啟動(dòng) node server.js
再看頁(yè)面 正常,這就是最基礎(chǔ)的服務(wù)端渲染
其實(shí)就是一個(gè)get請(qǐng)求,返回一個(gè)字符串,瀏覽器默認(rèn)展示返回結(jié)果;
然而對(duì)于這個(gè)字符串的解析還不明確,什么意思,比如:
去掉這句話,頁(yè)面就成了這樣,原因不深究,自己百度
>加入vue實(shí)例
跳過(guò)官網(wǎng)說(shuō)的built-server-bundle.js應(yīng)用,意思就是不用管這個(gè)文件了,只是一個(gè)過(guò)渡文件,項(xiàng)目中也不會(huì)用到。直接使用createBundleRenderer方法,直接用vue-ssr-server-bundle.json;
看下現(xiàn)在的目錄結(jié)構(gòu):
新增了5個(gè)文件;有關(guān)客戶端的配置entry-client.js不是必須的,這里先不管;
app.js是用來(lái)創(chuàng)建vue實(shí)例的;
entry-server.js是用來(lái)創(chuàng)建生成vue-ssr-server-bundle.json(需要用到app.js)所需的配置配件;是給webpack.server.config.js用的;
webpack.server.config.js是用來(lái)生成vue-ssr-server-bundle.json的;
vue-ssr-server-bundle.json是給server.js中的createBundleRenderer用的。
//app.js import Vue from 'vue' import Vue from './App.vue'//這里一定要寫上.vue,不然會(huì)匹配到app.js,require不區(qū)分大小寫0.0 export default createApp=function(){ return new Vue({ render:h => h(App) }) }
一個(gè)createApp生成一個(gè)vue實(shí)例;
//App.vue <template> <div id='app'> 這是個(gè)app </div> </template> <script> export default {} </script>
還沒(méi)用到<router-view>
//weback-base.config.js const path = require('path') const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { output:{ path:path.resolve(__dirname,'./dist'), filename:'build.js', }, module: { rules: [ { test:/\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } }, exclude:[/node_modules/,/assets/] }, { test:/\.vue$/, use:['vue-loader'] } ] }, resolve: { alias:{ '@':path.resolve(__dirname,'../') }, extensions:['.js','.vue','.json'] }, plugins:[ new VueLoaderPlugin() ] }
有關(guān)webpack配置不啰嗦
//webpack.server.config.js用來(lái)生成vue-ssr-server-bundle.json const merge = require('webpack-merge') const baseConfig = require('./webpack.base.js') const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') module.exports = merge(baseConfig, { entry: './entry-server.js', // 這允許 webpack 以 Node 適用方式(Node-appropriate fashion)處理動(dòng)態(tài)導(dǎo)入(dynamic import), // 并且還會(huì)在編譯 Vue 組件時(shí), // 告知 `vue-loader` 輸送面向服務(wù)器代碼(server-oriented code)。 target: 'node', // 對(duì) bundle renderer 提供 source map 支持 devtool: 'source-map', // 此處告知 server bundle 使用 Node 風(fēng)格導(dǎo)出模塊(Node-style exports) output: { libraryTarget: 'commonjs2' }, // 這是將服務(wù)器的整個(gè)輸出 // 構(gòu)建為單個(gè) JSON 文件的插件。 // 默認(rèn)文件名為 `vue-ssr-server-bundle.json` plugins: [ new VueSSRServerPlugin() ] })
這個(gè)配置哪都能找到,重點(diǎn)是VueSSRServerPlugin這個(gè)插件,生成vue-ssr-server-bundle.json全靠它,去掉的話生成的是built-server-bundle.js;關(guān)于merge插件,libraryTarget,target配置問(wèn)題自己百度webpack去0.0;
//entry-server.js import { createApp } from './src/app' export default context => { return createApp() }
固定寫法,返回一個(gè)函數(shù)供createBundleRenderer使用;
生成vue-ssr-server-bundle.json
到目前為止安裝的插件有:
自己手動(dòng)一個(gè)一個(gè)裝就行了。
生成vue-ssr-server-bundle.json,使用webpack命令
一切都手動(dòng),熟悉webpack;
修改server.js
const express = require('express'); const chalk = require('chalk'); const server = express(); const serverBundle = require('./dist/vue-ssr-server-bundle.json')//**新增**// const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle,{ runInNewContext: false, // 看名字也知道是生成某個(gè)新的Context對(duì)象,默認(rèn)是true,改成false理解為某種緩存機(jī)制,提高服務(wù)器效率 template: require('fs').readFileSync('./index.html', 'utf-8'), })//**新增**// server.get('*', (req, res) => { //res.set('content-type', "text/html"); //res.end(` //<!DOCTYPE html> //<html lang="en"> // <head><title>Hello</title></head> // <body > // <div style='color:red'>你好</div> // </body> // </html> //改成下面這樣 const context = {//這里的參數(shù)現(xiàn)在還沒(méi)用,但這個(gè)對(duì)象還是得用,要做renderToString的參數(shù) url:req.url } renderer.renderToString(context, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } else { res.end(html) } }) `) }) server.listen(8080,function(){ let ip = getIPAdress(); console.log(`服務(wù)器開(kāi)在:http://${chalk.green(ip)}:${chalk.yellow(8080)}`) }) function getIPAdress(){//node下的os模塊可以拿到啟動(dòng)該文件的服務(wù)端的部分信息,細(xì)節(jié)自己去node上面查 var interfaces = require('os').networkInterfaces(); for (var devName in interfaces) { var iface = interfaces[devName]; for (var i = 0; i < iface.length; i++) { var alias = iface[i]; if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) { return alias.address; } } } }
試一蛤:node server.js
正常,箭頭指的地方官網(wǎng)有解釋。別忘了inde.html中加入一行注釋:
后續(xù)修改title,meta頭部都是通過(guò)類似的注釋方式,原理就是正則匹配替換字符串-。-;
>加入路由vue-router
新增幾個(gè)文件
需要修改的文件有:
App.vue//加個(gè)router-view就行
//app.js import Vue from 'vue' import App from './App.vue' import router from './router' export function createApp(){ const app = new Vue({ router, render:h => h(App) }) return {app,router} }
把a(bǔ)pp實(shí)例和router都拋出去,給entry-server.js用
// entry-server.js import { createApp } from './src/app' export default context => { //這里用promise的原因有很多,其中有一個(gè)就是下面這個(gè)onReady方法是異步的。createBundleRenderer支持promise return new Promise((resolve, reject) => { const { app, router } = createApp() router.push(context.url) router.onReady(() => {//onReady方法還有g(shù)etMatchedComponents方法還是需要了解一下 const matchedComponents = router.getMatchedComponents() if (!matchedComponents.length) { return reject({ code: 404 }) } resolve(app) }, reject) }) }
最后看一下router.js
//router.js import Vue from 'vue' import VueRouter from 'vue-router' //頁(yè)面要先聲明后使用,不要問(wèn)為什么 import home from './pages/home' import store from './pages/store' Vue.use(VueRouter) export default new VueRouter({ mode: 'history', routes:[ {path:'/',name:'home',component:home}, {path:'/store',name:'store',component:store}, ] })
再看一下兩個(gè)頁(yè)面的代碼;
//store.vue <template> <div>this is store</div> </template> <script> export default {} </script>
改的差不多了,試一哈:
重新打個(gè)包webpack --config webpack.server.js
啟動(dòng)node server
>entry-client.js是干啥的
到目前為止還沒(méi)用到entry-client.js叫客戶端配置,不著急使用,先做個(gè)測(cè)試,寫點(diǎn)邏輯試試:
修改下store.vue
//store.vue <template> <div @click='run'>{{msg}}</div> </template> <script> export default { data(){ msg:'this is store' }, created(){ this.msg = 'this is created' }, mounted(){ this.msg = 'this is mounted' }, methods: { run(){ alert('this is methods') } } } </script>
看這個(gè)樣子頁(yè)面最終展示的結(jié)果應(yīng)該是this is mounted,然而結(jié)果是這樣的:
很好解釋,服務(wù)端對(duì)于鉤子函數(shù)的理解也是很正確的,created會(huì)在頁(yè)面返回之前執(zhí)行,而mounted是在vue實(shí)例成型之后執(zhí)行,就是頁(yè)面渲染后,這個(gè)是要在客戶端才會(huì)執(zhí)行,可是為什么頁(yè)面出來(lái)了沒(méi)有執(zhí)行mounted,而且run的點(diǎn)擊事件沒(méi)有生效;
看看頁(yè)面:
一個(gè)js文件都沒(méi)加載,怎么執(zhí)行邏輯,就是個(gè)靜態(tài)頁(yè)面0.0;
這時(shí)候entry-client.js就出場(chǎng)了
新增兩個(gè)文件
//entry-client.js import { createApp } from './src/app.js'; const { app } = createApp(); app.$mount('#app');
基本配置;
//webpack.client.config.js const merge = require('webpack-merge') const baseConfig = require('./webpack.base.config.js') const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') module.exports = merge(baseConfig, { entry: './entry-client.js', optimization:{ runtimeChunk:true }, plugins: [ // 此插件在輸出目錄中 // 生成 `vue-ssr-client-manifest.json`。 new VueSSRClientPlugin(), ] })
這個(gè)地方重點(diǎn)除了VueSSRClientPlugin生成vue-ssr-client-manifest.json外,optimization是webpack4產(chǎn)物,用來(lái)分離生成共公chunk,配置還算復(fù)雜,可以看下這里webpack4 optimization總結(jié)
修改下server.js
//server.js const express = require('express'); const chalk = require('chalk'); const server = express(); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json')//新增 const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle,{ runInNewContext: false, // 推薦 template: require('fs').readFileSync('./index.html', 'utf-8'), clientManifest // //新增 }) server.get('*', (req, res) => { res.set('content-type', "text/html"); const context = { url:req.url } renderer.renderToString(context, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } else { res.end(html) } }) }) server.listen(8080,function(){ let ip = getIPAdress(); console.log(`服務(wù)器開(kāi)在:http://${chalk.green(ip)}:${chalk.yellow(8080)}`) }) function getIPAdress(){//node下的os模塊可以拿到啟動(dòng)該文件的服務(wù)端的部分信息,細(xì)節(jié)自己去node上面查 var interfaces = require('os').networkInterfaces(); for (var devName in interfaces) { var iface = interfaces[devName]; for (var i = 0; i < iface.length; i++) { var alias = iface[i]; if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) { return alias.address; } } } }
打包下:webpack --config webpack.client.config.js
node server 一下,看看頁(yè)面
js有了,可是為什么還不行,不能點(diǎn)0.0;
看看。奧報(bào)錯(cuò)了
讀取不到靜態(tài)文件;
修改server.js加個(gè)靜態(tài)文件托管:
再看看
事件也有了,頁(yè)面沒(méi)變化,console一下,發(fā)現(xiàn)值其實(shí)已經(jīng)變了,只是失去了響應(yīng)式;這就是為什么要用vuex的緣故;
>加入vuex
開(kāi)始想在頁(yè)面中用this.$set方法,然而行不通,而且不可能給每個(gè)值都重新寫一個(gè)這個(gè)方法;
加個(gè)sotre.js
// store.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { msg: '' }, actions: { setMsg ({ commit }, val) { commit('setMsg', val) } }, mutations: { setMsg (state, val) { Vue.set(state, 'msg', val)//關(guān)鍵 } } })
很基礎(chǔ)的邏輯,關(guān)鍵在Vue.set這個(gè)方法,重新增加了響應(yīng)式;
修改下app.js
//app.js import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store'//加個(gè)store就行了 export function createApp(){ const app = new Vue({ router, store, render:h => h(App) }) return {app,router} }
store.vue改成這樣
<template> <div @click='run'>{{msg}}</div> </template> <script> export default { data(){}, created(){ this.$store.dispatch('setMsg','this is created') }, computed:{ msg(){ return this.$store.state.msg; } }, mounted(){ this.$store.dispatch('setMsg','this is mounted') }, methods: { run(){ alert('this is methods') } } } </script>
重新打個(gè)包,想一下,修改頁(yè)面的話只需要重新打包c(diǎn)lient,如果修改了app.js兩個(gè)就要都重新打包了;
node server 一下
這回總算完成了;
>總結(jié)
服務(wù)端渲染東西還是挺多的,涉及領(lǐng)域也非常廣,比如vue,webpack,node,它們的生態(tài)圈都大的可怕,需要學(xué)習(xí)東西非常多,
坑又多,又大,又深,后面還有很多問(wèn)題要解決:
異步數(shù)據(jù)加載;//html返回前先渲染一部分接口拿到的數(shù)據(jù) 怎么做seo優(yōu)化;//做服務(wù)端渲染的重要原因,處理異步數(shù)據(jù)加載問(wèn)題也是為了這個(gè) 緩存怎么加; 開(kāi)發(fā)環(huán)境搭建;//你并不希望每改一行代碼就重新手動(dòng)打個(gè)包,重啟下服務(wù)吧0.0 還有怎么實(shí)現(xiàn)部分頁(yè)面ssr;//一個(gè)項(xiàng)目不可能所有頁(yè)面都服務(wù)端渲染,太耗性能,服務(wù)器壓力大呀;
還有很多疑惑:
比如為什么會(huì)失去響應(yīng)式,webpack到底該怎么配置。。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于this.$refs獲取不到dom的可能原因及解決方法
這篇文章主要介紹了關(guān)于this.$refs獲取不到dom的可能原因及解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11vite項(xiàng)目配置less全局樣式的實(shí)現(xiàn)步驟
最近想實(shí)現(xiàn)個(gè)項(xiàng)目,需要配置全局less,本文主要介紹了vite項(xiàng)目配置less全局樣式的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-02-02vue項(xiàng)目中created()被調(diào)用多次的踩坑實(shí)戰(zhàn)
在vue項(xiàng)目中我在created中調(diào)用了兩次get數(shù)據(jù)請(qǐng)求,所以下面這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目中created()被調(diào)用多次的踩坑實(shí)戰(zhàn),需要的朋友可以參考下2023-03-03vue使用自定義指令實(shí)現(xiàn)按鈕權(quán)限展示功能
這篇文章主要介紹了vue中使用自定義指令實(shí)現(xiàn)按鈕權(quán)限展示功能,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04vue項(xiàng)目之index.html如何引入JS文件
這篇文章主要介紹了vue項(xiàng)目之index.html如何引入JS文件問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12Vue使用MD5對(duì)前后端進(jìn)行加密的實(shí)現(xiàn)
前后端分離的項(xiàng)目,遇到了對(duì)密碼進(jìn)行加密的情況,在前端或者是在后端加密都是可以的,本文主要介紹了Vue使用MD5對(duì)前后端進(jìn)行加密的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04解決Vue使用mint-ui loadmore實(shí)現(xiàn)上拉加載與下拉刷新出現(xiàn)一個(gè)頁(yè)面使用多個(gè)上拉加載后沖突問(wèn)題
這篇文章主要介紹了解決Vue使用mint-ui loadmore實(shí)現(xiàn)上拉加載與下拉刷新出現(xiàn)一個(gè)頁(yè)面使用多個(gè)上拉加載后沖突問(wèn)題,需要的朋友可以參考下2017-11-11淺談Vue.js之初始化el以及數(shù)據(jù)的綁定說(shuō)明
今天小編就為大家分享一篇淺談Vue.js之初始化el以及數(shù)據(jù)的綁定說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11