关于VueSSR的一些理解和详细配置

目录
  • 概念
    • 流程图
    • 编译图解
  • 相关配置
    • 目录结构
    • index.html 的改动
    • 需要安装的依赖
    • 启动命令
    • 异步处理例子
  • 结尾

如果是静态页面,例如官网的SSR处理可直接使用prerender-spa-plugin插件实现预渲染,参考我之前的博客:vue单页面通过prerender-spa-plugin插件进行SEO优化

以下是基于vue-cli@2.x生成的工程相关的结构改造。文章最后有异步请求的例子可供参考。

概念

流程图

这是具体的流程图,如何实现,后续配置会详解

编译图解

结合上面的流程图来理解编译的过程图,因为服务端渲染只是一个可以等待异步数据的预渲染,最终用户交互还是需要Client entry生成的js来控制,这就是为什么需要两个entry文件,但是拥有同样的入口(app.js)的原因。

串联server和client的枢纽就是store,server将预渲染页面的sotre数据放入window全局中,client再进行数据同步,完成异步数据预渲染

相关配置

知道大致步骤和流程,再去理解VueSSR的配置就不会那么突兀了。

注意:路由如果用懒加载会出现vue模板里面的样式没办法抽离到css中,而是用js渲染,浏览器会出现一个没有样式到最终页面的空白期。由于SSR渲染做了没有SPA首屏渲染问题,所以不用懒加载也没事。

目录结构

index.html 的改动

需要加入<!–vue-ssr-outlet–>占位符

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="renderer" content="webkit">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <link rel="icon" href="" type="image/x-icon" />
    <title>{{title}}</title>
</head>
<body>
    <div id="app">
        <!--vue-ssr-outlet-->
    </div>
</body>
</html>

需要安装的依赖

多了一些相关的依赖需要添加一下,这里做一个汇总:

npm install memory-fs chokidar vue-server-renderer@2.5.17 lru-cache serve-favicon compression route-cache vuex-router-sync --save

注意: vue-server-renderer要和工程的vue版本一致。

server.js

开发模式会调用setup-dev-server中的热更新插件,实时编译

const fs = require('fs'); //读取文件
const path = require('path');
const express = require('express');
const app= express();
const LRU = require('lru-cache');  //封装缓存的get set方法
/*
* 处理favicon.ico文件:作用:
* 1. 去除这些多余无用的日志
* 2. 将icon缓存在内存中,防止从因盘中重复读取
* 3. 提供了基于icon 的 ETag 属性,而不是通过文件信息来更新缓存
* 4. 使用最兼容的Content-Type处理请求
 */
const favicon = require('serve-favicon');
const compression = require('compression');  //压缩
const microcache = require('route-cache');  //请求缓存
const resolve = (file) => path.resolve(__dirname, file);  //返回绝对路径
const {createBundleRenderer} = require('vue-server-renderer');
const isProd = process.env.NODE_ENV === 'production';
const useMicroCache = process.env.MICRO_CACHE !== 'false';
const serverInfo =
    `express/${require('express/package').version}` +
    `vue-server-renderer/${require('vue-server-renderer/package').version}`;
let renderer;
let readyPromise;
//生成renderer函数
function createRenderer(bundle, options) {
    return createBundleRenderer(bundle, Object.assign(options, {
        cache: new LRU({
            max: 1000,
            maxAge: 1000 * 60 * 15
        }),
        basedir: resolve('./dist'),
        runInNewContext: false
    }));
}
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: 'ssr标题',
        url: req.url
    };
    renderer.renderToString(context, (err, html) => {
        console.log(err);
        if (err) {
            return handleError(err);
        }
        res.send(html);
        if (!isProd) {
            console.log(`whole request: ${Date.now() - s}ms`);
        }
    })
}
const templatePath = resolve('./index.html');
if (isProd) {
     const template = fs.readFileSync(templatePath, 'utf-8');
     const bundle = require('./dist/vue-ssr-server-bundle.json');
     const clientManifest = require('./dist/vue-ssr-client-manifest.json');
     renderer = createRenderer(bundle, {
         template,
         clientManifest
     })
 } else {
     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
 });
 //静态文件压缩,支持gzip和deflate方式(原来这步是nginx做的),threshold: 0, 0kb以上的都压缩,即所有文件都压缩,
 //可通过filter过滤
 //TODO
 app.use(compression({threshold: 0}));
 app.use(favicon('./favicon.ico'));
 app.use('/dist', serve('./dist', true));
 app.use('/static', serve('./static', true));
 app.use('/service-worker.js', serve('./dist/service-worker.js', true));
 //TODO
 app.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl));
 app.get('*', isProd ? render : (req, res) => {
     readyPromise.then(() => render(req, res));
 });
 // 监听
