[ 紀錄 ] 實戰練習 - 留言版 (實作前端)


Posted by stella572322 on 2020-12-28

確認需求

1.會員註冊、登入、登出功能
2.留言新增、編輯、刪除功能
3.留言顯示有分頁功能
4.資訊安全防護功能

開發流程規劃

  • index.js首頁切版
    「建立」顯示留言功能
    「建立」新增留言功能
    「建立」留言顯示分頁功能
  • login.js 切版
    「建立」會員登入功能
    「建立」會員登出功能
  • register.js 註冊頁面切版
    「建立」帳號註冊功能

    • 設定 token 判斷登入狀態
      「修改」新增留言功能 - 非登入狀態不可使用
  • update_comment.js 切版
    「建立」編輯留言功能
    「建立」刪除留言功能

實際實作順序

1.

  • index.js首頁切版
  • login.js 切版
  • register.js 註冊頁面切版
  • update_comment.js 切版

2.

  • 首頁「建立」顯示留言功能
  • 首頁「建立」新增留言功能
  • 註冊頁面「建立」帳號註冊功能
  • 登入頁面「建立」會員登入功能
  • 首頁「建立」會員登出功能
  • 首頁「確認」使用者登入/登出狀態,「動態產生」對應的navbar的功能列
  • 首頁「修改」新增留言功能 - 非登入狀態不可使用
  • 編輯頁面「建立」編輯留言功能
  • 首頁「建立」刪除留言功能
  • 首頁「分頁」功能

檔案路由規劃

index.js (首頁)

首頁下方正常顯示出留言紀錄

** 操作細節(下圖):

const content_type = "application/json";
let API_Url = "https://student-json-api.lidemy.me";
let isLogin = false;
let nickname = null;

/*
串後端api資料
取得留言匿名名稱、時間、留言內容
加escape防護
*/
function getComments(page) {
    var xhr = new XMLHttpRequest();
    xhr.open(
        "GET",
        `${API_Url}/comments?_page=${page}&_limit=10&_sort=id&_order=desc`,
        true
    );
    xhr.setRequestHeader("content-type", content_type);
    xhr.onload = function () {
        if (xhr.status >= 200 && xhr.status < 400) {
            var result = JSON.parse(xhr.response); /*json文字轉成物件*/
            console.log(result);
            for (let i = 0; i < result.length; i++) {
                /*讓最新的留言至頂*/
                let element = document.createElement("li");
                element.classList.add("message_detail");
                if (isLogin && result[i].nickname === nickname) {
                    /*判斷登入狀態,比對該使用者是否具有第i篇留言的編輯刪除權限*/
                    element.innerHTML = `
          <div class='message_user'>
            <a class='avatar' href='#'>
              <img src='./photo/user-circle.svg'>
            </a>
          </div>  
          <div class='message_card'>
            <span class='nickname'>${escape(result[i].nickname)}</span> 
            <span class='create_time'>${new Date(
                            result[i].createdAt
                        ).toLocaleString()}</span> 
            <a class='edit' href='./update_comments.html?id=${result[i].id}'>
              <img src='./photo/pencil.svg'>
            </a>
            <a class='delete' data-value='${result[i].id}'>
              <img src='./photo/delete.svg'>
            </a>
            <p>${escape(result[i].body)}</p>
          </div>
        `;
                } else {
                    element.innerHTML = `
          <div class='message_user'>
            <a class='avatar' href='#'>
              <img src='./photo/user-circle.svg'>
            </a>
          </div>  
          <div class='message_card'>
            <span class='nickname'>${escape(result[i].nickname)}</span> 
            <span class='create_time'>${new Date(
                            result[i].createdAt
                        ).toLocaleString()}</span> 
            <p>${escape(result[i].body)}</p>
          </div>
        `;
                }
                document.querySelector(".massage_content").appendChild(element);
            }
        }
    };
    xhr.send();
}

/*${new Date(result[i].createdAt).toLocaleString()} 建立一個日期的物件,將createdAt代入,並透過toLocaleString()轉換成使用者所在時區的顯示*/

送出首頁留言按鈕 -> 成功在下方第一欄新增最新留言

