美文网首页Vue
vue 服务端渲染(二):入门

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

作者: 梦想成真213 | 来源:发表于2020-09-30 16:43 被阅读0次

    上一篇文章介绍了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
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app"></div>
    </body>
    </html>
    

    index.ssr.html

    // index.ssr.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>ssr html</title>
    </head>
    <body>
      <!--vue-ssr-outlet-->
    </body>
    </html>
    

    components组件

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

    // Bar.vue
    <template>
      <div>
        bar
      </div>
    </template>
    <style scoped>
    div{
      background: #f00;
    }
    </style>
    
    // Foo.vue
    <template>
      <div @click="counter += 1">
        foo{{ counter }}
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          counter: 0
        }
      },
      method: {
      }
    }
    </script>
    

    App.vue文件

    // App.vue
    <template>
      <div id="app">
        <Bar />
        <Foo />
      </div>
    </template>
    <script>
    import Bar from './components/Bar.vue'
    import Foo from './components/Foo.vue'
    export default {
      components: {
        Bar,
        Foo
      }
    }
    </script>
    

    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 服务端渲染(二):入门

        本文链接:https://www.haomeiwen.com/subject/rpwzyktx.html