如何构建 vue-ssr 项目的方法步骤


Posted in Javascript onAugust 04, 2020

如何通过 web 服务器去渲染一个 vue 实例

构建一个极简的服务端渲染需要什么

  • web 服务器
  • vue-server-renderer
  • vue
const Vue = require('vue')
const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')
const router = new Router()
const renderer = require('vue-server-renderer').createRenderer()
router.get(/./, (ctx)=>{
 const app = new Vue({
 data: {
 url: ctx.request.url
 },
 template: `<div>访问的 URL 是: {{ url }}</div>`
 })

 renderer.renderToString(app, (err, html) => {
 if (err) {
 ctx.status = 500 
 ctx.body = err.toString()
 }
 ctx.body = `
 <!DOCTYPE html>
 <html lang="en">
 <head><title>Hello</title></head>
 <body>${html}</body>
 </html>
 `
 })
})
app.use(router.routes())
app.listen(4000,()=>{
 console.log('listen 4000')
})
  • 首先通过 koa、koa-router 快速起了一个 web 服务器,这个服务器接受任何路径
  • 创建了一个renderer对象,创建一个 vue 实例
  • renderer.renderToString 将 vue 实例解析为 html 字符串
  • 通过 ctx.body ,拼接成一个完整的 html 字符串模版返回。

相信经过上面的代码实例可得知,即使你没有使用过 vue-ssr 的经历,但是你简单地使用过 vue 和 koa 的同学都可以看出来这个代码非常明了。

唯一要注意的地方就是,我们是通过 require('vue-server-renderer').createRenderer() 来创建一个 renderer 对象 . 这个renderer 对象有一个 renderToString 的方法

renderer.renderToString(app,(err,html)=>{})

  •  app 就是创建的 vue 实例
  • callback, 解析 app 后执行的回调,回调的第二个参数就是解析完实例得到的 html 字符串,这个的 html 字符串是挂载到 #app 那部分,是不包含 head、body 的,所以我们需要将它拼接成完整的 html 字符串返回给客户端。 

使用 template 用法

上面方法中 ctx.body 的部分需要手动去拼接模版,vue-ssr 支持使用模版的方式。

来看下模版长啥样,发现出来多一行 <!--vue-ssr-outlet--> 注释,和普通的html文件没有差别

<!--vue-ssr-outlet--> 注释 -- 这里将是应用程序 HTML 标记注入的地方。也就是 renderToString 回调中的 html 会被注入到这里。

<!DOCTYPE html>
<html lang="en">
 <head><title>Hello</title></head>
 <body>
 <!--vue-ssr-outlet-->
 </body>
</html>

有了模版该如何使用它呢?

只需要在创建 renderer 之前给 createRenderer 函数传递 template 参数即可。

看下使用模版和自定义模版的区别,可以看到通过其他部分都相同,只是我们指定了 template 后,ctx.body 返回的地方我们不需要手动去拼接一个完整的 html 结构了。

const renderer = require('vue-server-renderer').createRenderer({
 template: fs.readFileSync('./index.template.html','utf-8')
})
router.get(/./, (ctx)=>{
 const app = new Vue({
 data: {
 url: ctx.request.url
 },
 template:"<div>访问路径{{url}}</div>"
 })
 renderer.renderToString(app, (err, html) => {
 if (err) {
 ctx.status = 500 
 ctx.body = err.toString()
 }
 ctx.body = html
 })
})

项目级

上面的实例是 demo 的展示,在实际项目中开发的话我们会根据客户端和服务端将它们分别划分在不同的区块中。

项目结构

// 一个基本项目可能像是这样:
build     -- webpack配置
|——- client.config.js 
|——- server.config.js
|——- webpack.base.config.js
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry) -- 生成 vue 的工厂函数
├── entry-client.js # 仅运行于浏览器 -- 将 vue 实例挂载,作为 webpack 的入口
|── entry-server.js # 仅运行于服务器 -- 数据预处理逻辑,作为 webpack 的入口
|-- server.js    -- web 服务器启动入口 
|-- store.js    -- 服务端数据预处理存储容器
|-- router.js    -- vue 路由表

加载一个vue-ssr应用整体流程

首先根据上面的项目结构我们可以大概知道,我们的服务端和客户端分别以 entry-client.js 和 entry-server.js 为入口,通过 webpack 打包出对应的 bundle.js 文件。

首先不考虑 entry-client.js 和 entry-server.js 做了什么(后续会补充),我们需要知道,它们经过 webpack 打包后生成了我们需要的创建 ssr 的依赖 .js 文件。 可以看下图打包出来的文件,.json 文件是用来关联 .js 文件的,就是一个辅助文件,真正起作用的还是两个 .js 文件。

如何构建 vue-ssr 项目的方法步骤

