zuppachu / Joanne-s-Learning-Blog

程式導師實驗計畫第二期 之 Coding 學習紀錄。
2 stars 0 forks source link

[ FE102 ] 前端必備:JavaScript #25

Open zuppachu opened 5 years ago

zuppachu commented 5 years ago

補充資料 Lesson 4-1 JavaScript 筆記

FE102 課程簡介

三個重要面向:介面、事件、資料!

  1. 如何改變介面?
  2. 如何監聽事件並作出反應?
  3. 如何跟伺服器交換資料?
zuppachu commented 5 years ago

JavaScript 與瀏覽器的溝通

執行 JS 的一百種方式

可以在哪裡執行 JS?

  1. node.js
  2. 瀏覽器上面
    
    // 在 index.html 
    <script src="./index.js"></script>

// ./ 代表同一個資料夾底下


以上兩個為不同的執行環境,因此語法上有一些差異。

那兩者的差別在哪呢?
有些時候某些語法可能在瀏覽器上面不支援,或是在 node.js 不支援。例如:Node.js 引入模組的`require` 用法在瀏覽器上無法使用。

## DOM 是啥毀?

全名是:Document Object Model。

為 JS 跟瀏覽器之間的橋樑。可以說是瀏覽器提供 DOM,讓程式語言,如:JS 可以操控 HTML 內的元素,用來改變介面(網頁的面貌)。

<!----- DOM 操作範例 ------>

## 如何選到想要的元素

>最好把 `<script></script>` 放在 `<body>` 結束之前,因為瀏覽器是由上而下跑的。如果先放前面的話, DOM 會沒抓取到跑出空白的答案

- getElementsByTagName

```javascript
const elements = document.getElementsByTagName('div')

但需注意:此方法只回傳 document 第一個符合的元素

用法:

  1. id 用 #
  2. class 用 .
  3. tag 用 <tag 的名字>
  4. 關係選擇器 > + ~
    <body>
    <div id="block">
        hello
    </div>
    <div class="box"></div>
    <script>
        const elements = document.querrySelector('#block')
        cosnt ans = document.querrySelector('.box')
        console.log(elements)
    </script>
    </body>
    // 如: id 底下的 <a>
    const elements = document.querrySelector('#block > a')

改變 CSS

改變元素的 Class

    <style>
        .active {
            background: blue;
        }
    </style>

<body>
    <div id="block">hello</div>
    <div class="box">Ciao</div>
    <script>
        const elements = document.querrySelector('#block')
        elements.classList.add('active')
    </script>
</body>

加上一個名叫 'active' 的 class 在 <div id='block'> 的裡面。

移除的話用: element.classList.remove('active')

還有一種是:toggle。這個很妙,比如你寫:

element.classList.toggle('active')
// 本來沒有 -> 變成新增
// 本來有 -> 變成移除

抓取、改變文字或標籤內容:

// 得 // ciao // hello

- innerHTML:
顯示標籤內的所有內容
與 innerText 不一樣的地方是可以寫入標籤
`element.innerHTML = '<div>icecream</div>'`
```javascript=
<body>
    <div id="block">
        Ciao
        <a>hello<a>
    </div>

    <script>
        const element = document.querrySelector('#id > a')
        console.log(element.innerHTML = 'I am a link')
    </script>
</body>

// 得 a= I am a link

// 得

Ciao hello

## 插入與刪除元素:appendChild 與 removeChild

- 刪掉元素:
首先要先知道他的上一層(parent)是誰 ,才可能刪除元素。

const element = document.querySelector('#block') element.removeChild(document.querySelector('a'))


- 新增元素:

const element = document.querySelector('#block') cosnt item = document.createElement('div') // 建立新 parent element.appendChild(document.querySelector('p'))



need to checkout more api ?
zuppachu commented 5 years ago

JavaScript 網頁事件處理

常用 event 事件

eventListener 與 callback function

就是選定頁面上的某一元素,然後監測此元素:

  1. 會發生什麼事?
  2. 之後的後續動作是什麼?

而上述行為會發生,是因為有人點擊某元素進而「觸發」此動作。

