VueX & SSR
状态管理
本质就是一个对象
状态? 怎么管理? 为什么管理?
前端发展到今天,更主流的模式是 UI = f(state),视图通过底层框架(vue,react 等提供的能力)根据状态进行驱动。
状态:描述视图某个时间点的数据; 状态大致可以分为两类,本地状态和共享状态
本地状态就是 vue 中的 data,react中的 state,这里我们一般会用来控制弹窗的现实隐藏、loading效果等
状态自管理应用包含以下几个部分:
状态,驱动应用的数据源; 视图,以声明方式将状态映射到视图; 操作,响应在视图上的用户输入导致的状态变化。
共享状态其实是最头疼的问题,但又是最常见的场景,业务中肯定会出现大量需要兄弟节点通信、祖孙节点通信等情况的场景,通信的目的是为了状态分享,虽然可以通过一些方式,比如回调函数等手段实现,但都不是最佳实践
状态管理的方式:中心化和去中心化 中心化:redux / vueX 组件的数据最终归到一个数中 去中心化:this.state 没有根节点,可以在任意场合调用。但需要有一个方式处理共享
SPA应用最佳实践:容器组件 + UI 组件 容器组件负责逻辑
所以 Flux 架构及其追随者 Redux Vuex被提出,主要思想是应用的状态被集中存放到一个仓库中,但是仓库中的状态不能被直接修改,必须通过特定的方式才能更新状态
理想的状态管理工具需要解决的问题
- 状态更新的设计,
API足够少,且简单 - 如何共享状态
- 状态提升
- 状态下降
- 同步、异步的处理
- 持久状态和临时状态如何区分维护
- 状态更新的事务如何管理
- 去中心化
- ...
总的来说,这仍然是一个有很大空间的方向
VueX 核心原理
集中化管理
VueX 以及 redux 都受到 FLUX 思想影响:
- 状态都需要中心化、集中式的存储
- 状态的拿取符合单一原则,方式单一,保证准确性
例子:重复
// a.vue
<h1>{{ username }}</h1>
// b.vue
<h2>
{{ username }}
</h2>
/**
* 如果 username 需要在每个组件都获取一次,是不是很麻烦,虽然可以通过共同的父级传入,但是不都是这种理想情况
*/
集中式状态的好处:
- 可以消除重复的状态声明
- 消除了不对等的风险(如前后端状态不一致,前端usrname小红,后端小黑)
- 代码方便维护和测试
如何更新状态
FLUX 推崇以一种可预测的方式发生变化,而且有且唯一一种,这样的好处是所有的行为可预测,可测试,对于之后做个 dev-tool 去调试、时间旅行都很方便,现在的问题就是要去思考同步和异步的问题了,为了区分的更清楚
定义两种行为,Actions 用来处理异步状态变更(内部还是调用 Mutations),Mutations 处理同步的状态变更,整个链路应该是一个闭环,单向的,完美契合 FLUX 的思想
「页面 用户或默认行为 dispatch/commit」-> 「actions/mutations」-> 「状态变更」-> 「页面更新」-> 「页面 dispatch/commit」...
同步 commit mutations 异步 dispatch action -> commit mutations
如何和 vue 集成
- 每个组件如何拿到 状态实例?
通过
mixin将$store这样的快速访问store的快捷属性注入到每一个vue实例中
// 例子,组件内
this.$store.state.a
this.$store.commit
react 不可以,直接import 引入
- 数据得和视图响应式的连接起来
利用 vue data 里的状态响应
整个设计思路完成!!
代码
Step 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
}
}
}
Step2 - 响应式
/**
* 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
}
})
}
Step3 - 衍生数据 getter
/**
* 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
}
})
}
Step4 - 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)
}
})
}
// action this 可以调用 commit
// 触发 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
}
})
}
Step5 - 分形,拆分出多个 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)
})
}
Step6 - 插件机制 使用方式
(options.plugins || []).forEach(plugin => plugin(this))
SSR -> 首屏和SEO 友好
CSR VS SSR
浏览器基本渲染原理:
过程如下:
- 浏览器通过请求得到一个
HTML文本 - 渲染进程解析
HTML文本,构建DOM树 - 解析
HTML的同时,如果遇到内联样式或者样式脚本,则下载并构建样式规则(stytle rules),若遇到JavaScript脚本,则会下载执行脚本。 构建样式的时候,会阻塞JS,因为JS可能会改变样式 DOM树和样式规则构建完成之后,渲染进程将两者合并成渲染树(render tree) 到此,浏览器准备好渲染- 渲染进程开始对渲染树进行布局,生成布局树(
layout tree) - 渲染进程对布局树进行绘制,生成绘制记录
- 渲染进程的对布局树进行分层,分别栅格化每一层,并得到合成帧
- 渲染进程将合成帧信息发送给
GPU进程显示到页面中 最后调用显卡,进行绘制
CSR 客户端渲染
一开始只有一个DIV -> 下载并运行 JS -> 框架本身 diff -> 合并数据 -> 调图层 -> 虚拟DOM 上调用 DOM 操作/vue -> 映射到真实的 DOM上
因此,首屏时间长并且SEO不友好,爬虫爬的都是空DIV
SSR 服务端渲染
框架diff 组装数据 等操作在服务端完成 -> 最终输出 html 的字符串 -> 前端直接渲染
缺点:
- 服务端开销较大,尤其是大型项目
- 服务端往前端传输的数据量较大
- 复杂,同构项目的代码复杂度直线上升,因为要兼容两种环境
同构应用 - 一份代码在服务端和客户端
挂载之后的生命周期都不可以在服务端渲染:服务端不会处理DOM操作,事件等,只会有简单的html轮廓,吐回给前端
水合:将事件等操作合并给拿到的 html,形成交互
水合的好处:更快的渲染了页面
客户端渲染,基本可以这样概括:页面 = 模版 + 数据,应用 = 路由 + 多个页面
模版(<template> + 样式)
数据(data + methods)
-
服务端的
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封装,至少区分环境,在客户端环境是需要做代理的
VUE-SSR 优化方案:
- 页面级别的缓存,比如
nginxmicro-caching - 设置
serverCacheKey,如果相同,将使用缓存,组件级别的缓存 CGI缓存,通过memcache等,将相同的数据返回缓存一下,注意设置缓存更新机制- 流式传输,但是必须在
asyncData之后,否则没有数据,说明也可能会被CGI耗时阻塞 - 分块传输,这样前置的
CGI完成就会渲染输出,但是这个方案难啊 - JSC,就是不用
vue-loader