关于vue.js:Vue-状态管理与与SSR详解

16次阅读

共计 21718 个字符,预计需要花费 55 分钟才能阅读完成。

1、vuex 简介
1、定义
在 vue 项⽬中,每个组件的数据都有其独⽴的作⽤域。当组件间须要跨层级或者同层之间频繁传递的时候,数据交互就会⾮常繁琐。vuex 的次要作⽤就是集中管理所有组件的数据和状态以及标准数据批改的⽅式。

官网解释:Vuex 是⼀个专为 Vue.js 应⽤程序开发的状态管理模式。它采⽤集中式存储管理应⽤的所有组件的状态,并以相应的规定保障状态以⼀种可预测的⽅式发⽣变动。

2、应用场景
⼀般来讲,是以项⽬中的数据交互复杂程度来决定的。具体包含以下场景:

项⽬组件间数据交互不频繁,组件数量较少:不使⽤状态治理
项⽬组件间数据交互频繁,但组件数量较少:使⽤ eventBus 或者 vue store 解决
项⽬组件间数据交互频繁,组件数量较多:vuex 解决
3、外围原理剖析

// a.vue
<h1>{{username}}</h1>

// b.vue
<h2>
  {{username}}
</h2>

/**
* 如果 username 须要在每个组件都获取一次,是不是很麻烦,尽管能够通过独特的父级传入,然而不都是这种现实状况
*/
复制代码

Flux 架构次要思维是利用的状态被集中寄存到一个仓库中,然而仓库中的状态不能被间接批改,必须通过特定的形式能力更新状态。

vuex 基于 flux 思维为 vue 框架定制,辨别同步和异步,定义两种行为,Actions 用来解决异步状态变更(外部还是调用 Mutations),Mutations 解决同步的状态变更,整个链路应该是一个闭环,单向的,完满符合 FLUX 的思维

「页面 dispatch/commit」->「actions/mutations」->「状态变更」->「页面更新」->「页面 dispatch/commit」…

2、vuex 五大外围
vue 应用繁多状态树,繁多状态树让咱们可能间接地定位任一特定的状态片段,在调试的过程中也能轻易地获得整个以后利用状态的快照。
用一个对象(骨干)就蕴含了全副的(分支)利用层级状态。
每个利用将仅仅蕴含一个 store 实例对象(骨干)。
每一个 Vuex 利用的外围就是 store(仓库)。“store”基本上就是一个容器,它蕴含着你的利用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地失去高效更新。
你不能间接扭转 store 中的状态。扭转 store 中的状态的惟一路径就是显式地提交 (commit) mutation。这样使得咱们能够不便地跟踪每一个状态的变动,从而让咱们可能实现一些工具帮忙咱们更好地理解咱们的利用。
1、State
以后应⽤状态,能够了解为组件的 data ⽅法返回的 Object

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {count: 0}
})

new Vue({
  store, // 把 store 的实例注入所有的子组件,this.$store 可拜访
  render: h => h(App)
}).$mount('#app')
复制代码

2、Getters
Getter 为 state 的计算属性,当须要反复对某个数据进⾏某种操作的时候能够封装在 getter ⾥⾯,当 state 中的数据扭转了当前对应的 getter 也会相应的扭转。

const store = new Vuex.Store({
  state: {date: new Date()
  },
  getters: {
    // Getter 承受 state 作为其第一个参数
    weekDate: (state) => {return moment(state.date).format('dddd'); 
    },
    //Getter 还也能够接管 getters 作为第二个参数
    dateLength: (state, getters) => {return getters.weekDate.length;},
    //Getter 自身为一属性,传参需返回一个函数
    weekDate: (state) => (fm) => {return moment(state.date).format(fm ? fm : 'dddd'); 
      }
  }
})

// 属性拜访
console.log(store.getters.weekDate)
console.log(store.getters.dateLength)
// 办法拜访,传参
console.log(store.getters.weekDate('MM Do YY'))
复制代码

3、Mutations
更改 Vuex 的 store 中的状态的惟一办法是提交 mutation,必须是同步函数。
Vuex 中的 mutation 十分相似于事件:每个 mutation 都有一个字符串的事件类型 (type) 和 一个 回调函数 (handler)。
回调函数就是咱们理论进行状态更改的中央,并且它会承受 state 作为第一个参数,第二个参数为载荷(payload)对象。

const store = new Vuex.Store({
  state: {count: 1},
  mutations: {
    // 事件类型 type 为 increment
    increment (state) {state.count++},
    // 增加第二个参数
    increment1 (state, payload) {state.count += payload.amount}
  }
})

// 参数调用
store.commit('increment')

