vue ssr服务端渲染(小白解惑)
liubingyang 人气:1>初学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,'.https://img.qb5200.com/download-x/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('.https://img.qb5200.com/download-x/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('.https://img.qb5200.com/download-x/dist/vue-ssr-server-bundle.json') const clientManifest = require('.https://img.qb5200.com/download-x/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.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;//一个项目不可能所有页面都服务端渲染,太耗性能,服务器压力大呀;
还有很多疑惑:
比如为什么会失去响应式,webpack到底该怎么配置。。
加载全部内容