CSR是Client Side Render简称;页面上的内容是咱们加载的js文件渲染出来的,js文件运行在浏览器上面,服务端只返回一个html模板。javascript
SSR是Server Side Render简称;页面上的内容是经过服务端渲染生成的,浏览器直接显示服务端返回的html就能够了。css
本文以Vue.js 作为演示框架来区分SSR和CSR。默认状况下,Vue.js能够在浏览器中输出 Vue 组件,进行生成 DOM 和操做 DOM。然而也能够将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上彻底可交互的应用程序。html
服务器渲染的 Vue.js 应用程序也能够被认为是"同构"或"通用",由于应用程序的大部分代码均可以在服务器和客户端上运行。vue
附:vue-ssr官方文档java
基本用法 | Vue SSR 指南从输入页面URL到页面渲染完成大体流程为:node
<canvas>
元素。根据上图devtool时间轴的结果,虽然CSR配合预渲染方式(loading、骨架图)能够提早FP、FCP从而减小白屏问题,但没法提早FMP;SSR将FMP提早至js加载前触发,提早显示网页中的"主角元素"。SSR不只能够减小白屏时间还能够大幅减小首屏加载时间。webpack
第一步 利用express框架写一个简单node服务nginx
Express是基于Node.js平台,快速、开放、极简的 Web 开发框架git
/* 第一步 利用express框架写一个简单node服务 */
var express = require('express');
var app = express();
app.get('*', function(req, res){
res.send('hello world');
});
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
复制代码
附:express文档github
Express - 基于 Node.js 平台的 web 应用开发框架第二步 利用vue-server-renderer提供的createRenderer将vue与node结合
renderer.renderToString(vm, context?, callback?): ?Promise<string>
将 Vue 实例渲染为字符串。上下文对象 (context object) 可选。回调函数是典型的 Node.js 风格回调,其中第一个参数是可能抛出的错误,第二个参数是渲染完毕的字符串。
/* 第一步 利用express框架写一个简单node服务 第二步 利用vue-server-renderer提供的createRenderer将vue与node结合 */
var express = require('express');
var app = express();
const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
app.get('*', function(req, res){
render(req,res)
});
function render(req, res) {
const app = new Vue({
data: {
url: req.url
},
template: `<div>req.url:{{ url }}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
} else {
res.end(`${html}`)
}
})
}
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
复制代码
第三步 读入index.template.html文件
建立 renderer 时提供一个页面模板。多数时候,咱们会将页面模板放在特有的文件中,例如index.template.html
<!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body> <!--vue-ssr-outlet--> </body> </html>复制代码
<!--vue-ssr-outlet-->
注释 -- 这里将是应用程序 HTML 标记注入的地方。
/* 第一步 利用express框架写一个简单node服务 第二步 利用vue-server-renderer提供的createRenderer将vue与node结合 第三步 读入index.template.html文件 */
var express = require('express');
var app = express();
const Vue = require('vue')
const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync( resolve('./src/index.template.html'), 'utf-8')
})
app.get('*', function(req, res){
render(req,res)
});
function render(req ,res){
const app = new Vue({
data: {
url: req.url
},
template: `<div>req.url:{{ url }}</div>`
})
const context = {
title: 'ssr测试',
}
renderer.renderToString(app,context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}else{
res.end(`${html}`)
}
})
}
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
复制代码
第四步 引入已经打包好的vue-ssr-server-bundle.json
vue-server-renderer
提供一个名为 createBundleRenderer
的 API,用于处理此问题,经过使用 webpack 的自定义插件,server bundle 将生成为可传递到 bundle renderer 的特殊 JSON 文件。所建立的 bundle renderer,用法和普通 renderer 相同,可是 bundle renderer 提供如下优势:
devtool: 'source-map'
)*.vue
文件时):自动内联在渲染过程当中用到的组件所需的CSS。更多细节请查看 CSS 章节。/* 第一步 利用express框架写一个简单node服务 第二步 利用vue-server-renderer提供的createRenderer将vue与node结合 第三步 读入index.template.html文件 第四步 引入已经打包好的vue-ssr-server-bundle.json */
var express = require('express');
var app = express();
const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const templatePath = resolve('./src/index.template.html')
const bundle = require('./dist/vue-ssr-server-bundle.json')
const { createBundleRenderer } = require('vue-server-renderer')
let renderer = createBundleRenderer(bundle, {
template: require('fs').readFileSync(templatePath, 'utf-8'),
})
app.get('*', function (req, res) {
render(req, res)
});
function render(req, res) {
const context = {
title: 'ssr测试',
url: req.url // 传递path,这个参数很重要
}
renderer.renderToString(context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
} else {
res.end(`${html}`)
}
})
}
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
复制代码
第五步 将bundle换成webpack实时输入的内存的bundle
webpack 默认使用普通文件系统来读取文件并将文件写入磁盘。可是,还可使用不一样类型的文件系统(内存(memory), webDAV 等)来更改输入或输出行为。为了实现这一点,能够改变inputFileSystem或outputFileSystem
调用watch方法会触发 webpack 执行器,但以后会监听变动(很像 CLI 命令:webpack --watch),一旦 webpack 检测到文件变动,就会从新执行编译。该方法返回一个Watching实例。
/* 第一步 利用express框架写一个简单node服务 第二步 利用vue-server-renderer提供的createRenderer将vue与node结合 第三步 读入index.template.html文件 第四步 引入已经打包好的vue-ssr-server-bundle.json 第五步 将bundle换成webpack实时输入的内存的bundle */
var express = require('express');
var app = express();
const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const templatePath = resolve('./src/index.template.html')
//const bundle = require('./dist/vue-ssr-server-bundle.json')
const webpack = require('webpack')
const serverConfig = require('./build/webpack.server.config')
const MFS = require('memory-fs')
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(serverConfig.output.path, file), 'utf-8')
} catch (e) { }
}
const { createBundleRenderer } = require('vue-server-renderer')
let renderer;
app.get('*', function (req, res) {
render(req, res)
});
function render(req, res) {
const context = {
title: 'ssr测试',
url: req.url // 传递path,这个参数很重要
}
renderer.renderToString(context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
} else {
res.end(`${html}`)
}
})
}
const serverCompiler = webpack(serverConfig)
const mfs = new MFS()
serverCompiler.outputFileSystem = mfs //打包至内存中
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
let bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
renderer = createBundleRenderer(bundle, {
template: require('fs').readFileSync(templatePath, 'utf-8'),
})
})
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
复制代码
附:webpack在Node.js 中的API
Node.js API | webpack 中文网
通用配置(Base Config)
服务器配置 (Server Config)
服务器配置,是用于生成传递给 createBundleRenderer
的 server bundle。它应该是这样的:
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: '/path/to/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'
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可使服务器构建速度更快,
// 并生成较小的 bundle 文件。
externals: nodeExternals({
// 不要外置化 webpack 须要处理的依赖模块。
// 你能够在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
whitelist: /\.css$/
}),
// 这是将服务器的整个输出
// 构建为单个 JSON 文件的插件。
// 默认文件名为 `vue-ssr-server-bundle.json`
plugins: [
new VueSSRServerPlugin()
]
})
复制代码
在生成 vue-ssr-server-bundle.json
以后,只需将文件路径传递给 createBundleRenderer
:
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
// ……renderer 的其余选项
})
复制代码
客户端配置 (Client Config)
除了 server bundle 以外,咱们还能够生成客户端构建清单 (client build manifest)。使用客户端清单 (client manifest) 和服务器 bundle(server bundle),renderer 如今具备了服务器和客户端的构建信息,所以它能够自动推断和注入资源预加载 / 数据预取指令(preload / prefetch directive),以及 css 连接 / script 标签到所渲染的 HTML。
好处是双重的:
html-webpack-plugin
来注入正确的资源 URL。<script>
标签,以免客户端的瀑布式请求 (waterfall request),以及改善可交互时间 (TTI - time-to-interactive)。要使用客户端清单 (client manifest),客户端配置 (client config) 将以下所示:
const webpack = require('webpack')
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: '/path/to/entry-client.js',
plugins: [
// 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
// 以即可以在以后正确注入异步 chunk。
// 这也为你的 应用程序/vendor 代码提供了更好的缓存。
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
}),
// 此插件在输出目录中
// 生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
复制代码
beforeCreate
和created
会在服务器端渲染 (SSR) 过程当中被调用。这就是说任何其余生命周期钩子函数中的代码(例如beforeMount
或mounted
),只会在客户端执行 2.通用代码不可接受特定平台的 API,所以若是你的代码中,直接使用了像window
或document
,这种仅浏览器可用的全局变量,则会在 Node.js 中执行时抛出错误,反之也是如此(global)
解决方案:
通用 entry(app.js
)
app.js
是咱们应用程序的「通用 entry」。在纯客户端应用程序中,咱们将在此文件中建立根 Vue 实例,并直接挂载到 DOM。可是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js
简单地使用 export 导出一个 createApp
函数
服务端数据预取 (Server entry)
在entry-server.js
中,咱们能够经过路由得到与router.getMatchedComponents()
相匹配的组件,若是组件暴露出asyncData
,咱们就调用这个方法。而后咱们须要将解析完成的状态,附加到渲染上下文(render context)中。
// entry-server.js
import { createApp } from './app'
export default context => {
// 由于有可能会是异步路由钩子函数或组件,因此咱们将返回一个 Promise,
// 以便服务器可以等待全部的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
// 设置服务器端 router 的位置
router.push(context.url)
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()//当前路由匹配到组件
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// 等到 router 将可能的异步组件和钩子函数解析完
// 对全部匹配的路由组件调用 `asyncData()`
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
// 在全部预取钩子(preFetch hook) resolve 后,
// 咱们的 store 如今已经填充入渲染应用程序所需的状态。
// 当咱们将状态附加到上下文,
// 而且 `template` 选项用于 renderer 时,
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
复制代码
客户端数据预取 (Client entry)
router.onReady该方法把一个回调排队,在路由完成初始导航时调用,这意味着它能够解析全部的异步进入钩子和路由初始化相关联的异步组件。router.beforeResolve在导航被确认以前,同时在全部组件内守卫和异步路由组件被解析以后,解析守卫就被调用。
router.onReady(() => {
// 添加路由钩子函数,用于处理 asyncData.
// 在初始路由 resolve 后执行,
// 以便咱们不会二次预取(double-fetch)已有的数据。
// 使用 `router.beforeResolve()`,以便确保全部异步组件都 resolve。
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to) //当前路由匹配的组件数组
const prevMatched = router.getMatchedComponents(from)
// 咱们只关心非预渲染的组件
// 因此咱们对比它们,找出两个匹配列表的差别组件
let diffed = false
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
if (!activated.length) {
return next()
}
// 这里若是有加载指示器 (loading indicator),就触发
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
// 中止加载指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app')
})
复制代码
同一个组件不一样参数切换路由时会触发重用组件内部beforeRouteUpdate,经过全局mixin路由钩子来监听调用asyncData方法拉取数据进行客户端渲染
Vue.mixin({
beforeRouteUpdate (to, from, next) {
const { asyncData } = this.$options
if (asyncData) {
asyncData({
store: this.$store,
route: to
}).then(next).catch(next)
} else {
next()
}
}
})
复制代码
附:完整的导航解析流程
beforeEach
守卫。beforeRouteUpdate
守卫 (2.2+)。beforeEnter
。beforeRouteEnter
。beforeResolve
守卫 (2.5+)。afterEach
钩子。beforeRouteEnter
守卫中传给 next
的回调函数。进程管理pm2
以cluster模式(多实例多进程模式)启动服务--watch参数,意味着当你的express应用代码发生变化时,pm2会帮你重启服务。
pm2 start server.js -i 4 --watch
或者pm2 -i 4 start npm -- run start --watch(同npm run start)
查询全部服务 pm2 list
附:pm2的cluster模式官方介绍
PM2 - Cluster Modenginx反向代理
修改nginx.config文件,增长对应虚拟主机反向代理到node对应的服务端口
server {
listen 80;
server_name csyry.com;
location / {
proxy_pass http://127.0.0.1:8080;
index index.html index.htm;
}
}复制代码
重启nginx服务器: sudo nginx -s reload
附:nginx中文配置文档
Nginx中文文档修改DNS
正式环境经过域名服务商修改映射解析,本机测试修改/etc/hosts文件