** 操作細節(下圖):
使用者按下sumit新增留言,home成功顯示留言,加escape防護

/*
使用者按下sumit新增留言,home成功顯示留言
*/
document.querySelector(".comments_submit").addEventListener("click", (e) => {
    const target = e.target.getAttribute("class");
    console.log(target);
    if (target === "comments_submit_btn") {
        const textarea = document.querySelector(".create_content");
        //console.log(textarea.value);
        const value = textarea.value;
        var xhr = new XMLHttpRequest();
        xhr.open("POST", `${API_Url}/comments`, true);
        xhr.setRequestHeader("content-type", content_type);
        xhr.onload = function () {
            if (xhr.status >= 200 && xhr.status < 400) {
                //console.log('成功')
                document.querySelector(".massage_content").innerHTML =
                    ""; /*清空舊的留言資料,讓新留言往上移至頂*/
                getComments(); /*新增新的留言*/
            }
        };
        xhr.send(
            JSON.stringify({
                /*物件轉成文字json*/
                nickname: nickname,
                body: value,
            })
        );
        textarea.value = ""; /*傳完內容拿到response後,清空輸入欄 */
    }
});

確認使用者登入/登出狀態,動態產生對應的navbar的功能列

** 操作細節(下圖):

/* 確認使用者登入/登出狀態,動態產生對應的navbar的功能列*/
function setNavbar() {
    let element_register = document.createElement("li");
    let element_status = document.createElement("li");
    let comments_submit = document.querySelector(".comments_submit");
    element_register.classList.add("nav_li");
    element_status.classList.add("nav_li");
    if (isLogin === false) {
        element_register.innerHTML = `
      <a href='./register_page.html'>Register</a>
    `;
        element_status.innerHTML = `
      <a href='./login_page.html'>Login</a>
    `;
        comments_submit.innerHTML = `
      <form class='home_comments'>
        <textarea class='create_content' rows='7' placeholder='You need to sign up before you leave a message.'></textarea>
      </form>
    `;
    } else if (isLogin === true) {
        element_register.innerHTML = `
      <a href='./register_page.html'>Register</a>
    `;
        element_status.innerHTML = `
      <a href='#'class='nav_li_Logout'>Logout</a>
    `;
        comments_submit.innerHTML = ` 
      <form class='home_comments'>
        <textarea class='create_content' rows='7' placeholder='Leave a message.'></textarea>
      </form>
      <button class='comments_button'>
        <a><div class='comments_submit_btn'>Submit</div></a>
      </button>
    `;
    }
    document.querySelector(".nav_title_list").appendChild(element_register);
    document.querySelector(".nav_title_list").appendChild(element_status);
}

logout.js (首頁登出)

  • 登出按鈕 -> 導回 index(首頁)
    ** 操作細節(下圖):
    監聽navbar功能列的logout
    清空token和nickname資料
    首頁重新整理
document.querySelector(".nav_title_list").addEventListener("click", (e) => {
    console.log(e.target);
    const target = e.target.getAttribute("class");
    if (target === "nav_li_Logout") {
        localStorage.setItem("token", ""); /*清空token資料*/
        nickname = ""; /*清空nickname資料*/
        window.location.reload("/"); /*首頁重新整理*/
    }
});

整個瀏覽器剛載入的時候,利用localstorage拿出的token取得使用者資料

** 操作細節(下圖):

/*拿token取得使用者資料 */
function getMe(token) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", `${API_Url}/me`, true);
    xhr.setRequestHeader("authorization", `Bearer ${token}`);
    xhr.onload = function () {
        if (xhr.status >= 200 && xhr.status < 400) {
            var result = JSON.parse(xhr.response); /*json文字轉成物件*/
            console.log(result);
            if (result.ok === 1) {
                isLogin = true;
                nickname = result.data.nickname;
                console.log(nickname);
            } else {
                isLogin = false;
            }
        }
        getComments(1);  /*修改顯示留言功能加入身分比對*/
        setNavbar();
    };
    xhr.send();
}
/*整個瀏覽器剛載入的時候,利用localstorage拿出的token取得使用者資料 */
let token = localStorage.getItem("token");
getMe(token);