// 1、把载荷和 type 离开提交
store.commit('increment1', {amount: 10})

// 2、整个对象都作为载荷传给 mutation 函数
store.commit({
  type: 'increment1',
  amount: 10
})

//----- 批改参数并应用常量,必须遵循 vue 规定,应用 set 或者对象解构 -------
// mutation-types.js
export const ADD_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import {ADD_MUTATION} from './mutation-types'
const store = new Vuex.Store({
  state: {
    student: {
      name: '小明',
      sex: '女'
    }
  },
  mutations: {
    // 应用 ES2015 格调的计算属性命名性能来应用一个常量作为函数名
    [ADD_MUTATION] (state) {Vue.set(state.student, 'age', 18) // 增加 age 属性
      // state.student = {...state.student, age: 18}
    }
  }
})
// 应用
import {ADD_PROPERTY} from '@/store/mutation-types'
this.$store.commit(ADD_PROPERTY)
复制代码

4、Actions
Action 相似于 mutation,不同在于:

Action 提交的是 mutation,而不是间接变更状态。
Action 能够蕴含任意异步操作
Action 函数承受一个 context 参数,它与 store 实例有着雷同的办法和属性,能够应用 context.commit 来提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters

const store = new Vuex.Store({
  state: {count: 0},
  mutations: {increment (state) {state.count++}
  },
  actions: {increment (context) {context.commit('increment')
    },
    // 应用解构简化
    increment ({commit}) {commit('increment')
      }
  }
})

// 散发 actions
store.dispatch('increment')
// 以载荷模式散发
store.dispatch('incrementAsync', {amount: 10})
// 以对象模式散发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})
复制代码

5、Modules
modules 的次要性能是为了防⽌ state 过于庞⼤和冗余,所以对其进⾏模块化分隔

模块外部的 state 是部分的,只属于模块自身所有,所以内部必须通过对应的模块名进行拜访
模块外部的 action、mutation 和 getter 默认可是注册在全局命名空间的,通过增加 namespaced: true 的形式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会主动依据模块注册的门路调整命名。

// 无命名空间
<script>
    import {mapState, mapMutations} from 'vuex';
    export default {
        computed: { //state 不同
            ...mapState({name: state => (state.moduleA.text + '和' + state.moduleB.text)
            }),
        },
        methods: { //mutation 全局
            ...mapMutations(['setText']),
            modifyNameAction() {this.setText();
            }
        },
    }
</script>

// 应用命名空间
export default {
    namespaced: true,
    // ...
}
<script>
    import {mapActions, mapGetters} from 'vuex';
    export default {
        computed: {
            ...mapState({name: state => (state.moduleA.text + '和' + state.moduleB.text)
            }),
            ...mapGetters({name: 'moduleA/detail'}),
        },
        methods: {
            ...mapActions({call: 'moduleA/callAction'}),
            /* 另外写法 */
            ...mapActions('moduleA', {call: 'callAction'}),
            ...mapActions('moduleA', ['callAction']),
            modifyNameAction() {this.call();
            }
        },
    }
</script>
复制代码

3、辅助函数
1、mapStates
应用 mapState 辅助函数帮忙咱们生成计算属性,入参为对象
当映射的计算属性的名称与 state 的子节点名称雷同时,咱们也能够给 mapState 传一个字符串数组

// 在独自构建的版本中辅助函数为 Vuex.mapState
import {mapState} from 'vuex'

export default {
  computed: {
    ...mapState({
      // 箭头函数可使代码更简练
      a: state => state.a,

      // 传字符串参数 'b'
      // 等同于 `state => state.b`
      bAlias: 'b',

      // 为了可能应用 `this` 获取部分状态
      // 必须应用惯例函数
      cInfo (state) {return state.c + this.info}
      }),
    ...mapState([
      // 映射 this.a 为 store.state.a
      'a',
      'b',
      'c'
    ])
}
复制代码

2、mapGetters

import {mapGetters} from 'vuex'

export default {
  // ...
  computed: {
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ]),
    ...mapGetters({doneCount: 'doneTodosCount'})
  }
}
复制代码

3、mapMutaions

import {mapMutations} from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([// 将 `this.increment()` 映射为 
      // `this.$store.commit('increment')`
      'increment', 
      // `mapMutations` 也反对载荷:// 将 `this.incrementBy(amount)` 映射为 
      // `this.$store.commit('incrementBy', amount)`
      'incrementBy' 
    ]),
    ...mapMutations({// 将 `this.add()` 映射为 
      // `this.$store.commit('increment')`
      add: 'increment' 
    })
  }
}
复制代码

4、mapActions

