2017年6月28日

[學習筆記目錄] JavaScript: Understanding the Weird Part(JavaScript 全攻略:克服JS 的奇怪部分)



這是我在Udemy上修的一門課,當初想增進自己JavaScript的能力,這門課在特價,同時老師的評價也相當的好,所以就想說學學看。這門課和其他一般線上免費的JavaScript教程很不一樣,不只是教程式怎麼寫,而是進一步說明背後的原理和邏輯,學到的很多之前不清楚的概念,特別是在JavaScript中關於繼承、原型還有建構式的地方之前看了很多教學都還是不太瞭解,但這堂課說明的超級清楚,同時又有搭配英文字幕,歡迎大家參考,也很推薦大家上這門課!

JavaScript基本觀念:認識不同的資料型別、運算子和重要概念


JavaScript物件建立:瞭解基本物件建立的方法


JavaScript函式:為什麼說函式也是物件的一種呢?


JavaScript函式進階:瞭解什麼是IIFEs、什麼是closures?


JavaScript原型、繼承和建構式:這是我上過最清楚的說明


親手打造屬於你的 Framework/Library


其他資源


[學習筆記目錄] JS30 系列文章


JS30 學習筆記連結目錄

另外在 Facebook 社團 線上 Node 讀書會 中有由 @Yujin Chne 主辦的線上讀書會,由 @技安 協助提供社群和直播軟體與錄影,如果想要獲得更多相關資訊的話,歡迎前往該讀書會。這裡也附上由@Yujin Chne整理好的影片連結
我覺得 JS30 真的是非常好的 JS 練習,關於 JS30 的說明可以參考 這一篇(JS30 系列 Day0 - 課程說明) ,下面列的是個人的學習筆記,如果有錯誤的部分歡迎告知:

2017年6月20日

[JS小細節] Node Element 在 appendChild 後消失(disappear)!?

為什麼無法正確 appendChild !?

情況描述:Node.appendChild 的使用

Node.appendChild() 是我們在 JavsScripy 中操作 DOM 的時候經常會使用到的方法,特別是在我們使用 JS 建立一個 DOM Element 之後。
舉例來說,假設現在我們的 HTML 結構長這樣:
<div class="demo-1">
  <div class="block block-1"></div>
  <div class="block block-2"></div>
  <div class="block block-3"></div>
</div>
這時候的畫面長這樣子:
假設我想要在每一個 .block 中都添加一個 .innerdiv 時,我們直覺上可能會這樣做:
// STEP1: 利用 document.createElement 建立 DOM Element
let innerElement = document.createElement('div')
innerElement.classList.add('inner')

// STEP2: 選擇每一個 .blocks 並且 appendChild 上去
const blocks = document.querySelectorAll('.block')
blocks.forEach(block => {
    block.appendChild(innerElement)
})
但這時候卻不會出現你預想的畫面,而是只有最後一個 .block 有添加到 .inner 這個 div,畫面會像這樣:
可是我們想要的畫面應該要是這樣:
到底為什麼會這樣呢?
你可能會猜想是 Array.prototype.forEach 的問題,於是我們試著一個一個 appendChild 上去:
// STEP1: 利用 document.createElement 建立 DOM Element
let innerElement = document.createElement('div')
innerElement.classList.add('inner')

// STEP2: 分別選擇各個 block
const block1 = document.querySelector('.block-1')
const block2 = document.querySelector('.block-2')
const block3 = document.querySelector('.block-3')

// STEP3-1: 先 appendChild 到 block1 上
block1.appendChild(innerElement)
看起來好像沒有太大的問題,如我們所料的,appendChild 到 .block1 這個 div 上了:
接著我們來對 .block2 做 appendChild()
// STEP3-2: appendChild 到 block2 上
block1.appendChild(innerElement)
block2.appendChild(innerElement)
不得了了,.block2 有加上 innerElement 了,但是 .block1 的 innerElement 卻不見了:
不死心的,我們在把 .block3 appendChild():
// STEP3-3: appendChild 到 block3 上
block1.appendChild(innerElement)
block2.appendChild(innerElement)
block3.appendChild(innerElement)
結果畫面變成和我們剛剛用 forEach 寫的狀況一樣,只有最後一個 .block3 有被 appendChild():
想必 appendChild() 是有蹊蹺!

使用 appendChild 要注意的小細節

