热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

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

这篇文章主要介绍了如何构建vue-ssr项目的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

如何通过 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: `
访问的 URL 是: {{ url }}
` }) renderer.renderToString(app, (err, html) => { if (err) { ctx.status = 500 ctx.body = err.toString() } ctx.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 支持使用模版的方式。

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

注释 -- 这里将是应用程序 HTML 标记注入的地方。也就是 renderToString 回调中的 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:"
访问路径{{url}}
" }) 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 文件。

假设我们以及打包好了这两份文件,我们来看 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,然后使他们成为动态的(能够响应后续的数据变化)。 如果你检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:

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 项目内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • 延迟注入工具(python)的SQL脚本
    本文介绍了一个延迟注入工具(python)的SQL脚本,包括使用urllib2、time、socket、threading、requests等模块实现延迟注入的方法。该工具可以通过构造特定的URL来进行注入测试,并通过延迟时间来判断注入是否成功。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 在Android中解析Gson解析json数据是很方便快捷的,可以直接将json数据解析成java对象或者集合。使用Gson解析json成对象时,默认将json里对应字段的值解析到java对象里对应字段的属性里面。然而,当我们自己定义的java对象里的属性名与json里的字段名不一样时,我们可以使用@SerializedName注解来将对象里的属性跟json里字段对应值匹配起来。本文介绍了使用@SerializedName注解解析json数据的方法,并给出了具体的使用示例。 ... [详细]
  • 本文介绍了一个免费的asp.net控件,该控件具备数据显示、录入、更新、删除等功能。它比datagrid更易用、更实用,同时具备多种功能,例如属性设置、数据排序、字段类型格式化显示、密码字段支持、图像字段上传和生成缩略图等。此外,它还提供了数据验证、日期选择器、数字选择器等功能,以及防止注入攻击、非本页提交和自动分页技术等安全性和性能优化功能。最后,该控件还支持字段值合计和数据导出功能。总之,该控件功能强大且免费,适用于asp.net开发。 ... [详细]
  • 本文介绍了一种图片处理应用,通过固定容器来实现缩略图的功能。该方法可以实现等比例缩略、扩容填充和裁剪等操作。详细的实现步骤和代码示例在正文中给出。 ... [详细]
  • 本文介绍了互联网思维中的三个段子,涵盖了餐饮行业、淘品牌和创业企业的案例。通过这些案例,探讨了互联网思维的九大分类和十九条法则。其中包括雕爷牛腩餐厅的成功经验,三只松鼠淘品牌的包装策略以及一家创业企业的销售额增长情况。这些案例展示了互联网思维在不同领域的应用和成功之道。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 单点登录原理及实现方案详解
    本文详细介绍了单点登录的原理及实现方案,其中包括共享Session的方式,以及基于Redis的Session共享方案。同时,还分享了作者在应用环境中所遇到的问题和经验,希望对读者有所帮助。 ... [详细]
  • 使用正则表达式爬取36Kr网站首页新闻的操作步骤和代码示例
    本文介绍了使用正则表达式来爬取36Kr网站首页所有新闻的操作步骤和代码示例。通过访问网站、查找关键词、编写代码等步骤,可以获取到网站首页的新闻数据。代码示例使用Python编写,并使用正则表达式来提取所需的数据。详细的操作步骤和代码示例可以参考本文内容。 ... [详细]
author-avatar
mobiledu2502896807
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有