9

书接上文
【教学向】再加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个类,分别是LoaderComponentGenerator,和一个描述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...


熊丸子
5.6k 声望293 粉丝

现在sf的文章质量堪忧~~~