陌小路的个人博客 陌小路的个人博客
首页
  • 技术专区

    • 面试
    • Vue
    • Electron
    • TypeScript
    • Serverless
    • GraphQL
  • 我的秋招之旅
  • 2019年终总结
Todo
收藏夹
关于作者
GitHub

陌小路

前端切图仔
首页
  • 技术专区

    • 面试
    • Vue
    • Electron
    • TypeScript
    • Serverless
    • GraphQL
  • 我的秋招之旅
  • 2019年终总结
Todo
收藏夹
关于作者
GitHub
  • Vue

    • Vue3-beta-初体验
    • Vue-nextTick源码分析
    • Vuex-源码分析01
    • Vuex-源码分析02
    • Vite源码分析
    • Vue服务端渲染

      • 介绍
      • 快速上手
      • Vue项目添加SSR
        • 服务端搭建
        • 一键式启动项目与热更新
        • 总结
      • Petite-Vue
    • React

    • 面试

    • Electron

    • Serverless

    • GraphQL

    • TypeScript

    • RxJS

    • 工程化

    • Webpack

    • Nestjs

    • WebRTC & P2P

    • Docker

    • Linux

    • Git

    • Svelte

    • 踩坑日记 & 小Tips

    • 其他

    • technology
    • Vue
    陌小路
    2021-01-24
    涉及技术栈
    整体架构
    准备工作
    前端部分
    webpack配置

    Vue项目添加SSR

    前面我们介绍完怎么将一个简单的Vue实例进行渲染给客户端之后,接下来我们要继续深入了,毕竟我们实际项目中,应该不存在单纯这么简单的业务代码吧。

    # 涉及技术栈

    • node.js
    • Vue
    • express
    • webpack

    # 整体架构

    vue-ssr流程.png

    # 准备工作

    在正式开始介绍如何配合现有Vue项目实现SSR之前,我们先初始化一个简易的Vue项目。你可以使用Vue-cli创建一个简易项目,也可以跟着笔者直接自己借鉴官方项目结构创建一个简单的项目。

    当然也可直接clone笔者的演示项目(建议):项目地址

    # 项目目录

    首先新建一个文件夹,作为我们整个项目的根目录,接着在命令行中输入npm init -y,初始化npm,然后按照按照如下目录结构创建对应的文件,创建时我们先不用关心各个文件中内容是什么,后面将分逐一进行讲解。

    ├─.babelrc
    ├─entry-client.js
    ├─entry-server.js
    ├─index.html
    ├─package.json
    ├─server.js
    ├─src
    |  ├─app.js
    |  ├─App.vue
    |  ├─store
    |  |   ├─actions.js
    |  |   ├─index.js
    |  |   └mutations.js
    |  ├─router
    |  |   └index.js
    |  ├─components
    |  |     ├─Foo.vue
    |  |     └Home.vue
    ├─build
    |   ├─webpack.base.config.js
    |   ├─webpack.client.config.js
    |   └webpack.server.config.js
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    # 前端部分

    这里主要写了两个页面,一个首页一个额外的页面,内容也很简单,主要为了演示路由。

    首页部分,笔者这里定义了一个asyncData,用于暴露给服务端渲染时预取数据。

    <template>
        <div class=''>
            <h1>{{title}}</h1>
            这里是Home
        </div>
    </template>
    
    <script>
    
    import { mapGetters } from 'vuex';
    import * as actions from '@/store/actions';
    
    export default {
        name: 'Home',
        computed: {
            ...mapGetters([
                'title'
            ])
        },
        asyncData({store}) {
            return store.dispatch({
                type: actions.FETCH_TITLE
            })
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    笔者在这个asyncData函数中分发了一个action用于异步获取数据,该函数只会在服务端执行,在服务端渲染的时候会去调用这个函数预取数据,我们可以大概看一下这个异步action具体实现:

    import * as mutations from './mutations';
    
    export const FETCH_TITLE = 'FETCH_TITLE'
    
    export default {
        [FETCH_TITLE]({commit}) {
            return new Promise(resolve => {
                setTimeout(() => {
                    commit(mutations.SET_TITLE, '这里是服务端渲染的title')
                    resolve();
                }, 3000)
            })
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    这里直接就用延时函数延时了三秒模拟异步请求,所以我们要等三秒之后才能看到页面中出现这里是服务端渲染的title。当然也是因为这个因素,在进行服务端渲染的时候,我们访问页面首页路由的时候需要等待三秒,服务器才会响应页面,因为它需要做数据预取操作,这个过程完成之后才能渲染出完整页面。

    也就是说如果你想要设定一些预取得数据,你可以定义一个asyncData用于满足需求。具体服务端渲染配合实现请接着往下看。

    # webpack配置

    如果想要对一个完整的Vue项目添加SSR,我们需要先对它进行打包,然后将结果作为我们服务器提供SSR服务的依赖文件。

    我们可以注意到,在上述的文件目录中,有一个build,目录,它就是用来放置我们的webpack相关配置的。这里我们可以再回过头回想一下前面放出来的官方SSR整体的流程图,我们可以清晰的知道,我们在在配置webpack客户端与服务端相关配置文件时,同时也需要创建对应的入口文件,也就上上述目录中的entry-client.js与entry-server.js。

    # entry-client.js

    客户端入口文件。

    import createApp from '@/app.js';
    
    const { app, store } = createApp();
    
    if (window.__INITIAL_STATE__) {
        store.replaceState(window.__INITIAL_STATE__)
    }
    
    app.$mount('#app');
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    这里导入的地方笔者用了@,这个是笔者配置了webpack的别名,相当于根目录下src目录,主要为了省略一点路径,也不用一层层找了。

    从整体代码来看,笔者这里写的也比较简单了,主要功能就是挂载Vue实例($mount),在整个渲染过程中也叫做客户端激活,同时将服务端预取的数据保存到Vuex中,这个过程主要通过调用replaceState方式,将服务端挂载在 window上的__INITIAL_STATE__替换 store 的根状态。

    这里的createApp方法主要用于创建一个新的Vue实例,并可以获取到挂载到Vue实例上的VueRouter或Vuex实例对象,根据我们需要,去做一些初始化的操作。

    这里说明一下为什么需要把这个操作抽离成一个单独的函数,因为对于服务端而言,如果不对每个用户创建一个全新的实例,那么就会出现多个请求共享一个实例的情况,这个时候就会很容易导致交叉请求状态污染,所以我们需要对每个请求创建一个新的实例。

    # createApp

    如果你是通过脚手架工具创建了一个新的项目,那么你需要将原有的src目录下的index.js改为app.js,并暴露一个createApp方法。

    import Vue from 'vue';
    import App from './App'
    import VueRouter from 'vue-router';
    import routes from './router'
    import Vuex from 'vuex';
    import storeConfig from './store';
    
    Vue.use(VueRouter);
    Vue.use(Vuex);
    
    const store = new Vuex.Store(storeConfig);
    const router = new VueRouter({
        routes,
        mode: 'history'
    })
    
    export default function createApp() {
        const app = new Vue({
            router,
            store,
            render: h => h(App)
        })
        return { app, router, store }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    从结构上看,和我们之前在普通Vue项目中index.js文件里编写的相关逻辑差别不大,都是进行根实例初始化的一些操作,唯一的区别是将原来直接new Vue(...)这部分逻辑转移到createApp这个函数当中提供给外部调用,用于产生新的实例,返回创建好的Vue实例和VueRouter、Vuex实例等。

    # entry-server.js

    服务端入口文件。

    import createApp from '@/app.js';
    
    export default context => {
        return new Promise((resolve, reject) => {
            const { app, router, store } = createApp();
            router.push(context.url);
            router.onReady(() => {
                const matchedComponents = router.getMatchedComponents();
                if (!matchedComponents.length) reject({code: 404});
    
                Promise.all(matchedComponents.map(component => {
                    return component.asyncData && component.asyncData({store, route: router.currentRoute})
                })).then(() => {
                    context.state = store.state;
                    resolve(app);
                }).catch(reject);
            }, reject)
        })
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    同样我们需要调用createApp获取我们需要的实例对象,同时导出一个方法供服务端调用,并且该方法返回一个Promise,因为我们需要执行一些异步操作,比如预取数据等操作。

    方法接收一个context参数,这里笔者主要用于获取当前请求的路由路径,并调用router实例的push方法,将路由置为用户当前请求的路由路径,同时,当路由跳转准备好之后,进行路由组件的匹配,获取到该路由下需要用到的组件列表,如果没有匹配到则直接返回404,否则遍历所有组件,调用组件的asyncData方法,把需要进行预取得数据准备好。

    当所有组件的数据准备完毕之后,将当前store.state挂载到context的state属性上,以便于在渲染模板时,在模板中添加一个script标签,内容为:window.__INITIAL_STATE__= xxx,这里的xxx就是我们的store.state,这样我们就能在客户端入口文件中通过这个window上的属性,初始化客户端的state。

    也就是我们上面entry-client.js中调用replaceState部分要用到的数据。

    # webpack.base.config.js

    对于我们整个项目来说,服务端webpack配置与客户端webpack配置也会存在一些公共配置,所以我们可以将共有部分抽出来,作为基础配置,最后合并到特定端的配置中。

    来看看都有啥:

    
    const VueLoaderPlugin = require('vue-loader/lib/plugin');
    const path = require('path');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const webpack = require('webpack')
    
    module.exports = {
        mode: 'development',
        module: {
            rules: [
                { test: /\.js$/, loader: 'babel-loader' },
                { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] },
                { test: /\.scss$/, loader: 'sass-loader' },
                { test: /\.vue$/, loader: 'vue-loader' },
            ]
        },
        output: {
            path: path.resolve(__dirname, '../dist')
        },
        plugins: [
            new VueLoaderPlugin(),
            new MiniCssExtractPlugin({
                filename: 'css/[name].[contenthash:8].css'
            }),
            new webpack.NoEmitOnErrorsPlugin()
        ],
        stats: {
            logging: 'none'
        },
        resolve: {
            extensions: ['.vue', '.ts', '.js'],
            alias: {
                '@': path.resolve(__dirname, '../src')
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

    这里对每个参数的含义就不进行过多介绍了,对于webpack配置项不太了解的可以参考相关中文文档。传送门:webpack中文网

    # loader

    这里主要用到了几个loader,分别是:

    • babel-loader:用来转换高级语法为低级语法,这里相关的配置笔者就将它写到.babelrc文件中了,参见上述目录结构。
      • 具体内容如下:
        •   {
                "presets": ["@babel/preset-env"],
                "plugins": ["@babel/plugin-syntax-dynamic-import"]
            }
          
          1
          2
          3
          4
    • MiniCssExtractPlugin.loader与css-loader:处理css相关内容(具体用法见官方文档)。
    • sass-loader:笔者在项目中比较喜欢使用scss,所以这里添加了对scss的处理。
    • vue-loader:对于Vue项目来说,这个loader应该还是很重要的吧,用来处理.vue文件。
    # plugins
    • VueLoaderPlugin:必须的插件, 它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。例如,如果你有一条匹配 /\.js$/ 的规则,那么它会应用到 .vue 文件里的 <script> 块。
    • MiniCssExtractPlugin:将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。
    • NoEmitOnErrorsPlugin:在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段。
    # 安装相关依赖

    笔者比较喜爱使用yarn进行包安装,你也可以采用npm或cnpm,只需要将下面的yarn add改成npm i即可。

    yarn add vue-loader babel-loader mini-css-extract-plugin webpack@4 webpack-cli sass-loader @babel/preset-env @babel/plugin-syntax-dynamic-import vue-template-compiler css-loader -D
    
    1

    这里还是建议搭建直接克隆笔者的项目比较方便,万一依赖项笔者漏写了,估计你们要锤死我。

    # webpack.client.config.js

    客户端构建相关配置项:

    const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
    const webpackMerge = require('webpack-merge');
    const baseConfig = require('./webpack.base.config');
    const path = require('path')
    const TerserPlugin = require("terser-webpack-plugin");
    const webpack = require('webpack')
    
    module.exports = webpackMerge.merge(baseConfig, {
        entry: path.resolve(__dirname, '../entry-client.js'),
        optimization: {
            splitChunks: {
                cacheGroups: {
                    common: {
                        minChunks: 2,
                        priority: -10,
                        reuseExistingChunk: true
                    }
                }
            },
            minimize: true,
            minimizer: [new TerserPlugin()]
        },
        plugins: [
            new VueSSRClientPlugin()
        ]
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    # plugins

    这里基于基础配置上,添加了热替换的插件和Vue SSR客户端构建插件,上面entry部分的写法是为了给我们的项目添加热更新能力,这里主要需要配合webpack-hot-middleware进行实现,具体配置方式可以参考官方文档:传送门 。

    这里笔者也配置了一下代码分割,将公共代码进行抽离,并改用terser-webpack-plugin对代码进行压缩(webpack5之后内置的,这里采用webpack4作为演示)。

    # 安装依赖

    在安装完基础配置文件的依赖后,客户端相关配置也需要进行依赖安装:

    yarn add webpack-merge terser-webpack-plugin webpack-hot-middleware -D
    
    1

    # webpack.server.config.js

    同理,这里是服务端相关配置。

    const webpackMerge = require('webpack-merge');
    const baseConfig = require('./webpack.base.config');
    const path = require('path');
    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
    const nodeExternals = require('webpack-node-externals')
    
    module.exports = webpackMerge.merge(baseConfig, {
        entry: path.resolve(__dirname, '../entry-server.js'),
        output: {
            path: path.resolve(__dirname, '../dist'),
            filename: 'server-bundle.js',
            libraryTarget: 'commonjs2'
        },
        target: 'node',
        externals: nodeExternals({
            allowlist: [/\.css$/]
        }),
        devtool: 'source-map',
        plugins: [new VueSSRServerPlugin()]
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    对于服务端相关配置来说,我们这里就不需要配置热更新相关了,所以这里只用到了一个官方提供用来构建服务端配置的插件server-plugin,然后我们这里配置了externals,对于服务端来说,它无法处理css相关逻辑,所以我们这里直接给他忽略一下。同时,这里还有一个注意点,我们需要把构建的目标改成node,也就是设置target: 'node',不仅如此,这里还需要配置libraryTarget: 'commonjs2',以便我们在node端进行导入。

    # 安装依赖
    yarn add webpack-node-externals -D
    
    1

    这里就新增了一个依赖项。

    好了,介绍完webpack配置相关之后,我们就可以分别构建出服务端需要的结果和客户端相关的结果了,离成功又近了一步。

    # 执行打包构建

    这里推荐将构建命令写入到package.json文件中,笔者这里将执行两端代码构建命令浓缩成一句:

    "scripts": {
      "build": "npm run build:client & npm run build:server",
      "build:server": "webpack --config ./build/webpack.server.config.js",
      "build:client": "webpack --config ./build/webpack.client.config.js"
    }
    
    1
    2
    3
    4
    5

    这样在package.json配置好之后,我们就可以直接执行一个命令就可以启动构建流程了:

    npm run build
    
    1
    编辑
    上次更新: 2023/11/25, 4:11:00
    快速上手
    服务端搭建

    ← 快速上手 服务端搭建→

    最近更新
    01
    github加速
    01-01
    02
    在线小工具
    01-01
    03
    Lora-Embeddings
    11-27
    更多文章>
    Theme by Vdoing | Copyright © 2020-2025 STDSuperman | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式