vue ssr服务端渲染小白解惑
>初学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.js
const 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.0
export 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.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()
]
}
有关webpack配置不啰嗦
//webpack.server.config.js用来生成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)处理动态导入(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.js
import { 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.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}
}
把app实例和router都抛出去,给entry-server.js用
// entry-server.js
import { 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.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(),
]
})
这个地方重点除了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一下,发现值其实已经变了,
看看代码,是这里忘加return了;
>加入vuex
加个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)//关键
}
}
})
很基础的逻辑,关键在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;//一个项目不可能所有页面都服务端渲染,太耗性能,服务器压力大呀;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。