import {mapActions} from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([// 将 `this.increment()` 映射为 
      // `this.$store. dispatch('increment')`
      'increment', 
      // `mapActions` 也反对载荷:// 将 `this.incrementBy(amount)` 映射为 
      // `this.$store. dispatch('incrementBy', amount)`
      'incrementBy' 
    ]),
    ...mapActions({// 将 `this.add()` 映射为 
      // `this.$store. dispatch('increment')`
      add: 'increment' 
    })
  }
}
复制代码

4、源码解析
1、思路
flux 思维

问题:在开发中面临最多的场景是状态反复然而不集中,在不同的组件中依赖了同样的状态,反复就会导致不对等的危险。
思维:基于 FLUX 的思维,咱们设计的状态治理将是中心化的工具,也就是集中式存储管理利用的所有组件的状态,将所有的状态放在一个全局的 Tree 构造中,集中放在一起的益处是能够无效防止反复的问题,也更好的治理,将状态和视图层解耦。
解决:应用全局的 store 对象治理状态和数据,繁多状态树
状态流转

繁多流转
同步和异步分层:mutations 负责同步状态治理、actions 负责异步事件(外部通过 mutations 扭转状态)
与 vue 集成

通过插件将 vue 集成在一起,通过 mixin 将 $store 这样的快速访问 store 的快捷属性注入到每一个 vue 实例中
响应式

利用 vue 的 data 响应式实现
扩大

辅助函数
模块化
插件反对
2、源码解析
1、store 注册

/**
* store.js - store 注册
*/
let Vue

// vue 插件必须要这个 install 函数
export function install(_Vue) {
  Vue = _Vue // 拿到 Vue 的结构器,存起来
  // 通过 mixin 注入到每一个 vue 实例 👉 https://cn.vuejs.org/v2/guide/mixins.html
  Vue.mixin({beforeCreate: vuexInit})
  
  function vuexInit () {
    const options = this.$options // 创建对象入参
    // 这样就能够通过 this.$store 拜访到 Vuex 实例,拿到 store 了
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {this.$store = options.parent.$store}
  }
}
复制代码

2、store 的响应式

/**
* store.js - 实现响应式
*/
export class Store {constructor(options = {}) {resetStoreVM(this, options.state)
  }
  
  get state () {return this._vm._data.$$state}
}

function resetStoreVM(store, state) {
  // 因为 vue 实例的 data 是响应式的,正好利用这一点,就能够实现 state 的响应式
  store._vm = new Vue({
    data: {$$state: state}
  })
}
复制代码

3、衍生数据

/**
* store.js - 衍生数据(getters)*/
export class Store {constructor(options = {}) {
    
    const state = options.state
    
    resetStoreVM(this, state)
    
    // 咱们用 getters 来收集衍生数据 computed
    this.getters = {}
    
    // 简略解决一下,衍生不就是计算一下嘛,传人 state
    _.forEach(this.getters, (name, getterFn) => {
      Object.defineProperty(this.getters, name, {get: () => getterFn(this.state)
      })
    })
  }
  
  get state () {return this._vm._data.$$state}
}

function resetStoreVM(store, state) {
  store._vm = new Vue({
    data: {$$state: state}
  })
}
复制代码

4、Actions/Mutations

/**
* store.js - Actions/Mutations 行为扭转数据
*/
export class Store {constructor(options = {}) {
    
    const state = options.state
    
    resetStoreVM(this, state)
    
    this.getters = {}
    
    _.forEach(options.getters, (name, getterFn) => {
      Object.defineProperty(this.getters, name, {get: () => getterFn(this.state)
      })
    })
    
    // 定义的行为,别离对应异步和同步行为解决
    this.actions = {}
    this.mutations = {}
    
    _.forEach(options.mutations, (name, mutation) => {this.mutations[name] = payload => {
        // 最终执行的就是 this._vm_data.$$state.xxx = xxx 这种操作
        mutation(this.state, payload)
      }
    })
    
    _.forEach(options.actions, (name, action) => {this.actions[name] = payload => {
        // action 专一于解决异步,这里传入 this,这样就能够在异步外面通过 commit 触发 mutation 同步数据变动了
        action(this, payload)
      }
    })
  }
  
  // 触发 mutation 的形式固定是 commit
  commit(type, payload) {this.mutations[type](payload)
  }
  
  // 触发 action 的形式固定是 dispatch
  dispatch(type, payload) {this.actions[type](payload)
  }
  
  get state () {return this._vm._data.$$state}
}

function resetStoreVM(store, state) {
  store._vm = new Vue({
    data: {$$state: state}
  })
}
复制代码

5、分形,拆分出多个 Module

// module 能够对状态模型进行分层,每个 module 又含有本人的 state、getters、actions 等

// 定义一个 module 基类
class Module {constructor(rawModule) {this.state = rawModule || {}
    this._rawModule = rawModule
    this._children = {}}
  
  getChild (key) {return this._children[key]
  }
  
   addChild (key, module) {this._children[key] = module
  }
}

// module-collection.js 把 module 收集起来
class ModuleCollection {constructor(options = {}) {this.register([], options)
  }
  
  register(path, rawModule) {const newModule = new Module(rawModule)
    if (path.length === 0) {
      // 如果是根模块 将这个模块挂在到根实例上
      this.root = newModule
    }
    else {const parent = path.slice(0, -1).reduce((module, key) => {return module.getChild(key)
      }, this.root)
      
      parent.addChild(path[path.length - 1], newModule)
    }
    
    // 如果有 modules,开始递归注册一波
    if (rawModule.modules) {_.forEach(rawModule.modules, (key, rawChildModule) => {this.register(path.concat(key), rawChildModule)
      })
    }
  }
}

// store.js 中
export class Store {constructor(options = {}) {
    // 其余代码...
    
    // 所有的 modules 注册进来
    this._modules = new ModuleCollection(options)
    
    // 然而这些 modules 中的 actions, mutations, getters 都没有注册,所以咱们原来的办法要从新写一下
    // 递归的去注册一下就行了,这里抽离一个办法进去实现
    installModule(this, this.state, [], this._modules.root);
  }
}

function installModule(store, state, path, root) {
  // getters
  const getters = root._rawModule.getters
  if (getters) {_.forEach(getters, (name, getterFn) => {
      Object.defineProperty(store.getters, name, {get: () => getterFn(root.state)
      })
    })
  }
  
  // mutations
  const mutations = root._rawModule.mutations
  if (mutations) {_.forEach(mutations, (name, mutation) => {let _mutations = store.mutations[name] || (store.mutations[name] = [])
      _mutations.push(payload => {mutation(root.state, payload)
      })
      
      store.mutations[name] = _mutations
    })
  }
  
  // actions
  const actions = root._rawModule.actions
  if (actions) {_.forEach(actions, (name, action) => {let _actions = store.actions[name] || (store.actions[name] = [])
      _actions.push(payload => {action(store, payload)
      })
      
      store.actions[name] = _actions
    })
  }
  
  // 递归
  _.forEach(root._children, (name, childModule) => {installModule(this, this.state, path.concat(name), childModule)
  })
}
复制代码

6、插件机制

(options.plugins || []).forEach(plugin => plugin(this))
复制代码

以上只是以最简化的代码实现了 vuex 外围的 state module actions mutations getters 机制,如果对源代码感兴趣,能够看看若川的文章

二、Vue-ssr
参考资料:

1、vue-ssr 官网指南

1、模版渲染
1、实例 render 属性
渲染函数:(createElement: () => VNode, [context]) => VNode

入参:接管一个 createElement 办法作为第一个参数用来创立 VNode
出参:VNode 对象
如果组件是一个函数组件,渲染函数还会接管一个额定的 context 参数,为没有实例的函数组件提供上下文信息
Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数,留神:.vue 组件中 temlate 局部会渲染,需去除。
组件树中的所有 VNode 必须是惟一的,须要反复很屡次的元素 / 组件,你能够应用工厂函数来实现

