文章内容输出来源:拉勾教育前端高薪训练营

简单的初始化体验

安装

yarn init --yes
yarn add vue vue-server-renderer

渲染一个 Vue 实例

server.js

// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({template: `<div>{{ msg }}</div>`,data: {msg: 'Hello World',},
})// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {if (err) throw errconsole.log(html)// => <div data-server-rendered="true">Hello World</div>
})// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer.renderToString(app).then(html => {console.log(html)
}).catch(err => {console.error(err)
})

与服务器集成

yarn add express

server.js

const Vue = require('vue')
const server = require('express')()const renderer = require('vue-server-renderer').createRenderer()server.get('*', (req, res) => {const app = new Vue({template: `<div>{{ msg }}</div>`,data: {msg: '前端你好',},})renderer.renderToString(app, (err, html) => {if (err) {return res.status(500).end('Internal Server Error')}res.setHeader('Content-type', 'text/html; charset=utf8')res.end(`<!DOCTYPE html><meta charset="UTF-8"><html lang="en"><head><title>Hello</title></head><body>${html}</body></html>`)})
})server.listen(3000, () => {console.log('server running at port 3000')
})

使用一个单独的页面模板维护html

创建一个index.template.html
注意 注释 – 这里将是应用程序 HTML 标记注入的地方

