Vue 脚手架热更新技术探秘
前言
热替换 (Hot Module Replacement) 或热重载 (Hot Reload) 是指在不停机状态下,实时更新,在前端利于来说,在各大框架中及库中都有体现,比如 NG 从 5 开始就提供了热更新,RN 也有对应的热更新技术,其实客户端技术很早就已经有这方面的探索,本文主要针对 Vue 脚手架的热更新,其实主要是 Vue-hot-reload-api 这个包的应用,对 webpack 的 HMR 比较感兴趣的同学推荐冉四夕大佬的这篇文章 Webpack HMR 原理解析,多说一句,个人认为 Webpack 可能是最好的 node.js 的工具库应用。
目录
- vue-cli 脚手架结构
- vue-hot-reload-api 源码分析
- vue-cli 热更新 vs webpack 热更新
探索案例
vue-cli 脚手架结构
[目录结构]
-
bin
- vue
- vue-build
- vue-init
- vue-list
-
lib
- ask.js (自定义工具 - 用于询问开发者)
- check-version.js
- eval.js
- filter.js (自定义工具 - 用于文件过滤)
- generate.js
- git-user.js
- local-path.js
- logger.js (自定义工具 - 用于日志打印)
- options.js (自定义工具 - 用于获取模板配置)
- warnings.js
[目录描述] vue-cli2 的目录结构就是 bin 下的相关模块,vue-cli 最新版将各个模块又单独抽成了独立的文件,并引入了插件机 pwa 等相关周边的工具引入,使得脚手架更加丰富(见下图),但主要构建流程并未改变,主要就是在 bin 目录下去配置相关的命令,主要用到了 commander 处理命令行的包,对于想独立开发个人脚手架的同学可以参考这两篇文章教你从零开始搭建一款前端脚手架工具,走进 Vue-cli 源码,自己动手搭建前端脚手架工具
vue-hot-reload-api 源码分析
let Vue // late bind
let version
const map = Object.create(null)
if (typeof window !== 'undefined') {window.__VUE_HOT_MAP__ = map}
let installed = false
let isBrowserify = false
let initHookName = 'beforeCreate'
exports.install = (vue, browserify) => {if (installed) return
installed = true
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
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 = (id, options) => {if(map[id]) return
let Ctor = null
if (typeof options === 'function') {
Ctor = options
options = Ctor.options
}
makeOptionsHot(id, options)
map[id] = {
Ctor,
options,
instances: []}
}
/**
* Check if module is recorded
*
* @param {String} id
*/
exports.isRecorded = (id) => {return typeof map[id] !== 'undefined'
}
/**
* Make a Component options object hot.
*
* @param {String} id
* @param {Object} options
*/
function makeOptionsHot(id, options) {if (options.functional) {
const render = options.render
options.render = (h, ctx) => {const instances = map[id].instances
if (ctx && instances.indexOf(ctx.parent) < 0) {instances.push(ctx.parent)
}
return render(h, ctx)
}
} else {injectHook(options, initHookName, function() {const record = map[id]
if (!record.Ctor) {record.Ctor = this.constructor}
record.instances.push(this)
})
injectHook(options, 'beforeDestroy', function() {const 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) {const existing = options[name]
options[name] = existing
? Array.isArray(existing) ? existing.concat(hook) : [existing, hook]
: [hook]
}
function tryWrap(fn) {return (id, arg) => {
try {fn(id, arg)
} catch (e) {console.error(e)
console.warn('Something went wrong during Vue component hot-reload. Full reload required.')
}
}
}
function updateOptions (oldOptions, newOptions) {for (const key in oldOptions) {if (!(key in newOptions)) {delete oldOptions[key]
}
}
for (const key in newOptions) {oldOptions[key] = newOptions[key]
}
}
exports.rerender = tryWrap((id, options) => {const record = map[id]
if (!options) {record.instances.slice().forEach(instance => {instance.$forceUpdate()
})
return
}
if (typeof options === 'function') {options = options.options}
if (record.Ctor) {
record.Ctor.options.render = options.render
record.Ctor.options.staticRenderFns = options.staticRenderFns
record.instances.slice().forEach(instance => {
instance.$options.render = options.render
instance.$options.staticRenderFns = options.staticRenderFns
// reset static trees
// pre 2.5, all static trees are cached together on the instance
if (instance._staticTrees) {instance._staticTrees = []
}
// 2.5.0
if (Array.isArray(record.Ctor.options.cached)) {record.Ctor.options.cached = []
}
// 2.5.3
if (Array.isArray(instance.$options.cached)) {instance.$options.cached = []
}
// post 2.5.4: v-once trees are cached on instance._staticTrees.
// Pure static trees are cached on the staticRenderFns array
// (both already reset above)
// 2.6: temporarily mark rendered scoped slots as unstable so that
// child components can be forced to update
const restore = patchScopedSlots(instance)
instance.$forceUpdate()
instance.$nextTick(restore)
})
} else {
// functional or no instance created yet
record.options.render = options.render
record.options.staticRenderFns = options.staticRenderFns
// handle functional component re-render
if (record.options.functional) {
// rerender with full options
if (Object.keys(options).length > 2) {updateOptions(record.options, options)
} else {
// template-only rerender.
// need to inject the style injection code for CSS modules
// to work properly.
const injectStyles = record.options._injectStyles
if (injectStyles) {
const render = options.render
record.options.render = (h, ctx) => {injectStyles.call(ctx)
return render(h, ctx)
}
}
}
record.options._Ctor = null
// 2.5.3
if (Array.isArray(record.options.cached)) {record.options.cached = []
}
record.instances.slice().forEach(instance => {instance.$forceUpdate()
})
}
}
})
exports.reload = tryWrap((id, options) => {const record = map[id]
if (options) {if (typeof options === 'function') {options = options.options}
makeOptionsHot(id, options)
if (record.Ctor) {if (version[1] < 2) {
// preserve pre 2.2 behavior for global mixin handling
record.Ctor.extendOptions = options
}
const newCtor = record.Ctor.super.extend(options)
// prevent record.options._Ctor from being overwritten accidentally
newCtor.options._Ctor = record.options._Ctor
record.Ctor.options = newCtor.options
record.Ctor.cid = newCtor.cid
record.Ctor.prototype = newCtor.prototype
if (newCtor.release) {
// temporary global mixin strategy used in < 2.0.0-alpha.6
newCtor.release()}
} else {updateOptions(record.options, options)
}
}
record.instances.slice().forEach(instance => {if (instance.$vnode && instance.$vnode.context) {instance.$vnode.context.$forceUpdate()
} else {
console.warn('Root or manually mounted instance modified. Full reload required.')
}
})
})
// 2.6 optimizes template-compiled scoped slots and skips updates if child
// only uses scoped slots. We need to patch the scoped slots resolving helper
// to temporarily mark all scoped slots as unstable in order to force child
// updates.
function patchScopedSlots (instance) {if (!instance._u) return
// https://github.com/vuejs/vue/blob/dev/src/core/instance/render-helpers/resolve-scoped-slots.js
const original = instance._u
instance._u = slots => {
try {
// 2.6.4 ~ 2.6.6
return original(slots, true)
} catch (e) {
// 2.5 / >= 2.6.7
return original(slots, null, true)
}
}
return () => {instance._u = original}
}
整体来说 vue-hot-reload-api 的思路还是很清晰的,主要就是通过维护一个 map 映射对象,通过对 component 名称进行对比,这里主要维护了一个 Ctor 对象,通过 hook 的方法在 vue 的生命周期中进行 watch 监听,然后更新后进行 rerender 以及 reload
vue-cli 热更新 vs webpack 热更新
vue-cli 热重载和 webpack 热更新不同的区别主要在于:
1、依赖:vue-cli 热重载是强依赖于 vue 框架的,利用的是 vue 自身的 Watcher 监听,通过 vue 的生命周期函数进行名称模块的变更的替换;而 webpack 则是不依赖于框架,利用的是 sock.js 进行浏览器端和本地服务端的通信,本地的 watch 监听则是 webpack 和 webpack-dev-server 对模块名称的监听,替换过程用的则是 jsonp/ajax 的传递;
2、粒度:vue-cli 热重载主要是利用的 vue 自身框架的 component 粒度的更新,虽然 vue-cli 也用到了 webpack,其主要是打包和本地服务的用途;而 webpack 的热更新则是模块粒度的,其主要是模块名称的变化定位去更新,由于其自身是一个工具应用,因而不能确定是哪个框架具体的生命周期,因而其监听内容变化就必须通过自身去实现一套类似的周期变化监听;
3、定位:vue-cli 定位就是 vue 框架的命令行工具,因而其并不需要特别大的考虑到双方通信以及自定义扩展性等;而 webpack 本身定位在一个打包工具,或者说其实基于 node.js 运行时环境的应用,因而也就决定了它必须有更方便、更个性化的扩展和抽象性
总结
在简单小型项目中,直接使用 vue-cli 脚手架进行 vue 相关应用的开发即可,但在开发过程中遇到了相关不太明白的渲染问题,也需要弄懂弄通其深层原理(ps: 这次就是基于一个生命周期渲染的问题引发的探究,大概描述下就是页面渲染在 f5 刷新和 vue-cli 热重载下会出现不同的数据形式,然后便研究了下 vue-cli 的源码);而对于大型定制化项目,或者说需要对前端项目组提供一整套的前端工程化工具模板的开发,webpack 还是首当其冲的选择,毕竟 webpack 还是在 node.js 运行时下有压倒性优势的工具应用。
参考
- vue-cli 官网
- Vue-hot-reload-api 源码解析
- 走进 Vue-cli 源码,自己动手搭建前端脚手架工具
- Webpack HMR 原理解析