实战三:模仿腾讯课堂,带搜索功能的课程选项卡

259 阅读6分钟

一.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="css/index.css">
</head>

<body>
    <div class="courses-wrap">
        <input type="text" id="js-search-input" placeholder="搜索课程">
        <div class="course-tab">
            <ul class="course-tab-list js-course-tab-list clearfix">
                <li class="tab-item">
                    <a href="javascript:;" class="course-tab-lk current" data-field="all">全部</a>
                </li>
                <li class="tab-item">
                    <a href="javascript:;" class="course-tab-lk" data-field="free">公益课程</a>
                </li>
                <li class="tab-item">
                    <a href="javascript:;" class="course-tab-lk" data-field="vip">vip课程</a>
                </li>
            </ul>
        </div>


        <!-- <div class="course-card-list-wrap">
            
        </div>  -->

        <div class="course-card-list-wrap">
            <ul class="course-card-list js-course-card-list"></ul>
        </div>

    </div>

    <div id="js-course-data" style="display: none">
        [{"id":
        "1","course":"前端开发之企业级深度JavaScript特训课【JS++前端】","classes":"19","teacher":"小野","img":"ecmascript.jpg","is_free":"1","datetime":"1540454477","price":"0"},
        {"id":
        "2","course":"WEB前端工程师就业班之深度JSDOM+讲师辅导-第3期【JS++前端】","classes":"22","teacher":"小野","img":"dom.jpg","is_free":"0","datetime":"1540454477","price":"699"},
        {"id":
        "3","course":"前端开发之企业级深度HTML特训课【JS++前端】","classes":"3","teacher":"小野","img":"html.jpg","is_free":"1","datetime":"1540454477","price":"0"},
        {"id":
        "4","course":"前端开发之企业级深度CSS特训课【JS++前端】","classes":"5","teacher":"小野","img":"css.jpg","is_free":"1","datetime":"1540454477","price":"0"},
        {"id":
        "5","course":"前端就业班VueJS+去哪儿网+源码课+讲师辅导-第3期【JS++前端】","classes":"50","teacher":"哈默","img":"vuejs.jpg","is_free":"0","datetime":"1540454477","price":"1280"},
        {"id":
        "6","course":"前端就业班ReactJS+新闻头条实战+讲师辅导-第3期【JS++前端】","classes":"21","teacher":"托尼","img":"reactjs.jpg","is_free":"0","datetime":"1540454477","price":"2180"},
        {"id":
        "7","course":"WEB前端开发工程师就业班-直播/录播+就业辅导-第3期【JS++前端】","classes":"700","teacher":"JS++名师团","img":"jiuyeban.jpg","is_free":"0","datetime":"1540454477","price":"4980"}]
    </div>

    <script type="text/html" id="js-card-item-tpl">
        <li class='card-item'>
            <a href="javascript:;" class='img-lk'>
                <img src="img/{{img}}" alt="">
            </a>
            <div class="item-status">
                <span class="item-status-text">随到随学</span>
            </div>

            <h4 class="item-tt"> 
                <!--经验:tt代表title  -->
                <a href="javascript:;" class='tt-lk'>{{courseName}}</a>
            </h4>
            <div class="item-line">
                <span class='item-price {{isFree}}'>{{price}}</span>
                <span class="item-info">{{hours}}课时</span>
            </div>

        </li>

    </script>
    <script src="js/1.js"></script>
</body>

</html>

二.js

