09月30, 2016

记一次项目总结

最近两个月,持续地加班,所以输出较少。

好在这两个月也算做出了一些成绩,以下记录一些项目中的思考。

背景

我们的业务是一些报表,比如说报表的表内计算、校验,表间计算、校验等。之前是用jQuery来实现的,在性能上比较差,而且有些扩展也做不了。

这次主管提出要用MVVM框架来实现,于是就有了框架选型,因为考虑到成果可能需要移植到IE8下面去,所以就排除了angularjs和vue。

目前市面上,能兼容IE8的mvvm框架还是有一些的,如:knockoutjsavalonregularjs。最后选择了regularjs,理由是它的写法比较方便,并且有较丰富的UI组件

之前做报表,还有一个痛苦是根据cell华表,来画HTML(一般需要大半天的功夫)。所以在这两个月里面,我的另外一个同事做了一个工具出来,由工具生成HTML、规则json、表初始数据json,而我就根据这三样东西来生成具体的报表组件。

碰到的问题和思考

1. 引用还是克隆?

在实际的项目中,很容易碰到这个问题。像facebook就比较提倡immutable,因为数据一旦多了,杂乱无章,如果引用的数据,一旦哪里做了修改,排查起来甚是麻烦。但有时候又不得不引用,所以最好有一个store仓库来做管理。

简单克隆的话,代码可以如下:

{
     /**
     * 深拷贝json对象
     * @param data
     */
    cloneData: function(data){

        var newData = null;

        try{

            newData = JSON.parse(JSON.stringify(data));

        }catch(e){
            console.log(e);
        }

        return newData;

    },
}

这里说明一下,所有涉及到JSON.parse的地方,建议都try catch,不然很容易出错。

2. 弹窗阻塞

这个比较好实现,自己写一个promise即可。粗略代码如下:

{
    confirm: function () {
        if (arguments.length === 0) return null;
        var title, content;
        if (arguments.length === 1) {
            title = "提示";
            content = arguments[0];
        } else {
            title = arguments[0];
            content = arguments[1];
        }
        return new Promise(function (resolve) {
            var modal = new RGUI.Modal({
                data: {
                    title: title,
                    content: content,
                    draggable: true,
                    okButton: "确认",
                    cancelButton: "取消"
                },

                //重写close方法
                close: function (result) {
                    /**
                     * @event close 关闭对话框时触发
                     * @property {boolean} result 点击了确定还是取消
                     */
                    this.$emit("close", {
                        result: result
                    });
                    result ? this.ok() : this.cancel();

                    return resolve(result);
                }
            });
        })
    }
}

3. toFixed

之前在看正美的框架设计时,里面有这样的一段代码是用来兼容IE6 7的toFixed的

//http://stackoverflow.com/questions/10470810/javascript-tofixed-bug-in-ie6
if (0.9.toFixed(0) !== "1") {
    Number.prototype.toFixed = function(n) {
        var power = Math.pow(10, n);
        var fixed = (Math.round(this * power) / power).toString();
        if (n == 0)
            return fixed;
        if (fixed.indexOf(".") < 0)
            fixed += ".";
        var padding = n + 1 - (fixed.length - fixed.indexOf("."));
        for (var i = 0; i < padding; i++)
            fixed += "0";
        return fixed;
    };
}

但其实toFixed本身也是有bug的,比如:

(17.005).toFixed(2) ; //期望得到17.01,但实际得到的是17.00

这个在一般情况下是可以接受的,但是在“钱”上面却是无法忍受了。

于是我就盗用了这位哥们的代码:关于JavaScript中计算精度丢失的问题(一)

/**
 * 左补齐字符串
 *
 * @param nSize
 *            要补齐的长度
 * @param ch
 *            要补齐的字符
 * @return
 */
String.prototype.padLeft = function(nSize, ch)
{
    var len = 0;
    var s = this ? this : "";
    ch = ch ? ch : "0";// 默认补0

    len = s.length;
    while (len < nSize)
    {
        s = ch + s;
        len++;
    }
    return s;
}

/**
 * 右补齐字符串
 *
 * @param nSize
 *            要补齐的长度
 * @param ch
 *            要补齐的字符
 * @return
 */
