案例

数据筛选

数据筛选案例1:tab栏切换筛选(全部课程/免费课程/收费课程)显示在页面

  1. <p class="J_nav">
  2. <a href="javascript:;" data-field="all" >全部课程</a>
  3. <a href="javascript:;" data-field="free" >免费课程</a>
  4. <a href="javascript:;" data-field="vip" >收费课程</a>
  5. </p>
  6. <ul class="J_list"></ul>
  7. <!-- 隐藏数据域 -->
  8. <div id="J_data" style="display: none;">
  9. [{"id": "1","course":"前端开发之企业级深度JavaScript特训课【JS++前端】","classes":"19","teacher":"小野","img":"ecmascript.jpg","is_free":"1","datetime":"1540454477","price":"0"},
  10. {"id": "2","course":"WEB前端工程师就业班之深度JS DOM+讲师辅导-第3期【JS++前端】","classes":"22","teacher":"小野","img":"dom.jpg","is_free":"0","datetime":"1540454477","price":"699"},
  11. {"id": "3","course":"前端开发之企业级深度HTML特训课【JS++前端】","classes":"3","teacher":"小野","img":"html.jpg","is_free":"1","datetime":"1540454477","price":"0"},
  12. {"id": "4","course":"前端开发之企业级深度CSS特训课【JS++前端】","classes":"5","teacher":"小野","img":"css.jpg","is_free":"1","datetime":"1540454477","price":"0"},
  13. {"id": "5","course":"前端就业班VueJS+去哪儿网+源码课+讲师辅导-第3期【JS++前端】","classes":"50","teacher":"哈默","img":"vuejs.jpg","is_free":"0","datetime":"1540454477","price":"1280"},
  14. {"id": "6","course":"前端就业班ReactJS+新闻头条实战+讲师辅导-第3期【JS++前端】","classes":"21","teacher":"托尼","img":"reactjs.jpg","is_free":"0","datetime":"1540454477","price":"2180"},
  15. {"id": "7","course":"WEB前端开发工程师就业班-直播/录播+就业辅导-第3期【JS++前端】","classes":"700","teacher":"JS++名师团","img":"jiuyeban.jpg","is_free":"0","datetime":"1540454477","price":"4980"}]
  16. </div>
  17. <!-- tpl模板 -->
  18. <script type="text/html" id="J_tpl" >
  19. <li>{{course}}</li><hr />
  20. </script>
  1. //数据筛选案例:tab栏切换筛选(全部课程/免费课程/收费课程)显示在页面
  2. (function () {
  3. //获取nav节点,ul-list节点,data数据, tpl模板节点
  4. var oNav = document.getElementsByClassName("J_nav")[0],
  5. oList = document.getElementsByClassName("J_list")[0],
  6. data = JSON.parse(document.getElementById("J_data").innerHTML),
  7. tpl = document.getElementById("J_tpl").innerHTML;
  8. //初始化案例
  9. var init = function () {
  10. var all = filterData(data, "all");
  11. console.log(all);
  12. //首页页面加载
  13. // oList.innerHTML = renderList(all);
  14. bindEvent();
  15. };
  16. //绑定事件管理
  17. function bindEvent() {
  18. //委托代理
  19. oNav.addEventListener("click", navClick, false);
  20. }
  21. //定义点击导航事件处理函数
  22. function navClick(e) {
  23. var e = e || window.event,
  24. tar = e.target || e.srcElement,
  25. //事件源:被点击的标签a
  26. tagName = tar.tagName.toLowerCase();
  27. // console.log(tagName); //a
  28. if (tagName === "a") {
  29. var field = tar.getAttribute("data-field");
  30. // console.log(field); //拿到自定义属性 all/free/vip
  31. //测试筛选数据
  32. // console.log(filterData(data, field));
  33. //把修改后的数据给渲染到页面
  34. oList.innerHTML = renderList(filterData(data, field));
  35. }
  36. }
  37. //定义数据筛选函数
  38. function filterData(data, field) {
  39. //使用自己封装的myFilter方法
  40. var arr = data.myFilter(function (elem, index, arr) {
  41. switch (field) {
  42. case "all":
  43. //不需要过滤
  44. return true;
  45. break;
  46. case "free":
  47. return elem.is_free === "1";
  48. break;
  49. case "vip":
  50. return elem.is_free === "0";
  51. break;
  52. default:
  53. return true;
  54. }
  55. });
  56. return arr;
  57. }
  58. //模板渲染
  59. function renderList(data) {
  60. var list = "";
  61. data.forEach(function (elem, index, arr) {
  62. list += tpl.replace(/{{(.*?)}}/g, function (node, key) {
  63. return {
  64. course: elem.course,
  65. }[key];
  66. });
  67. });
  68. return list;
  69. }
  70. init();
  71. })();
  72. //封装myFilter
  73. Array.prototype.myFilter = function (fn) {
  74. var arr = this,
  75. len = arr.length,
  76. arg2 = arguments[1] || window,
  77. _newArr = [],
  78. item;
  79. for (var i = 0; i < len; i++) {
  80. item = deepClone(arr[i]);
  81. fn.apply(arg2, [arr[i], i, arr]) ? _newArr.push(item) : "";
  82. }
  83. return _newArr;
  84. };
  85. //利用递归克隆函数进行再次循环
  86. function deepClone(origin, target) {
  87. var target = target || {},
  88. toStr = Object.prototype.toString,
  89. arrType = "[object Array]";
  90. for (var key in origin) {
  91. if (origin.hasOwnProperty(key)) {
  92. if (typeof origin[key] === "object" && origin[key] !== null) {
  93. if (toStr.call(origin[key]) === arrType) {
  94. target[key] = [];
  95. } else {
  96. target[key] = {};
  97. }
  98. deepClone(origin[key], target[key]);
  99. } else {
  100. target[key] = origin[key];
  101. }
  102. }
  103. }
  104. return target;
  105. }

数据筛选案例2:利用reduce函数实现搜索框输入内容返回匹配到的搜索结果

  1. <input type="text" id="J_searchInput" placeholder="搜索课程">
  2. <ul class="J_list">
  3. <ul class="J_list">
  4. <span>- 暂无数据 -</span>
  5. </ul>

课程数据

  1. /**
  2. * 案例:搜索课程
  3. */
  4. ;
  5. (function (doc, data) {
  6. //获取节点元素
  7. var oInput = doc.getElementById('J_searchInput'),
  8. oList = doc.getElementsByClassName('J_list')[0];
  9. function init() {
  10. bindEvent();
  11. }
  12. function bindEvent() {
  13. oInput.addEventListener('input', searchInput, false);
  14. }
  15. function searchInput() {
  16. var val = this.value,
  17. len = val.length;
  18. if (len > 0) {
  19. if (makeList(searchData(data, val))) {
  20. oList.innerHTML = makeList(searchData(data, val));
  21. } else {
  22. oList.innerHTML = '<span>- 暂无数据 -</span>';
  23. }
  24. } else {
  25. oList.innerHTML = '<span>- 暂无数据 -</span>';
  26. }
  27. }
  28. /**
  29. * 处理数据函数-筛选数据
  30. *
  31. * @data 后台数据
  32. * @keyword input关键字
  33. * @返回值 如果输入内容存在数据字段里就筛选出来并push到数组里
  34. *
  35. */
  36. function searchData(data, keyword) {
  37. return data.reduce(function (prev, elem) {
  38. //筛选操作
  39. var res = elem.course.indexOf(keyword);
  40. //该字段数据存在
  41. if (res !== -1) {
  42. prev.push(elem);
  43. }
  44. return prev;
  45. }, []);
  46. }
  47. /**
  48. * 字符串拼接list列表
  49. * @matchData 筛选整理好的数据
  50. */
  51. function makeList(matchData) {
  52. var list = '';
  53. //没有数据什么都不做
  54. if (!matchData || matchData.length <= 0) {
  55. return false;
  56. } else {
  57. matchData.forEach(function (item) {
  58. list += '<li>' + item.course + '</li>';
  59. })
  60. }
  61. return list;
  62. }
  63. init();
  64. })(document, courseData);

JS运动

案例:鼠标移入下拉菜单动态下滑列表

  1. a {
  2. text-decoration: none;
  3. }
  4. ul {
  5. padding: 0;
  6. margin: 0;
  7. list-style: none;
  8. }
  9. .dropdown {
  10. position: relative;
  11. width: 200px;
  12. height: 50px;
  13. background-color: #000;
  14. }
  15. .dropdown::after {
  16. content: '';
  17. display: table;
  18. position: absolute;
  19. top: 18px;
  20. right: 15px;
  21. width: 15px;
  22. height: 15px;
  23. background-image: url(icons/arrow-down.png);
  24. background-size: 100% 100%;
  25. background-repeat: no-repeat;
  26. }
  27. .dropdown.up::after {
  28. background-image: url(icons/arrow-top.png);
  29. }
  30. .dropdown .list {
  31. height: 0px;
  32. overflow: hidden;
  33. }
  34. .dropdown a {
  35. display: block;
  36. }
  37. .dropdown .main {
  38. height: 100%;
  39. text-align: center;
  40. line-height: 50px;
  41. color: #fff;
  42. }
  43. .dropdown .item {
  44. height: 40px;
  45. background-color: #333;
  46. }
  47. .dropdown .item:hover {
  48. background-color: #000;
  49. }
  50. .dropdown .item a {
  51. height: 100%;
  52. color: #999;
  53. text-align: center;
  54. line-height: 40px;
  55. }
  1. <div class="dropdown">
  2. <a href="javascript:;" class="main">下拉菜单</a>
  3. <ul class="list">
  4. <li class="item"><a href=""></a> 第1个项目</li>
  5. <li class="item"><a href=""></a> 第2个项目</li>
  6. <li class="item"><a href=""></a> 第3个项目</li>
  7. <li class="item"><a href=""></a> 第4个项目</li>
  8. <li class="item"><a href=""></a> 第5个项目</li>
  9. </ul>
  10. </div>
  1. var dropdown = document.getElementsByClassName('dropdown')[0],
  2. oList = elemChildren(dropdown)[1],
  3. timer = null,
  4. listHeight = 0,
  5. speed = 8;
  6. dropdown.onmouseenter = function () {
  7. clearInterval(timer);
  8. timer = setInterval(function () {
  9. if (listHeight >= 200) {
  10. clearInterval(timer);
  11. } else {
  12. listHeight = parseInt(getStyles(oList, 'height')) + speed;
  13. oList.style.height = listHeight + 'px';
  14. }
  15. }, 1);
  16. this.className += ' up';
  17. }
  18. dropdown.onmouseleave = function () {
  19. clearInterval(timer);
  20. timer = setInterval(function () {
  21. if (listHeight <= 0) {
  22. clearInterval(timer);
  23. } else {
  24. listHeight = parseInt(getStyles(oList, 'height')) - speed;
  25. oList.style.height = listHeight + 'px';
  26. }
  27. }, 1);
  28. this.className += 'dropdown';
  29. }
  30. function getStyles(elem, prop) {
  31. //检测getComputedStyle是否存在
  32. if (window.getComputedStyle) {
  33. //存在,打印具体属性值
  34. if (prop) {
  35. return window.getComputedStyle(elem, null)[prop];
  36. }
  37. //不存在,打印集合
  38. return window.getComputedStyle(elem, null);
  39. } else {
  40. if (prop) {
  41. return elem.currentStyle[prop];
  42. } else {
  43. return elem.currentStyle;
  44. }
  45. }
  46. }
  47. //找子元素函数
  48. function elemChildren(node) {
  49. var temp = {
  50. 'length': 0,
  51. 'push': Array.prototype.push,
  52. 'splice': Array.prototype.splice
  53. };
  54. var children = node.childNodes,
  55. len = children.length,
  56. item;
  57. for (var i = 0; i < len; i++) {
  58. item = children[i];
  59. if (item.nodeType === 1) {
  60. temp.push(item);
  61. }
  62. }
  63. return temp;
  64. }

