Open jiangjiu opened 7 years ago
最近在搞san框架的热加载方案,自然是少不了向成熟的框架学习(偷窥ing)。热加载方案基本也只是主流框架在做,且做的比较成熟,大部分应用开发者并不会接触到这部分东西,所以相应的资料比较少。google了一下这个库,发现木有人做相应的解析,顺手记录下好了。
众所周知,*.vue文件为广大开发者提供了良好的开发体验,vue-loader的原理不多赘述,在vue的脚手架中,webpack通过vue-loader来解析*.vue文件,把template、js和style文件分离并让相应的loader去处理。
*.vue
在这个过程中,vue-loader还会做些其他事情,比如向client端注入hot-reload相应的代码,构建时编译等等。
webpack的hmr原理也不多说了,vue的热加载就是通过注入的代码来实现组件的热更新,下面来看下使用时的文档和源码。
先来看下官方文档。
你仅会在开发一个基于 Vue components 构建工具的时候用到这个。对于普通的应用,使用 vue-loader 或者 vueify 就可以了。
文档中明确说明了,一般使用不需要用到这个,只有在开发相应的构建工具时才会用到。
// 定义一个组件作为选项对象 // 在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暴露的接口还是很清晰的,下面来看下具体源码实现。
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,这个过程很受启发。
起因
最近在搞san框架的热加载方案,自然是少不了向成熟的框架学习(偷窥ing)。热加载方案基本也只是主流框架在做,且做的比较成熟,大部分应用开发者并不会接触到这部分东西,所以相应的资料比较少。google了一下这个库,发现木有人做相应的解析,顺手记录下好了。
什么是Vue-hot-reload-api?
众所周知,
*.vue
文件为广大开发者提供了良好的开发体验,vue-loader的原理不多赘述,在vue的脚手架中,webpack通过vue-loader来解析*.vue
文件,把template、js和style文件分离并让相应的loader去处理。在这个过程中,vue-loader还会做些其他事情,比如向client端注入hot-reload相应的代码,构建时编译等等。
webpack的hmr原理也不多说了,vue的热加载就是通过注入的代码来实现组件的热更新,下面来看下使用时的文档和源码。
用法
先来看下官方文档。
文档中明确说明了,一般使用不需要用到这个,只有在开发相应的构建工具时才会用到。
通过使用说明可以看出,vue-hot-reload-api暴露的接口还是很清晰的,下面来看下具体源码实现。
短短的100多行代码,从这个库支持2.x的第一个commit读起,慢慢由简单实现到覆盖大部分边界及兼容性考虑,再到vue-loader的调用,webpack的hmr各种坑和debug,这个过程很受启发。