>初学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一下,发现值其实已经变了,只是失去了响应式;这就是为什么要用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到底该怎么配置。。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
暂无评论内容