页面尺寸

案例:自动阅读读书

知识点:

  • 两栏布局:左边盒子绝对定位,右边盒子不用设置宽度,只设置margin-left
  • 省略三件套:

    • overflow: hidden;
    • text-overflow: ellipsis;
    • white-space: nowrap;
  • 滚动事件
  • 插件三件套

    • 立即执行函数
    • 原型上挂载方法
    • 构造函数赋值给window window.AutoReader = AutoReader;

功能:

  1. 回到顶部
  2. 自动播放/暂停阅读
  3. 内容触底会暂停播放
  4. 具有配置项的插件

源码地址:https://gitee.com/kevinleeeee/auto-reader-demo

面向过程

过程开发todolist

DOM结构划分组件

图标库:https://fontawesome.com.cn/faicons

  1. ///common.css
  2. body {
  3. margin: 0;
  4. }
  5. a {
  6. text-decoration: none;
  7. }
  8. ul {
  9. padding: 0;
  10. margin: 0;
  11. list-style: none;
  12. }
  13. h1,
  14. h2,
  15. h3,
  16. h4,
  17. h5,
  18. h6,
  19. p {
  20. margin: 0;
  21. font-weight: bold;
  22. }
  23. input,
  24. button {
  25. outline: none;
  26. box-sizing: border-box;
  27. border: none;
  28. }
  1. //index.css
  2. .wrap {
  3. position: relative;
  4. width: 500px;
  5. height: 500px;
  6. margin: 50px auto;
  7. box-shadow: 0 0 5px #999;
  8. border-radius: 10px;
  9. overflow: hidden;
  10. }
  11. .wrap .list-hd {
  12. position: absolute;
  13. top: 0;
  14. left: 0;
  15. width: 100%;
  16. height: 44px;
  17. background-color: #000;
  18. color: #fff;
  19. text-align: center;
  20. line-height: 44px;
  21. }
  22. .list-hd .fa-plus {
  23. position: absolute;
  24. top: 15px;
  25. right: 15px;
  26. color: #fff;
  27. }
  28. .input-wrap {
  29. display: none;
  30. position: absolute;
  31. top: 44px;
  32. left: 0;
  33. width: 100%;
  34. height: 40px;
  35. border-bottom: 1px solid #ddd;
  36. }
  37. .input-wrap .input-bd {
  38. float: left;
  39. width: 384px;
  40. height: 100%;
  41. padding: 0px 5px 5px 10px;
  42. }
  43. .input-wrap .input-bd input {
  44. width: 100%;
  45. height: 100%;
  46. border: 1px solid #ddd;
  47. }
  48. .input-wrap .btn-bd {
  49. float: left;
  50. width: 86px;
  51. height: 100%;
  52. padding: 0px 10px 5px 5px;
  53. }
  54. .input-wrap .btn-bd button {
  55. width: 100%;
  56. height: 100%;
  57. border: 1px solid #ddd;
  58. background-color: #fff;
  59. }
  60. .list-warp {
  61. height: 456px;
  62. margin-top: 44px;
  63. overflow: auto;
  64. }
  65. .list-wrap .item {
  66. position: relative;
  67. height: 50px;
  68. }
  69. .list-warp .item:nth-child(odd),
  70. .list-warp .item:hover {
  71. background-color: #eee;
  72. }
  73. .list-warp .item .item-content {
  74. position: absolute;
  75. left: 0;
  76. top: 0;
  77. width: 400px;
  78. height: 100%;
  79. line-height: 50px;
  80. text-indent: 15px;
  81. }
  82. .list-warp .item .btn-group {
  83. height: 100%;
  84. margin-left: 400px;
  85. line-height: 50px;
  86. }
  87. .list-warp .item .btn-group a {
  88. margin-left: 15px;
  89. }
  90. .list-warp .item .btn-group .edit-btn {
  91. color: green;
  92. }
  93. .list-warp .item .btn-group .remove-btn {
  94. color: red;
  95. }
  1. <!-- plus 图标 -->
  2. <link rel="stylesheet" href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" />
  3. <link rel="stylesheet" href="css/normalize.css" />
  4. <link rel="stylesheet" href="./css/common.css" />
  5. <link rel="stylesheet" href="./css/index.css" />
  1. <!-- 包装器 -->
  2. <div class="wrap">
  3. <!-- header -->
  4. <div class="list-hd">
  5. <h2>TodoList</h2>
  6. <!-- 承载图标 -->
  7. <a href="javascript:;" class="j-show-input fa fa-plus"></a>
  8. </div>
  9. <div class="input-wrap">
  10. <div class="input-bd">
  11. <input type="text" id="textInput" />
  12. </div>
  13. <div class="btn-bd">
  14. <button class="j-add-item">增加项目</button>
  15. </div>
  16. </div>
  17. <div class="list-warp">
  18. <ul class="j-list list"></ul>
  19. </div>
  20. </div>
  21. <script type="text/javascript" src="./js/utils.js"></script>
  22. <script type="text/javascript" src="./js/index.js"></script>
  1. init();
  2. //模块化和组件化开发todolist
  3. function init() {
  4. initTodoList;
  5. }
  6. var initTodoList = (function () {
  7. var showInput = document.getElementsByClassName('j-show-input')[0],
  8. inputWrap = document.getElementsByClassName('input-wrap')[0],
  9. addItem = document.getElementsByClassName('j-add-item')[0],
  10. textInput = document.getElementById('textInput'),
  11. oList = document.getElementsByClassName('j-list')[0];
  12. inputShow = false;
  13. //事件绑定函数
  14. addEvent(showInput, 'click', function () {
  15. //如果为真,点击为假
  16. if (inputShow) {
  17. inputWrap.style.display = 'none';
  18. inputShow = false;
  19. } else {
  20. inputWrap.style.display = 'block';
  21. inputShow = true;
  22. }
  23. });
  24. addEvent(addItem, 'click', function () {
  25. //获取动态的lis列表(类数组)
  26. var oItems = document.getElementsByClassName('item'),
  27. val = textInput.value,
  28. len = val.length,
  29. itemLen = oItems.length,
  30. item;
  31. //情况1:输入框为空
  32. if (len === 0) {
  33. return;
  34. }
  35. //遍历对比模板字符p标签里面的text
  36. for (var i = 0; i < itemLen; i++) {
  37. //item 是 li
  38. // item = oItems[i];
  39. //但希望找的是p里面的text
  40. //解决方法
  41. item = elemChildren(oItems[i])[0];
  42. //此时能找到p元素
  43. // console.log(item);
  44. var text = item.innerText;
  45. //去重
  46. if (val === text) {
  47. alert('已存在此项目');
  48. return;
  49. }
  50. }
  51. //新增li
  52. var oLi = document.createElement('li');
  53. oLi.className = 'item';
  54. //把输入框的值传入模板函数里
  55. oLi.innerHTML = itemTpl(val);
  56. oList.appendChild(oLi);
  57. //清空输入框
  58. textInput.value = '';
  59. //隐藏输入框
  60. inputWrap.style.display = 'none';
  61. //更改状态
  62. inputShow = false;
  63. })
  64. // 处理模板
  65. //直接返回字符串替换
  66. function itemTpl(text) {
  67. return (
  68. '<p class="item-content">' + text + '</p>' +
  69. '<div class="btn-group">' +
  70. '<a href="javascript:;" class="edit-btn fa fa-edit"></a>' +
  71. '<a href="javascript:;" class="remove-btn fa fa-times"></a>' +
  72. '</div>'
  73. )
  74. }
  75. })();

面向对象

插件化开发todolist

index.css

  1. .wrap {
  2. position: relative;
  3. width: 500px;
  4. height: 500px;
  5. margin: 50px auto;
  6. box-shadow: 0 0 5px #999;
  7. border-radius: 10px;
  8. overflow: hidden;
  9. }
  10. .wrap .list-hd {
  11. position: absolute;
  12. top: 0;
  13. left: 0;
  14. width: 100%;
  15. height: 44px;
  16. background-color: #000;
  17. color: #fff;
  18. text-align: center;
  19. line-height: 44px;
  20. }
  21. .list-hd .fa-plus {
  22. position: absolute;
  23. top: 15px;
  24. right: 15px;
  25. color: #fff;
  26. }
  27. .input-wrap {
  28. display: none;
  29. position: absolute;
  30. top: 44px;
  31. left: 0;
  32. width: 100%;
  33. height: 40px;
  34. border-bottom: 1px solid #ddd;
  35. z-index: 1;
  36. }
  37. .input-wrap .input-bd {
  38. float: left;
  39. width: 384px;
  40. height: 100%;
  41. padding: 0px 5px 5px 10px;
  42. }
  43. .input-wrap .input-bd input {
  44. width: 100%;
  45. height: 100%;
  46. border: 1px solid #ddd;
  47. }
  48. .input-wrap .btn-bd {
  49. float: left;
  50. width: 86px;
  51. height: 100%;
  52. padding: 0px 10px 5px 5px;
  53. }
  54. .input-wrap .btn-bd button {
  55. width: 100%;
  56. height: 100%;
  57. border: 1px solid #ddd;
  58. background-color: #fff;
  59. }
  60. .list-warp {
  61. height: 456px;
  62. margin-top: 44px;
  63. overflow: auto;
  64. }
  65. .list-warp .item {
  66. position: relative;
  67. height: 50px;
  68. }
  69. .list-warp .item:nth-child(odd),
  70. .list-warp .item:hover {
  71. background-color: #eee;
  72. }
  73. .list-warp .list.item.active{
  74. background-color: #dff0d8;
  75. }
  76. .list-warp .item .item-content {
  77. position: absolute;
  78. left: 0;
  79. top: 2%;
  80. width: 400px;
  81. height: 100%;
  82. line-height: 50px;
  83. text-indent: 15px;
  84. }
  85. .list-warp .item .btn-group {
  86. height: 50px;
  87. line-height: 50px;
  88. margin-left: 400px;
  89. }
  90. .list-warp .item .btn-group a {
  91. margin-left: 15px;
  92. }
  93. .list-warp .item .btn-group .edit-btn {
  94. color: green;
  95. }
  96. .list-warp .item .btn-group .remove-btn {
  97. color: red;
  98. }

