Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

数说官网开发总结——Vue从CSR到SSR的实践思路 #4

Open
WarpPrism opened this issue Nov 13, 2016 · 0 comments
Open

数说官网开发总结——Vue从CSR到SSR的实践思路 #4

WarpPrism opened this issue Nov 13, 2016 · 0 comments

Comments

@WarpPrism
Copy link
Owner

WarpPrism commented Nov 13, 2016

CSR:client side render 客户端渲染

SSR:server side render 服务端渲染

下文中统一使用上述简写。

csr VS ssr

官网初版采用的就是csr,不需要服务端代码,服务端只需返回index.html文件,然后index.html再通过link,script标签引用webpack打包后的css,js文件。这样的话,所有组件的渲染都放在客户端完成,最后借助vue-router根据不同的页面路由,在 <router-view></router-view> 里面渲染对应的组件,就可以把官网做成基于vue的SPA应用。

这样做的好处是:

  1. 项目结构简单,不需要后端代码渲染组件

  2. 采用vue1.0 + vue-router0.7.x + webpack 有规范的文档可以参考,API相对固定

  3. SPA的种种优点,用户体验nice

但同时也存在以下弊端:

  1. 对SEO不友好,作为官网而言,这一点不可忽视,爬虫只能看到 <router-view></router-view> ,却看不到里面实际的内容

  2. 大量的前端js代码,提升客户端负载,导致页面首屏时间延长(更何况官网还不存在ajax请求)

为了解决csr的弊端,决定将官网项目迁移至ssr版本,即vue2.0的new feature: ssr

既然是迁移,注定会遇到很多的问题,主要有:

  1. 从vue1.0 到 vue2.0 API的变更,例如不支持组件间dispatch 和 broadcast,只能采用vuex进行全局组件状态管理。

vuex 是一种借鉴flux的应用架构,也是以单向数据流的方式进行信息传递,它的数据流动结构如下图所示:

Vuex

即 vue component 触发 action 操作,action 通过调度mutation触发对state的变更,state变更的时候就会更新所有受其影响的组件。action, mutation和store存储在vuex的store对象中。

  1. vue-router 也升级为vue-router2.0,导致路由列表变动:
const router = new VueRouter({
  mode: 'history',
  scrollBehavior: ()=>{{y:0}},
  routes: [
    { path: '/', component: Index },
    { path: '/index', component: Index },
    { path: '/pands', component: PandS,
      children: [
        { path: '', component: Solution},
        { path: 'solution', component: Solution},
        { path: 'product', component: Product}
      ]
    },
    { path: '/report', component: Report },
    { path: '/about', component: About },
    { path: '*', redirect: '/' },
  ]
});
  1. 如何实现 vue 组件的服务端渲染,并返回html代码? (How to code T_T)

vue2.0 开启ssr

介绍下vue2.0的新内容

vue2.0于2016年4月开启public preview版本,提供了一系列新特性: 原文链接

  1. virtual dom渲染层,更轻更快,提升渲染速度,降低内存消耗

  2. 既支持模板语法,也支持类似jsx的render函数语法

  3. 支持流式服务端渲染,渲染组件的时候返回stream,然后直接pipe到HTTP response

  4. weex + Native?

  5. PS: 这很 react

如何做?

我们可以先看一下vue官方给的项目架构图:

vue-ssr-architecture

很显然,要兵分两路:

1 服务端渲染: server-entry => server-bundle => bundleRenderer => render to html

&

2 客户端混合: client-entry => client-bundle => virtual dom => Hydrate

兵线 1 服务端渲染

代码参考官方 demo vue-hacker-news2.0

首先,建立服务端,可以搭建一个简单的express服务器:

const app = express()

const port = process.env.PORT || 8080
app.listen(port, () => {
  console.log(`server started at localhost: ${port}`)
})

然后借助vue-server-render API创建renderer

const createBundleRenderer = require('vue-server-renderer').createBundleRenderer

function createRenderer(bundle) {
  return createBundleRenderer(bundle, {
    cache: require('lru-cache')({
      max: 1000,
      maxAge: 1000 * 60 * 15
    })
  })
}