/* 確認使用者登入/登出狀態,動態產生對應的navbar的功能列*/
function setNavbar() {
    let element_register = document.createElement("li");
    let element_status = document.createElement("li");
    let comments_submit = document.querySelector(".comments_submit");
    element_register.classList.add("nav_li");
    element_status.classList.add("nav_li");
    if (isLogin === false) {
        element_register.innerHTML = `
      <a href='./register_page.html'>Register</a>
    `;
        element_status.innerHTML = `
      <a href='./login_page.html'>Login</a>
    `;
        comments_submit.innerHTML = `
      <form class='home_comments'>
        <textarea class='create_content' rows='7' placeholder='You need to sign up before you leave a message.'></textarea>
      </form>
    `;
    } else if (isLogin === true) {
        element_register.innerHTML = `
      <a href='./register_page.html'>Register</a>
    `;
        element_status.innerHTML = `
      <a href='#'class='nav_li_Logout'>Logout</a>
    `;
        comments_submit.innerHTML = ` 
      <form class='home_comments'>
        <textarea class='create_content' rows='7' placeholder='Leave a message.'></textarea>
      </form>
      <button class='comments_button'>
        <a><div class='comments_submit_btn'>Submit</div></a>
      </button>
    `;
    }
    document.querySelector(".nav_title_list").appendChild(element_register);
    document.querySelector(".nav_title_list").appendChild(element_status);
}

刪除留言按鈕 -> 首頁重新整理

** 操作細節(下圖):

/*
刪除留言
利用事件代理,取得垃圾桶和ID
串刪除API
*/
document.querySelector(".massage_content").addEventListener("click", (e) => {
    // console.log(e.target.parentNode.getAttribute("class"));
    // console.log(e.target.parentNode.getAttribute("data-value"));
    const delete_id = e.target.parentNode.getAttribute("data-value");
    const delete_button = e.target.parentNode.getAttribute("class");
    if (delete_button === "delete") {
        var xhr = new XMLHttpRequest();
        xhr.open("DELETE", `${API_Url}/comments/${delete_id}`, true);
        xhr.setRequestHeader("content-type", content_type);
        xhr.onload = function () {
            console.log(JSON.parse(xhr.response));
            if (xhr.status >= 200 && xhr.status < 400) {
                window.location.reload("/"); /*重新整理*/
            }
        };
        xhr.send();
    }
});

首頁(分頁功能)

** 操作細節(下圖):

/*分頁功能*/
let now_page = 1;
let total_page = null;
let pagination = document.querySelector(".pagination");

function getTotalComments() {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", `${API_Url}/comments`, true);
    xhr.setRequestHeader("content-type", content_type);
    xhr.onload = function () {
        if (xhr.status >= 200 && xhr.status < 400) {
            var result = JSON.parse(xhr.response); /*json文字轉成物件*/
            console.log(result);
            total_page = Math.ceil(result.length / 10); /*無條件進位*/
            pagination.innerText = `頁碼 : ${now_page} / ${total_page}`;
        }
    };
    xhr.send();
}

getTotalComments();

document.querySelector(".first").addEventListener("click", (e) => {
    if (now_page === 1) return;
    now_page = 1;
    setPage();
});

document.querySelector(".previous").addEventListener("click", (e) => {
    if (now_page === 1) return;
    now_page = now_page - 1;
    setPage();
});

document.querySelector(".next").addEventListener("click", (e) => {
    if (now_page === total_page) return;
    now_page = now_page + 1;
    setPage();
});

document.querySelector(".last").addEventListener("click", (e) => {
    if (now_page === total_page) return;
    now_page = total_page;
    setPage();
});

