vue 服务端渲染(二):入门

上一篇文章介绍了vue ssr 的实现思路和打包流程,实现了一个简单版的 ssr。这一篇将实现客户端和服务端分离打包,根据不同端的配置打包出来不同的端文件。服务端返回打包出来 html 的内容,然后配合客户端打包出来的 js 逻辑,来实现服务端渲染。

具体的实现可以参照官方文档,本文也是照搬照抄。。。
Vue SSR 指南
https://ssr.vuejs.org/zh/

新建工程

新建srcpublic目录,public 目录有两个 html 模板文件,index.ssr.html 不同的是需要引入 ssr 的标记,表示服务端渲染,目录结果如下:

.
├── public
│   └── index.html // 客户端模板
│   └── index.ssr.html // 服务端模板
├── build // webpack打包配置
│   └── webpack.base.js // 公用配置
│   └── webpack.client.js // 客户端配置
│   └── webpack.server.js // 服务端配置
├── src
│   ├── app.js  // app 入口文件
│   ├── App.vue // page入口
│   ├── client-entry.js  // 客户端打包入口文件
│   ├── server-entry.js // 服务端打包入口文件
│   ├── components
│   │   ├── Foo.vue
│   │   ├── Bar.vue
├── server.js

客户端配置

这一步先实现将客户端应用运行起来。webpack的配置需要使用如下的包:

  • webpack webpack-cli webpack-dev-server(webpack相关包)
  • html-webpack-plugin(html模板插件,将打出来的文件直接插入模板中)
  • 解析css文件: vue-style-loader(支持服务端渲染) css-loader vue-template-compiler
  • 解析js文件:@babel/core @babel/preset-env babel-loader
  • 解析vue文件:vue-loader
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin vue-loader vue-style-loader css-loader vue-template-compiler @babel/core @babel/preset-env babel-loader -D

配置webpack文件

配置webpack.base.js

// webpack.base.js
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')

const resolve = dir => {
  return path.resolve(__dirname, dir)
}

