chiyan-lin / Blog

welcome gaidy Blog
6 stars 3 forks source link

vue 的 hot-reload 与 $forceUpdate #18

Open chiyan-lin opened 3 years ago

chiyan-lin commented 3 years ago

看到一篇文章介绍 react 在浏览器上的编译以及 hot reload ,实现了一个 codebox 的小工具。

于是就想来做一个 vue 的这样的 小工具,最终实现效果 https://hello-8gy2f02b3ad207bb-1256406188.tcloudbaseapp.com/

基本思路

布局就简单处理,使用 codemirror 作为编辑器,右侧的显示使用 iframe 作为沙盒加载子页面,使用沙盒的原因就是为了防止页面之前的相互影响。这里我用的是 iframe 的一个属性 srcdoc ,可以将 html字符直接放到这个属性里面,页面就可以直接渲染,没有什么跨域的问题。

上述就是这个小工具面临的两个主要问题

vue 组件的编译

在平时实际的项目中,vue 组件都是交给 vue-loader 进行处理的,里面的编译和处理逻辑被黑盒掉了,所以这个实现其实就是来扒一扒 vue-loader 的裤子。

image

插件处理完配置,webpack 运行起来之后,vue SFC 文件会被多次传入不同的loader,经历多次中间形态变换之后才产出最终的js结果,大致上可以分为如下步骤:

  1. 路径命中 /.vue$/i 规则,调用 vue-loader 生成中间结果A
  2. 结果A命中 xx.vue?vue 规则,调用 vue-loader pitcher 生成中间结果B
  3. 结果B命中具体loader,直接调用loader做处理

在插件的行为上相对有点复杂,其实就是代码拆分,把 template ,script, style 这三块拆散 template 转换为 render 函数加塞给 vue 组件,style 交给 css 处理器处理。

本来想把完整的 vue-loader 在浏览器中实现一遍,还是有些许难的,所以这里先简单处理

例子

<template>
  <div class="red">
    data is {{ aa }} 3211呃 就是
    <input></input>
  </div>
</template>

<script>
export default {
  name: 'MyComponent',
  data() {
    return {
      aa: 1114
    }
  }
}
</script>

parseVue 和 resolveCode 用 indexOf 对几个模块进行拆解

parseVue(code, type) {
      const len = type.length + 2;
      const start = code.indexOf("<" + type + ">");
      const end = code.lastIndexOf("<\/" + type + ">");
      return start > -1 ? code.slice(start + len, end) : "";
    },
resolveCode(index, app) {
      // 获取 indexjs 的代码以及 appvue 的代码
      // 获取 template 的
      const template = this.parseVue(app, "template");
      const script = this.parseVue(app, "script");
      const style = this.parseVue(app, "style");
      return {
        wrap: index,
        code: app,
        template,
        script,
        style,
      };
}

merge 方法就是简单粗暴,直接 Vue.compile 把 template 编译了再塞回去,构造一个完整的组件

window.merge = function (script, template) {
  const App = new Function(script.replace('export default', 'return '))()
  const render = Vue.compile(template)
  App.render = render.render
  App.staticRenderFns = render.staticRenderFns
  return App
}

hot-reload 的实现

仔细思考,平时使用 hot reload ,hot 的是什么。跟抢刷新不同,hot reload 保留了 vue 组件的状态,也就是 data 在组件变化之后,其原本的数据还存在内存中。

参考了 vue-hot-reload-api ,原来就是 $forceUpdate 这个 vue 方法起作用的地方,下面一起来看看在浏览器中的 vue-hot-reload

使用全局变量来存这个 vue 的实例,createRecord 用来记录,Ctor存储组件 extend 之后的组件类;makeOptionsHot 在 beforeCreate 和 beforeDestroy 中,使用 instances 存储对应的 vue 实例,$forceUpdate 就是在这个实例上调用的;options 存储下这个组件的对象

const map = Object.create(null);

