書接上文
【教學向】再加150行代碼教你實現一個低配版的web component庫(2) —原理篇css
雖然低配版的web component篇較以前的mvvm篇沒有什麼人氣,有點曲高和寡的趕腳,可是教程仍是要繼續出完的,給本身一個交代。html
仍是再先上一遍設計圖和組件定義格式node
<!-- 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定義的類型ComponentDefinitiongit
咱們看到設計圖中的入口是register component,咱們須要給SegmentFault類增長個registerComponent的接口github
export let SegmentFault = class SegmentFault { ... private componentPool = {}; //傳入自定義component的tagName,tagName能夠隨便起,例如my-comp //path爲定義文件的路徑,例如components/myComp.html public registerComponent(tagName,path){ this.componentPool[tagName] = path; } ... }
ComponentDefinitiony用來維護component的定義文件,咱們須要把每一個xxxComp.html定義文件中的<template/><script/><style/>以及component的tagName維護在內存對象中web
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; } }
這個類就是在sf.init的時候去加載全部定義的component定義文件,把html中的內容維護在內存對象中,而且也會同時維護一張map,來反應tagName和ComponentDefinition之間的映射關係,鑑因而經過ajax請求來load,這些*.html定義文件,因此會使用promise的返回類型。ajax
import {ComponentDefinition} from "./ComponentDefinition"; export class Loader { private componentPool; private componentDefinitionPool = {}; constructor(componentPool) { this.componentPool = componentPool; } //經過ajax請求,異步加載各個.html的定義文件,並維護componentDefinitionPool public load(): Promise<any> { } }
Loader加載完定義以後,就須要Generator來掃描解析整棵DOM Tree,並用template中的內容替換<my-comp>這種tag標籤了,具體原理請參考《原理篇》
主要分紅2步 掃描scan,和 替換生成generator!因爲須要遞歸掃描子節點,我比較多的使用了async/await來確保同步。segmentfault
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) { } }
仍是那句老話,建議碼友能夠本身獨立實現邏輯,個人實現未必是最好的promise
//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...app