Open chiyan-lin opened 3 years ago
看到一篇文章介绍 react 在浏览器上的编译以及 hot reload ,实现了一个 codebox 的小工具。
于是就想来做一个 vue 的这样的 小工具,最终实现效果 https://hello-8gy2f02b3ad207bb-1256406188.tcloudbaseapp.com/
布局就简单处理,使用 codemirror 作为编辑器,右侧的显示使用 iframe 作为沙盒加载子页面,使用沙盒的原因就是为了防止页面之前的相互影响。这里我用的是 iframe 的一个属性 srcdoc ,可以将 html字符直接放到这个属性里面,页面就可以直接渲染,没有什么跨域的问题。
上述就是这个小工具面临的两个主要问题
在平时实际的项目中,vue 组件都是交给 vue-loader 进行处理的,里面的编译和处理逻辑被黑盒掉了,所以这个实现其实就是来扒一扒 vue-loader 的裤子。
插件处理完配置,webpack 运行起来之后,vue SFC 文件会被多次传入不同的loader,经历多次中间形态变换之后才产出最终的js结果,大致上可以分为如下步骤:
在插件的行为上相对有点复杂,其实就是代码拆分,把 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 的是什么。跟抢刷新不同,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) }) }
这就是缓存 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 } }
在获取到对应的变更之后
对 css 的操作,直接插进 style标签的 innerHTML 中。
在 init 也就是页面进行第一次挂载的时候进行 createRecord,在执行 /src/index.js 的时候,直接 new Function 并将对应的 vue 组件对象传给他,实现了一次初始化。
仅修改了 render/template ,直接调用 rerender('App', Vue.compile(code)) 传入新的 render。
对于修改了 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)) } }
看到一篇文章介绍 react 在浏览器上的编译以及 hot reload ,实现了一个 codebox 的小工具。
于是就想来做一个 vue 的这样的 小工具,最终实现效果 https://hello-8gy2f02b3ad207bb-1256406188.tcloudbaseapp.com/
基本思路
布局就简单处理,使用 codemirror 作为编辑器,右侧的显示使用 iframe 作为沙盒加载子页面,使用沙盒的原因就是为了防止页面之前的相互影响。这里我用的是 iframe 的一个属性 srcdoc ,可以将 html字符直接放到这个属性里面,页面就可以直接渲染,没有什么跨域的问题。
上述就是这个小工具面临的两个主要问题
vue 组件的编译
在平时实际的项目中,vue 组件都是交给 vue-loader 进行处理的,里面的编译和处理逻辑被黑盒掉了,所以这个实现其实就是来扒一扒 vue-loader 的裤子。
插件处理完配置,webpack 运行起来之后,vue SFC 文件会被多次传入不同的loader,经历多次中间形态变换之后才产出最终的js结果,大致上可以分为如下步骤:
在插件的行为上相对有点复杂,其实就是代码拆分,把 template ,script, style 这三块拆散 template 转换为 render 函数加塞给 vue 组件,style 交给 css 处理器处理。
本来想把完整的 vue-loader 在浏览器中实现一遍,还是有些许难的,所以这里先简单处理
例子
parseVue 和 resolveCode 用 indexOf 对几个模块进行拆解
merge 方法就是简单粗暴,直接 Vue.compile 把 template 编译了再塞回去,构造一个完整的组件
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 存储下这个组件的对象
这就是缓存 map 存储的东西了
然后就是两个核心方法,修改 render/template 以及修改其他的行为。修改 render/template 之后,只需要更新这个组件实例上的 render 函数,并且触发就行了,这个时候页面的状态还是存在的。修改了除了 render/template 的数据,就需要更新整个组件了。
原来 $forceUpdate 是这么使用的,在平时写代码的时候根本不理解这个 api 的作用,继续看在实际中怎么调用这个
因为用了 iframe ,所以页面之间的通信就直接 postMessage 了,在主页面也做了一个缓存,存储组件各个模块的字符串信息,方便在修改了之后是否触发对应的更新。
在获取到对应的变更之后
对 css 的操作,直接插进 style标签的 innerHTML 中。
在 init 也就是页面进行第一次挂载的时候进行 createRecord,在执行 /src/index.js 的时候,直接 new Function 并将对应的 vue 组件对象传给他,实现了一次初始化。
仅修改了 render/template ,直接调用 rerender('App', Vue.compile(code)) 传入新的 render。
对于修改了 script 的组件,直接 reload 整个组件更新整个组件。