Vue-服务端渲染SSR
服务端渲染(都在node中):
SSR(Server-Side-Rendering)是用户第一次请求、刷新页面时,又服务端相应HTML字符串,可以省去浏览器端首次渲染的工作,加快首屏显示速度。
特点:1、更好的SEO优化;2、更快的内容到达时间。

CSR:


项目目录:

app.js:
import Vue from 'vue' import App from './App.vue' import { createRouter } from './router' import { createStore } from './store' import { sync } from 'vuex-router-sync' export function createApp () { const router = createRouter() const store = createStore() sync(store, router) const app = new Vue({ router, store, render: h => h(App) }) return { app, router, store } }
entry-client.js:
import { createApp } from './app'
const { app, router } = createApp()
router.onReady(() => {
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')
})
entry-server.js:
import { createApp } from './app'
export default context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
// 设置服务器端 router 的位置
router.push(context.url)
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// 对所有匹配的路由组件调用 `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)
})
}
webpack.base.config.js:
const path = require('path')
const {
VueLoaderPlugin
} = require('vue-loader')
const resolve = (dir) => path.join(path.resolve(__dirname, '../'), dir)
const FriendlyErrorsPluginWebpackPlugin = require('friendly-errors-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
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: {
'public': resolve('public')
}
},
module: {
noParse: /es6-promise\.js$/, // avoid webpack shimming process
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url-loader',
options: {
limit: 10000,
name: '[name].[ext]?[hash]'
}
},
{
test: /\.s(a|c)ss?$/,
use: ['vue-style-loader', 'css-loader', 'sass-loader']
},
]
},
performance: {
hints: false
},
plugins: [
new VueLoaderPlugin(),
new FriendlyErrorsPluginWebpackPlugin(),
new CleanWebpackPlugin()
]
}
webpack.client.config.js:
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.merge(baseConfig, {
entry: {
app: './src/entry-client.js'
},
optimization: {
// 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
// 以便可以在之后正确注入异步 chunk。
// 这也为你的 应用程序/vendor 代码提供了更好的缓存。
splitChunks: {
name: "manifest",
minChunks: Infinity
}
},
plugins: [
// new webpack.optimize.CommonsChunkPlugin({
// name: "manifest",
// minChunks: Infinity
// }),
// 此插件在输出目录中
// 生成 `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.merge(baseConfig, {
// 将 entry 指向应用程序的 server entry 文件
entry: './src/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
allowlist: /\.css$/
}),
// 这是将服务器的整个输出
// 构建为单个 JSON 文件的插件。
// 默认文件名为 `vue-ssr-server-bundle.json`
plugins: [
new VueSSRServerPlugin()
]
})
server.js:
const Vue = require('vue')
const server = require('express')()
const { createBundleRenderer } = require('vue-server-renderer')
const path = require('path')
const fs = require('fs')
const resolve = file => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENV === 'production'
let renderer, readyPromise
const createRenderer = (bundle, options) => {
return createBundleRenderer(bundle, Object.assign(options, {
basedir: resolve('./dist'),
runInNewContext: false,
}))
}
const templatePath = resolve('./src/index.template.html')
if (isProd) {
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-srr-cclient-manifest.json')
const template = fs.readFileSync(templatePath, 'utf-8')
renderer = createRenderer(serverBundle, {
template, // (可选)页面模板
clientManifest // (可选)客户端构建 manifest
})
} else {
//开发模式
//1、 server bundle
//2、 client manifest
//3、 等待编译完成 调用 createBundleRenderer
// renderToString
// setupServer readyPromise
readyPromise = require('./config/setup-dev-server')(server, templatePath, (bundle, options) => {
render = createRenderer(bundle, options)
})
}
const render = (req, res) => {
const context = {
title: 'vue ssr',
metas: `
<meta charest="UTF-8">
<meta name="keyword" content="vue,ssr">
<meta name="description" content="vue srr demo">
`,
url: req.url
}
// 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。
// 现在我们的服务器与应用程序已经解耦!
renderer.renderToString(context, (err, html) => {
// 处理异常……
if (err) {
if (err.code === 404) {
res.status(404).end('Page not found')
} else {
res.status(500).end('Internal Server Error')
}
} else {
res.end(html)
}
})
}
// 在服务器处理函数中……
server.get('*', isProd ? (req, res) => render(req, res) : (req, res) => {
readyPromise().then(() => render(req, res))
})
server.listen(8080)
setup-dev-server.js:
const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webapck = require('webpack')
const clientConfig = require('./webpack.client.config')
const serverConfig = require('./webpack.server.config')
const middleware = require('webpack-dev-middleware')
const HMR = require('webpack-hot-middleware')
const webpack = require('webpack')
const MFS = require('memory-fs')
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(clientConfig.output.publicPath, file), 'utf-8')
} catch (error) {
}
}
const setupServer = (app, templatePath, cb) => {
let bundle, clientManifest, template, ready
const redayPromise = new Promise(r => ready = r)
template = fs.readFileSync(templatePath, 'utf-8')
const update = () => {
if (bundle && clientManifest) {
//server 渲染
//执行 createRenderer RednerToString
ready()
cb(bundle, {
template,
clientManifest
})
}
}
//webpack entry-server bundle
const mfs = new MFS();
const serverCompiler = webpack(serverConfig);
serverCompiler.outputFileSystem = mfs;
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
// 之后读取输出:
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
if (stats.errors.length) return
bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
update()
});
//webpack entry-client clientManifest
//hot-middleware
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin()
)
clientConfig.entry.app = ['webpack-hot-niddleware.client', clientConfig.entry.app]
const clientCompiler = webapck(clientConfig)
const devMiddleware = middleware(clientCompiler, {
noInfo: true, publicPath: clientConfig.output.publicPath
})
app.use(devMiddleware)
app.use(HMR(clientCompiler))
clientCompiler.hooks.done.tap('clientsBuild', states => {
states = states.toJson()
if (states.errors.length !== 0) {
return
}
clientManifest = JSON.parse(readFile(
devMiddleware.fileSystem,
'vue-ssr-client-manifest.json'
))
})
//fs tempaltePath template
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf-8')
console.log('template is update')
update()
})
return redayPromise
}
module.exports = setupServer
index.template.html:
<html> <head> <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) --> <title>{{ title }}</title> <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) --> {{{ meta }}} </head> <body> <!--vue-ssr-outlet--> </body> </html>
执行命令:package.json中配置
"scripts": { "dev": "nodemon server.js", "build": "npm run build:client && npm run build:server", "build:client": "webpack --config config/webpack.client.config.js", "build:server": "webpack --config config/webpack.server.config.js" }
至此Vue服务端渲染项目搭建完毕!
浙公网安备 33010602011771号