String.prototype.padRight = function(nSize, ch)
{
    var len = 0;
    var s = this ? this : "";
    ch = ch ? ch : "0";// 默认补0

    len = s.length;
    while (len < nSize)
    {
        s = s + ch;
        len++;
    }
    return s;
}
/**
 * 左移小数点位置(用于数学计算,相当于除以Math.pow(10,scale))
 *
 * @param scale
 *            要移位的刻度
 * @return
 */
String.prototype.movePointLeft = function(scale)
{
    var s, s1, s2, ch, ps, sign;
    ch = ".";
    sign = "";
    s = this ? this : "";

    if (scale <= 0) return s;
    ps = s.split(".");
    s1 = ps[0] ? ps[0] : "";
    s2 = ps[1] ? ps[1] : "";
    if (s1.slice(0, 1) == "-")
    {
        s1 = s1.slice(1);
        sign = "-";
    }
    if (s1.length <= scale)
    {
        ch = "0.";
        s1 = s1.padLeft(scale);
    }
    return sign + s1.slice(0, -scale) + ch + s1.slice(-scale) + s2;
}
/**
 * 右移小数点位置(用于数学计算,相当于乘以Math.pow(10,scale))
 *
 * @param scale
 *            要移位的刻度
 * @return
 */
String.prototype.movePointRight = function(scale)
{
    var s, s1, s2, ch, ps;
    ch = ".";
    s = this ? this : "";

    if (scale <= 0) return s;
    ps = s.split(".");
    s1 = ps[0] ? ps[0] : "";
    s2 = ps[1] ? ps[1] : "";
    if (s2.length <= scale)
    {
        ch = "";
        s2 = s2.padRight(scale);
    }
    return s1 + s2.slice(0, scale) + ch + s2.slice(scale, s2.length);
}
/**
 * 移动小数点位置(用于数学计算,相当于(乘以/除以)Math.pow(10,scale))
 *
 * @param scale
 *            要移位的刻度(正数表示向右移;负数表示向左移动;0返回原值)
 * @return
 */
String.prototype.movePoint = function(scale)
{
    if (scale >= 0)
        return this.movePointRight(scale);
    else
        return this.movePointLeft(-scale);
}

/**
 * 修正toFixed方法
 * @param s
 * @returns {string}
 */
Number.prototype.toFixed = function(scale)
{
    var s, s1, s2, start;

    s1 = this + "";
    start = s1.indexOf(".");
    s = s1.movePoint(scale);

    if (start >= 0)
    {
        s2 = Number(s1.substr(start + scale + 1, 1));
        if (s2 >= 5 && this >= 0 || s2 < 5 && this < 0)
        {
            s = Math.ceil(s);
        }
        else
        {
            s = Math.floor(s);
        }
    }

    return s.toString().movePoint(-scale);
}

4. 表达式的一些处理

我们都知道像excel,都有类似A3[1:10]这样的,表示sum,或者怎样。

所以不可避免的是,表达式字符串难免会有类似sum这样的写法,需要在转js代码前,做一次replace。

