vue的两种服务器端渲染方案
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue的两种服务器端渲染方案相关的知识,希望对你有一定的参考价值。
作者:京东零售 姜欣
关于服务器端渲染方案,之前只接触了基于react的Next.js,最近业务开发vue用的比较多,所以调研了一下vue的服务器端渲染方案。 首先:长文预警,下文包括了两种方案的实践,没有耐心的小伙伴可以直接跳到方案标题下,down代码体验一下。
前置知识:
1、什么是服务器端渲染(ssr)?
简单来说就是用户第一次请求页面时,页面上的内容是通过服务器端渲染生成的,浏览器直接显示服务端返回的完整html就可以,加快首屏显示速度。
举个栗子:
当我们访问一个商品列表时,如果使用客户端渲染(csr),浏览器会加载空白的页面,然后下载js文件,通过js在客户端请求数据并渲染页面。如果使用服务器端渲染(ssr),在请求商品列表页面时,服务器会获取所需数据并将渲染后的HTML发送给浏览器,浏览器一步到位直接展示,而不用等待数据加载和渲染,提高用户的首屏体验。
2、服务器端渲染的优缺点
优点:
(1)更好的seo:抓取工具可以直接查看完全渲染的页面。现在比较常用的交互是页面初始展示 loading 菊花图,然后通过异步请求获取内容,但是但抓取工具并不会等待异步完成后再行抓取页面内容。
(2)内容到达更快:不用等待所有的 js 都完成下载并执行,所以用户会更快速地看到完整渲染的页面。
缺点:
(1)服务器渲染应用程序,需要处于 Node.js server 运行环境
(2)开发成本比较高
总结:
总得来说,决定是否使用服务器端渲染,取决于具体的业务场景和需求。对于具有大量静态内容的简单页面,客户端渲染更合适一些,因为它可以更快地加载页面。但是对于需要从服务器动态加载数据的复杂页面,服务器端渲染可能是一个更好的选择,因为他可以提高用户的首屏体验和搜索引擎优化。
下面进入正文
方案一:vue插件vue-server-render
结论前置:不建议用,配置成本高
官网地址: <https://v2.ssr.vuejs.org/zh/>
首先要吐槽一下官网,按官网教程比较难搞,目录安排的不太合理,一顿操作项目都没起来...
并且官网示例的构建配置代码是webpack4的,现在初始化项目后基本安装的都是webpack5,有一些语法不同
(1)首先,先初始化一个npm项目,然后安装依赖得到一个基础项目 。(此处要注意vue-server-renderer 和 vue 必须匹配版本)
npm init -y
yarn add vue vue-server-renderer -S
yarn add express -S
yarn add webpack webpack-cli friendly-errors-webpack-plugin vue-loader babel-loader @babel/core url-loader file-loader vue-style-loader css-loader sass-loader sass webpack-merge webpack-node-externals -D
yarn add clean-webpack-plugin @babel/preset-env -D
yarn add rimraf // 模拟linx的删除命令,在build时先删除dist
yarn add webpack-dev-middleware webpack-hot-middleware -D
yarn add chokidar -D //监听变化
yarn add memory-fs -D
yarn add nodemon -D
...实在太多,如有缺失可以在package.json中查找
另外:我现在用的"vue-loader": "^15.9.0"版本,之前用的是"vue-loader": "^17.0.1",报了一个styles的错
(2)配置app.js,entry-client.js,entry-server.js,将官网参考中的示例代码(传送门: 构建配置 )拷贝至对应文件。
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
// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp ()
// 创建 router 和 store 实例
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 Vue from vue
import createApp from ./app
Vue.mixin(
beforeMount ()
const asyncData = this.$options
if (asyncData)
this.dataPromise = asyncData(
store: this.$store,
route: this.$route
)
)
const app, router, store = createApp()
if (window.__INITIAL_STATE__)
store.replaceState(window.__INITIAL_STATE__)
router.onReady(() =>
// 在初始路由 resolve 后执行,
// 使用 `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()
Promise.all(activated.map(c =>
if (c.asyncData)
return c.asyncData( store, route: to )
)).then(() =>
next()
).catch(next)
)
app.$mount(#app)
)
entry-server.js
import createApp from ./app
export default context =>
// 返回一个promise,服务器能够等待所有的内容在渲染前,已经准备就绪,
return new Promise((resolve, reject) =>
const app, router, store = createApp()
router.push(context.url)
router.onReady(() =>
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length)
return reject( code: 404 )
// 对所有匹配的路由组件调用 `asyncData()`
Promise.all(matchedComponents.map(Component =>
if (Component.asyncData)
return Component.asyncData(
store,
route: router.currentRoute
)
)).then(() =>
context.state = store.state
resolve(app)
).catch(reject)
, reject)
)
(3)在根目录下创建server.js 文件
其中一个非常重要的api:createBundleRenderer,这个api上面有一个方法renderToString将代码转化成html字符串,主要功能就是把用webpack把打包后的服务端代码渲染出来。具体了解可看官网bundle renderer指引(传送门: bundle renderer指引 )
// server.js
const app = require(express)()
const createBundleRenderer = require(vue-server-renderer)
const fs = require(fs)
const path = require(path)
const resolve = file => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENE === "production"
const createRenderer = (bundle, options) =>
return createBundleRenderer(bundle, Object.assign(options,
basedir: resolve(./dist),
runInNewContext: false,
))
let renderer, readyPromise
const templatePath = resolve(./src/index.template.html)
if (isProd)
const bundle = require(./dist/vue-ssr-server-bundle.json)
const clientManifest = require(./dist/vue-ssr-client-manifest.json)
const template = fs.readFileSync(templatePath, utf-8)
renderer = createRenderer(bundle,
// 推荐
template, // (可选)页面模板
clientManifest // (可选)客户端构建 manifest
)
else
// 开发模式
readyPromise = require(./config/setup-dev-server)(app, templatePath, (bundle, options) =>
renderer = createRenderer(bundle, options)
)
const render = (req, res) =>
const context =
title: hello ssr with webpack,
meta: `
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
`,
url: req.url
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)
)
// 在服务器处理函数中……
app.get(*, isProd ? render : (req, res) =>
readyPromise.then(() => render(req, res))
)
app.listen(8080) // 监听的是8080端口
(4)接下来是config配置
在根目录新增config文件夹,然后新增四个配置文件:webpack.base.config,webpack.client.config,webpack.server.config,setup-dev-server(此方法是一个封装,为了配置个热加载,差点没搞明白,参考了好多)(官网传送门: 构建配置 )
大部分官网有示例代码,但是要在基础上进行一些更改
webpack.base.config
// webpack.base.config
const path = require(path)
// 用来处理后缀为.vue的文件
const VueLoaderPlugin = require(vue-loader)
const FriendlyErrorsWebpackPlugin = require(friendly-errors-webpack-plugin)
// 定位到根目录
const resolve = (dir) => path.join(path.resolve(__dirname, "../"), dir)
// 打包时会先清除一下
// 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].[chunk-hash].js
,
resolve:
alias:
public: resolve(public)
,
module:
noParse: /es6-promise.js$/,
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 FriendlyErrorsWebpackPlugin(),
// 打包时会先清除一下
// new CleanWebpackPlugin()
]
webpack.client.config
// webpack.client.config
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
,
optimization:
// 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
// 以便可以在之后正确注入异步 chunk。
// 这也为你的 应用程序/vendor 代码提供了更好的缓存。
splitChunks:
name: "manifest",
minChunks: Infinity
,
plugins: [
// 此插件在输出目录中
// 生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
)
webpack.server.config
// webpack.server.config
const merge = require(webpack-merge)
const nodeExternals = require(webpack-node-externals)
// webpack的基础配置,比如sass,less预编译等
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,
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)的依赖模块列入白名单
allowlist: /.css$/
),
// 这是将服务器的整个输出
// 构建为单个 JSON 文件的插件。
// 默认文件名为 `vue-ssr-server-bundle.json`
plugins: [
new VueSSRServerPlugin()
]
)
setup-dev-server:封装createRenderer方法
const webpack = require(webpack)
const fs = require(fs)
const path = require(path)
const chokidar = require(chokidar)
const middleware = require("webpack-dev-middleware")
const HMR = require("webpack-hot-middleware")
const MFS = require(memory-fs)
const clientConfig = require(./webpack.client.config)
const serverConfig = require(./webpack.server.config)
const readFile = (fs, file) =>
try
return fs.readFileSync(path.join(clientConfig.output.path, file), utf8)
catch (error)
const setupServer = (app, templatePath, cb) =>
let bundle
let clientManifest
let template
let ready
const readyPromise = new Promise(r => ready = r)
template = fs.readFileSync(templatePath, utf8)
const update = () =>
if (bundle && clientManifest)
// 通知 server 进行渲染
// 执行 createRenderer -> RenderToString
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()
);
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin()
)
clientConfig.entry.app = [webpack-hot-middleware/client, clientConfig.entry.app]
clientConfig.output.filename = [name].js
const clientCompiler = webpack(clientConfig);
const devMiddleware = middleware(clientCompiler,
noInfo: true, publicPath: clientConfig.output.publicPath, logLevel: silent
)
app.use(devMiddleware);
app.use(HMR(clientCompiler));
clientCompiler.hooks.done.tap(clientsBuild, stats =>
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
if (stats.errors.length) return
clientManifest = JSON.parse(readFile(
devMiddleware.fileSystem,
vue-ssr-client-manifest.json
))
update()
)
// fs -> templatePath -> template
chokidar.watch(templatePath).on(change, () =>
template = fs.readFileSync(templatePath, utf8)
console.log(template is updated);
update()
)
return readyPromise
module.exports = setupServer
(5)配置搞完了接下来是代码渲染
在src目录下,新增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>
(6)再搞个store和api模拟一下数据请求
这里介绍一下一个很重要的东西asyncData 预取数据,预取数据是在vue挂载前,所以下文这里用了上下文来获取store而不是this
asyncData: ( store ) => return store.dispatch(getDataAction) ,
在src下创建api文件夹,并在下面创建data.js文件
// data.js
const getData = () => new Promise((resolve) =>
setTimeout(() =>
resolve([
id: 1,
item: 测试1
,
id: 2,
item: 测试2
,
])
, 1000)
)
export
getData
在src下创建store文件夹,并在下面创建index.js文件
// store.js
import Vue from vue
import Vuex from vuex
Vue.use(Vuex)
import getData from ../api/data
export function createStore ()
return new Vuex.Store(
state:
lists: []
,
actions:
getDataAction ( commit )
return getData().then((res) =>
commit(setData, res)
)
,
mutations:
setData (state, data)
state.lists = data
)
(7)编写组件,在src/components文件夹下写两个组件,在app.vue中引用一下,用上刚写的模拟数据
Hello.vue
<template>
<div>
这里是测试页面一
<p>item</p>
<router-link to="/hello1">链接到测试页面二</router-link>
</div>
</template>
<script>
export default
asyncData: ( store ) =>
return store.dispatch(getDataAction)
,
computed:
item ()
return this.$store.state.lists
</script>
<style lang="scss" scoped>
</style>
Hello1.vue
<template>
<div>这里是测试页面二item</div>
</template>
<script>
export default
asyncData: ( store ) =>
return store.dispatch(getDataAction)
,
computed:
item ()
return this.$store.state.lists
</script>
<style lang="scss" scoped>
</style>
(8)配置路由并在app.vue使用路由
router.js
import Vue from vue
import Router from vue-router
Vue.use(Router)
export function createRouter ()
return new Router(
mode: history,
routes: [
path: /hello,
component: () => import(./components/Hello.vue)
,
path: /hello1,
component: () => import(./components/Hello1.vue)
,
]
)
app.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default
name: App,
</script>
<style lang="scss" scoped>
</style>
(9)根目录下创建一个.babelrc,进行配置
"presets": [
[
"@babel/preset-env",
"modules": false
]
]
(10)改写package.json执行命令
"dev": "nodemon server.js",
"build": "rimraf dist && 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"
大搞告成,执行一下dev命令,可以通过访问localhost:8080端口看到页面,记得带上路由哦~
执行build命令可看到,最后dist文件下共有三个文件:main.[chunk-hash].js,vue-ssr-client-manifest.json,vue-ssr-server-bundle.json
附上文件整体目录结构
方案二:基于vue的nuxt.js通用应用框架
一对比,这个就显得丝滑多了~ 官网地址: nuxt.js
先对比一下两种方案的差别
1.vue初始化虽然有cli,但是nuxt.js的cli更加完备
2.nuxt有更合理的工程化目录,vue过于简洁,比如一些component,api文件夹都是要手动创建的
3.路由配置:传统应用需要自己来配置,nuxt.js自动生成
4.没有统一配置,需手动创建。nuxt.js会生成nuxt.config.js
5.传统不易与管理底层框架逻辑(nuxt支持中间件管理,虽然我还没探索过这里)
显而易见这个上手就快多了,也不需要安装一大堆依赖,如果用了sass需要安装sass和sass-loader,反正我是用了
(1)创建一个项目 可选npm,npx,yarn,具体看官方文档
npm init nuxt-app <project-name>
(2)pages下面创建几个文件
nuxt是通过pages页面形成动态的路由,不用手动配置路由。比如在pages下面新增了个文件about.vue,那么这个页面对应的路由就是/about
其实这个时候运行npm run dev 就可以看到简单的页面了
(3)模拟接口
这里介绍一个插件,可以快速创建一个服务
npm i json-server
安装完后,在根目录新增db.json文件,模拟几个接口
"post": ["id": 1, "title": "json-server", "author": "jx"],
"comments": ["id": 1, "body": "some comment", "postId": 1],
"profile": "name": "typicode"
运行命令json-server --watch db.json --port=8000(不加会端口冲突),就可以看到
因为是get请求,可以直接点击访问可以看到mock的数据已经返回了
(4)页面调用
先配置一下axios,推荐使用nuxt.js封装的axios:"@nuxtjs/axios": "^5.13.6"
,然后再在nuxt.config.js文件中modules下面配置一下就可以使用了
modules: [ @nuxtjs/axios],
随便找个接口调用一下
<template>
<div>
<div>
这里是测试页面一
</div>
接口返回数据:posts
</div>
</template>
<script>
export default
name: IndexPage,
async asyncData($axios)
const result = await $axios.get(http://localhost:8000/post)
return
posts: result.data
</script>
刷新下页面就可以看到效果了,这里注意
axios.get还会返回头部等信息,另一个
get只返回结果
总结:
从页面篇幅上应该也能看到哪个容易上手了,nuxt相对于插件来说限定了文件夹的结构,并通过此预定了一些功能,更好上手。预设了利用vue.js开发服务端渲染所需要的各种配置,并且提供了提供了静态站点,异步数据加载,中间件支持,布局支持等
以上是关于vue的两种服务器端渲染方案的主要内容,如果未能解决你的问题,请参考以下文章