vue ssr服务端渲染(小白解惑)

时间:2021-05-26

>初学ssr入坑

初学vue服务端渲染疑惑非常多,我们大部分前端都是半路出家,上手都是前后端分离,对服务端并不了解,不说java、php语言了,连node服务都还没搞明白,理解服务端渲染还是有些困难的;

网上有非常多的vue服务渲染的入门案例,但看了很久,很多,还是一头雾水,搞不明白这些文件和关键字的联系和意思:

  • server.js
  • entrt-client.js
  • server-js
  • built-server-bundle.js
  • vue-ssr-server-bundle.json
  • vue-ssrclientmanifest.json
  • createBundleRenderer
  • clientManifest

这篇内容会按照 基础服务端渲染--vue实例渲染--加入vueRouter--加入vueX的顺序入坑,后续应该还有--开发模式--seo优化--部分渲染,这里先不挖那么多坑了;

>基础服务端渲染

顾名思义,得启个服务:(建个新项目,不要用vue-cli)

//server.jsconst express = require('express');const chalk = require('chalk');//加个chalk就是console好看点。。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(`服务器开在:http://${chalk.green(ip)}:${chalk.yellow(8080)}`)})function getIPAdress(){//node下的os模块可以拿到启动该文件的服务端的部分信息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

再看页面 正常,这就是最基础的服务端渲染


其实就是一个get请求,返回一个字符串,浏览器默认展示返回结果;

然而对于这个字符串的解析还不明确,什么意思,比如:

去掉这句话,页面就成了这样,原因不深究,自己百度

>加入vue实例

跳过官网说的built-server-bundle.js应用,意思就是不用管这个文件了,只是一个过渡文件,项目中也不会用到。直接使用createBundleRenderer方法,直接用vue-ssr-server-bundle.json;

看下现在的目录结构:


新增了5个文件;有关客户端的配置entry-client.js不是必须的,这里先不管;

app.js是用来创建vue实例的;

entry-server.js是用来创建生成vue-ssr-server-bundle.json(需要用到app.js)所需的配置配件;是给webpack.server.config.js用的;

webpack.server.config.js是用来生成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,不然会匹配到app.js,require不区分大小写0.0export default createApp=function(){return new Vue({ render:h => h(App)})}

一个createApp生成一个vue实例;

//App.vue<template><div id='app'> 这是个app</div></template><script>export default {}</script>

还没用到<router-view>

//weback-base.config.jsconst 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()]}

有关webpack配置不啰嗦

//webpack.server.config.js用来生成vue-ssr-server-bundle.jsonconst 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)处理动态导入(dynamic import), // 并且还会在编译 Vue 组件时, // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 target: 'node', // 对 bundle renderer 提供 source map 支持 devtool: 'source-map', // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) output: { libraryTarget: 'commonjs2' }, // 这是将服务器的整个输出 // 构建为单个 JSON 文件的插件。 // 默认文件名为 `vue-ssr-server-bundle.json` plugins: [ new VueSSRServerPlugin() ]})

这个配置哪都能找到,重点是VueSSRServerPlugin这个插件,生成vue-ssr-server-bundle.json全靠它,去掉的话生成的是built-server-bundle.js;关于merge插件,libraryTarget,target配置问题自己百度webpack去0.0;

//entry-server.jsimport { createApp } from './src/app'export default context => { return createApp()}

固定写法,返回一个函数供createBundleRenderer使用;

生成vue-ssr-server-bundle.json

到目前为止安装的插件有:

自己手动一个一个装就行了。

生成vue-ssr-server-bundle.json,使用webpack命令

一切都手动,熟悉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, // 看名字也知道是生成某个新的Context对象,默认是true,改成false理解为某种缓存机制,提高服务器效率 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 = {//这里的参数现在还没用,但这个对象还是得用,要做renderToString的参数 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(`服务器开在:http://${chalk.green(ip)}:${chalk.yellow(8080)}`)})function getIPAdress(){//node下的os模块可以拿到启动该文件的服务端的部分信息,细节自己去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