{
    /**
     * sum(body.e8[0:12]) -->(body[1].e8*1+body[2].e8*1+body[3].e8*1)
     * sum(body.e8) --> (body[0].e8*1 + body[1].e8*1 + body[2].e8*1 + ...)
     * @param exp
     * @returns exp
     * @private
     */
    _initSum : function(exp) {
        var reg = /(sum\()(\w|.)+?(\[\d:\d]){0,1}\)/ig;
        var arr = exp.match(reg);
        if (arr) {

            arr.forEach(function(_exp) {

                //TODO

            }
        }
    }
}

再说说如何将字符串转成可执行的js代码。

json大概是这样的:

{
     "expression": "",
     "type": "expr"
}

type有两个值:exprfunc。在我的报表框架里面的处理是,如果是func,我会在expression包括上funciton(){}(),然后with上下文通过eval来执行结果。

现在想想好lower,其实我不需要通过eval,直接new Function就好了,都不需要with了,在性能上显然能提升很多。

var obj = {test: "123"}

new Function("console.log(obj.test)")();

当然上面的new Function需要try catch,以免字符串变成可执行的js报错。

当然这里也要思考一个问题是,怎么把这个规则放到控制台上面去跑,去找具体哪里出错了,或者想知道一下具体的结果。(重要也不重要,一般出错的话,把哪条规则出错,提示出来即可)。

5. 比较两个对象是否相等

需求是这样的,当切换左侧菜单时,需要判断数据是否有修改。最简单的方式是比较初始数据和最新数据。我知道像lodash有提供equal的方法,不过单纯为了一两个方法而引入lodash,想想没这个必要。

{
    //代码来源:http://stackoverflow.com/questions/201183/how-to-determine-equality-for-two-javascript-objects
    isEqual: function(x, y){

        var _this = this;

        if (x === null || x === undefined || y === null || y === undefined) { return x === y; }
        // after this just checking type of one would be enough
        if (x.constructor !== y.constructor) { return false; }
        // if they are functions, they should exactly refer to same one (because of closures)
        if (x instanceof Function) { return x === y; }
        // if they are regexps, they should exactly refer to same one (it is hard to better equality check on current ES)
        if (x instanceof RegExp) { return x === y; }
        if (x === y || x.valueOf() === y.valueOf()) { return true; }
        if (Array.isArray(x) && x.length !== y.length) { return false; }

        // if they are dates, they must had equal valueOf
        if (x instanceof Date) { return false; }

        // if they are strictly equal, they both need to be object at least
        if (!(x instanceof Object)) { return false; }
        if (!(y instanceof Object)) { return false; }

        // recursive object equality check
        var p = Object.keys(x);
        return Object.keys(y).every(function (i) { return p.indexOf(i) !== -1; }) &&
            p.every(function (i) { return _this.isEqual(x[i], y[i]); });

    }
}

6. 脏检查原理

这个还是得要了解一下的,因为像regularjs,很多时候数据变了,但视图没变,需要自己手动调用$update方法,包括像$watch也是在视图变了之后,才会监听到。虽然麻烦,但加深了自己对脏检查的理解。

之前使用angularjs,没有使用过$digest,很容易陷入一个误区“所见即所得”,其实不是的,它在内部很多地方,都帮我们做了处理。

我强烈推荐阅读构建自己的AngularJS,第一部分:Scope和Digest,跑一遍demo,然后自己也写一下,就能简单地理解一些脏检查的原理吧。

哦对,如果在使用regularjs的时候,出了问题,只要在github上提issue就行了。他们回复issue的速度还是超级快的,比较给力。

7. 继承的必要性

一开始没觉得,但在做弹出层时意识到了。因为这个弹出层可不是简单的确认、提示框,而是一个有功能模块的page。对于框架来说,只是做loadjs的功能,然后实例化对象。具体这个弹出层内部如何实现,是要扔到外部去做的。

/*! loadJS: load a JS file asynchronously. [c]2014 @scottjehl, Filament Group, Inc. (Based on http://goo.gl/REQGQ by Paul Irish). Licensed MIT */
(function( w ){
    var loadJS = function( src, cb ){
        "use strict";
        var ref = w.document.getElementsByTagName( "script" )[ 0 ];
        var script = w.document.createElement( "script" );
        script.src = src;
        script.async = true;
        ref.parentNode.insertBefore( script, ref );
        if (cb && typeof(cb) === "function") {
            script.onload = cb;
        }
        return script;
    };
    // commonjs
    if( typeof module !== "undefined" ){
        module.exports = loadJS;
    }
    else {
        w.loadJS = loadJS;
    }
}( typeof global !== "undefined" ? global : this ));

简单粗暴的loadJS代码,但是它未对次数做控制。比如说弹出层关闭时,还去loadJS的话,那么script会加载多次。

在我的代码中,用了一个变量,做了下简单的处理。

8. 其他

在报表中,还是会存在一些如日期、下拉框、单选框、复选框的组件。

工具提供给我的只是model的值,以及组件要插入的DOM的位置(ref)。需要注意的是,强烈建议在mvvm中用ref来替代DOM。

而我在框架内部根据model(即当前值selected)和ref来生成组件,并且完成当change的时候,改变model的值。

看似完成了,实则不然。还有一种是外部的规则json里面,直接改变某个组件model的值,然后要让控件view发生改变。

这里还有一些其他的问题,如:要disable某个单选框。

{
    renderRadio: function(){
        //TODO
    },
    disableRadio: function(){
        //TODO
    }
}

我目前的做法是在下面的一个方法里面跑定时器,当renderRadio,渲染好了radio后,清除定时器。

最后的最后,一定要尽量多的捕获异常,然后在控制台输出。

小结

抛开一些蛋疼的需求,写这个申报框架对我自身也是一种提高。总的来说,痛并快乐着。

本文链接:www.my-fe.pub/post/project-summary.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。