為什麼會這樣呢?其實在使用 appendChild 時,有一個很需要留意的小細節,讓我們來看一下 MDN 怎麼說:
要留意的是 如果 appendChild 使用時,append 上去的是一個已存在的 node 時,它會做的是搬移,而非複製
這是什麼意思呢?以剛剛的程式碼為例:
// 把 innerElement append 到 block1 上
block1.appendChild(innerElement)

// 這時候 innerElement 已經是存在的 Node 了,所會把這個 Node 進行"搬移",於是原本在 .block1 的 innerElement 被搬到 .block2
block2.appendChild(innerElement)

// 同理,原本在 .block2 的 innerElement 被搬到 .block3
block3.appendChild(innerElement)
重點:如果 appendChild 使用時,append 上去的是一個已存在的 node 時,它會做的是搬移,而非複製。
我們可以怎麼證明這一點呢?
我們可以寫一個按鈕,每點一次它就會依序 append 到 .block1, .block2, .block3 來看看變化:
<!-- pug -->
button(type="button" id="appendNode") 切換 appendChild
let i = 0
const buttonAppend = document.querySelector('#appendNode')
buttonAppend.addEventListener('click', function(){
    console.log(i)
    if (i === 0) {
        block1.appendChild(innerElement)    
    }  else if (i === 1) {
        block2.appendChild(innerElement)
    } else {
        block3.appendChild(innerElement)
    }
    i = (i + 1) % 3        // i 會在 0 ~ 2 之間依序循環
})
操作的畫面會像下面這樣,你可以看到當我們把 innerElement appen 到 .block2 時,innerElement 就會從 .block1 被搬到 .block2,同理,也會從 .block2 搬移到 .block3:

使用 Node.cloneNode() 複製 Node Element

從剛剛的範例中,我們可以看到當我們使用 appendChild() 時,對於現存的 Node 它會採用搬移的方式,讓如果我們是想要複製一整個 element 呢?
在 MDN 中也提供的貼心的說明,告訴我們可以使用 Node.cloneNode() 這個方法:
Node.cloneNode() 的用法很簡單,在括弧中可以帶一個參數,true 的話表示深層複製(也是就不只複製 tag,還會複製裡面的內容),讓我們來試試看。可以看到這次 Node 不會是搬移,而是不斷的複製新的 Node:
重點:如果 appendChild 使用時要複製而非搬移,記得先使用 Node.cloneNode() 這個方法複製 Node Element。。
<!-- pug -->
button(type="button" id="cloneNode") 添加 cloneNode
在這裡我們多了一句 cloneElement = innerElement.cloneNode(true) 這樣就會真的複製這個 Node,然後在 appendChild() 進去,而不是搬移同一個 Node。
const buttonClone = document.querySelector('#cloneNode')
buttonClone.addEventListener('click', function () {
    let cloneElement = innerElement.cloneNode(true)
     if (i === 0) {
        block1.appendChild(cloneElement)    
    }  else if (i === 1) {
        block2.appendChild(cloneElement)
    } else {
        block3.appendChild(cloneElement)
    }
    i = (i + 1) % 3
})

程式範例

appendChild 小細節 @ PJCHENder CodePen

參考資料

2017年6月13日

[學習筆記] Chrome Dev Tools 開發者工具實用功能整理

重點速記

基本 console 使用

/**
 * 幫助視覺化呈現
**/
console.warn('<output>')
console.error('<output>')
console.info('<output>')
console.assert([Condition Expression], '<output>')
console.clear()

console.dir([DOMElement])
console.table([object/array])


/**
 * 將輸出資料分群顯示
**/
console.group('<groupName>')             // 開始分群,預設展開
console.groupCollapsed('<groupName>')    // 開始分群,預設不展開
console.groupEnd()                       // 結束分群

/**
 * 使用 '%c' 幫輸出的內容添加樣式
**/
console.log('%c What a Cool Console', 'font-size: 32px; color: red')

console 特殊功能

// 計數
console.count([String])

// 計時
console.time([String])            // 開始計時
console.timeEnd([String])         // 結束計時

監聽事件

monitorEvents(element [,event])     //    監聽某一元素
unmonitorEvents(element [,event])   //    取消監聽某一元素 
getEventListeners(element)          //    查看某一元素綁定了哪些事件

選擇 DOM 元素

$0                 // 表示當前所選元素
$(selector)        // 等同於 document.querySelector()
$$(selector)       // 等同於 document.querySelectorAll()

Console 的使用

