Vue-hot-reload-api 源碼解析

Vue-hot-reload-api 源碼解析

原由

最近在搞san框架的熱加載方案,天然是少不了向成熟的框架學習(偷窺ing)。熱加載方案基本也只是主流框架在作,且作的比較成熟,大部分應用開發者並不會接觸到這部分東西,因此相應的資料比較少。google了一下這個庫,發現木有人作相應的解析,順手記錄下好了。前端

什麼是Vue-hot-reload-api?

衆所周知,*.vue文件爲廣大開發者提供了良好的開發體驗,vue-loader的原理很少贅述,在vue的腳手架中,webpack經過vue-loader來解析*.vue文件,把template、js和style文件分離並讓相應的loader去處理。vue

在這個過程當中,vue-loader還會作些其餘事情,好比向client端注入hot-reload相應的代碼,構建時編譯等等。node

webpack的hmr原理也很少說了,vue的熱加載就是經過注入的代碼來實現組件的熱更新,下面來看下使用時的文檔和源碼。webpack

用法

先來看下官方文檔。git

你僅會在開發一個基於 Vue components 構建工具的時候用到這個。對於普通的應用,使用 vue-loader 或者 vueify 就能夠了。github

文檔中明確說明了,通常使用不須要用到這個,只有在開發相應的構建工具時纔會用到。web

// 定義一個組件做爲選項對象
// 在vue-loader中,這個對象是Component.options
const myComponentOptions = {
  data () { ... },
  created () { ... },
  render () { ... }
}

// 檢測 Webpack 的 HMR API
// https://doc.webpack-china.org/guides/hot-module-replacement/
if (module.hot) {
  const api = require('vue-hot-reload-api')
  const Vue = require('vue')

  // 將 API 安裝到 Vue,而且檢查版本的兼容性
  api.install(Vue)

  // 在安裝以後使用 api.compatible 來檢查兼容性
  if (!api.compatible) {
    throw new Error('vue-hot-reload-api與當前Vue的版本不兼容')
  }

  // 此模塊接受熱重載
  // 在這兒多說一句,webpack關於hmr的文檔實在是太。。。
  // 各大框架的loader中關於hmr的實現都是基於自身模塊接受更新來實現
  module.hot.accept()

  if (!module.hot.data) {
    // 爲了將每個組件中的選項變得能夠熱加載,
    // 你須要用一個不重複的id建立一次記錄,
    // 只須要在啓動的時候作一次。
    api.createRecord('very-unique-id', myComponentOptions)
  } else {
    // 若是一個組件只是修改了模板或是 render 函數,
    // 只要把全部相關的實例從新渲染一遍就能夠了,而不須要銷燬重建他們。
    // 這樣就能夠完整的保持應用的當前狀態。
    api.rerender('very-unique-id', myComponentOptions)

    // --- 或者 ---

    // 若是一個組件更改了除 template 或 render 以外的選項,
    // 就須要整個從新加載。
    // 這將銷燬並重建整個組件(包括子組件)。
    api.reload('very-unique-id', myComponentOptions)
  }
}

經過使用說明能夠看出,vue-hot-reload-api暴露的接口仍是很清晰的,下面來看下具體源碼實現。api

var Vue // late bind
var version

// 全局對象__VUE_HOT_MAP__來保存全部的構造器和實例
var map = window.__VUE_HOT_MAP__ = Object.create(null)
var installed = false

// 這個參數來判斷是vue-loader仍是vueify在調用
var isBrowserify = false

// 2.0.0-alpha.7版本前的初始化鉤子名是init,這個參數來做區分
var initHookName = 'beforeCreate'

exports.install = function (vue, browserify) {
  if (installed) return
  installed = true

// 判斷打包的是esodule仍是普通的js函數
  Vue = vue.__esModule ? vue.default : vue
  version = Vue.version.split('.').map(Number)
  isBrowserify = browserify

  // compat with < 2.0.0-alpha.7
  if (Vue.config._lifecycleHooks.indexOf('init') > -1) {
    initHookName = 'init'
  }

  exports.compatible = version[0] >= 2
  // 兼容性,1.x和2.x的框架實現和loader實現都有很大差別
  if (!exports.compatible) {
    console.warn(
      '[HMR] You are using a version of vue-hot-reload-api that is ' +
      'only compatible with Vue.js core ^2.0.0.'
    )
    return
  }
}

/**
 * Create a record for a hot module, which keeps track of its constructor
 * and instances
 *
 * @param {String} id
 * @param {Object} options
 */