Vue.component('anchored-heading', {render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
/* 渲染模版如下:<script type="text/x-template" id="anchored-heading-template">
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
</script>
*/

//VNode 惟一,应用工程函数实现
{render: function(h){
    return h('div',
      Array.apply(null, { length: 20}).map(function () {return createElement('p', 'hi')
      })
      )
  }
}
复制代码

2、createElement 函数
通常将 h 作为 createElement 的别名

/** 创立虚构 dom 函数:* createElement({String | Object | Function}, [object], {string | array}): VNode
 */
createElement(//1、必输:{String | Object | Function}
  //HTML 标签名、组件选项对象,或者 resolve 了任何一种的一个 async 函数
    'div',
  
  //2、可选 {Object}
  // 与模板中 attribute 对应的数据对象
  {
      // 与 `v-bind:class` 的 API 雷同,// 承受一个字符串、对象或字符串和对象组成的数组
      'class': {
        foo: true,
        bar: false
      },
      // 与 `v-bind:style` 的 API 雷同,// 承受一个字符串、对象,或对象组成的数组
      style: {
        color: 'red',
        fontSize: '14px'
      },
      // 一般的 HTML attribute
      attrs: {id: 'foo'},
      // 组件 prop
      props: {myProp: 'bar'},
      // DOM property
      domProps: {innerHTML: 'baz'},
      // 事件监听器在 `on` 内,// 但不再反对如 `v-on:keyup.enter` 这样的润饰器。// 须要在处理函数中手动查看 keyCode。on: {click: this.clickHandler},
      // 仅用于组件,用于监听原生事件,而不是组件外部应用
      // `vm.$emit` 触发的事件。nativeOn: {click: this.nativeClickHandler},
      // 自定义指令。留神,你无奈对 `binding` 中的 `oldValue`
      // 赋值,因为 Vue 曾经主动为你进行了同步。directives: [
        {
          name: 'my-custom-directive',
          value: '2',
          expression: '1 + 1',
          arg: 'foo',
          modifiers: {bar: true}
        }
      ],
      // 作用域插槽的格局为
      // {name: props => VNode | Array<VNode>}
      scopedSlots: {default: props => createElement('span', props.text)
      },
      // 如果组件是其它组件的子组件,需为插槽指定名称
      slot: 'name-of-slot',
      // 其它非凡顶层 property
      key: 'myKey',
      ref: 'myRef',
      // 如果你在渲染函数中给多个元素都利用了雷同的 ref 名,// 那么 `$refs.myRef` 会变成一个数组。refInFor: true
    },
  
    //3、可选:{String | Array}
    // 子级虚构节点 (VNodes),由 `createElement()` 构建而成,也能够应用字符串来生成“文本虚构节点”[
      '先写一些文字',
      createElement('h1', '一则头条'),
      createElement(MyComponent, {
        props: {someProp: 'foobar'}
      })
    ]
)
复制代码

3、Vnode 对象

// VNode 对象
{
  asyncFactory: undefined
  asyncMeta: undefined
  children: (2) [VNode, VNode]
  componentInstance: undefined
  componentOptions: undefined
  context: VueComponent {_uid: 4, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: VueComponent, …}
  data: {
    attr: {
      id: 'div-id',
      name: 'div-name'
    }
  },
  elm: div
  fnContext: undefined
  fnOptions: undefined
  fnScopeId: undefined
  isAsyncPlaceholder: false
  isCloned: false
  isComment: false
  isOnce: false
  isRootInsert: true
  isStatic: false
  key: undefined  //key 属性
  ns: undefined
  parent: VNode {tag: "vue-component-92", data: {…}, children: undefined, text: undefined, elm: div, …}
  raw: false
  tag: "div"   // 组件名
  text: undefined
  child: undefined
  [[Prototype]]: Object
}
复制代码

4、模版渲染
1、v-if/v-for
应用 if 和 map 函数实现

<ul v-if="items.length">
  <li v-for="item in items">{{item.name}}</li>
</ul>
<p v-else>No items found.</p>

<!-- 如下渲染函数实现 -->
<!--
{render(h){if(this.items.length){
            return h('ul', this.items.map(item => {return h('li', item.name)
            })
        )else{return h('p', 'No items found.')
        }
    }
}
-->
复制代码

2、v-model
本人实现相干逻辑

<input v-model="value"></input>

<!-- 如下渲染函数实现 -->
<!--
{props:[value],
    render(h){
        let self = this
        return h('input', {
      //DOM property
            domPops:{value: self.value},
      on: {input: function(event){self.$emit('input', event.target.value)
                }
            }
        })
    }
}
-->
复制代码

3、事件 & 按键修饰符
.passive/.capture/.once:提供相应前缀应用

.passive:前缀 &
.capture:前缀!.once:前缀~
.capture.once 或.once.capture:前缀~!其余润饰符号能够调用公有办法

//.passive .capture .once
on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}

// 其余修饰符
on: {keyup: function (event) {
    //.self
    // 如果触发事件的元素不是事件绑定的元素
    // 则返回
    if (event.target !== event.currentTarget) return
    
    //.shift | .enter/.13
    // 如果按上来的不是 enter 键或者
    // 没有同时按下 shift 键
    // 则返回
    if (!event.shiftKey || event.keyCode !== 13) return
    
    //.stop
    // 阻止 事件冒泡
    event.stopPropagation()
    
    //.prevent
    // 阻止该元素默认的 keyup 事件
    event.preventDefault()
    // ...
  }
}
复制代码

4、插槽
动态插槽:this.$slots
作用域插槽:this.$scopedSlots
组件中传递作用域插槽:scopedSlots 属性

<!-- <div><slot></slot></div> -->
render: function(createElement){return createElement('div', this.$slots.default)
}