常用的 console 指令

有一些可以幫助我們 debug 方便檢是的 console 指令像是console.warn(), console.error(), console.assert(condition, '<output'>)

檢視 HTML 元素

使用 console.dir(<HTMLElement>) 可以幫助我們檢視這個 DOM 元素中的所有屬性:

將輸出的資料群組起來

假設我們有一組色票檔:
let colorData = [
    {
        name: 'facebook',
        colorCode: '#4267b2'
    },
    {
        name: 'green',
        colorCode: '#41CEC0' 
    },
    {
        name: 'vue',
        colorCode: '#41b883' 
    }
]
我們可以使用 console.group() 搭配 console.groupEnd() 來將輸出的資料分群,像是這樣:
colorData.forEach(color => {
  console.group(color.name)
  console.log('name', color.name)
  console.log('hexi', color.colorCode)
  console.groupEnd()
})
輸出的結果會像這樣:
如果我們希望預設群組的結果是關閉的,則可以使用 console.groupCollapsed(),需要的時候在打開來看:
colorData.forEach(color => {
  console.groupCollapsed(color.name)
  console.log('name', color.name)
  console.log('hexi', color.colorCode)
  console.groupEnd()
})
輸出的結果會像這樣:

檢視 AJAX Request

在 console 視窗中點右鍵,勾選 “Log XMLHTTPRequest” 就可以看到該網站所發出的 AJAX request:

讓瀏覽器可以直接編輯網頁

在 console 中輸入
document.designMode = 'on'                               
點選網頁文字會直接出現游標,可以直接編輯:

改變 console.log 的樣式

我們可以在 console.log() 的函式中使用 %c,後面再放入 CSS 樣式,就可以改變 console.log 輸出的文字樣式:
console.log('%c What a Cool Console', 'font-size: 32px; color: red')

Debugger 的使用

Debug 流程

  1. 發現(重現)問題
  2. 使用 Sources -> Event Listener Breakpoints 選擇要中斷的事件
  3. 審視原始碼,找出可能有問題的函式
  4. 在該函式可能有問題的程式碼行數的地方點一下,設定斷點(breakpoint),當程式碼執行到該處時,會出現相關訊息。
  5. 利用 watch 輸出某變項或 expression (類似 console.log),利用 console 視窗確認問題
  6. 找出問題後,直接在 source 的區塊內修改程式碼,修改好後按 cmd + S 儲存。
  7. 取消斷點,再次執行,看是否修正錯誤。

設定斷點(BreakPoints)

如果想要看該元素到底是觸發了什麼 JavaScript 事件,可以在該元素上面設定斷點。這裡因為點選該文字後會導致該元素的 CSS 屬性改變,因此我們選擇監聽 attribute modifications
如此當你點選該文字觸發 JS 事件時,Debugger 就會停在這裡:

使用 Debugger

Step over Line of Code (step over)

類似逐步執行的概念。在 A 停下來,按「Step Over Next Function Call」,則會執行B、C,最後停在 D:
function updateHeader() {
  var day = new Date().getDay();
  var name = getName();     // A
  updateName(name);         // D
}
function getName() {
  var name = app.first + ' ' + app.last; // B
  return name;              // C
}

Step into Next Function Call (step into)

進入該函式,在 A 停下來,按「Step Into Next Function Call」之後,會停在 B。
function updateHeader() {
  var day = new Date().getDay();
  var name = getName(); // A
  updateName(name);
}
function getName() {
  var name = app.first + ' ' + app.last; // B
  return name;
}

Step out of Current Function (step out)

跳出該函式。停在 A ,按下 step out,會執行 getName ( ) 中剩下的 code(B),然後停在 C:
function updateHeader() {
  var day = new Date().getDay();
  var name = getName();
  updateName(name); // C
}
function getName() {
  var name = app.first + ' ' + app.last; // A
  return name; // B
}

監控事件處理(monitor event)

除了前面提到的可以在 attribute modifications 設定斷點來偵測觸發的事件外,我們也可以使用 Chrome 內建監聽事件的功能:

開啟事件監聽

我們可以在 console 中輸入 monitorEvents(element, [event]),後面的 event 是 optional 的,如果沒有填的話,它會監測該 element 被觸發的所有事件,一旦事件被觸發就會出現在 console 中:

關閉事件監聽