假设我们以及打包好了这两份文件,我们来看 server.js 中做了什么。

server.js

// ... 省略不重要步骤
const renderer = require('vue-server-renderer').createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'),{
 runInNewContext:false,
 template: fs.readFileSync('./index.template.html','utf-8'),
 // 客户端构建
 clientManifest:require('./dist/vue-ssr-client-manifest.json')
})
router.get('/home', async (ctx)=>{
 ctx.res.setHeader('Content-Type', 'text/html')
 const html = await renderer.renderToString()
 ctx.body = html
})
app.listen(4000,()=>{
})

省略了一些不重要的步骤,来看 server.js,其实它和我们上面创建一个简单的服务端渲染步骤基本相同

  • 创建一个 renderer 对象,不同点在于创建这个对象是根据已经打包好的 .json 文件去找到真正起作用.js 文件去生成的。
  • 由于在 createBunldeRenderer 创建 renderer 对象的时候同时传入了 server.json 和 client-mainfest.json 两个部分,所以我们在使用 renderer.renderToString() 的时候也不需要去传入 vue实例了。
  • 最终得到 html 字符串和上面相同,返回客户端就完成了服务端渲染的部分。接下来就是客户端解析渲染 dom 的过程。

 流程梳理

有了对项目结构的了解,和 server.js 的基本了解后来梳理下 vue-ssr 整个工作流程是怎么样的?

首先我们会启动一个 web 服务,也就上面的 server.js ,来查看一个服务端路径

router.get('/home', async (ctx)=>{
 const context = {
 title:'template render',
 url:ctx.request.url
 }
 ctx.res.setHeader('Content-Type', 'text/html')
 const html = await renderer.renderToString(context)
 ctx.body = html
})
app.listen(4000,()=>{
 console.log('listen 4000')
})

当我们访问 http://localhost:4000/home 就会命中该路由,执行 renderer.renderToString(context) ,renderer 是根据我们已经打包好的 bundle 文件生成的 renderer对象。相当于去执行 entry-server.js 服务端数据处理和存储的操作

根据模版文件,得到 html 文件后返回给客户端,Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。相当于去执行 entry-client.js 客户端的逻辑

由于服务器已经渲染好了 HTML,我们显然无需将其丢弃再重新创建所有的 DOM 元素。相反,我们需要"激活"这些静态的 HTML,然后使他们成为动态的(能够响应后续的数据变化)。 如果你检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:

<div id="app" data-server-rendered="true">

entry-client.js 和 entry-server.js

经过上面的流程梳理我们知道了当访问一个 vue-ssr 的整个流程: 访问 web 服务器地址 > 执行 renderer.renderToString(context) 解析已经打包的 bunlde 返回 html 字符串 > 在客户端激活这些静态的 html,使它们成为动态的。

接下来我们需要看看 entry-client.js 和 entry-server.js 做了什么。

entry-server.js

  • 这里的 context 就是 renderer.renderToString(context) 传递的值,至于你想传递什么是你在 web 服务器中自定义的,可以传递任何你想给客户端的值。
  • 这里我们可以通过 context 来获取到客户端返回 web 服务器的地址,通过 context.url (需要你在服务端传递该值)获取到该路径,并且通过 router.push(context.url) 实例来访问相同的路径。
  • context.url 对应的组件中会定义一个 asyncData 的静态方法,并且将服务端存储在 store 的值传递给该方法。
  • 将 store 中的值存储给 context.state ,context.state 将作为 window. INITIAL_STATE 状态,自动嵌入到最终的 HTML 中。就是一个全局变量。
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()
  // 匹配不到的路由,执行 reject 函数,并返回 404
  if (!matchedComponents.length) {
  return reject({ code: 404 })
  }
  // 对所有匹配的路由组件调用 asyncData
  // Promise.all([p1,p2,p3])
  const allSyncData = matchedComponents.map(Component => {
  if(Component.asyncData) {
   return Component.asyncData({
   store,route:router.currentRoute
   })
  }
  })
  Promise.all(allSyncData).then(() => {
  // 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中。
  context.state = store.state
  resolve(app)
  }).catch(reject)
 }, reject)
 })
}

entry-client.js

执行匹配到的组件中定义的 asyncData 静态方法,将 store 中的值取出来作为客户端的数据。

import { createApp } from './app'
// 你仍然需要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子。
const { app,router,store } = createApp()

if (window.__INITIAL_STATE__) {
 store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() => {
 // 使用 `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')
})

构建配置

 webpack.base.config.js

服务端和客户端相同的配置一些通用配置,和我们平时使用的 webpack 配置相同,截取部分展示