let renderer

if (isProd) {
    const bundlePath = resolve('./dist/server-bundle.js')
    renderer = createRenderer(fs.readFileSync(bundlePath, 'utf-8'))
}

在上述代码中,我们通过createBundleRenderer函数将server-bundle.js 打包,新建了一个renderer, 而这个server-bundle.js 其实就是用webpack的node模式将整个vue app包括router,store神马的单独打包,这样在每次请求和渲染的时候,可以通过node的vm模块将代码运行在一个全新的context下,避免了由于缓存导致的vuex state冲突,虽然冲突过程不得而知,但尽可能解耦总是对的。

此外,server-bundle.js需要暴露一个函数,并传入render context object,返回值是一个Promise

export default context => {
  // set router's location
  router.push(context.url)
  
  const s = isDev && Date.now()

  // Call preFetch 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.
  return Promise.all(router.getMatchedComponents().map(component => {
    // if (component.preFetch) {
    //   return component.preFetch(store)
    // }
  })).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.initialState = store.state
    return app
  })
}

其中context object参数就是 renderer.renderToStream(context)中的context参数。

const html = (() => {
  const template = fs.readFileSync(resolve('./index.html'), 'utf-8')
  const i = template.indexOf('{{ APP }}')

  const style = isProd ? '<link rel="stylesheet" href="/dist/styles.css">':  ''
  return {
    head: template.slice(0, i).replace('{{ STYLE }}', style),
    tail: template.slice(i + '{{ APP }}'.length)
  }
})()

app.get('*', (req, res) => {
  if (!renderer) {
    return res.end('waiting for compilation...')
  }

  const s = Date.now()
  const context = {url: req.url}
  const renderStream = renderer.renderToStream(context)

  let firstChunk = true

  res.write(html.head)

  renderStream.on('data', chunk => {
    if (firstChunk) {
      if (context.initialState) {
        res.write(
          `<script>window.__INITIAL_STATE__=${
            serialize(context.initialState, { isJSON: true })
          }</script>`
        )
      }
      firstChunk = false
    }
    res.write(chunk)
  })

  renderStream.on('end', ()=>{
    res.end(html.tail)
    console.log(`whole request; ${Date.now() - s}ms`)
  })

  renderStream.on('error', err => {
    throw err
  })
})

这里过程有点复杂TAT,可以看到,服务器在收到请求后,会将请求url封装成context对象,有了context就可以 renderer.renderToStream(context),同时这个context也传入server-bundle.js的暴露函数中,router拿到context url中对应的组件,然后blabla,需要数据就preFetchData(这里我们的应用不需要获取额外数据),如果所有preFetch的promise 变为 resolve,就将store的state写入context中,这样渲染需要的所有数据就准备就绪了。

最后一步,就开始渲染,渲染成html字符串,然后通过response返回,实现了所谓高大上的SSR,服务器最终吐出了页面。。。。

兵线 2 客户端混合 client side hydration

client side的过程和 csr 类似,只不过vue2.0输出的是virtual dom,在挂载的时候需要进行hydration

在服务端渲染的输出中,根元素会带有server-rendered="true"的属性,当尝试向这个元素挂载 vue instance 的时候,vue就会尝试让 virtual dom 和 rendered dom 进行融合,如果两者不一致,则会尝试重新渲染,从而实现DOM内容的更新。

总结

以上就是我在迁移官网项目时对ssr过程的理解,很多地方理解也不深入,期待 vue2.0 的beta版本,作者能够更详细的阐明实现的细节,或者优化现有的应用架构,特别是dataFetch那一块,太多promise调用了。。。

能否有更好的实践呢?

接下来,需要发一下测试平台,比较两个版本的性能好坏以及进一步的优化。。。

vue的实现方式很有新意,性能也可以,所以我决定 好好学习,天天向上,早日精通React。。。^_^|||

参考

Vue 2 服务端渲染初探

vue-server-renderer

官方例子vue-hacker-news2.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant