搭建自己的SSR

一、VUE SSR是什么

  • 官方文档:https://ssr.vuejs.org/
  • Vue SSR(Vue.js Server-Side Rendering) 是 Vue.js 官方提供的一个服务端渲染(同构应用)解决方案
  • 使用它可以构建同构应用
  • 还是基于原有的 Vue.js 技术栈

官方文档的解释:Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器客户端上运行

二、使用场景

在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。

技术层面:

  • 更快的首屏渲染速度
  • 更好的 SEO

业务层面:

  • 不适合管理系统
  • 适合门户资讯类网站,例如企业官网、知乎、简书等
  • 适合移动网站

三、如何实现 Vue SSR

(1)基于 Vue SSR 官方文档提供的解决方案

官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中,也会对Vue SSR有更加深入的了解。

该方式需要你熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。

(2)Nuxt.js 开发框架

NUXT提供了平滑的开箱即用的体验,它建立在同等的Vue技术栈之上,但抽象出很多模板,并提供了一些额外的功能,例如静态站点生成。通过 Nuxt.js 可以快速的使用 Vue SSR 构建同构应用。

四、Vue SSR 基本使用

接下来我们以 Vue SSR 的官方文档为参考,来学习一下它的基本用法。

渲染一个 Vue 实例

  • mkdir vue-ssr
  • cd vue-ssr
  • npm init -y
  • npm i vue vue-server-renderder

server.js

const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const app = new Vue({
  template: `
    

{{message}}

`, data: { message: '肖战' } }) renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) })

node server.js,运行结果:

肖战

data-server-rendered="true"这个属性是为了将来客户端渲染激活接管的接口

五、结合到Web服务器中

使用express对所有的get请求都做同样的处理,new一个Vue,使用vue-server-renderer的renderToString的方法传入Vue实例,回调函数中的html就是最终得到的DOM结构

server.js

const Vue = require('vue')
const express = require('express')

const renderer = require('vue-server-renderer').createRenderer()

const server = express()

server.get('/', (req, res) => {
  const app = new Vue({
    template: `
      

{{message}}

`, data: { message: '肖战' } }) renderer.renderToString(app, (err, html) => { if (err) { return res.status(500).end('Internal Server Error.') } res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,防止乱码 res.end(` Document ${html} `) }) }) server.listen(3000, () => { console.log('server running at port 3000...') })

六、使用HTML模板

1. 创建HTML模板文件




  
  
  Document


  



是占位符,为了接收将来要渲染的变量,不能写错,不能有多余的空格

2. js代码中的createRenderer方法指定模板文件

server.js

const Vue = require('vue')
const express = require('express')
const fs = require('fs')

const renderer = require('vue-server-renderer').createRenderer({
  // 这里指定模板文件
  template: fs.readFileSync('./index.template.html', 'utf-8')
})

const server = express()

server.get('/', (req, res) => {
  const app = new Vue({
    template: `
      

{{message}}

`, data: { message: '拉钩教育' } }) renderer.renderToString(app, (err, html) => { // 此处的html参数是被模板文件处理过了的,可以直接输出到用户的页面上 if (err) { return res.status(500).end('Internal Server Error.') } res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,防止乱码 res.end(html) }) }) server.listen(3000, () => { console.log('server running at port 3000...') })

七、在模板中使用外部数据

Index.template.html




  
  
  {{{ meta }}}
  {{ title }}


  



使用两个花括号可以数据外部数据变量,而标签也会进行转义后输出在页面上。此时可以使用三个花括号原样输出数据,不会对标签进行转义处理

在js代码中给renderer.renderToString增加第二个参数为外部数据对象

renderer.renderToString(app, {
    title: '拉勾教育',
    meta: `
      
    `
  }, (err, html) => {
    if (err) {
      return res.status(500).end('Internal Server Error.')
    }
    res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,防止乱码
    res.end(html)
  })

八、构建配置-基本思路

1、流程图
搭建自己的SSR_第1张图片
2、源码结构

src
├── components
│   ├── Foo.vue
│   ├── Bar.vue
│   └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器

App.vue





app.js 是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js 简单地使用 export 导出一个 createApp 函数:

import Vue from 'vue'
import App from './App.vue'

// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {
  const app = new Vue({
    // 根实例简单的渲染应用程序组件。
    render: h => h(App)
  })
  return { app }
}

entry-client.js 客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:

import { createApp } from './app'

// 客户端特定引导逻辑……

const { app } = createApp()

// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')

entry-server.js 服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。

import { createApp } from './app'

export default context => {
  const { app } = createApp()
  return app
}

3、安装依赖
(1) 安装生产依赖

npm i vue vue-server-renderer express cross-env

说明
vue Vue.js核心库
vue-server-renderer Vue服务端渲染工具
express 基于Node的webpack服务框架
cross-env 通过npm scripts设置跨平台环境变量

(2) 安装开发依赖

npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url-loader file-loader rimraf vue-loader vue-template-compiler friendly-errors-webpack-plugin

说明
webpack webpack核心包
webpack-cli webpack的命令行工具
webpack-merge webpack配置信息合并工具
webpack-node-externals 排除webpack中的Node模块
rimraf 基于Node封装的一个跨平台rm -rf工具
friendly-errors-webpack-plugin 友好的webpack错误提示
@babel/core
@babel/plugin-transform-runtime
@babel/preset-envbabel-loader
Babel相关工具
vue-loader
vue-template-compiler
处理.vue资源
file-loader 处理字体资源
css-loader 处理CSS资源
url-loader 处理图片资源

4、webpack配置文件及打包命令

(1) 初始化webpack打包配置文件

build
|---webpack.base.config.js # 公共配置
|---webpack.client.config.js # 客户端打包配置文件
|---webpack.server.config.js # 服务端打包配置文件

webpack.base.config.js

/**
 * 公共配置
 */
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const resolve = file => path.resolve(__dirname, file)

const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  mode: isProd ? 'production' : 'development',
  output: {
    path: resolve('../dist/'),
    publicPath: '/dist/',
    filename: '[name].[chunkhash].js'
  },
  resolve: {
    alias: {
      // 路径别名,@ 指向 src
      '@': resolve('../src/')
    },
    // 可以省略的扩展名
    // 当省略扩展名的时候,按照从前往后的顺序依次解析
    extensions: ['.js', '.vue', '.json']
  },
  devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
  module: {
    rules: [
      // 处理图片资源
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            },
          },
        ],
      },

      // 处理字体资源
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          'file-loader',
        ],
      },

      // 处理 .vue 资源
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },

      // 处理 CSS 资源
      // 它会应用到普通的 `.css` 文件
      // 以及 `.vue` 文件中的 `


将数据预取同步到客户端
entry-server.js

// entry-server.js
import { createApp } from './app'

export default async context => {
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
    // 以便服务器能够等待所有的内容在渲染前,
    // 就已经准备就绪。
    const { app, router, store } = createApp()

    const meta = app.$meta()

    // 设置服务器端 router 的位置
    router.push(context.url)

    context.meta = meta

    // 等到 router 将可能的异步组件和钩子函数解析完
    await new Promise(router.onReady.bind(router))

    // 这个rendered函数会在服务端渲染完毕之后被调用
    context.rendered = () => {
      // Renderer会把 context.state 数据对象内联到页面模板中
      // 最终发送到客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state
      // 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中 
      context.state = store.state
    }

    return app
}

entry-client.js

// entry-client.js

import { createApp } from './app'

const { app, router, store } = createApp()

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

router.onReady(() => {
  app.$mount('#app')
})

你可能感兴趣的:(javascript)