前言
- 为什么服务端渲染
- 客户端不利于SEO搜索引擎优化
- 服务端渲染是可以被爬虫抓到的, 客户端很难被抓取到
- SSR直接将HTML字符串传递给浏览器 大大加快了首屏加载时间
- 但同时SSR占用更多的CPU和内存资源
- 一些常用的浏览器API可能无法使用
- 只支持vue的beforeCreate和created两个生命周期
- 本文服务用的是node
Vue SSR 指南
示例
src/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>
src/Bar.vue
<template>
<div>
<!-- lisi -->
{{ $store.state.name }}
</div>
</template>
<script>
export default {
/**
* @description 在服务端执行的方法 这个方法在后端执行
*/
asyncData(store){
return store.dispatch('changeName')
},
/**
* @description 服务端也会执行 beforeCreate 和 created
*/
beforeCreate() {
console.log('服务端会调用beforeCreate')
},
created() {
console.log('服务端会调用created')
},
/**
* @description 浏览器执行 后端忽略
*/
mounted() {
console.log('服务端不会调用mounted')
}
}
</script>
<style scoped="true">
div {
width: 100%;
height: 50px;
line-height: 50px;
background: goldenrod;
}
</style>
src/components/Foo.vue
<template>
<div @click="show">foo</div>
</template>
<script>
export default {
methods:{
show(){
alert('前端逻辑操作, 与服务端渲染无关')
}
}
}
</script>
效果
服务端掉用vue的钩子 只支持beforeCreate 和 create, 在bar.vue
文件中
基本流程
- 将导出的vue实例分成两份 一份是客户端要打包的 一份是服务端要打包的 (都是webpack打包)
- 客户端打包的是针对客端使用的(比如一些事件 视图操作 路由跳转等)
- 服务端打包的是node server要执行的函数
- 在node server中通过
VueServerRenderer
中的createBundleRenderer
方法 去调用函数(server-entry.js
导出的函数), 获取实例
- 将要执行的函数结合
createBundleRenderer
选项中添加的html模板(模板要加入<!--vue-ssr-outlet-->
)
- 通过
.renderToString
根据实例生成一个字符串 传给浏览器
有vue-router流程
- 这里用到模式是history(问题刷新时返回404)
- 用户输入url, 服务端会将路径传给render函数
- 在函数中向让路由跳转完毕
router.push(url)
- 跳转完毕后通过
router.onReady
方法(这可以有效确保服务端渲染时服务端和客户端输出的一致
)
- 接着
router.getMatchedComponents()
获取当前路由的所有组件(返回数组)
- 如果length等于0 没有匹配路由 服务端返回
not found
(前端路由也可做处理, 具体看router.js
文件)
有vuex流程
- 服务端渲染的数据 只针对路由可以访问的组件
- 组件有一个函数
asyncData
专为服务端调用
- 当用户输入完路径 组件中有
asyncData
并调用
- 在
context.state = store.state
将其挂载到window.__INITIAL_STATE__
- 接着浏览器 开始渲染 将服务端加载好的数据替换掉
目录结构
├── config
│ ├── webpack.base.js
│ ├── webpack.client.js
│ └── webpack.server.js
├── dist
│ ├── client.bundle.js
│ ├── index.html
│ ├── index.ssr.html
│ └── server.bundle.js
├── public
│ ├── index.html
│ └── index.ssr.html
├── src
│ ├── App.vue
│ ├── components
│ │ ├── Bar.vue
│ │ └── Foo.vue
│ ├── entry-client.js
│ ├── entry-server.js
│ ├── router.js
│ ├── store.js
│ └── app.js
├── server.js
└── package.json
有哪些包
* vue vuex vue-router
# node 服务端
* koa
* koa-router 监听路由
* koa-static 前端静态文件、图片等静态资源处理模块。配置静态资源目录后,将不会出现静态资源not found错误
* vue-server-renderer node服务渲染vue
# webpack 打包部分
* webpack
* webpack-cli
* webpack-merge 合并
* babel-loader webpack和babel的一个桥梁
* @babel/core babel的核心模块(默认直接调用)
* @babel/preset-env 把es6+转换成低级语法
* vue-loader 解析.vue
* vue-template-compiler 编译模板
* css-loader 解析css样式
* vue-style-loader 解析的css文件插入到style标签中(vue-style-loader支持服务端渲染)
* concurrently 可以同时执行两个脚本命令
* nodemon 修改node服务端直接更新, 无需重新启动
package.json
"scripts": {
"client:dev": "webpack serve --config scripts/webpack.client.js",
"client:build": "webpack --config scripts/webpack.client.js --watch",
"server:build": "webpack --config scripts/webpack.server.js --watch",
"run:all": "concurrently \"npm run client:build\" \"npm run server:build\""
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"concurrently": "^5.3.0",
"koa": "^2.13.1",
"koa-router": "^10.0.0",
"koa-static": "^5.0.0",
"vue": "^2.6.12",
"vue-router": "^3.4.9",
"vue-server-renderer": "^2.6.12",
"vuex": "^3.6.0",
"webpack-merge": "^5.7.3"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"babel-loader": "^8.2.2",
"css-loader": "^5.0.1",
"html-webpack-plugin": "^4.5.1",
"vue-loader": "^15.9.6",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.12",
"webpack": "^5.13.0",
"webpack-cli": "^4.3.1"
}
正题
src/app.js
import Vue from 'vue'
import App from './App.vue'
/**
* @description 入口改装成了函数 目的是服务端渲染时 每次访问的适合都可以通过这个工厂函数返回一个全新的实例
* @description 保证每个人访问都可以拿到一个自己的实例
*/
export default () => {
const app = new Vue({
render: h => h(App)
})
return { app }
}
src/client-entry.js
/** 客户端 */
import createApp from './app.js'
let {app} = createApp()
app.$mount('#app')
src/server-entry.js
/** 服务端入口 */
import createApp from './app.js'
/**
* @description 服务端渲染可以返回一个函数
* @description 每次都能产生一个新的应用
* @param {context} 调用方法时 服务端会传入url
*/
export default (context)=>{
const { url } = context
/** 路由是异步组件 promise等待路由加载完毕 */
return new Promise((resolve, reject) => {
let { app, router, store } = createApp()
// 要跳转的url路径
router.push(url)
// 路由跳转完毕后 组件触发
router.onReady(() => {
const matchComponents = router.getMatchedComponents()
// 没有匹配到
if (matchComponents.length == 0) {
return reject({ code: 404 })
} else {
// matchComponents 指的是当前路径下的所有组件
// 服务端在渲染的时候 默认会找当前路径下的所有组间中的asyncData
// 并且在服务端也会创建一个vuex 传递给asyncData(store)
Promise.all(matchComponents.map(component => {
if (component.asyncData) {
return component.asyncData(store)
}
})).then(() => {
// 会默认在window下生成一个变量 内部默认就这样做的
// window.__INITIAL_STATE__ = {"name":"jiangwen"}
// 服务器执行完毕后 最新的状态保存在store.state上
context.state = store.state
// app 是已经获取到数据的实例
resolve(app)
})
}
})
})
}
config/webpack.base.js
/** 打包的公告配置文件 */
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
mode: 'development', // 开发模式
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname,'../dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.js$/,
use: {
loader: 'babel-loader', // babel-loader自动调用 @babel/core -> preset-env
options: {
presets: ['@babel/preset-env'],
}
},
exclude: /node_modules/ // node_modules文件排除
},
{
test: /\.css$/,
/** 执行顺序 从右向左执行 从下向上执行 */
use: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
esModule: false, // 注意为了配套使用vue-style-loader 置为false 不然样式出不来
}
}
]
}
]
},
plugins: [
/** 固定的 */
new VueLoaderPlugin()
]
}
config/webpack.client.js
const { merge } = require('webpack-merge')
const base = require('./webpack.base')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
/**
* @description 客户端打包入口
*/
module.exports = merge(base,{
entry: {
client: path.resolve(__dirname, '../src/client-entry.js')
},
plugins:[
new HtmlWebpackPlugin({ // html模板
template: path.resolve(__dirname, '../public/index.html'),
filename:'client.html' // 修改导出后的默认名(index.html)
}),
]
})
config/webpack.server.js
const base = require('./webpack.base')
const { merge } = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
/** 服务端打包入口 */
module.exports = merge(base,{
target: 'node', // node 使用
entry: {
server: path.resolve(__dirname, '../src/server-entry.js')
},
output:{
libraryTarget: "commonjs2" // export.modules
},
plugins:[
new HtmlWebpackPlugin({ /** html 模板 */
template: path.resolve(__dirname, '../public/index.ssr.html'),
filename: 'server.html',
excludeChunks: ['server'], // 不让server文件注入
minify: false, // 不压缩
client:'/client.bundle.js' // 加个参数 方便注入 index.ssr.html模板
}),
]
})
public/index.ssr.html
模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
<script src="/client.bundle.js"></script>
<!-- ejs模板 -->
<script src="<%=htmlWebpackPlugin.options.client%>"></script>
</body>
</html>
server.js
服务端文件
const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')
const router = new Router()
const VueServerRenderer = require('vue-server-renderer')
const static = require('koa-static')
// 读取文件
const fs = require('fs')
const path = require('path')
const serverBundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.bundle.js'), 'utf8')
const template = fs.readFileSync(path.resolve(__dirname, 'dist/server.html'), 'utf8')
// 根据实例
const render = VueServerRenderer.createBundleRenderer(serverBundle, {
template
})
router.get('/', async (ctx) => {
ctx.body = await new Promise((resolve, reject) => {
render.renderToString({url: ctx.url}, (err, html) => { // 如果想让css生效 只能使用回调的方式
if (err) reject(err);
resolve(html)
})
})
})
// 只要用户刷新就会像服务器发请求
router.get('/(.*)',async (ctx)=>{
ctx.body = await new Promise((resolve, reject) => {
// 通过服务端渲染 渲染后返回
render.renderToString({url: ctx.url}, (err, html) => {
if (err && err.code == 404) resolve(`not found`)
console.log("? ~ file: server.js ~ line 36 ~ render.renderToString ~ html", html)
resolve(html)
})
})
})
app.use(static(path.resolve(__dirname, 'dist')))
app.use(router.routes())
app.listen(3000)
发表评论
还没有评论,快来抢沙发吧!