common.css

  1. body {
  2. margin: 0;
  3. }
  4. a {
  5. text-decoration: none;
  6. }
  7. ul {
  8. padding: 0;
  9. margin: 0;
  10. list-style: none;
  11. }
  12. h1,
  13. h2,
  14. h3,
  15. h4,
  16. h5,
  17. h6,
  18. p {
  19. margin: 0;
  20. font-weight: bold;
  21. }
  22. input,
  23. button {
  24. outline: none;
  25. box-sizing: border-box;
  26. border: none;
  27. }
  1. <!-- plus 图标 -->
  2. <link rel="stylesheet" href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" />
  3. <link rel="stylesheet" href="./css/normalize.css" />
  4. <link rel="stylesheet" href="./css/common.css" />
  5. <link rel="stylesheet" href="./css/index.css" />
  1. <!-- 包装器 -->
  2. <div class="wrap" data-config='{
  3. "plusBtn": "j-plus-btn",
  4. "inputArea": "input-wrap",
  5. "addBtn": "j-add-item",
  6. "list": "item-list",
  7. "itemClass": "item"
  8. }'>
  9. <!-- header -->
  10. <div class="list-hd">
  11. <h2>TodoList</h2>
  12. <!-- 承载图标 -->
  13. <a href="javascript:;" class="j-plus-btn j-show-input fa fa-plus"></a>
  14. </div>
  15. <div class="input-wrap">
  16. <div class="input-bd">
  17. <input type="text" id="textInput" class="content"/>
  18. </div>
  19. <div class="btn-bd add-btn">
  20. <button class="j-add-item">增加项目</button>
  21. </div>
  22. </div>
  23. <div class="list-warp">
  24. <ul class="j-list list"></ul>
  25. </div>
  26. </div>
  1. (function (doc) {
  2. //构造函数
  3. var TodoList = function () {
  4. var _self = this;
  5. this.inputShowStatus = "close";
  6. this.configList = this.getConfig();
  7. this.editStatus = false;
  8. this.idx = 0;
  9. //默认配置项
  10. this.defaultConfig = {
  11. addBtn: "",
  12. inputArea: "",
  13. itemClass: "",
  14. list: "",
  15. plusBtn: "",
  16. };
  17. //对比有没有填写完整
  18. //对比是否键名/键值匹配
  19. for (var key in this.defaultConfig) {
  20. if (!this.configList.hasOwnProperty(key)) {
  21. //如果用户书写插件配置不完整时抛出错误提示
  22. console.log(errorLog(key));
  23. return;
  24. }
  25. }
  26. //获取节点元素
  27. this.setConfig();
  28. //事件
  29. //点击加号:开启/关闭输入文本框
  30. addEvent(this.plusBtn, "click", function () {
  31. _self.inputShow.call(_self);
  32. });
  33. //增加项目按钮:新增/修改
  34. addEvent(this.addBtn, "click", function () {
  35. _self.addOrEditItem.call(_self);
  36. });
  37. //列表: 编辑
  38. addEvent(this.list, "click", function (e) {
  39. _self.listPage.apply(_self, [e]);
  40. });
  41. };
  42. //原型
  43. TodoList.prototype = {
  44. //获取配置列表
  45. getConfig: function () {
  46. return JSON.parse(
  47. doc.getElementsByClassName("wrap")[0].getAttribute("data-config")
  48. );
  49. },
  50. //设置配置列表
  51. setConfig: function () {
  52. //获取元素节点
  53. this.plusBtn = doc.getElementsByClassName(this.configList.plusBtn)[0];
  54. this.inputArea = doc.getElementsByClassName(this.configList.inputArea)[0];
  55. this.addBtn = doc.getElementsByClassName(this.configList.addBtn)[0];
  56. this.content = doc.getElementsByClassName("content")[0];
  57. this.list = doc.getElementsByClassName("j-list")[0];
  58. // console.log(this);
  59. },
  60. //事件处理函数:开启/关闭输入框
  61. inputShow: function () {
  62. if (this.inputShowStatus === "open") {
  63. this.inputArea.style.display = "block";
  64. this.inputShowStatus = "close";
  65. } else if (this.inputShowStatus === "close") {
  66. this.inputArea.style.display = "none";
  67. this.inputShowStatus = "open";
  68. }
  69. },
  70. //事件处理函数:点击新增/修改按钮
  71. addOrEditItem: function () {
  72. var inputValue = this.content.value,
  73. valueLen = inputValue.length,
  74. _oItems = doc.getElementsByClassName("item"),
  75. _item;
  76. //情况1:如果文本框没有内容
  77. if (valueLen === 0) {
  78. alert("请您在输入框输入文本内容");
  79. return;
  80. }
  81. //情况2:去重
  82. for (var i = 0; i < _oItems.length; i++) {
  83. _item = _oItems[i];
  84. if (inputValue === elemChildren(_item)[0].innerText) {
  85. alert("该项目已经存在");
  86. return;
  87. }
  88. }
  89. //情况3:判断按钮状态 新增/编辑
  90. if (this.editStatus) {
  91. elemChildren(_oItems[this.idx])[0].innerText = inputValue;
  92. this.addBtn.innerText = "增加项目";
  93. this.editStatus = false;
  94. } else {
  95. this.add();
  96. }
  97. this.reset();
  98. },
  99. edit: function (tar) {
  100. // console.log("editing");
  101. var _p = elemChildren(elemParent(tar, 2))[0],
  102. _item = elemParent(tar, 2),
  103. _items = elemChildren(this.list),
  104. _idx = Array.prototype.indexOf.call(_items, _item),
  105. _lItem;
  106. this.idx = _idx;
  107. this.inputShowStatus === "open";
  108. this.inputArea.style.display = "block";
  109. //修改被点击编辑按钮时li的背景色
  110. for (var i = 0; i < _items.length; i++) {
  111. _lItem = _items[i];
  112. _lItem.className = "list item";
  113. }
  114. _item.className += " active";
  115. this.content.value = _p.innerText;
  116. this.addBtn.innerText = "修改第" + (_idx + 1) + "项";
  117. this.editStatus = true;
  118. },
  119. add: function () {
  120. // console.log("adding");
  121. var inputValue = this.content.value,
  122. oLi = doc.createElement("li"),
  123. oList = doc.getElementsByClassName("list")[0];
  124. oLi.innerHTML = this.tpl(inputValue);
  125. oLi.className += " item";
  126. oList.appendChild(oLi);
  127. },
  128. //事件处理函数:点击页面修改/删除
  129. listPage: function (e) {
  130. var e = e || window.event,
  131. tar = e.target || e.srcElement,
  132. tagName = tar.tagName;
  133. if (tagName === "A") {
  134. if (tar.className === "edit-btn fa fa-edit") {
  135. this.edit(tar);
  136. } else if (tar.className === "remove-btn fa fa-times") {
  137. this.delItem(tar);
  138. }
  139. }
  140. },
  141. //删除当前项
  142. delItem: function (tar) {
  143. var _item = elemParent(tar, 2),
  144. _itemF = elemParent(tar, 3);
  145. _itemF.removeChild(_item);
  146. },
  147. //重置
  148. reset: function () {
  149. this.content.value = "";
  150. this.inputArea.style.display = "none";
  151. },
  152. //模板处理
  153. tpl: function (text) {
  154. return (
  155. '<p class="item-content">' +
  156. text +
  157. "</p>" +
  158. '<div class="btn-group">' +
  159. '<a href="javascript:;" class="edit-btn fa fa-edit"></a>' +
  160. '<a href="javascript:;" class="remove-btn fa fa-times"></a>' +
  161. "</div>"
  162. );
  163. },
  164. };
  165. //普通函数
  166. //错误日志模板
  167. function errorLog(key) {
  168. return new Error(
  169. "您没有配置参数" +
  170. key +
  171. "\n" +
  172. "必须配置的参数列表如下:\n" +
  173. "打开输入框按钮元素类名:plusBtn \n" +
  174. "输入框区域元素类名: inputArea \n" +
  175. "增加项目按钮元素类名:addBtn \n" +
  176. "列表承载元素类名:list \n" +
  177. "列表项承载元素类名:itemClass"
  178. );
  179. }
  180. new TodoList();
  181. })(document);

输入框事件

京东搜索框

  • 功能一:随机轮播提示
  • 功能二:获得焦点/失去焦点样式变化

