san-hot-loader
Version:
San hmr loader
195 lines (157 loc) • 5.46 kB
JavaScript
/**
* Copyright (c) Baidu Inc. All rights reserved.
*
* This source code is licensed under the MIT license.
* See LICENSE file in the project root for license information.
*
* @file component-client-api.js
* @author clark-t
*/
var utils = require('./utils');
var San;
var map = {};
var compatible;
global.__SAN_HOT_MAP__ = map;
function install(san) {
if (compatible == null) {
San = utils.esm(san);
var versions = San.version.split('.');
compatible = +versions[0] > 3 || +versions[1] > 8 || +versions[2] > 0;
}
if (!compatible) {
throw new Error(
'[HMR] You are using a version of san-hot-loader that is '
+ 'only compatible with san.js ^3.8.1'
);
}
}
function createRecord(id, ComponentClass) {
var desc = makeComponentHot(id, ComponentClass);
map[id] = {
Ctor: desc.Ctor,
proto: desc.proto,
instances: []
};
}
var SAN_HOOK_ORIGIN = '__SAN_HOOK_ORIGIN__';
function injectHook(options, name, callback) {
var existing = options[name];
var className = options.constructor && options.constructor.name;
var hookName = className ? SAN_HOOK_ORIGIN + '.' + className : SAN_HOOK_ORIGIN;
// 防止多次热更新之后出现套娃
if (existing && existing[hookName]) {
return;
}
options[name] = existing
? function () {
existing.call(this);
callback.call(this);
}
: callback;
options[name][hookName] = true;
}
function makeComponentHot(id, ComponentClass) {
ComponentClass = utils.esm(ComponentClass);
var isClassComp = typeof ComponentClass === 'function';
if (!isClassComp && typeof ComponentClass !== 'object') {
throw new Error(
'[HMR] Unrecognized component type'
);
}
var hookTarget = isClassComp ? ComponentClass.prototype : ComponentClass;
injectHook(hookTarget, 'attached', function () {
map[id].instances.push(this);
});
injectHook(hookTarget, 'detached', function () {
var instances = map[id].instances;
instances.splice(instances.indexOf(this), 1);
});
var Ctor = isClassComp ? ComponentClass : San.defineComponent(ComponentClass);
return {
Ctor,
proto: Ctor.prototype
};
}
function hotReload(id, ComponentClass) {
var newDesc = makeComponentHot(id, ComponentClass);
var recDesc = map[id];
var recANode;
var recCmptReady;
var newANode;
var newCmptReady;
// 热更新新旧组件构造函数相同的例子如下:
// import template from './template.html';
// import ComponentClass from './app';
// ComponentClass.template = template;
// export default ComponentClass;
//
// 在单纯修改 template 的时候,热更新时新旧组件指向都是 ComponentClass 的地址,在这种情况下,需要采用特殊手段将 ComponentClass 的 template 和对应的 aNode 更新掉
if (!isProtoChange(newDesc, recDesc)) {
recANode = recDesc.proto.aNode;
recCmptReady = recDesc.proto._cmptReady;
}
recDesc.Ctor = newDesc.Ctor;
recDesc.proto = newDesc.proto;
recDesc.instances.slice().forEach(function (instance) {
var parentEl = instance.el.parentElement;
var beforeEl = instance.el.nextElementSibling;
var data = instance.data.get();
var options = {
subTag: instance.subTag,
owner: instance.owner,
scope: instance.scope,
parent: instance.parent,
source: instance.source,
data: data
};
var newInstance;
if (recANode != null) {
recDesc.proto.aNode = recANode;
recDesc.proto._cmptReady = recCmptReady;
instance.dispose();
if (newANode == null) {
delete newDesc.proto.aNode;
delete newDesc.proto._cmptReady;
}
else {
newDesc.proto.aNode = newANode;
newDesc.proto._cmptReady = newCmptReady;
}
newInstance = new newDesc.Ctor(options);
newInstance.attach(parentEl, beforeEl);
if (newANode == null) {
newANode = newDesc.proto.aNode;
newCmptReady = newDesc.proto._cmptReady;
}
}
else {
instance.dispose();
newInstance = new newDesc.Ctor(options);
newInstance.attach(parentEl, beforeEl);
// 将父节点当中缓存的子组件实例给手动替换掉
// 不然父组件往子组件里绑定的数据就不再变化了
if (instance.parent) {
instance.parentComponent.constructor.prototype.components[instance.subTag] = newDesc.Ctor;
var parent = instance.parent;
parent.children.splice(parent.children.indexOf(instance), 1, newInstance);
}
}
});
}
function isProtoChange(newDesc, recDesc) {
if (recDesc.Ctor === newDesc.Ctor) {
return false;
}
var recProto = recDesc.proto;
var newProto = newDesc.proto;
if (recProto.constructor === newProto.constructor && recProto.constructor != null) {
return false;
}
return true;
}
module.exports = {
install: install,
createRecord: createRecord,
hotReload: hotReload,
compatible: compatible
};