首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >任务列表应用程序,它允许从列表中添加和删除任务。

任务列表应用程序,它允许从列表中添加和删除任务。
EN

Code Review用户
提问于 2020-12-09 23:54:13
回答 3查看 183关注 0票数 2

我写了我的第一个网络应用程序。请让我知道我应该改进什么。我想尽可能地写干净的代码。

App是关于从列表中添加和删除任务。后端用Django编写,前端用Sass和JavaScript编写。

在“项代码”视图下面。整个项目可以在链接中看到:

链接到GitHub上的代码

演示:

链接到heroku上的应用程序

项目代码视图:

  1. JavaScript
  2. 沙斯
  3. HTML
代码语言:javascript
复制
// Start with first item
let counter = 0;

// Load 6 items at a time
const QUANTITY_START = 6; 
const QUANTITY_TO_ADD_ON_SCROLL = 3; 

let isListEmpty = true;
let removed = [];
let areMoreItems = true;

const HIDE_ANIMATION_DURATION_OPACITY = 1500

document.addEventListener('DOMContentLoaded', () => {

  // SETTING LISTENERS:
  window.onscroll = () => {
    if (areMoreItems && isScrolledToBottom()){
      const scroll = window.onscroll;
      window.onscroll = "none";
      load(QUANTITY_TO_ADD_ON_SCROLL);
      setTimeout(() => {
        window.onscroll = scroll;
      }, 1000);
    } 
  }

  deleteItemListener();
  confirmDeleteButtonListener();
  // END OF SETTING LISTENERS

  load(QUANTITY_START);
})

function confirmDeleteButtonListener() {
  const confirm_button = document.querySelector(".confirm-delete-button");
  confirm_button.addEventListener("click", () => {
    confirm_button.value = removed.join();
  })
}

function deleteItemListener() {
  document.querySelector(".main").addEventListener("click", function(event) {
    if(event.target.nodeName === "I") {

      const itemElement = event.target.parentElement.parentElement;
      const item_id = itemElement.querySelector('.id').innerHTML;
      removed.push(item_id);

      itemElement.style.animationPlayState = 'running';
      // itemElement.addEventList;

      document.querySelector(".confirm-delete-button").disabled = false;
    }
  });
}

function isScrolledToBottom() {
  return window.innerHeight + window.scrollY >= document.body.offsetHeight
}

function load(quantity) {

  const start = counter;
  const end = start + quantity - 1;
  counter = end + 1;

  fetch(`itemslist?start=${start}&end=${end}`)
  .then(response => response.json())
  .then(data => {
    if (data.itemslist.length > 0) {
      isListEmpty = false;
      data.itemslist.forEach(addItem);
    } else {
      areMoreItems = false;
      if (isListEmpty) {
        addEmptyListInfo();
      }
    }
  });
}

function addEmptyListInfo() {
  const item = document.createElement('section');
  item.className = "empty-list";
  item.innerText = "Your list is empty";
  document.querySelector('.main-items').append(item)
}

function addItem(item) {
  const itemNode = document.createElement('section');
  itemNode.className = "todo-item";
  itemNode.innerHTML =     
    `${item.id}
    
      
    
    ${item.name}
    Details:
    ${item.details}
    Deadline: ${item.date}`;
  document.querySelector('.main-items').append(itemNode);
};
代码语言:javascript
复制
.nav {
  .li {
    background-color: transparent;
    border: none;
    cursor: pointer;
    font-family: 'Caveat', cursive;
    color: #fff;
    outline: none;
  }

  form {
    display: flex;
    display: -webkit-box;
    display: -ms-flexbox;
    flex-direction: row;
  }
}

