回顾

上篇文章大概展示了kmdjs0.1.x时期的编程范式:
如下面所示,可以直接依赖注入到function里,

kmdjs.define('main',['util.bom','app.Ball','util.dom.test'], function(bom,Ball,test) {
    var ball = new Ball(0, 0, 28, 1, -2, 'kmdjs');
    var vp = bom.getViewport();
});

也可以直接在代码里把full namespace加上来调用,如:

kmdjs.define('main',['util.bom','app.Ball','util.dom.test'], function() {
    var ball = new app.Ball(0, 0, 28, 1, -2, 'kmdjs');
    var vp = util.bom.getViewport();
});

而且,在循环依赖的场景,因为执行顺序的问题,会导致第一种方式注入undefined,所以循环依赖的情况下只能用full namespace的方式来调用。

这种编程体验虽然已经足够好,但是可以更好。怎样才算更好?

  1. 不用依赖注入function

  2. 不用写full namespace,自动匹配依赖

如下所示:

kmdjs.define('main',['util.bom','app.Ball','util.dom.test'], function() {
    var ball = new Ball(0, 0, 28, 1, -2, 'kmdjs');
    var vp = bom.getViewport();
});

这就要借助uglifyjs能力,把function的字符串替换成带有namespace就可以实现上面的效果。

uglifyjs依赖分析和代码重构

function fixDeps(fn,deps) {
    var U2 = UglifyJS;
    //uglify2不支持匿名转ast
    var code = fn.toString().replace('function','function ___kmdjs_temp');
    var ast = U2.parse(code);
    ast.figure_out_scope();
    var nodes = [];


    ast.walk(new U2.TreeWalker(function (node) {

        if (node instanceof U2.AST_New) {
            var ex = node.expression;
            var name = ex.name;
            isInWindow(name) || isInArray(nodes, node) || isInScopeChainVariables(ex.scope, name) || nodes.push({name:name,node:node});
        }

        if (node instanceof U2.AST_Dot) {
            var ex = node.expression;
            var name = ex.name;
            var scope = ex.scope;
            if (scope) {
                isInWindow(name) || isInArray(nodes, node) || isInScopeChainVariables(ex.scope, name) || nodes.push({name:name,node:node});
            }
        }

        if (node instanceof U2.AST_SymbolRef) {
            var name = node.name;
            isInWindow(name) || isInArray(nodes, node) || isInScopeChainVariables(node.scope, name) || nodes.push({name:name,node:node});
        }
    }));

    var cloneNodes = [].concat(nodes);
    //过滤new nodes 中的symbo nodes
    for (var i = 0, len = nodes.length; i < len; i++) {
        var nodeA = nodes[i].node;
        for (var j = 0, cLen = cloneNodes.length; j < cLen; j++) {
            var nodeB = cloneNodes[j].node;
            if (nodeB.expression === nodeA) {
                nodes.splice(i, 1);
                i--;
                len--;
            }
        }
    }

    for (var i = nodes.length; --i >= 0;) {
        var item = nodes[i],
            node=item.node,
            name=item.name;
        var fullName=getFullName(deps,name);
        var replacement;
        if (node instanceof  U2.AST_New) {
            replacement = new U2.AST_New({
                expression: new U2.AST_SymbolRef({
                    name:fullName
                }),
                args: node.args
            });
        } else if (node instanceof  U2.AST_Dot) {
            replacement = new U2.AST_Dot({
                expression: new U2.AST_SymbolRef({
                    name: fullName
                }),
                property: node.property
            });
        }else if(node instanceof U2.AST_SymbolRef){
            replacement = new U2.AST_SymbolRef({
                    name: fullName
            });
        }

        var start_pos = node.start.pos;
        var end_pos = node.end.endpos;

        code = splice_string(code, start_pos, end_pos, replacement.print_to_string({
            beautify: true
        }));
    }
    return code.replace('function ___kmdjs_temp','function');
}

function getFullName(deps,name){
    var i= 0,
        len=deps.length,
            matchCount= 0,
            result=[];

    for(;i<len;i++) {
        var fullName = deps[i];
        if (fullName.split('.').pop() === name) {
            matchCount++;
            if (!isInArray(result, fullName))  result.push(fullName);
        }
    }

    if(matchCount>1){
        throw "the same name conflict: "+result.join(" and ");
    } else if(matchCount===1){
        return result[0];
    }else{
        throw ' can not find module ['+name+']';
    }
}

function splice_string(str, begin, end, replacement) {
    return str.substr(0, begin) + replacement + str.substr(end);
}

function isInScopeChainVariables(scope, name) {
    var vars = scope.variables._values;
    if (Object.prototype.hasOwnProperty.call(vars, "$" + name)) {
        return true;
    }

    if (scope.parent_scope) {
        return isInScopeChainVariables(scope.parent_scope, name);
    }

    return false;
}

function isInArray(arr,name){
    var i= 0,len=arr.length;
    for(;i<len;i++){
        if(arr[i]===name){
            return true;
        }
    }
    return false;
}

function isInWindow(name){
    if(name==='this')return true;
    return name in window;
}

通过上面的fixDeps,可以对代码就行变换。如:

 console.log(fixDeps(function (A) {
        var eee = m;
        var b = new A();
        var b = new B();
        var c = new C();
        var d = G.a;
    },['c.B','AAA.G','SFSF.C','AAAA.m'] ))

输出:

function (A) {
        var eee = AAAA.m;
        var b = new A();
        var b = new c.B();
        var c = new SFSF.C();
        var d = AAA.G.a;
}

这样,kmdjs在执行模块function的时候,只需要fixDeps加上full namespace就行:

function buildBundler(){
    var topNsStr = "";
    each(kmdjs.factories, function (item) {
        nsToCode(item[0]);
    });
    topNsStr+=  kmdjs.nsList.join('\n') +"\n\n";
    each(kmdjs.factories, function (item) {
        topNsStr+=item[0]+' = ('+ fixDeps(item[2],item[1])+')();\n\n' ;
    });
    if(kmdjs.buildEnd) kmdjs.buildEnd(topNsStr);
    return topNsStr;
}

build出来的包,当然全都加上了namespace。再也不用区分循环依赖和非循环依赖了~~~

Github

上面的所有代码可以Github上找到:
https://github.com/kmdjs/kmdjs


dntzhang
634 声望74 粉丝

腾讯高级工程师