网易云- 基于vue做ssr

262 阅读2分钟

什么是ssr

传统vue项目
	流程: 输入网址 -> 请求后端服务器 -> 返回打包资源到浏览器(js,html模板)
    		-> 浏览器渲染(执行js) -> 发送请求 -> 返回数据
    缺点: 页面由js生成,不利于seo; 一次性获取所有js,不利于首屏渲染
    
ssr项目: 输入网址 -> node服务解析路由 -> node服务渲染对应页面,
		生成对应页面html字符串 -> 返回页面字符串给浏览器 -> 浏览器渲染

搭建ssr+vue项目

  • vue-server-renderer: 将vue实例转化为字符串
  • 步骤
    • 根据路由解析获取模板文件
    • 读取对应页面的template和数据生成一个vue实例
    • 把vue实例编译为字符串,返回给浏览器
  const app = new Vue({
      template: `<div></div>`,
      data: {}
  })
  const rederer = require('vue-server-renderer').createRenderer()
  renderer.renderToString(app).then(html => {
      console.log(html)
  })
  • 结合vue实现ssr

    • 每次访问必须创建一个vue实例

    • 只会触发组件的beforeCreate和created钩子,所以需要客户端js

    • js拆分为服务端js,客户端js,服务端负责渲染页面和beforeCreate和created, 客户端js负责后续操作

    • 需要通过webpack生成客户端和服务端js,然后在node服务器中,根据路由解析不同页面,获取对应页面client/server打包结果以及对应的template组合生成vue实例返回给浏览器

  • index.ssr.html

// <div id="app"></div> 删除该标签
<!-- vue-ssr-outlet --> // 该注释必须保持
<script type="text/javascript" src="<%=htmlWebpackPlugin.options.files.js=%"></script> // ejs
  • router/index.js
  export default function createRouter() {
      return new Router({
          mode: 'history',
          routes: [ ... ]
      })
  }
  • main.js
  import App from './App'
  const router = createRouter()
  export function createApp({
      const app = new Vue({
          router,
          render: h => h(App)
      })
      return { app, router }
  })
  • client.js
  import { createApp } from './main'
  const { app, router } = createApp()
  router.onReady(() => {
      app.$amount('#app')
  })
  • serve.js 服务端入口文件
  import { createApp } from './main'
  export default context => { // context
      return new Promise((resolve, reject) => {
          const { app, router } = createApp()
          router.push(context.url)
          router.onReady(() => {
              const mathComponents = router.getMathedComponents()
              if (!mathComponents) {
                  return resolve({ status: 404 })
              }
              resolve(app)
          }, reject)
      })
  }
  • webpack.server.config.js copy生产环境的webpack配置
  entry: {
      app: './src/serve.js'
  },
  target: 'node',
  output: {
      libraryTarget: 'commonjs2' // node规范
  },
  plugins: [
      new VueSSRServerPlugin(), // vue-server-renderer/server-plugin
      new HtmlWebpackPlugin({
        filename: 'index.ssr.html',
        template: 'index.ssr.html',
        inject: 'true',
        files: { js: 'app.js' },
        minify: {
            // removeComments: true, 需要保留注释
            collapseWhitespace: true,
            removeAttributeQuotes: true
        }
    })
  }]
  • webpack.client.config.js copy生产环境的webpack配置
  entry: './src/client.js',
  plugins: [
      new VueSSRClientPlugin(), // vue-server-renderer/client-plugin
  ]
  • service 运行的node服务器
const app = require('express')()
const { createBundleRenderer } = require('vue-server-renderer')
const serveBuild = require(path.resolve(__dirname,'./dist/vue-ssr-server-bundle.json))
const clientManifest = require(path.resolve(__dirname, './dist/vue-ssr-client-manifest.json'))
const template = fs.readFileSync(path.resolve(__dirname, './dist/index.srr.html'), 'utf-8')
const renderer = createBundleRenderer(serverBuild, {
    runInNewContext: false,
    template,
    clientManifest: clientManifest
})
server.get('*', (req, res) => {
    if(req.url != '/favicon.icon') {
        const context = { url: req.url }
        const ssrStream = renderer.renderToStream(context) // 生成流
        let buffers = []
        ssrStream.on('error', () => {})
        ssrStream.on('data', data => buffers.push(data))
        ssrStream.on('end', () => { res.end(
            Buffer.concat(buffers)
        )})
    }
})
server.listen(3000)

ssr框架nuxt/react-next

- pages 页面, 新建文件会自动注册路由
- components 放组件
- assets 放资源文件
- middleware 中间件
- 打包上线: 打包客服端和服务端js -> 开启node服务