<!-- <div><slot :text="message"></slot></div> -->
render: function(createELement){
  return createElement('div', this.$scopedSlots.default({text: this.message}))
}

<!-- 
    <div>
      <child v-slot="props">
        <span>{{props.text}}</span>
      </child>
  </div>
-->
render: function(createElement){
  return createElement('div', [
    createElement('child', {
      scopedSlots: {
        // 在数据对象中传递 `scopedSlots`
        // 格局为 {name: props => VNode | Array<VNode>}
        default: function(props){return createElement('span', props.text)
        }
      }
    })
  })
}        
复制代码

5、jsx
应用 babel 插件,渲染函数应用 h

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render: function (h) {
    return (<AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})
复制代码

2、csr 与 ssr
首先让咱们看看 CSR 的过程(划重点,浏览器渲染原理根本流程)

1、csr
浏览器渲染原理根本流程
浏览器通过申请失去一个 HTML 文本
渲染过程解析 HTML 文本,构建 DOM 树
解析 HTML 的同时,如果遇到内联款式或者款式脚本,则下载并构建款式规定(stytle rules),若遇到 JavaScript 脚本,则会下载执行脚本。
DOM 树和款式规定构建实现之后,渲染过程将两者合并成渲染树(render tree)
渲染过程开始对渲染树进行布局,生成布局树(layout tree)
渲染过程对布局树进行绘制,生成绘制记录
渲染过程的对布局树进行分层,别离栅格化每一层,并失去合成帧
渲染过程将合成帧信息发送给 GPU 过程显示到页面中
流程:加载 html 文件、下载资源、脚本解析、运行往 div 中插入元素,渲染页面
特点:不利用 seo、首屏加载工夫长(time-to-content)
很容易发现,CSR 的特点就是会在浏览器端的运行时去动静的渲染、更新 DOM 节点,特地是 SPA 利用来说,其模版 HTML 只有一个 DIV,而后是运行时(React,Vue,Svelte 等)动静的往里插入内容,这样的话各种 BaiduSpider 拿不到啥无效信息,天然 SEO 就不好了,我的项目一旦简单起来,bundle 可能超乎寻常的大 … 这也是一个开销。

2、ssr


服务端实现了渲染过程,将渲染实现的 HTML 字符串或者流返回给浏览器,就少了脚本解析、运行这一环节,实践上 FP 体现的更佳,SEO 同样晋升。毛病:

简单,同构我的项目的代码复杂度直线回升,因为要兼容两种环境
对服务端的开销大,既然 HTML 都是拼接好的,那么传输的数据必定就大多了,同时,拿 Node 举例,在解决 Computed 密集型逻辑的时候是阻塞的,不得不上负载平衡、缓存策略等来晋升
CI/CD 更麻烦了,须要在一个 Server 环境,比方 Node
一般来说,ToB 的业务场景根本不须要 SSR,须要 SSR 的肯定是对首屏或者 SEO 有强诉求的,局部场景可应用预渲染。

CSR 和 SSR,咱们现今常见的渲染计划有 6 - 7 种:

3、同构直出
一份代码,既能够客户端渲染,也能够服务端渲染,采纳水合 hydration 计划,对 FP 有帮忙,然而不能晋升 TTI(可交互工夫)。

1、原理
ssr 渲染页面整体构造晋升 fp,在此基础上 csr 中增加事件等操作实现最终可交互的页面。ssr 只⽀持 beforeCreate、created ⽣命周期,csr 中反对所有操作倡议搁置在 mounted 事件后。csr 为页面 = 模块 + 数据,利用 = 路由 + 页面,同构就是同构路由、模版、数据。

2、实际
css 如何解决:服务端的 webpack 不必关注 CSS,客户端会打包进去的,到时候推 CDN,而后批改 public path 即可

服务端的代码不须要分 chunk,Node 基于内存一次性读取反而更高效

如果有一些办法须要在特定的环境执行,比方客户端环境中上报日志,能够利用 beforeMouted 之后的生命周期都不会在服务端执行这一特点,当然也能够应用 isBrowser 这种判断

CSR 和 SSR 的切换和降级

// 总有一些奇奇怪怪的场景,比方就只须要 CSR,不须要 SSR
// 或者在 SSR 渲染的时候出错了,页面最好不要解体啊,能够降级成 CSR 渲染,保障页面可能进去

// 相互切换的话,总得有个标识是吧,通知我用 CSR 还是 SSR
// search 就不错,/demo?ssr=true
module.exports = function(req, res) {if(req.query.ssr === 'true'){const context = { url: req.url}
    renderer.renderToString(context, (err, html) => {if(err){res.render('demo') // views 文件下的 demo.html
      }
      res.end(html)
    })
  } else {res.render('demo')
  }
}
复制代码