module.exports = {
  mode: 'production',
  output: {
    filename: '[name].bundle.js', // 入口文件
    path: resolve('../dist'), // 出口目录
  },
  resolve: {
    extensions: ['.js', '.vue', '.css', '.jsx'] // 引入文件时省略后缀
  },
  module: {
    rules: [
      {
        test: /\.vue$/, // 解析vue文件
        use: 'vue-loader'
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader', 'css-loader'] // loader的执行顺序是从上到下,从右到左
      },
      {
        test: /\.js$/, // 解析es6以上文件
        use: {
          options: { // babel 配置
            presets: ['@babel/preset-env'] // 将es6转化为es5
          },
          loader: 'babel-loader' // babel-loader 会默认调babel-core
        },
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
  ]
}

配置webpack.client.js

// webpack.client.js
const base = require('./webpack.base')
const { merge } = require('webpack-merge') // 合并webpack配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') // 客户端映射插件

const path = require('path')
const resolve = dir => {
  return path.resolve(__dirname, dir)
}

module.exports = merge(base, {
  entry: {
    client: resolve('../src/client-entry.js') // 客户端打包入口文件
  },
  plugins: [
    new VueSSRClientPlugin(), // 打包出来的是一个映射json文件,不需要写死引入client.bundle.js,因为打包出来的名字可能不是固定的,带有hash值的
    // 客户端打包其实不需要html,因为用的是服务端打包出来的index.ssr.html
// 因为我们需要先预览保证客户端能跑通,所以先留着
    new HtmlWebpackPlugin({
      template: resolve('../public/index.html')
    }),
  ]
})

配置webpack.server.js

const base = require('./webpack.base')
const { merge } = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

const path = require('path')
const { node } = require('./webpack.client')
const resolve = dir => {
  return path.resolve(__dirname, dir)
}

module.exports = merge(base, {
  entry: {
    server: resolve('../src/server-entry.js'), // server端入口文件
  },
  output: {
    libraryTarget: 'commonjs2', // 打包出来按照module.exports方式
  },
  target: 'node', // 服务端打出来的文件是要给node服务用的
  plugins: [
    new VueSSRServerPlugin(), // 打包出来的是服务端的映射
    new HtmlWebpackPlugin({
      filename: 'index.ssr.html',
      template: resolve('../public/index.ssr.html'),
      minify: false, // 不压缩,这样打包的时候就不会把ssr的注释标记给删掉。默认打出来的文件index.html
      excludeChunks: ['server'], // 排除引入文件,因为服务端引入的是客户端打包出来的文件
    }),
  ]
})

public 模板文件

index.html

// index.html



  
  
  Document


  

index.ssr.html

// index.ssr.html



  
  
  ssr html


  


components组件

这里面暂时写了两个组件,一个写了样式,一个写了事件绑定,主要是为了验证服务端渲染是不是可以用。

// Bar.vue


// Foo.vue


App.vue文件

// App.vue


app.js文件

这里的写法需要注意的是,返回一个函数,函数里返回app实例,这样写是为了客户端访问服务器的时候 可以产生多个实例,这样每个实例都是独立的。之前客户端渲染不需要这样写,是因为本身每个浏览器访问都会产生不同的实例。

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

// const vm = new Vue({
//   el: '#app',
//   render: h => h(App)
// })
// 1. 客户端渲染的时候每打开一个浏览器都会产生一个vue的实例,
// 而服务器如果按照这样的写法,会在所有人访问时都产生同样的实例,
// 所有app.js一定要导出一个函数,每次访问都产生新的实例

export default () => {
  const app = new Vue({    render: h => h(App)
  })

  return {// 返回一个对象,后续会加入router等
    app
  }
}

client入口 client-entry.js

// src/client-entry.js
import createApp from './app'
const { app } = createApp()

app.$mount('#app')

server入口 server-entry.js

// src/server-entry.js
import createApp from './app'

// 服务端入口导出函数,每次请求进来返回的都是全新

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

server.js 启动文件

// server.js
const Koa = require('koa')
const Router = require('@koa/router')
const { createBundleRenderer } = require('vue-server-renderer')
const fs = require('fs')
const path = require('path')
const static = require('koa-static')

const app = new Koa()
const router = new Router()

// 换一种方式:json
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const template = fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf8')
const render = createBundleRenderer(serverBundle, {
  template,
  clientManifest // 通过后端注入前端的js脚本
})

router.get('/', async (ctx) => {
  // 在渲染页面的时候,需要让服务器根据当前路径渲染对应的路由
  ctx.body = await render.renderToString()
})

app.use(router.routes())
app.use(static(path.resolve(__dirname, 'dist'))) // 静态文件查找路径
app.listen(3006)

配置打包命令 package.json

// package.json
"start": "nodemon server.js",
"client:dev": "webpack-dev-server --config ./build/webpack.client.js",
"client:build": "webpack --config ./build/webpack.client.js --watch",
"server:build": "webpack --config ./build/webpack.server.js --watch",
"build:all": "concurrently \"npm run client:build\" \"npm run server:build\""

因为服务端要引入客户端打包的文件,所以需要同时打包,可以使用concurrently包,这个包可以同时启动多个命令,安装npm install concurrently -D,如下命令同时启动客户端和服务端打包:
"build:all": "concurrently \"npm run client:build\" \"npm run server:build\""

启动

以上就是全部配置和代码实现,现在我们先启动npm run client:dev,访问http://localhost:8080/看看客户端跑通之后的效果:


可以看到客户端已经跑通了。
接下来再看看服务端,同时打包客户端和服务端代码,运行 npm run build:all

打包出来的文件如下:

现在运行server.js 看一下ssr的效果:
npm start,访问http://localhost:3006/

可以看到服务端也正常启动了,事件交互也没问题。

这一步基本的 ssr 实现就算通过了。

但是到这里,还是有问题的,因为服务端如果直接刷新非根路由,页面是会报 404 的,因为我们在 server.js 中只处理了/路由,下一篇我们会加上 vue-router 和 store 来完善应用。

github:https://github.com/mxcz213/vue-ssr-demo/tree/part-two

你可能感兴趣的:(vue 服务端渲染(二):入门)