exports.createRecord = function (id, options) {
  var Ctor = null
  // 判斷傳入的options是對象仍是函數
  if (typeof options === 'function') {
    Ctor = options
    options = Ctor.options
  }
  // 燥起來,這個函數會在組件初始化和結束時的生命週期注入hook函數
  // 當實例化之後,hook函數調用會把實例記錄到map中
  // destroy後會從map中刪除實例自身
  makeOptionsHot(id, options)
  
  map[id] = {
    Ctor: Vue.extend(options),
    instances: []
  }
}

/**
 * Make a Component options object hot.
 *
 * @param {String} id
 * @param {Object} options
 */

function makeOptionsHot (id, options) {
// 注入hook函數,到達相應聲明週期後執行
  injectHook(options, initHookName, function () {
    map[id].instances.push(this)
  })
  injectHook(options, 'beforeDestroy', function () {
    var instances = map[id].instances
    instances.splice(instances.indexOf(this), 1)
  })
}

/**
 * Inject a hook to a hot reloadable component so that
 * we can keep track of it.
 *
 * @param {Object} options
 * @param {String} name
 * @param {Function} hook
 */

function injectHook (options, name, hook) {
// 判斷未注入時,生命週期init/beforeDestroy是否已經有了函數
// 不存在的話,直接把生命週期函數置爲[hook]
// 存在的話,判斷是否爲Array,從而把已存在的函數和hook鏈接起來
  var existing = options[name]
  options[name] = existing
    ? Array.isArray(existing)
      ? existing.concat(hook)
      : [existing, hook]
    : [hook]
}

// 不得不說,這個一開始確實沒搞懂是爲啥要包一層
// 本身實現的時候才知道,當有error彈出時
// 若是不手動這樣接住error,webpack會接到而後當即location.reload()
// 根原本不及看reload以前給出的提示
// 因此要手動處理下error
function tryWrap (fn) {
  return function (id, arg) {
    try { fn(id, arg) } catch (e) {
      console.error(e)
      console.warn('Something went wrong during Vue component hot-reload. Full reload required.')
    }
  }
}

exports.rerender = tryWrap(function (id, options) {
  var record = map[id]
  // 邊界處理
  // 若是沒有傳options或者已經爲空
  // 會把這個構造函數生成的全部實例強制刷新並返回
  if (!options) {
    record.instances.slice().forEach(function (instance) {
      instance.$forceUpdate()
    })
    return
  }
  // 判斷是不是構造函數仍是proto
  if (typeof options === 'function') {
    options = options.options
  }
  
  // 修改map對象中的Ctor以便記錄
  record.Ctor.options.render = options.render
  record.Ctor.options.staticRenderFns = options.staticRenderFns
  // .slice方法保證了instances的length是有效的
  record.instances.slice().forEach(function (instance) {
    // 把更新過的模塊render函數和靜態方法指到舊的實例上
    // reset static trees
    // 而後重刷新
    instance.$options.render = options.render
    instance.$options.staticRenderFns = options.staticRenderFns
    instance._staticTrees = [] // reset static trees
    instance.$forceUpdate()
  })
})

exports.reload = tryWrap(function (id, options) {
  var record = map[id]
  if (options) {
    if (typeof options === 'function') {
      options = options.options
    }
    makeOptionsHot(id, options)
    if (version[1] < 2) {
      // preserve pre 2.2 behavior for global mixin handling
      record.Ctor.extendOptions = options
    }
    
    // 其實最開始的commit中,並未繼承Ctor的父類,是直接Vue.extend(options)
    // 對vue瞭解不深,不知道爲啥改爲這樣
    // 有興趣的同窗能夠思考下
    var newCtor = record.Ctor.super.extend(options)
    record.Ctor.options = newCtor.options
    record.Ctor.cid = newCtor.cid
    record.Ctor.prototype = newCtor.prototype
    // 2.0早期版本兼容
    if (newCtor.release) {
      // temporary global mixin strategy used in < 2.0.0-alpha.6
      newCtor.release()
    }
  }
  record.instances.slice().forEach(function (instance) {
  // 判斷vNode和上下文是否存在
  // 不存在的須要手動刷新
    if (instance.$vnode && instance.$vnode.context) {
      instance.$vnode.context.$forceUpdate()
    } else {
      console.warn('Root or manually mounted instance modified. Full reload required.')
    }
  })
})

短短的100多行代碼,從這個庫支持2.x的第一個commit讀起,慢慢由簡單實現到覆蓋大部分邊界及兼容性考慮,再到vue-loader的調用,webpack的hmr各類坑和debug,這個過程很受啓發。框架

更多的前端、健身內容,請點擊 將就的博客查看ide

相關文章
相關標籤/搜索