app.listen(8082, function () {
  console.log('success listen...8082');
});

entry-server.js

在路由resolve之前,做数据预渲染

import { createApp } from './app'
const isDev = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'sit';
// This exported function will be called by `bundleRenderer`.
// This is where we perform data-prefetching to determine the
// state of our application before actually rendering it.
// Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance.
export default context => {
    return new Promise((resolve, reject) => {
        const s = isDev && Date.now();
        const { app, router, store } = createApp();
        const { url } = context;
        const { fullPath } = router.resolve(url).route;
        if (fullPath !== url) {
            return reject({ url: fullPath })
        }
        // set router's location
        router.push(url);
        // wait until router has resolved possible async hooks
        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents()
            // no matched routes
            if (!matchedComponents.length) {
                return reject({ code: 404 })
            }
            // Call fetchData hooks on components matched by the route.
            // A preFetch hook dispatches a store action and returns a Promise,
            // which is resolved when the action is complete and store state has been
            // updated.
            Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
                store,
                route: router.currentRoute
            }))).then(() => {
                isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`)
                // After all preFetch hooks are resolved, our store is now
                // filled with the state needed to render the app.
                // Expose the state on the render context, and let the request handler
                // inline the state in the HTML response. This allows the client-side
                // store to pick-up the server-side state without having to duplicate
                // the initial data fetching on the client.
                context.state = store.state;
                resolve(app)
            }).catch(reject)
        }, reject)
    })
}

entery-client.js

window.INITIAL_STATE 就是服务端存在html中的store数据,客户端做一次同步,router.onReady在第一次不会触发,只有router接管页面之后才会触发,在beforeResolved中手动进行数据请求(否则asyncData中的请求不会触发)

import {createApp} from './app';
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__)
}
router.onReady(() => {
    // Add router hook for handling asyncData.
    // Doing it after initial route is resolved so that we don't double-fetch
    // the data that we already have. Using router.beforeResolve() so that all
    // async components are resolved.
    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) {
            console.log('there no client async');
            return next()
        }
        // bar.start()
        console.log('client async begin');
        Promise.all(asyncDataHooks.map(hook => hook({ store, route: to }))).then(() => {
                // bar.finish()
            console.log('client async finish');
                next()
            }).catch(next)
    });
    //以激活模式挂载,不会改变浏览器已经渲染的内容
    app.$mount('#app');
});
// service worker
if ('https:' === location.protocol && navigator.serviceWorker) {
    navigator.serviceWorker.register('/service-worker.js')
}

app.js

import Vue from 'vue';
import App from './App.vue';
import {createStore} from './store';
import {createRouter} from './router';
import {sync} from 'vuex-router-sync';
export function createApp() {
    const store = createStore();
    const router = createRouter();
    // sync the router with the vuex store.
    // this registers `store.state.route`
    sync(store, router);
    // create the app instance.
    // here we inject the router, store and ssr context to all child components,
    // making them available everywhere as `this.$router` and `this.$store`.
    const app = new Vue({
        router,
        store,
        render: h => h(App)
    });
    // expose the app, the router and the store.
    // note we are not mounting the app here, since bootstrapping will be
    // different depending on whether we are in a browser or on the server.
    return {app, router, store}
}

router.js 和 store.js

防止数据污染,每次都要创造新的实例

注意:路由如果用懒加载会出现vue模板里面的样式没办法抽离到css中,而是用js渲染,浏览器会出现一个没有样式到最终页面的空白期。由于SSR渲染做了没有SPA首屏渲染问题,所以不用懒加载也没事。

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore () {
    return new Vuex.Store({
      state: {
        token,
      },
      mutations: {},
      actions: {}
    })
}
//router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Table from '@/views/Table';
import page1 from '@/views/page1';
export const constantRouterMap = [
  { path: '/', component: Table, hidden: true },
  { path: '/page1', component: page1, hidden: true }
];
export function createRouter() {
    return new Router({
        mode: 'history',
        fallback: false, // 设置浏览器不支持history.pushState时,不回退
        linkActiveClass: 'open active',
        scrollBehavior: () => ({ y: 0 }),
        routes: constantRouterMap
    })
}

setup-dev-server.js

const fs = require('fs');
const path = require('path');
//文件处理工具
const MFS = require('memory-fs');
const webpack = require('webpack');
//对fs.watch的包装,优化fs,watch原来的功能
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);
    //1. 生成新的renderer函数; 2. renderer.renderToString();
    const update = () => {
        if (bundle && clientManifest) {
            //执行server.js中的render函数,但是是异步的
            ready();
            cb(bundle, {
                template,
                clientManifest
            })
        }
    };
    template = fs.readFileSync(templatePath, 'utf-8');
    //模板改了之后刷新 TODO
    chokidar.watch(templatePath).on('change', () => {
        template = fs.readFileSync(templatePath, 'utf-8');
        console.log('index.html template updated');
        update();
    });
    clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app];
    clientConfig.plugins.push(
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()
    );
    const clientComplier = webpack(clientConfig);
    const devMiddleware = require('webpack-dev-middleware')(clientComplier, {
        publicPath: clientConfig.output.publicPath,
        noInfo: true
    });
    app.use(devMiddleware);
    clientComplier.plugin('done', stats => {
        stats = stats.toJson();
        stats.errors.forEach(err => console.log(err));
        stats.warnings.forEach(err => console.log(err));
        if (stats.errors.length) return;
        clientManifest = JSON.parse(readFile(
            devMiddleware.fileSystem,
            'vue-ssr-client-manifest.json'
        ));
        update();
    });
    app.use(require('webpack-hot-middleware')(clientComplier, {heartbeat: 5000}));
    const serverCompiler = webpack(serverConfig);
    const mfs = new MFS();
    serverCompiler.outputFileSystem = mfs;
    //监听server文件修改 TODO
    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;
};

webpack.base.config.js

vue-loader.conf就是vue脚手架自动生成的文件,就不再贴出了

const path = require('path');
var utils = require('./utils');
var config = require('../config');
var vueLoaderConfig = require('./vue-loader.conf');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin');
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin')
function resolve(dir) {
    return path.join(__dirname, '..', dir)
}
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
    devtool: isProd
        ? false
        : '#cheap-module-source-map',
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/dist/',
        filename: isProd ? utils.assetsPath('js/[name].[chunkhash].js') : utils.assetsPath('[name].js'),
        chunkFilename: isProd ? utils.assetsPath('js/[id].[chunkhash].js') : utils.assetsPath('[id].js')
    },
    resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
            '@': resolve('client'),
            'src': path.resolve(__dirname, '../client'),
            'assets': path.resolve(__dirname, '../client/assets'),
            'components': path.resolve(__dirname, '../client/components'),
            'views': path.resolve(__dirname, '../client/views'),
            'api': path.resolve(__dirname, '../client/api'),
            'utils': path.resolve(__dirname, '../client/utils'),
            'router': path.resolve(__dirname, '../client/router'),
            'vendor': path.resolve(__dirname, '../client/vendor'),
            'static': path.resolve(__dirname, '../static'),
        }
    },
    externals: {
        jquery: 'jQuery'
    },
    module: {
        rules: [
            // {
            //     test: /\.(js|vue)$/,
            //     loader: 'eslint-loader',
            //     enforce: "pre",
            //     include: [resolve('src'), resolve('test')],
            //     options: {
            //         formatter: require('eslint-friendly-formatter')
            //     }
            // },
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: vueLoaderConfig
            },
            {
                test: /\.js$/,
                loader: 'babel-loader?cacheDirectory',
                include: [resolve('client'), resolve('test')]
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                loader: 'url-loader',
                query: {
                    limit: 10000,
                    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]')
                }
            },
            ...utils.styleLoaders({sourceMap: config.dev.cssSourceMap})
        ]
    },
    plugins: isProd
        ? [
            // new webpack.optimize.ModuleConcatenationPlugin(),
            new ExtractTextPlugin({
                filename: 'common.[chunkhash].css'
            }),
            // Compress extracted CSS. We are using this plugin so that possible
            // duplicated CSS from different components can be deduped.
            new OptimizeCSSPlugin(),
            // copy custom static assets
            new CopyWebpackPlugin([
                {
                    from: path.resolve(__dirname, '../static'),
                    to: config.build.assetsSubDirectory,
                    ignore: ['.*']
                }
            ]),
        ]
        : [
            new FriendlyErrorsPlugin(),
            // copy custom static assets
            new CopyWebpackPlugin([
                {
                    from: path.resolve(__dirname, '../static'),
                    to: config.build.assetsSubDirectory,
                    ignore: ['.*']
                }
            ]),
        ]
};

webpack.server.config.js

const webpack = require('webpack');
const merge = require('webpack-merge');
const config = require('../config');
const baseConfig = require('./webpack.base.config');
// const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
let env, NODE_ENV = process.env.NODE_ENV;
if (NODE_ENV === 'development') {
    env = config.dev.env;
} else if (NODE_ENV === 'production') {
    env = config.build.prodEnv;
} else {
    env = config.build.sitEnv;
}
module.exports = merge(baseConfig, {
    target: 'node',
    devtool: '#source-map',
    entry: './client/entry-server.js',
    output: {
        filename: 'server-bundle.js',
        libraryTarget: 'commonjs2'
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env': env,
            'process.env.VUE_ENV': '"server"',
        }),
        new VueSSRServerPlugin()
    ]
});

webpack.client.config.js

const webpack = require('webpack');
const merge = require('webpack-merge');
const config = require('../config');
const utils = require('./utils');
const base = require('./webpack.base.config');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
// TODO
// const SWPrecachePlugin = require('sw-precache-webpack-plugin');
let env, NODE_ENV = process.env.NODE_ENV;
if (NODE_ENV === 'development') {
    env = config.dev.env;
} else if (NODE_ENV === 'production') {
    env = config.build.prodEnv;
} else {
    env = config.build.sitEnv;
}
module.exports = merge(base, {
    entry: {
        app: './client/entry-client.js'
    },
    plugins:
        NODE_ENV !== 'development' ? [
            new webpack.DefinePlugin({
                'process.env': env,
                'process.env.VUE_ENV': '"client"'
            }),
            new webpack.optimize.UglifyJsPlugin({
                compress: {warnings: false}
            }),
            // extract vendor chunks for better caching
            new webpack.optimize.CommonsChunkPlugin({
                name: 'vendor',
                minChunks: function (module) {
                    // a module is extracted into the vendor chunk if...
                    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)
                    )
                }
            }),
            // extract webpack runtime & manifest to avoid vendor chunk hash changing
            // on every build.
            new webpack.optimize.CommonsChunkPlugin({
                name: 'manifest'
            }),
            new VueSSRClientPlugin(),
        ] : [
            new webpack.DefinePlugin({
                'process.env': env,
                'process.env.VUE_ENV': '"client"'
            }),
            new VueSSRClientPlugin(),
        ],
});

启动命令

  • 开发模式:npm run dev
  • 生产模式:npm run build & npm run start
	"scripts": {
	    "dev": "cross-env NODE_ENV=development supervisor server/app.js",
	    "start": "cross-env NODE_ENV=production node server/app.js",
	    "build": "npm run build:client && npm run build:server",
	    "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
	    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js"
	  },

异步处理例子

store.js

//store.js
import {TableRequest} from '@/api/Table';
export default {
  state: {
    userName: ''
  },
  mutations: {
    GETUSERNAME(state, data) {
      state.userName = data;
    }
  },
  actions: {
    GetUserName({commit}) {
      return TableRequest().then(res => {
        commit('GETUSERNAME', res.data.content);
      })
    }
  }
}
// api.js  请求可以用node做一个
export function GetUserName (user, password) {
  const data = {
    user,
    password
  }
  return fetch({
    url: '/apis/getUserName',
    data
  })
}

vue页面

<template>
    <div>
        {{$store.state.userName}}
    </div>
</template>
<script>
    export default {
        name: 'APP',
        asyncData({store, route}) {
            return  store.dispatch('fetchItem', route.params.id);
        },
    }
</script>

server.js添加请求处理

const fs = require('fs'); //读取文件
const path = require('path');
const express = require('express');
const app= express();
const LRU = require('lru-cache');  //封装缓存的get set方法
/*
* 处理favicon.ico文件:作用:
* 1. 去除这些多余无用的日志
* 2. 将icon缓存在内存中,防止从因盘中重复读取
* 3. 提供了基于icon 的 ETag 属性,而不是通过文件信息来更新缓存
* 4. 使用最兼容的Content-Type处理请求
 */
const favicon = require('serve-favicon');
const compression = require('compression');  //压缩
const microcache = require('route-cache');  //请求缓存
const resolve = (file) => path.resolve(__dirname, file);  //返回绝对路径
const {createBundleRenderer} = require('vue-server-renderer');
const isProd = process.env.NODE_ENV === 'production';
const useMicroCache = process.env.MICRO_CACHE !== 'false';
const serverInfo =
    `express/${require('express/package').version}` +
    `vue-server-renderer/${require('vue-server-renderer/package').version}`;
let renderer;
let readyPromise;
//生成renderer函数
function createRenderer(bundle, options) {
    return createBundleRenderer(bundle, Object.assign(options, {
        cache: new LRU({
            max: 1000,
            maxAge: 1000 * 60 * 15
        }),
        basedir: resolve('./dist'),
        runInNewContext: false
    }));
}
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: 'ssr标题',
        url: req.url
    };
    renderer.renderToString(context, (err, html) => {
        console.log(err);
        if (err) {
            return handleError(err);
        }
        res.send(html);
        if (!isProd) {
            console.log(`whole request: ${Date.now() - s}ms`);
        }
    })
}
const templatePath = resolve('./index.html');
if (isProd) {
     const template = fs.readFileSync(templatePath, 'utf-8');
     const bundle = require('./dist/vue-ssr-server-bundle.json');
     const clientManifest = require('./dist/vue-ssr-client-manifest.json');
     renderer = createRenderer(bundle, {
         template,
         clientManifest
     })
 } else {
     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.get('/getName', function (req, res, next) {
    res.json({
        code: 200,
        content: '我是userName',
        msg: '请求成功'
    })
});
 /**
 * 结束
 * 测试数据请求的配置,可删
 **/
 //静态文件压缩,支持gzip和deflate方式(原来这步是nginx做的),threshold: 0, 0kb以上的都压缩,即所有文件都压缩,
 //可通过filter过滤
 //TODO
 app.use(compression({threshold: 0}));
 app.use(favicon('./favicon.ico'));
 app.use('/dist', serve('./dist', true));
 app.use('/static', serve('./static', true));
 app.use('/service-worker.js', serve('./dist/service-worker.js', true));
 //TODO
 app.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl));
 app.get('*', isProd ? render : (req, res) => {
     readyPromise.then(() => render(req, res));
 });
 // 监听
app.listen(8082, function () {
  console.log('success listen...8082');
});

结尾

以上就是结合vue-cli改造的一个ssr框架,流程和原理理解了之后可自行改造相关配置。希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Vue SSR 即时编译技术的实现

    当我们在服务端渲染 Vue 应用时,无论服务器执行多少次渲染,大部分 VNode 渲染出的字符串是不变的,它们有一些来自于模板的静态 html,另一些则来自模板动态渲染的节点(虽然在客户端动态节点有可能会变化,但是在服务端它们是不变的).将这两种类型的节点提取出来,仅在服务端渲染真正动态的节点(serverPrefetch 预取数据相关联的节点),可以显著的提升服务端的渲染性能. 提取模板中静态的 html 只需在编译期对模板结构做解析,而判断动态节点在服务端渲染阶段是否为静态,需在运行时对 V

  • vue的ssr服务端渲染示例详解

    为什么使用服务器端渲染 (SSR) 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面. 请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引.在这里,同步是关键.如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容.也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题. 更快的内容到达时间 (ti

  • 如何构建 vue-ssr 项目的方法步骤

    如何通过 web 服务器去渲染一个 vue 实例 构建一个极简的服务端渲染需要什么 web 服务器 vue-server-renderer vue const Vue = require('vue') const Koa = require('koa') const app = new Koa() const Router = require('koa-router') const router = new Router() const renderer = require('vue-serve

  • vue中vue-router的使用说明(包括在ssr中的使用)

    目录 安装vue-router 创建配置文件 路由映射规则配置 路由设置内容 入口文件配置 app.vue配置 router中使用props 其他配置属性 导航守卫 vue笔记之vue-router的使用(包括ssr中的使用) 安装vue-router 命令行执行: npm i vue-router -S 创建配置文件 在项目src文件夹下创建config文件夹存放路由配置 在config文件夹下新建router.js和routes.js router.js: 存放路由设置 routes.js:

  • vuecli项目构建SSR服务端渲染的实现

    服务端渲染(SSR) 将一个 Vue 组件在服务端渲染成 HTML 字符串并发送到浏览器,最后将这些静态标记"激活"为可交互应用程序的过程就叫服务端渲染(SSR) 服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行 为什么使用 服务端渲染(SSR) 更好的 SEO:传统的 spa 页面数据都是异步加载,搜索引擎爬虫无法抓取,服务端渲染(SSR)使搜索引擎爬虫抓取工具可以直接查

  • Vue使用预渲染代替SSR的方法

    页面渲染方式 前段时间了解到页面有几种渲染方式:SPA SSR,以前写的一个网站但是用的渲染方式是 SPA,导致搜索引擎爬虫抓不到任何信息,对 SEO 优化不很好,本想改成 SSR,但是改动配置很多,弄来弄去怕影响开发,今天在 Vue 官网看到预渲染,今天试了下,感觉是一个折中的方法,而且配置改动不大,大家可以试试 什么是服务器端渲染 (SSR)? Vue.js 是构建客户端应用程序的框架.默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM.然而,也可以将同一个组件渲

  • 关于VueSSR的一些理解和详细配置

    目录 概念 流程图 编译图解 相关配置 目录结构 index.html 的改动 需要安装的依赖 启动命令 异步处理例子 结尾 如果是静态页面,例如官网的SSR处理可直接使用prerender-spa-plugin插件实现预渲染,参考我之前的博客:vue单页面通过prerender-spa-plugin插件进行SEO优化 以下是基于vue-cli@2.x生成的工程相关的结构改造.文章最后有异步请求的例子可供参考. 概念 流程图 这是具体的流程图,如何实现,后续配置会详解 编译图解 结合上面的流程图

  • struts2开发流程及详细配置

    一:Struts开发步骤: 1. web项目,引入struts - jar包 2. web.xml中,引入struts的核心功能 配置过滤器 3. 开发action 4. 配置action src/struts.xml 二:详细配置    1.引入8个jar文件 commons-fileupload-1.2.2.jar   [文件上传相关包] commons-io-2.0.1.jar struts2-core-2.3.4.1.jar           [struts2核心功能包] xwork-

  • asp中的ckEditor的详细配置小结

    ckeditor的详细配置: 在网上找了好久终于找到了!O(∩_∩)O哈哈~ 一.使用方法: 1.在页面<head>中引入ckeditor核心文件ckeditor.js 复制代码 代码如下: <script type="text/javascript" src="ckeditor/ckeditor.js"></script> 2.在使用编辑器的地方插入HTML控件<textarea> 复制代码 代码如下: <te

  • SQL Server 远程连接服务器详细配置(sp_addlinkedserver)

    远程链接服务器详细配置 --建立连接服务器 EXEC sp_addlinkedserver '远程服务器IP','SQL Server' --标注存储 EXEC sp_addlinkedserver @server = 'server', --链接服务器的本地名称.也允许使用实例名称,例如MYSERVER\SQL1 @srvproduct = 'product_name' --OLE DB数据源的产品名.对于SQL Server实例来说,product_name是'SQL Server' , @

  • 详解Linux下的sudo及其配置文件/etc/sudoers的详细配置

    详解Linux下的sudo及其配置文件/etc/sudoers的详细配置 1.sudo介绍 sudo是linux下常用的允许普通用户使用超级用户权限的工具,允许系统管理员让普通用户执行一些或者全部的root命令,如halt,reboot,su等等.这样不仅减少了root用户的登陆 和管理时间,同样也提高了安全性.Sudo不是对shell的一个代替,它是面向每个命令的. 它的特性主要有这样几点: § sudo能够限制用户只在某台主机上运行某些命令. § sudo提供了丰富的日志,详细地记录了每个用

  • Cisco 3550速率限制的详细配置过程

    一.网络说明 PC1接在Cisco3550 F0/1上,速率为1M: PC1接在Cisco3550 F0/2上,速率为2M: Cisco3550的G0/1为出口. 二.详细配置过程 注:每个接口每个方向只支持一个策略:一个策略可以用于多个接口.因此所有PC的下载速率的限制都应该定义在同一个策略(在本例子当中 为policy-map user-down),而PC不同速率的区分是在Class-map分别定义. 1.在交换机上启动QOS Switch(config)#mls qos //在交换机上启动

  • 使用注解开发SpringMVC详细配置教程

    1.使用注解开发SpringMVC 1.新建一个普通的maven项目,添加web支持 2.在pom.xml中导入相关依赖 SpringMVC相关 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.8.RELEASE</version> </dependency&

  • 2020年IntelliJ IDEA最新最详细配置图文教程详解

    推荐阅读: IntelliJ IDEA 2020最新激活码(亲测有效,可激活至 2089 年) 最新idea2020注册码永久激活(激活到2100年) IDEA使用设置 继续idea最新安装的步骤后,对IDEA工作开发进行配置,让开发的时候更加便利顺手. 点击欢迎页右下角"Configure",选择"Settings",进入全局设置界面. 注意:IDEA有全局配置和项目配置两种设置,在欢迎页进行的Settings是对全局配置进行设置.而在项目中setting有可能为

  • angular8.5集成TinyMce5的使用和详细配置(推荐)

    编写人:mkl 日期:2020.11.16 本篇主要讲解的是TinyMce的配置,原理不做讲解,请自行查阅文档TinyM TinyMCE是什么? TinyMCE是一款易用.且功能强大的所见即所得的富文本编辑器.同类程序有:UEditor.Kindeditor.Simditor.CKEditor.wangEditor.Suneditor.froala等等. TinyMCE的优势: 开源可商用,基于LGPL2.1 插件丰富,自带插件基本涵盖日常所需功能(示例看下面的Demo-2) 接口丰富,可扩展性

  • 深入理解Vue-cli4路由配置

    目录 前言-vue路由 一.最基本路由配置 1.配置router/index.js 2.配置App.vue 二.路由懒加载技术 三.路由嵌套 四.动态路由 1.动态路由配置 2.动态路由传参 总结 前言-vue路由 Vue-router是Vue官方的路由插件,与Vue.js深度集成. 在使用了vue-router的单页面应用中,url的改变会引起组件的切换,从而达到页面切换的效果,所以如何让URL按照我们的意愿去改变和URL改变后页面去向何处是配置vue-router的两大问题. [SPA网页]

随机推荐