all articles

Thoughts about front-end dev by implementing a ToDo App(1) -- the most primitive approach

2016-09-15 @sunderls

A_ToDo_App js

毕业之后做PM了两年,然后做了3年前端工程师到现在,前端的世界有太多的新技术新框架。想当初自己的CollaMark第一版还是用的Ember.js,后来换到了Angular.js,还没来得及怎么优化,Angular 2就已经在9月15号正式release了,宣告了初代目Angular的消亡。同时React.js正火得发紫,但是无论自己当项目还是公司的项目由于历史的原因(比如接手别人代码啥的),并不能很快滴跟上节奏,react.js也还在不停的进化,旁边也还有各种其他的类似angular和类似react.js的框架,还有一些其他的比如circle.js等框架(悄悄地说,我都没有时间去学习这些东西)。

有的时候很疑惑: 作为进化超速的互联网是不是需要这么多新奇的东西,使用这些新技术的性价比到底如何,是不是快速迭代最重要,使用前端框架spa是不是把原来server的逻辑搬到了前端而并没有降低工作总量。。。之类的。嘛,很多事情大概理解一下就行了不用深究,所以从现在开始我准备整理一下自己的思绪,通过一个简单的To-Do app的不同实现来感受一下框架的变化,看看具体现代框架到底解决了什么问题,基本思想是什么,做到了什么没有做到什么。

作为这一系列的第一章,我们将使用很淳朴的模版+纯js的方法来实现。

应用需求文档

  1. 标题显示list的数量
  2. 显示完整的todo list
  3. 可以添加和删除

效果如下:

思考

  1. 最初的状态可以直接把html吐出来,添加需要的id class添加事件
  2. js中管理todo list和click 事件
  3. 变动的count可以js直接更新
  4. 新增todo item的时候需要定义个模版,可以写死在js中,但是感觉定义在模版中更加直观。

step 1: html template

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
<div>
    <h1>ToDO: <span class="jsCount">0</span></h1>
    <ul class="jsList">
        <li class="jsListEmpty">no item</li>
        <script type="js/tmpl" id="js-tmpl-item">
            <li>{this}<button class="jsDelete">x</button></li>
        </script>
    </ul>
    <p><input type="text" class="jsInput"><button class="jsAdd">add</button></p>
</div>
<script src="./plain_2.js"></script>
</body>
</html>

需要js操作的部分我们都增加了以js开头的class 以免和css发生冲突。其中todo item的模版我们用了<script>标签。 基本上app的框架就算完成了

step2: js base

我们将使用pure js,包括事件处理等,为了方便起见我们假设只针对Chrome浏览器,所以使用了querySelector等方法。

首先我们缓存下所有需要用到的dom节点,以及需要用到的方法

(function(){

    var $count = document.querySelector('.jsCount');
    var $list = document.querySelector('.jsList');
    var $listEmpty = document.querySelector('.jsListEmpty');
    var $input = document.querySelector('.jsInput');
    var $add = document.querySelector('.jsAdd');
    var tmplItem = document.querySelector('#js-tmpl-item').innerHTML.trim();

    var data = [];

    var addItem = function(item){
       //...
    };

    var removeItem = function(item){
        //..
    };

    var updateCount = function(){
        //...
    };

    var addHandler = function(){
        //...
    };

    $add.addEventListener('click', addHandler, false);
})();

step3: implement methods

对于addHanlder,我们只需要调用addItem然后清空输入框,更行数量就好了。

var addHandler = function(){
    var input = $input.value.trim();
    if (input.length === 0){
        return;
    }
    addItem(input);
    $input.value = '';
    updateCount();
};

updateCount非常简单。

var updateCount = function(){
    $count.innerHTML = data.length;

    if (data.length > 0){
        $listEmpty.style.display = 'none';
    } else {
        $listEmpty.style.display = 'block';
    }
};

addItem稍微复杂一点,我们需要把模版转化为真实的dom。参数是字符串,我们需要在<ul>中添加新的<li>,假设item模版只有一个root节点,我们进行简单的字符串匹配。

