书接上文
【教学向】再加150行代码教你实现一个低配版的web component库(2) —原理篇
虽然低配版的web component篇较之前的mvvm篇没有什么人气,有点曲高和寡的赶脚,但是教程还是要继续出完的,给自己一个交代。
还是再先上一遍设计图和组件定义格式
component定义格式
<!-- myComp.html -->
<sf-component>
<style>
button{
color:red;
}
p{
color:yellow;
}
</style>
<template>
<div>
<input type="text" sf-value="this.message"/>
<button sf-innerText="this.buttonName" onclick="this.clickHandler()"></button>
<p sf-innerText="this.message">
</p>
</div>
</template>
<script>
this.message = "this is a component";
this.buttonName = "click me";
this.clickHandler = function(){
alert(this.message);
};
</script>
</sf-component>
设计图
还是先搭骨架,再填血肉
这次增加web component功能,只是在原来mvvm的基础上新增了3个类,分别是Loader,ComponentGenerator,和一个描述component定义的类型ComponentDefinition
我们看到设计图中的入口是register component,我们需要给SegmentFault类增加个registerComponent的接口
SegmentFault
export let SegmentFault = class SegmentFault {
...
private componentPool = {};
//传入自定义component的tagName,tagName可以随便起,例如my-comp
//path为定义文件的路径,例如components/myComp.html
public registerComponent(tagName,path){
this.componentPool[tagName] = path;
}
...
}
ComponentDefinition
ComponentDefinitiony用来维护component的定义文件,我们需要把每个xxxComp.html定义文件中的<template/><script/><style/>以及component的tagName维护在内存对象中
export class ComponentDefinition{
public tagName; //例如 <my-comp>中的my-comp就是tagName,tagName由registerComponent时传入。
public style; //样式,即<style/>中的字符串
public template; //DOM,即<template/>中的字符串
public script; //逻辑,即<script/>中的字符串
constructor(tagName,style,template,script){
this.tagName = tagName;
this.style = style;
this.template = template;
this.script = script;
}
}
Loader
这个类就是在sf.init的时候去加载所有定义的component定义文件,把html中的内容维护在内存对象中,并且也会同时维护一张map,来反应tagName和ComponentDefinition之间的映射关系,鉴于是通过ajax请求来load,这些*.html定义文件,所以会使用promise的返回类型。
import {ComponentDefinition} from "./ComponentDefinition";
export class Loader {
private componentPool;
private componentDefinitionPool = {};
constructor(componentPool) {
this.componentPool = componentPool;
}
//通过ajax请求,异步加载各个.html的定义文件,并维护componentDefinitionPool
public load(): Promise<any> {
}
}
ComponentGenerator
Loader加载完定义之后,就需要Generator来扫描解析整棵DOM Tree,并用template中的内容替换<my-comp>这种tag标签了,具体原理请参考《原理篇》
主要分成2步 扫描scan,和 替换生成generator!由于需要递归扫描子节点,我比较多的使用了async/await来确保同步。
export class ComponentGenerator {
private sf;
private componentDefinitionPool;
constructor(sf, componentDefinitionPool) {
this.componentDefinitionPool = componentDefinitionPool;
this.sf = sf;
}
public async scanComponent(element): Promise<any> {
}
private async generate(tagElement: HTMLElement, compDef: ComponentDefinition, attrs) {
}
}
填上骨肉 上最终实现代码
还是那句老话,建议码友可以自己独立实现逻辑,我的实现未必是最好的
//SegmentFault.ts
import {Scanner} from "./Scanner";
import {Watcher} from "./Watcher";
import {Renderer} from "./Renderer";
import {Loader} from "./component/Loader";
import {ComponentGenerator} from "./component/ComponentGenerator";
export let SegmentFault = class SegmentFault {
private viewModelPool = {};
private viewViewModelMap = {};
private renderer = new Renderer();
private generator:ComponentGenerator;
public init():Promise<any>{
return new Promise((resolve,reject)=>{
let loader = new Loader(this.componentPool);
loader.load().then( componentDefinitionPool =>{
console.log(componentDefinitionPool);
if(componentDefinitionPool){
this.generator = new ComponentGenerator(this,componentDefinitionPool);
return this.generator.scanComponent(document);
}else{
return;
}
}).then(()=>{
let scanner = new Scanner(this.viewModelPool);
let watcher = new Watcher(this);
for (let key in this.viewModelPool) {
watcher.observe(this.viewModelPool[key],this.viewModelChangedHandler);
}
this.viewViewModelMap = scanner.scanBindDOM();
Object.keys(this.viewViewModelMap).forEach(alias=>{
this.refresh(alias);
});
resolve();
});
});
};
public registerViewModel(alias:string, viewModel:object) {
viewModel["_alias"] = alias;
window[alias] = this.viewModelPool[alias] = viewModel;
};
public refresh(alias:string){
let boundItems = this.viewViewModelMap[alias];
boundItems.forEach(boundItem => {
this.renderer.render(boundItem);
});
}
private viewModelChangedHandler(viewModel,prop) {
this.refresh(viewModel._alias);
}
private componentPool = {};
public registerComponent(tagName,path){
this.componentPool[tagName] = path;
}
}
//ComponentDefinition.ts
export class ComponentDefinition{
public tagName;
public style;
public template;
public script;
constructor(tagName,style,template,script){
this.tagName = tagName;
this.style = style;
this.template = template;
this.script = script;
}
}
//Loader.ts
import {ComponentDefinition} from "./ComponentDefinition";
export class Loader {
private componentPool;
private componentDefinitionPool = {};
constructor(componentPool) {
this.componentPool = componentPool;
}
public load(): Promise<any> {
let compArray = [];
for (var tagName in this.componentPool) {
compArray.push({ name: tagName, path: this.componentPool[tagName] });
}
return this.doAsyncSeries(compArray).then(result=>{
return result;
});
}
private doAsyncSeries(componentArray): Promise<any> {
return componentArray.reduce( (promise, comp) =>{
return promise.then( (result) => {
return fetch(comp.path).then((response) => {
return response.text().then(definition => {
let def = this.getComponentDefinition(comp.name,definition);
this.componentDefinitionPool[comp.name] = def;
return this.componentDefinitionPool;
});
});
});
}, new Promise<void>((resolve, reject) => {
resolve();
}));
}
private getComponentDefinition(tagName:string, htmlString: string): ComponentDefinition {
let tempDom: HTMLElement = document.createElement("div");
tempDom.innerHTML = htmlString;
let template: string = tempDom.querySelectorAll("template")[0] && tempDom.querySelectorAll("template")[0].innerHTML;
let script: string = tempDom.querySelectorAll("script")[0] && tempDom.querySelectorAll("script")[0].innerHTML;
let style: string = tempDom.querySelectorAll( "style")[0] && tempDom.querySelectorAll( "style")[0].innerHTML;
return new ComponentDefinition(tagName,style,template,script);
}
}
//ComponentGenerator.ts
import { ComponentDefinition } from "./ComponentDefinition";
export class ComponentGenerator {
private sf;
private componentDefinitionPool;
constructor(sf, componentDefinitionPool) {
this.componentDefinitionPool = componentDefinitionPool;
this.sf = sf;
}
public async scanComponent(element): Promise<any> {
let compDef: ComponentDefinition = this.componentDefinitionPool[element.localName];
if (compDef) {
let attrs = element.attributes;
return this.generate(element, compDef, attrs);
} else {
if (element.children && element.children.length > 0) {
for (let i = 0; i < element.children.length; i++) {
let child = element.children[i];
await this.scanComponent(child);
}
return element;
} else {
return element;
}
}
}
private async generate(tagElement: HTMLElement, compDef: ComponentDefinition, attrs) {
if(compDef.style){
this.dealWithShadowStyle(compDef);
}
let randomAlias = 'vm_' + Math.floor(10000 * Math.random()).toString();
let template = compDef.template;
template = template.replace(new RegExp('this', 'gm'), randomAlias);
let tempFragment = document.createElement('div');
tempFragment.insertAdjacentHTML('afterBegin' as InsertPosition, template);
if (tempFragment.children.length > 1) {
template = tempFragment.outerHTML;
}
tagElement.insertAdjacentHTML('beforeBegin' as InsertPosition, template);
let htmlDom = tagElement.previousElementSibling;
htmlDom.classList.add(tagElement.localName);
let vm_instance;
if (compDef.script) {
let debugComment = "//# sourceURL="+tagElement.tagName+".js";
let script = compDef.script + debugComment;
let ViewModelClass: Function = new Function(script);
vm_instance = new ViewModelClass.prototype.constructor();
this.sf.registerViewModel(randomAlias, vm_instance);
vm_instance._dom = htmlDom;
vm_instance.dispatchEvent = (eventType: string, data: any, bubbles: boolean = false, cancelable: boolean = true) => {
let event = new CustomEvent(eventType.toLowerCase(), { "bubbles": bubbles, "cancelable": cancelable });
event['data'] = data;
vm_instance._dom.dispatchEvent(event);
};
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if(attr.nodeName.search("sf-") !== -1){
if(attr.nodeName.search("sf-on") !== -1){
let eventName = attr.nodeName.substr(5);
vm_instance._dom.addEventListener(eventName,eval(attr.nodeValue));
}else{
let setTarget = attr.nodeName.split("-")[1];
vm_instance[setTarget] = eval(attr.nodeValue);
}
}
}
}
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if(attr.nodeName.search("sf-") === -1){
htmlDom.setAttribute(attr.nodeName, attr.nodeValue);
}
}
tagElement.parentNode.removeChild(tagElement);
if (htmlDom.children && htmlDom.children.length > 0) {
for (let j = 0; j < htmlDom.children.length; j++) {
let child = htmlDom.children[j];
await this.scanComponent(child);
}
callInit();
return htmlDom;
} else {
callInit();
return htmlDom;
}
function callInit(){
if (vm_instance && vm_instance._init && typeof (vm_instance._init) === 'function') {
vm_instance._init();
}
}
}
private dealWithShadowStyle(compDef:ComponentDefinition): void {
let stylesheet = compDef.style;
let tagName = compDef.tagName;
let head = document.getElementsByTagName('HEAD')[0];
let style = document.createElement('style');
style.type = 'text/css';
var styleArray = stylesheet.split("}");
var newArray = [];
styleArray.forEach((value: string, index: number) => {
var newValue = value.replace(/^\s*/, "");
if (newValue) {
newArray.push(newValue);
}
});
stylesheet = newArray.join("}\n" + "." + tagName + " ");
stylesheet = "." + tagName + " " + stylesheet + "}";
style.innerHTML = stylesheet;
head.appendChild(style);
}
}
最后放上github地址,所有代码都在上面哟!
https://github.com/momoko8443...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。