module.exports = {
 mode:isProd ? 'production' : 'development',
 devtool: isProd
 ? false
 : '#cheap-module-source-map',
 output: {
 path: path.resolve(__dirname, '../dist'),
 publicPath: '/dist/',
 filename: '[name].[chunkhash].js'
 },
 module: {
 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: /\.styl(us)?$/,
  use: isProd
   ? ExtractTextPlugin.extract({
    use: [
    {
     loader: 'css-loader',
     options: { minimize: true }
    },
    'stylus-loader'
    ],
    fallback: 'vue-style-loader'
   })
   : ['vue-style-loader', 'css-loader', 'stylus-loader']
  },
 ]
 },
 plugins: [
  new VueLoaderPlugin()
  ]
}

client.config.js

const webpack = require('webpack')
const {merge} = require('webpack-merge')
const baseConfig = require('./webpack.base.config')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const path = require('path')
module.exports = merge(baseConfig,{
 entry:path.resolve('__dirname','../entry-client.js'),
 plugins:[
 // 生成 `vue-ssr-client-manifest.json`。
 new VueSSRClientPlugin()
 ]
})

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')
const path = require('path')
module.exports = merge(baseConfig,{
 entry:path.resolve('__dirname','../entry-server.js'),
 target:'node',
 devtool:'source-map',
 // 告知 server bundle 使用 node 风格导出模块
 output:{
 libraryTarget:'commonjs2'
 },
 externals: nodeExternals({
 allowlist:/\.css$/
 }),
 plugins:[
 new VueSSRServerPlugin()
 ]
})

开发环境配置

webpack 提供 node api可以在 node 运行时使用。

修改 server.js

server.js 作为 web 服务器的入口文件,我们需要判断当前运行的环境是开发环境还是生产环境。

const isProd = process.env.NODE_ENV === 'production'
async function prdServer(ctx) {
 // ...生产环境去读取 dist/ 下的 bundle 文件
}
async function devServer(ctx){
 // 开发环境
}
router.get('/home',isProd ? prdServer : devServer)
app.use(router.routes())
app.listen(4000,()=>{
 console.log('listen 4000')
})

dev-server.js

生产环境中是通过读取内存中 dist/ 文件夹下的 bundle 来解析生成 html 字符串的。在开发环境中我们该怎么拿到 bundle 文件呢?

  • webpack function 读取 webpack 配置来获取编译后的文件
  • memory-fs 来读取内存中的文件
  • koa-webpack-dev-middleware 将 bundle 写入内存中,当客户端文件发生变化可以支持热更新

 webpack 函数使用

导入的 webpack 函数会将 配置对象 传给 webpack,如果同时传入回调函数会在 webpack compiler 运行时被执行:

• 方式一:添加回调函数

const webpackConfig = {
 // ...配置项
}
const callback = (err,stats) => {}
webpack(webpackConfig, callback)

err对象 不包含 编译错误,必须使用 stats.hasErrors() 单独处理,文档的 错误处理 将对这部分将对此进行详细介绍。err 对象只包含 webpack 相关的问题,例如配置错误等。

方式二:得到一个 compiler 实例

你可以通过手动执行它或者为它的构建时添加一个监听器,compiler 提供以下方法

compiler.run(callback)

compiler.watch(watchOptions,handler) 启动所有编译工作

const webpackConfig = {
 // ...配置项
}
const compiler = webpack(webpackConfig)

客户端配置

const clientCompiler = webpack(clientConfig)
  const devMiddleware = require('koa-webpack-dev-middleware')(clientCompiler,{
  publicPath:clientConfig.output.publicPath,
  noInfo:true,
  stats:{
   colors:true
  }
  })

  app.use(devMiddleware)
  // 编译完成时触发
  clientCompiler.hooks.done.tap('koa-webpack-dev-middleware', 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()
  })

默认情况下,webpack 使用普通文件系统来读取文件并将文件写入磁盘。但是,还可以使用不同类型的文件系统(内存(memory), webDAV 等)来更改输入或输出行为。为了实现这一点,可以改变 inputFileSystem 或 outputFileSystem。例如,可以使用 memory-fs 替换默认的 outputFileSystem,以将文件写入到内存中。

koa-webpack-dev-middleware 内部就是用 memory-fs 来替换 webpack 默认的 outputFileSystem 将文件写入内存中的。

读取内存中的 vue-ssr-client-mainfest.json

调用 update 封装好的更新方法

服务端配置

读取内存中的vue-ssr-server-bundle.json文件

调用 update 封装好的更新方法

