Vue-服务端渲染SSR

服务端渲染(都在node中):

  SSR(Server-Side-Rendering)是用户第一次请求、刷新页面时,又服务端相应HTML字符串,可以省去浏览器端首次渲染的工作,加快首屏显示速度。

  特点:1、更好的SEO优化;2、更快的内容到达时间。

 

 CSR:

 

 

 项目目录:

 

 

 app.js:

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
export function createApp () {
  const router = createRouter()
  const store = createStore()

  sync(store, router)
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })

  return { app, router, store }
}

entry-client.js:

import { createApp } from './app'

const { app, router } = createApp()

router.onReady(() => {
  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')
})

entry-server.js:

import { createApp } from './app'

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp()

    // 设置服务器端 router 的位置
    router.push(context.url)

    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      // 匹配不到的路由,执行 reject 函数,并返回 404
      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)
  })
}

webpack.base.config.js:

 

const path = require('path')

const {
  VueLoaderPlugin
} = require('vue-loader')

const resolve = (dir) => path.join(path.resolve(__dirname, '../'), dir)

const FriendlyErrorsPluginWebpackPlugin = require('friendly-errors-webpack-plugin')

const { CleanWebpackPlugin } = require('clean-webpack-plugin')

const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  mode: isProd ? 'production' : 'development',
  output: {
    path: resolve('dist'),
    publicPath: '/dist/',
    filename: '[name].[chunkhash].js'
  },
  resolve: {
    alias: {
      'public': resolve('public')
    }
  },
  module: {
    noParse: /es6-promise\.js$/, // avoid webpack shimming process
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          compilerOptions: {
            preserveWhitespace: false
          }
        }
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: '[name].[ext]?[hash]'
        }
      },
      {
        test: /\.s(a|c)ss?$/,
        use: ['vue-style-loader', 'css-loader', 'sass-loader']
      },
    ]
  },
  performance: {
    hints: false
  },
  plugins: [
    new VueLoaderPlugin(),
    new FriendlyErrorsPluginWebpackPlugin(),
    new CleanWebpackPlugin()
  ]
}

webpack.client.config.js:

const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

module.exports = merge.merge(baseConfig, {
  entry: {
    app: './src/entry-client.js'
  },
  optimization: {
    // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
    // 以便可以在之后正确注入异步 chunk。
    // 这也为你的 应用程序/vendor 代码提供了更好的缓存。
    splitChunks: {
      name: "manifest",
      minChunks: Infinity
    }
  },
  plugins: [
    // new webpack.optimize.CommonsChunkPlugin({
    //   name: "manifest",
    //   minChunks: Infinity
    // }),
    // 此插件在输出目录中
    // 生成 `vue-ssr-client-manifest.json`。
    new VueSSRClientPlugin()
  ]
})

 

webpack.server.config.js:

const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge.merge(baseConfig, {
  // 将 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: {
    libraryTarget: 'commonjs2'
  },

  // https://webpack.js.org/configuration/externals/#function
  // https://github.com/liady/webpack-node-externals
  // 外置化应用程序依赖模块。可以使服务器构建速度更快,
  // 并生成较小的 bundle 文件。
  externals: nodeExternals({
    // 不要外置化 webpack 需要处理的依赖模块。
    // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单  whitelist
    allowlist: /\.css$/
  }),

  // 这是将服务器的整个输出
  // 构建为单个 JSON 文件的插件。
  // 默认文件名为 `vue-ssr-server-bundle.json`
  plugins: [
    new VueSSRServerPlugin()
  ]
})

server.js:

const Vue = require('vue')
const server = require('express')()
const { createBundleRenderer } = require('vue-server-renderer')

const path = require('path')
const fs = require('fs')
const resolve = file => path.resolve(__dirname, file)

const isProd = process.env.NODE_ENV === 'production'

let renderer, readyPromise



const createRenderer = (bundle, options) => {
  return createBundleRenderer(bundle, Object.assign(options, {
    basedir: resolve('./dist'),
    runInNewContext: false,
  }))
}
const templatePath = resolve('./src/index.template.html')
if (isProd) {
  const serverBundle = require('./dist/vue-ssr-server-bundle.json')
  const clientManifest = require('./dist/vue-srr-cclient-manifest.json')



  const template = fs.readFileSync(templatePath, 'utf-8')


  renderer = createRenderer(serverBundle, {
    template, // (可选)页面模板
    clientManifest // (可选)客户端构建 manifest
  })
} else {
  //开发模式
  //1、 server bundle
  //2、 client manifest
  //3、 等待编译完成 调用 createBundleRenderer 
  // renderToString
  // setupServer   readyPromise
  readyPromise = require('./config/setup-dev-server')(server, templatePath, (bundle, options) => {
    render = createRenderer(bundle, options)
  })
}