var initCourseTab = (function (doc) {
    var oCourseTabLks = doc.getElementsByClassName('course-tab-lk'),
        oCourseCardList = doc.getElementsByClassName("js-course-card-list")[0],
        courseData = JSON.parse(doc.getElementById('js-course-data').innerHTML),
        cardItemTpl = doc.getElementById('js-card-item-tpl').innerHTML,
        oSearchInput = doc.getElementById('js-search-input');       //3
        oCourseTabLksLen = oCourseTabLks.length;

    return {
        searchCourse: function () {
            // var val = this.value ;
            var val = oSearchInput.value,    //   val等于输入的内容
                len = val.length;

            if (len > 0) {
                var data = this.searchData(courseData, val);
                if (data && data.length > 0) {
                    oCourseCardList.innerHTML = this.makeList(data);
                } else {
                    oCourseCardList.innerHTML = this.showTip('没有搜索到相关课程');
                }

            } else {
                this.restoreList();
            }
        },

        showTip: function (text) {
            return '<div><span>' + text + '</span></div>'
        },

        tabClick: function () {
            var e = e || window.event,
                tar = e.target || e.srcElement,
                className = tar.className,
                item;
            if (className === 'course-tab-lk') {     //让点击的出现current效果
                var field = tar.getAttribute('data-field')
                this.changeTabCurrent(tar);
                oCourseCardList.innerHTML = this.makeList(this.filterData(field, courseData));
            }
        },

        initCourseList: function () {
            var data = this.filterData('all', courseData);
            oCourseCardList.innerHTML = this.makeList(data);
        },

        makeList: function (data) {
            var list = '';
            data.forEach(function (elem) {
                list += cardItemTpl.replace(/{{(.*?)}}/g, function (node, key) {
                    return {
                        img: elem.img,
                        courseName: elem.course,
                        isFree: elem.is_free === '1' ? 'free' : 'vip',
                        price: elem.is_free === '1' ? '免费' : ('¥' + elem.price + '.00'),
                        hours: elem.classes
                    }[key];
                });
            });
            return list;
        },


        filterData: function (field, data) {
            return data.filter(function (elem) {
                switch (field) {
                    case 'all':
                        return true;
                        break;
                    case 'free':
                        return elem.is_free === '1';
                        break;
                    case 'vip':
                        return elem.is_free === '0';
                        break;
                    default:
                        return true;
                }
            })
        },

        restoreList: function () {
            oCourseCardList.innerHTML = this.makeList(courseData);
            this.changeTabCurrent(oCourseTabLks[0]);
        },

        changeTabCurrent: function (currentDom) {
            for (var i = 0; i < oCourseTabLksLen; i++) {
                item = oCourseTabLks[i];//oCourseTabLks为 上面3个的DOM名
                item.className = 'course-tab-lk';
            }
            currentDom.className += ' current'

        },

        searchData: function (data, keyword) {           //  keyword就是val
            return data.reduce(function (prev, elem) {    //reduce返回一个新的数组,把符合条件的值放入新数组中
                var res = elem.course.indexOf(keyword)
                // indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。 如果没有找到匹配的字符串则返回 -1。
                if (res !== -1) {
                    prev.push(elem);
                }
                return prev;
            }, [])
        }

    };
})(document);

; (function (doc) {
    var oSearchInput = doc.getElementById('js-search-input');
    var oTabList = doc.getElementsByClassName('js-course-tab-list')[0];

    var init = function () {
        initCourseTab.initCourseList();
        bindEvent();
    }
    function bindEvent() {
        // oSearchInput.addEventListener('input', initCourseTab.searchCourse, false);
        oSearchInput.addEventListener('input', initCourseTab.searchCourse.bind(initCourseTab), false),     //5
            oTabList.addEventListener('click', initCourseTab.tabClick.bind(initCourseTab), false);//第三个参数布尔值,默认值是false:事件在冒泡阶段执行,
        // 而true:时间在捕获阶段执行
        //从此this指向本身,bind改变this指向,和call等一样
    }
    init();
}(document));

最终效果展示:

JS基本逻辑步骤:

(1)

1.写模板 text/html
2.用一个主模板搭配一个子模板,这样更好维护: 主模板专门调用方法,而子模板专门细写方法, 如下图:

细节总结:

  • 立即执行函数后面的括号内填入 实参document ,对应形参doc ,这样doc就能代替document 节省了时间