正常,箭头指的地方官网有解释。别忘了inde.html中加入一行注释:

后续修改title,meta头部都是通过类似的注释方式,原理就是正则匹配替换字符串-。-;

>加入路由vue-router

新增几个文件


需要修改的文件有:

App.vue//加个router-view就行

//app.jsimport 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}}

把app实例和router都抛出去,给entry-server.js用

// entry-server.jsimport { createApp } from './src/app'export default context => { //这里用promise的原因有很多,其中有一个就是下面这个onReady方法是异步的。createBundleRenderer支持promise return new Promise((resolve, reject) => { const { app, router } = createApp() router.push(context.url) router.onReady(() => {//onReady方法还有getMatchedComponents方法还是需要了解一下 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'//页面要先声明后使用,不要问为什么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}, ]})

再看一下两个页面的代码;

//store.vue <template> <div>this is store</div> </template> <script> export default {} </script>

改的差不多了,试一哈:

重新打个包webpack --config webpack.server.js

启动node server

>entry-client.js是干啥的

到目前为止还没用到entry-client.js叫客户端配置,不着急使用,先做个测试,写点逻辑试试:
修改下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>

看这个样子页面最终展示的结果应该是this is mounted,然而结果是这样的:


很好解释,服务端对于钩子函数的理解也是很正确的,created会在页面返回之前执行,而mounted是在vue实例成型之后执行,就是页面渲染后,这个是要在客户端才会执行,可是为什么页面出来了没有执行mounted,而且run的点击事件没有生效;

看看页面:


一个js文件都没加载,怎么执行逻辑,就是个静态页面0.0;

这时候entry-client.js就出场了


新增两个文件

//entry-client.js import { createApp } from './src/app.js';const { app } = createApp();app.$mount('#app');

基本配置;

//webpack.client.config.jsconst 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(), ]})

这个地方重点除了VueSSRClientPlugin生成vue-ssr-client-manifest.json外,optimization是webpack4产物,用来分离生成共公chunk,配置还算复杂,可以看下这里webpack4 optimization总结

修改下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(`服务器开在:http://${chalk.green(ip)}:${chalk.yellow(8080)}`) }) function getIPAdress(){//node下的os模块可以拿到启动该文件的服务端的部分信息,细节自己去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 一下,看看页面


js有了,可是为什么还不行,不能点0.0;

看看。奥报错了


读取不到静态文件;

修改server.js加个静态文件托管:


再看看


事件也有了,页面没变化,console一下,发现值其实已经变了,只是失去了响应式;这就是为什么要用vuex的缘故;

>加入vuex

开始想在页面中用this.$set方法,然而行不通,而且不可能给每个值都重新写一个这个方法;


加个sotre.js

// store.jsimport 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)//关键 } } })

很基础的逻辑,关键在Vue.set这个方法,重新增加了响应式;
修改下app.js

//app.js import Vue from 'vue'import App from './App.vue'import router from './router'import store from './store'//加个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>

重新打个包,想一下,修改页面的话只需要重新打包client,如果修改了app.js两个就要都重新打包了;

node server 一下


这回总算完成了;

>总结

服务端渲染东西还是挺多的,涉及领域也非常广,比如vue,webpack,node,它们的生态圈都大的可怕,需要学习东西非常多,
坑又多,又大,又深,后面还有很多问题要解决:

异步数据加载;//html返回前先渲染一部分接口拿到的数据怎么做seo优化;//做服务端渲染的重要原因,处理异步数据加载问题也是为了这个缓存怎么加;开发环境搭建;//你并不希望每改一行代码就重新手动打个包,重启下服务吧0.0还有怎么实现部分页面ssr;//一个项目不可能所有页面都服务端渲染,太耗性能,服务器压力大呀;

还有很多疑惑:

比如为什么会失去响应式,webpack到底该怎么配置。。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。

相关文章