<!DOCTYPE html>
<meta charset="UTF-8">
<html lang="en"><head><meta charset="UTF-8">{{{ meta }}}<title>{{ title }}</title></head><body><!--vue-ssr-outlet--></body>
</html>
const Vue = require('vue')
const server = require('express')()
const fs = require('fs')const renderer = require('vue-server-renderer').createRenderer({template: fs.readFileSync('./index.template.html', 'utf-8')
})
const context = {title: 'vue ssr',meta: `<meta name="keyword" content="vue,ssr"><meta name="description" content="vue srr demo">`,
}server.get('*', (req, res) => {const app = new Vue({template: `<div>{{ msg }}</div>`,data: {msg: '前端你好',},})renderer.renderToString(app, context, (err, html) => {if (err) {return res.status(500).end('Internal Server Error')}res.setHeader('Content-type', 'text/html; charset=utf8') // 设置响应编码utf8res.end(html)})
})server.listen(3000, () => {console.log('server running at port 3000')
})

Vue ssr构建

当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。
所以需要为每个请求创建一个新的根 Vue 实例,如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)

服务器上的数据响应
因为实际的渲染过程需要确定性,所以我们也将在服务器上“预取”数据 (“pre-fetching” data) - 这意味着在我们开始渲染时,我们的应用程序就已经解析完成其状态。也就是说,将数据进行响应式的过程在服务器上是多余的,所以默认情况下禁用。禁用响应式数据,还可以避免将「数据」转换为「响应式对象」的性能开销。

组件生命周期钩子函数
由于没有动态更新,所有的生命周期钩子函数中,只有 beforeCreate 和 created 会在服务器端渲染 (SSR) 过程中被调用。这就是说任何其他生命周期钩子函数中的代码(例如 beforeMount 或 mounted),只会在客户端执行。应该避免在 beforeCreate 和 created 生命周期时产生全局副作用的代码,例如在其中使用 setInterval 设置 timer。

访问特定平台(Platform-Specific) API
通用代码不可接受特定平台的 API,因此如果你的代码中,直接使用了像 window 或 document,这种仅浏览器可用的全局变量,则会在 Node.js 中执行时抛出错误,反之也是如此。

对于共享于服务器和客户端,但用于不同平台 API 的任务(task),建议将平台特定实现包含在通用 API 中,或者使用为你执行此操作的 library。例如,axios 是一个 HTTP 客户端,可以向服务器和客户端都暴露相同的 API。

对于仅浏览器可用的 API,通常方式是,在「纯客户端 (client-only)」的生命周期钩子函数中惰性访问 (lazily access) 它们。

请注意,考虑到如果第三方 library 不是以上面的通用用法编写,则将其集成到服务器渲染的应用程序中,可能会很棘手。你可能要通过模拟 (mock) 一些全局变量来使其正常运行,但这只是 hack 的做法,并且可能会干扰到其他 library 的环境检测代码。

自定义指令
大多数自定义指令直接操作 DOM,因此会在服务器端渲染 (SSR) 过程中导致错误。有两种方法可以解决这个问题:

推荐使用组件作为抽象机制,并运行在「虚拟 DOM 层级(Virtual-DOM level)」(例如,使用渲染函数(render function))。

如果你有一个自定义指令,但是不是很容易替换为组件,则可以在创建服务器 renderer 时,使用 directives 选项所提供"服务器端版本(server-side version)"。

const renderer = createRenderer({directives: {example (vnode, directiveMeta) {// 基于指令绑定元数据(metadata)转换 vnode}}
})

文件结构

src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器

App.vue

<template><div id="app"><div>{{ msg }}</div><input v-model="msg"><button @click="handleClick">点击</button></div>
</template><script>
export default {name: 'App',data() {return {msg: '前端你好111',}},methods: {handleClick () {console.log('Hello Front')},},
}
</script><style></style>

app.js

import Vue from 'vue'
import App from './App.vue'// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {const app = new Vue({// 根实例简单的渲染应用程序组件。render: h => h(App)})return { app }
}

entry-client.js

import { createApp } from './app'// 客户端特定引导逻辑……const { app } = createApp()// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')

entry-server.js

import { createApp } from './app'export default context => {const { app } = createApp()return app
}

安装依赖

yarn add cross-env
yarn add -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url-loader file-loader rimraf vue-loader vue-template-compiler friendly-errors-webpack-plugin

配置webpack打包文件

webpack.base.config.js

/*** 公共配置*/
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const resolve = file => path.resolve(__dirname, file)const isProd = process.env.NODE_ENV === 'production'module.exports = {mode: isProd ? 'production' : 'development',output: {path: resolve('../dist/'),publicPath: '/dist/',filename: '[name].[chunkhash].js'},resolve: {alias: {// 路径别名,@ 指向 src'@': resolve('../src/')},// 可以省略的扩展名// 当省略扩展名的时候,按照从前往后的顺序依次解析extensions: ['.js', '.vue', '.json']},devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',module: {rules: [// 处理图片资源{test: /\.(png|jpg|gif)$/i,use: [{loader: 'url-loader',options: {limit: 8192,},},],},// 处理字体资源{test: /\.(woff|woff2|eot|ttf|otf)$/,use: ['file-loader',],},// 处理 .vue 资源{test: /\.vue$/,loader: 'vue-loader'},// 处理 CSS 资源// 它会应用到普通的 `.css` 文件// 以及 `.vue` 文件中的 `<style>` 块{test: /\.css$/,use: ['vue-style-loader','css-loader']},// CSS 预处理器,参考:https://vue-loader.vuejs.org/zh/guide/pre-processors.html// 例如处理 Less 资源// {//   test: /\.less$/,//   use: [//     'vue-style-loader',//     'css-loader',//     'less-loader'//   ]// },]},plugins: [new VueLoaderPlugin(),new FriendlyErrorsWebpackPlugin()]
}

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: {app: './src/entry-client.js'},module: {rules: [// ES6 转 ES5{test: /\.m?js$/,exclude: /(node_modules|bower_components)/,use: {loader: 'babel-loader',options: {presets: ['@babel/preset-env'],cacheDirectory: true,plugins: ['@babel/plugin-transform-runtime']}}},]},// 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,// 以便可以在之后正确注入异步 chunk。optimization: {splitChunks: {name: "manifest",minChunks: Infinity}},plugins: [// 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。new VueSSRClientPlugin()]
})

webpack.server.config.js

/*** 服务端打包配置*/
const { merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')module.exports = merge(baseConfig, {// 将 entry 指向应用程序的 server entry 文件entry: './src/entry-server.js',// 这允许 webpack 以 Node 适用方式处理模块加载// 并且还会在编译 Vue 组件时,// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。target: 'node',output: {filename: 'server-bundle.js',// 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)libraryTarget: 'commonjs2'},// 不打包 node_modules 第三方包,而是保留 require 方式直接加载externals: [nodeExternals({// 白名单中的资源依然正常打包allowlist: [/\.css$/]})],plugins: [// 这是将服务器的整个输出构建为单个 JSON 文件的插件。// 默认文件名为 `vue-ssr-server-bundle.json`new VueSSRServerPlugin()]
})

配置构建命令

package.json

"scripts": {"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js","build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js","build": "rimraf dist && npm run build:client && npm run build:server","start": "cross-env NODE_ENV=production node server.js","dev": "node server.js"
}

改造server.js

使用createBundleRenderer,引入打包后的文件

const Vue = require('vue')
const express = require('express')
const fs = require('fs')const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const template = fs.readFileSync('./index.template.html', 'utf-8')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {runInNewContext: false,template, // (可选)页面模板clientManifest, // (可选)客户端构建 manifest
})
const context = {title: 'vue ssr',meta: `<meta name="keyword" content="vue,ssr"><meta name="description" content="vue srr demo">`,
}const server = express()server.use('/dist', express.static('./dist'))server.get('*', (req, res) => {renderer.renderToString(context, (err, html) => {if (err) {return res.status(500).end('Internal Server Error')}res.setHeader('Content-type', 'text/html; charset=utf8') // 设置响应编码utf8res.end(html)})
})server.listen(3000, () => {console.log('server running at port 3000')
})

激活客户端

在 entry-client.js 中,我们用下面这行挂载(mount)应用程序:

app.$mount('#app')

检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:

<div id="app" data-server-rendered="true">

data-server-rendered 特殊属性,让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式进行挂载,在没有 data-server-rendered 属性的元素上,还可以向 $mount 函数的 hydrating 参数位置传入 true,来强制使用激活模式(hydration):

app.$mount('#app', true)

为App.vue添加id

<template><div id="app">// ...</div>
</template>

在开发模式下,Vue 将推断客户端生成的虚拟 DOM 树 (virtual DOM tree),是否与从服务器渲染的 DOM 结构 (DOM structure) 匹配。如果无法匹配,它将退出混合模式,丢弃现有的 DOM 并从头开始渲染。在生产模式下,此检测会被跳过,以避免性能损耗。
注意:浏览器可能会更改的一些特殊的 HTML 结构,比如会在

内部自动注入 ,由于 Vue 生成的虚拟 DOM (virtual DOM) 不包含 ,所以会导致无法匹配。所以请确保模板中写入有效的HTML

实现开发模式自动打包和热更新

const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')const resolve = file => path.resolve(__dirname, file)module.exports = (server, callback) => {let readyconst onReady = new Promise(r => ready = r)// 监视构建 -> 更新 Rendererlet templatelet serverBundlelet clientManifestconst update = () => {if (template && serverBundle && clientManifest) {ready()callback(serverBundle, template, clientManifest)}}// 监视构建 template -> 调用 update -> 更新 Renderer 渲染器const templatePath = path.resolve(__dirname, '../index.template.html')template = fs.readFileSync(templatePath, 'utf-8')update()// fs.watch、fs.watchFilechokidar.watch(templatePath).on('change', () => {template = fs.readFileSync(templatePath, 'utf-8')update()})// 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器const serverConfig = require('./webpack.server.config')const serverCompiler = webpack(serverConfig)const serverDevMiddleware = devMiddleware(serverCompiler, {// logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理})serverCompiler.hooks.done.tap('server', () => {serverBundle = JSON.parse(serverDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8'))update()})// 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器const clientConfig = require('./webpack.client.config')clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())clientConfig.entry.app = ['webpack-hot-middleware/client?quiet=true&reload=true', // 和服务端交互处理热更新一个客户端脚本clientConfig.entry.app]clientConfig.output.filename = '[name].js' // 热更新模式下确保一致的 hashconst clientCompiler = webpack(clientConfig)const clientDevMiddleware = devMiddleware(clientCompiler, {publicPath: clientConfig.output.publicPath,// logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理})clientCompiler.hooks.done.tap('client', () => {clientManifest = JSON.parse(clientDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8'))update()})server.use(hotMiddleware(clientCompiler, {log: false // 关闭它本身的日志输出}))// 重要!!!将 clientDevMiddleware 挂载到 Express 服务中,提供对其内部内存中数据的访问server.use(clientDevMiddleware)return onReady
}