const createRecord = function (id, options) {
  console.log('createRecord', id)
  var Ctor = null
  // 判断传入的options是对象还是函数
  if (typeof options === 'function') {
    Ctor = options
    options = Ctor.options
  }
  makeOptionsHot(id, options)
  map[id] = {
    Ctor: Vue.extend(options),
    instances: [],
    options: options
  }
}

function injectHook (options, name, hook) {
  var existing = options[name]
  options[name] = existing
    ? Array.isArray(existing)
      ? existing.concat(hook)
      : [existing, hook]
    : [hook]
}

function makeOptionsHot (id, options) {
  injectHook(options, 'beforeCreate', function () {
    map[id].instances.push(this)
  })
  injectHook(options, 'beforeDestroy', function () {
    var instances = map[id].instances
    instances.splice(instances.indexOf(this), 1)
  })
}

image

这就是缓存 map 存储的东西了

然后就是两个核心方法,修改 render/template 以及修改其他的行为。修改 render/template 之后,只需要更新这个组件实例上的 render 函数,并且触发就行了,这个时候页面的状态还是存在的。修改了除了 render/template 的数据,就需要更新整个组件了。

const rerender = function (id, options) {
  const record = map[id]
  // 修改map对象中的Ctor以便记录
  record.Ctor.options.render = options.render
  record.Ctor.options.staticRenderFns = options.staticRenderFns
  // .slice方法保证了instances的length是有效的
  record.instances.forEach(function (instance) {
    instance.$options.render = options.render
    instance.$options.staticRenderFns = options.staticRenderFns
    instance._staticTrees = []
    instance.$forceUpdate()
  })
}

const reload = function (id, options) {
  var record = map[id]
  // 更新缓存的信息
  if (options) {
    makeOptionsHot(id, options)
    const newCtor = Vue.extend(options)
    record.Ctor.options = newCtor.options
    record.Ctor.cid = newCtor.cid
    record.Ctor.prototype = newCtor.prototype
  }
  // 调用 instance.$vnode.context 的实例更新整个组件
  record.instances.forEach(function (instance) {
    if (instance.$vnode && instance.$vnode.context) {
      instance.$vnode.context.$forceUpdate()
    } else {
      console.warn('Root or manually mounted instance modified. Full reload required.')
    }
  })
}

原来 $forceUpdate 是这么使用的,在平时写代码的时候根本不理解这个 api 的作用,继续看在实际中怎么调用这个

因为用了 iframe ,所以页面之间的通信就直接 postMessage 了,在主页面也做了一个缓存,存储组件各个模块的字符串信息,方便在修改了之后是否触发对应的更新。

onCodeChange(newCode) {
      console.log("code update", newCode);
      const { template, script, style } = this.resolveCode('', newCode);
      if (template !== cache.template) {
        this.updateVue('render', template)
        cache.template = template
      }
      if (script !== cache.script) {
        this.updateVue('app', { script, template })
        cache.script = script
      }
      if (style !== cache.style) {
        this.updateVue('style', style)
        cache.style = style
      }
}

在获取到对应的变更之后

  1. 对 css 的操作,直接插进 style标签的 innerHTML 中。

  2. 在 init 也就是页面进行第一次挂载的时候进行 createRecord,在执行 /src/index.js 的时候,直接 new Function 并将对应的 vue 组件对象传给他,实现了一次初始化。

  3. 仅修改了 render/template ,直接调用 rerender('App', Vue.compile(code)) 传入新的 render。

  4. 对于修改了 script 的组件,直接 reload 整个组件更新整个组件。

window.onmessage = function(event) {
  const { type, code } = event.data
  console.warn('event.data', event.data)
  if (type === 'style') {
    document.getElementById('miancss').innerHTML = code
    return
  }
  if (type === 'init') {
    const { script, template, wrap } = code
    const App = merge(script, template)
    createRecord('App', App)
    new Function('App', wrap)(App)
  }
  if (type === 'render') {
    rerender('App', Vue.compile(code))
  }
  if (type === 'app') {
    const { script, template } = code
    reload('App', merge(script, template))
  }
}