.main-items {
  opacity: 0.9;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  flex-direction: row !important;
  justify-content: space-evenly;
  flex-wrap: wrap;

  .todo-item {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-orient: vertical;
    -webkit-box-direction: normal;
        -ms-flex-direction: column;
            flex-direction: column;
    color: #000;
    height: 300px;
    width: 400px;
    padding: 20px;
    margin: 0 15px;
    position: relative;
    -webkit-animation-name: hide;
            animation-name: hide;
    -webkit-animation-duration: 1.5s;
            animation-duration: 1.5s;
    -webkit-animation-fill-mode: forwards;
            animation-fill-mode: forwards;
    -webkit-animation-play-state: paused;
            animation-play-state: paused;
    background-image: url(../img/sticky1.png);
    background-size: 400px 300px;
    line-height: 100%;

    .todo-item-name {
      display: -webkit-box;
      display: -ms-flexbox;
      display: flex;
      -webkit-box-pack: center;
         -ms-flex-pack: center;
       justify-content: flex-start;
      -webkit-box-align: center;
         -ms-flex-align: center;
            align-items: center;
      margin-bottom: 10px;
      margin-left: 40px;
      margin-right: 25px;
      height: 40px;
      font-size: 25px;
      font-weight: bold;
      margin-top: 20px;
      padding: 5px;
      outline: none;
      color: #000;
      overflow-y: auto;
      scrollbar-width: thin;
      scroll-padding: 0;
      scroll-margin: 0;
      white-space: nowrap;
      cursor: default;
    }

    .details {
      border-radius: 5px;
      overflow-wrap: break-word;
      overflow-y: auto;
      height: 100px;
      padding: 10px;
      margin-left: 40px;
      margin-right: 25px;
      text-align: justify;
      resize: none;
      margin-bottom: 10px;
      outline: none;
      font-size: 22px;
      scrollbar-width: thin;
      scroll-padding: 0;
      scroll-margin: 0;
      cursor: default;
      }

    .x-wrapper {
      position: absolute;
      right: 0;
      top: 0;
      margin: 10px;
      font-size: 20px;
      -webkit-transition: all 0.2s;
      transition: all 0.2s;
      cursor: pointer;
      border: none;
      color: #e9c1c1;
      background-color: transparent;
    }

    .x-wrapper:hover {
      color: #fff;
    }
  }

  .empty-list {
    font-size: 44px;
    margin-bottom: 40px;
    background-color: rgba(0, 0, 0, 0.5);
    padding: 10px;
    padding-right: 15px;
    border-radius: 10px;
    text-align: center;
    pointer-events: none;
  }

  .todo-item label {
    font-size: 20px;
    margin-bottom: 2px;
    margin-left: 40px;
    margin-right: 25px;
  }

  .date {
    text-align: right;
    margin-right: 25px;
    pointer-events: none;
  }
}