注:企业开发的黑色#424242

  1. input {
  2. outline: none;
  3. border: none;
  4. box-sizing: border-box;
  5. }
  6. .input-wrap {
  7. position: relative;
  8. width: 250px;
  9. height: 35px;
  10. margin: 50px auto;
  11. }
  12. .input-wrap .auto-kw {
  13. position: absolute;
  14. top: 8px;
  15. left: 5px;
  16. z-index: -1;
  17. font-style: 14px;
  18. color: #989898;
  19. }
  20. .input-wrap .auto-kw.show {
  21. display: block;
  22. color: #989898;
  23. }
  24. .input-wrap .auto-kw.hide {
  25. display: none;
  26. color: #666;
  27. }
  28. .input-wrap .search-input {
  29. width: 100%;
  30. height: 100%;
  31. background-color: transparent;
  32. border: 1px solid #ddd;
  33. text-indent: 5px;
  34. color: #424242;
  35. }
  1. <div class="input-wrap">
  2. <!-- 自动关键字 -->
  3. <div class="auto-kw" id="J_autoKw">推荐词</div>
  4. <input type="text" id="J_search_kw" class="search-input">
  5. </div>
  6. <!-- 推荐词 -->
  7. <div id="J_recomKw" style="display: none;">
  8. ["美丽独立日","LG显示器","洗发水套装","电脑主机","笔记本内存条"]
  9. </div>
  1. function init() {
  2. keySearch();
  3. }
  4. var keySearch = (function () {
  5. //获取input
  6. var searchKw = document.getElementById('J_search_kw'),
  7. //获取默认推荐词div
  8. autoKw = document.getElementById('J_autoKw'),
  9. //获取推荐词列表
  10. recomKw = JSON.parse(document.getElementById('J_recomKw').innerHTML),
  11. //下标
  12. kwOrder = 0,
  13. //计时器
  14. t = null;
  15. //计时器
  16. setAutoKeys();
  17. //获得焦点
  18. addEvent(searchKw, 'focus', function () {
  19. clearInterval(t);
  20. autoKw.style.color = '#ccc';
  21. })
  22. //失去焦点
  23. addEvent(searchKw, 'blur', function () {
  24. autoKwShow(this.value, true);
  25. t = setInterval(autoKwChange, 3000);
  26. })
  27. //input事件
  28. addEvent(searchKw, 'input', function () {
  29. autoKwShow(this.value, false);
  30. })
  31. //input事件兼容
  32. addEvent(searchKw, 'propertychange', function () {
  33. autoKwShow(this.value, false);
  34. })
  35. //随机轮播提示
  36. function setAutoKeys() {
  37. // 因为一加载页面就显示推荐词,所以得在计时器前执行一次autoKwChange
  38. autoKwChange();
  39. t = setInterval(autoKwChange, 3000);
  40. }
  41. //随机轮播提示
  42. function autoKwChange() {
  43. var len = recomKw.length;
  44. autoKw.innerHTML = recomKw[kwOrder];
  45. //列表超出列表数组范围赋值为0否则下标号自增
  46. kwOrder = kwOrder >= len - 1 ? 0 : kwOrder + 1;
  47. }
  48. //是否显示输入框文本内容
  49. function autoKwShow(val, isBlur) {
  50. if (val.length <= 0) {
  51. autoKw.className = 'auto-kw show';
  52. autoKw.style.color = isBlur ? '#989898' : '#ccc';
  53. } else {
  54. autoKw.className = 'auto-kw hide';
  55. }
  56. }
  57. });
  58. init();

移入移出事件

案例一:移动鼠标改变手机列表样式

源码地址:https://gitee.com/kevinleeeee/mouseover-event-demo/tree/master

  1. ;
  2. (function (doc) {
  3. var oItems = doc.getElementsByClassName('list-item'),
  4. curIdx = 0,
  5. oList = doc.getElementsByClassName('list')[0];
  6. var init = function () {
  7. bindEvent();
  8. }
  9. function bindEvent() {
  10. //写法一:循环绑定
  11. for (var i = 0; i < oItems.length; i++) {
  12. addEvent(oItems[i], 'mouseover', function () {
  13. oItems[curIdx].className = 'list-item';
  14. //this -> oItems[i]
  15. curIdx = Array.prototype.indexOf.call(oItems, this);
  16. oItems[curIdx].className += ' active';
  17. })
  18. }
  19. //写法二:事件代理
  20. addEvent(oList, 'mouseover', slide);
  21. }
  22. //写法二:事件代理
  23. function slide(ev) {
  24. var e = ev || window.event,
  25. tar = e.target || e.srcElement;
  26. oLi = getLi(tar, 'li');
  27. thisIdx = Array.prototype.indexOf.call(oItems, oLi);
  28. // 证明不是active
  29. if (curIdx !== thisIdx) {
  30. //原来的项还原类名
  31. oItems[curIdx].className = 'list-item';
  32. curIdx = thisIdx;
  33. oItems[curIdx].className += ' active';
  34. }
  35. }
  36. //写法二:事件代理
  37. //找到被触发的li元素
  38. function getLi(target, element) {
  39. // console.log(target.tagName.toLowerCase());
  40. // console.log(target.parentNode);
  41. while (target.tagName.toLowerCase() !== element) {
  42. target = target.parentNode;
  43. }
  44. return target;
  45. }
  46. init();
  47. })(document)

案例二:移入移出菜单栏显示相应的菜单内容

源码地址:https://gitee.com/kevinleeeee/mouseover-event-demo2

  1. function init() {
  2. initMenu();
  3. }
  4. var initMenu = (function () {
  5. var oMenu = document.getElementsByClassName('menu-wrap')[0],
  6. oMenuItems = oMenu.getElementsByClassName('main-item'),
  7. oSub = oMenu.getElementsByClassName('sub')[0],
  8. oSubItems = oSub.getElementsByClassName('sub-item'),
  9. menuLen = oMenuItems.length,
  10. subLen = oSubItems.length,
  11. menuItem,
  12. subItem,
  13. isInSub = false,
  14. isFirst = true,
  15. t = null,
  16. // 鼠标坐标数组
  17. mousePoses = [];
  18. for (var i = 0; i < menuLen; i++) {
  19. menuItem = oMenuItems[i];
  20. addEvent(menuItem, 'mouseenter', menuItemMouseEnter);
  21. }
  22. addEvent(oMenu, 'mouseenter', function () {
  23. addEvent(document, 'mousemove', mouseMove);
  24. });
  25. addEvent(oMenu, 'mouseleave', menuMouseOut);
  26. //鼠标移入移出
  27. addEvent(oSub, 'mouseenter', function () {
  28. isInSub = true;
  29. })
  30. //鼠标移入移出
  31. addEvent(oSub, 'mouseleave', function () {
  32. isInSub = false;
  33. })
  34. function menuItemMouseEnter(e) {
  35. var e = e || window.event,
  36. tar = e.target || e.srcElement,
  37. thisIdx = Array.prototype.indexOf.call(oMenuItems, tar),
  38. lastPos = mousePoses[mousePoses.length - 2] || {
  39. x: 0,
  40. y: 0
  41. },
  42. curPos = mousePoses[mousePoses.length - 1] || {
  43. x: 0,
  44. y: 0
  45. },
  46. toDelay = doTimeout(lastPos, curPos);
  47. //显示sub盒子
  48. oSub.className = 'sub';
  49. if (t) {
  50. clearTimeout(t);
  51. }
  52. if (!isFirst) {
  53. if (toDelay) {
  54. t = setTimeout(function () {
  55. //鼠标在子菜单里
  56. if (isInSub) {
  57. return;
  58. }
  59. addActive(thisIdx);
  60. t = null;
  61. }, 300);
  62. } else {
  63. addActive(thisIdx);
  64. }
  65. } else {
  66. addActive(thisIdx);
  67. isFirst = false;
  68. }
  69. }
  70. //抽象:新增类名
  71. function addActive(index) {
  72. removeAllActive();
  73. oMenuItems[index].className += ' active';
  74. oSubItems[index].className += ' active';
  75. }
  76. //抽象:移除类名
  77. function removeAllActive(item) {
  78. for (var i = 0; i < menuLen; i++) {
  79. item = oMenuItems[i];
  80. item.className = 'main-item';
  81. }
  82. for (var i = 0; i < subLen; i++) {
  83. item = oSubItems[i];
  84. item.className = 'sub-item';
  85. }
  86. }
  87. function mouseMove(e) {
  88. var e = e || window.event;
  89. mousePoses.push({
  90. x: pagePos(e).X,
  91. y: pagePos(e).Y
  92. });
  93. if (mousePoses.length > 3) {
  94. //把第一个元素删除 把前面的删除
  95. mousePoses.shift();
  96. }
  97. console.log(mousePoses);
  98. }
  99. //鼠标移出菜单
  100. function menuMouseOut() {
  101. oSub.className += ' hide';
  102. removeAllActive();
  103. removeEvent(document, 'mousemove', mouseMove);
  104. }
  105. function doTimeout(lastPos, curPos) {
  106. var topLeft = {
  107. x: getStyles(oMenu, 'width') + getStyles(oMenu, 'margin-left'),
  108. y: getStyles(oMenu, 'margin-top')
  109. };
  110. var bottomLeft = {
  111. x: getStyles(oMenu, 'width') + getStyles(oMenu, 'margin-left'),
  112. y: getStyles(oMenu, 'margin-top') + getStyles(oSub, 'height')
  113. }
  114. return pointInTriangle(curPos, lastPos, topLeft, bottomLeft);
  115. }
  116. });
  117. init();

案例三:电商网站商品图片放大镜效果

  1. .img-wrap {
  2. position: relative;
  3. width: 390px;
  4. height: 600px;
  5. margin: 100px auto;
  6. border: 1px solid #ddd;
  7. box-shadow: 0 0 5px #999;
  8. }
  9. .img-wrap .mag-wrap {
  10. display: none;
  11. position: absolute;
  12. top: 0;
  13. left: 0;
  14. width: 150px;
  15. height: 150px;
  16. background-color: #fff;
  17. box-shadow: 0 0 3px #ccc;
  18. overflow: hidden;
  19. }
  20. .img-wrap .mag-wrap.show {
  21. display: block;
  22. transform: scale(1.5);
  23. }
  24. .img-wrap .mag-wrap .mag-img {
  25. position: absolute;
  26. top: 0;
  27. left: 0;
  28. width: 488px;
  29. height: 750px;
  30. }
  31. .img-wrap .static-img {
  32. height: 100%;
  33. }
  1. <div class="img-wrap">
  2. <div class="mag-wrap">
  3. <img src="./img/1.jpg" class="mag-img" alt="">
  4. </div>
  5. <a href="javascript:;" class="img-lk">
  6. <img src="./img/1.jpg" class="static-img" alt="">
  7. </a>
  8. </div>
  1. window.onload = function () {
  2. init();
  3. }
  4. function init() {
  5. initMagifier();
  6. }
  7. var initMagifier = (function () {
  8. var oImgWrap = document.getElementsByClassName('img-wrap')[0],
  9. oMagWrap = oImgWrap.getElementsByClassName('mag-wrap')[0],
  10. oMagImg = oMagWrap.getElementsByClassName('mag-img')[0],
  11. //放大盒子正方形宽高
  12. magWidth = getStyles(oMagWrap, 'width'), //150
  13. magHeight = getStyles(oMagWrap, 'height'), //150
  14. //imgX原图左边离页面左边的距离
  15. imgX = oImgWrap.offsetLeft,
  16. //imgY原图上边离页面上边的距离
  17. imgY = oImgWrap.offsetTop;
  18. //原图大盒子绑定鼠标经过事件
  19. addEvent(oImgWrap, 'mouseover', function (e) {
  20. /**
  21. * 原理:
  22. * 鼠标移动到图片上面小盒子跟着走
  23. * 鼠标小手正处于小盒子一半的位置
  24. * 每走一步都要计算出小盒子左上坐标的top/left值
  25. * 即计算出小盒子左侧到原图盒子左侧的距离,小盒子上侧到原图盒子上侧的距离
  26. * 小盒子左侧到原图盒子左侧的距离 = pageLeft - offsetLeft - 小盒子中心位置到小盒子左侧的距离即 oMagWrap/2
  27. * 小盒子上侧到原图盒子上侧的距离 = pageTop - offsetTop - 小盒子中心位置到小盒子上侧的距离即 oMagWrap/2
  28. *
  29. * 边缘问题:
  30. * 鼠标盒子移出边框外会消失
  31. * 鼠标目前位置(鼠标到边缘距离) = 页面坐标 - offsetLeft/Top
  32. * 鼠标到边缘距离 < 0 或者 > 原图的宽度/高度 都为超出边界
  33. */
  34. showMag(
  35. getXY(e).x,
  36. getXY(e).y
  37. );
  38. //步骤1:
  39. //让正方形显示出来
  40. oMagWrap.className += ' show';
  41. //里面有鼠标移动事件 跟document搭配使用
  42. addEvent(document, 'mousemove', mouseMove)
  43. })
  44. //原图大盒子绑定鼠标离开事件
  45. addEvent(oImgWrap, 'mouseout', mouseOut);
  46. //每移动一步也同样操作
  47. function mouseMove(e) {
  48. showMag(
  49. getXY(e).x,
  50. getXY(e).y,
  51. getXY(e).mouseX,
  52. getXY(e).mouseY
  53. );
  54. }
  55. function mouseOut(e) {
  56. oMagWrap.className = 'mag-wrap';
  57. //解绑事件
  58. removeEvent(document, 'mousemove', mouseMove)
  59. }
  60. //抽象:
  61. function getXY(e) {
  62. var e = e || window.event;
  63. return {
  64. //小盒子左上的x,y坐标
  65. x: pagePos(e).X - imgX - magWidth / 2,
  66. y: pagePos(e).Y - imgY - magHeight / 2,
  67. //鼠标到边框距离
  68. mouseX: pagePos(e).X - imgX,
  69. mouseY: pagePos(e).Y - imgY
  70. }
  71. }
  72. //抽象:
  73. function showMag(x, y, mouseX, mouseY) {
  74. //步骤2:
  75. //给正方形设置top/left值
  76. //可以让小盒子移动
  77. oMagWrap.style.left = x + 'px';
  78. oMagWrap.style.top = y + 'px';
  79. //步骤3:
  80. //让小盒子里面的图片重新赋值
  81. //小盒子往右下移动 里面的图片往左上移动(负数)
  82. oMagImg.style.left = -x + 'px';
  83. oMagImg.style.top = -y + 'px';
  84. //这两参数有传值就执行下面代码
  85. if (mouseX && mouseY) {
  86. //步骤4 判断鼠标距离是否超出边框
  87. //小盒子移出到边框外会消失
  88. if (mouseX < 0 ||
  89. mouseX > getStyles(oImgWrap, 'width') ||
  90. mouseY < 0 ||
  91. mouseY > getStyles(oImgWrap, 'height')) {
  92. oMagWrap.className = 'mag-wrap';
  93. }
  94. }
  95. }
  96. });