配置VueRouter

src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/pages/Home'Vue.use(VueRouter)export const createRouter = () => {const router = new VueRouter({mode: 'history', // 兼容前后端routes: [{path: '/',name: 'home',component: Home},{path: '/about',name: 'about',component: () => import('@/pages/About')},{path: '/posts',name: 'post-list',component: () => import('@/pages/Posts')},{path: '*',name: 'error404',component: () => import('@/pages/404')}]})return router
}

app.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router/'// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {const router = createRouter()const app = new Vue({router, // 把路由挂载到 Vue 根实例中// 根实例简单的渲染应用程序组件。render: h => h(App)})return { app, router }
}

entry-server.js

// entry-server.js
import { createApp } from './app'// 此处context来自于server.js中的renderer.renderToString
export default async context => {// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,// 以便服务器能够等待所有的内容在渲染前,// 就已经准备就绪。const { app, router } = createApp()// 设置服务器端 router 的位置router.push(context.url)// 等到 router 将可能的异步组件和钩子函数解析完await new Promise(router.onReady.bind(router))return app
}

server.js

const Vue = require('vue')
const express = require('express')
const fs = require('fs')
const { createBundleRenderer } = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')const server = express()server.use('/dist', express.static('./dist'))let renderer
let onReady
const isProd = process.env.NODE_ENV === 'production'
if (isProd) {const serverBundle = require('./dist/vue-ssr-server-bundle.json')const template = fs.readFileSync('./index.template.html', 'utf-8')const clientManifest = require('./dist/vue-ssr-client-manifest.json')renderer = createBundleRenderer(serverBundle, {runInNewContext: false,template,clientManifest,})
} else {// 开发模式 => 监视打包构建 => 重新生成Renderer渲染器onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {renderer = createBundleRenderer(serverBundle, {runInNewContext: false,template,clientManifest,})})
}const render = async (req, res) => {try {const html = await renderer.renderToString({title: 'vue ssr',meta: `<meta name="description" content="vue ssr">`,url: req.url})res.setHeader('Content-type', 'text/html; charset=utf8') // 设置响应编码utf8res.end(html)} catch (error) {res.status(500).end('Internal Server Error')}}server.get('*', isProd ? render : async (req, res) => {await onReadyrender(req, res)
})server.listen(3000, () => {console.log('server running at port 3000')
})