const render = (req, res) => {
  const context = {
    title: 'vue ssr',
    metas: `
        <meta charest="UTF-8">
        <meta name="keyword" content="vue,ssr">
        <meta name="description" content="vue srr demo">
    `,
    url: req.url
  }
  // 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。
  // 现在我们的服务器与应用程序已经解耦!
  renderer.renderToString(context, (err, html) => {
    // 处理异常……
    if (err) {
      if (err.code === 404) {
        res.status(404).end('Page not found')
      } else {
        res.status(500).end('Internal Server Error')
      }
    } else {
      res.end(html)
    }
  })
}


// 在服务器处理函数中……
server.get('*', isProd ? (req, res) => render(req, res) : (req, res) => {
  readyPromise().then(() => render(req, res))
})

server.listen(8080)

setup-dev-server.js:

const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webapck = require('webpack')
const clientConfig = require('./webpack.client.config')
const serverConfig = require('./webpack.server.config')
const middleware = require('webpack-dev-middleware')
const HMR = require('webpack-hot-middleware')
const webpack = require('webpack')
const MFS = require('memory-fs')
const readFile = (fs, file) => {
  try {
    return fs.readFileSync(path.join(clientConfig.output.publicPath, file), 'utf-8')
  } catch (error) {

  }
}
const setupServer = (app, templatePath, cb) => {
  let bundle, clientManifest, template, ready
  const redayPromise = new Promise(r => ready = r)

  template = fs.readFileSync(templatePath, 'utf-8')
  const update = () => {
    if (bundle && clientManifest) {
      //server  渲染
      //执行 createRenderer  RednerToString
      ready()
      cb(bundle, {
        template,
        clientManifest
      })
    }
  }

  //webpack  entry-server bundle
  const mfs = new MFS();
  const serverCompiler = webpack(serverConfig);

  serverCompiler.outputFileSystem = mfs;
  serverCompiler.watch({}, (err, stats) => {
    if (err) throw err
    // 之后读取输出:
    stats = stats.toJson()
    stats.errors.forEach(err => console.error(err))
    stats.warnings.forEach(err => console.warn(err))
    if (stats.errors.length) return
    bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
    update()
  });
  //webpack entry-client clientManifest
  //hot-middleware 
  clientConfig.plugins.push(
    new webpack.HotModuleReplacementPlugin()
  )
  clientConfig.entry.app = ['webpack-hot-niddleware.client', clientConfig.entry.app]

  const clientCompiler = webapck(clientConfig)

  const devMiddleware = middleware(clientCompiler, {
    noInfo: true, publicPath: clientConfig.output.publicPath
  })
  app.use(devMiddleware)

  app.use(HMR(clientCompiler))

  clientCompiler.hooks.done.tap('clientsBuild', states => {
    states = states.toJson()
    if (states.errors.length !== 0) {
      return
    }
    clientManifest = JSON.parse(readFile(
      devMiddleware.fileSystem,
      'vue-ssr-client-manifest.json'
    ))
  })
  //fs  tempaltePath  template
  chokidar.watch(templatePath).on('change', () => {
    template = fs.readFileSync(templatePath, 'utf-8')
    console.log('template is update')
    update()
  })


  return redayPromise
}

module.exports = setupServer

index.template.html:

<html>

<head>
  <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
  <title>{{ title }}</title>

  <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
  {{{ meta }}}
</head>

<body>
  <!--vue-ssr-outlet-->
</body>

</html>

执行命令:package.json中配置

"scripts": {
    "dev": "nodemon server.js",
    "build": "npm run build:client && npm run build:server",
    "build:client": "webpack --config config/webpack.client.config.js",
    "build:server": "webpack --config config/webpack.server.config.js"
  }

至此Vue服务端渲染项目搭建完毕!

posted @ 2020-10-25 21:58  广广-t  阅读(119)  评论(0)    收藏  举报