当前位置: 首页 > 后端技术 > Node.js

vue-ssr 手写服务端渲染(集成路由,vuex)

时间:2023-04-03 15:09:32 Node.js

1. 介绍构建过程如上图所示,使用webpack利用我们配置不同的入口生成服务端和客户端的bundle,服务端的bundle是用来生成html字符串,客户端bundle是用来注入到服务端生成的html字符串中的,由于服务端返回的是字符串,一系列的事件需要依赖客户端打包的js代码(客户端的js + 服务端渲染的字符串)由浏览器渲染这样就完成了一个ssr的构建优点1.利于seo优化在浏览器渲染的时候当我们查看源代码只能看到一个<div id='app'></div> 内容是由js生成,这样不利于爬虫所爬取到,服务端渲染是将解析过程放到了服务端来做,服务端将解析好的字符串传给前端,当查看源代码时就会显示解析后的元素,爬虫更容易被检索2.解决首页白屏的效果如果数据量比较大那么浏览器会卡顿处于白屏状态,使用服务端渲染直接将解析好的HTML字符串传递给浏览器,大大加快了首屏加载时间缺点1.占用内存所有的渲染逻辑都在服务端进行的,那么会占用更多的CPU和内存资源,当请求过多时不停的解析页面返回给客户端,会导致卡顿效果2.浏览器Api不能使用由于页面在服务端渲染那么服务端是不能调用浏览器的api的3.生命周期由于服务器端不知道什么时候挂载完成,在vue中只支持beforeCreated和created两个生命周期2. 开发前配置1.安装依赖包cnpm i webpack webpack-cli webpack-dev-server koa koa-router koa-static vue vue-router vuex vue-server-renderer vue-loader vue-style-loader css-loader html-webpack-plugin @babel/core @babel/preset-env babel-loader vue-template-compiler webpack-merge url-loader2.认识目录3.基础代码App.vue<template> <!-- id="app" 客户端激活,服务端解析成字符串返回给客户端,使其变为由 Vue 管理的动态 DOM 的过程 --> <div id="app"> <Bar></Bar> <Foo></Foo> </div></template><script>import Bar from "./components/Bar";import Foo from "./components/Foo";export default { components: { Bar, Foo, },};</script>Bar.vue<template> <div id="bar"> Bar </div></template><style scoped>#bar { background: red;}</style>Foo.vue<template> <div> Foo <button @click="clickMe">点击</button> </div></template><script>export default { methods: { clickMe() { alert("点我"); }, },};</script>public/server.html<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <!--vue-ssr-outlet--> </body></html>main.jsNode.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。所以我们需要保证每次访问都会产生一个新的Vue实例,暴露一个函数每次调用都保证是新的根实例import Vue from 'vue'import App from './App'export default () => { let app = new Vue({ el: '#app', render: h => h(App) }) return { app }}client-entry.js客户端正常挂载import createApp from './main'let { app } = createApp()app.$mount('#app')server-entry.jsimport createApp from './main'export default () => { let { app } = createApp(); return app}集成路由增加router.js文件import Vue from 'vue'import VueRouter from 'vue-router'import Foo from './components/Foo.vue'Vue.use(VueRouter)export default () => { const router = new VueRouter({ mode: "history", routes: [ { path: "/", component: Foo }, { path: "/bar", component: () => import("./components/Bar.vue") } ] }); return router;}main.jsimport Vue from 'vue'import App from './App'import createRouter from './router'export default () => { let router = createRouter() let app = new Vue({ el: '#app', router, render: h => h(App) }) return { app, router }}App.vue<template> <div id="app"> <router-link to="/">foo</router-link> <router-link to="/bar">bar</router-link> <router-view></router-view> </div></template>server-entry.jsimport createApp from './main'// 服务端需要调用当前这个文件 产生一个vue的实例export default (context) => { // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, //以便服务器能够等待所有的内容在渲染前,就已经准备就绪。 return new Promise((resolve, reject) => { let { app, router } = createApp(); //返回的实例应该跳转到/ 或者/bar context.url是服务端跳转的默认路径 router.push(context.url) // 涉及到异步组件的问题 router.onReady(() => { //获取当前跳转到的匹配组件 let matchs = router.getMatchedComponents() //matchs匹配到的所有的组件,整个都在服务端执行 if (matchs.length == 0) { reject({ code: 404 }) } resolve(app) }, reject) })}集成vuex增加store文件import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)export default () => { let store = new Vuex.Store({ state: { name: '' }, mutations: { changeName(state) { state.name = 'myh' } }, actions: { changeName({ commit }) { return new Promise((resolve, reject) => { setTimeout(() => { commit('changeName'); resolve() }, 1000) }) } } }) // 如果浏览器执行时 我需要将服务器设置的最新状态替换成客户端的状态,设置到window上的操作是server-entry.js下的操作 if (typeof window !== 'undefined' && window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } return store}main.jsimport Vue from 'vue'import App from './App'import createRouter from './router'import createStore from './store'export default () => { let router = createRouter() let store = createStore() let app = new Vue({ el: '#app', router, store, render: h => h(App) }) return { app, router, store }}Foo.vue<template> <div> Foo <button @click="clickMe">点击</button> {{ $store.state.name }} </div></template><script>export default { asyncData(store) { //asyncData只在服务端执行 只在页面组件中执行 return store.dispatch("changeName"); }, methods: { clickMe() { alert("点我"); }, },};</script>server-entry.jsimport createApp from './main'export default (context) => { return new Promise((resolve, reject) => { let { app, router, store } = createApp(); router.push(context.url) router.onReady(() => {//获取到匹配到的所有路径 let matchs = router.getMatchedComponents() if (matchs.length == 0) { reject({ code: 404 }) }//如果匹配到的组件中有asyncData 默认执行 Promise.all(matchs.map(v => { if (v.asyncData) { // asyncData是在服务端调用的 return v.asyncData(store) } })).then(() => { // 以上all中的方法会改变store中的state context.state = store.state;//把vuex的状态挂载到上下文中 会将状态挂载window上 resolve(app) }) }, reject) })}服务端代码 server.jslet Koa = require('koa')let Router = require('koa-router')let Static = require('koa-static')let fs = require('fs')let path = require('path');let app = new Koa()let router = new Router()let VueServerRender = require('vue-server-renderer')let ServerBundle = require('./dist/vue-ssr-server-bundle.json')// 渲染打包后的结果let template = fs.readFileSync('./dist/server.html', 'utf8')let clientManifest = require('./dist/vue-ssr-client-manifest.json')//createBundleRenderer 找到webpack打包后的函数 内部会调用这个函数获取到vue的实例let render = VueServerRender.createBundleRenderer(ServerBundle, { template, clientManifest})router.get('/(.*)', async ctx => { try { ctx.body = await new Promise((resolve, reject) => {//renderToString=>根据实例生成一个字符串返回给浏览器 render.renderToString({ url: ctx.url }, (err, data) => { if (err) reject(err) resolve(data); }); }); } catch (e) { ctx.body = '404' }})app.use(Static(path.resolve(__dirname, 'dist')))app.use(router.routes())app.listen(3002)webpack配置webpack.base.jslet path = require('path')let VueLoader = require('vue-loader/lib/plugin')let resolve = dir => { return path.resolve(__dirname, dir)}module.exports = { output: { filename: '[name].bundle.js', path: resolve('../dist') }, resolve: { extensions: ['.js', '.vue'] }, module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } }, exclude: /node_modules/ },//vue-style-loader基于style-loader实现的 支持服务端渲染 { test: /\.css$/, use: ['vue-style-loader', 'css-loader'] }, { test: /\.vue$/, use: 'vue-loader' }, { test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/, loader: 'url-loader' }, { test: /\.(png|jpg|gif|svg)$/, loader: 'url-loader' }, ] }, plugins: [ new VueLoader(), ]}webpack.client.jslet {merge} = require('webpack-merge')let base = require('./webpack.base')let path = require('path')let ClientRenderPlugin = require('vue-server-renderer/client-plugin')module.exports = merge(base, { entry: { client: path.resolve(__dirname,'../src/client-entry.js') }, output:{//不设置这个的话 打包出来的vue-ssr-client-manifest.json中的publicPath为'auto',默认请求静态资源http://localhost:3002/auto/client.bundle.js publicPath:'/', }, plugins: [ // 此插件在输出目录中,生成 `vue-ssr-client-manifest.json`。 new ClientRenderPlugin() ]})webpack.server.jslet { merge } = require('webpack-merge')let base = require('./webpack.base')let path = require('path')let ServerRenderPlugin = require('vue-server-renderer/server-plugin')let HtmlWebpackPlugin = require('html-webpack-plugin')let resolve = dir => { return path.resolve(__dirname, dir)}module.exports = merge(base, { entry: { server: resolve('../src/server-entry.js') }, target: 'node',//要给node来使用 output: { libraryTarget: 'commonjs2' }, devtool: 'source-map', plugins: [ // 这是将服务器的整个输出 构建为单个 JSON 文件的插件。 默认文件名为 `vue-ssr-server-bundle.json` new ServerRenderPlugin(), new HtmlWebpackPlugin({ filename: 'server.html', template: resolve('../public/server.html'), minify: false,//不压缩 excludeChunks: ['server'] }), ]})