// hot middleware
  app.use(require('koa-webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))
  // watch and update server renderer
  const serverCompiler = webpack(serverConfig)
  serverCompiler.outputFileSystem = mfs
  serverCompiler.watch({}, (err, stats) => {
  if (err) throw err
  stats = stats.toJson()
  if (stats.errors.length) return

  // read bundle generated by vue-ssr-webpack-plugin
  bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
  update()
  })

update 方法

const update = async () => {
  if(bundle && clientManifest) {
   const renderer = createRenderer(bundle,{
   template:require('fs').readFileSync(templatePath,'utf-8'),
   clientManifest
   })
   // 自定义上下文
   html = await renderer.renderToString({url:ctx.url,title:'这里是标题'})
   ready()
  }
  }

总结

本文将自己理解的 vue-ssr 构建过程做了梳理,到此这篇关于如何构建 vue-ssr 项目的文章就介绍到这了,更多相关如何构建 vue-ssr 项目内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript实现动态增加删除表格行(兼容IE/FF)
Apr 02 Javascript
IE 条件注释详解总结(附实例代码)
Aug 29 Javascript
JavaScript之自定义类型
May 04 Javascript
根据json字符串生成Html的一种方式
Jan 09 Javascript
javascript中强制执行toString()具体实现
Apr 27 Javascript
JS验证身份证有效性示例
Oct 11 Javascript
JS实现仿新浪黄色经典滑动门效果代码
Sep 27 Javascript
jQuery无刷新分页完整实例代码
Oct 27 Javascript
神奇!js+CSS+DIV实现文字颜色渐变效果
Mar 16 Javascript
动态加载权限管理模块中的Vue组件
Jan 16 Javascript
vue2.0 资源文件assets和static的区别详解
Apr 08 Javascript
VUE.js实现动态设置输入框disabled属性
Oct 28 Javascript
vue-quill-editor的使用及个性化定制操作
Aug 04 #Javascript
vue 添加和编辑用同一个表单,el-form表单提交后清空表单数据操作
Aug 03 #Javascript
浅谈vue中get请求解决传输数据是数组格式的问题
Aug 03 #Javascript
VUE使用axios调用后台API接口的方法
Aug 03 #Javascript
vue cli3.0打包上线静态资源找不到路径的解决操作
Aug 03 #Javascript
js数组中去除重复值的几种方法
Aug 03 #Javascript
Vue打包部署到Nginx时,css样式不生效的解决方式
Aug 03 #Javascript
You might like
对text数据类型不支持代码页转换 从: 1252 到: 936
2011/04/23 PHP
sphinx增量索引的一个问题
2011/06/14 PHP
CodeIgniter CLI模式简介
2014/06/17 PHP
js实现拖拽 闭包函数详细介绍
2012/11/25 Javascript
如何在JavaScript中实现私有属性的写类方式(二)
2013/12/04 Javascript
jquery制作弹窗提示窗口代码分享
2014/03/02 Javascript
javascript使用switch case实现动态改变超级链接文字及地址
2014/12/16 Javascript
jQuery操作表单常用控件方法小结
2015/03/23 Javascript
html+js实现简单的计算器代码(加减乘除)
2016/07/12 Javascript
JS面试题---关于算法台阶的问题
2016/07/26 Javascript
vue下跨域设置的相关介绍
2017/08/26 Javascript
详解使用Typescript开发node.js项目(简单的环境配置)
2017/10/09 Javascript
在js代码拼接dom对象到页面上的模板总结
2018/10/21 Javascript
基于vue2的canvas时钟倒计时组件步骤解析
2018/11/05 Javascript
解决mui框架中switch开关通过js控制开或者关状态时小圆点不动的问题
2019/09/03 Javascript
js判断复选框是否选中的方法示例【基于jQuery】
2019/10/10 jQuery
详解钉钉小程序组件之自定义模态框(弹窗封装实现)
2020/03/07 Javascript
Python字符编码与函数的基本使用方法
2017/09/30 Python
python3安装speech语音模块的方法
2018/12/24 Python
详解爬虫被封的问题
2019/04/23 Python
Python解析命令行读取参数之argparse模块
2019/07/26 Python
Python安装OpenCV的示例代码
2020/03/05 Python
基于Python的图像阈值化分割(迭代法)
2020/11/20 Python
CSS3 Calc实现滚动条出现页面不跳动问题
2017/09/14 HTML / CSS
详解Html5微信支付爬坑之路
2018/07/24 HTML / CSS
html5 css3实例教程 一款html5和css3实现的小机器人走路动画
2014/10/20 HTML / CSS
西班牙床垫网上商店:Colchones.es
2018/05/06 全球购物
屈臣氏乌克兰:Watsons UA
2019/10/29 全球购物
资深财务管理人员自我评价
2013/09/22 职场文书
触摸春天教学反思
2014/02/03 职场文书
餐饮营销方案
2014/02/23 职场文书
报纸媒体创意广告词
2014/03/17 职场文书
保险公司早会主持词
2014/03/22 职场文书
学术会议通知范文
2015/04/15 职场文书
单位车辆管理制度
2015/08/05 职场文书
php实现自动生成验证码的实例讲解
2021/11/17 PHP