键盘事件

贪食蛇

原理:数组存放x/y坐标的形式且对数组进行操作

  1. .wrap {
  2. position: relative;
  3. width: 500px;
  4. height: 500px;
  5. margin: 50px auto;
  6. background-color: #000;
  7. overflow: hidden;
  8. }
  9. .round {
  10. display: block;
  11. position: absolute;
  12. width: 20px;
  13. height: 20px;
  14. border-radius: 50%;
  15. background-color: green;
  16. }
  17. .round.head {
  18. background-color: red;
  19. }
  1. <div class="wrap"></div>
  1. /**
  2. * 案例:贪吃蛇
  3. * 原理:操作存放对象为x/y坐标的数组,并操作数组
  4. * -问题1:如何显示有一条蛇(初始为6个小圆圈竖向并列)?
  5. * 数组应显示:
  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}]
  7. * -问题2:如何让蛇移动起来?
  8. * 数组最后的一个的坐标是倒数第二个的坐标
  9. * -问题3:如何实现改变蛇移动的方向?
  10. * 如果蛇往下走,此时不能往上或往下走
  11. */
  12. window.onload = function () {
  13. init();
  14. }
  15. function init() {
  16. initGame();
  17. }
  18. var initGame = (function () {
  19. var wrap = document.getElementsByClassName('wrap')[0],
  20. t = null;
  21. //画一条蛇
  22. var Snake = function () {
  23. this.bodyArr = [{
  24. x: 0,
  25. y: 0
  26. },
  27. {
  28. x: 0,
  29. y: 20
  30. },
  31. {
  32. x: 0,
  33. y: 40
  34. },
  35. {
  36. x: 0,
  37. y: 60
  38. },
  39. {
  40. x: 0,
  41. y: 80
  42. },
  43. {
  44. x: 0,
  45. y: 100
  46. }
  47. ];
  48. //初始化方向状态
  49. this.dir = 'DOWN';
  50. }
  51. Snake.prototype = {
  52. init: function () {
  53. this.initSnake();
  54. this.bindEvent();
  55. //运行程序
  56. this.run();
  57. },
  58. bindEvent: function () {
  59. var _self = this;
  60. //绑定键盘按下事件
  61. addEvent(document, 'keydown', function () {
  62. _self.changeDir();
  63. });
  64. },
  65. //初始化一条蛇
  66. initSnake: function () {
  67. var arr = this.bodyArr,
  68. len = arr.length,
  69. frag = document.createDocumentFragment(),
  70. item;
  71. //循环创建小圆点
  72. for (var i = 0; i < len; i++) {
  73. item = arr[i];
  74. //新增小圆点
  75. var round = document.createElement('i');
  76. //新增类名
  77. round.className = i === len - 1 ? 'round head' : 'round';
  78. //新增坐标
  79. round.style.left = item.x + 'px';
  80. round.style.top = item.y + 'px';
  81. //存入碎片容器
  82. frag.append(round);
  83. }
  84. //将碎片容器存入wrap盒子里
  85. wrap.append(frag);
  86. },
  87. //程序运行
  88. run: function () {
  89. var _self = this;
  90. t = setInterval(function () {
  91. _self.move();
  92. }, 500)
  93. },
  94. //让小蛇移动起来
  95. move: function () {
  96. var arr = this.bodyArr,
  97. len = arr.length,
  98. head = arr[len - 1];
  99. //循环:把每一个坐标项移动一项
  100. for (var i = 0; i < len; i++) {
  101. //证明是头部
  102. if (i === len - 1) {
  103. //判断方向再移动
  104. //改变头部项的坐标
  105. //往下走
  106. switch (this.dir) {
  107. case 'LEFT':
  108. head.x -= 20;
  109. break;
  110. case 'RIGHT':
  111. head.x += 20;
  112. break;
  113. case 'UP':
  114. head.y -= 20;
  115. break;
  116. case 'DOWN':
  117. head.y += 20;
  118. break;
  119. default:
  120. break;
  121. }
  122. } else {
  123. //证明不为头部
  124. //数组最后的一个的坐标是倒数第二个的坐标
  125. arr[i].x = arr[i + 1].x;
  126. arr[i].y = arr[i + 1].y;
  127. }
  128. }
  129. this.removeSnake();
  130. //新的坐标系去绘制
  131. this.initSnake();
  132. },
  133. //绘制之前删除蛇
  134. removeSnake: function () {
  135. //选出所有的小圆点
  136. var bodys = document.getElementsByClassName('round');
  137. //一直循环一直删除第一项的小圆点
  138. //每次删除一个就会有新的第一项
  139. //直到把bodys里面的每一项都删除干净了 0 !> 0 终止循环
  140. while (bodys.length > 0) {
  141. //移除第一个小圆点
  142. bodys[0].remove();
  143. }
  144. },
  145. //改变移动方向
  146. changeDir: function (e) {
  147. var e = e || window.event,
  148. //获取keyCode
  149. code = e.keyCode; //UP/DOWN/LEFT/RIGHT
  150. this.setDir(code);
  151. },
  152. //重新定义移动方向
  153. setDir: function (code) {
  154. switch (code) {
  155. //按左按键往左走
  156. case 37:
  157. // 如果蛇已经往左走,此时不能再按往左或往右走
  158. if (this.dir !== 'RIGHT' && this.dir !== 'LEFT') {
  159. this.dir = 'LEFT';
  160. }
  161. break;
  162. //按右按键往右走
  163. case 39:
  164. // 如果蛇已经往右走,此时不能再按往右或往左走
  165. if (this.dir !== 'RIGHT' && this.dir !== 'LEFT') {
  166. this.dir = 'RIGHT';
  167. }
  168. break;
  169. //按上按键往上走
  170. case 38:
  171. // 如果蛇已经往上走,此时不能再按往上或往下走
  172. if (this.dir !== 'UP' && this.dir !== 'DOWN') {
  173. this.dir = 'UP';
  174. }
  175. break;
  176. //按下按键往下走
  177. case 40:
  178. // 如果蛇已经往下走,此时不能再按往上或往下走
  179. if (this.dir !== 'UP' && this.dir !== 'DOWN') {
  180. this.dir = 'DOWN';
  181. }
  182. break;
  183. default:
  184. break;
  185. }
  186. }
  187. }
  188. return new Snake().init();
  189. })

动态渲染

根据模拟后台的JSON数据来动态渲染页面

里面有两种写法:

  • 循环字符串替换
  • 正则匹配内容替换

源码地址:https://gitee.com/kevinleeeee/render-pageby-jsondemo01

缓存池

案例:点击页码渲染课堂列表

根据Ajax请求后台接口把页码数据渲染到页面

含有缓存池机制

源码地址:https://gitee.com/kevinleeeee/buffer-pool-demo

网络请求

案例:腾讯课堂评论模块

利用AJAX技术写评论列表

基本功能:

  • 有数据显示列表没有的话显示暂无评论
  • 评论框有5星好评并写评论(如有空格忽略)
  • 评论列表显示评论条数数据
  • 根据评论星星实时计算出好评度
  • 实现分页切换评论列表

服务器配置:

  1. 安装WAMP保持绿色图标运行
  2. api_for_study, comments, ThinkPHP 保持在wamp/www目录下
  3. 把study.sql数据表导入到数据库后台

服务器搭建后台管理页面:

企业级模块化写法

  • 评论模块里是方法的集合

js > module > index > comment_module.js

项目源代码:https://gitee.com/kevinleeeee/tx-course-comment-demo

跨域