@-webkit-keyframes hide {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes hide {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
代码语言:javascript
复制
{% extends "TodoListApp/base.html" %}
{% load static %}

{% block style %}
  
  
  Items
{% endblock %}

{% block body %}
  
    
    
    
  

  
    
      Add new item
    
    
      {% csrf_token %}
      add 10 items
      confirm deleting
    
    
      Logout
    
  

  
    
      Hello :{{ name }}:
      Here is a list of your activities:
    
    

    
  

  
  
{% endblock %}
EN

回答 3

Code Review用户

发布于 2020-12-10 17:37:58

添加元素

正如"CertainPerformance's“中所指出的,通过HTML添加内容在这种情况下可能会带来安全风险。

但是,应该始终避免通过innerHTML添加元素,因为它效率很低,噪音很大,很难阅读和维护(因为字符串中没有代码格式)。

使用一些简单的函数,您可以避免非常慢的setter .innerHTML,并以一种高效的方式添加内容,而无需使用DOM。

下面的一组函数可以满足大多数HTML创建需求

  • tag创建一个元素并添加属性,返回新标记。
  • append将兄弟姐妹附加到元素中,并返回父元素。
  • query查询返回查找元素的元素(如果找到)

代码

代码语言:javascript
复制
const tag = (tag, props = {}) => Object.assign(document.createElement(tag), props);
const append = (par, ...sibs) => sibs.reduce((p, sib) => (p.appendChild(sib), p), par);
const query = (qStr, el = document) => el.querySelector(qStr);

并将您的代码替换为

代码语言:javascript
复制
function load(quantity) {
    const url = `itemslist?start=${counter}&end=${counter + quantity - 1}`;
    const mainItems = query(".main-items");
    counter = end + 1;

    fetch(url)
        .then(response => response.json())
        .then(data => {
            if (data.itemslist.length > 0) {
                isListEmpty = false;
                append(mainItems, ...data.itemslist.map(addItem));
            } else {
                areMoreItems = false;
                isListEmpty && append(mainItems, addEmptyListInfo());
            }
        });
}

function addEmptyListInfo() {
  return tag("section", {className: "empty-list", textContent: "Your list is empty"});
}
function addItem(item) {
    return append(tag("section", {className: "todo-item"}),
        tag("p",     {hidden: true, className: "id", textContent: item.id}),
        append(tag("div", {name: "delete-button", value: item.id, className:"x-wrapper"}), 
            tag("i", {className: "far fa-times-circle"})
        ),
        tag("h2",    {name: "name", className: "todo-item-name", textContent: item.name}),
        tag("label", {for: "details", textContent: "Details:"}),
        tag("p",     {className: "details", name:"details", textContent: item.details}),
        tag("p",     {className: "date", textContent: `Deadline: ${item.date}`}),
    );
}

使代码为您工作!

就我个人而言,我使用一种修改,结合tagquery,并将其命名为$ (因为我从不使用jQuery),并用帮助器函数命名append $,以简化公共属性。例如$.text$.classText$.class、create属性、textContentclassNametextContent以及className加上任何其他属性。

有几十种方法可以简化向页面中添加内容。最好是闪电般的快速,当使用DOM调用构建页面时,您真的可以注意到速度的提高。

代码语言:javascript
复制
function addItem(item) {
    return ( $("", $.class("todo-item")),
        $("", $.classText("id", item.id, {hidden: true})),
        #qcStackCode#( $("", $.class("x-wrapper", {name: "delete-button", value: item.id}), 
            $("", $.class("far fa-times-circle"))
        ),
        $("", $.classText("todo-item-name", item.name, {name: "name"})),
        $("", $.text("Details:", {for: "details"})),
        $("", $.classText("details", item.details, {name:"details"})),
        $("", $.classText("date", `Deadline: ${item.date}`))
    );
}
票数 2
EN

Code Review用户

发布于 2020-12-10 00:23:32

非常好的代码,我会删除变量isListEmpty,因为它根本不做任何事情,您的if else分支已经检查了空列表,然后去掉了一个丑陋的全局变量。

考虑通过包装函数来消除所有的全局变量,这些函数需要将它们转换为类或其他函数。这样,变量就不是全局的,甚至可以将其中一些变量作为参数传递(例如:QUANTITY_START)。

addEmptyListInfoaddItem有相同的第一行和最后一行。您可以将它们中的中间提取到单独的函数中,并创建第三个通用函数以避免冗余,并确保添加新的“节”只在一个地方。

我不喜欢fetch url硬编码到.js文件中。考虑将其作为数据属性提取到html中,这样就可以控制url是如何生成的,如果更改django路由,则不必在多个地方修改url。

考虑将滚动的侦听器提取为单独的函数,例如:https://stackoverflow.com/a/12009497/4070660

至于scss,您可以使用自动修复程序来处理供应商前缀,并稍微清理代码。

大多数情况下,最好使用其他单位而不是像素。考虑使用em/rem或vw/vh/

在检查html :-)时,什么都没有想到。)

票数 1
EN

Code Review用户

发布于 2020-12-10 03:05:03

使用 const when (可能)--使用let是对代码读者的警告,表明将来可能会重新分配变量。如果实际情况并非如此,则对更好使用const变量进行removed;考虑一条短袜

removed 数组?它的用途乍一看并不完全清楚,我必须阅读几节代码才能理解它正在做什么。考虑将它命名为itemIdsToBeRemoved。此外,counter可能更好地命名为lastItemIdLoaded或类似的东西。

使用 camelCase for普通变量,这是JS中压倒一切的惯例:例如,考虑使用itemIdconfirmButton。(顶部的全大写数值常数很好。)

DOMContentLoaded部分在这里做了4件事:

  • 添加滚动监听器
  • 调用deleteItemListenerconfirmDeleteButtonListenerload