最經典的栗子是 click 點擊事件,如下。

const element = document.querySelector('#block')
element.addEventListener('click', onClick)
//監聽的事件名稱 = click
//發生的動作是 onClick,它是一個 callback function
function onClick() {
    alert('click!')
}

比較常見/偷懶的寫法是直接宣告在裡面:

const element = document.querySelector('#block')
element.addEventListener('click',function() {
    alert('click!')
})

這種寫法因為沒有名字又叫做匿名函式,但他跟 callback function 沒有關係喔~

總之,上述程式碼代表:

  1. element 綁定了一個 click 事件
  2. 當滑鼠點擊到 element 時,觸發了 element 的 click 事件
  3. 之後會進行 callback function

它的目的就是為了不要讓其他事情被阻塞 (block),而延伸出來的方式。 概念有點像是美食街的呼叫器,『好了後再叫我』!

document.getElementById('btn').addEventListener('click', function() {
  ...
})

//arrow function 寫法:
document.getElementById('btn').addEventListener(‘click', () => {
})

上面的 function: 等到 click 事件發生後,才呼叫裡面的函式 = callback function

event or e 是啥毀?

const element = document.querySelector('#block')
element.addEventListener('click',function(e) {
    console.log(e)
})

得 e 是個 MouseEvent。 若要知道我到底在網頁上點(click)了哪個元素可用: e.target

const element = document.querySelector('input')
element.addEventListener('keydown', function(e) {
    console.log(e.key)
})
// 在輸入窗內輸入的每個按鍵都會被印出來(keydown)
// 不知道哪個關鍵字的時候查文件: MDN/Web/Events/keydown

實作 event:

<body>
    <button class='change-btn'>change</button>
    <script>
        const element = document.querySelector('.change-btn')
        element.addEventListener('click', function(e) {
        document.querySelector('body').classList.toggle('active')
        })
    </script>
</body>

表單事件處理:onSubmit

回顧:表單實作筆記

通常用在表單驗證上。

<body>
    <form class="form">
        <div>
            username:<input name='username'/>
            password:<input name='password1' type='password' />
            password check:<input name='password2'type='password'/>

        </div>

        <input type='submit'/>
    </form>
    <script>
        const element = document.querySelector('.form')
        element.addEventListener('submit', function(e) {
            const input1 = document.querySelector('input[name=password1]')
            const input2 = document.querySelector('input[name=password2]')

            // 驗證密碼是否相同,如果不同的話,防止表單送出
            if(input1.value !== input2.value) {
                alert('密碼錯誤')
                e.preventDefault // 防止預設發生。(預設就是表單送出)
            }
    })    
    </script>
</body>

阻止預設行為:preventDefault

除了上述表單事件外,還有以下幾個適用 preventDefault 的情況:

  1. 超連結 <a>:阻止跳網址
  2. <input>keypress 事件:阻止輸入按鍵

(不懂為什麼用 link 卻要阻止 link 的行為???)

比複雜更複雜的「事件傳遞機制」 (瀏覽器傳遞機制)

 <div class="outer">
    outer
    <div class="inner">
      inner
      <a href="#" class="btn">btn</a>
    </div>
</div>

<script>
 addEvent('.outer')
 addEvent('.inner')
 addEvent('.btn')

 function addEvent(className) {
     document.querySelector(className)
        .addEventListener('click', function(){
            console.log(className)
        })
 }

 </script>

裡面的outerinnerbtn 分別監聽 click 事件。

但當我們點擊最內層元素btn 時,連帶上兩層的元素(inner 和 outer)也都被觸發了,這詭異的現象叫「冒泡事件」。

事件傳遞機制詳解:捕獲與冒泡

事件傳遞機制有三階段:

  1. Capture Phase
  2. Target Phase
  3. Bubbling Phase
// PhaseType
const unsigned short      CAPTURING_PHASE     = 1; 捕獲 
const unsigned short      AT_TARGET           = 2; 元素本身
const unsigned short      BUBBLING_PHASE      = 3; 冒泡

