实现ssr服务端渲染demo

最近在研究SSR服务器端渲染,自己写了的小demo。

项目布局

├── build                                       // 配置文件
│   │── webpack.base                            // 公共配置
│   │── webpack.client                          // 生成Client Bundle的配置
│   │── webpack.server                          // 生成Server Bundle的配置
├── dist                                        // 项目打包路径
├── public                                      // 模板文件
│   │── index.html                              // Client模板html文件
│   │── index.ssr.html                          // Server模板html文件
├── src                                         // 源码目录
│   ├── assets                                  // 图片目录
│   ├── components                              // 组件
│   │   ├── Bar.vue                             // Bar测试组件
│   │   ├── Foo.vue                             // Foo测试组件
│   │── App.vue                                 // Vue应用的根组件
│   │── main.js                                 //  入口基础文件
│   ├── client-entry.js                         // 浏览器环境入口
│   ├── server-entry.js                         // 服务器环境入口
│   │   ├── router.js                           // 路由配置
│   │   ├── store.js                            // vuex的状态管理
├── favicon.ico                                 // 图标

注:以防版本不对应产生的问题。package.json我也把放出来了,不过在文章的最后面

上图是Vue官方的SSR原理介绍图片。从这张图片,我们可以知道:我们需要通过Webpack打包生成两份bundle文件:

  • Client Bundle,给浏览器用。和纯Vue前端项目Bundle类似
  • Server Bundle,供服务端SSR使用,一个json文件

技术栈

vue + vuex + vue-router + webpack +ES6/7 + less + koa

拆分 Webpack 打包配置

构建文件目录

webpack.base.js 是公共配置,配置如下:

// 基础的webpack配置
// webpack专用配置
const path = require(‘path‘)
const VueLoader = require(‘vue-loader/lib/plugin‘)
const resolve = dir => {
    return path.resolve(__dirname, dir)
}
module.exports = {
    output: {
        filename: ‘[name].bundle.js‘,
        path: resolve(‘../dist‘)
    },
    resolve: {
        extensions: [‘.js‘, ‘.vue‘]
    },
    module: {
        rules: [{
                test: /\.js$/,
                use: {
                    loader: ‘babel-loader‘,
                    options: {
                        presets: [‘@babel/preset-env‘]
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [‘vue-style-loader‘, ‘css-loader‘]
            },
            {
                test: /\.vue$/,
                use: ‘vue-loader‘
            },
            {
                test: /\.less$/,
                loader: ‘vue-style-loader!css-loader!less-loader‘
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                use: {
                    loader: ‘url-loader‘,
                    options: {
                        limit: 300000,
                        name: ‘[name].[ext]?[hash]‘
                    }
                }
            }
        ]
    },
    plugins: [
        new VueLoader()
    ]
}

webpack.client.js 是生成Client Bundle的配置,配置如下:

const merge = require(‘webpack-merge‘)
const base = require(‘./webpack.base‘)
const path = require(‘path‘)
const resolve = dir => {
    return path.resolve(__dirname, dir)
}
const ClientRenderPlugin = require(‘vue-server-renderer/client-plugin‘)
const HtmlWebpackPlugin = require(‘html-webpack-plugin‘)
module.exports = merge(base, {
    entry: {
        client: resolve(‘../src/client-entry.js‘)
    },
    plugins: [
        new ClientRenderPlugin(),
        new HtmlWebpackPlugin({
            filename: ‘index.html‘,
            template: resolve(‘../public/index.html‘)
        })
    ]
})

webpack.server.js是生成Server Bundle的配置,配置如下:

const merge = require(‘webpack-merge‘)
const base = require(‘./webpack.base‘)
const path = require(‘path‘)
const resolve = dir => {
    return path.resolve(__dirname, dir)
}
const VueSSRServerPlugin = require(‘vue-server-renderer/server-plugin‘)
const HtmlWebpackPlugin = require(‘html-webpack-plugin‘)
module.exports = merge(base, {
    entry: {
        server: resolve(‘../src/server-entry.js‘)
    },
    target: ‘node‘, // 用给node来使用
    // devtool: ‘source-map‘,
    output: {
        libraryTarget: ‘commonjs2‘
    },
    plugins: [
        new VueSSRServerPlugin(),
        new HtmlWebpackPlugin({
            filename: ‘index.ssr.html‘,
            template: resolve(‘../public/index.ssr.html‘),
            excludeChunks: [‘server‘] // 排查某个模块
        }),

    ]
})

下图是我的项目文件目录

components 目录下是组件

App.vue Vue应用的根组件

client-entry.js 浏览器环境入口

server-entry.js 服务器环境入口

main.js 入口基础文件

router.js 路由配置文件

store.js vuex状态管理文件

前端渲染 Demo

前端渲染demo部分比较简单,就包含两个组件:Foo 和 Ba

Foo.vue

<template>
    <div >
        <p @click="handleClick">Foo--{{num}}-点击测试js是否正常</p>
        <p>{{this.$store.state.name}}</p>
        <p>-----图片分割线----</p>
        <img :src="logo" >
        <img src="../assets/images/kfbg.png" >
    </div>
</template>
<script>
export default {
    data(){
        return {
            num:0,
            logo: require(‘../assets/images/kfbg.png‘)
        }
    },
    asyncData(store) {
        // asyncData 方法只在服务端执行,并且只在页面组件中执行
        return store.dispatch(‘changeName‘)
    },
    mounted: function() {
        this.$store.dispatch(‘changeName‘)
    },
    methods: {
        handleClick() {
           this.num ++;
        }
    }
}
 // vue 优化 pwa+ ssr 实现预缓存效果 //vue多页面一般都用ssr写 //学而思、掘金、新闻类网站用的的ssr
</script>

Bar.vue

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

App.vue

<template>
    <div id="app">
        <p class="nav">
            <router-link :class="{‘currentClass‘:path==‘/‘}" to="/">foo</router-link>
            <router-link :class="{‘currentClass‘:path==‘/Bar‘}" to="/Bar">Bar</router-link>
        </p>
       <router-view></router-view>
    </div>
</template>
<script>
export default {
    // data(){
    //     return {
    //         path: this.$store.state.route.path
    //     }
    // },
    computed: {
        path(){
            return this.$store.state.route.path
        }
    }
}
</script>
<style lang="less" scope>
    .nav{
        text-align: center;
        display: flex;
        align-items: center;
      a{
          flex: 2;
          background: #f5f5f5;
           text-decoration:none;
           color: #333;
           &.currentClass{
               background:#f43553;
               color: #fff;
           }
      }
    }
</style>

router.js

import Vue from ‘vue‘
import Foo from ‘./components/Foo.vue‘
import VueRouter from ‘vue-router‘

Vue.use(VueRouter)

export default () => {
    const router = new VueRouter({
        mode: ‘history‘,
        routes: [
            {
                path: ‘/‘,
                component: Foo
            },
            {
                path: ‘/bar‘,
                component: () => import(‘./components/Bar.vue‘)
            },
        ]
    })
    return router
}

store.js

import Vue from ‘vue‘
import Vuex from ‘vuex‘
Vue.use(Vuex)

export default () => {
    const store = new Vuex.Store({
        state: {
            name: ‘‘
        },
        mutations: {
            changeName(state) {
                state.name = ‘yxf‘
            }
        },
        actions: {
            changeName({ commit }) {
                return new Promise((resolve, reject) => {
                    setTimeout(() => {
                        commit(‘changeName‘)
                        resolve()
                    })
                })
            }
        }
    })
    if(typeof window !== ‘undefined‘ && window.__INITIAL_STATE__) {
        store.replaceState(window.__INITIAL_STATE__)
    }
    return store
}

拆分 JS 入口

在前端渲染的时候,只需要一个入口 main.js。现在要做后端渲染,就得有两个 JS 文件:client-entry.js 和 server-entry.js 分别作为浏览器和服务器的入口。

main.js基础文件

//入口文件
import Vue from ‘vue‘
import createRouter from ‘./router‘
import App from ‘./App.vue‘
import createStore from ‘./store‘
import { sync } from ‘vuex-router-sync‘ // 把当前VueRouter状态同步到Vuex中
export default () => {
    const router = createRouter()
    const store = createStore()
    sync(store, router)
    const app = new Vue({
        router,
        store,
        render: h => h(App)
    })
    return { app, router, store }
}

client-entry.js 浏览器入口

import createApp from ‘./main‘
const { app,  router } = createApp()
router.onReady(() => {
    app.$mount(‘#app‘)
})

server-entry.js 服务器入口

import createApp from ‘./main‘
// 服务器需要调用当前这个文件产生一个vue实例

export default context => {
    // 涉及到异步组件的问题
    return new Promise((resolve, reject) => {
        const { app, router, store } = createApp()
        // 设置路由
        router.push(context.url)
        // 返回的实例应跳转到 /     如/bar
        router.onReady(() => {
            const matchs = router.getMatchedComponents()
            console.log(matchs.length)
            if(matchs.length === 0) {
                reject({ code: 404 })
            }
            // matchs匹配到所有的组件,整个都在服务端执行的
            Promise.all(
                matchs.map(component => {
                    if(component.asyncData) {
                        // asyncData 是在服务端调用的
                        return component.asyncData(store)
                    }
                })
            ).then(() => {
                // 以上all中的方法,会改变store中的state
                context.state = store.state;// 把vuex的状态挂载到上下文中,会将状态挂到window上
                resolve(app)
            }).catch(reject)
        },reject)
    })
}
// 服务器端配置好后,需要导出给node使用

模板文件

index.html client模板html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <!--vue-ssr-outlet-->
</body>
</html>

index.ssr.html server模板html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <!--vue-ssr-outlet-->
</body>
</html>

编写服务端渲染主体逻辑

Vue SSR 依赖于包 vue-server-render,它的调用支持两种入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 组件为入口,后者以打包后的 JS 文件为入口,本文采取后者。

server.js

const Koa = require(‘koa‘)
const Router = require(‘koa-router‘)
const server = new Koa()
const router = new Router()
const path = require(‘path‘)
const static = require(‘koa-static‘)
const fs = require(‘fs‘)
const { createBundleRenderer } = require(‘vue-server-renderer‘)

const serverBundle = require(‘./dist/vue-ssr-server-bundle.json‘)
//渲染打包后的结果
const template = fs.readFileSync(path.resolve(__dirname, ‘./dist/index.ssr.html‘), ‘utf8‘)
//客户端manifest.json
const clientManifest = require(‘./dist/vue-ssr-client-manifest.json‘)
const render = createBundleRenderer(serverBundle, {
    template, // 模板里必须要有 vue-ssr-outlet
    clientManifest
})
router.get(‘/‘,async ctx => {
    ctx.body = await new Promise((resolve, reject) => {
        render.renderToString({url: ‘/‘}, (err, data) => {
            if(err) reject(err);
            resolve(data);
        })
    })
   })
server.use(router.routes())

// koa 静态服务中间件
server.use(static(path.resolve(__dirname,‘./dist‘)))

server.use( async ctx => {
    try{
        ctx.body = await new Promise((resolve, reject) => {
            render.renderToString({ url: ctx.url }, (err, data) => {
                if(err) reject(err)
                resolve(data)
            })
        })
    }catch (e) {
        ctx.body = ‘404‘
    }
}) 

server.listen(3002, () => {
    console.log(‘服务器已启动!‘)
  })

项目地址:https://github.com/xiaonizi66/vue-ssr-demo

package.json

{
  "name": "ssr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "client:dev": "webpack-dev-server --config ./build/webpack.client.js --mode development",
    "client:build": "webpack --config ./build/webpack.client.js --mode production",
    "server:build": "webpack --config ./build/webpack.server.js --mode production"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "koa": "^2.7.0",
    "koa-router": "^7.4.0",
    "koa-static": "^5.0.0",
    "vue": "^2.6.10",
    "vue-loader": "^15.7.1",
    "vue-router": "^3.1.2",
    "vue-server-renderer": "^2.6.10",
    "vuex": "^3.1.1",
    "vuex-router-sync": "^5.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/preset-env": "^7.5.5",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.2.0",
    "html-webpack-plugin": "^3.2.0",
    "less": "^3.9.0",
    "less-loader": "^5.0.0",
    "url-loader": "^2.1.0",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.39.1",
    "webpack-cli": "^3.3.6",
    "webpack-dev-server": "^3.8.0",
    "webpack-merge": "^4.2.1"
  }
}

最终渲染效果:

项目运行

git clone https://github.com/xiaonizi66/vue-ssr-demo

npm install

npm run server:build

npm run cilent:build

nodemon server.js

也可在build后面加上 -- --watch 如:npm run server:build -- --watch 用来监听

原文地址:https://www.cnblogs.com/yanxiafei/p/11346829.html

时间: 2024-10-09 10:57:23

实现ssr服务端渲染demo的相关文章

从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 二十五║实战二:初探SSR服务端渲染(个人博客二)

缘起 时间真快,现在已经是这个系列教程的下半部 Vue 第 12 篇了,昨天我也简单思考了下,可能明天再来一篇,Vue 就基本告一段落了,因为什么呢,这里给大家说个题外话,当时写博文的时候,只是想给大家增加点儿学习的动力,每天提醒下,完全没有提纲或者安排说明什么的,就是按照我自己学的方向走,正好发现了一个规律就是:每一个系列正好是 1 个引子 + 12 篇正文,不知道大家对这个有没有感觉,大家可能看到我的头像就知道了,哈哈,其实我是一个红迷,正好这里机缘巧合,两个系列都形成了这样的,我自私的给自

使用 PHP 来做 Vue.js 的 SSR 服务端渲染

对于客户端应用来说,服务端渲染是一个热门话题.然而不幸的是,这并不是一件容易的事,尤其是对于不用 Node.js 环境开发的人来说. 我发布了两个库让 PHP 从服务端渲染成为可能.spatie/server-side-rendering 和 spatie/laravel-server-side-rendering适配 laravel 应用. 让我们一起来仔细研究一些服务端渲染的概念,权衡优缺点,然后遵循第一法则用 PHP 建立一个服务端渲染. 什么是服务端渲染 一个单页应用(通常也叫做 SPA

Vue服务端渲染和Vue浏览器端渲染的性能对比

Vue 2.0 开始支持服务端渲染的功能,所以本文章也是基于vue 2.0以上版本.网上对于服务端渲染的资料还是比较少,最经典的莫过于Vue作者尤雨溪大神的 vue-hacker-news.本人在公司做Vue项目的时候,一直苦于产品.客户对首屏加载要求,SEO的诉求,也想过很多解决方案,本次也是针对浏览器渲染不足之处,采用了服务端渲染,并且做了两个一样的Demo作为比较,更能直观的对比Vue前后端的渲染. talk is cheap,show us the code!话不多说,我们分别来看两个D

实例PK(Vue服务端渲染 VS Vue浏览器端渲染)

Vue 2.0 开始支持服务端渲染的功能,所以本文章也是基于vue 2.0以上版本.网上对于服务端渲染的资料还是比较少,最经典的莫过于Vue作者尤雨溪大神的 vue-hacker-news.本人在公司做Vue项目的时候,一直苦于产品.客户对首屏加载要求,SEO的诉求,也想过很多解决方案,本次也是针对浏览器渲染不足之处,采用了服务端渲染,并且做了两个一样的Demo作为比较,更能直观的对比Vue前后端的渲染. 话不多说,我们分别来看两个Demo:(欢迎star 欢迎pull request) 1.浏

追求极致的用户体验ssr(基于vue的服务端渲染)

首先这篇博客并不是ssr建议教程,需要ssr入门的我建议也不要搜索博客了,因为官网给出了详细的入门步骤,只需要step by step就可以了,这篇博客的意义是如何使用ssr,可能不同的人有不同的意见,我舍弃了ssr中的vuex和vue-router增加了redis,serverfetch等等实现了适合自己公司的业务,个人认为并不是所有的东西都值得吸收,对我来说我能用到的只是ssr将vue生成一个html和对应的js. 虾面我们来看看什么是服务端渲染? 官网给出的解释: Vue.js 是构建客户

vue服务端渲染 同构渲染

引言 自JavaScript诞生以来,前端技术发展非常迅速.移动端白屏优化是前端界面体验的一个重要优化方向,Web 前端诞生了 SSR .CSR.预渲染等技术. 十年前,几乎所有网站都使用 ASP.Java.PHP 这类做后端渲染,但后来随着 jQuery.Angular.React.Vue 等 JS 框架的崛起,开始转向了前端渲染.2014年起又兴起了同构渲染,号称是未来,集成了前后端渲染的优点,当真如此? 我们先明确三个概念: 后端渲染:后端渲染指传统的 ASP.Java 或 PHP 的渲染

React服务端渲染总结

欢迎吐槽 : ) 本demo地址( 前端库React+mobx+ReactRouter ):https://github.com/Penggggg/react-ssr.本文为笔者自学总结,有错误的地方恳请各位指出 O(∩_∩)O          序:前言.原因与思路.注意事项与问题.详解.       一.前言 为什么需要服务端渲染?什么情况下进行服务端渲染?笔者认为,当我们要求渲染时间尽量快.页面响应速度快时(优点),才会采用服务器渲染,并且应该“按需”对页面进行渲染 ——“首次加载/首屏”

Vue 2.0 服务端渲染入门

1 什么是服务端渲染 SSR server side render 就是通过后端吐模板,而不是通过前端ajax获取数据,拼接字符串. 2 为什么需要SSR 需要SEO,因为爬虫不会等待ajax结果. 客户端网络慢,加载速度慢,影响用户体验. 3 另一种解决办法 预渲染 不是一次性下载整个单页应用,预渲染只是在构建时为了特定的路由生成特定的几个静态页面 你用webpack可以很简单地通过prerender-spa-plugin来添加预渲染 4 NodeJS编写Vue的SSR 首先npm insta

使用 Vue 2.0 实现服务端渲染的 HackerNews

Vue 2.0 支持服务端渲染 (SSR),并且是流式的,可以做组件级的缓存,这使得极速渲染成为可能.同时, 和 2.0 也都能够配合 SSR 提供同构路由和客户端 state hydration.vue-hackernews-2.0 是 Vue 作者在GitHub上面放的 Vue 2.0 的一个示例项目,结合 Express.vue-router & vuex 来构建,是很好的学习案例. Features Server Side Rendering Vue + vue-router + vue