監聽完事件後,我們可以使用 unmonitorEvents(element, [event]) 來關閉事件監聽,後面的 event 一樣是 optional 的,沒有填的話,會關閉監聽該 element 的所有事件。

查看元素被綁定的事件

另外,我們也可以使用 getEventListeners(element),來看看某一個 DOM 元素被綁定了哪些事件:

其他功能

清除記錄

在 console 中,一般我們可以直接使用 console.clear() 來清除畫面,但是在 chrome 裡面會自動幫你記憶一些你打過的字,有時候自動填補的字並不是自己想要的,這時候我們可以在 console 視窗上點右鍵,選擇clear console history 就可以清除在 console 中曾經輸入過的歷史紀錄:

選擇 HTML 元素

在 chrome 中我們可以像使用 jQuery 一樣,使用 $ 當做選擇器,$ 表示的是 document.querySelector([selector]);如果要選擇多個 DOM 元素,要使用 $$,表示的是 document.querySelectorAll([selector])
另外,你也可以在 element 視窗中點選一個 DOMElement,接著在 console 中輸入 $0,一樣可以選到該元素,$0 表示的是當前 chrome 所選取到的元素:
輸入 $0 可以選到當前在 element 選擇的 DOM 元素:

查看物件

在 console 中我們可以使用 keys(obj)values(obj) 來取的物件的鍵和值:

其他尚未詳細整理的功能

/**
 * Handling Error and Exceptions
**/

console.trace()                                    //    印出目前的 JavaScript call stacks

window.onerror = function(message, url, line){     //    當有錯誤沒有被 try catch 補捉,就會促發 window.onerror
    console.log(`window.onerror is invoked with 
    message = ${message}, url = ${url}, at line = ${line}`)
}

/**
 * Debug
 **/
debug(function)                                   //    將某函式進入 debug 模式
undebug(function)                                 //    將某函式退出 debug 模式
monitor(fn)                                       //    監聽某一函式,會回傳函式名稱和使用的參數

/**
 * Output
 **/
dir(obj)                                          //    條列出物件,等同於 console.dir()
table(data [,columns])                            //    以資料表的方式調列出物件
inspect(obj/fn)                                   //    查看某元素或函式

參考資料

2017年6月12日

[Vue] 在 Vue 中使用(ES6 import) Bootstrap 4 和 jQuery

由於 bootstrap4 需要依賴 jquery 和 tether 這兩個套件,因此在 webpack 的環境底下使用 bootstrap4 有一些需要留意的細節才能正常載入使用。
⚠️ 這裡使用 @vue/cli 版本為 4.0.5,不同版本的設定方式可能略有不同,須特別留意。

使用 Vue CLI 安裝 vue

# 安裝 Vue CLI,目前版本為 4.0.5
$ npm install -g @vue/cli

# 使用 Vue CLI 建立專案
$ vue create vue-sandbox
vue-cli

安裝 Bootstrap

# 安裝 Bootstrap,目前版本為 4.3.1
$ npm i bootstrap
Imgur

載入 Bootstrap CSS 檔

可以直接在 main.js 中引入 bootstrap 的 css 檔:
// ./src/main.js
import 'bootstrap/dist/css/bootstrap.css'
Imgur

試試看:使用 Bootstrap 的 Alert 元件

現在,先來試試看是否有成功載入 Bootstrap 的樣式。打開 ./src/components/HelloWorld.vue,在裡面放入 Bootstrap 中的 alerts 元件,像這樣:
<!-- ./src/components/HelloWorld.vue -->
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>

    <!-- 開始:Bootstrap alert -->
    <div class="container">
      <div class="alert alert-warning alert-dismissible fade show" role="alert">
        <strong>Holy guacamole!</strong> You should check in on some of those fields below.
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
    </div><!-- 結束:Bootstrap alert -->

    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>

    <!-- ... -->
  </div>
</template>
使用 npm run serve 就可以把專案執行起來。沒有問題的話,畫面應該會像這樣子,可以看到中間已經套用了 Bootstrap 的 Alert 樣式:
Imgur
但此時若點擊 Alert 組件的關閉按鈕時,該警告並不會消失。這是因為我們還沒載入和 Bootstrap 有關的 JavaScript 檔案。

安裝和 Bootstrap 有關的 JavaScript 檔

如果你只是要載入 Bootstrap 的樣式檔,基本上到上面那步就可以了。
但是如果你有需要使用到 bootstrap 的其他互動功能,那麼就需要在額外載入 jQuery, Popper.js 和 Bootstrap 的 js 檔。
因此,讓我們一併安裝 jQuery 和 Popper,js:
$ npm install --save jquery popper.js