function setPage() {
    pagination.innerText = `頁碼 : ${now_page} / ${total_page}`;
    getComments(now_page);
}
加escape防護XSS攻擊
function escape(str) {
    return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

register.js (註冊頁面)

註冊送出按鈕 -> 導回首頁

監聽註冊sumit -> 串後端api -> 取得token
** 操作細節(下圖):

const content_type = "application/json";
let API_Url = "https://student-json-api.lidemy.me";

/*監聽註冊sumit,串後端api,取得token */
document.querySelector(".submit_btn").addEventListener("click", (e) => {
    const nickname = document
        .querySelector('input[name="nickname"]')
        .value.trim();
    const username = document
        .querySelector('input[name="username"]')
        .value.trim();
    const password = document
        .querySelector('input[name="password"]')
        .value.trim();
    if (!nickname || !username || !password) {
        /*偵測帳號或密碼未填、空白,利用.trim()去除字串前後空白 */
        alert("nickname, username and password are required");
        return; /*終止發Api */
    }
    console.log(nickname, username, password);
    var xhr = new XMLHttpRequest();
    xhr.open("POST", `${API_Url}/register`, true);
    xhr.setRequestHeader("content-type", content_type);
    xhr.onload = function () {
        if (xhr.status >= 200 && xhr.status < 400) {
            var result = JSON.parse(xhr.response); /*json文字轉成物件*/
            let token = result.token;
            localStorage.setItem("token", token); /*把通行證token存在localStorage*/
            window.location.replace("index.html"); /*返回首頁*/
        } else {
            let errorMessage = JSON.parse(xhr.response);
            alert(errorMessage.message); /*警示錯誤視窗*/
        }
    };
    xhr.send(
        JSON.stringify({
            /*物件轉成文字json*/
            nickname: nickname,
            username: username,
            password: password,
        })
    );
});

login.js (登入頁面)

登入送出按鈕 -> 導回 login.js (登入頁面)

監聽登入sumit -> 串後端api -> 取得token
** 操作細節(下圖):

const content_type = "application/json";
let API_Url = "https://student-json-api.lidemy.me";

/*監聽登入sumit,串後端api,儲存token */
document.querySelector(".submit_btn").addEventListener("click", (e) => {
    const username = document
        .querySelector('input[name="username"]')
        .value.trim();
    const password = document
        .querySelector('input[name="password"]')
        .value.trim();
    if (!username || !password) {
        /*偵測帳號或密碼未填、空白,利用.trim()去除字串前後空白 */
        alert("username and password are required");
        return; /*終止發Api */
    }
    console.log(username, password);
    var xhr = new XMLHttpRequest();
    xhr.open("POST", `${API_Url}/login`, true);
    xhr.setRequestHeader("content-type", content_type);
    xhr.onload = function () {
        if (xhr.status >= 200 && xhr.status < 400) {
            var result = JSON.parse(xhr.response); /*json文字轉成物件*/
            //console.log(result);
            let token = result.token;
            //console.log(token);
            localStorage.setItem("token", token); /*把通行證token存在localStorage*/
            window.location.replace("index.html"); /*返回首頁 */
        } else {
            let errorMessage = JSON.parse(xhr.response);
            alert(errorMessage.message); /*警示使用者視窗 */
        }
    };
    xhr.send(
        JSON.stringify({
            /*物件轉成文字json*/
            username: username,
            password: password,
        })
    );
});

update_commentS.js (編輯頁面)

編輯留言按鈕 -> 導到首頁
** 操作細節(下圖):

/*修改留言*/
document.querySelector(".edit_submit_btn").addEventListener("click", (e) => {
    const target = e.target.getAttribute("class");
    //console.log(target);
    if (target === "edit_submit_btn") {
        const textarea = document.querySelector("textarea");
        //console.log(textarea.value);
        const value = textarea.value;
        var xhr = new XMLHttpRequest();
        xhr.open("PATCH", `${API_Url}/comments/${id}`, true);
        xhr.setRequestHeader("content-type", content_type);
        xhr.onload = function () {
            if (xhr.status >= 200 && xhr.status < 400) {
                window.location.replace("index.html"); /*返回首頁*/
            }
        };
        xhr.send(
            JSON.stringify({
                /*物件轉成文字json*/
                body: value,
            })
        );
    }
});

#GUESTBOOK







Related Posts

React(10) - useState & useReducer & useEffect

React(10) - useState & useReducer & useEffect

Day04: state 及 props 介紹

Day04: state 及 props 介紹

[第一週] 認識 Command Line

[第一週] 認識 Command Line


Comments