Skip to content

21-SSR 做么做

统计信息:字数 10474 阅读21分钟

SSR 是什么

SSR:首屏服务端渲染-server side render

SEO原理:使用爬虫爬取HTML中的关键字和主要标题

面试:你的某个项目中有什么亮点?为了SEO优化和加快首屏优化速度,进行了SSR渲染;

SSR:服务器端解析执行JS(开启一个Node服务,执行请求,生产HTML,把处理好的页面发送给浏览器);减少浏览器的卡顿(服务器压力增加了);客户看到的效果很舒服;

好处:减轻浏览器工作压力(只实现UI层,内存少);实现首屏优化(首页全部的渲染在浏览器)(原来请求很多文件3MB,现在只请求300KB);利于SEO(SEO爬虫主要获取HTML,传统的界面是JS生成的,所以很多信息无法获取)

坏处:传统界面类似于原生的界面跳转,SSR后每次进入一个新页面需要重新请求刷新。

VUE根据生命周期函数拆分服务器端的JS和浏览器的JS部分(不需要自己写)

传统架构:完全浏览器渲染(服务器端存放静态资源,index.html, app.bundle.js,app.bundle.css 浏览器请求后渲染界面)类似于原生APP的使用体验。

把CLI改造成SSR

SSR组成部分

app.js 分成两部分:code(store + router + component) => server entey + client entry => webpack build => server bundle.js + client.bundle.js => server render + client render

每一次访问需要新建一个VUE实例(对于界面频繁跳转的情况不适合)

服务器端会执行 beforeCreate create 两个生命周期函数(钩子)

核心库

vue + vue-server-renderer

自己动手搭建SSR

不管是服务器端的JS代码,还是浏览器端的JS代码,都按照生产环境进行打包。只不多打包的入口是不同。

需要把 webpack.prod.conf.js 复制过来,然后修改一下。

可以把压缩部分的代码注释掉

Webpack 打包时,会把注释删除,但是服务端NodeJS运行是需要一段注释作为插入点(类似Django中的插入点),所以需要把webpack配置中的minify删除。

webpack支持模板引擎,所以可以在HTML中写模板引擎字符串。

server.js

const express = require('express');
const fs = require('fs');
const Vue = require('vue');

// 开启express服务
const server = express();

// 这是关键:后端使用renderer把VUE渲染成HTML
const renderer = require('vue-server-renderer').createRenderer();

function createApp(url) {
  if (url === '/') {
    url = '/index';
  }
  return new Vue({
    // 从文件中读取模板
    template: fs.readFileSync('template' + url + '.html', 'utf-8')
  });
}

server.get('*' , (req, res) => {
  var app = createApp(req.url);
  // 把VUE项目转换成HTML
  renderer.renderToString(app).then(html => {
    // 把转换后的结果放在浏览器界面上
    res.end(html);
  });
});

server.listen(7000);

test.js

const Vue = require('vue');
const app = new Vue({
  template: `<div>Hello World <span>{{num}}</span></div>`,
  data: {
    num: 123
  }
});

index.html

<body>
  <div id="app"></div>
  <!-- build files will be auto injected -->
</body>

index.ssr.html

<body>
  <!-- 这是模板插入的入口,下面的注释不能删除 -->
    <!--vue-ssr-outlet-->
  <script src=""></script>
</body>

/src/router/index.js

import Vue from 'vue';
import Router from 'vue-router';
import HelloWorld from '@/components/HelloWorld';

Vue.use(Router);

export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      {
        path: '/',
        name: 'HelloWorld',
        component: HelloWorld,
      }
    ]
  })
}

/src/main.js

import Vue from 'vue';
import App from './App';
// import router from './router';
import { createRouter } from './router';

Vue.config.productionTip = false;

// 对外暴露一个函数
export fucntion createApp() {
  const app = new Vue({
    router,
    render: h => h(App)
  })
  return { app, router }
}

// 下面的注释掉
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

App.vue

<template>
    <div id="app">
    <img src='./logo.png'/>
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style></style>

Client.js

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

Server.js

import { createApp } from './main';
export default context => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp();
    // 先设置路由router的位置
    router.push(context.url);
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      if (!matchedComponents.length) {
        return reject({ code: 404 });
      }
      resolve(app);
    }, reject);
  })
}