Axios 封装,至多辨别环境,在客户端环境是须要做代理的

3、Vue-ssr 优化计划
页面级别的缓存,比方 nginx micro-caching
设置 serverCacheKey,如果雷同,将应用缓存,组件级别的缓存
CGI 缓存,通过 memcache 等,将雷同的数据返回缓存一下,留神设置缓存更新机制
流式传输,然而必须在 asyncData 之后,否则没有数据,阐明也可能会被 CGI 耗时阻塞(服务端获取数据后,加载至全局,客户端间接应用即可)
分块传输,这样前置的 CGI 实现就会渲染输入
JSC,就是不必 vue-loader,最快的计划
4、代码实战
1、Npm Script

"build": "npm run build:client && npm run build:server",
"server": "cross-env NODE_ENV=production nodemon ./server/index.js",
"dev:server": "cross-env NODE_ENV=development nodemon ./server/index.js",
"build:client": "vue-cli-service build",
"build:server": "cross-env VUE_ENV=server vue-cli-service build  --no-clean",
"dev:client": "vue-cli-service serve"

vue inspect --mode "production" > output.js
复制代码

2、Project Info
cli: vue-cli @vue/cli 4.5.13
ui: element-ui
state: vuex
router: vue-router hash mode
lang: vue-i18n
http: axios
package manager: npm
unit-test: jest、mocha
动态检查和格式化工具: eslint、prettier
Mock:mockjs
server: koa、koa-mount、koa-router、koa-static、xss
vue plugins: vue-infinite-loading、vue-meta、vue-server-renderer(ssr)
tools: reset-css、cross-env(环境变量配置)
举荐服务端渲染框架:nuxt.js
3、Project Structure

├── mock              //mock 脚步及数据
├── node_modules      // 模块内容
├── public            // 动态资源文件,不参加 webpack 打包,蕴含 server.html 和 index.html
├── server            //web 服务器代码,实现 ssr
├── utils             //server 和 client 专用 utils 工具类
├── src               // 源代码目录
│   ├── assets        // 动态资源,被 webpack 打包
│   ├── components    // 专用组件地位
│   ├── directives    // 全局指令
│   ├── lang          // 语言文件
│   ├── router        //router
│   ├── store         //store
│   ├── utils         // 工具办法,request 为 axios 库文件
│   ├── views         // 依照性能板块划分出的页面
│   |   └── home      // 子功能模块
│   |       ├── store // 子功能模块数据
│   |       ├── router// 子功能模块路由
│   |       ├── i18n  // 子功能模块语言
│   |       └── mock  // 子功能模块 mock 数据
│   ├── App.vue       // 入口页面
│   ├── app.js        // 利用入口文件,app 工程函数
│   ├── client-entry.js  // 客户端入口文件,初始化 store、挂载 dom
│   └── server-entry.js  // 服务端入口文件,返回 promise,解决路由、挂载 store
├── test              // 单元测试
├── .browserslistrc   //browserify 配置
├── .eslintrc.js      //eslint 配置
├── .babel.config.js  //bable 配置
├── package.json      //npm 包配置
├── nodemon.json      //nodemon 配置文件,设置 node 环境变量
├── vue.config.js     //vue 配置,辨别 server 和 client
└── README.md         // 我的项目阐明文件
复制代码

4、SSR 革新步骤
相干插件及库装置,配置打包 package.json 脚本

server:koa、koa-mount、koa-router、koa-static、xss、nodemon
vue-ssr:vue-server-renderer(应用 server-plugin 和 client-plugin)
环境变量配置:cross-env NODE_ENV=production 或者 nodemon.json
server 代码增加:

koa 代码启动:监听端口

动态资源:应用 koa-static、koa-mount 挂载动态服务器

api 接口:应用 koa-mount 挂载 api 接口,app.use(KoaMount(“/api”, ApiRequest))

路由挂载:返回 ssr 渲染页面

应用 koa-router 启动路由,监听任何根门路,返回 promise:router.get(‘/(.*)’, async ctx => {})
应用 vue-server-renderer 插件,渲染返回最终的 ssr 渲染页面
应用 vue-ssr-server-manifest.json 渲染页面,应用 vue-ssr-client-manifest.json 激活页面

 const VueServerRenderer = require("vue-server-renderer"); // 插件
 const serverBundle = require("../dist/vue-ssr-server-bundle.json"); // 服务端 bundle
 const clientManifest = require("../dist/vue-ssr-client-manifest.json");// 客户端激活 bundle
 const template = fs.readFileSync( // 渲染模版
   path.resolve(__dirname, "../dist/server.html"),
   "utf-8"
 );

 router.get("/(.*)", async (ctx) => {ctx.body = await new Promise((resolve, reject) => {
     const render = VueServerRenderer.createBundleRenderer(serverBundle, {
       runInNewContext: false,
       template,
       clientManifest, // 注入客户端 js,script 标签
     });
     // 渲染函数入参 context,拜访的门路 path 传入渲染函数中,获取 ssr 渲染页面
     const context = {url: ctx.path}; 
     render.renderToString(context, (err, html) => {if (err) {reject(err);
       }
       resolve(html);
     });
   });
 });