當點擊 td 時,這一個點擊的事件會先從 window 開始往下傳,一直傳到 td 為止,到這邊就叫做「捕獲階段」。

接著事件傳遞到 td 本身,這時候叫做「AT_TARGET」。

最後事件會從 td 一路傳回去 window,這時候叫做「冒泡階段」。

但其實只要記住兩個原則:

  1. 先捕獲,後冒泡
  2. 當事件傳到 target 本身,沒有分捕獲跟冒泡

改變事件監聽的時機

要改變,需在 .addEventListener 方法加上第 3 個參數,為 boolean 值。 事件傳遞方式是無法改變的,只能改變監聽的時機點。

 addEvent('.outer')
 addEvent('.inner')
 addEvent('.btn')

 function addEvent(className) {
     document.querySelector(className)
        .addEventListener('click', function(e){
            console.log(className, '冒泡', e.eventPhase)
        }, false) // false : 放在冒泡階段上 (預設)

    document.querySelector(className)
        .addEventListener('click', function(e){
            console.log(className, '捕獲', e.eventPhase)
        }, true) // true : 放在捕獲階段上
 }

可是噢,當我們重新點擊最內層的 btn 時,我們看到的順序是: 1 > 2 > 3

.outer 捕獲 1
.inner 捕獲 1
.btn 冒泡 2 // 注意
.btn 捕獲 2 // 注意
.inner 冒泡 3
.outer 冒泡 3

注意:

別向上級回報:stopPropagation

= 阻止事件繼續傳遞

你加在哪裡,事件的傳遞就斷在那邊,不會繼續往下傳遞。

現在我們將前面的例子,改成先「捕獲」再「冒泡」~