客户端仍然需要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子

/*** 客户端入口*/import { createApp } from './app'// 客户端特定引导逻辑……const { app, router } = createApp()router.onReady(() => {app.$mount('#app')})

App.vue

<template><div id="app"><ul><li><router-link to="/">Home</router-link></li><li><router-link to="/about">About</router-link></li><li><router-link to="/posts">Posts</router-link></li></ul><!-- 路由出口 --><router-view/><div>{{ msg }}</div><input v-model="msg"><button @click="handleClick">点击</button></div>
</template><script>
export default {name: 'App',data() {return {msg: '前端你好111',}},methods: {handleClick () {console.log('Hello Front')},},
}
</script><style></style>

使用vue-meta进行head管理

yarn add vue-meta

app.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router/'
import VueMeta from 'vue-meta'Vue.use(VueMeta)Vue.mixin({metaInfo: {titleTemplate: '%s - 拉勾教育'}
})// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {const router = createRouter()const app = new Vue({router, // 把路由挂载到 Vue 根实例中// 根实例简单的渲染应用程序组件。render: h => h(App)})return { app, router }
}

entry-server.js

// entry-server.js
import { createApp } from './app'export default async context => {// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,// 以便服务器能够等待所有的内容在渲染前,// 就已经准备就绪。const { app, router, store } = createApp()const meta = app.$meta()// 设置服务器端 router 的位置router.push(context.url)context.meta = meta// 等到 router 将可能的异步组件和钩子函数解析完await new Promise(router.onReady.bind(router))return app
}

index.template.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">{{{ meta.inject().title.text() }}}{{{ meta.inject().meta.text() }}}
</head><body><!--vue-ssr-outlet-->
</body></html>

Home.vue

<template><div><h1>Home Page</h1></div>
</template><script>
export default {name: 'HomePage',metaInfo: {title: '首页'}
}
</script><style></style>

