2017-03-04 21:47:04

博客优化点:轮子之心

本博客虽然使用了SSR,但仍然采用了各种优化手段来提高首屏访问速度。本文介绍其中一种比较少见的优化手段:造轮子

造Ajax库

以tree-shaking后打包大小为30KB的axios为例,你的项目真正使用了axios的多少API呢?

对于本博客前端来说,仅仅使用了axios的get方法而已。另外,axios甚至有cancelable promise这种已经被tc39干掉的API,我们要它何用?

实际上,一个1KB的的AJAX库非常容易,这也是本博客正在使用的版本

function get(url, cb) {
  let requestTimeout, xhr
  try {
    xhr = new window.XMLHttpRequest()
  } catch (e) {
    try {
      xhr = new window.ActiveXObject('Msxml2.XMLHTTP')
    } catch (e) {
      return cb(new Error('XHR: not supported'))
    }
  }

  xhr.onreadystatechange = function() {
    if (xhr.readyState !== 4) return
    cb(xhr.status !== 200 ? new Error('XHR: server response status is ' + xhr.status) : false, xhr.responseText)
  }
  xhr.open('GET', url, true)
  xhr.setRequestHeader('Content-type', 'application/json')
  xhr.send()
}

如上的代码仅仅20行,但提供了IE6+级别的兼容性,如果想增加POST方法,仅仅需要多加2行而已

如果读者不大相信上面代码的健壮性,那么可以使用一个更流行的策略:使用fetch api,在fetch不存在时通过document.write写入cdn上的fetch垫片。

这样,打包大小会轻易减小30KB左右

造状态库

以vuex这个简单的库为例,虽然它在有些API中假装自己是immutable的库,然而并不是。只需要明白,vuex的全局状态只是Vue.prototype.$store.state这个全局mutable对象,那么造一个专供自己项目使用的vuex实际上是相当简单的。

对博主来说,既然vuex并不是Redux那样依赖immutable,那么我更倾向于让状态库偏向mutable的方向,此时,代码就更加好写了。

本博客使用如下的自造状态库,兼容了所有用到的vuex的api,例如install,getters,mapGetters以及vue-router-sync的api。

const global = typeof window !== 'undefined' ? window : process

class Store {
  constructor({
    state,
    actions,
    mutations,
    getters
  }) {
    this.actions = actions
    this.mutations = mutations
    this.getters = {}
    this._watcherVM = new global.Vue()
    const store = this
    const computed = {}

    forEachValue(getters, (fn, key) => {
      computed[key] = () => {
        const state = store.state
        return fn(state)
      }
      Object.defineProperty(store.getters, key, {
        get: () => {
          return store._vm[key]
        },
        enumerable: true
      })
    })
    store._vm = new global.Vue({
      data: {
        $$state: state
      },
      computed
    })
  }

  get state() {
    return this._vm._data.$$state
  }

  set state(state) {
    this._vm._data.$$state = state
  }

  commit(mutation, options) {
    if (mutation === 'router/ROUTE_CHANGED') {
      return global.Vue.set(this.state, 'route', options)
    }
    return this.mutations[mutation].call(null, this.state, options)
  }

  dispatch(action, options) {
    const result = this.actions[action].call(null, {
      commit: this.commit.bind(this),
      state: this.state,
      dispatch: this.dispatch.bind(this)
    }, options)
    if (result && typeof result.then === 'function') {
      return result
    } else {
      return Promise.resolve(result)
    }
  }

  watch(getter, cb, options) {
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }

  replaceState(state) {
    this._vm._data.$$state = state
  }

  registerModule(path, rawModule) {
    this.state.route = rawModule
  }

}

function install(_Vue) {
  if (global.Vue || _Vue.isInstalled === true) return
  _Vue.isInstalled = true
  global.Vue = _Vue
  global.Vue.mixin({ beforeCreate: vuexInit })
}

const mapGetters = (getters) => {
  const res = {}
  getters.forEach(item => {
    res[item] = function() {
      const result = this.$store.getters[item]
      return result
    }
  })
  return res
}

function vuexInit() {
  const options = this.$options
  // store injection
  if (options.store) {
    this.$store = options.store
  } else if (options.parent && options.parent.$store) {
    this.$store = options.parent.$store
  }
}

export default {
  install,
  Store
}

export {
  mapGetters
}

function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

上面的代码打包后仅为2KB,比vuex全套代码少了整整10KB。

另外注意上面install函数中如下两句

  if (global.Vue || _Vue.isInstalled === true) return
  _Vue.isInstalled = true

如果没有这两句的话,会造成SSR端的内存泄露,具体泄露的是Vue.mixin({})语句。

解决的办法有俩,一是像这样给导入进来的Vue一个标志,检测到被安装过就退出。另外一个就是使用webpack的externals,让Vue能够自动检测是否被安装。由于externals需要发布到npm上,因此博主选择第一种办法来手动修复了。

关于SSR端的Vue.mixin({})的内存泄露,官方的解释在这里

路由库和核心库

本文从上往下介绍的库,实际上是越来越难的。

路由已经是很靠近核心的库,造一个路由库的产入产出不会成正比,更别说造核心库了。

这种时候,通常是直接寻找开源的替代库,例如React的preact和rax,vue博主倒没有发现类似的库。

结语

本博客通过自造Ajax和状态库,减小了40KB的打包大小,再配合之前介绍的全部页面都进行分块按需加载的SSR优化策略,再次减小了10KB的打包大小。

这样,博客前台部署的代码从150KB的代码压缩到100KB,并且没有破坏博客使用的任何API以及功能,得到的首屏加载效果是相当喜人的。

本文链接:https://smallpath.me/post/blog-vuex-wheel

-- EOF --