;(function(doc){...

}(document)
  • addEventListener() 第一个参数event,第二个参数function, event,第二个参数function 第三个参数布尔值,默认值是false:事件在冒泡阶段执行, 而true:事件在捕获阶段执行
  • 为了让子模块自动执行,设置几个函数调用: 初始化函数:init (init翻译为初始化)
    绑定事件bindEvent(bindEvent翻译为绑定事件), 绑定事件里面设置 Click点击事件等,详细click函数在子模块里面写
    最后写 init() , 让bindEvent等执行起来

(2) 细写方法:子模板

1.获取DOM元素事件

注意点:第3行获取JSON数据的话要在后面 加 innerHTML

2.设置click事件

注意点:return一个对象,所以后面写{}

3. 过滤

基本步骤为:获取属性、改变指向、填写方法

  • 获取属性: 注意点:第19行的 tar.getAttribute('data-field'),能直接得出其存储的值(all、vip等)

  • 改变this指向: 第64行更改了指向: 点击页面时,this指向子模块自己 bind改变this指向,和call等一样

指向自己的话就能拿函数内获取的数据去过滤

  • 填写方法:

这样搞好过滤函数后,第25行就能输出JSON数据

4. 渲染模板

  1. 在html中额外填写展示块
  <div class="course-card-list-wrap">
            <ul class="course-card-list js-course-card-list"></ul>
  </div>

之前为了迎合css写的ul、li样式 都要消除掉

  1. JS中获取元素
  oCourseCardList = doc.getElementsByClassName("js-course-card-list"),
  1. 数据赋值
  oCourseCardList.innerHTML =   this.makeList(this.filterData(field, courseData));
  1. 数据渲染: makelist函数
      makeList: function (data) {
            var list = '';
            data.forEach(function (elem) {
             list += cardItemTpl.replace(/{{.*?}}/g, function(node,key){
                 return {
                        img:elem.img,
                        courseName:elem.course,
                        isFree: elem.is_free === '1' ? 'free' : 'vip',
                        price: elem.is_free === '1'? '免费' : ('¥' + elem.price + '.00'),
                        hours: elem.classes
                 }[key];
             });
            });
        },
  • 注意点: cardItemTpl是之前 text/html 的id名,
    return 里面的键名是根据 tetx/html 里面写的

  • 正则表达式经典写法模式:

 list += cardItemTpl.replace(/{{.*?}}/g, function(node,key){
                 return{
                        
                 }[key]
             })
  • 《span class='item-price {{isFree}} ' > {{price}} 》在css表达式中 改变免费和价格的颜色: 两个类名不需要打空格:
.course-card-list-wrap .card-item  .item-line .item-price.free{
    color: green;
}
.course-card-list-wrap .card-item  .item-line .item-price.vip{
    color: orange;
}

总结

模板渲染知识回顾: 在body中消除掉原来为了迎合css写的li样式
但是要留下 ul
在 text/html 中写原本消除的 li 等可变的数据
text/html中的类名都不变,这样css的效果还在

5.最开始的页面

由于之前去掉了body中的li,所以只能通过点击才能获得li,如果不点击就一直没有页面展示

所以接下来要设置最初展示页面:
1.子模块return{}中 新添方法

   initCourseList: function(){
            var data = this.filterData('all', courseData);
            oCourseCardList.innerHTML = this.makeList(data);
        },

没必要填写field,填写个all即可,反正第一页也是all的页面


2.自执行:在主模块中写上方法

  var init = function () {
        initCourseTab.initCourseList();
        bindEvent();
    }

三. 搜索功能的实现

因为之前主模块已经写好search事件了,所以这次都是在子模块中填写对应的搜索功能

(1).做出后台输出数据的功能

  • 1.因为109行以及获取了输入元素
 var oSearchInput = doc.getElementById('js-search-input');

所以 oSearchInput中有自己的属性 'value' ,可以获取输入的内容

第18行就是测试 value 有无效果

  • 2. 20行: 如果输入了,即value的长度大于0 就使用searchData函数 21行注意: 这里要在下面改变this的指向,指向函数本身, 改变指向才能拿函数内获取的数据 ;
    和 二.(2).3 过滤那里 改变指向一个道理


21行注意: 传入的数据需要传courseData,即json数据

  • 3. 第94、96 行仅仅是为了测试
    注意: indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。 如果没有找到匹配的字符串则返回 -1。

(2) 做出页面显示效果

1. 第21 行直接去掉console.log(), 将里面的值赋给页面元素的innerHTML

  if (len > 0) {  //如果输入了,就使用searchData函数
   oCourseCardList.innerHTML = this.makeList(this.searchData(courseData, val)); //courseData是JSON数据  
            }

2.设置输入框字体减少至0时的清空效果

  if (len > 0) {  //如果输入了,就使用searchData函数
                oCourseCardList.innerHTML = this.makeList(this.searchData(courseData, val)); //courseData是JSON数据  
            } else {
                oCourseCardList.innerHTML = oCourseCardList.innerHTML = this.makeList(courseData);
            }      

逻辑为:当输入框的字符为0时,全部展现

3.上述清空效果只对第一项('全部')有用, 所以还需设置一个方法

    if (len > 0) {  //如果输入了,就使用searchData函数
                oCourseCardList.innerHTML = this.makeList(this.searchData(courseData, val)); //courseData是JSON数据  
            } else {
                this.restoreList();
            }
        },

四. 缩减代码/抽象函数(经验)

有很多重复的代码,可以将重复的代码抽象出来,单独变成一个方法,其他地方需要时调用即可 如:36行代码与上述restoreList方法中的代码类似 所以我们可以额外创立一个方法 changeTabCurrent ,缩减代码量,提升运算速度 :

36行的也可以调用它:

    if (className === 'course-tab-lk') {     //让点击的出现current效果
      var field = tar.getAttribute('data-field') 
      this.changeTabCurrent(tar);
     oCourseCardList.innerHTML = this.makeList(this.filterData(field, courseData));
            }         

五. 功能完善:提示没有相关课程等小功能的实现

对应三.(1) 注意:第23行showTip 前面必须加 this 才能找到下面的方法