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