VUE2--从零实现服务端渲染(模拟vue-cli初始化项目的目录结构)
概要
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。在服务端渲染模板字符串的这一过程即为SSR。
初始化项目并安装依赖
创建一个项目目录为vue-ssr-demo, 进入目录,输入如下命令:
// 初始化项目
npm init
// 安装依赖
// 用于创建node服务
npm install express -D
npm install vue -S
npm install vue-router -S
// 用于将 Vue 实例渲染为 HTML
npm install vue-server-renderer -S
安装完成后,可以看到我们的package.json文件如下:
{
"name": "vue-ssr-demo",
"version": "1.0.0",
"description": "vue ssr demo",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"ssr"
],
"author": "elwin",
"license": "ISC",
"dependencies": {
"vue": "^2.6.14",
"vue-router": "^3.5.2",
"vue-server-renderer": "^2.6.14"
},
"devDependencies": {
"express": "^4.17.1"
}
}
按照如下创建目录结构
router.js代码
编写路由模块,返回一个路由实例
const VueRouter = require("vue-router")
const Vue = require("vue")
// 安装路由插件,如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法
Vue.use(VueRouter)
// 因为是node环境,所以要使用COMMON JS规范
module.exports = () => {
// 返回一个vue路由实例
return new VueRouter({
mode:"history",
routes:[
{
path:"/",
component: {
template:`<h1><b style="color:red;">Hello SSR</b> this is home page</h1>`
},
name:"home"
},
{
path:"/about",
component: {
template:`<h1><b style="color:red;">Hello SSR</b> this is about page</h1>`
},
name:"about"
}
]
})
}
app.js代码
编写vue项目入口文件相关代码,返回app实例和路由实例
const Vue = require("vue")
const createRouter = require("./router")
module.exports = (context) => {
// 创建一个路由实例
const router = createRouter()
const app = new Vue({
router, // 挂载到vue实例上
data:{
message:"Hello SSR",
},
template:`
<div>
<h1>{{message}}</h1>
<ul>
<li>
<router-link to="/">home</router-link>
</li>
<li>
<router-link to="/about">about</router-link>
</li>
</ul>
<router-view></router-view>
</div>
`
})
// 返回app实例和路由实例
return {
app,
router
}
}
entry-server.js代码
现在我们需要在 entry-server.js 中实现服务器端路由逻辑 (server-side routing logic):
const createApp = require("./app.js")
// context执行上下文环境
module.exports = (context) => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise(async (reslove, reject) => {
// 解构出浏览器访问的url
const { url } = context
// 传入执行上下文,实例化app实例,解构出app和router
const {app, router} = createApp(context)
// 导航到浏览器访问URL,使app实例匹配到对应的组件
router.push(url)
/**
* router.onReady(callback, [errorCallback])
* 在路由完成初始导航时调用,这意味着它可以解析所有的异步进入钩子和路由初始化相关联的异步组件。
* 这可以有效确保服务端渲染时服务端和客户端输出的一致。
* 第二个参数 errorCallback 只在 2.4+ 支持。它会在初始化路由解析运行出错 (比如解析一个异步组件失败) 时被调用。
*/
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,执行 reject 函数,并返回 404
if(!matchedComponents.length) {
return reject({ code: 404 })
}
// Promise 应该 resolve 应用程序实例,以便它可以渲染
reslove(app)
},reject)
})
}
server.js代码
构建服务器端执行逻辑,返回浏览器访问的url所对应的app实例渲染出的html字符串
const express = require("express")
const app = express()
const App = require('./src/entry-server.js')
const path = require("path")
const vueServerRender = require("vue-server-renderer").createRenderer({
template: require("fs").readFileSync(path.join(__dirname,"./index.template.html"), "utf-8")
})
app.get('*', async(request, response) => {
response.status(200)
response.setHeader("Content-type", "text/html;charset=utf-8")
// 解构出浏览器访问的url
const { url } = request
// favicon.ico 图标用于收藏夹图标和浏览器标签上的显示,如果不设置,浏览器会请求网站根目录的这个图标,如果网站根目录也没有这图标会产生 404。
if (url ==='/favicon.ico') return
let vm
vm = await App({ url }).catch(err => { console.log(err) })
vueServerRender.renderToString(vm).then((html) => {
response.end(html)
}).catch(err => console.log(err))
})
// 监听5000端口
app.listen(5000, () => {
console.log('服务已开启^_^')
})
启动node服务,效果如下:
home页面
about页面