如何构建 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 相关文章推荐
jquery获取input的value问题说明
Aug 19 Javascript
Three.js源码阅读笔记(Object3D类)
Dec 27 Javascript
jQuery easyui datagrid动态查询数据实例讲解
Feb 26 Javascript
jquery结婚电子请柬特效源码分享
Aug 21 Javascript
jQuery实现的Tab滑动选项卡及图片切换(多种效果)小结
Sep 14 Javascript
jQuery实现为控件添加水印文字效果(附源码)
Dec 02 Javascript
JavaScript语言精粹经典实例(整理篇)
Jun 07 Javascript
AngularJS 视图详解及示例代码
Aug 17 Javascript
ajax与json 获取数据并在前台使用简单实例
Jan 19 Javascript
AngularJS改变元素显示状态
Apr 20 Javascript
vue axios 给生产环境和发布环境配置不同的接口地址(推荐)
May 08 Javascript
node实现爬虫的几种简易方式
Aug 22 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
php preg_match_all结合str_replace替换内容中所有img
2008/10/11 PHP
发款php蜘蛛统计插件只要有mysql就可用
2010/10/12 PHP
PHP实现即时输出、实时输出内容方法
2015/05/27 PHP
WordPress中制作导航菜单的PHP核心方法讲解
2015/12/11 PHP
PHP基于回溯算法解决n皇后问题的方法示例
2017/11/07 PHP
基于laravel where的高级使用方法
2019/10/10 PHP
Auntion-TableSort国人写的一个javascript表格排序的东西
2007/11/12 Javascript
JS的框架Polymer中的dom-if和is属性使用说明
2015/07/29 Javascript
基于JavaScript代码实现兼容各浏览器的设为首页和加入收藏
2016/01/07 Javascript
jquery.validate提示错误信息位置方法
2016/01/22 Javascript
JavaScript中ES6 Babel正确安装过程
2016/07/18 Javascript
DOM 事件的深入浅出(二)
2016/12/05 Javascript
EditPlus中的正则表达式 实战(4)
2016/12/15 Javascript
jQuery在header中设置请求信息的方法
2017/03/06 Javascript
JavaScript 事件流、事件处理程序及事件对象总结
2017/04/01 Javascript
BootStrap模态框和select2合用时input无法获取焦点的解决方法
2017/09/01 Javascript
webpack5 联邦模块介绍详解
2020/07/08 Javascript
vue Treeselect 树形下拉框:获取选中节点的ids和lables操作
2020/08/15 Javascript
js实现滑动进度条效果
2020/08/21 Javascript
使用python BeautifulSoup库抓取58手机维修信息
2013/11/21 Python
Python的Tornado框架异步编程入门实例
2015/04/24 Python
python高手之路python处理excel文件(方法汇总)
2016/01/07 Python
python使用标准库根据进程名如何获取进程的pid详解
2017/10/31 Python
快速解决安装python没有scripts文件夹的问题
2018/04/03 Python
python多线程高级锁condition简单用法示例
2019/11/07 Python
Pytorch技巧:DataLoader的collate_fn参数使用详解
2020/01/08 Python
tensorflow模型保存、加载之变量重命名实例
2020/01/21 Python
美国知名的女性服饰品牌:LOFT(洛芙特)
2016/08/05 全球购物
Keds加拿大官网:购买帆布运动鞋和皮鞋
2019/09/26 全球购物
PHP笔试题
2012/02/22 面试题
简单叙述一下MYSQL的优化
2016/05/09 面试题
婚礼答谢宴主持词
2014/03/14 职场文书
群众路线调研报告范文
2014/11/03 职场文书
2015年酒店工作总结范文
2015/04/07 职场文书
SpringBoot SpringEL表达式的使用
2021/07/25 Java/Android
彩虹社八名人气艺人全新周边限时推出,性转女装男装一次拥有!
2022/04/01 日漫