案例:JSONP跨域之豆瓣网音乐搜索

  1. //https://api.douban.com/v2/music/search?q=关键字&callback=回调函数&count=返回条数
  2. ;(function(doc){
  3. var oInput = doc.getElementById('J_searchInput'),
  4. searchItemTpl = doc.getElementById('J_searchItemTpl').innerHTML,
  5. oList = doc.getElementsByClassName('J_list')[0],
  6. musicCount = 7;
  7. var init = function(){
  8. bindEvent();
  9. }
  10. function bindEvent(){
  11. oInput.addEventListener('input', debounce(musicSearch, 500, false), false);
  12. }
  13. function showList(show){
  14. if(show){
  15. oList.style.display = 'block';
  16. }else{
  17. oList.innerHTML = '';
  18. oList.style.display = 'none';
  19. }
  20. }
  21. function renderList(data){
  22. var list = '';
  23. data.forEach(function(elem){
  24. list += searchItemTpl.replace(/{{(.*?)}}/g, function(node, key){
  25. return {
  26. url: elem.alt,
  27. image: elem.image,
  28. title: elem.alt_title || elem.attrs.title[0],
  29. singer: elem.attrs.singer ? elem.attrs.singer[0] : ''
  30. }[key];
  31. });
  32. });
  33. oList.innerHTML = list;
  34. showList(true);
  35. }
  36. function musicSearch(){
  37. var kw = trimSpace(this.value),
  38. len = kw.length;
  39. if(len > 0){
  40. dataRequest(kw, musicCount);
  41. }else{
  42. showList(false);
  43. }
  44. }
  45. function dataRequest(keyword, musicCount){
  46. xhr.ajax({
  47. url: 'https://api.douban.com/v2/music/search?q=' + keyword + '&count=' + musicCount,
  48. type: 'GET',
  49. dataType: 'JSONP',
  50. jsonp: 'callback',
  51. success: function(data){
  52. if(data && data.musics.length > 0){
  53. renderList(data.musics);
  54. }
  55. }
  56. })
  57. }
  58. init();
  59. })(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信息

    • 冒号分割authident_codetoken字段
    • 分别校验新旧ident_codetoken字段信息

      • 找不到,报错1006,token值不存在
      • 找到了,继续往下执行程序
    • 校验timeout字段看是否过期

      • 过期了,报错1007
      • 登录成功,验证通过

源码地址:https://gitee.com/kevinleeeee/web-login-cookie-token-demo

文件上传

案例1:上传图片显示进度条(可多个同时)

  1. ul {
  2. padding: 0;
  3. margin: 0;
  4. list-style: none;
  5. margin-top: 100px;
  6. }
  7. .progress-bar {
  8. width: 300px;
  9. height: 40px;
  10. border: 1px solid #666;
  11. text-align: center;
  12. }
  13. .progress {
  14. width: 0%;
  15. height: 100%;
  16. background-color: green;
  17. }
  18. .error-info {
  19. line-height: 40px;
  20. font-size: 14px;
  21. color: #333;
  22. }
  1. <!-- multiple:支持多文件上传 -->
  2. <input type="file" id="file" multiple />
  3. <input type="submit" id="submitBtn" value="上传" />
  4. <ul class="progress-wrap">
  5. <li class="progress-bar">
  6. <!-- <div class="progress"></div> -->
  7. <span class="error-info">文件类型错误</span>
  8. </li>
  9. </ul>
  1. //选择文件 输入框
  2. var oFile = document.getElementById('file'),
  3. //上传 按钮
  4. oSubmitBtn = document.getElementById('submitBtn'),
  5. //进度条包装盒子
  6. oProgressWrap = document.getElementsByClassName('progress-wrap')[0];
  7. //输入框变化事件
  8. //files属性对应file列表
  9. /**
  10. * console.log(oFile.files):
  11. * FileList {0: File, 1: File, length: 2}
  12. * 0: File
  13. * lastModified: 1625059303199
  14. * lastModifiedDate: Wed Jun 30 2021 21:21:43 GMT+0800 (中国标准时间) {}
  15. * name: "1.jpeg"
  16. * size: 8396
  17. * type: "image/jpeg"
  18. * webkitRelativePath: ""
  19. * __proto__: File
  20. * 1: File {name: "1.jpg", lastModified: 1624970943554 …}
  21. * length: 2
  22. * __proto__: FileList
  23. */
  24. oFile.onchange = function () {
  25. var files = oFile.files,
  26. fileLen = files.length,
  27. fileName = '',
  28. fileSize = 0,
  29. //1M = 1024 * 1024
  30. maxSize = 1048576,
  31. //fd = new FormData
  32. fd = null,
  33. errorInfo = '';
  34. if (fileLen <= 0) {
  35. console.log('您还没有选择图片');
  36. return;
  37. }
  38. if (fileLen > 5) {
  39. console.log('最多可同时上传5张图片');
  40. return;
  41. }
  42. for (var i = 0; i < fileLen; i++) {
  43. fileName = files[i].name;
  44. fileSize = files[i].size;
  45. //判断是否是想要的后缀名称
  46. //转义. 且以gif,jpg,jpeg,png结尾的
  47. // console.log(/\.(gif|jpg|jpeg|png)$/.test(fileName)); //true
  48. if (!/\.(gif|jpg|jpeg|png)$/.test(fileName)) {
  49. errorInfo = fileName + '文件不是图片类型';
  50. }
  51. //判断是否超出上传内存大小
  52. if (fileSize > maxSize) {
  53. errorInfo = fileName + '超过可上传大小';
  54. }
  55. //动态创建li
  56. var oProgressBar = document.createElement('li');
  57. oProgressBar.className = 'progress-bar';
  58. //插入li到ul
  59. oProgressWrap.appendChild(oProgressBar);
  60. //不符合上传条件
  61. if (errorInfo !== '') {
  62. oProgressBar.innerHTML = '<span class="error-info">' + errorInfo + '</span>';
  63. } else {
  64. //符合上传条件
  65. oProgressBar.innerHTML = '<div class="progress"></div>';
  66. //实例化
  67. fd = new FormData();
  68. //这里file为后端提供字段
  69. //append(属性名,属性值)
  70. fd.append('file', files[i]);
  71. //通过Ajax请求数据
  72. var o = window.XMLHttpRequest ? new Window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
  73. o.open('post', 'server/upload.php');
  74. //闭包情况
  75. (function (j) {
  76. //upload属性里自带的onprogress事件
  77. o.upload.onprogress = function (e) {
  78. var e = e || window.event,
  79. //此时事件源e 里面有 loaded属性:加载完的数据大小 total属性:总数据大小
  80. percent = e.loaded / e.total * 100 + '%',
  81. //当前li
  82. thisProgressBar = oProgressWrap.getElementsByClassName('progress-bar')[j];
  83. //当前progress盒子显示的宽度根据百分比计算出来的宽度显示
  84. thisProgressBar.getElementsByClassName('progress')[0].style.width = percent;
  85. }
  86. })(i);
  87. o.send(fd);
  88. }
  89. }
  90. }

案例2:腾讯课堂上传文件

上传视频显示百分比

源码地址:https://gitee.com/kevinleeeee/file-upload-demo

AJAX

案例1:腾讯课堂列表后台管理系统

  • 功能一:列表分类
  • 功能二:带垃圾箱恢复删除数据的操作
  • 功能三:课程搜索
  • 功能四:点击课程名称显示输入框并可以修改数据
  • 功能五:点击删除/恢复操作

源码地址:https://gitee.com/kevinleeeee/txcourses-management-system-ajax-demo

案例2:瀑布流

  1. 原理demo
  2. 插件化ajax请求开发

后端数据:

  • 如果是瀑布流图片,JSON数据里必须带宽高数据

原理:

  1. 确定第一行的排列(每行5张)
  2. 确保5张图片的盒子的高度到数组
  3. 第六张图片开始照最小盒子高度的那一列排(通过第一行保存的数组)
  4. 排一张图片,修改一次数组相应列的下标
  5. 找到盒子的左边缘到页面左侧的距离和最小盒子高度确定定位

问题:

  1. 如何找到每次最短的高度的图片?

源码地址: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基础上继承样式,样式嵌套

  1. //stylus安装:
  2. npm i -D stylus-loader@3.0.2
  3. npm i -D stylus@0.54.5
  4. //stylus使用:
  5. <style lang="stylus" scoped>
  6. .message {
  7. padding: 10px;
  8. border: 1px solid #eee;
  9. }
  10. .warning {
  11. @extend .message;
  12. color: #e2e2e2;
  13. }
  14. </style>

关于swiper:封装好的轮播图插件

  1. npm i -S vue-awesome-swiper@2.6.7

关于better-scroll:滚动条插件,实现滚动效果

  1. npm i -S better-scroll@1.15.2
  2. //使用
  3. //引入
  4. import BScroll from 'better-scroll';

源码地址:https://gitee.com/kevinleeeee/qunaer

城市一线通(仿美团)案例

关键词:移动端, es6. flex布局

技术:

  • 项目架构/组件结构设计
  • 项目抽象及复用/封装与配置
  • 模型层抽象与应用
  • 工具函数集合抽象与应用
  • 数据请求与格式化与缓存
  • Vuex中央状态管理器
  • 组件缓存机制/单页面路由/组件间传值(父子/兄弟)

项目目录:

  1. assets - 静态文件
  2. conponents - 组件文件
  3. models - 模型文件
  4. pages - 页面组件
  5. router - 路由设置文件
  6. store - 中央状态管理
  7. utils - 配置与工具
  8. App.vue - 项目根组件
  9. main.js - 项目入口文件

关于better-scroll插件

针对移动端上下滑动页面的动画效果

  1. //如何使用better-scroll?
  2. //安装
  3. npm i -S better-scroll@1.15.2
  4. //在组件里引入插件
  5. import BetterScroll from "better-scroll";
  6. //被操作dom写入ref标签属性
  7. <div class="scroll-wrapper" ref="wrapper">...</div>
  8. //实例化插件
  9. mounted:function(){
  10. this.scroll = new BetterScroll(this.$refs.wrapper);
  11. }

组件管理:

所有的组件都会汇入到App.vue组件,通过main.js导入注册组件, 管理页面与渲染,component存放嵌套关系的子组件

如何实现组件复用和拓展?

答:拆分组件

  1. components:
  2. - Header
  3. - Sub
  4. - BackWard.vue
  5. - CitySellector.vue
  6. - FalseInput.vue
  7. - Common.vue
  8. - Home.vue
  9. - Tab
  10. - Index.vue
  11. - Home.vue
  12. - SearchInput
  13. - Index.vue
  14. - ScrollWrapper
  15. - CategoryIcon
  16. - Index.vue
  17. - Sub.vue
  18. - CityList
  19. - CurrentCity.vue
  20. - Index.vue
  21. - Sub.vue
  22. - FoodList
  23. - Index.vue
  24. - Sub.vue
  25. - HotelList
  26. - Index.vue
  27. - Sub.vue
  28. - KtvList
  29. - Index.vue
  30. - Sub.vue
  31. - MassageList
  32. - Index.vue
  33. - Sub.vue
  34. - ViewList
  35. - Index.vue
  36. - Sub.vue
  37. - Sub
  38. - Error.vue
  39. - HomeTitle.vue
  40. - Loading.vue
  41. - NoDataTip.vue
  42. - Stars.vue
  43. - Swiper.vue
  44. - Detail
  45. - Sub
  46. - Address.vue
  47. - CommentKeyword.vue
  48. - Intro.vue
  49. - Name.vue
  50. - OpenDateTime.vue
  51. - Price.vue
  52. - Recom.vue
  53. - Service.vue
  54. - Stars.vue
  55. - TicketInfo.vue
  56. - Tip.vue
  57. - Title.vue
  58. - Food.vue
  59. - Hotel.vue
  60. - Ktv.vue
  61. - Massage.vue
  62. - View.vue
  63. - City.vue
  64. - Detail.vue
  65. - Home.vue
  66. - List.vue
  67. - Search.vue

组件抽离原则:

  • 可复用的组件
  • 可配置的项,在组件内部要抽离属性(放入props里)
  • 前瞻性,用前瞻的眼光抽离组件
  • 有独立的功能性的组件
  • 组件集合一定要有结构

页面组件pages 与 组件文件components关系?

页面组件所需要被拆分的components组件(结构化),组件拆分便于维护,迭代,功能拓展

项目页面导图(组件结构):

  1. - 首页
  2. - 专用header
  3. - 城市选择器
  4. - 假输入框
  5. - 滚动区域
  6. - 图标列表
  7. - 子图标项
  8. - 标题
  9. - 各分类列表
  10. - 错误提示
  11. - 列表页
  12. - 通用header
  13. - 页面回退
  14. - 选项导航
  15. - 子选项
  16. - 滚动区域
  17. - 各分类列表
  18. - Loading
  19. - 详情页
  20. - 通用header
  21. - 页面回退
  22. - 滚动区域
  23. - 各分类
  24. - 轮播图/地址/评论关键词/简介/名称
  25. - 营业时间/价格/推荐/星级/购票信息/商家服务
  26. - 城市选择页
  27. - 通用header
  28. - 页面回退
  29. - 滚动区域
  30. - 当前城市
  31. - 城市列表
  32. - 搜索页
  33. - 通用header
  34. - 页面回退
  35. - 搜索输入框
  36. - 滚动区域
  37. - 各分类列表
  38. - 错误提示
  39. - 无结果提示
  40. - Loading

组件缓存:
切换城市会存在再次请求数据的不合理时候,只有城市数据有变化的时候才重新请求数据

解决方法:给App.vue里根组件挂载的地方用keep-alive标签包裹着

  1. <div id="app">
  2. <keep-alive>
  3. <router-view />
  4. </keep-alive>
  5. </div>

keep-alive是什么?

vue里内置组件,也是一个抽象组件,不在DOM里占用结构的,具有自己的生命周期activateddeactivated

判断当前城市的状态是否被更改(是否被切换)会触发该生命函数

通过当前城市id和被选择城市id对比是否一致

include标签属性用法:

  1. <keep-alive include="被监听的组件">

若已经被缓存时,程序不会再走mounted生命周期函数,但是会保持activated生命周期函数的程序触发,说明组件都被缓存不再经过装载的过程

关于轮播图插件

vue-awesome-swiper

  1. //安装
  2. npm i -S vue-awesome-swiper@3.1.3
  3. //引入组件和样式
  4. import { swiper, swiperSlide } from "vue-awesome-swiper";
  5. import "swiper/dist/css/swiper.css";
  6. //注册组件
  7. components:{
  8. swiper:swiper,
  9. swiperSlide:swiperSlide
  10. }
  11. //标签固定写法
  12. <div class="swiper-wrap">
  13. <swiper :options="swiperOption">
  14. <swiper-slide v-for="(item, index) of picDatas" :key="index">
  15. //自定义内容区域
  16. </swiper-slide>
  17. <div class="swiper-pagination" slot="pagination"></div>
  18. </swiper>
  19. </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

  1. //初始化
  2. npm i -g @vue/cli@4.4.1
  3. //创建项目
  4. vue create calendar_pro
  5. //安装依赖
  6. npm i -D vue-cli-plugin-vue-next@0.1.3
  7. npm i -D @vue/compiler-sfc@3.0.0

解决跨域设置:

  1. //vue.config.js
  2. module.exports = {
  3. devServer: {
  4. //关闭eslint警告
  5. overlay: {
  6. warnings: false,
  7. errors: false
  8. },
  9. //代理解决跨域
  10. proxy: {
  11. '/api': {
  12. target: 'http://v.juhe.cn/',
  13. //改变源
  14. changeOrigin: true,
  15. //开启websocket
  16. ws: true,
  17. //关闭https检查
  18. secure: false,
  19. //重写路径
  20. pathRewrite: {
  21. '^/api': ''
  22. }
  23. }
  24. }
  25. },
  26. lintOnSave: false
  27. }

keep alive 针对vue3写法:

  1. <router-view v-slot="{Component}">
  2. <keep-alive>
  3. <component :is="Component" />
  4. </keep-alive>
  5. </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

项目创建

  1. npx create-nuxt-app@2 meituan-demo

项目选择配置

  1. UI: element
  2. server: koa
  3. modules: axios
  4. render: universal(ssr)

启动项目

  1. npm run dev
  2. //访问
  3. http://localhost:3000/

目录结构

  1. ├─nuxt.config.js -配置文件
  2. ├─package-lock.json
  3. ├─package.json
  4. ├─README.md
  5. ├─store -管理vuex状态
  6. ├─static -管理图标(非必须)
  7. | ├─favicon.ico
  8. ├─server -管理服务端代码
  9. | index.js
  10. ├─plugins -管理插件
  11. | ├─element-ui.js
  12. ├─pages
  13. | ├─about.vue
  14. | ├─index.vue
  15. ├─middleware -管理中间件
  16. ├─layouts -管理默认模板
  17. | ├─default.vue
  18. ├─components -管理组件
  19. | ├─Logo.vue
  20. ├─assets -管理静态资源文件
  21. | ├─css

创建.babelrc配置文件

  1. //配置babel
  2. {
  3. "presets": ["es2015"]
  4. }

安装babel处理es6语法

  1. npm i -D babel-preset-es2015@6.24.1
  2. npm i -D babel-cli@6.26.0
  3. npm i -D babel-core@6.26.3

安装scss处理器

  1. npm i -D sass-loader@7.1.0
  2. npm i -D node-sass@4.11.0

配置nuxt.config.js

  1. //引入初始化css文件
  2. css: [
  3. 'element-ui/lib/theme-chalk/reset.css',
  4. 'element-ui/lib/theme-chalk/index.css'
  5. '@/assets/css/main.css'
  6. ],

编写后台接口

  1. //server目录新建dbs数据库目录
  2. //dbs数据库目录里创建models模型目录
  3. //server > dbs > models模型目录创建users.js文件
  4. //server > dbs目录下创建config.js配置文件(数据库相关信息)
  5. //server目录新建interface接口目录
  6. //interface目录新建utils工具目录
  7. //server > interface新建users.js文件
  8. //server > interface > utils新建axios.js文件(封装全局使用axios)
  9. //server > interface > utils新建passport.js文件(验证相关权限)
  1. //POP3邮箱开启授权码
  2. adgkisdouidsbhda
  3. //IMAP/SMTP服务授权码
  4. pthqrkenxqzdbhji
  1. //server > dbs > config.js
  2. //数据库相关信息
  3. export default {
  4. dbs: 'mongodb://127.0.0.1:27017/meituan/users',
  5. redis: {
  6. //主机地址,只读
  7. get host() {
  8. return '127.0.0.1';
  9. },
  10. get port() {
  11. //端口号
  12. return 6379;
  13. }
  14. },
  15. //腾讯提供的服务 邮箱
  16. smtp: {
  17. get host() {
  18. return 'smtp.qq.com'
  19. },
  20. get user() {
  21. return '273122188@qq.com'
  22. },
  23. get pass() {
  24. return '保密字符';
  25. },
  26. get code() {
  27. return () => {
  28. //返回随机16进制字符
  29. return Math.random().toString(16).slice(2, 6).toUpperCase();
  30. }
  31. },
  32. //过期时间
  33. get expire() {
  34. return () => {
  35. return new Date().getTime() + 60 * 1000
  36. }
  37. }
  38. }
  39. }

关于nodemailer

https://nodemailer.com/about/

是一个用于 Node.js 应用程序的模块(中间件),它轻松地发送电子邮件

  1. //安装nodemailer
  2. npm i -S nodemailer@6.1.0
  3. //引入
  4. import nodeMailer from 'nodemailer';
  5. //接口内部使用
  6. let transporter = nodeMailer.createTransport({
  7. host: EmailConfig.smtp.host,
  8. port: 587,
  9. secure: false,
  10. auth: {
  11. user: EmailConfig.smtp.user,
  12. pass: EmailConfig.smtp.pass,
  13. },
  14. });
  1. //安装依赖
  2. npm i -S koa-redis@3.1.3
  3. npm i -S koa-router@7.4.0
  4. npm i -S axios@0.18.0
  5. npm i -S koa-generic-session@2.0.1
  6. npm i -S koa-bodyparser@4.2.1
  7. npm i -S koa-json@2.0.2
  8. npm i -S koa-redis@3.1.2
  9. npm i -S mongoose@5.5.2
  10. npm i -S koa-passport@4.1.1
  11. npm i -S passport-local@1.0.0
  1. //编写后台接口
  2. //server > interface > users.js

关于PassportJs

http://www.passportjs.org/docs/

Node.js 的身份验证中间件。Passport 非常灵活和模块化,可以不显眼地插入任何 基于Express的 Web 应用程序。一套全面策略支持认证使用的用户名和密码

关于CryptoJS

密码加密的前端库

  1. //安装
  2. npm i -S crypto-js@3.1.9-1
  3. //引入
  4. import CryptoJS from 'crypto-js';
  5. //使用
  6. this.$axios.pist("/users/signup", {
  7. username: encodeURIComponent(this.ruleForm.name),
  8. password: CryptoJS.MD5(this.ruleForm.pwd).toString(),
  9. });

由于后端数据接口失效,请求功能不完善。

源码地址:https://gitee.com/kevinleeeee/meituan-web

观察者

案例:购物车(课程列表)

页面包含课程列表和待报名课程两个模块

实现:点击课程列表其中的项目后会与待报名课程模块立即会有联动(一个模块里有两个功能模块)

问题1:为什么需要两个子模块分开处理?

因为如果两个模块一起编写,在修改其中模块时会影响另一个模块,而且不方便维护,且逻辑之间杂乱不清

问题2:两个分开模块之间如何实现数据联动管理?

利用观察者模式实现管理数据更新

观察者原理:

每一个观察者实际上是一个函数,一旦触发事件,执行一个函数完成一个程序,当一个程序分为1,2,3件事情,那么这个程序就为一个observers,当执行程序123(observers)时不能单独执行,利用obsevers里面的notify通知特性,将123一次性执行,真正事件触发时仅仅执行的是notify函数

也就是说,希望123函数作为一个个obsevers放入数组里,统一用notify一次去执行,在项目里,点击单击事件时触发执行notify函数,然后notify里面所有的observers会依次执行

  1. //Observers.js
  2. observers = [fn, fn, fn];
  3. add() -> fn -> observers
  4. //执行notify()会使每一项observer执行
  5. notify() -> forEach -> observers -> item();

项目渲染原理:

数据 (需要处理) -> 容器 -> 渲染 -> 插入节点

观察者管理的好处:

一个notify函数的执行同时 把handle底下的所有函数同时执行,实现参数实时共享 好处是模块功能同时联动,实现高内聚的代码编写

项目结构:

  1. ├─package.json
  2. ├─webpack.config.js
  3. ├─src
  4. | ├─index.html
  5. | ├─utils
  6. | | ├─Observer.js - 观察者类 驱动
  7. | | tools.js - 工具类方法集合
  8. | ├─templates - 模板文件
  9. | | ├─cartItem.tpl
  10. | | courseItem.tpl
  11. | ├─js
  12. | | ├─index.js - 网页入口文件
  13. | | ├─ShoppingCart
  14. | | | ├─index.js - 主程序入口文件
  15. | | | ├─Course
  16. | | | | ├─Event.js - 管理子模块的绑定事件
  17. | | | | ├─Handle.js - 管理子模块里绑定事件函数底下的所有逻辑方法
  18. | | | | ├─index.js - 子模块入口文件
  19. | | | | Render.js - 渲染函数
  20. | | | ├─Cart
  21. | | | | ├─Event.js
  22. | | | | ├─Handle.js
  23. | | | | ├─index.js
  24. | | | | Render.js
  25. | ├─data - 数据
  26. | | ├─cart.js
  27. | | course.js

源码地址:https://gitee.com/kevinleeeee/data-response-observers-shoppingcart-demo

虚拟节点

虚拟节点和diff算法源码实现

  1. //命名
  2. vNode -> virtual Node
  3. vnPatch -> virtual Node patch
  4. rNode -> real Node
  5. rnNode -> real Node patch

案例 - 图1

功能:

  1. 构建虚拟节点
  2. 转换真实DOM
  3. 渲染DOM节点
  4. 创建补丁包
  5. 给真实DOM打补丁
  1. //补丁格式:
  2. const patches = {
  3. 0: [
  4. {
  5. //属性更改了
  6. type: 'ATTR',
  7. attrs: {
  8. class: 'list-wrapper'
  9. }
  10. }
  11. ],
  12. 2: [
  13. {
  14. type: 'ATTR',
  15. attrs: {
  16. class: 'title'
  17. }
  18. }
  19. ],
  20. 3: [
  21. {
  22. type: 'TEXT',
  23. text: '特殊项'
  24. }
  25. ]
  26. 6: [
  27. {
  28. type: 'REMOVE',
  29. index: 6
  30. }
  31. ],
  32. 7: [
  33. {
  34. type: 'REPLACE',
  35. newNode: newNode
  36. }
  37. ]
  38. }

问题:如何对比新老DOM?

利用domDiff函数对比新老节点处理返回补丁包

  1. //项目结构:
  2. ├─package.json
  3. ├─webpack.config.js
  4. ├─src
  5. | ├─index.html
  6. | ├─js
  7. | | ├─domDiff.js - diff算法等函数
  8. | | ├─doPatch.js - 打补丁等函数
  9. | | ├─Element.js - 构造函数元素对象
  10. | | ├─index.js - 用户入口文件/模拟两个虚拟DOM函数/执行程序
  11. | | ├─patchTypes.js - 管理补丁名称类型
  12. | | virtualDom.js - 创建虚拟DOM/将虚拟DOM转为真实DOM/设置属性/渲染页面函数
  13. ├─public
  14. | index.html

实现步骤:

  1. 用户写一个执行createElement函数实例化之后的对象返回的结果vDom
  2. render函数把虚拟节点转换为真实DOM结构

    1. 给每个真实节点的标签设置属性
    2. 给子元素节点再次递归render渲染
    3. 给子文本节点创建文本
    4. 将子节点插入到父真实节点里
  3. renderDOM函数把渲染后的真实DOM插入到根节点里实现渲染页面
  4. diffDOM函数对比两个虚拟的DOM,内部有私有属性index,内部执行vNodeWalk函数传入新老节点和index,函数最后返回一个补丁patches对象
  5. vNodeWalk函数

    1. 定义一个数组容器vnPacth装载补丁
    2. 当没有新节点时打入移除类型为REMOVEindex 的补丁
    3. 当节点类型是字符串时打入文本类型为TEXT和文本内容的补丁
    4. 当标签名一样时

      1. attrsWalk函数遍历标签里属性里的新老属性
      2. 定义attrPatch对象容器
      3. 当老的属性里的新老属性值不相同时把新的属性和属性值变为attPatch容器对象
      4. 当新的属性里的老的属性值有自身的属性时把新的属性和属性值变为attPatch容器对象
      5. 返回attrPatch对象容器
      6. attrPatch对象容器打入属性类型为ATTR和属性内容的补丁
      7. childrenWalk深度遍历子节点
    5. 当替换标签名时打入属性类型为REPLACE和新节点的补丁
  6. 给真实DOM打补丁doPatch函数传入真实DOM和patches补丁

    1. 定义finalPatches对象和rnIndex = 0
    2. patches补丁赋值给finalPatches对象
    3. 执行rNodeWalk函数传入真实节点

      1. 每次取值finalPatches[rnIndex++]rnIndex加1
      2. 将真实节点的子节点的类数组转为数组并遍历每一个子节点
      3. 递归嵌套的子节点reNodeWalk
      4. 当有补丁时去打补丁patchAction函数传入真实节点和真实节点的补丁
      5. 遍历每一个补丁
      6. 当补丁的类型为ATTR时遍历拿到属性底下所有的属性和属性值

        1. 如果有属性值时给真实节点设置属性
        2. 有属性但没有属性值时删除真实节点下的属性
      7. 当补丁的类型为TEXT时将真实节点的文本内容填入补丁里的text内容
      8. 当补丁的类型为REPLACE

        1. 如果新的节点是虚拟节点时将它render渲染出来创建
        2. 如果新的节点不是虚拟节点时将它作为文本节点创建
        3. 将原来的节点替换为新的节点
      9. 当补丁的类型为REMOVE时删除自己的节点

用户使用的顺序:

  1. 拿到真实节点const rDom = render(vDom1);
  2. 对比新老节点返回补丁包const patches = domDiff(vDom1, vDom2);
  3. 渲染页面renderDOM(rDom, document.getElementById('app'));
  4. 给真实DOM打补丁doPatch(rDom, patches);

源码地址:https://gitee.com/kevinleeeee/dom-diff-demo

路由权限

案例:后台路由权限管理

技术:koa2/vue/前后端

案例 - 图2

原理:

  1. 用户uid -> 后端API -> 路由权限API
  2. 后端 -> 用户对应路由权限列表 -> 前端 -> JSON
  3. JSON -> 树型结构化
  4. 树型结构化的数据 -> vue路由结构
  5. 路由结构动态 -> 静态路由
  6. 根据树型结构化的数据 -> 菜单组件
  1. //JSON
  2. [
  3. {
  4. id: 2,
  5. //parent id
  6. pid: 3,
  7. path:
  8. name:
  9. link:
  10. title:
  11. }
  12. ]

问题:如何做后端跨域?

  1. npm i koa2-cors -S
  1. //app.js
  2. const cors = require('koa2-cors');
  3. app.use(cors({
  4. origin: function (ctx) {
  5. return 'http://localhost:8080'
  6. }
  7. }));

前端项目顺序:

  1. 编写后台界面
  2. 请求后端接口
  3. 封装请求函数
  4. 将请求到的数据存入vuex
  5. actions里异步获取后端数据
  6. 将数据进行树型结构化格式化
  7. mutations里定义方法存储state
  8. 前端动态生成路由
  9. 将数据生成树状结构路由配置对象
  10. 配置路由守卫beforeEach

    1. 没有权限时:

      1. 请求后端数据
      2. 格式化后的树形结构的路由规则对象数组
      3. addroute新增到路由列表实现动态添加路由
      4. 编写各个地址的组件文件
      5. next回调分支处理实现访问不同地址显示不同的页面
    2. 有权限时:

      1. 直接访问不做守卫拦截
  11. 编写SideBar组件视图根据路由渲染路由列表
  12. 实现点击路由导航名称跳转到路由页面显示路由组件
  1. //注册一个全局前置守卫
  2. const router = new VueRouter({ ... })
  3. router.beforeEach((to, from, next) => {
  4. // 注意:确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。
  5. })

后端项目顺序:

  1. 编写用户表
  2. 编写路由数据表
  3. 遍历用户表和路由数据表,将符合条件的那一项放入容器返回给前端
  1. //前端项目目录:
  2. ├─package.json
  3. ├─src
  4. | ├─App.vue - 根组件/管理布局组件
  5. | ├─main.js - 入口文件/请求路由数据/动态生成路由列表/拼接路由列表/路由守卫
  6. | ├─views - 各路由视图组件
  7. | | ├─Course.vue
  8. | | ├─CourseAdd.vue
  9. | | ├─CourseInfoData.vue
  10. | | ├─CourseOperate.vue
  11. | | ├─Home.vue
  12. | | ├─NotFound.vue
  13. | | ├─Student.vue
  14. | | ├─StudentAdd.vue
  15. | | StudentOperate.vue
  16. | ├─store
  17. | | ├─actions.js - 定义请求路由列表数据函数/权限函数
  18. | | ├─index.js
  19. | | ├─mutations.js - 定义操作state的方法
  20. | | state.js - 中央状态管理池/hasAuth/userRouters数组
  21. | ├─services
  22. | | index.js - axios函数封装
  23. | ├─router
  24. | | index.js - 路由入口文件
  25. | ├─libs
  26. | | utils.js - 格式化路由列表为树形结构化/生成树状结构路由配置对象
  27. | ├─components - 页面布局组件
  28. | | ├─Header.vue
  29. | | ├─MenuItem.vue
  30. | | ├─PageBoard.vue
  31. | | SideBar.vue
  32. | ├─assets
  33. | | ├─css
  34. | | | common.css

前端源码地址:https://gitee.com/kevinleeeee/vue-router-admin-frondend-demo

后端源码地址:https://gitee.com/kevinleeeee/vue-router-admin-backend-demo