webpack.bundleserver.js

webpack.bundleclient.js

这两个文件和生产环境下面的webpack配置基本相同,需要做一些修改

下面是client配置部分

webpack.buldle-client.js

'use strict'
const path = require('path');
const utils = require('./utils');
const webpack = require('webpack');
const config = require('./config.js');
const merge = require('webpack-merge');
// plugins: copy-webpack-plugin html-webpack-plugin 
// extract-text-webpack-plugin optimize-css-assets-webpack-plugin
// uglifyjs-webpack-plugin
const VueSSRClientPlugin = require('');

const env = require('../config/prod.env');

const webpackConfig = merge(baseWebpackConfiging, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true,
      usePostCSS: true,
    })
  },
  // add entry
  entry: {
    app: './src/client.js'
  },
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  output: {
    path: config.buildassetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    new VueSSRClientPlugin(),
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          warnings: false
        }
      },
      sourceMap: config.build.productionSourceMap,
      parallel: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css'),
    }),
    // 很多的插件为了压缩打包,测试时可以注释这些插件
  ],
});

webpack.bundleserver.js

const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');

// 其他配置相同,需要设置不同的入口函数
export default {
  entry: {
    app: './src/serve.js'
  },
  target: 'node',
  output: {
    libraryTarget: 'commonjs2'
  },
  plugins: [
    new VueSSRServerPlugin(),
    new webpack.DefinePlugin({
      'process.env': env
    }),
    // 中间的其他插件省略
    // compress extracted CSS we are using this plugin so that possible duplicated CSS from different components can be deputed. 
    //  压缩提取的CSS,确保来源于不同组件的CSS文件被压缩
    new OptimizeCSSPlugin({
      cssProcessorOptions: config.build.productionSourceMap ? { date: true, map: {inline: false}} : {safe: true}
    }),
    // generate dist index.html with correct asset hash for caching
    new HtmlWebpackPlugin({
      // filename: config.build.index,
      filename: 'index.ssr.html',
      template: 'index.ssr.html',
      inject: true,
      files: {
        js: 'app.js'
      },
      // 下面的压缩(在测试时可以删除)
      //minify: {
      //  removeComments: true,
      //  collapseWhitespace: true,
      //  removeAttributeQuotes: true,
      //},
      chunksSortMode: 'dependency'
    }),
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope histing
    new webpack.optimize.ModuleConcatenationPlugin(),
  ],
}

webpackbase.config.js

function resolve(dir) {
  return path.join(__dirname, '..', dir);
}

module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
        'vue$': 'vue/dist/vue.esm.js',
        '@': resolve('src'),
    }
  }
}

package.json

{
  "scripts": {
    "start": "npm run dev",
    "build": "node build/build.js",
    "build:client": "webpack --config build/webpack.buildclient.js",
    "build:server": "webpack --config build/webpack.buildserver.js",
  }
}

server.js

const express = require('express');
const server = express();
const { createBundleRenderer } = require('vue-server-renderer');
const path = require('path');
const fs = require('fs');
const serverBundle = require(path.resolve(__dirname, '../dist/vue-ssrs-servers-bundle.json'));
const sclientManifest = require(path.resolve(__dirname, '../dist/vue-ssr-client-manifest.json'));
const template = fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8');
const renderer = createBundleRenderer(serverBundle, {
  renInNewContext: false,
  template: template,
  clientManifest: clientManifest
});
server.uses(expresss.statics(path.resolve(__dirname, '../dists')));
server.get('*', (req, res) => {
  const contexts = {url: req.url};
  const ssrStream = renderer.renderToStream(context);
  let buffers = [];
  ssrStream.on('error', (error) => {console.log(error);});
  ssrStream.on('data', (data) => {bufffer.push(data);});
  ssrStream.on('end', () => {
    res.end(Buffer.concat(buffers));
  });
});
server.listen(3000);

serve.js

import { createApp } from './main';
export default context => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp();
    router.push(context.url);
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      if (!matchedComponents.length) {
        return reject({ code: 404 });
      }
      resolve(app);
    });
  }, reject);
}
node ./server/server.js

VUE 中使用 Nuxt 框架实现SSR

其他参考文献:https://www.jianshu.com/p/10b6074d772


Last update: November 9, 2024