精华内容
下载资源
问答
  • Vue 2.0 开始支持服务端渲染的功能,所以本文章也是基于vue 2.0以上版本。网上对于服务端渲染的资料还是比较少,最经典的莫过于Vue作者尤雨溪大神的 vue-hacker-news。本人在公司做Vue项目的时候,一直苦于产品、...
  • 主要介绍了Nuxt之vue服务端渲染,NUXT集成了利用Vue开发服务端渲染的应用所需要的各种配置,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • Vue SSR 服务端渲染深度解析及实践

    千次阅读 2019-06-03 17:33:50
    一 . SSR 的优缺点 更利于SEO 不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本(Google除外,据说Googlebot可以运行javaScript)。使用了Vue或者其它MVVM...服务端渲染返回给客户端的是已经获取了异...

    一 . SSR 的优缺点

    • 更利于SEO

    不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本(Google除外,据说Googlebot可以运行javaScript)。使用了Vue或者其它MVVM框架之后,页面大多数DOM元素都是在客户端根据js动态生成,可供爬虫抓取分析的内容大大减少。另外,浏览器爬虫不会等待我们的数据完成之后再去抓取我们的页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行JavaScript脚本的最终HTML,网络爬中就可以抓取到完整页面的信息。

    • 更利于首屏渲染

    首屏的渲染是node发送过来的html字符串,并不依赖于js文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。

    二 . SSR的局限

    • 服务端压力较大

      本来是通过客户端完成渲染,现在统一到服务端node服务去做。尤其是高并发访问的情况,会大量占用服务端CPU资源;

    • 开发条件受限

      在服务端渲染中,created和beforeCreate之外的生命周期钩子不可用,因此项 目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制;

    • 学习成本相对较高

      除了对webpack、Vue要熟悉,还需要掌握node、Express相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。

    了解完服务端渲染的有缺点后下面开始详细分析一波

    三 . 解析构建流程
    来先看一眼官方构建图
    在这里插入图片描述

    • app.js / main.js入口文件

      app.js是我们的通用entry,它的作用就是构建一个Vue的实例以供服务端和客户端使用,注意一下,在纯客户端的程序中我们的app.js将会挂载实例到dom中,而在ssr中这一部分的功能放到了Client entry中去做了。

    • 两部分入口entry.js
      我们来看Client entry和Server entry,这两者分别是客户端的入口和服务端的入口。Client entry的功能很简单,就是挂载我们的Vue实例到指定的dom元素上;Server entry是一个使用export导出的函数。主要负责调用组件内定义的获取数据的方法,获取到SSR渲染所需数据,并存储到上下文环境中。这个函数会在每一次的渲染中重复的调用。

    • webpack打包构建
      我们的服务端代码和客户端代码通过webpack分别打包,生成Server Bundle和Client Bundle,前者会运行在服务器上通过node生成预渲染的HTML字符串,发送到我们的客户端以便完成初始化渲染;而客户端bundle就自由了,初始化渲染完全不依赖它了。客户端拿到服务端返回的HTML字符串后,会去“激活”这些静态HTML,是其变成由Vue动态管理的DOM,以便响应后续数据的变化。

    • 解析运行流程

      这里我们该谈谈ssr的程序是怎么跑起来的了。首先我们得去构建一个vue的实例,也就是我们前面构建流程中说到的app.js做的事情,但是这里不同于传统的客户端渲染的程序,我们需要用一个工厂函数去封装它,以便每一个用户的请求都能够返回一个新的实例,也就是官网说到的避免交叉污染了。

      然后我们可以暂时移步到服务端的entry中了,这里要做的就是拿到当前路由匹配的组件,调用组件里定义的一个方法(官网取名叫asyncData)拿到初始化渲染的数据,而这个方法要做的也很简单,就是去调用我们vuex store中的方法去异步获取数据。

      接下来node服务器如期启动了,跑的是我们刚写好的服务端entry里的函数。在这里还要做的就是将我们刚刚构建好的Vue实例渲染成HTML字符串,然后将拿到的数据混入我们的HTML字符串中,最后发送到我们客户端。

      打开浏览器的network,我们看到了初始化渲染的HTML,并且是我们想要初始化的结构,且完全不依赖于客户端的js文件了。再仔细研究研究,里面有初始化的dom结构,有css,还有一个script标签。script标签里把我们在服务端entry拿到的数据挂载了window上。原来只是一个纯静态的HTML页面啊,没有任何的交互逻辑,所以啊,现在知道为啥子需要服务端跑一个vue客户端再跑一个vue了,服务端的vue只是混入了个数据渲染了个静态页面,客户端的vue才是去实现交互的!

    四 . SSR服务端渲染注意点

    在SSR中,创建Vue实例、创建store和创建router都是套了一层工厂函数的,目的就是避免数据的交叉污染。

    • 注意点一

      服务端只能执行生命周期中的created和beforeCreate,原因是在服务端是无法操纵dom的,所以可想而知其他的周期也就是不能执行的了。

    • 注意点二
      服务端渲染和客户端渲染不同,需要创建两个entry分别跑在服务端和客户端,并且需要webpack对其分别打包;

    • 注意点三
      SSR服务端请求不带cookie,需要手动拿到浏览器的cookie传给服务端的请求。实现方式戳这里

    • 注意点四
      SSR要求dom结构规范,因为浏览器会自动给HTML添加一些结构比如tbody,但是客户端进行混淆服务端放回的HTML时,不会添加这些标签,导致混淆后的HTML和浏览器渲染的HTML不匹配。

    • 注意点五
      性能问题需要多加关注。
      vue.mixin、axios拦截请求使用不当,会内存泄漏。
      lru-cache向内存中缓存数据,需要合理缓存改动不频繁的资源。如何实现戳这里

    五 .完整项目模板

    利用 webpack 3 可以非常快速的搭建一个简单的 vue 开发环境,你也可以走捷径(git地址)
    项目目录如下所示

    ├─.babelrc // babel 配置文件
    ├─index.template.html // html 模板文件
    ├─server.js // 提供服务端渲染及 api 服务
    ├─src // 前端代码
    | ├─app.js // 主要用于创建 vue 实例
    | ├─App.vue // 根组件
    | ├─entry-client.js // 客户端渲染入口文件
    | ├─entry-server.js // 服务端渲染入口文件
    | ├─stores // vuex 相关
    | ├─routes // vue-router 相关
    | ├─components // 组件
    ├─dist // 代码编译目标路径
    ├─build // webpack 配置文件

    六 . 正式开始构建项目

    以下配置是根据在vue项目中改造完成

    项目构建完成后的基本架构

    在这里插入图片描述

    在这里插入图片描述

    打包运行在客户端文件的配置 , 在build中添加webpack.client.config.js文件

    const webpack = require('webpack')
    const merge = require('webpack-merge')
    const base = require('./webpack.base.conf.js')
    const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
    const SWPrecachePlugin = require('sw-precache-webpack-plugin')
    const uglify = require('uglifyjs-webpack-plugin');
    //压缩html配置
    const htmlPlugin = require('html-webpack-plugin')
    
    const config = merge(base, {
      entry: './src/entry-client.js',
      plugins: [
        new uglify(),//打包压缩
        new webpack.DefinePlugin({
          'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
          'process.env.VUE_ENV': '"client"'
        }),
        // split vendor js into its own file
        new webpack.optimize.CommonsChunkPlugin({
          name: 'vendor',
          minChunks: function (module) {
            // any required modules inside node_modules are extracted to vendor
            return (
              // it's inside node_modules
              /node_modules/.test(module.context) &&
              // and not a CSS file (due to extract-text-webpack-plugin limitation)
              !/\.css$/.test(module.request)
            )
          }
        }),
        new webpack.optimize.CommonsChunkPlugin({
          name: 'manifest'
        }),
        new VueSSRClientPlugin(),
        // 拷贝静态文件
        // new CopyWebpackPlugin([
        //     {
        //         from: path.resolve(__dirname, '../static'),
        //         to: conf.build.assetsSubDirectory,
        //         ignore: ['.*']
        //     }
        // ]),
        new htmlPlugin({
          /*压缩文件,removeAttributeQuotes指去掉属性的双引号,目前你随便不用也行*/
          minify: {
            removeAttributeQuotes: true
          },
          /*加入hash值,为了避免浏览器缓存js*/
          hash: true,
          /*要打包的html文件的路径及名称*/
          template: './index.template.html'
        }),
        new VueSSRClientPlugin()
      ]
    })
    if (process.env.NODE_ENV === 'production') {
      config.plugins.push(
        // auto generate service worker
        new SWPrecachePlugin({
          cacheId: 'vue-hn',
          filename: 'service-worker.js',
          minify: true,
          dontCacheBustUrlsMatching: /./,
          staticFileGlobsIgnorePatterns: [/\.map$/, /\.json$/],
          runtimeCaching: [
            {
              urlPattern: '/',
              handler: 'networkFirst'
            },
            {
              urlPattern: /\/(top|new|show|ask|jobs)/,
              handler: 'networkFirst'
            },
            {
              urlPattern: '/item/:id',
              handler: 'networkFirst'
            },
            {
              urlPattern: '/user/:id',
              handler: 'networkFirst'
            }
          ]
        })
      )
    }
    
    module.exports = config
    

    打包运行在服务端文件的配置 , 在build中添加webpack.server.config.js文件

    const webpack = require('webpack')
    const merge = require('webpack-merge')
    const nodeExternals = require('webpack-node-externals')
    const base = require('./webpack.base.conf.js')
    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
    const uglify = require('uglifyjs-webpack-plugin');
    
    module.exports = merge(base, {
      // 将 entry 指向应用程序的 server entry 文件
      entry: './src/entry-server.js',
    
      // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
      // 并且还会在编译 Vue 组件时,
      // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
      target: 'node',
    
      // 对 bundle renderer 提供 source map 支持
      devtool: 'source-map',
    
      // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
      output: {
        filename: 'server-bundle.js',
        libraryTarget: 'commonjs2'
      },
    
      // https://webpack.js.org/configuration/externals/#function
      // https://github.com/liady/webpack-node-externals
      // 外置化应用程序依赖模块。可以使服务器构建速度更快,
      // 并生成较小的 bundle 文件。
      externals: nodeExternals({
        // 不要外置化 webpack 需要处理的依赖模块。
        // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
        // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
        whitelist: /\.(scss|sass|css|less)$/
      }),
    
      // 这是将服务器的整个输出
      // 构建为单个 JSON 文件的插件。
      // 默认文件名为 `vue-ssr-server-bundle.json`
      plugins: [
        new uglify(),
        new webpack.DefinePlugin({
          'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
          'process.env.VUE_ENV': '"server"'
        }),
        new VueSSRServerPlugin()
      ]
    })
    

    修改公共配置文件webpack.base.conf.js

    var path = require('path');
    var utils = require('./utils');
    var config = require('../config');
    var ExtractTextPlugin = require('extract-text-webpack-plugin');
    const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
    const { VueLoaderPlugin } = require('vue-loader')
    const webpack = require('webpack')
    
    function resolve (dir) {
        return path.join(__dirname, '..', dir)
    }
    const isProd = process.env.NODE_ENV === 'production'
    module.exports = {
        devtool: isProd
            ? false
            : '#cheap-module-source-map',
        // 入口文件
        entry: {
            app: ["babel-polyfill", "./src/main.js"]
        },
        // 出口文件
        output: {
            path: config.build.assetsRoot,
            filename: '[name].js',
            publicPath: '/dist/',
        },
        resolve: {
            // 自动补全文件后缀
            extensions: ['.js', '.vue', '.json'],
            alias: {
                'vue$': 'vue/dist/vue.esm.js',
                '@': resolve('src'),
                'src': path.resolve(__dirname, '../src'),
                'assets': path.resolve(__dirname, '../src/assets'),
                'components': path.resolve(__dirname, '../src/components'),
                'views': path.resolve(__dirname, '../src/views'),
                'styles': path.resolve(__dirname, '../src/styles'),
                'api': path.resolve(__dirname, '../src/api'),
                'utils': path.resolve(__dirname, '../src/utils'),
                'store': path.resolve(__dirname, '../src/store'),
                'router': path.resolve(__dirname, '../src/router'),
                'mock': path.resolve(__dirname, '../src/mock'),
                'vendor': path.resolve(__dirname, '../src/vendor'),
                'static': path.resolve(__dirname, '../static'),
    
            }
        },
        module: {
            noParse: /es6-promise\.js$/, // avoid webpack shimming process
            rules: [
                {
                    test: /\.vue$/,
                    loader: 'vue-loader',
                    options: {
                        // // enable CSS extraction
                        // extractCSS: isProd,
                        compilerOptions: {
                            preserveWhitespace: false
                        }
                    }
                },
                {
                    test: /\.js$/,
                    loader: 'babel-loader?cacheDirectory',
                    include: [resolve('src'), resolve('test')]
                },
                // 解析图片配置
                {
                    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                    loader: 'url-loader',
                    query: {
                        limit: 10000, // 小于10000会转成base64格式
                        name: utils.assetsPath('img/[name].[hash:7].[ext]')
                    }
                },
                {
                    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                    loader: 'url-loader',
                    query: {
                        limit: 10000,
                        name: utils.assetsPath('fonts/[name].[ext]')
                    }
                },
                // 解析css文件
                {
                    test: /\.css$/,
                    // ��Ҫ��ʹ�� vue-style-loader ��� style-loader
                    use: isProd
                        ? ExtractTextPlugin.extract({
                            use: 'css-loader',
                            fallback: 'vue-style-loader'
                        })
                        : ['vue-style-loader', 'css-loader']
                },
                // 解析less文件
                {
                    test: /\.less$/,
                    use: [{
                        loader: "vue-style-loader"
                    }, {
                        loader: "css-loader"
                    }, {
                        loader: "less-loader"
                    }]
                }
            ],
            loaders: [{    // babel loader
                test: /\.js?$/,
                exclude: /node_modules/,
                loader: "babel-loader"
            }, {
                test: /\.(png|jpg|jpng)$/, // pack images
                loader: 'url-loader?limit=8192&name=resource/image/[name]-[hash:8].[ext]'
            },
            {
                test: /\.(woff|woff2|eot|ttf|svg)(\?[a-z0-9]+)?$/,
                loader: 'url-loader?limit=1000&name=resource/fonts/[name]-[hash:8].[ext]'
            },
            {
                test: /\.ejs$/,
                loader: 'ejs-loader',
            },
            {
                test: /\.(scss|sass|css|less)$/,  // pack sass and css files
                loader: ExtractTextPlugin.extract({ fallback: "style-loader", use: "css-loader!postcss-loader!sass-loader" })
            }
            ]
        },
        performance: {
            hints: false
        },
        plugins: isProd
            ? [
                new VueLoaderPlugin(),
                new webpack.optimize.UglifyJsPlugin({
                    compress: { warnings: false }
                }),
                new webpack.optimize.ModuleConcatenationPlugin(),
                new ExtractTextPlugin({ filename: 'common.[chunkhash].css' })
            ]
            : [
                new VueLoaderPlugin(),
                new FriendlyErrorsPlugin()
            ]
    }
    
    

    在build中添加setup-dev-server.js文件

    const fs = require('fs')
    const path = require('path')
    const MFS = require('memory-fs')
    const webpack = require('webpack')
    const chokidar = require('chokidar')
    const clientConfig = require('./webpack.client.config')
    const serverConfig = require('./webpack.server.config')
    
    const readFile = (fs, file) => {
      try {
        return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
      } catch (e) {}
    }
    
    module.exports = function setupDevServer (app, templatePath, cb) {
      let bundle
      let template
      let clientManifest
    
      let ready
      const readyPromise = new Promise(r => { ready = r })
      const update = () => {
        if (bundle && clientManifest) {
          ready()
          cb(bundle, {
            template,
            clientManifest
          })
        }
      }
    
      // read template from disk and watch
      template = fs.readFileSync(templatePath, 'utf-8')
      chokidar.watch(templatePath).on('change', () => {
        template = fs.readFileSync(templatePath, 'utf-8')
        console.log('index.html template updated.')
        update()
      })
    
      // modify client config to work with hot middleware
      clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
      clientConfig.output.filename = '[name].js'
      clientConfig.plugins.push(
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()
      )
    
      // dev middleware
      const clientCompiler = webpack(clientConfig)
      const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
        publicPath: clientConfig.output.publicPath,
        noInfo: true
      })
      app.use(devMiddleware)
      clientCompiler.plugin('done', stats => {
        stats = stats.toJson()
        stats.errors.forEach(err => console.error(err))
        stats.warnings.forEach(err => console.warn(err))
        if (stats.errors.length) return
        clientManifest = JSON.parse(readFile(
          devMiddleware.fileSystem,
          'vue-ssr-client-manifest.json'
        ))
        update()
      })
    
      // hot middleware
      app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))
    
      // watch and update server renderer
      const serverCompiler = webpack(serverConfig)
      const mfs = new MFS()
      serverCompiler.outputFileSystem = mfs
      serverCompiler.watch({}, (err, stats) => {
        if (err) throw err
        stats = stats.toJson()
        if (stats.errors.length) return
    
        // read bundle generated by vue-ssr-webpack-plugin
        bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
        update()
      })
    
      return readyPromise
    }
    
    

    在src文件下添加打包运行在客户端入口文件 src / entry-client.js . 里面可能涉及原项目部分内容请酌情删改

    import Vue from 'vue'
    import 'es6-promise/auto'
    import { createApp } from './main'
    import {getToken} from 'utils/util';
    import { Base64 } from 'js-base64';
    import NProgress from 'nprogress'; // Progress 进度条
    import 'nprogress/nprogress.css';// Progress 进度条 样式
    
    
    Vue.mixin({
      beforeRouteUpdate (to, from, next) {
        const { asyncData } = this.$options
        if (asyncData) {
          asyncData({
            store: this.$store,
            route: to
          }).then(next).catch(next)
        } else {
          next()
        }
      }
    })
    const { app, router, store } = createApp()
    
    // prime the store with server-initialized state.
    // the state is determined during SSR and inlined in the page markup.
    if (window.__INITIAL_STATE__) {
      store.replaceState(window.__INITIAL_STATE__)
    }
    if(store.getters.needLoginFlag){
      //递归获取菜单数据,并将其修改为符合路由规则的数据
      store.dispatch('GenerateRoutes').then((createdRouters) => {
      router.addRoutes(createdRouters) // 动态添加可访问路由表
      })
    }else{
      store.dispatch('GenerateRoutesNoLogin').then((createdRouters) => { // 生成可访问的路由表
      router.addRoutes(createdRouters) // 动态添加可访问路由表
      })
    }
    router.onReady(() => {
    // 添加路由钩子函数,用于处理 asyncData.
      // 在初始路由 resolve 后执行,
      // 以便我们不会二次预取(double-fetch)已有的数据。
      // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
      router.beforeResolve((to, from, next) => {
        const matched = router.getMatchedComponents(to)
        const prevMatched = router.getMatchedComponents(from)
        // 我们只关心非预渲染的组件
        // 所以我们对比它们,找出两个匹配列表的差异组件
        let diffed = false
        const activated = matched.filter((c, i) => {
          return diffed || (diffed = (prevMatched[i] !== c))
        })
        const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)
        if (!asyncDataHooks.length) {
          return next()
        }
        NProgress.start() // 开启Progress
        // 这里如果有加载指示器 (loading indicator),就触发
        Promise.all(asyncDataHooks.map(hook => hook({ store, route: to })))
          .then(() => {
            NProgress.done() // 结束Progress
            next()
          })
          .catch(next)
      })
    router.beforeEach((to, from, next) => { 
      //如果已经有token了,就直接登陆系统
      if(localStorage.getItem("TOKEN") || !store.getters.needLoginFlag){
          next();
        }else{
          //如果没有token,则获取token值
            const tgc = getToken('TGC');
            const u = getToken('U');
            const tokenNums = getToken('_TOKENUUMS');
            /**
              * 判断是否已登录
              * 未登录跳转到无访问权限页
              * 已登录获取工号,存到 localStorage 中
              */
            if (!tgc || !u || !tokenNums) {
              //TODO dispatch(routerRedux.push('/noAccess'));
            //FIXME 这个需要跳转到dpboot中的action中,
              store.dispatch('loginToServer');
    
            } else {
              const token = Base64.decode(tokenNums);
              localStorage.setItem("TOKEN",tokenNums);
              // store.dispatch('setUserToken',tokenNums);
              const tokenArr = token.split(',');
              //1,设置用户信息
              store.dispatch('setUserInfo',{userName:tokenArr[1],deptCode: tokenArr[3],deptName: tokenArr[4]});
              //2,产生动态路由,生成菜单树
              next();
    
              //FIXME 拿到 token后,可以访问
            }
        }
    })
      // actually mount to DOM
      app.$mount('#app')
    })
    
    // service worker
    if ('https:' === location.protocol && navigator.serviceWorker) {
      navigator.serviceWorker.register('/service-worker.js')
    }
    
    

    在src文件下添加打包运行在服务端入口文件 src / entry-server.js

    // entry-server.js
    import { createApp } from './main'
    
    const isDev = process.env.NODE_ENV !== 'production'
    
    export default context => {
      return new Promise((resolve, reject) => {
        const { app, router, store } = createApp()
        if (store.getters.needLoginFlag) {
          //递归获取菜单数据,并将其修改为符合路由规则的数据
          store.dispatch('GenerateRoutes').then((createdRouters) => {
            router.addRoutes(createdRouters) // 动态添加可访问路由表
            const s = isDev && Date.now()
            const { url } = context
            const { fullPath } = router.resolve(url).route
            if (fullPath !== url) {
              return reject({ url: fullPath })
            }
            // set router's location
            router.push(url)
            router.onReady(() => {
              const matchedComponents = router.getMatchedComponents()
              if (!matchedComponents.length) {// no matched routes
                return reject({ code: 404 })
              }
    
              // 对所有匹配的路由组件调用 `asyncData()`
              Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
                store,
                route: router.currentRoute
              }))).then(() => {
                // 在所有预取钩子(preFetch hook) resolve 后,
                // 我们的 store 现在已经填充入渲染应用程序所需的状态。
                // 当我们将状态附加到上下文,
                // 并且 `template` 选项用于 renderer 时,
                // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
                context.state = store.state
                resolve(app)
              }).catch(reject)
            }, reject)
          })
        } else {
          store.dispatch('GenerateRoutesNoLogin').then((createdRouters) => { // 生成可访问的路由表
            router.addRoutes(createdRouters) // 动态添加可访问路由表
            const s = isDev && Date.now()
            const { url } = context
            const { fullPath } = router.resolve(url).route
            if (fullPath !== url) {
              return reject({ url: fullPath })
            }
            // set router's location
            router.push(url)
            router.onReady(() => {
              const matchedComponents = router.getMatchedComponents()
              if (!matchedComponents.length) {// no matched routes
                return reject({ code: 404 })
              }
    
              // 对所有匹配的路由组件调用 `asyncData()`
              Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
                store,
                route: router.currentRoute
              }))).then(() => {
                // 在所有预取钩子(preFetch hook) resolve 后,
                // 我们的 store 现在已经填充入渲染应用程序所需的状态。
                // 当我们将状态附加到上下文,
                // 并且 `template` 选项用于 renderer 时,
                // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
                context.state = store.state
                resolve(app)
              }).catch(reject)
            }, reject)
          })
        }
      })
    }
    

    在根目录下创建server.js文件添加可运行的node代码

    const fs = require('fs')
    const path = require('path')
    const LRU = require('lru-cache')
    const resolve = file => path.resolve(__dirname, file)
    const express = require('express')
    const compression = require('compression')
    const { createBundleRenderer } = require('vue-server-renderer')
    const microcache = require('route-cache')
    const isProd = process.env.NODE_ENV === 'production'
    const useMicroCache = process.env.MICRO_CACHE !== 'false'
    const serverInfo =
      `express/${require('express/package.json').version} ` +
      `vue-server-renderer/${require('vue-server-renderer/package.json').version}`
    
    
    const app = express()
    
    function createRenderer (bundle, options) {
      // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer
      return createBundleRenderer(bundle, Object.assign(options, {
        // this is only needed when vue-server-renderer is npm-linked
        cache: LRU({
          max: 1000,
          maxAge: 1000 * 60 * 5
        }),
        basedir: resolve('./dist'),
        // recommended for performance
        runInNewContext: false
      }))
    }
    
    let renderer
    let readyPromise
    
    const templatePath = resolve('./dist/index.html')
    if (isProd) {
      // In production: create server renderer using template and built server bundle.
      // The server bundle is generated by vue-ssr-webpack-plugin.
      const template = fs.readFileSync(templatePath, 'utf-8')
      const bundle = require('./dist/vue-ssr-server-bundle.json')
      // The client manifests are optional, but it allows the renderer
      // to automatically infer preload/prefetch links and directly add <script>
      // tags for any async chunks used during render, avoiding waterfall requests.
      const clientManifest = require('./dist/vue-ssr-client-manifest.json')
      renderer = createRenderer(bundle, {
        template,
        clientManifest
      })
    } else {
      // 开发环境使用webpack热更新服务
      // In development: setup the dev server with watch and hot-reload,
      // and create a new renderer on bundle / index template update.
      readyPromise = require('./build/setup-dev-server')(
        app,
        templatePath,
        (bundle, options) => {
          renderer = createRenderer(bundle, options)
        }
      )
    }
    const serve = (path, cache) => express.static(resolve(path), {
      maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
    })
    
    app.use(compression({ threshold: 0 }))
    app.use('/', serve('./dist', true))
    app.use('/static', serve('./static', true))
    app.use('/favicon.ico', serve('./favicon.ico', true))
    app.use('/dist', serve('./dist', true))
    app.use('/manifest.json', serve('./manifest.json', true))
    app.use('/service-worker.js', serve('./dist/service-worker.js'))
    
    
    app.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl))
    
    function render (req, res) {
      const s = Date.now()
    
      res.setHeader("Content-Type", "text/html")
      res.setHeader("Server", serverInfo)
    
      const handleError = err => {
        if (err.url) {
          res.redirect(err.url)
        } else if (err.code === 404) {
          res.status(404).send('404 | Page Not Found')
        } else {
          // Render Error Page or Redirect
          res.status(500).send('500 | Internal Server Error')
          console.error(`error during render : ${req.url}`)
          console.error(err.stack)
        }
      }
    
      const context = {
        title: 'dpboot前端框架', // default title
        url: req.url
      }
      renderer.renderToString(context, (err, html) => {
        if (err) {
          return handleError(err)
        }
        res.send(html)
        if (!isProd) {
          console.log(`whole request: ${Date.now() - s}ms`)
        }
      })
    }
    
    
    // // 组件缓存
    // export default {
    //   name: 'Home',
    //   title () {
    //     return {
    //       title: 'vue-ssr',
    //       keywords: 'vue-ssr服务端脚手架, home',
    //       description: 'vue-ssr-template, vue-server-renderer, home'
    //     }
    //   },
    //   asyncData ({ store }) { },
    //   serverCacheKey: props => props.id
    // }
    
    app.get('*', isProd ? render : (req, res) => {
      readyPromise.then(() => render(req, res))
    })
    
    
    
    
    const port = process.env.PORT || 8080
    app.listen(port, () => {
      console.log(`server started at localhost:${port}`)
    })
    
    

    修改package.json文件执行打包命令

     "scripts": {
        "build": "rimraf dist && npm run build:client && npm run build:server",
        "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules",
        "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules"
      },
    

    配置好以上文件可尝试运行npm run build 命令, 若打包成功即可运行 node server.js 启动项目
    以上配置皆由本人实践成功后所得经验 , 对于不同项目可能有不同差别 , 请灵活配置 .

    展开全文
  • Vue服务端渲染和客户端渲染

    千次阅读 2019-08-09 18:23:36
    一、前言 主要涉及: 1、什么是服务端渲染 2、什么是客户端渲染 3、两者的区别,以及什么场合使用 二、主要内...

    一、前言                                                                                        

     主要涉及:

                     1、什么是服务端渲染

                     2、什么是客户端渲染

                     3、两者的区别,以及什么场合使用

    二、主要内容                                                                                 

     1、客户端请求

           (1)用户在浏览器输入请求的地址例如:172.0.0.1:8080 到服务器

                    服务器接受到客户端的请求拿到一个没有被数据渲染的空页面

                    

     

            (2)客户端拿到服务端的空字符串页面,然后从上往下开始执行里面的代码,当执行到script中有请求或者渲染等代码时,就会对服务器再次发出请求

              

     

           (3)服务端接收到客户端的第二次请求,就把响应的数据发送给客户端,然后客户端再进行渲染

     

     

     

     

     

           在客户端渲染中, 客户端至少要对服务端发送两次请求

     

     

    2、服务端渲染

     

        (1)客户端只发送一次请求,服务端直接返回给客户端一个被渲染好的页面,

     

     

    3、如何辨别是客户端渲染还是服务端渲染

          比如,京东网站:

        看看选中的内容是客户端渲染还是服务端渲染

     

      右击查看网页源代码,ctrl+f查找:发现可以找到,说明为服务端渲染

         

       

       一般的用户评论为客户端渲染。

     

     

     

    三、总结                                                                                        

    1、客户端渲染需要对服务端进行两次请求,响应的开销较大,而服务端渲染只需要客户端对服务端进行一次请求

    2、如何查看一个网页是客户端渲染还是服务端渲染:可以通过右键查看源代码的形式

         客户端渲染: 右击查看源代码找不到内容

        服务段渲染:是可以在源代码中找到内容的

    3、网站一般都是用客户端渲染和服务端渲染结合的形式

    4、正真的网站既不是纯异步,也不是纯服务端渲染,而是两者结合,

     

    5、商品的商品列表采用的是服务端渲染,目的是为了SEO搜索引擎优化,而他的商品评论为了用户体验,用户体验更好

    6、服务端渲染可以被爬虫抓取到,客户端渲染爬虫抓取不到

    展开全文
  • vue-ssr 结合Express使用Vue2服务端渲染
  • vue-cli3.0搭建服务端渲染SSR

    千次阅读 2020-07-31 21:58:04
    文章目录关于SSR什么是SSR为什么要用SSRSSR原理通用代码约束:构建SSR应用程序创建vue项目修改router/index.js修改store/index....简单点说:就是将页面在服务端渲染完成后在客户端直接显示。无需等待所有的 JavaScript

    关于SSR

    什么是SSR

    • 可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序
    • 简单点说:就是将页面在服务端渲染完成后在客户端直接显示。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。

    为什么要用SSR

    • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
    • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。

    SSR原理

    在这里插入图片描述

    • 1)所有的文件都有一个公共的入口文件app.js
    • 2)进入ServerEntry(服务端入口)与clientEntry(客户端入口)
    • 3)经过webpack打包生成ServerBundle(供服务端SSR使用,一个json文件)与ClientBundle(给浏览器用,和纯Vue前端项目Bundle类似)
    • 4)当请求页面的时候,nodeServerBundle会生成html界面,通过ClientBundle混合到html页面中

    通用代码约束:

    • 实际的渲染过程需要确定性,所以我们也将在服务器上“预取”数据 ("pre-fetching" data) - 这意味着在我们开始渲染时,我们的应用程序就已经解析完成其状态。
    • 避免在 beforeCreatecreated 生命周期时产生全局副作用的代码,例如在其中使用 setInterval 设置 timer
    • 通用代码不可接受特定平台的 API,因此如果你的代码中,直接使用了像 windowdocument,这种仅浏览器可用的全局变量,则会在 Node.js 中执行时抛出错误,反之也是如此

    构建SSR应用程序

    创建vue项目

    vue create vue-ssr-demo
    
    • 根据提示启动项目
      在这里插入图片描述

    修改router/index.js

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)
    
    export function createRouter(){
      return new VueRouter({
        mode: 'history',
        routes: [
          {
            path: '/',
            name: 'Home',
            component: () => import(/* webpackChunkName: "about" */ '../views/Home.vue')
          },
          {
            path: '/about',
            name: 'About',
            component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
          }
        ]
      })
    }
    

    修改store/index.js

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export function createStore(){
      return new Vuex.Store({
        state: {
        },
        mutations: {
        },
        actions: {
        },
        modules: {
        }
      })
    }
    

    修改main.js

    import Vue from 'vue'
    import App from './App.vue'
    import { createRouter } from './router'
    import { createStore } from './store'
    
    Vue.config.productionTip = false
    
    export function createApp(){
      const store = createStore();
      const router = createRouter();
      
      const app = new Vue({
        store, 
        router,
        render: h => h(App)
      })
      
      return {app, store, router}
    }
    

    服务端数据预取

    • src目录下创建entry-server.js
    // entry-server.js
    import { createApp } from './main'
    
    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) {
            return reject({ code: 404 })
          }
    
          // 对所有匹配的路由组件调用 `asyncData()`
          Promise.all(matchedComponents.map(Component => {
            if (Component.asyncData) {
              return Component.asyncData({
                store,
                route: router.currentRoute
              })
            }
          })).then(() => {
            // 在所有预取钩子(preFetch hook) resolve 后,
            // 我们的 store 现在已经填充入渲染应用程序所需的状态。
            // 当我们将状态附加到上下文,
            // 并且 `template` 选项用于 renderer 时,
            // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
            context.state = store.state
    
            resolve(app)
          }).catch(reject)
        }, reject)
      })
    }
    

    客户端数据预取

    • src创建entry-client.js
    import { createApp } from './main';
    
    const {app, store, router} = createApp();
    
    router.onReady(() => {
      // 添加路由钩子函数,用于处理 asyncData.
      // 在初始路由 resolve 后执行,
      // 以便我们不会二次预取(double-fetch)已有的数据。
      // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
      router.beforeResolve((to, from, next) => {
        const matched = router.getMatchedComponents(to)
        const prevMatched = router.getMatchedComponents(from)
    
        // 我们只关心非预渲染的组件
        // 所以我们对比它们,找出两个匹配列表的差异组件
        let diffed = false
        const activated = matched.filter((c, i) => {
          return diffed || (diffed = (prevMatched[i] !== c))
        })
    
        if (!activated.length) {
          return next()
        }
    
        // 这里如果有加载指示器 (loading indicator),就触发
    
        Promise.all(activated.map(c => {
          if (c.asyncData) {
            return c.asyncData({ store, route: to })
          }
        })).then(() => {
    
          // 停止加载指示器(loading indicator)
    
          next()
        }).catch(next)
      })
    
      app.$mount('#app')
    })
    

    构建配置

    • 在根目录下创建vue.config.js
    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
    const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
    const nodeExternals = require('webpack-node-externals')
    const merge = require('lodash.merge')
    const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
    const target = TARGET_NODE ? 'server' : 'client'
    module.exports = {
      css: {
        extract: false
      },
      configureWebpack: () => ({
        // 将 entry 指向应用程序的 server / client 文件
        entry: `./src/entry-${target}.js`,
        // 对 bundle renderer 提供 source map 支持
        devtool: 'source-map',
        target: TARGET_NODE ? 'node' : 'web',
        node: TARGET_NODE ? undefined : false,
        output: {
          libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
        },
        // https://webpack.js.org/configuration/externals/#function
        // https://github.com/liady/webpack-node-externals
        // 外置化应用程序依赖模块。可以使服务器构建速度更快,
        // 并生成较小的 bundle 文件。
        externals: TARGET_NODE
          ? nodeExternals({
            // 不要外置化 webpack 需要处理的依赖模块。
            // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
            // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
            allowlist: [/\.css$/]
          })
          : undefined,
        optimization: {
          splitChunks: TARGET_NODE ? false : undefined
        },
        plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
      }),
      chainWebpack: config => {
        config.module
          .rule('vue')
          .use('vue-loader')
          .tap(options => {
            return merge(options, {
              optimizeSSR: false
            })
          })
      }
    }
    

    webpack进行打包操作

    • 修改package.json
    "build:client": "vue-cli-service build",
    "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build",
    "build:win": "npm run build:server && move dist\\vue-ssr-server-bundle.json bundle && npm run build:client && move bundle dist\\vue-ssr-server-bundle.json"
    
    • 执行build:win, 生成的dist目录如下:
      ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200731171212338.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI2NDQzNTM1,size_16,color_FFFFFF,t_70

    创建服务

    • 在根目录下创建server.js
    const express = require('express');
    const fs = require('fs');
    const path = require('path');
    const { createBundleRenderer } = require('vue-server-renderer');
    
    const app = express();
    
    const serverBundle = require('./dist/vue-ssr-server-bundle.json');
    const clientManifest = require('./dist/vue-ssr-client-manifest.json');
    const template = fs.readFileSync(path.resolve('./src/index.template.html'), 'utf-8');
    
    const render = createBundleRenderer(serverBundle, {
      runInNewContext: false, // 推荐
      template, // (可选)页面模板
      clientManifest
    });
    
    app.use(express.static('./dist',{index:false}))
    
    app.get('*', (req, res) => {
      const context = { url: req.url }
      // 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。
      // 现在我们的服务器与应用程序已经解耦!
      render.renderToString(context, (err, html) => {
        console.log(html)
        // 处理异常……
        res.end(html)
      })
    })
    
    const port = 3003;
    app.listen(port, function() {
     console.log(`server started at localhost:${port}`);
    });
    
    • index.template.html
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <title></title>
      </head>
      <body>
        <!--vue-ssr-outlet-->
      </body>
    </html>
    
    • 运行node server.js
      在这里插入图片描述
    展开全文
  • vue的官方提供了服务端渲染的方法,但没有做成框架发布出来,而是让开发者自己按照里面的步骤一步一步去搭建,然而里面的步骤十分多,要看懂例子也十分困难。 这样大家就遇到了一个问题,读ssr原理本身不熟的同学们...

    vue的官方提供了服务端渲染的方法,但没有做成框架发布出来,而是让开发者自己按照里面的步骤一步一步去搭建,然而里面的步骤十分多,要看懂例子也十分困难。

    这样大家就遇到了一个问题,读ssr原理本身不熟的同学们,搭建起来十分吃力,大部分人多中途放弃了。

    在这里我分享一个成熟的搭建好的ssr框架,minissr。他是基于vue官方的技术,搭建出来的。使用方法十分方便,拿到它的源码后,一个命令就可以部署测试了:

    npm install
    npm run ssr

    框架的地址如下:

    https://www.wechatmini.com/vue/minissrdetail

     

    通过观察生成的源代码,你会发现,当我们请求地址的时候,内容已经添加到div里面去了,不再依赖于js来修改div的内容。

     

    我身边大部分使用vue开发官网的同学们,都使用这个框架来做seo优化了,因为用他开发跟做普通vue的开发差别很小,只是启动的命令不同。

    假如大家不想使用php这些老技术做网站开发,却又想做seo优化,想对搜索引擎友好,那么minissr就是你的最佳选择了。

    展开全文
  • Vue.js 是目前最火热的前端框架之一,而 Nuxt.js 是针对 Vue.js 推出的服务端渲染框架,通过高度定制化的配置以及简洁的 API,开发者可以快速进行服务端渲染项目的开发,本文将对 Nuxt.js 框架做一个简要介绍。...
  • vue如何实现服务端渲染

    千次阅读 2020-05-01 10:13:49
    一、服务端渲染 服务器端渲染:后端先调用数据库,获得数据以后,将数据和页面元素进行拼装,组合成完整的 html页面,再直接返回给浏览器,以用户浏览,也就是说明数据和页面是由服务器所去完成,返回浏览器展示。 ...
  • 如何使用vite,做vue3.0的服务端渲染(ssr)

    千次阅读 热门讨论 2021-02-24 16:28:41
    于是趁着现在各种完全体的方案和框架还没出来之前,那我们把vue3.0的服务端搭一搭吧,自己写写还是很有意思的。 好了,废话就先到此,开局调研了基于webpack和vue-cli去搭,途中碰到了一些问题就放弃了,由于尤大...
  • 本篇文章主要介绍了如何使用Vue2做服务端渲染 ,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • Vue服务端渲染,数据接口

    千次阅读 2020-01-17 18:48:53
    项目中常会包含多个页面及组件,需要用到 vue-router 来进行路由控制,服务端渲染组件时,都需要从数据库中获取真实数据。 一下强调几个重难点 入口文件分离 服 务端不同于客户端,需要整个应用的 Vue 实例,但可以...
  • 主要介绍了Egg Vue SSR 服务端渲染数据请求与asyncData,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
  • 本篇文章主要介绍了详解vue服务端渲染(SSR)初探,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 初学vue服务端渲染疑惑非常多,我们大部分前端都是半路出家,上手都是前后端分离,对服务端并不了解,不说java、php语言了,连node服务都还没搞明白,理解服务端渲染还是有些困难的; 网上有非常多的vue服务渲染的...
  • 本篇文章主要介绍了Vue 2.0 服务端渲染入门,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • Vue服务端渲染

    千次阅读 2019-08-09 18:26:20
    一、前言 1、服务端渲染图解 2、简介服务端渲染 ...
  • vue ssr 服务端渲染的理解

    千次阅读 2018-09-05 22:20:16
    为每个请求创建一个新的根 Vue 实例 router 服务器端数据预取 混合 修改title 提取css //安装 npm install vue-server-renderer --save // 第 1 步:创建一个 Vue 实例 const Vue = require('vue') const...
  • Vue SSR服务端渲染 vue预渲染

    千次阅读 2020-05-17 18:04:22
    了解SSR之前,需要知道最原始的服务器渲染、前后端一起开发。使用用一个服务器。原始的服务器渲染是整个web项目放入后端,提供路由访问。好处坏处也很多,举例:MVC模式。JavaWeb 原始服务渲染 优点: 安全性:...
  • 当咱们在作vue的服务器端渲染时,可能会碰到各类各样的坑,内存泄露就是其中的一种。固然,致使内存泄露的缘由有不少,不合理使用Axios也是其中一种,那下面我给你们介绍一下如何有效的避免请求中的内存泄露。vue1. ...
  • 完整的 vue- vue-class-componen typescriptt 的结合,支持降级能力,他的代码风格像极了 React ,这可能就是爱情。
  • 服务端渲染简介服务端渲染不是一个新的技术;在 Web 最初的时候,页面就是通过服务端渲染来返回的,用 PHP 来说,通常是使用 Smarty 等模板写模板文件,然后 PHP 服务端框架将数据和模板渲染为页面返回,这样的...
  • 本篇文章主要介绍了vue服务端渲染的实例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 手把手教你搭建 Vue 服务端渲染项目

    千次阅读 2020-11-02 17:00:20
    建议先阅读官方指南——Vue.js 服务器端渲染指南,再回到本文开始阅读。 本文将分成以下两部分: 简述 Vue SSR 过程 从零开始搭建 SSR 项目 好了,下面开始正文。 简述 Vue SSR 过程 ...服务端渲染过程
  • vue-ssr-demo 文档 使用 克隆项目 git clone https://github.com/woai3c/vue-ssr-demo.git 下载依赖 npm i 启动开发服务器 npm run dev 打包项目 npm run build 开启服务器 npm run server
  • npm i vue-server-renderer -S 调用后整体不会有javaScript输出,输出的是一个json npm i koa -S node框架KOA npm i koa-router -S KOA框架的路由工具 npm i axios -S 在node端发生请求 npm i ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 19,225
精华内容 7,690
关键字:

vue3服务端渲染

vue 订阅