资讯专栏INFORMATION COLUMN

Vue-hot-reload-api 源码解析

DobbyKim / 980人阅读

摘要:源码解析起因最近在搞框架的热加载方案,自然是少不了向成熟的框架学习偷窥。这将销毁并重建整个组件包括子组件。通过使用说明可以看出,暴露的接口还是很清晰的,下面来看下具体源码实现。

Vue-hot-reload-api 源码解析 起因

最近在搞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 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,这个过程很受启发。

更多的前端、健身内容,请点击 将就的博客查看

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87350.html

相关文章

  • vue-loader 源码解析系列之 整体分析

    摘要:笔者系贡献者之一官方说明简单来说就是将文件变成,然后放入浏览器运行。部分首先分析部分从做右到左,也就是被先后被和处理过了。源码解析之二源码解析之三写作中源码解析之四写作中作者博客作者微博 笔者系 vue-loader 贡献者(#16)之一 官方说明 vue-loader is a loader for Webpack that can transform Vue components ...

    icattlecoder 评论0 收藏0
  • WebPack2配置我的Vue开发环境

    摘要:包中导出的默认是运行时构建。当然,我们期待的是只修改代码,不用重新运行命令,甚至不需要刷新浏览器即看到代码的改动效果,这时候需要新的插件来配置实现的热重载。 首先已经全局安装了node/vue/webpack; 新建文件夹demo4并初始化 cd demo4 npm init -y 这是页面会生成一个package.json文件。 安装webpack及相关插件 npm install ...

    Sike 评论0 收藏0
  • webpack配置信息说明

    摘要:参考令人困惑的地方项目名称项目名称版本描述作者开源协议主文件指定了运行脚本命令的命令行缩写,比如这是的指定了运行时,所要执行的命令。要解析并且完成相应的功能,这些基本都是必须的。 参考 Webpack——令人困惑的地方 package.json { name: 项目名称, //项目名称 version: 1.0.0, //版本 description: vue+...

    wind3110991 评论0 收藏0
  • 小白上学のwebpack+vue初级使用指南

    摘要:但我们今天学的是,原因我还不会而且新手还是学习为主吧。原因中文文档全,学习曲线简单,很容易上手。后续总结在学习打包工具过程中由于出现的问题各种蛋疼,让很多人都半途而废。大家互相学习共同进步本节讲的都是很基础的东西,自己可以延展一下。 写这篇文章的时候先说一下原因:webpack:现在很流行的打包工具;推荐原因:学习成本比gulp,fis3等简单,就是这么直接!vue:国人开发的MVVM...

    PascalXie 评论0 收藏0
  • 单文件组件下的vue,可以擦出怎样的火花

    摘要:线上另加入了排行榜功能,如需查看源码的,请切换到分支整个项目结构清晰,尤其单文件组件的表现力尤为突出,使得每个组件的逻辑都没有过于复杂,而且在的统筹下,的单向数据流模式使得所有的变化都在可控制可预期的范围内。 2016注定不是个平凡年,无论是中秋节问世的angular2,还是全面走向稳定的React,都免不了面对另一个竞争对手vue2。喜欢vue在设计思路上的先进性(原谅我用了这么一个...

    Keven 评论0 收藏0

发表评论

0条评论

DobbyKim

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<