載入 Bootstrap 的 JavaScript 檔

要使用 Bootstrap 的 JS 檔,一樣直接在 ./src/main.js 中載入 bootstrap 就可以了:
// ./src/main.js
import Vue from 'vue'
import App from './App.vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'      // 在這裡載入 Bootstrap 的 JavaScript 檔

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
此時當我們點擊 Alert 組件的關閉按鈕時,該警告就會消失:
Imgur

載入 jQuery 使用

在上面的例子中,只要載入 Bootstrap 的 JavaScript 檔案後,它會自動去找到相依的 jQuery 套件,因此並不需要額外載入 jQuery 就可以使用。
但有些時候,Bootstrap 的有些互動行為是需要先透過 jQuery 來初始化的,例如 Tooltip 組件。Tooltip 組件在使用前需要針對想要產生 Tooltip 的元素使用 jQuery 來初始化它:
$(function () {
  $('[data-toggle="tooltip"]').tooltip()
})
這時候我們就會需要使用到 jQuery 提供的 $。要怎麼在 Vue 專案中取用到 jQuery 的 $ 呢?這時候我們會需要對 Vue 或者說是 Webpack 進行一些設定。

透過 vue.config.js 設定 webpack

在 Vue 專案中要進行 webpack 的設定,需要在根目錄中新增一支名為 vue.config.js 的檔案(放在和 package.json 同一層):
// 新增一隻名為 vue.config.js 的檔案在專案的根目錄

const webpack = require('webpack');

module.exports = {
  configureWebpack: {
    plugins: [
      new webpack.ProvidePlugin({
        $: 'jquery',
        jQuery: 'jquery',
        'windows.jQuery': 'jquery',
      }),
    ],
  },
};
設定好了之後,在 Vue 專案中,就可以在需要使用 jQuery 的地方匯入 $ 就可以了:
import $ from 'jquery';

試試看:使用 Bootstrap 的 Tooltip 元件

現在讓我們用 Bootstrap Tooltip 元件來測試一下。先在 ./src/components/HelloWord.vue 中加入 Bootstrap 的 Tooltip 元件:
<!-- ./src/components/HelloWorld.vue -->
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>

    <div class="container">
      <!-- Bootstrap Alert -->
      <div class="alert alert-warning alert-dismissible fade show" role="alert">
        <strong>Holy guacamole!</strong> You should check in on some of those fields below.
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div> <!-- /Bootstrap Alert -->

      <!-- Bootstrap Tooltip -->
      <button type="button" class="btn btn-secondary" data-toggle="tooltip" data-placement="top" title="Tooltip on top">
        Tooltip on top
      </button> <!-- /Bootstrap Tooltip -->
    </div>

    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>

    <!-- ... -->
  </div>
</template>
這時候畫面會像這樣,但實際上滑鼠移過去並不會有任何效果:
Imgur
要達到滑鼠移過去有效果的話,需要載入 jQuery 並初始化它。因此我們可以在 ./src/components/HelloWorld.vue<script></script> 內去載入 jQuery 並組件 mounted 之後初始化它,像是這樣:
<!-- ./src/components/HelloWorld.vue -->
<template>
  <!-- ... -->
</template>

<script>
import $ from "jquery";    // STEP 1:載入 jQuery
export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  mounted() {
    // STEP 2:在 mounted 時初始化 tooltip
    $(function() {
      $('[data-toggle="tooltip"]').tooltip();
    });
  }
};
</script>
完成後,當滑鼠移過去時,就會出現 Tooltip 的提示文字:
Imgur
如此就可以繼續開心的使用 Bootstrap 啦!

完整程式碼

完整程式碼可在 vue-import-bootstrap4 @ github 檢視。

額外補充(將 jQuery 載入到全域環境)

如果我們只是使用 import 'jquery' 這種作法,是無法在全域環境(window)下使用 jQuery(這裡抓到的 $ 是 chrome 中內建的選擇器):
img
因此如果我們希望在全域環境下也可以使用 jQuery,我們可以使用下面這樣的寫法:
// ./src/main.js
import Vue from 'vue'
import App from './App.vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'

// 讓瀏覽器的全域環境可以使用到 $
import jQuery from 'jquery'
window.$ = window.jQuery = jQuery
img

參考資料