案例
数据筛选
数据筛选案例1:tab栏切换筛选(全部课程/免费课程/收费课程)显示在页面
<p class="J_nav"><a href="javascript:;" data-field="all" >全部课程</a><a href="javascript:;" data-field="free" >免费课程</a><a href="javascript:;" data-field="vip" >收费课程</a></p><ul class="J_list"></ul><!-- 隐藏数据域 --><div id="J_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前端工程师就业班之深度JS DOM+讲师辅导-第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><!-- tpl模板 --><script type="text/html" id="J_tpl" ><li>{{course}}</li><hr /></script>
//数据筛选案例:tab栏切换筛选(全部课程/免费课程/收费课程)显示在页面(function () {//获取nav节点,ul-list节点,data数据, tpl模板节点var oNav = document.getElementsByClassName("J_nav")[0],oList = document.getElementsByClassName("J_list")[0],data = JSON.parse(document.getElementById("J_data").innerHTML),tpl = document.getElementById("J_tpl").innerHTML;//初始化案例var init = function () {var all = filterData(data, "all");console.log(all);//首页页面加载// oList.innerHTML = renderList(all);bindEvent();};//绑定事件管理function bindEvent() {//委托代理oNav.addEventListener("click", navClick, false);}//定义点击导航事件处理函数function navClick(e) {var e = e || window.event,tar = e.target || e.srcElement,//事件源:被点击的标签atagName = tar.tagName.toLowerCase();// console.log(tagName); //aif (tagName === "a") {var field = tar.getAttribute("data-field");// console.log(field); //拿到自定义属性 all/free/vip//测试筛选数据// console.log(filterData(data, field));//把修改后的数据给渲染到页面oList.innerHTML = renderList(filterData(data, field));}}//定义数据筛选函数function filterData(data, field) {//使用自己封装的myFilter方法var arr = data.myFilter(function (elem, index, arr) {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;}});return arr;}//模板渲染function renderList(data) {var list = "";data.forEach(function (elem, index, arr) {list += tpl.replace(/{{(.*?)}}/g, function (node, key) {return {course: elem.course,}[key];});});return list;}init();})();//封装myFilterArray.prototype.myFilter = function (fn) {var arr = this,len = arr.length,arg2 = arguments[1] || window,_newArr = [],item;for (var i = 0; i < len; i++) {item = deepClone(arr[i]);fn.apply(arg2, [arr[i], i, arr]) ? _newArr.push(item) : "";}return _newArr;};//利用递归克隆函数进行再次循环function deepClone(origin, target) {var target = target || {},toStr = Object.prototype.toString,arrType = "[object Array]";for (var key in origin) {if (origin.hasOwnProperty(key)) {if (typeof origin[key] === "object" && origin[key] !== null) {if (toStr.call(origin[key]) === arrType) {target[key] = [];} else {target[key] = {};}deepClone(origin[key], target[key]);} else {target[key] = origin[key];}}}return target;}
数据筛选案例2:利用reduce函数实现搜索框输入内容返回匹配到的搜索结果
<input type="text" id="J_searchInput" placeholder="搜索课程"><ul class="J_list"><ul class="J_list"><span>- 暂无数据 -</span></ul>
课程数据
/*** 案例:搜索课程*/;(function (doc, data) {//获取节点元素var oInput = doc.getElementById('J_searchInput'),oList = doc.getElementsByClassName('J_list')[0];function init() {bindEvent();}function bindEvent() {oInput.addEventListener('input', searchInput, false);}function searchInput() {var val = this.value,len = val.length;if (len > 0) {if (makeList(searchData(data, val))) {oList.innerHTML = makeList(searchData(data, val));} else {oList.innerHTML = '<span>- 暂无数据 -</span>';}} else {oList.innerHTML = '<span>- 暂无数据 -</span>';}}/*** 处理数据函数-筛选数据** @data 后台数据* @keyword input关键字* @返回值 如果输入内容存在数据字段里就筛选出来并push到数组里**/function searchData(data, keyword) {return data.reduce(function (prev, elem) {//筛选操作var res = elem.course.indexOf(keyword);//该字段数据存在if (res !== -1) {prev.push(elem);}return prev;}, []);}/*** 字符串拼接list列表* @matchData 筛选整理好的数据*/function makeList(matchData) {var list = '';//没有数据什么都不做if (!matchData || matchData.length <= 0) {return false;} else {matchData.forEach(function (item) {list += '<li>' + item.course + '</li>';})}return list;}init();})(document, courseData);
JS运动
案例:鼠标移入下拉菜单动态下滑列表
a {text-decoration: none;}ul {padding: 0;margin: 0;list-style: none;}.dropdown {position: relative;width: 200px;height: 50px;background-color: #000;}.dropdown::after {content: '';display: table;position: absolute;top: 18px;right: 15px;width: 15px;height: 15px;background-image: url(icons/arrow-down.png);background-size: 100% 100%;background-repeat: no-repeat;}.dropdown.up::after {background-image: url(icons/arrow-top.png);}.dropdown .list {height: 0px;overflow: hidden;}.dropdown a {display: block;}.dropdown .main {height: 100%;text-align: center;line-height: 50px;color: #fff;}.dropdown .item {height: 40px;background-color: #333;}.dropdown .item:hover {background-color: #000;}.dropdown .item a {height: 100%;color: #999;text-align: center;line-height: 40px;}
<div class="dropdown"><a href="javascript:;" class="main">下拉菜单</a><ul class="list"><li class="item"><a href=""></a> 第1个项目</li><li class="item"><a href=""></a> 第2个项目</li><li class="item"><a href=""></a> 第3个项目</li><li class="item"><a href=""></a> 第4个项目</li><li class="item"><a href=""></a> 第5个项目</li></ul></div>
var dropdown = document.getElementsByClassName('dropdown')[0],oList = elemChildren(dropdown)[1],timer = null,listHeight = 0,speed = 8;dropdown.onmouseenter = function () {clearInterval(timer);timer = setInterval(function () {if (listHeight >= 200) {clearInterval(timer);} else {listHeight = parseInt(getStyles(oList, 'height')) + speed;oList.style.height = listHeight + 'px';}}, 1);this.className += ' up';}dropdown.onmouseleave = function () {clearInterval(timer);timer = setInterval(function () {if (listHeight <= 0) {clearInterval(timer);} else {listHeight = parseInt(getStyles(oList, 'height')) - speed;oList.style.height = listHeight + 'px';}}, 1);this.className += 'dropdown';}function getStyles(elem, prop) {//检测getComputedStyle是否存在if (window.getComputedStyle) {//存在,打印具体属性值if (prop) {return window.getComputedStyle(elem, null)[prop];}//不存在,打印集合return window.getComputedStyle(elem, null);} else {if (prop) {return elem.currentStyle[prop];} else {return elem.currentStyle;}}}//找子元素函数function elemChildren(node) {var temp = {'length': 0,'push': Array.prototype.push,'splice': Array.prototype.splice};var children = node.childNodes,len = children.length,item;for (var i = 0; i < len; i++) {item = children[i];if (item.nodeType === 1) {temp.push(item);}}return temp;}
页面尺寸
案例:自动阅读读书
知识点:
- 两栏布局:左边盒子绝对定位,右边盒子不用设置宽度,只设置margin-left
省略三件套:
overflow: hidden;text-overflow: ellipsis;white-space: nowrap;
- 滚动事件
插件三件套
- 立即执行函数
- 原型上挂载方法
- 构造函数赋值给window
window.AutoReader = AutoReader;
功能:
- 回到顶部
- 自动播放/暂停阅读
- 内容触底会暂停播放
- 具有配置项的插件
面向过程
过程开发todolist
DOM结构划分组件
///common.cssbody {margin: 0;}a {text-decoration: none;}ul {padding: 0;margin: 0;list-style: none;}h1,h2,h3,h4,h5,h6,p {margin: 0;font-weight: bold;}input,button {outline: none;box-sizing: border-box;border: none;}
//index.css.wrap {position: relative;width: 500px;height: 500px;margin: 50px auto;box-shadow: 0 0 5px #999;border-radius: 10px;overflow: hidden;}.wrap .list-hd {position: absolute;top: 0;left: 0;width: 100%;height: 44px;background-color: #000;color: #fff;text-align: center;line-height: 44px;}.list-hd .fa-plus {position: absolute;top: 15px;right: 15px;color: #fff;}.input-wrap {display: none;position: absolute;top: 44px;left: 0;width: 100%;height: 40px;border-bottom: 1px solid #ddd;}.input-wrap .input-bd {float: left;width: 384px;height: 100%;padding: 0px 5px 5px 10px;}.input-wrap .input-bd input {width: 100%;height: 100%;border: 1px solid #ddd;}.input-wrap .btn-bd {float: left;width: 86px;height: 100%;padding: 0px 10px 5px 5px;}.input-wrap .btn-bd button {width: 100%;height: 100%;border: 1px solid #ddd;background-color: #fff;}.list-warp {height: 456px;margin-top: 44px;overflow: auto;}.list-wrap .item {position: relative;height: 50px;}.list-warp .item:nth-child(odd),.list-warp .item:hover {background-color: #eee;}.list-warp .item .item-content {position: absolute;left: 0;top: 0;width: 400px;height: 100%;line-height: 50px;text-indent: 15px;}.list-warp .item .btn-group {height: 100%;margin-left: 400px;line-height: 50px;}.list-warp .item .btn-group a {margin-left: 15px;}.list-warp .item .btn-group .edit-btn {color: green;}.list-warp .item .btn-group .remove-btn {color: red;}
<!-- plus 图标 --><link rel="stylesheet" href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" /><link rel="stylesheet" href="css/normalize.css" /><link rel="stylesheet" href="./css/common.css" /><link rel="stylesheet" href="./css/index.css" />
<!-- 包装器 --><div class="wrap"><!-- header --><div class="list-hd"><h2>TodoList</h2><!-- 承载图标 --><a href="javascript:;" class="j-show-input fa fa-plus"></a></div><div class="input-wrap"><div class="input-bd"><input type="text" id="textInput" /></div><div class="btn-bd"><button class="j-add-item">增加项目</button></div></div><div class="list-warp"><ul class="j-list list"></ul></div></div><script type="text/javascript" src="./js/utils.js"></script><script type="text/javascript" src="./js/index.js"></script>
init();//模块化和组件化开发todolistfunction init() {initTodoList;}var initTodoList = (function () {var showInput = document.getElementsByClassName('j-show-input')[0],inputWrap = document.getElementsByClassName('input-wrap')[0],addItem = document.getElementsByClassName('j-add-item')[0],textInput = document.getElementById('textInput'),oList = document.getElementsByClassName('j-list')[0];inputShow = false;//事件绑定函数addEvent(showInput, 'click', function () {//如果为真,点击为假if (inputShow) {inputWrap.style.display = 'none';inputShow = false;} else {inputWrap.style.display = 'block';inputShow = true;}});addEvent(addItem, 'click', function () {//获取动态的lis列表(类数组)var oItems = document.getElementsByClassName('item'),val = textInput.value,len = val.length,itemLen = oItems.length,item;//情况1:输入框为空if (len === 0) {return;}//遍历对比模板字符p标签里面的textfor (var i = 0; i < itemLen; i++) {//item 是 li// item = oItems[i];//但希望找的是p里面的text//解决方法item = elemChildren(oItems[i])[0];//此时能找到p元素// console.log(item);var text = item.innerText;//去重if (val === text) {alert('已存在此项目');return;}}//新增livar oLi = document.createElement('li');oLi.className = 'item';//把输入框的值传入模板函数里oLi.innerHTML = itemTpl(val);oList.appendChild(oLi);//清空输入框textInput.value = '';//隐藏输入框inputWrap.style.display = 'none';//更改状态inputShow = false;})// 处理模板//直接返回字符串替换function itemTpl(text) {return ('<p class="item-content">' + text + '</p>' +'<div class="btn-group">' +'<a href="javascript:;" class="edit-btn fa fa-edit"></a>' +'<a href="javascript:;" class="remove-btn fa fa-times"></a>' +'</div>')}})();
面向对象
插件化开发todolist
index.css
.wrap {position: relative;width: 500px;height: 500px;margin: 50px auto;box-shadow: 0 0 5px #999;border-radius: 10px;overflow: hidden;}.wrap .list-hd {position: absolute;top: 0;left: 0;width: 100%;height: 44px;background-color: #000;color: #fff;text-align: center;line-height: 44px;}.list-hd .fa-plus {position: absolute;top: 15px;right: 15px;color: #fff;}.input-wrap {display: none;position: absolute;top: 44px;left: 0;width: 100%;height: 40px;border-bottom: 1px solid #ddd;z-index: 1;}.input-wrap .input-bd {float: left;width: 384px;height: 100%;padding: 0px 5px 5px 10px;}.input-wrap .input-bd input {width: 100%;height: 100%;border: 1px solid #ddd;}.input-wrap .btn-bd {float: left;width: 86px;height: 100%;padding: 0px 10px 5px 5px;}.input-wrap .btn-bd button {width: 100%;height: 100%;border: 1px solid #ddd;background-color: #fff;}.list-warp {height: 456px;margin-top: 44px;overflow: auto;}.list-warp .item {position: relative;height: 50px;}.list-warp .item:nth-child(odd),.list-warp .item:hover {background-color: #eee;}.list-warp .list.item.active{background-color: #dff0d8;}.list-warp .item .item-content {position: absolute;left: 0;top: 2%;width: 400px;height: 100%;line-height: 50px;text-indent: 15px;}.list-warp .item .btn-group {height: 50px;line-height: 50px;margin-left: 400px;}.list-warp .item .btn-group a {margin-left: 15px;}.list-warp .item .btn-group .edit-btn {color: green;}.list-warp .item .btn-group .remove-btn {color: red;}
common.css
body {margin: 0;}a {text-decoration: none;}ul {padding: 0;margin: 0;list-style: none;}h1,h2,h3,h4,h5,h6,p {margin: 0;font-weight: bold;}input,button {outline: none;box-sizing: border-box;border: none;}
<!-- plus 图标 --><link rel="stylesheet" href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" /><link rel="stylesheet" href="./css/normalize.css" /><link rel="stylesheet" href="./css/common.css" /><link rel="stylesheet" href="./css/index.css" />
<!-- 包装器 --><div class="wrap" data-config='{"plusBtn": "j-plus-btn","inputArea": "input-wrap","addBtn": "j-add-item","list": "item-list","itemClass": "item"}'><!-- header --><div class="list-hd"><h2>TodoList</h2><!-- 承载图标 --><a href="javascript:;" class="j-plus-btn j-show-input fa fa-plus"></a></div><div class="input-wrap"><div class="input-bd"><input type="text" id="textInput" class="content"/></div><div class="btn-bd add-btn"><button class="j-add-item">增加项目</button></div></div><div class="list-warp"><ul class="j-list list"></ul></div></div>
(function (doc) {//构造函数var TodoList = function () {var _self = this;this.inputShowStatus = "close";this.configList = this.getConfig();this.editStatus = false;this.idx = 0;//默认配置项this.defaultConfig = {addBtn: "",inputArea: "",itemClass: "",list: "",plusBtn: "",};//对比有没有填写完整//对比是否键名/键值匹配for (var key in this.defaultConfig) {if (!this.configList.hasOwnProperty(key)) {//如果用户书写插件配置不完整时抛出错误提示console.log(errorLog(key));return;}}//获取节点元素this.setConfig();//事件//点击加号:开启/关闭输入文本框addEvent(this.plusBtn, "click", function () {_self.inputShow.call(_self);});//增加项目按钮:新增/修改addEvent(this.addBtn, "click", function () {_self.addOrEditItem.call(_self);});//列表: 编辑addEvent(this.list, "click", function (e) {_self.listPage.apply(_self, [e]);});};//原型TodoList.prototype = {//获取配置列表getConfig: function () {return JSON.parse(doc.getElementsByClassName("wrap")[0].getAttribute("data-config"));},//设置配置列表setConfig: function () {//获取元素节点this.plusBtn = doc.getElementsByClassName(this.configList.plusBtn)[0];this.inputArea = doc.getElementsByClassName(this.configList.inputArea)[0];this.addBtn = doc.getElementsByClassName(this.configList.addBtn)[0];this.content = doc.getElementsByClassName("content")[0];this.list = doc.getElementsByClassName("j-list")[0];// console.log(this);},//事件处理函数:开启/关闭输入框inputShow: function () {if (this.inputShowStatus === "open") {this.inputArea.style.display = "block";this.inputShowStatus = "close";} else if (this.inputShowStatus === "close") {this.inputArea.style.display = "none";this.inputShowStatus = "open";}},//事件处理函数:点击新增/修改按钮addOrEditItem: function () {var inputValue = this.content.value,valueLen = inputValue.length,_oItems = doc.getElementsByClassName("item"),_item;//情况1:如果文本框没有内容if (valueLen === 0) {alert("请您在输入框输入文本内容");return;}//情况2:去重for (var i = 0; i < _oItems.length; i++) {_item = _oItems[i];if (inputValue === elemChildren(_item)[0].innerText) {alert("该项目已经存在");return;}}//情况3:判断按钮状态 新增/编辑if (this.editStatus) {elemChildren(_oItems[this.idx])[0].innerText = inputValue;this.addBtn.innerText = "增加项目";this.editStatus = false;} else {this.add();}this.reset();},edit: function (tar) {// console.log("editing");var _p = elemChildren(elemParent(tar, 2))[0],_item = elemParent(tar, 2),_items = elemChildren(this.list),_idx = Array.prototype.indexOf.call(_items, _item),_lItem;this.idx = _idx;this.inputShowStatus === "open";this.inputArea.style.display = "block";//修改被点击编辑按钮时li的背景色for (var i = 0; i < _items.length; i++) {_lItem = _items[i];_lItem.className = "list item";}_item.className += " active";this.content.value = _p.innerText;this.addBtn.innerText = "修改第" + (_idx + 1) + "项";this.editStatus = true;},add: function () {// console.log("adding");var inputValue = this.content.value,oLi = doc.createElement("li"),oList = doc.getElementsByClassName("list")[0];oLi.innerHTML = this.tpl(inputValue);oLi.className += " item";oList.appendChild(oLi);},//事件处理函数:点击页面修改/删除listPage: function (e) {var e = e || window.event,tar = e.target || e.srcElement,tagName = tar.tagName;if (tagName === "A") {if (tar.className === "edit-btn fa fa-edit") {this.edit(tar);} else if (tar.className === "remove-btn fa fa-times") {this.delItem(tar);}}},//删除当前项delItem: function (tar) {var _item = elemParent(tar, 2),_itemF = elemParent(tar, 3);_itemF.removeChild(_item);},//重置reset: function () {this.content.value = "";this.inputArea.style.display = "none";},//模板处理tpl: function (text) {return ('<p class="item-content">' +text +"</p>" +'<div class="btn-group">' +'<a href="javascript:;" class="edit-btn fa fa-edit"></a>' +'<a href="javascript:;" class="remove-btn fa fa-times"></a>' +"</div>");},};//普通函数//错误日志模板function errorLog(key) {return new Error("您没有配置参数" +key +"\n" +"必须配置的参数列表如下:\n" +"打开输入框按钮元素类名:plusBtn \n" +"输入框区域元素类名: inputArea \n" +"增加项目按钮元素类名:addBtn \n" +"列表承载元素类名:list \n" +"列表项承载元素类名:itemClass");}new TodoList();})(document);
输入框事件
京东搜索框
- 功能一:随机轮播提示
- 功能二:获得焦点/失去焦点样式变化
注:企业开发的黑色#424242
input {outline: none;border: none;box-sizing: border-box;}.input-wrap {position: relative;width: 250px;height: 35px;margin: 50px auto;}.input-wrap .auto-kw {position: absolute;top: 8px;left: 5px;z-index: -1;font-style: 14px;color: #989898;}.input-wrap .auto-kw.show {display: block;color: #989898;}.input-wrap .auto-kw.hide {display: none;color: #666;}.input-wrap .search-input {width: 100%;height: 100%;background-color: transparent;border: 1px solid #ddd;text-indent: 5px;color: #424242;}
<div class="input-wrap"><!-- 自动关键字 --><div class="auto-kw" id="J_autoKw">推荐词</div><input type="text" id="J_search_kw" class="search-input"></div><!-- 推荐词 --><div id="J_recomKw" style="display: none;">["美丽独立日","LG显示器","洗发水套装","电脑主机","笔记本内存条"]</div>
function init() {keySearch();}var keySearch = (function () {//获取inputvar searchKw = document.getElementById('J_search_kw'),//获取默认推荐词divautoKw = document.getElementById('J_autoKw'),//获取推荐词列表recomKw = JSON.parse(document.getElementById('J_recomKw').innerHTML),//下标kwOrder = 0,//计时器t = null;//计时器setAutoKeys();//获得焦点addEvent(searchKw, 'focus', function () {clearInterval(t);autoKw.style.color = '#ccc';})//失去焦点addEvent(searchKw, 'blur', function () {autoKwShow(this.value, true);t = setInterval(autoKwChange, 3000);})//input事件addEvent(searchKw, 'input', function () {autoKwShow(this.value, false);})//input事件兼容addEvent(searchKw, 'propertychange', function () {autoKwShow(this.value, false);})//随机轮播提示function setAutoKeys() {// 因为一加载页面就显示推荐词,所以得在计时器前执行一次autoKwChangeautoKwChange();t = setInterval(autoKwChange, 3000);}//随机轮播提示function autoKwChange() {var len = recomKw.length;autoKw.innerHTML = recomKw[kwOrder];//列表超出列表数组范围赋值为0否则下标号自增kwOrder = kwOrder >= len - 1 ? 0 : kwOrder + 1;}//是否显示输入框文本内容function autoKwShow(val, isBlur) {if (val.length <= 0) {autoKw.className = 'auto-kw show';autoKw.style.color = isBlur ? '#989898' : '#ccc';} else {autoKw.className = 'auto-kw hide';}}});init();
移入移出事件
案例一:移动鼠标改变手机列表样式
源码地址:https://gitee.com/kevinleeeee/mouseover-event-demo/tree/master
;(function (doc) {var oItems = doc.getElementsByClassName('list-item'),curIdx = 0,oList = doc.getElementsByClassName('list')[0];var init = function () {bindEvent();}function bindEvent() {//写法一:循环绑定for (var i = 0; i < oItems.length; i++) {addEvent(oItems[i], 'mouseover', function () {oItems[curIdx].className = 'list-item';//this -> oItems[i]curIdx = Array.prototype.indexOf.call(oItems, this);oItems[curIdx].className += ' active';})}//写法二:事件代理addEvent(oList, 'mouseover', slide);}//写法二:事件代理function slide(ev) {var e = ev || window.event,tar = e.target || e.srcElement;oLi = getLi(tar, 'li');thisIdx = Array.prototype.indexOf.call(oItems, oLi);// 证明不是activeif (curIdx !== thisIdx) {//原来的项还原类名oItems[curIdx].className = 'list-item';curIdx = thisIdx;oItems[curIdx].className += ' active';}}//写法二:事件代理//找到被触发的li元素function getLi(target, element) {// console.log(target.tagName.toLowerCase());// console.log(target.parentNode);while (target.tagName.toLowerCase() !== element) {target = target.parentNode;}return target;}init();})(document)
案例二:移入移出菜单栏显示相应的菜单内容
function init() {initMenu();}var initMenu = (function () {var oMenu = document.getElementsByClassName('menu-wrap')[0],oMenuItems = oMenu.getElementsByClassName('main-item'),oSub = oMenu.getElementsByClassName('sub')[0],oSubItems = oSub.getElementsByClassName('sub-item'),menuLen = oMenuItems.length,subLen = oSubItems.length,menuItem,subItem,isInSub = false,isFirst = true,t = null,// 鼠标坐标数组mousePoses = [];for (var i = 0; i < menuLen; i++) {menuItem = oMenuItems[i];addEvent(menuItem, 'mouseenter', menuItemMouseEnter);}addEvent(oMenu, 'mouseenter', function () {addEvent(document, 'mousemove', mouseMove);});addEvent(oMenu, 'mouseleave', menuMouseOut);//鼠标移入移出addEvent(oSub, 'mouseenter', function () {isInSub = true;})//鼠标移入移出addEvent(oSub, 'mouseleave', function () {isInSub = false;})function menuItemMouseEnter(e) {var e = e || window.event,tar = e.target || e.srcElement,thisIdx = Array.prototype.indexOf.call(oMenuItems, tar),lastPos = mousePoses[mousePoses.length - 2] || {x: 0,y: 0},curPos = mousePoses[mousePoses.length - 1] || {x: 0,y: 0},toDelay = doTimeout(lastPos, curPos);//显示sub盒子oSub.className = 'sub';if (t) {clearTimeout(t);}if (!isFirst) {if (toDelay) {t = setTimeout(function () {//鼠标在子菜单里if (isInSub) {return;}addActive(thisIdx);t = null;}, 300);} else {addActive(thisIdx);}} else {addActive(thisIdx);isFirst = false;}}//抽象:新增类名function addActive(index) {removeAllActive();oMenuItems[index].className += ' active';oSubItems[index].className += ' active';}//抽象:移除类名function removeAllActive(item) {for (var i = 0; i < menuLen; i++) {item = oMenuItems[i];item.className = 'main-item';}for (var i = 0; i < subLen; i++) {item = oSubItems[i];item.className = 'sub-item';}}function mouseMove(e) {var e = e || window.event;mousePoses.push({x: pagePos(e).X,y: pagePos(e).Y});if (mousePoses.length > 3) {//把第一个元素删除 把前面的删除mousePoses.shift();}console.log(mousePoses);}//鼠标移出菜单function menuMouseOut() {oSub.className += ' hide';removeAllActive();removeEvent(document, 'mousemove', mouseMove);}function doTimeout(lastPos, curPos) {var topLeft = {x: getStyles(oMenu, 'width') + getStyles(oMenu, 'margin-left'),y: getStyles(oMenu, 'margin-top')};var bottomLeft = {x: getStyles(oMenu, 'width') + getStyles(oMenu, 'margin-left'),y: getStyles(oMenu, 'margin-top') + getStyles(oSub, 'height')}return pointInTriangle(curPos, lastPos, topLeft, bottomLeft);}});init();
案例三:电商网站商品图片放大镜效果
.img-wrap {position: relative;width: 390px;height: 600px;margin: 100px auto;border: 1px solid #ddd;box-shadow: 0 0 5px #999;}.img-wrap .mag-wrap {display: none;position: absolute;top: 0;left: 0;width: 150px;height: 150px;background-color: #fff;box-shadow: 0 0 3px #ccc;overflow: hidden;}.img-wrap .mag-wrap.show {display: block;transform: scale(1.5);}.img-wrap .mag-wrap .mag-img {position: absolute;top: 0;left: 0;width: 488px;height: 750px;}.img-wrap .static-img {height: 100%;}
<div class="img-wrap"><div class="mag-wrap"><img src="./img/1.jpg" class="mag-img" alt=""></div><a href="javascript:;" class="img-lk"><img src="./img/1.jpg" class="static-img" alt=""></a></div>
window.onload = function () {init();}function init() {initMagifier();}var initMagifier = (function () {var oImgWrap = document.getElementsByClassName('img-wrap')[0],oMagWrap = oImgWrap.getElementsByClassName('mag-wrap')[0],oMagImg = oMagWrap.getElementsByClassName('mag-img')[0],//放大盒子正方形宽高magWidth = getStyles(oMagWrap, 'width'), //150magHeight = getStyles(oMagWrap, 'height'), //150//imgX原图左边离页面左边的距离imgX = oImgWrap.offsetLeft,//imgY原图上边离页面上边的距离imgY = oImgWrap.offsetTop;//原图大盒子绑定鼠标经过事件addEvent(oImgWrap, 'mouseover', function (e) {/*** 原理:* 鼠标移动到图片上面小盒子跟着走* 鼠标小手正处于小盒子一半的位置* 每走一步都要计算出小盒子左上坐标的top/left值* 即计算出小盒子左侧到原图盒子左侧的距离,小盒子上侧到原图盒子上侧的距离* 小盒子左侧到原图盒子左侧的距离 = pageLeft - offsetLeft - 小盒子中心位置到小盒子左侧的距离即 oMagWrap/2* 小盒子上侧到原图盒子上侧的距离 = pageTop - offsetTop - 小盒子中心位置到小盒子上侧的距离即 oMagWrap/2** 边缘问题:* 鼠标盒子移出边框外会消失* 鼠标目前位置(鼠标到边缘距离) = 页面坐标 - offsetLeft/Top* 鼠标到边缘距离 < 0 或者 > 原图的宽度/高度 都为超出边界*/showMag(getXY(e).x,getXY(e).y);//步骤1://让正方形显示出来oMagWrap.className += ' show';//里面有鼠标移动事件 跟document搭配使用addEvent(document, 'mousemove', mouseMove)})//原图大盒子绑定鼠标离开事件addEvent(oImgWrap, 'mouseout', mouseOut);//每移动一步也同样操作function mouseMove(e) {showMag(getXY(e).x,getXY(e).y,getXY(e).mouseX,getXY(e).mouseY);}function mouseOut(e) {oMagWrap.className = 'mag-wrap';//解绑事件removeEvent(document, 'mousemove', mouseMove)}//抽象:function getXY(e) {var e = e || window.event;return {//小盒子左上的x,y坐标x: pagePos(e).X - imgX - magWidth / 2,y: pagePos(e).Y - imgY - magHeight / 2,//鼠标到边框距离mouseX: pagePos(e).X - imgX,mouseY: pagePos(e).Y - imgY}}//抽象:function showMag(x, y, mouseX, mouseY) {//步骤2://给正方形设置top/left值//可以让小盒子移动oMagWrap.style.left = x + 'px';oMagWrap.style.top = y + 'px';//步骤3://让小盒子里面的图片重新赋值//小盒子往右下移动 里面的图片往左上移动(负数)oMagImg.style.left = -x + 'px';oMagImg.style.top = -y + 'px';//这两参数有传值就执行下面代码if (mouseX && mouseY) {//步骤4 判断鼠标距离是否超出边框//小盒子移出到边框外会消失if (mouseX < 0 ||mouseX > getStyles(oImgWrap, 'width') ||mouseY < 0 ||mouseY > getStyles(oImgWrap, 'height')) {oMagWrap.className = 'mag-wrap';}}}});
键盘事件
贪食蛇
原理:数组存放x/y坐标的形式且对数组进行操作
.wrap {position: relative;width: 500px;height: 500px;margin: 50px auto;background-color: #000;overflow: hidden;}.round {display: block;position: absolute;width: 20px;height: 20px;border-radius: 50%;background-color: green;}.round.head {background-color: red;}
<div class="wrap"></div>
/*** 案例:贪吃蛇* 原理:操作存放对象为x/y坐标的数组,并操作数组* -问题1:如何显示有一条蛇(初始为6个小圆圈竖向并列)?* 数组应显示:* arr = [{x:0,y:0},{x:0,y:20},{x:0,y:30},{x:0,y:40},{x:0,y:50},{x:0,y:60}]* -问题2:如何让蛇移动起来?* 数组最后的一个的坐标是倒数第二个的坐标* -问题3:如何实现改变蛇移动的方向?* 如果蛇往下走,此时不能往上或往下走*/window.onload = function () {init();}function init() {initGame();}var initGame = (function () {var wrap = document.getElementsByClassName('wrap')[0],t = null;//画一条蛇var Snake = function () {this.bodyArr = [{x: 0,y: 0},{x: 0,y: 20},{x: 0,y: 40},{x: 0,y: 60},{x: 0,y: 80},{x: 0,y: 100}];//初始化方向状态this.dir = 'DOWN';}Snake.prototype = {init: function () {this.initSnake();this.bindEvent();//运行程序this.run();},bindEvent: function () {var _self = this;//绑定键盘按下事件addEvent(document, 'keydown', function () {_self.changeDir();});},//初始化一条蛇initSnake: function () {var arr = this.bodyArr,len = arr.length,frag = document.createDocumentFragment(),item;//循环创建小圆点for (var i = 0; i < len; i++) {item = arr[i];//新增小圆点var round = document.createElement('i');//新增类名round.className = i === len - 1 ? 'round head' : 'round';//新增坐标round.style.left = item.x + 'px';round.style.top = item.y + 'px';//存入碎片容器frag.append(round);}//将碎片容器存入wrap盒子里wrap.append(frag);},//程序运行run: function () {var _self = this;t = setInterval(function () {_self.move();}, 500)},//让小蛇移动起来move: function () {var arr = this.bodyArr,len = arr.length,head = arr[len - 1];//循环:把每一个坐标项移动一项for (var i = 0; i < len; i++) {//证明是头部if (i === len - 1) {//判断方向再移动//改变头部项的坐标//往下走switch (this.dir) {case 'LEFT':head.x -= 20;break;case 'RIGHT':head.x += 20;break;case 'UP':head.y -= 20;break;case 'DOWN':head.y += 20;break;default:break;}} else {//证明不为头部//数组最后的一个的坐标是倒数第二个的坐标arr[i].x = arr[i + 1].x;arr[i].y = arr[i + 1].y;}}this.removeSnake();//新的坐标系去绘制this.initSnake();},//绘制之前删除蛇removeSnake: function () {//选出所有的小圆点var bodys = document.getElementsByClassName('round');//一直循环一直删除第一项的小圆点//每次删除一个就会有新的第一项//直到把bodys里面的每一项都删除干净了 0 !> 0 终止循环while (bodys.length > 0) {//移除第一个小圆点bodys[0].remove();}},//改变移动方向changeDir: function (e) {var e = e || window.event,//获取keyCodecode = e.keyCode; //UP/DOWN/LEFT/RIGHTthis.setDir(code);},//重新定义移动方向setDir: function (code) {switch (code) {//按左按键往左走case 37:// 如果蛇已经往左走,此时不能再按往左或往右走if (this.dir !== 'RIGHT' && this.dir !== 'LEFT') {this.dir = 'LEFT';}break;//按右按键往右走case 39:// 如果蛇已经往右走,此时不能再按往右或往左走if (this.dir !== 'RIGHT' && this.dir !== 'LEFT') {this.dir = 'RIGHT';}break;//按上按键往上走case 38:// 如果蛇已经往上走,此时不能再按往上或往下走if (this.dir !== 'UP' && this.dir !== 'DOWN') {this.dir = 'UP';}break;//按下按键往下走case 40:// 如果蛇已经往下走,此时不能再按往上或往下走if (this.dir !== 'UP' && this.dir !== 'DOWN') {this.dir = 'DOWN';}break;default:break;}}}return new Snake().init();})
动态渲染
根据模拟后台的JSON数据来动态渲染页面
里面有两种写法:
- 循环字符串替换
- 正则匹配内容替换
源码地址:https://gitee.com/kevinleeeee/render-pageby-jsondemo01
缓存池
案例:点击页码渲染课堂列表
根据Ajax请求后台接口把页码数据渲染到页面
含有缓存池机制
源码地址:https://gitee.com/kevinleeeee/buffer-pool-demo
网络请求
案例:腾讯课堂评论模块
利用AJAX技术写评论列表
基本功能:
- 有数据显示列表没有的话显示暂无评论
- 评论框有5星好评并写评论(如有空格忽略)
- 评论列表显示评论条数数据
- 根据评论星星实时计算出好评度
- 实现分页切换评论列表
服务器配置:
- 安装WAMP保持绿色图标运行
- api_for_study, comments, ThinkPHP 保持在wamp/www目录下
- 把study.sql数据表导入到数据库后台
服务器搭建后台管理页面:
企业级模块化写法
- 评论模块里是方法的集合
js > module > index > comment_module.js
项目源代码:https://gitee.com/kevinleeeee/tx-course-comment-demo
跨域
案例:JSONP跨域之豆瓣网音乐搜索
//https://api.douban.com/v2/music/search?q=关键字&callback=回调函数&count=返回条数;(function(doc){var oInput = doc.getElementById('J_searchInput'),searchItemTpl = doc.getElementById('J_searchItemTpl').innerHTML,oList = doc.getElementsByClassName('J_list')[0],musicCount = 7;var init = function(){bindEvent();}function bindEvent(){oInput.addEventListener('input', debounce(musicSearch, 500, false), false);}function showList(show){if(show){oList.style.display = 'block';}else{oList.innerHTML = '';oList.style.display = 'none';}}function renderList(data){var list = '';data.forEach(function(elem){list += searchItemTpl.replace(/{{(.*?)}}/g, function(node, key){return {url: elem.alt,image: elem.image,title: elem.alt_title || elem.attrs.title[0],singer: elem.attrs.singer ? elem.attrs.singer[0] : ''}[key];});});oList.innerHTML = list;showList(true);}function musicSearch(){var kw = trimSpace(this.value),len = kw.length;if(len > 0){dataRequest(kw, musicCount);}else{showList(false);}}function dataRequest(keyword, musicCount){xhr.ajax({url: 'https://api.douban.com/v2/music/search?q=' + keyword + '&count=' + musicCount,type: 'GET',dataType: 'JSONP',jsonp: 'callback',success: function(data){if(data && data.musics.length > 0){renderList(data.musics);}}})}init();})(document);
cookie
案例:登录模态框填写用户名,密码,30天数据保存,登入登出
技术:cookie + token 持久登录
服务器操作:
检查用户名/密码的合法性如不符合报错
- 1001:不合法的用户名长度
- 1002:不合法的密码长度
验证数据库
- 加密用户名/密码
判断用户名是否存在于数据库表里
- 真:继续往下执行
- 假:1003错误,用户名不存在
校验数据库表里的加密密码与填写的密码
- 一样:继续往下执行
- 不一样:1004错误,密码错误
- 将用户名加密(md5 + salt随机字符串) 生成身份识别码
ident_code - 重新生成
token身份令牌(32位A-Za-z0-9随机字符串) 根据填写保存30天字段和生成时间戳生成新的持久登录时间戳
- 是持久登录:30天
- 不是持久登录:1天
将
ident_code,token,timeout存入数据库表里- 如果保存成功继续往下执行
- 如果网络不好没有保存成功返回错误1005
将成功存到数据库里的
ident_code,token,timeout字段数据拼接组成新的cookie信息- 若成功,返回200,且重新加载页面,并重新检查权限
检验已存在的cookie里面的auth信息
- 冒号分割
auth为ident_code和token字段 分别校验新旧
ident_code和token字段信息- 找不到,报错1006,token值不存在
- 找到了,继续往下执行程序
校验
timeout字段看是否过期- 过期了,报错1007
- 登录成功,验证通过
- 冒号分割
源码地址:https://gitee.com/kevinleeeee/web-login-cookie-token-demo
文件上传
案例1:上传图片显示进度条(可多个同时)
ul {padding: 0;margin: 0;list-style: none;margin-top: 100px;}.progress-bar {width: 300px;height: 40px;border: 1px solid #666;text-align: center;}.progress {width: 0%;height: 100%;background-color: green;}.error-info {line-height: 40px;font-size: 14px;color: #333;}
<!-- multiple:支持多文件上传 --><input type="file" id="file" multiple /><input type="submit" id="submitBtn" value="上传" /><ul class="progress-wrap"><li class="progress-bar"><!-- <div class="progress"></div> --><span class="error-info">文件类型错误</span></li></ul>
//选择文件 输入框var oFile = document.getElementById('file'),//上传 按钮oSubmitBtn = document.getElementById('submitBtn'),//进度条包装盒子oProgressWrap = document.getElementsByClassName('progress-wrap')[0];//输入框变化事件//files属性对应file列表/*** console.log(oFile.files):* FileList {0: File, 1: File, length: 2}* 0: File* lastModified: 1625059303199* lastModifiedDate: Wed Jun 30 2021 21:21:43 GMT+0800 (中国标准时间) {}* name: "1.jpeg"* size: 8396* type: "image/jpeg"* webkitRelativePath: ""* __proto__: File* 1: File {name: "1.jpg", lastModified: 1624970943554 …}* length: 2* __proto__: FileList*/oFile.onchange = function () {var files = oFile.files,fileLen = files.length,fileName = '',fileSize = 0,//1M = 1024 * 1024maxSize = 1048576,//fd = new FormDatafd = null,errorInfo = '';if (fileLen <= 0) {console.log('您还没有选择图片');return;}if (fileLen > 5) {console.log('最多可同时上传5张图片');return;}for (var i = 0; i < fileLen; i++) {fileName = files[i].name;fileSize = files[i].size;//判断是否是想要的后缀名称//转义. 且以gif,jpg,jpeg,png结尾的// console.log(/\.(gif|jpg|jpeg|png)$/.test(fileName)); //trueif (!/\.(gif|jpg|jpeg|png)$/.test(fileName)) {errorInfo = fileName + '文件不是图片类型';}//判断是否超出上传内存大小if (fileSize > maxSize) {errorInfo = fileName + '超过可上传大小';}//动态创建livar oProgressBar = document.createElement('li');oProgressBar.className = 'progress-bar';//插入li到uloProgressWrap.appendChild(oProgressBar);//不符合上传条件if (errorInfo !== '') {oProgressBar.innerHTML = '<span class="error-info">' + errorInfo + '</span>';} else {//符合上传条件oProgressBar.innerHTML = '<div class="progress"></div>';//实例化fd = new FormData();//这里file为后端提供字段//append(属性名,属性值)fd.append('file', files[i]);//通过Ajax请求数据var o = window.XMLHttpRequest ? new Window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');o.open('post', 'server/upload.php');//闭包情况(function (j) {//upload属性里自带的onprogress事件o.upload.onprogress = function (e) {var e = e || window.event,//此时事件源e 里面有 loaded属性:加载完的数据大小 total属性:总数据大小percent = e.loaded / e.total * 100 + '%',//当前lithisProgressBar = oProgressWrap.getElementsByClassName('progress-bar')[j];//当前progress盒子显示的宽度根据百分比计算出来的宽度显示thisProgressBar.getElementsByClassName('progress')[0].style.width = percent;}})(i);o.send(fd);}}}
案例2:腾讯课堂上传文件
上传视频显示百分比
源码地址:https://gitee.com/kevinleeeee/file-upload-demo
AJAX
案例1:腾讯课堂列表后台管理系统
- 功能一:列表分类
- 功能二:带垃圾箱恢复删除数据的操作
- 功能三:课程搜索
- 功能四:点击课程名称显示输入框并可以修改数据
- 功能五:点击删除/恢复操作
源码地址:https://gitee.com/kevinleeeee/txcourses-management-system-ajax-demo
案例2:瀑布流
- 原理demo
- 插件化ajax请求开发
后端数据:
- 如果是瀑布流图片,JSON数据里必须带宽高数据
原理:
- 确定第一行的排列(每行5张)
- 确保5张图片的盒子的高度到数组
- 第六张图片开始照最小盒子高度的那一列排(通过第一行保存的数组)
- 排一张图片,修改一次数组相应列的下标
- 找到盒子的左边缘到页面左侧的距离和最小盒子高度确定定位
问题:
- 如何找到每次最短的高度的图片?
源码地址:https://gitee.com/kevinleeeee/water-fall-demo
模块化
案例:购物车功能实战
关键词:购物车,打包,webpack
技术:webpack + es6module + scss
实现功能:
获取列表展示首页列表
详情页显示商品详情内容
详情页点击添加购物车显示toast框信息
购物车管理
计算总价
立即购买(录入数据库)
清空购物车
全选/单选
全选/单选总价实时变更
删除商品
源码地址:https://gitee.com/kevinleeeee/shopping-cart-es6
Vue
去哪儿案例
关键词:移动端, nodejs, git, npm
技术:vue + vuex + axios + router + stylus
关于stylus:css预处理器,在原有css基础上继承样式,样式嵌套
//stylus安装:npm i -D stylus-loader@3.0.2npm i -D stylus@0.54.5//stylus使用:<style lang="stylus" scoped>.message {padding: 10px;border: 1px solid #eee;}.warning {@extend .message;color: #e2e2e2;}</style>
关于swiper:封装好的轮播图插件
npm i -S vue-awesome-swiper@2.6.7
关于better-scroll:滚动条插件,实现滚动效果
npm i -S better-scroll@1.15.2//使用//引入import BScroll from 'better-scroll';
源码地址:https://gitee.com/kevinleeeee/qunaer
城市一线通(仿美团)案例
关键词:移动端, es6. flex布局
技术:
- 项目架构/组件结构设计
- 项目抽象及复用/封装与配置
- 模型层抽象与应用
- 工具函数集合抽象与应用
- 数据请求与格式化与缓存
- Vuex中央状态管理器
- 组件缓存机制/单页面路由/组件间传值(父子/兄弟)
项目目录:
assets - 静态文件conponents - 组件文件models - 模型文件pages - 页面组件router - 路由设置文件store - 中央状态管理utils - 配置与工具App.vue - 项目根组件main.js - 项目入口文件
关于
better-scroll插件
针对移动端上下滑动页面的动画效果
//如何使用better-scroll?//安装npm i -S better-scroll@1.15.2//在组件里引入插件import BetterScroll from "better-scroll";//被操作dom写入ref标签属性<div class="scroll-wrapper" ref="wrapper">...</div>//实例化插件mounted:function(){this.scroll = new BetterScroll(this.$refs.wrapper);}
组件管理:
所有的组件都会汇入到App.vue组件,通过main.js导入注册组件, 管理页面与渲染,component存放嵌套关系的子组件
如何实现组件复用和拓展?
答:拆分组件
components:- Header- Sub- BackWard.vue- CitySellector.vue- FalseInput.vue- Common.vue- Home.vue- Tab- Index.vue- Home.vue- SearchInput- Index.vue- ScrollWrapper- CategoryIcon- Index.vue- Sub.vue- CityList- CurrentCity.vue- Index.vue- Sub.vue- FoodList- Index.vue- Sub.vue- HotelList- Index.vue- Sub.vue- KtvList- Index.vue- Sub.vue- MassageList- Index.vue- Sub.vue- ViewList- Index.vue- Sub.vue- Sub- Error.vue- HomeTitle.vue- Loading.vue- NoDataTip.vue- Stars.vue- Swiper.vue- Detail- Sub- Address.vue- CommentKeyword.vue- Intro.vue- Name.vue- OpenDateTime.vue- Price.vue- Recom.vue- Service.vue- Stars.vue- TicketInfo.vue- Tip.vue- Title.vue- Food.vue- Hotel.vue- Ktv.vue- Massage.vue- View.vue- City.vue- Detail.vue- Home.vue- List.vue- Search.vue
组件抽离原则:
- 可复用的组件
- 可配置的项,在组件内部要抽离属性(放入props里)
- 前瞻性,用前瞻的眼光抽离组件
- 有独立的功能性的组件
- 组件集合一定要有结构
页面组件pages 与 组件文件components关系?
页面组件所需要被拆分的components组件(结构化),组件拆分便于维护,迭代,功能拓展
项目页面导图(组件结构):
- 首页- 专用header- 城市选择器- 假输入框- 滚动区域- 图标列表- 子图标项- 标题- 各分类列表- 错误提示- 列表页- 通用header- 页面回退- 选项导航- 子选项- 滚动区域- 各分类列表- Loading- 详情页- 通用header- 页面回退- 滚动区域- 各分类- 轮播图/地址/评论关键词/简介/名称- 营业时间/价格/推荐/星级/购票信息/商家服务- 城市选择页- 通用header- 页面回退- 滚动区域- 当前城市- 城市列表- 搜索页- 通用header- 页面回退- 搜索输入框- 滚动区域- 各分类列表- 错误提示- 无结果提示- Loading
组件缓存:
切换城市会存在再次请求数据的不合理时候,只有城市数据有变化的时候才重新请求数据
解决方法:给App.vue里根组件挂载的地方用keep-alive标签包裹着
<div id="app"><keep-alive><router-view /></keep-alive></div>
keep-alive是什么?
vue里内置组件,也是一个抽象组件,不在DOM里占用结构的,具有自己的生命周期activated和deactivated
判断当前城市的状态是否被更改(是否被切换)会触发该生命函数
通过当前城市id和被选择城市id对比是否一致
include标签属性用法:
<keep-alive include="被监听的组件">
若已经被缓存时,程序不会再走mounted生命周期函数,但是会保持activated生命周期函数的程序触发,说明组件都被缓存不再经过装载的过程
关于轮播图插件
vue-awesome-swiper
//安装npm i -S vue-awesome-swiper@3.1.3//引入组件和样式import { swiper, swiperSlide } from "vue-awesome-swiper";import "swiper/dist/css/swiper.css";//注册组件components:{swiper:swiper,swiperSlide:swiperSlide}//标签固定写法<div class="swiper-wrap"><swiper :options="swiperOption"><swiper-slide v-for="(item, index) of picDatas" :key="index">//自定义内容区域</swiper-slide><div class="swiper-pagination" slot="pagination"></div></swiper></div>
兄弟组件如何传值?
watch监听数据变化就执行数据请求
源码地址:https://gitee.com/kevinleeeee/vue-yixiantong-demo
MVVM
手写MVVM案例
实现:
- 模板编译
- 数据劫持
- 发布订阅
- 双向绑定
源码地址:https://gitee.com/kevinleeeee/vue-mvvm-dep-demo
Vue3
案例:万年历
技术:vue3.0
功能实现:
- 首页输入框搜索日期显示该日期的日历详细描述
- 输入日期显示最近日期的假期列表
- 输入日期查询当年的假期节日
后台接口:https://www.juhe.cn/docs/api/id/177
//初始化npm i -g @vue/cli@4.4.1//创建项目vue create calendar_pro//安装依赖npm i -D vue-cli-plugin-vue-next@0.1.3npm i -D @vue/compiler-sfc@3.0.0
解决跨域设置:
//vue.config.jsmodule.exports = {devServer: {//关闭eslint警告overlay: {warnings: false,errors: false},//代理解决跨域proxy: {'/api': {target: 'http://v.juhe.cn/',//改变源changeOrigin: true,//开启websocketws: true,//关闭https检查secure: false,//重写路径pathRewrite: {'^/api': ''}}}},lintOnSave: false}
keep alive 针对vue3写法:
<router-view v-slot="{Component}"><keep-alive><component :is="Component" /></keep-alive></router-view>
源码地址:https://gitee.com/kevinleeeee/vue3-wannianli-demo
工程化
案例:小米网页版官网
项目思想:
- 组件类的方式开发项目
- 模块抽离
- 父子/公共组件类传参
技术:JavaScript原生 + webpack + 组件化
组件结构:
首页
- 页眉
- 轮播图
- 展示面板
- 页尾
- 列表页
- 详情页
源码地址:https://gitee.com/kevinleeeee/xiaomi-web-project
koa
美团网页版项目
技术:vue + koa2 + ElementUI + Redis + MongoDB + SSR + NuxtJS
项目创建
npx create-nuxt-app@2 meituan-demo
项目选择配置
UI: elementserver: koamodules: axiosrender: universal(ssr)
启动项目
npm run dev//访问http://localhost:3000/
目录结构
├─nuxt.config.js -配置文件├─package-lock.json├─package.json├─README.md├─store -管理vuex状态├─static -管理图标(非必须)| ├─favicon.ico├─server -管理服务端代码| └index.js├─plugins -管理插件| ├─element-ui.js├─pages| ├─about.vue| ├─index.vue├─middleware -管理中间件├─layouts -管理默认模板| ├─default.vue├─components -管理组件| ├─Logo.vue├─assets -管理静态资源文件| ├─css
创建.babelrc配置文件
//配置babel{"presets": ["es2015"]}
安装babel处理es6语法
npm i -D babel-preset-es2015@6.24.1npm i -D babel-cli@6.26.0npm i -D babel-core@6.26.3
安装scss处理器
npm i -D sass-loader@7.1.0npm i -D node-sass@4.11.0
配置nuxt.config.js
//引入初始化css文件css: ['element-ui/lib/theme-chalk/reset.css','element-ui/lib/theme-chalk/index.css','@/assets/css/main.css'],
编写后台接口
//server目录新建dbs数据库目录//dbs数据库目录里创建models模型目录//server > dbs > models模型目录创建users.js文件//server > dbs目录下创建config.js配置文件(数据库相关信息)//server目录新建interface接口目录//interface目录新建utils工具目录//server > interface新建users.js文件//server > interface > utils新建axios.js文件(封装全局使用axios)//server > interface > utils新建passport.js文件(验证相关权限)
//POP3邮箱开启授权码adgkisdouidsbhda//IMAP/SMTP服务授权码pthqrkenxqzdbhji
//server > dbs > config.js//数据库相关信息export default {dbs: 'mongodb://127.0.0.1:27017/meituan/users',redis: {//主机地址,只读get host() {return '127.0.0.1';},get port() {//端口号return 6379;}},//腾讯提供的服务 邮箱smtp: {get host() {return 'smtp.qq.com'},get user() {return '273122188@qq.com'},get pass() {return '保密字符';},get code() {return () => {//返回随机16进制字符return Math.random().toString(16).slice(2, 6).toUpperCase();}},//过期时间get expire() {return () => {return new Date().getTime() + 60 * 1000}}}}
关于
nodemailer:是一个用于 Node.js 应用程序的模块(中间件),它轻松地发送电子邮件
//安装nodemailernpm i -S nodemailer@6.1.0//引入import nodeMailer from 'nodemailer';//接口内部使用let transporter = nodeMailer.createTransport({host: EmailConfig.smtp.host,port: 587,secure: false,auth: {user: EmailConfig.smtp.user,pass: EmailConfig.smtp.pass,},});
//安装依赖npm i -S koa-redis@3.1.3npm i -S koa-router@7.4.0npm i -S axios@0.18.0npm i -S koa-generic-session@2.0.1npm i -S koa-bodyparser@4.2.1npm i -S koa-json@2.0.2npm i -S koa-redis@3.1.2npm i -S mongoose@5.5.2npm i -S koa-passport@4.1.1npm i -S passport-local@1.0.0
//编写后台接口//server > interface > users.js
关于
PassportJshttp://www.passportjs.org/docs/
Node.js 的身份验证中间件。Passport 非常灵活和模块化,可以不显眼地插入任何 基于Express的 Web 应用程序。一套全面策略支持认证使用的用户名和密码
关于
CryptoJS密码加密的前端库
//安装npm i -S crypto-js@3.1.9-1//引入import CryptoJS from 'crypto-js';//使用this.$axios.pist("/users/signup", {username: encodeURIComponent(this.ruleForm.name),password: CryptoJS.MD5(this.ruleForm.pwd).toString(),});
由于后端数据接口失效,请求功能不完善。
源码地址:https://gitee.com/kevinleeeee/meituan-web
观察者
案例:购物车(课程列表)
页面包含课程列表和待报名课程两个模块
实现:点击课程列表其中的项目后会与待报名课程模块立即会有联动(一个模块里有两个功能模块)
问题1:为什么需要两个子模块分开处理?
因为如果两个模块一起编写,在修改其中模块时会影响另一个模块,而且不方便维护,且逻辑之间杂乱不清
问题2:两个分开模块之间如何实现数据联动管理?
利用观察者模式实现管理数据更新
观察者原理:
每一个观察者实际上是一个函数,一旦触发事件,执行一个函数完成一个程序,当一个程序分为1,2,3件事情,那么这个程序就为一个observers,当执行程序123(observers)时不能单独执行,利用obsevers里面的notify通知特性,将123一次性执行,真正事件触发时仅仅执行的是notify函数
也就是说,希望123函数作为一个个obsevers放入数组里,统一用notify一次去执行,在项目里,点击单击事件时触发执行notify函数,然后notify里面所有的observers会依次执行
//Observers.jsobservers = [fn, fn, fn];add() -> fn -> observers//执行notify()会使每一项observer执行notify() -> forEach -> observers -> item();
项目渲染原理:
数据 (需要处理) -> 容器 -> 渲染 -> 插入节点
观察者管理的好处:
一个notify函数的执行同时 把handle底下的所有函数同时执行,实现参数实时共享 好处是模块功能同时联动,实现高内聚的代码编写
项目结构:
├─package.json├─webpack.config.js├─src| ├─index.html| ├─utils| | ├─Observer.js - 观察者类 驱动| | └tools.js - 工具类方法集合| ├─templates - 模板文件| | ├─cartItem.tpl| | └courseItem.tpl| ├─js| | ├─index.js - 网页入口文件| | ├─ShoppingCart| | | ├─index.js - 主程序入口文件| | | ├─Course| | | | ├─Event.js - 管理子模块的绑定事件| | | | ├─Handle.js - 管理子模块里绑定事件函数底下的所有逻辑方法| | | | ├─index.js - 子模块入口文件| | | | └Render.js - 渲染函数| | | ├─Cart| | | | ├─Event.js| | | | ├─Handle.js| | | | ├─index.js| | | | └Render.js| ├─data - 数据| | ├─cart.js| | └course.js
源码地址:https://gitee.com/kevinleeeee/data-response-observers-shoppingcart-demo
虚拟节点
虚拟节点和diff算法源码实现
//命名vNode -> virtual NodevnPatch -> virtual Node patchrNode -> real NodernNode -> real Node patch

功能:
- 构建虚拟节点
- 转换真实DOM
- 渲染DOM节点
- 创建补丁包
- 给真实DOM打补丁
//补丁格式:const patches = {0: [{//属性更改了type: 'ATTR',attrs: {class: 'list-wrapper'}}],2: [{type: 'ATTR',attrs: {class: 'title'}}],3: [{type: 'TEXT',text: '特殊项'}]6: [{type: 'REMOVE',index: 6}],7: [{type: 'REPLACE',newNode: newNode}]}
问题:如何对比新老DOM?
利用domDiff函数对比新老节点处理返回补丁包
//项目结构:├─package.json├─webpack.config.js├─src| ├─index.html| ├─js| | ├─domDiff.js - diff算法等函数| | ├─doPatch.js - 打补丁等函数| | ├─Element.js - 构造函数元素对象| | ├─index.js - 用户入口文件/模拟两个虚拟DOM函数/执行程序| | ├─patchTypes.js - 管理补丁名称类型| | └virtualDom.js - 创建虚拟DOM/将虚拟DOM转为真实DOM/设置属性/渲染页面函数├─public| └index.html
实现步骤:
- 用户写一个执行
createElement函数实例化之后的对象返回的结果vDom render函数把虚拟节点转换为真实DOM结构- 给每个真实节点的标签设置属性
- 给子元素节点再次递归
render渲染 - 给子文本节点创建文本
- 将子节点插入到父真实节点里
renderDOM函数把渲染后的真实DOM插入到根节点里实现渲染页面diffDOM函数对比两个虚拟的DOM,内部有私有属性index,内部执行vNodeWalk函数传入新老节点和index,函数最后返回一个补丁patches对象vNodeWalk函数- 定义一个数组容器
vnPacth装载补丁 - 当没有新节点时打入移除类型为
REMOVE和index的补丁 - 当节点类型是字符串时打入文本类型为
TEXT和文本内容的补丁 当标签名一样时
attrsWalk函数遍历标签里属性里的新老属性- 定义
attrPatch对象容器 - 当老的属性里的新老属性值不相同时把新的属性和属性值变为
attPatch容器对象 - 当新的属性里的老的属性值有自身的属性时把新的属性和属性值变为
attPatch容器对象 - 返回
attrPatch对象容器 - 将
attrPatch对象容器打入属性类型为ATTR和属性内容的补丁 childrenWalk深度遍历子节点
- 当替换标签名时打入属性类型为
REPLACE和新节点的补丁
- 定义一个数组容器
给真实DOM打补丁
doPatch函数传入真实DOM和patches补丁- 定义
finalPatches对象和rnIndex = 0 - 将
patches补丁赋值给finalPatches对象 执行
rNodeWalk函数传入真实节点- 每次取值
finalPatches[rnIndex++]时rnIndex加1 - 将真实节点的子节点的类数组转为数组并遍历每一个子节点
- 递归嵌套的子节点
reNodeWalk - 当有补丁时去打补丁
patchAction函数传入真实节点和真实节点的补丁 - 遍历每一个补丁
当补丁的类型为
ATTR时遍历拿到属性底下所有的属性和属性值- 如果有属性值时给真实节点设置属性
- 有属性但没有属性值时删除真实节点下的属性
- 当补丁的类型为
TEXT时将真实节点的文本内容填入补丁里的text内容 当补丁的类型为
REPLACE时- 如果新的节点是虚拟节点时将它
render渲染出来创建 - 如果新的节点不是虚拟节点时将它作为文本节点创建
- 将原来的节点替换为新的节点
- 如果新的节点是虚拟节点时将它
- 当补丁的类型为
REMOVE时删除自己的节点
- 每次取值
- 定义
用户使用的顺序:
- 拿到真实节点
const rDom = render(vDom1); - 对比新老节点返回补丁包
const patches = domDiff(vDom1, vDom2); - 渲染页面
renderDOM(rDom, document.getElementById('app')); - 给真实DOM打补丁
doPatch(rDom, patches);
源码地址:https://gitee.com/kevinleeeee/dom-diff-demo
路由权限
案例:后台路由权限管理
技术:koa2/vue/前后端

原理:
- 用户uid -> 后端API -> 路由权限API
- 后端 -> 用户对应路由权限列表 -> 前端 ->
JSON JSON-> 树型结构化- 树型结构化的数据 -> vue路由结构
- 路由结构动态 -> 静态路由
- 根据树型结构化的数据 -> 菜单组件
//JSON[{id: 2,//parent idpid: 3,path:name:link:title:}]
问题:如何做后端跨域?
npm i koa2-cors -S
//app.jsconst cors = require('koa2-cors');app.use(cors({origin: function (ctx) {return 'http://localhost:8080'}}));
前端项目顺序:
- 编写后台界面
- 请求后端接口
- 封装请求函数
- 将请求到的数据存入vuex
actions里异步获取后端数据- 将数据进行树型结构化格式化
mutations里定义方法存储state- 前端动态生成路由
- 将数据生成树状结构路由配置对象
配置路由守卫
beforeEach没有权限时:
- 请求后端数据
- 格式化后的树形结构的路由规则对象数组
addroute新增到路由列表实现动态添加路由- 编写各个地址的组件文件
- 给
next回调分支处理实现访问不同地址显示不同的页面
有权限时:
- 直接访问不做守卫拦截
- 编写SideBar组件视图根据路由渲染路由列表
- 实现点击路由导航名称跳转到路由页面显示路由组件
//注册一个全局前置守卫const router = new VueRouter({ ... })router.beforeEach((to, from, next) => {// 注意:确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。})
后端项目顺序:
- 编写用户表
- 编写路由数据表
- 遍历用户表和路由数据表,将符合条件的那一项放入容器返回给前端
//前端项目目录:├─package.json├─src| ├─App.vue - 根组件/管理布局组件| ├─main.js - 入口文件/请求路由数据/动态生成路由列表/拼接路由列表/路由守卫| ├─views - 各路由视图组件| | ├─Course.vue| | ├─CourseAdd.vue| | ├─CourseInfoData.vue| | ├─CourseOperate.vue| | ├─Home.vue| | ├─NotFound.vue| | ├─Student.vue| | ├─StudentAdd.vue| | └StudentOperate.vue| ├─store| | ├─actions.js - 定义请求路由列表数据函数/权限函数| | ├─index.js| | ├─mutations.js - 定义操作state的方法| | └state.js - 中央状态管理池/hasAuth/userRouters数组| ├─services| | └index.js - axios函数封装| ├─router| | └index.js - 路由入口文件| ├─libs| | └utils.js - 格式化路由列表为树形结构化/生成树状结构路由配置对象| ├─components - 页面布局组件| | ├─Header.vue| | ├─MenuItem.vue| | ├─PageBoard.vue| | └SideBar.vue| ├─assets| | ├─css| | | └common.css
前端源码地址:https://gitee.com/kevinleeeee/vue-router-admin-frondend-demo
后端源码地址:https://gitee.com/kevinleeeee/vue-router-admin-backend-demo