复制代码

原 client 代码革新为同构代码

增加 server.html 入口模版,用于服务器渲染

拆分 webpack 入口:server-entry.js、client-entry.js,批改 webpack 配置

client-entry.js: 挂载 dom,同步服务器端 store 数据

server-entry.js: 返回 promise 函数,resolve(app),解决路由和 store 数据

vue.config.js: 辨别 server 和 client

server:服务端 ajax 申请必须是是绝对路径,增加 DefinePlugin 插件
server 和 client 辨别:entry、target、output、server 增加 server-plugin,client 增加 client-plugin
server:node 端不须要打包 node_modules ⾥⾯的模块但容许 css 文件,externals = nodeExternals({allowlist: /.css$/})
其余优化:vue-loader 的 cache 解决、extract-css 的 bug 修复

 //server-entry.js
 export default (context) => {return new Promise((resolve, reject) => {const { app, router, store} = createVueApp();
     const meta = app.$meta();
     // 依据服务端传入的 url,设置以后客户端的 url
     router.push(context.url);
     // 有可能是异步路由,要期待异步路由和钩子函数解析完
     router.onReady(() => {
       // 期待 serverPrefetch 中的数据返回收,设置 state
       context.rendered = () => {
         // After the app is rendered, our store is now
         // filled with the state from our components.
         // When we attach the state to the context, and the `template` option
         // is used for the renderer, the state will automatically be
         // serialized and injected into the HTML as `window.__INITIAL_STATE__`.
         context.state = store.state;
         context.meta = meta;
       };
       resolve(app);
     }, reject);
   });
 };
 //client-entry.js
 if (window.__INITIAL_STATE__) {store.replaceState(window.__INITIAL_STATE__);
 }
 app.$mount("#app");
复制代码

vue 实例拆分:app.js,应用工程函数,拜访服务器屡次申请同实例净化,下同

router 实例拆分:应用工厂函数

路由如何解决:server 代码中通过 context 传入 url,打包入口 server-entry.js 中传入 router.push(context.url);
异步路由解决:应用 router.onReady(() => {resolve(app)}, reject)
store 实例拆分:应用工厂函数

view 页面数据处理:增加 serverPrefetch 服务端数据获取函数,可在服务器获取数据,留神:老 api 为 asycndata 只能在根组件应用、serverPrefect 新 api 任意组件应用

5、SSR 相干问题
如何期待异步申请的数据返回后,再渲染⻚⾯;客户端中的 store 如何同步服务端的数据?

组件中增加 serverPrefetch 选项,返回 promise
服务端⼊⼝ server-entry.js 增加 context.rendered 回调,渲染实现后执⾏,主动序列化注入 window.__INITIAL_STATE__变量
设置 context.state = store.state
客户端⼊⼝获取 window.__INITIAL_STATE__,设置 store 初始状态
meta、title 等标签如何注⼊?

应用 vue-meta 插件:vue.use(vueMeta)
主组件 App.vue,应用 metaInfo 增加 meta 和 title
子组件中可应用 metaInfo 函数,笼罩默认的 metaInfo
server-entry 中拿到 meta,并将其传⼊到 context 中,渲染 server.html 中的 meta 信息

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    {{{meta.inject().title.text()}}} 
    {{{meta.inject().meta.text()}}}
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>
复制代码

服务端渲染会执⾏的⽣命周期: 只执⾏ beforeCreate created, 不要在这两个⽣命周期⾥⾯增加定时器或者使⽤ window 等

服务器本地开发热更新:webpack 配置读取在内存中

最初
如果你感觉此文对你有一丁点帮忙,点个赞。或者能够退出我的开发交换群:1025263163 互相学习,咱们会有业余的技术答疑解惑

如果你感觉这篇文章对你有点用的话,麻烦请给咱们的开源我的项目点点 star:http://github.crmeb.net/u/defu 不胜感激!

PHP 学习手册:https://doc.crmeb.com
技术交换论坛:https://q.crmeb.com

正文完
 0