要在风格上保持一致,请考虑将滚动监听器抽象为一个单独的函数,就像您对其他侦听器所做的那样。(但这些函数本身并不是事件侦听器-它们添加了事件侦听器,因此可能会调用它们,例如,addDeleteConfirmListener)

Load数量复杂性处理从服务器请求的动态项目数,对我来说似乎有点过于复杂了。除非您期望有大量的有效负载,否则我认为应该立即获取所有项目,然后对客户端上应该显示的内容进行排序。同时发送所有项目也将减少用户的可见延迟。

如果要立即发送所有项,请考虑将数据插入到HTML中,而不是在页面加载后使用请求。你本可以

代码语言:javascript
复制
{"itemslist": [{"id": 191, "user_id": 26, "name": "name", "details": "details", "date": "2020-12-09"}]}
代码语言:javascript
复制
const items = JSON.parse(document.querySelector('.items-data').textContent);

这将使页面为用户加载得更快;假设应用程序以前已经加载过,并且您有一个合理的缓存策略,那么他们将不必等待两次往返到您的服务器来显示待办事项,而只需要一次。

Return早期避免缩进地狱如下:

代码语言:javascript
复制
function deleteItemListener() {
  document.querySelector(".main").addEventListener("click", function(event) {
    if(event.target.nodeName === "I") {
      // big block of code

可以是

代码语言:javascript
复制
function deleteItemListener() {
  document.querySelector(".main").addEventListener("click", function(event) {
    if (event.target.nodeName !== "I") {
      return;
    }
    // big block of code

甚至是

代码语言:javascript
复制
function deleteItemListener() {
  document.querySelector(".main").addEventListener("click", function(event) {
    if (event.target.nodeName === "I") {
      deleteItem(event.target);
    }

nodeName?不清楚nodeName of i是否对应于X图标。考虑给i一个说明它是什么的类(如delete-icon,允许您执行e.target.matches('.delete-icon') )。

使用 .closest当导航到父母时-传递给.closest的选择器可以提供更多的信息,说明您要导航到哪个父服务器,并且它比使用.parentElement.parentElement更短:

代码语言:javascript
复制
const itemElement = event.target.closest('section');

innerHTML ** 、** textContent**和** innerText

  • 避免innerText。它是几乎从来没有你想要的 -它有一些令人困惑的行为。除了在很少的情况下需要使用innerText的S特有的格式怪癖检索文本,最好使用.textContent.innerHTML
  • 仅在设置或检索HTML时使用.innerHTML。如果您只想设置或检索纯文本,请使用.textContent.textContent更快、更安全,而且在语义上更合适。

与上述有关:

innerHTML通过用户输入的安全风险我可以通过在todo中输入以下描述来让您的站点执行任意代码:

代码语言:javascript
复制

我可以在其他文本中伪装标记,然后告诉另一个用户:“嘿,尝试用这个描述做一个新的事情,你不会相信接下来会发生什么!”--然后如果他们上当了,就窃取他们的登录凭据。

问题是,您使用的是:

代码语言:javascript
复制
  itemNode.innerHTML =     
    `${item.id}
    ...
    ${item.details}

像这样的HTML字符串的直接连接应该总是值得怀疑的。如果插值的值不是100%可信的,那么您可能会遇到问题。要修复它,在创建元素后通过设置.textContent将不值得信任的值填充到DOM中,如下所示:

代码语言:javascript
复制
itemNode.innerHTML = `
  ${item.id}
  
    
  
  
  Details:
  
  Deadline: ${item.date}
`;
itemNode.querySelector('h2').textContent = item.name;
itemNode.querySelector('.details').textContent = item.details;

如果iddate是通过后端以安全的形式保存在数据库中的话,我希望它们是安全的,尽管代码没有问题--比如id应该保存为数字,日期应该保存为unix时间戳或日期。如果它们不是,而且您也不能因为任何原因,那么在保存它之前,您需要手动清除来自用户的任何内容。如果由于某种原因也不能这样做,那么就必须使用DOM方法而不是通过HTML级联将iddate属性插入到上面的DOM中,但最好是确保它们提前达到预期的安全格式。

您还可以在将namedetails保存到数据库之前,通过消毒液将它们直接连接起来。

票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/253287

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档