因为是新增的dom,所以删除按钮的click事件也需要绑定处理方法。

var addItem = function(item){
    data.push(item);
    var matches = tmplItem.match(/<(\w+)>(.*)<\/(\w+)>/);
    var newItem = document.createElement(matches[1]);
    newItem.innerHTML = matches[2].replace('{this}', item);

    newItem.querySelector('.jsDelete').addEventListener('click', function(){
        removeItem(item);
        $list.removeChild(newItem);
    }, false);
    $list.appendChild(newItem);
};  

removeItem也是很简单。

var removeItem = function(item){
    data.splice(data.indexOf(item), 1);
    updateCount();
};

step 4: improvements

感觉到addItem中模版渲染的部分可以单独提出来,模版+data=>dom,其实就是一个模版引擎了。我们单独写一个方法模拟这个:

// rendering engine
var Render = function(selector, data){
    var tmpl =  document.querySelector(selector).innerHTML.trim();
    var matches = tmpl.match(/<(\w+)>(.*)<\/(\w+)>/);
    var newItem = document.createElement(matches[1]);
    newItem.innerHTML = matches[2].replace('{this}', data);
    return newItem;
};

done!

full code https://github.com/sunderls/lsdom/commit/b8347df85775ef45cf7b497178e419324757272b

(function(){
    // rendering engine
    var Render = function(selector, data){
        var tmpl =  document.querySelector(selector).innerHTML.trim();
        var matches = tmpl.match(/<(\w+)>(.*)<\/(\w+)>/);
        var newItem = document.createElement(matches[1]);
        newItem.innerHTML = matches[2].replace('{this}', data);
        return newItem;
    };

    // app
    var $count = document.querySelector('.jsCount');
    var $list = document.querySelector('.jsList');
    var $listEmpty = document.querySelector('.jsListEmpty');
    var $input = document.querySelector('.jsInput');
    var $add = document.querySelector('.jsAdd');

    var data = [];

    var addItem = function(item){
        data.push(item);
        var newItem = Render('#js-tmpl-item', item);
        newItem.querySelector('.jsDelete').addEventListener('click', function(){
            removeItem(item);
            $list.removeChild(newItem);
        }, false);
        $list.appendChild(newItem);
    };

    var removeItem = function(item){
        data.splice(data.indexOf(item), 1);
        updateCount();
    };

    var updateCount = function(){
        $count.innerHTML = data.length;

        if (data.length > 0){
            $listEmpty.style.display = 'none';
        } else {
            $listEmpty.style.display = 'block';
        }
    };

    var addHandler = function(){
        var input = $input.value.trim();
        if (input.length === 0){
            return;
        }
        addItem(input);
        $input.value = '';
        updateCount();
    };

    $add.addEventListener('click', addHandler, false);
})();

至此我们就算写完啦。没有什么大问题。可以在这里试玩: https://jsfiddle.net/sunderls/65v4ty3y/

但是总感觉不是那么顺手。其实很多时候找到感觉很麻烦可以省略代码的地方也就是我们可以优化的地方了。针对上面的代码我试着总结出了以下几个值得优化的地方,咱们一起想想怎么优化:

  1. dom初始化比较烦人,var $count;var $list;var $listEmpty这一些有没有办法省略
  2. 事件绑定也比较繁琐,添加todo的按钮,已经后续js声称的item中的event都要js中手动进行绑定,如果能在模版中定义自动完成就好了。
  3. updateCount被调用了多次,能否处理成自动的。
  4. html中出现了<script>模版,感觉有点不好看。
  5. ..

虽然有各种缺点,不过咱们上面这个app性能绝对是最快级别(出去模版引擎工作的部分)。针对用户的每一个操作,做到了dom点最小操作,行为->反应,我们进行了逐一定义,这已经是极限了。但是现实世界中我们还要考虑代码可读性,复杂性等问题。咱们下一章再见。