function addEvent(className) { document.querySelector(className) .addEventListener('click', function (e) { console.log(className); }, true) document.querySelector(className) .addEventListener('click', function (e) { console.log(className); e.stopPropagation(); // 阻止冒泡事件傳遞 }, false) };

接著一樣點擊 `btn` ,會輸出如下:

.outer 1 .inner 1 .btn 2 .btn 2

可以看出來捕獲階段(1) 和 At_Target(2) 繼續傳遞,但在冒泡階段(3)則被阻止了。

- 那再來看看,如果改成「阻止捕獲事件」呢?
```javascript=
function addEvent(className) {
  document.querySelector(className)
    .addEventListener('click', function (e) {
      console.log(className, '捕獲');
      e.stopPropagation(); // 阻止捕獲事件傳遞
    }, true)

  document.querySelector(className)
  .addEventListener('click', function (e) {
    console.log(className, '冒泡');
  }, false)
};

一樣再點擊一次 btn 得到下列結果:

.outer 1

只得到 outer!Why oh why?

因為事件傳遞的順序是根據 1 > 2 > 3,而我們這次在捕獲階段就阻止了,根本沒法傳到 btn,所以後續的 2、3 階段就沒有執行下去啦。

這邊指的「事件傳遞被停止」,意思是說不會再把事件傳遞給「下一個節點」,但若是你在同一個節點上有不只一個 listener,還是會被執行到。

若是你想要讓其他同一層級的 listener 也不要被執行,可以改用e.stopImmediatePropagation();

話鋒一轉,若點擊outer 呢?

.outer 2
.outer 2

因為 .outer 上面並沒有元素可以傳遞,所以也沒有捕獲階段 (1),兩次都輸出都是在階段二: target phase。

由此可以觀察到一件有趣的事:

如果該元素是最上層的元素,事件傳遞方式指定為捕獲,那底下所有的元素事件傳遞都會被停止。

阻止「後續」相同事件:stopImmediatePropagation

阻止之後同一層級的 listener 不要被執行到,可用此法。

<button id="myBtn">Try it</button>

<script>
function myFunction(event) {
  alert ("Hello World!");
  event.stopImmediatePropagation(); // Try to remove me
}

function someOtherFunction() {
  alert ("I will not get to say Hello World");
}

var x = document.getElementById("myBtn");
x.addEventListener("click", myFunction);
x.addEventListener("click", someOtherFunction);
</script>

得到:彈出視窗的Hello World!,但若刪除了 Try to remove me 那段,會得到兩個彈出視窗:

  1. Hello World!
  2. I will not get to say Hello World

因此可看出 e.stopImmediatePropagation() 是阻止之後相同層級的 listener。

事件代理 Delegation

它的存在是為了減少 DOM 的操作,以提高效率。

假如說我們有 100 li,若為每個 li 都添加事件,會造成 DOM 抓取次數太多,瀏覽器效能降低。為了不要重複做一樣的事情,我們可用事件代理方式!

而事件代理是利用「事件傳遞的冒泡原理」,將子元素的監聽事件綁在父元素上。

例如:當我們在最外層添加點擊事件時,裡面的 ullia 的點擊事件都會冒泡到最外層的節點上,委託它代理執行事件。

其中好處是:

  1. 若有很多子元素,就無需一個一個綁定監聽事件。
  2. 動態新增來的子元素,也會冒泡到上層,因此不怕沒監聽到它。
<!DOCTYPE html>
<html>
<body>
  <ul id="list">
    <li data-index="1">1</li>
    <li data-index="2">2</li>
    <li data-index="3">3</li>
        <a>點我<a>
  </ul>

<script>

document.getElementById('list').addEventListener('click', (e) => {
  console.log(e.target.getAttribute('data-index'));
})

</script>
</body>
</html>

補充

DOM 的事件傳遞機制:捕獲與冒泡 該掌握:

  1. 事件傳遞的原則與順序
  2. 了解 e.preventDefault and e.stopPropagation的差別
  3. Delegation
zuppachu commented 5 years ago

綜合示範一:簡易密碼產生器

請看 codepen

<!DOCTYPE html>
<html>
    <head>
        <style>
            .box {
                border-radius: 5px;
                width: 40%;
                display: flex;
                flex-direction: column;
                margin: 80px auto;
                background: lightblue;

                }

                label {
                text-align: center;
                margin-top: 5px;
                }

                h2 {
                text-align: center;
                }

                button {
                width: 30%;
                height: 30px;
                margin: 5px auto 25px auto;
                border-radius: 5px;
                background: blueviolet;
                font-size:14px;
                color: white;
                font-size: 15px;
                }

                .result {
                width :55%;
                height: 40px;
                background: whitesmoke;
                margin: 10px auto;
                padding: 5px 10px 0px 10px;;
                border-radius: 5px;
                text-align: center;
                text-justify: kashida;
                }

                .opt {
                margin: 30px auto;
                }

                .opt ~ label {
                padding: 5px;
                }    
        </style>
</head>
<body>
        <div class="box">
                <h2>簡易密碼產生器</h2>

                <div class="result"></div>

                <div class="opt">
                        <label><input type="checkbox" name='num'>Numbers</label>
                        <br>
                        <label><input type="checkbox" name='upper_en'>Uppercase English</label>
                        <br>
                        <label><input type="checkbox" name='lower_en'>Lowercase English</label>
                </div>

                <button class="btn">產生</button>
              </div>
              <script>
                  // 所有事情要在按下「產生」後進行,所以監聽「產生按鈕」
                  document.querySelector('.btn').addEventListener('click', function(e) {
                    /* 想做出沒有選擇任何選項無法按下按鈕
                    if (document.querySelectorAll('.opt') !== document.querySelectorAll('.opt').checked) {
                        alert('plz choose one option');
                        e.preventDefault();
                    }
                    */

                    let avaChar = '';

                    if (document.querySelector('input[name = num]').checked) {
                        avaChar += '0123456789'
                    };

                    if (document.querySelector('input[name = upper_en]').checked) {
                        avaChar += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
                    };

                    if (document.querySelector('input[name = lower_en]').checked) {
                        avaChar += 'abcdefghijklmnopqrstuvwxyz'
                    };

                    let ans = '';
                    for (let i = 0; i < 10; i += 1) {
                        const number = Math.floor(Math.random() * avaChar.length);
                        ans += avaChar[number];
                    }

                    document.querySelector('.result').innerText = ans;

                    }

                    )
              </script>
</body>

</html>
zuppachu commented 5 years ago

綜合示範二:動態比單通訊錄

參考:codepen

<!DOCTYPE html>
<html>
    <head>
        <style>
            input {
                border: 1px solid grey;
                background: gainsboro;
                border-radius: 5px;
            } 
            .container {
                margin-left: 40px; 
                width: 60%;
            }

            h2 {
                margin: 40px auto;
            }

            .add-btn {
                border: 1px solid balck;
                margin-left: 0px;
                margin-bottom: 20px;
                background: white;
                border-radius: 5px;
            }

            .delete {
                border: 1px solid black;
                background: white;
                border-radius: 5px;
            }

        </style>
    </head>
    <body>
        <div class="container">
                <h2>動態表單通訊錄</h2>

                <div>
                    <button class="add-btn">新增聯絡人</button>
                </div>
                <div class="contacts">
                    <div class="row">
                        Name:<input name="name" />
                        Mobile:<input name="mobile">
                        <button class="delete">刪除</button>
                </div>
                </div>
        </div>
        <script>
            document.querySelector('.add-btn').addEventListener('click', function(e) {
                const div = document.createElement('div');
                div.classList.add('row');
                div.innerHTML = `
                        Name:<input name="name" />
                        Mobile:<input name="mobile">
                        <button class="delete">刪除</button>
                `
                document.querySelector('.contacts').appendChild(div)
                }
            )

            document.querySelector('.contacts').addEventListener('click', function(e) {
                if (e.target.classList.contains('delete')) {
                    document.querySelector('.contacts').removeChild(e.target.closest('.row'))
                }
            })

        </script>

    </body>
</html>
zuppachu commented 5 years ago

如何在瀏覽器上儲存資料?

網頁的資料都存在哪裡?為什麼換台電腦購物車就清空了?

zuppachu commented 5 years ago

網頁與伺服器的溝通

參考: [ MTR01 ] - Lesson 4-2 之 HTTP 基礎與 Ajax 筆記

API 與 網頁伺服器

Client --- request ---> Server
Server --- response ---> Client : HTML or JSON

用 「node.js 呼叫 API」與「在網頁上呼叫」的根本差異是什麼?

名稱 被限制住? 備註
node.js no
瀏覽器上的 JS yes 1. 被瀏覽器規則限制住。
2. 瀏覽器會加上一些東西:版本或是額外資訊

網頁前端傳資料到後端的方式:

參考:

  1. 筆記 - 前端跟後端溝通的方法(怎麼從前端發 request ?)
  2. 輕鬆理解 Ajax 與跨來源請求
  1. 傳送資料的第一種方式:表單 form

<div class="app">
<form method="GET" action="https://google.com">
    username:<input name="username">
    <input type="submit" />
</form>
</div>

<script>

</script>

dev tool 裡面的 Network -> 勾選 Preserve log

流傳過程:

瀏覽器 --- request ----> Server
Server ---- response ----> 瀏覽器 (render response)

此法跟 js 一點關係都沒有,存粹透過 HTML 元素。

它比較像是:我要到這個頁面,我要帶什麼參數到這頁面?

表單缺點:每次要新的資料時都需換頁。

若只是想要擷取某些資料,可以利用下面的方法。

  1. 傳送資料的第二種方式:透過 JS 交換資料:AJAX

任何非同步跟伺服器交換資料的 js 都可叫 AJAX。

他不會換頁。

<body>
    <div class="app"></div>
    <script>
        const request = new HMLHttpRequest()
        request.onload = function() {
            if (requset.status >= 200 && request.status < 400) {
                console.log(request.responseText)
            } else {
                console.log('err')
            }
        }

        request.onerror = function() {
            console.log('error')
        }

        request.open('GET', "送去某網址", true)
        // true = 同步,要在餐廳排隊等很久
        // false = 非同步,「好了叫我」,可以中途做其他事   
        request.send();
    </script>
</body>
  1. 傳送資料的第三種方式:JSONP

    • 全名叫做:JSON with Padding。
    • 透過 script 標籤,不受同源限制影響,JSONP 就是利用 Githubissues.
    • Githubissues is a development platform for aggregating issues.