数据预取和状态管理

在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。

另一个需要关注的问题是在客户端,在挂载 (mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。

为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container)"中。首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到 store 中。此外,我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。

src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'Vue.use(Vuex)export const createStore = () => {return new Vuex.Store({state: () => ({posts: []}),mutations: {setPosts (state, data) {state.posts = data}},actions: {// 在服务端渲染期间务必让 action 返回一个 Promiseasync getPosts ({ commit }) {// return new Promise()const { data } = await axios.get('https://cnodejs.org/api/v1/topics')commit('setPosts', data.data)}}})
}

app.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router/'
import VueMeta from 'vue-meta'
import { createStore } from './store'Vue.use(VueMeta)Vue.mixin({metaInfo: {titleTemplate: '%s - ssr'}
})// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {const router = createRouter()const store = createStore()const app = new Vue({router, // 把路由挂载到 Vue 根实例中store, // 把容器挂载到 Vue 根实例中// 根实例简单的渲染应用程序组件。render: h => h(App)})return { app, router, store }
}

src/pages/Posts.vue

<template><div><h1>Post List</h1><ul><li v-for="post in posts" :key="post.id">{{ post.title }}</li></ul></div>
</template><script>
// import axios from 'axios'
import { mapState, mapActions } from 'vuex'export default {name: 'PostList',metaInfo: {title: 'Posts'},data () {return {// posts: []}},computed: {...mapState(['posts'])},// Vue SSR 特殊为服务端渲染提供的一个生命周期钩子函数serverPrefetch () {// 发起 action,返回 Promise// this.$store.dispatch('getPosts')return this.getPosts()},methods: {...mapActions(['getPosts'])}// 服务端渲染//     只支持 beforeCreate 和 created//     不会等待 beforeCreate 和 created 中的异步操作//     不支持响应式数据// 所有这种做法在服务端渲染中是不会工作的!!!// async created () {//   console.log('Posts Created Start')//   const { data } = await axios({//     method: 'GET',//     url: 'https://cnodejs.org/api/v1/topics'//   })//   this.posts = data.data//   console.log('Posts Created End')// }
}
</script><style></style>

entry-server.js

// entry-server.js
import { createApp } from './app'export default async context => {// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,// 以便服务器能够等待所有的内容在渲染前,// 就已经准备就绪。const { app, router, store } = createApp()const meta = app.$meta()// 设置服务器端 router 的位置router.push(context.url)context.meta = meta// 等到 router 将可能的异步组件和钩子函数解析完await new Promise(router.onReady.bind(router))context.rendered = () => {// Renderer 会把 context.state 数据对象内联到页面模板中// 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state// 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中context.state = store.state}return app
}

entry-client.js

/*** 客户端入口*/
import { createApp } from './app'// 客户端特定引导逻辑……const { app, router, store } = createApp()if (window.__INITIAL_STATE__) {store.replaceState(window.__INITIAL_STATE__)
}router.onReady(() => {app.$mount('#app')
})

vue-ssr的使用相关推荐

  1. 了解 Vue SSR 这一篇足以

    文章目录 1 - 什么是服务器端渲染? 1.1 新建server文件夹 1.2 生成一个node项目 1.3 安装express 1.4 服务端渲染小案例 1.5 运行查看效果 1.6 打开浏览器 1 ...

  2. [vue] SSR解决了什么问题?有做过SSR吗?你是怎么做的?

    [vue] SSR解决了什么问题?有做过SSR吗?你是怎么做的? SSR server side render服务端渲染,解决spa应用缺点的首屏加载速度慢.不利于SEO问题 个人简介 我是歌谣,欢迎 ...

  3. 【服务端渲染】之 Vue SSR

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:内容较多,建议通过左侧导航栏进行阅读 Vue SSR 基本介绍 Vue SSR 是什么 官方文档:https://ssr.vuejs.org/ V ...

  4. Vue SSR(Vue2 + Koa2 + Webpack4)配置指南

    正如Vue官方所说,SSR配置适合已经熟悉 Vue, webpack 和 Node.js 开发的开发者阅读.请先移步 ssr.vuejs.org 了解手工进行SSR配置的基本内容. 从头搭建一个服务端 ...

  5. 一个极简版本的 VUE SSR demo

    我本人在刚开始看 VUE SSR 官方文档的时候遇到很多问题,它一开始是建立在你有一个可运行的构建环境的,所以它直接讲代码的实现,但是对于刚接触的开发者来说并没有一个运行环境,所以所有的代码片段都无法 ...

  6. 服务器端渲染-Vue SSR搭建

    阅读建议:建议通过左侧导航栏进行阅读 文章简介:本文是Vue.js服务器端渲染的另一种解决方案-SSR(Server-Side Rendering)学习笔记 Vue SSR是什么 官方文档解释:Vue ...

  7. 理解vue ssr原理,自己搭建简单的ssr框架

    理解vue ssr原理,自己搭建简单的ssr框架 前言 大多数Vue项目要支持SSR应该是为了SEO考虑,毕竟对于WEB应用来说,搜索引擎是一个很大的流量入口.Vue SSR现在已经比较成熟了,但是如 ...

  8. Vue SSR 性能优化实践

    齐云雷,微医云服务团队前端工程师,本文是作者在<第二届缤纷前端技术沙龙>分享主题的文字版. 估计大部分读者对标题中的性能优化更感兴趣,可惜我分享的重点其实更多在于实践.实践有深有浅,下面介 ...

  9. 手把手带你从零打造Vue SSR,清晰易懂!

    Vue SSR,服务端渲染,优点大家都很清楚,能大大提升首屏渲染速度,优化用户体验,还有利于SEO. 但说实话,Vue SSR并不好上手.官网给的例子大而全,太复杂.而网上很多从0到1打造Vue SS ...

  10. 骨架屏 之 Vue SSR(快捷简易版本解决方案)

    一. 骨架屏简介 简单来说, 骨架屏就是填充了背景等特效的真实页面手稿轮廓图. 它可以是精确/粗略的描述了页面各个元素大小,形状,位置占位的一种页面真实数据渲染加载前的排版. 目的是加载页面过程中给用 ...

最新文章

  1. 黑马lavarel教程---5、模型操作(AR模式)
  2. python locust 能压测数据库_python locust 性能测试:HOOKS钩子方法
  3. 小度智能音响拆解 芯片_打磨小度智能音箱:深度拆解,发掘升级潜能
  4. 分布式架构在农业银行的应用实践与展望
  5. 转: c#.net利用RNGCryptoServiceProvider产生任意范围强随机数的办法
  6. MVC源码分析 - 路由匹配
  7. 身为程序员的唐僧说:只要我不死,就能取到真经!
  8. 基于R语言实现的交通时空大数据处理
  9. 用户输入和命令行参数
  10. 《软件工程导论》期末复习知识点总结(全)
  11. java学生管理系统论文_(定稿)毕业论文基于java的学生信息管理系统设计报告(完整版)最新版...
  12. 妄撮小游戏的开发思想-Android开发资料-《妄撮(撕开美女衣服)》游戏源代码外传...
  13. C语言拯救者 番外篇 (Windows实用调试技巧)
  14. 严重的PHP缺陷可导致QNAP NAS 设备遭RCE攻击
  15. chm 转化为 html 转化为 txt
  16. JavaWeb - 工作窃取算法 Work-Stealing
  17. 海洋地球科学开放数据库
  18. 201871010134-周英杰《面向对象程序设计(java)》第一周学习总结
  19. 股票中的KD指标金叉和死叉
  20. mysql勒索_mysql数据库被勒索删库怎么办

热门文章

  1. 2017年1月/4月新番表中文版
  2. 维生素c和b1_维生素C和维生素B1各有什么作用,长期使用有没有害??
  3. Python:图像数据增强和保存
  4. 全国省会城市商业银行核心系统集成商列表
  5. RFID防碰撞算法(三)
  6. 中关村-DIY之Opera秘诀
  7. 婚恋 php,php婚恋
  8. 2021年必读的12本机器学习书籍
  9. ZigBee TI ZStack CC2530 3.10 IO口01-输入输出
  10. [附源码]java毕业设计大学生家教服务推荐系统