新手入門

課程目標

完成一個完整的網站

角色

角色關係

flowchart LR
    A[前端]<-->B[後端]<-->C[維運] 

前端

主要負責 UI 呈現與 API 串接,最容易被看到

後端

主要負責商務邏輯,不太容易被看到

維運

主要負責背後的服務穩定,一般使用者看不到

現況

狹義來說,前端的範圍侷限在 HTML, CSS, JavaScript 這三個技術基礎內

廣義來說,只要是可以將東西在網頁上呈現出來給使用者看的,都可以稱為前端

前端只有兩種人: 會寫程式的;不會寫程式的

最終,大家都要朝著一條龍的路上走去...

網站溝通模式

sequenceDiagram
    Browser->>Server: Request (輸入網址按下 enter)
    Server->>Browser: Response (畫面顯示)

網站架構

flowchart TD
    A[首頁]
    A-->B[關於我們]
    A-->C[商品介紹]
    A-->D[聯絡我們]
    A-->E[成功案例]
    C-->C1[商品列表]
    C1-->C2[商品詳細]

開發工具

套件

Live Server

簡易型 Web Server,模擬正式環境

Material Icon Theme

方便識別檔案型態 icon 的主題

Chrome Dev Tools

Chrome 提供的開發者工具,開發人員必備!

會『除錯』比會『開發』來得重要!

開啟方式

  • MAC

    • command + shift + i
  • Windows

    • F12
  • 通用

    • 更多工具 > 開發人員工具

頁籤功能

  • element (元素)

    • 觀察頁面 HTML 結構與動態修改
  • network (網路)

    • 觀察頁面傳輸檔案狀態與流量(消耗的網路頻寬)
  • console (主控台)

    • 觀察頁面程式輸出狀態,寫程式必備

HTML

負責網頁結構而非呈現樣式,所以預設跟純文字差不多

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <meta name="description" content="This is my first page">
</head>
<body>
    Hello
</body>
</html>

vscode 快速產生 html:5 + tab or enter

<!DOCTYPE html>

宣告文件類型為 HTML

<html lang="en"></html>

HTML 主體,lang="en" 預設顯示語言(不用改)

<head></head>

表頭,給瀏覽器看的

<meta charset="UTF-8">

網頁語系,統一使用 UTF-8

<meta http-equiv="X-UA-Compatible" content="IE=edge">

相容 IE 模式

<meta name="viewport" content="width=device-width, initial-scale=1.0">

預設寬度為裝置寬度 (RWD 必備!)

<title>Document</title>

標題,顯示在瀏覽器頁籤上面

<meta name="description" content="This is my first page">

網頁說明,顯示在搜尋結果下方說明

建議字數:

  • 英文 230 ~ 320 字
  • 中文 110 ~ 150 字

無輸入時,搜尋引擎會抓取頁面內容自動產生

<body>Hello</body>

給人看的,要顯示的東西都寫在這邊

Favorite icon

除了網頁標題以外,代表網頁的形象圖是也很重要

favorite icon generator

favorite icon download

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <!-- favorite icon start -->
    <link rel="apple-touch-icon" sizes="57x57" href="/icon/apple-icon-57x57.png">
    <link rel="apple-touch-icon" sizes="60x60" href="/icon/apple-icon-60x60.png">
    <link rel="apple-touch-icon" sizes="72x72" href="/icon/apple-icon-72x72.png">
    <link rel="apple-touch-icon" sizes="76x76" href="/icon/apple-icon-76x76.png">
    <link rel="apple-touch-icon" sizes="114x114" href="/icon/apple-icon-114x114.png">
    <link rel="apple-touch-icon" sizes="120x120" href="/icon/apple-icon-120x120.png">
    <link rel="apple-touch-icon" sizes="144x144" href="/icon/apple-icon-144x144.png">
    <link rel="apple-touch-icon" sizes="152x152" href="/icon/apple-icon-152x152.png">
    <link rel="apple-touch-icon" sizes="180x180" href="/icon/apple-icon-180x180.png">
    <link rel="icon" type="image/png" sizes="192x192"  href="/icon/android-icon-192x192.png">
    <link rel="icon" type="image/png" sizes="32x32" href="/icon/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="96x96" href="/icon/favicon-96x96.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/icon/favicon-16x16.png">
    <link rel="manifest" href="/icon/manifest.json">
    <meta name="msapplication-TileColor" content="#ffffff">
    <meta name="msapplication-TileImage" content="/icon/ms-icon-144x144.png">
    <meta name="theme-color" content="#ffffff">
    <!-- favorite icon end -->
</head>
<body>
    Hello
</body>
</html>

demo

Facebook meta

支援 facebook 資訊抓取

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Facebook meta demo.</title>
        <link rel="icon" type="image/png" sizes="180x180" href="https://book.niceinfos.com/frontend/assets/html_facebook_meta_v3/images/logo.png" />

        <meta property="og:url" content="https://book.niceinfos.com/frontend/assets/html_facebook_meta_v3/" />
        <meta property="og:type" content="website" />
        <meta property="og:title" content="My First Page" />
        <meta property="og:description" content="This is my first page" />
        <meta property="og:image" content="https://book.niceinfos.com/frontend/assets/html_facebook_meta_v3/images/logo.png" />
        <meta property="og:image:secure_url" content="https://book.niceinfos.com/frontend/assets/html_facebook_meta_v3/images/logo.png" />
        <meta property="og:image:type" content="image/png" />
        <meta property="og:image:width" content="310" />
        <meta property="og:image:height" content="310" />
        <meta property="fb:app_id" content="" />
    </head>
    <body>
        Facebook meta demo.
    </body>
</html>

og:url

頁面網址

og:type

頁面類型,預設帶入 website

og:title

頁面標題

og:description

頁面說明,對應 <meta name="description" content="This is my first page"> 即可

og:image

顯示圖片,最低寬高 200 x 200

og:image:secure_url

顯示圖片 https 協定

og:image:type

顯示圖片類型

og:image:width

顯示圖片寬度

og:image:height

顯示圖片高度

fb:app_id

應用程式 ID,需要做洞察報告才需要填寫

Facebook 分享偵錯工具

demo

元素 (element)

給個顯示內容都需要給予一個標籤包住,此標籤就是元素

宣告

使用 <> 將元素名稱包住

<div>Hello, world!</div>

宣告一個 div 元素,顯示內容為 Hello, world!

內容

顯示給使用者看的東西,稱為內容

只要元素有內容,就必須做關閉

<div>Hello, world!

這是錯誤的宣告,打開瀏覽器後會發現好像沒什麼差別

這是因為瀏覽器會幫你做補充的行為(本質上還是漏掉的)

補充一時爽,套版火葬場!

一定要再三檢查,該關閉的就要關閉

demo

  1. 開啟 Chrome dev tools 查看元素頁籤 (補充行為)
  2. 檢視原始碼查看 Command + Alt + u

屬性

描述元素,在宣告元素與 > 之間宣告

屬性不會顯示給使用者看,通常用於跟 CSSJavaScript 串接使用

<div id="first-name" class="fullname">Lin</div>

宣告一個 div 元素,擁有屬性 id 值為 first-name 以及 class 值為 fullname,顯示內容為 Lin

能夠口語化描述自己宣告的元素,表示知道自己在做什麼了

屬性格式

屬性跟屬性之間『至少』一個空格區分

錯誤

<div id="first-name"class="fullname">Lin</div>

正確

<div id="first-name" class="fullname">Lin</div>
<div id="first-name"        class="fullname">Lin</div>
<div       id="first-name"     class="fullname">Lin</div>

屬性數值使用『雙引號』包起來

錯誤

<div id=first-name class=fullname>Lin</div>
<div id='first-name' class='fullname'>Lin</div>

正確

<div id="first-name" class="fullname">Lin</div>

使用單引號看似沒問題,但是不符合規範,在特定情境下一樣爆炸給你看!

同一屬性多個數值時,數值使用空格區分

錯誤

<div id="first-name" class="fullname" class="my-name">Lin</div>
<div id="first-name" class="fullnamemy-name">Lin</div>

正確

<div id="first-name" class="fullname my-name">Lin</div>

顯示方式

HTML 元素顯示基本上分為兩個

區塊 (block)

佔據整列,強制後面的元素換行

內聯 (inline)

『不會』佔據整列,後面的元素會接著顯示

元素跟元素之間會有小縫隙(要視為 Bug 或是 Feature 就看個人了)

demo

標題 h1 ~ h6

如同文書處理軟體一樣,擁有 h1 ~ h6 標籤代表標題

h1 最大,h6 最小

<h1>h1</h1>
<h2>h2</h2>
<h3>h3</h3>
<h4>h4</h4>
<h5>h5</h5>
<h6>h6</h6>

demo

注重語意與結構而非呈現樣式

頁面上看到的 h6 比起 h1 小,這是因為瀏覽器預設有定義一些基本的 CSS 樣式

開啟 Chrome dev tools > 元素 > 樣式 觀察

超連結 a

提供網頁連結能力,連結位置與目前網域不同時,稱為『外部連結』,反之為『內部連結』

外部連結

<a href="https://www.google.com/">To Google</a>

建立一個連結到 https://www.google.com/ 的超連結,顯示內容為 To Google

內部連結

<a href="/about.html">About</a>

建立一個連結到 https://{{ domain }}/about.html 的超連結,顯示內容為 About

{{ domain }} 頁面當下網域

例如當下網域為 https://www.test.com/ 則此超連結為 https://www.test.com/about.html

開啟目標

目前的超連結,都會將頁面直接轉跳到目的網頁

這是因為還有一個屬性 target 我們沒宣告,預設為 _self

如果想要點擊後,跳出一個新的分頁顯示,將 target 的數值設定為 _blank

<a href="https://www.google.com/" target="_blank">To Google</a>

錨點

id 當成目標位置,使用 # 表示

<div id="top">Top</div>

<a href="#top">To top</a>

demo

捲軸滑順效果

<style>
    html,
    body {
        scroll-behavior: smooth;
    }
</style>

圖片 img

提供網頁顯示圖片能力

dog image download

<img src="dog.jpeg" alt="this is dog" title="this is dog" />

src 圖片位置

alt 當圖片不存在時顯示 (無障礙友善)

title 移動到圖片上時顯示說明 (Options)

demo

區塊 div

用於規劃範圍,沒有任何語意

具備語意的區塊元素

article 表示一篇文章

section 表示一段主題

初學階段,不用太在意語意。先求有,再求好。

內聯 span

用於同一區塊內特定範圍呈現需求,沒有任何語意

<div>
    <span style="color:red;">我</span>很好!
</div>

按鈕 button

用於與使用者互動,經常搭配 form 表單一起出現

也常綁定事件來做互動

<button id="do-btn">Click Me</button>

<script>
    let btn = document.querySelector('#do-btn')

    btn.addEventListener('click', (e) => {
        alert('You click me!')
    })
</script>

demo

form 內時,預設為 type="submit" 所以按下去就會送出表單。如果不要送出,要設定 type="button"

列舉 ul > li

用於呈現同性質多項目內容,例如:選單

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
</ul>

demo

刪除黑點點

預設樣式每個項目都有一個黑色點點,可以透過 CSS 來取消

list-style-type: none;

表格 table

用於呈現欄位項目長度相同內容,例如:學生資料

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Age</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>David</td>
            <td>18</td>
        </tr>
        <tr>
            <td>Helen</td>
            <td>20</td>
        </tr>
    </tbody>
</table>

thead 表頭

tbody 表身

tr table row,橫向

th table head,縱向

td table body,縱向

demo

沒有宣告 theadtbody 對於顯示沒有影響,但是架構非正規。在後續使用第三方套件時,可能無法運作

顯示格線

透過 CSS 顯示格線

<style>
th, td {
    border: 1px solid #000;
}
</style>

消除間距

透過 CSS 消除間距

<style>
table {
    border-spacing: 0;
    border-collapse: collapse;
}
</style>

嵌入頁面 iframe

提供其他頁面顯示

<iframe
    src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3640.6532286461784!2d120.68338871602994!3d24.148813384394593!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x34693d691d272657%3A0x2ed7da18282bc17c!2z6LWr57a16Zu76IWm6Kit6KiI5Z-56KiT5a246ZmiLeWPsOS4reWtuOmZog!5e0!3m2!1szh-TW!2stw!4v1664443717678!5m2!1szh-TW!2stw"
    width="600"
    height="450"
    style="border: 0"
    allowfullscreen=""
    loading="lazy"
    referrerpolicy="no-referrer-when-downgrade"
></iframe>

google map

demo iframe google map

youtube

demo iframe youtube video

注意跨站(CORS)與安全性問題

CORS

表單 form

提供發送資料功能,無外觀

<form method="get" action="url">

</form>

method 表單發送方法,預設為 get。發送方法有: get, post, put, delete

action 表單發送目的地,沒有宣告表示自己

GET

會將發送資料顯示在網址列,有長度限制(瀏覽器限制與伺服器限制)

無法發送二進制內容(檔案)

demo

POST

不會將發送資料顯示在網址列,無長度限制(瀏覽器不限制,但是伺服器還是會有限制)

可以發送二進制內容(檔案)

demo

輸入 input

提供表單輸入介面,透過屬性表示不同用途

文字

<input type="text" name="account">

type="text" 表示文字類型

name="account" 表示送出時,欄位名稱為 account

密碼

<input type="password" name="pwd">

type="password" 表示密碼類型

該類型會將輸入的內容使用 * 來表示,無法看到真實的內容

密碼類型使用 * 表示內容,感覺很安全,真的嗎?

demo

數字

<input type="number" name="money">

type="number" 代表數字類型

e 為數學符號,可以輸入是正常的

電話

<input type="tel" name="phone">

type="tel" 代表電話類型

手機介面時,只會出現單純數字鍵

顏色

<input type="color" name="bg-color">

type="color" 代表顏色類型

不同瀏覽器顯示的介面會有所不同

日期

<input type="date" name="schedule-date">

type="date" 代表日期類型

不同瀏覽器顯示的介面會有所不同

時間

<input type="time" name="schedule-time">

type="time" 代表日期類型

不同瀏覽器顯示的介面會有所不同

提交

<input type="submit">

type="submit" 代表提交類型

該類型外觀會長得跟按鈕一樣,預設文字為『提交』

建議使用 <button type="submit">提交</button> 來表示更為適合

form demo

檔案

<input type="file" name="my-pic">

type="file" 代表檔案類型

該類型外觀由各瀏覽器定義,提供選取本地檔案功能

如要上傳檔案,form 須滿足一定條件

  1. form 須設定屬性 enctype="multipart/form-data"
  2. form 必須為 POST 方法

測試 API: https://book.niceinfos.com/frontend/assets/html_file_upload/response.php

demo

單選

<input type="radio" name="gender" value="male" /> 男
<input type="radio" name="gender" value="female" /> 女

demo

使用 name 屬性為分類群組

<input type="radio" name="male" value="male" /> 男
<input type="radio" name="female" value="female" /> 女

demo

多選

<input type="checkbox" name="music" value="a1" /> A1
<input type="checkbox" name="music" value="a2" /> A2
<input type="checkbox" name="music" value="a3" /> A3

demo

提交後發現,music 只有一個數值,但是我明明勾選兩個以上

使用『陣列』([])儲存多數值

<input type="checkbox" name="music[]" value="a1" /> A1
<input type="checkbox" name="music[]" value="a2" /> A2
<input type="checkbox" name="music[]" value="a3" /> A3

demo

下拉選單 select

提供下拉式挑選列表

單選

<select name="county">
    <option value="">選擇縣市</option>
    <option value="TP">台北</option>
    <option value="台中">台中</option>
    <option>高雄</option>
</select>

value 為數值,如沒有宣告,帶入內容

demo

多選

<select name="county[]" multiple>
    <option value="">選擇縣市</option>
    <option value="TP">台北</option>
    <option value="台中">台中</option>
    <option>高雄</option>
</select>

multiple 為多選屬性

county[] 為陣列類型,才可容納多數值

demo

多行輸入

允許換行輸入

<textarea name="remark" cols="30" rows="10"></textarea>

cols 欄數,影響寬度

rows 列數,影響高度

demo

禁止拉伸

textarea {
    resize: none;
}

標籤 label

提供表單元素說明,並可透過 for 屬性對應 id 代理錨點執行 focus 鎖定動作

<div>
    <label for="f-fullname">全名</label>
    <input type="text" name="fullname" id="f-fullname">
</div>

demo

聯絡表單實作

API

https://book.niceinfos.com/frontend/assets/html_contact_form/api.php

demo

上傳作業

將完成檔案上傳,使用英文名字,例如: david.html

查看

提交作業

訪問密碼
BIQGy

CSS

負責樣式呈現,建構在 HTML 架構之上

宣告格式

h1 {
    color: red;
}

h1 選擇器,對應 HTML 特徵

color 屬性名稱

red 屬性數值

每個屬性結束要記得用 ; 關閉,最後一個屬性可以不用關閉

宣告位置

HTML 屬性

<div style="color:#ffffff; background: #000000">屬性宣告</div>

使用 HTML 屬性 style 來宣告

HTML 元素

<style>
    div {
        color: #ffffff;
        background: #000000;
    }
</style>

CSS 檔案

css/main.css

div {
    color: #ffffff;
    background: #000000;
}

CSS 檔案內不要宣告 <style> 標籤

index.html

<head>
    <link rel="stylesheet" href="css/main.css">
</head>

rel="stylesheet" 使用樣式表 (CSS)

href CSS 位置

選擇器特性

範圍 scope

精準度決定影響範圍

<style>
    div {
        color: red;
    }
</style>

<div>一號</div>

<span>二號</span>

<span>
    <div>三號</div>
</span>

一號 符合 div 選擇器,顯示為紅色

二號 不符合 div 選擇器,顯示為黑色

三號 符合 div 選擇器,顯示為紅色

雖然三號的 divspan 內,但此選擇器嚴謹度為:只要是 div 就可以

demo

繼承 inherit

子層有條件的繼承父層屬性

property-defs

<style>
    span {
        color: blue;
    }
</style>

<div>一號</div>

<span>二號</span>

<span>
    <div>三號</div>
</span>

一號 不符合 span 選擇器,顯示為黑色

二號 符合 span 選擇器,顯示為藍色

三號 不符合 span 選擇器,但是上層符合 span 選擇器,屬性繼承後顯示為藍色

demo

複寫 overwrite

越接近目標,優先權越高

HTML 屬性 > HTML 元素 > CSS 檔案

CSS 宣告位置

<style>
    div {
        color: red;
        background: #000000;
    }
</style>

<div>一號</div>
<div style="color: yellow;">二號</div>

demo

由上往下複寫

後面宣告屬性會覆寫前面宣告屬性

<style>
    div {
        color: red;
    }

    div {
        color: yellow;
    }
</style>

<div>一號</div>

demo

權重 priority

遇到同樣精準度屬性時,採用複寫機制處理

!important > HTML 屬性 > ID 選擇器 > Class 選擇器 | 偽類 | 屬性選擇器 > 元素 (Element) > 全域 (*)

<style>
    div {
        color: red !important;
    }

    div {
        color: yellow;
    }

    span > div {
        color: blue;
    }
</style>

<div>一號</div>

<span>
    <div>二號</div>
</span>

依據複寫機制,理論上 一號 要呈現黃色,但在 !important 的干涉下,後面的宣告無法複寫(權重)

demo

<style>
    div#sp {
        color: green;
    }

    div {
        color: yellow;
    }

    span > div {
        color: blue;
    }

    div {
        color: red;
    }
</style>

<div>一號</div>

<span>
    <div>二號</div>
</span>

<span>
    <div id="sp">三號</div>
</span>

一號

  1. 符合 yellow
  2. 符合 red
  3. 最終顯示 red

二號

  1. 符合 yellow
  2. 符合 blue
  3. 符合 redred 精準度較低,複寫失敗
  4. 最後顯示 blue

三號

  1. 符合 green
  2. 符合 yellowyellow 權重較低 (ID > Element),複寫失敗
  3. 符合 blueblue 權重較低 (ID > Element),複寫失敗
  4. 符合 redred 權重較低 (ID > Element),複寫失敗
  5. 最後顯示 green

demo

ID 選擇器

使用符號 # 表示,同一個頁面不可以出現重複的 ID 名稱

<style>
    #one {
        color: #ffffff;
        background: #000000;
    }
</style>

<div id="one">One</div>

CLASS 選擇器

使用符號 . 表示,同一個頁面可以出現重複的 CLASS 名稱

<style>
    .more {
        color: #ffffff;
        background: #000000;
    }
</style>

<div class="more">One</div>
<div class="more">Two</div>

盒子模型

盒子模型表示一個元素佔據範圍

觀察方式:

Chrome Developer Tool > Elements > Computed

組成:

content > padding > border > margin

內容 content

除了內容文字本身外,寬度(width)與高度(height)兩個屬性也會影響

寬度 width

.w-100 {
    width: 100px;
}

數值由一個數字與一個單位組成,像素(px)表示固定單位

高度 height

.h-100 {
    height: 100px;
}

數值由一個數字與一個單位組成,像素(px)表示固定單位

建立一個正方形

.cube {
    width: 100px;
    height: 100px;
    background: #000;
    color: #fff;
}

demo

行高 line-height

.nh-100 {
    line-height: 100px;
}

當行高與高度數值一樣時,會產生垂直置中的效果

換行觸發條件:

  1. 多個區塊元素
  2. <br/> 換行元素
  3. 內容超過元素本身寬度

demo

使用行高要注意破版問題

文字顏色 color

.color-red {
    color: red;
}

.color-custom {
    color: #dedede;
}

使用個別顏色的英文名稱,或使用十六進制的色碼皆可 (#dedede)

文字大小 font-size

.fs-18 {
    font-size: 18px;
}

數值由一個數字與一個單位組成,像素(px)表示固定單位

文字線條 font-weight

.fw-100 {
    font-weight: 100;
}

.fw-900 {
    font-weight: 900;
}

100 線條最細,900 線條最粗

不可以帶單位,例如 100px 這樣是錯誤的

水平對齊

.text-left {
    text-align: left;
}

.text-center {
    text-align: center;
}

.text-right {
    text-align: right;
}

預設是 left

背景顏色 background-color

.bg-red {
    background-color: red;
}

背景圖片 background-image

.bg-image {
    background-image: url(images/some.png);
}

使用 url 抓取目標圖片

當圖片小於元素寬高時,會自動重複顯示

demo

使用 background-repeat: no-repeat; 來取消重複

.bg-image {
    background-image: url(images/some.png);
    background-repeat: no-repeat;
}

demo

內距 padding

content 與 border 之間的距離

.pt-20 {
    padding-top: 20px;
}

.pe-20 {
    padding-right: 20px;
}

.pb-20 {
    padding-bottom: 20px;
}

.ps-20 {
    padding-left: 20px;
}

.p-20 {
    padding: 20px;
}

.p-20-40 {
    padding: 20px 40px;
}

.p-custom {
    padding: 10px 20px 30px 40px;
}

padding: 20px 表示『上下左右』都是 20px

padding: 20px 40px 表示『上下』是 20px,『左右』是 40px

padding: 10px 20px 30px 40px 表示『上』『右』『下』『左』分別為 10px 20px 30px 40px

邊匡 border

.border {
    border-width: 3px;
    border-color: #000000;
    border-style: solid;

    /* border: 3px solid #000000; */
}

.border-radius {
    border-top-right-radius: 10px;
    border-top-left-radius: 10px;
    border-bottom-right-radius: 10px;
    border-bottom-left-radius: 10px;

    /* border-radius: 10px; */
}

border: 3px solid #000000; 等同三個屬性合併宣告

border-radius: 10px; 倒圓角

demo

畫圓圈

.circle {
    width: 100px;
    height: 100px;
    border-radius: 100px;
}

條件:

  1. 正方型
  2. 倒圓角數據大於等於寬高

外距 margin

border 與其他區塊之間的距離

.mt-20 {
    margin-top: 20px;
}

.me-20 {
    margin-right: 20px;
}

.mb-20 {
    margin-bottom: 20px;
}

.ms-20 {
    margin-left: 20px;
}

.m-20 {
    margin: 20px;
}

.m-20-40 {
    margin: 20px 40px;
}

.m-custom {
    margin: 10px 20px 30px 40px;
}

margin: 20px 表示『上下左右』都是 20px

margin: 20px 40px 表示『上下』是 20px,『左右』是 40px

margin: 10px 20px 30px 40px 表示『上』『右』『下』『左』分別為 10px 20px 30px 40px

將左右外距設為 auto,區塊會水平置中

計算模式 box-sizing

.block {
    width: 200px;
    height: 100px;
    padding: 20px;
    border: 2px solid #000;
}

demo

明明設定了寬 200 高 100,但是卻沒有如預期的寬高顯示?

content-box

預設計算模式為 content-box,此模式下內容寬高計算公式為

content + padding + border

帶入上面的範例得出寬高

寬 = 200 + (20 * 2) + (2 * 2) = 244

高 = 100 + (20 * 2) + (2 * 2) = 144

如此計算方式,在排版上造成很大的困擾(每次異動都要計算)

border-box

計算模式改為 border-box,此模式下寬高計算公式為

content = width - padding - border

表示設定寬高後,content 可用範圍為扣除 paddingborder

如此,寬高就不會異動

.border-box {
    box-sizing: border-box;
}

demo

顯示模式

每個元素都有預設的顯示模式(瀏覽器定義),可以透過 CSS 來修改

區塊

.block {
    display: block;
}
  • 會佔據一列
  • 具備寬度(width)與高度(height)的概念

demo

內聯

display: inline;
  • 不會佔據一列
  • 不具備寬度(width)與高度(height)的概念
  • 預設兩個元素之間會有空隙,且無法消除

demo

內聯區塊

display: inline-block;
  • 不會佔據一列
  • 具備寬度(width)與高度(height)的概念
  • 預設兩個元素之間會有空隙,且無法消除

demo

隱藏區塊

display: none;

超出寬高處理

overflow: hidden;
overflow: auto;
overflow: scroll;
overflow-y: auto;
overflow-x: auto;

初始化

由於每個瀏覽器都會預設不同的預設樣式,所以在開始切版之前,要先將樣式初始化,讓每個瀏覽器的預設值一致

初始化套件 reset css

reset css

簡單暴力的初始化套件,將所以元素都重置,適合 0% -> 100% 樣式打造

優點

  • 適合充滿想法的設計師
  • 所有樣式都一致

缺點

  • 全部樣式都要自己寫
  • 使用第三方套件時,可能會導致樣式無法正常顯示問題

初始化套件 Normalize.css

Normalize.css

現代化的初始化套件,保留部分預設樣式並且修正一些瀏覽器 bug,適合 50% -> 100% 樣式打造

優點

  • 不需要全部樣式自己寫就可以得到一個還能看得樣式
  • 原始碼註解清楚
  • 第三方套件友善

缺點

  • 不同瀏覽器還是有可能發生樣式不一致問題
  • 充滿想法的設計師可能無法正常發揮

簡易初始化

*,
*::before,
*::after {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

切版初體驗

image

來源

定位

區塊內容可以透過定位做進階排版

靜態定位 static

預設定位方式

.pos-static {
    position: static;
}

由於是靜態,所以無法自由移動

相對定位 relative

原地浮起,佔據寬高,定位原點為自己

.pos-relative {
    position: relative;
}

原則上使用 relative 都是作為下一層的定位原點用,不會拿來做移動使用

絕對定位 absolute

原地浮起,不佔寬高,定位原點為第一個『非靜態』上層

.pos-absolute {
    position: absolute;
}

原則上使用 absolute 上一層要設定為 relative 當成定位原點,都沒找到時,使用根元素(html)

固定定位 fixed

原地浮起,不佔寬高,定位原點為根元素

.pos-fixed {
    position: fixed;
}

原則上使用 fixed 都會是第一層元素,因為該方式會抓取根元素(html),如非在第一層,會使結構錯亂

demo

圖層 z-index

決定浮起區塊的上下位置,類似 PS 中的圖層

.z-99 {
    z-index: 99;
}

數字越大,圖層越高

demo

移動

漂浮後,可進行『上』『下』『左』『右』移動

.left-10 {
    left: 10px;
}
.right-10 {
    right: 10px;
}
.top-10 {
    top: 10px;
}
.bottom-10 {
    bottom: 10px;
}

non relative parent

relative parent

畫臉

使用目前所學畫出一張臉

demo

單位

像素 px

絕對大小

繼承倍數 em

1em = 100%

使用上一層的大小當 base 去相乘

.parent {
    font-size: 20px;
}

.child-1 {
    font-size: 1.5em;
}

.child-2 {
    font-size: 1.5em;
}
<div class="parent">
    <div class="child-1">
        <div class="child-2">
            Child-2
        </div>
    </div>
</div>

Child-2 = 20px * 1.5 * 1.5 = 45px

demo

根倍數 rem

使用 html 大小當 base 去相乘

html {
    font-size: 20px;
}

.parent {
    font-size: 40px;
}

.child-1 {
    font-size: 1.5em;
}

.child-2 {
    font-size: 1.5rem;
}
<div class="parent">
    <div class="child-1">
        <div class="child-2">
            Child-2
        </div>
    </div>
</div>

Child-2 = 20px * 1.5rem = 45px`

demo

百分比 %

em 會繼承,120% = 1.2em

點陣 pt

點陣式印表機單位, 1pt = 1/72in

  • 72 dpi1px = 1pt
  • 96 dpi1px = 0.75pt (72 / 96 = 0.75)

英吋 in

96 dpi1in = 96px

公分 cm

96 dpi1cm = 37.795275593333px

公釐 mm

96 dpi1mm = 3.779527559333px

『pt』『im』『cm』『mm』 為印刷單位,實際上幾乎很少用到

flexbox

由主軸跟副軸組成,目前排版主流

Flex Sample

宣告 display: flex

display: flex;
display: inline-flex;
<style>
    *,
    *::after,
    *::before {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
    }

    .flex {
        display: flex;
        background: yellow;
        width: 90%;
        margin: 20px auto;
        height: 300px;
    }

    .flex-item {
        width: 100px;
        height: 100px;
        background: #000;
        color: #fff;
        margin: 0 10px;
    }
</style>

<div class="flex">
    <div class="flex-item">1</div>
    <div class="flex-item">2</div>
    <div class="flex-item">3</div>
</div>

demo

flex 內的區塊元素,不會佔據整列

方向 flex-direction

flex-direction: row;
flex-direction: row-reverse;
flex-direction: column;
flex-direction: column-reverse;

row 由左到右 (預設)

row-reverse 由右到左

column 由上到下

column-reverse 由下到上

demo

內容超出行為 flex-wrap

flex-wrap: no-wrap;
flex-wrap: wrap;
flex-wrap: wrap-reverse;

no-wrap 不換行,內容寬高會自動調整 (預設)

wrap 換行

wrap-reverse 換行,上下反轉

demo

主軸對齊 justify-content

justify-content: flex-start;
justify-content: flex-end;
justify-content: center;
justify-content: space-around;
justify-content: space-between;
justify-content: space-evenly;

flex-start 靠左 (預設)

flex-end 靠右

center 置中

space-around 左右平均寬度分散

space-between 左右貼平分散

space-evenly 平均分散

demo

副軸對齊 align-content

align-content: flex-start;
align-content: flex-end;
align-content: center;
align-content: space-around;
align-content: space-between;
align-content: space-evenly;

flex-start 靠上 (預設)

flex-end 靠下

center 置中

space-around 上下平均寬度分散

space-between 上下貼平分散

space-evenly 平均分散

demo

要設定 flex-wrap: wrap; 才會有效果

副軸內容對齊 align-items

align-items: stretch;
align-items: flex-start;
align-items: flex-end;
align-items: center;
align-items: baseline;

stretch 高度延伸,內容區塊有設定高度時,則依據高度為主 (預設)

flex-start 靠上

flex-end 靠下

center 置中

baseline 內容基準線

demo

排序 order

order: 1;
order: -1;

數字越小越前面,預設 0

demo

練習

使用 flex 重新切版 切版初體驗

參考資源

flexbox froggy

偽類

元素狀態,使用 : 符號

游標停留 :hover

.block {
    width: 100px;
    height: 100px;
    background: #000;
    color: #fff;
    margin: 10px auto;
    display: flex;
    justify-content: center;
    align-items: center;
}

.block:hover {
    background: red;
}

demo

已選取 :checked

用於可切換選取狀態的元素

  • radio
  • checkbox
.on-checked:checked {
    width: 100px;
    height: 100px;
}

demo

反向 :not(selector)

.block {
    background: red;
}

.block div:not(.item) {
    color: yellow;
}

demo

子層指定 :nth-child(n)

ul li:nth-child(3) {
    background: red;
    color: yellow;
}

demo

子層倍數指定 :nth-child(an+b)

a 基數

n0 開始自動遞增

b 常數

ul.n2 li:nth-child(2n) {
    background: red;
    color: yellow;
}

ul.n2_1 li:nth-child(2n + 1) {
    background: red;
    color: yellow;
}

ul.n3 li:nth-child(3n) {
    background: red;
    color: yellow;
}

demo

子層第一個 :first-child

等同 :nth-child(1)

ul li:first-child {
    background: red;
    color: yellow;
}

demo

子層最後一個 :last-child

ul li:last-child {
    background: red;
    color: yellow;
}

demo

偽元素

不存在於 document tree (DOM),符號 ::

前方插入 ::before

.block::before {
    content: 'David ';
}
<div class="block">Say Hi!</div>

demo

由於不存在於 DOM 內,無法選取

content 為必要屬性,如沒有此屬性,::before 無法生效

.block::before {
    content: attr(data-name) ' ';
}
<div class="block" data-name="David">Say Hi!</div>

demo

content 也可以搭配 attr 抓取元素本身屬性數值來呈現

自訂 ul li icon

後方插入 ::after

.block::after {
    content: ' David';
}
<div class="block">Hello</div>

demo

文字反白 ::selection

.blue {
    height: 50px;
    line-height: 50px;
    border: 1px solid #dedede;
    padding: 10px;
}

.blue::selection {
    background: blue;
    color: #fff;
}
<div class="blue">Select Me Show Blue.</div>

demo

第一行 ::first-line

.block::first-line {
    color: red;
}
<div class="block">
    我是誰?<br />
    我在哪?
</div>

demo

第一個字 ::first-letter

.block::first-letter {
    color: red;
    font-size: 40px;
}
<div class="block">
    我是誰?<br />
    我在哪?
</div>

demo

進階選擇器

後代 空格

指定子層樣式,不分階層

.block .item {
    background: red;
    color: yellow;
}
<div class="block">
    <div class="item">1</div>
    <div class="item">2</div>
    <div>
        <div class="item">3</div>
    </div>
</div>

demo

子階 >

指定子層樣式,僅限第一層

.block > .item {
    background: red;
    color: yellow;
}
<div class="block">
    <div class="item">1</div>
    <div class="item">2</div>
    <div>
        <div class="item">3</div>
    </div>
</div>

demo

嚴謹相鄰 +

元素 x 之後的第一個元素 y

.x-item + .y-item {
    color: red;
    font-size: 30px;
}
<div class="block">
    <div class="x-item">X</div>
    <div class="y-item">Y</div>
    <div class="y-item">Y</div>
    <div class="y-item">Y</div>
</div>

demo

寬鬆相鄰 ~

元素 x 之後的元素 y

.x-item ~ .y-item {
    color: red;
    font-size: 30px;
}
<div class="block">
    <div class="x-item">X</div>
    <div class="y-item">Y</div>
    <div class="y-item">Y</div>
    <div class="y-item">Y</div>
</div>

demo

屬性選擇器

基本 [attr]

符合屬性名稱

.item[title] {
    color: red;
}
<div class="block">
    <div class="item" title="1">1</div>
    <div class="item" title="2">2</div>
    <div class="item">3</div>
</div>

demo

指定值 [attr='value']

符合屬性名稱與數值

.item[title='1'] {
    color: red;
}
<div class="block">
    <div class="item" title="1">1</div>
    <div class="item" title="2">2</div>
    <div class="item">3</div>
</div>

demo

包含值 [attr*='value']

符合屬性名稱與包含數值(模糊比對)

.item[title*='1'] {
    color: red;
}
<div class="block">
    <div class="item" title="123">1</div>
    <div class="item" title="213">2</div>
    <div class="item">3</div>
</div>

demo

開頭值 [attr^='value']

符合屬性名稱與數值開頭

.item[title^='1'] {
    color: red;
}
<div class="block">
    <div class="item" title="123">1</div>
    <div class="item" title="213">2</div>
    <div class="item">3</div>
</div>

demo

結尾值 [attr$='value']

符合屬性名稱與數值最後

.item[title$='1'] {
    color: red;
}
<div class="block">
    <div class="item" title="123">1</div>
    <div class="item" title="251">2</div>
    <div class="item">3</div>
</div>

demo

多值 [attr~='value']

符合屬性名稱與其中一個數值

.item[title~='888'] {
    color: red;
}
<div class="block">
    <div class="item" title="123 888">1</div>
    <div class="item" title="213">2</div>
    <div class="item" title="888">3</div>
</div>

demo

手風琴選單

demo

提示

Step 1

建立 html 架構

<div class="accrodion">
    <div class="tab">
        <input type="checkbox" />
        <label for="">One</label>
        <div class="tab-content">Hello 1</div>
    </div>
</div>

demo

Step 2

定義基本樣式

.accrodion {
    width: 800px;
    margin: 20px auto;
}

.accrodion .tab {
    border: 1px solid #1459e3;
}

.accrodion .tab label {
    display: flex;
    align-items: center;
    width: 100%;
    height: 50px;
    padding: 0 10px;
    background: #1459e3;
    color: #fff;
    font-size: 20px;
}

.accrodion .tab .tab-content {
    padding: 10px;
}

demo

Step 3

串接 labelinput

<div class="accrodion">
    <div class="tab">
        <input type="checkbox" id="tab-1" />
        <label for="tab-1">One</label>
        <div class="tab-content">Hello 1</div>
    </div>
</div>

demo

Step 4

建立 .tab-content 連動樣式

.accrodion .tab .tab-content {
    padding: 10px;
    display: none;
}

.accrodion .tab input:checked ~ .tab-content {
    display: block;
}

demo

Step 5

建立 label 符號

.accrodion .tab label {
    display: flex;
    align-items: center;
    width: 100%;
    height: 50px;
    padding: 0 10px;
    background: #1459e3;
    color: #fff;
    font-size: 20px;
    cursor: pointer;
    position: relative;
}

.accrodion .tab label::after {
    content: '+';
    position: absolute;
    width: 50px;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    right: 0;
    top: 0;
    font-size: 30px;
}

.accrodion .tab input:checked ~ label::after {
    content: '-';
}

demo

Step 6

隱藏 checkbox

.accrodion .tab input[type='checkbox'] {
    display: none;
}

demo

done

複製兩個 tab 更改 id

<div class="accrodion">
    <div class="tab">
        <input type="checkbox" id="tab-1" />
        <label for="tab-1">One</label>
        <div class="tab-content">Hello 1</div>
    </div>
    <div class="tab">
        <input type="checkbox" id="tab-2" />
        <label for="tab-2">TWO</label>
        <div class="tab-content">Hello 2</div>
    </div>
    <div class="tab">
        <input type="checkbox" id="tab-3" />
        <label for="tab-3">THREE</label>
        <div class="tab-content">Hello 3</div>
    </div>
</div>

demo

SCSS

可程式化階層式開發 CSS

安裝編譯套件

VSCode > 延伸套件 > Live Sass Complier

建議專案結構

flowchart LR
    A[fa:fa-folder project root]
    A-->dist[fa:fa-folder dist]
    dist-->css[fa:fa-folder css]
    dist-->js[fa:fa-folder js]
    dist-->images[fa:fa-folder images]
    dist-->index[fa:fa-file index.html]
    css-->|fa:fa-ban 禁止異動| cssfile[fa:fa-file some.css]
    A-->src[fa:fa-folder src]
    src-->scss[fa:fa-folder scss]
    scss-->scssfile[fa:fa-file some.scss]
    scssfile-->|fa:fa-bolt 編譯產生| cssfile

編譯設定

Windows: Ctrl + Shift + P

Mac: Command + Shift + P

> Open Workspace Settings (JSON)

{
    "liveSassCompile.settings.formats": [
        {
            "format": "expanded",
            "extensionName": ".css",
            "savePath": "/dist/css"
        }
    ],
    "liveServer.settings.root": "/dist",
    "liveServer.settings.CustomBrowser": "chrome"
}

format 編譯格式

  • compressed 最小化 (Production)
  • expanded 展開 (Development)

extensionName 編譯後副檔名

savePath 編譯後檔案放置路徑

live sass compiler 設定

啟動 watch

VSCode 右下角 Watch my Sass

變數

scss

$main-color: red;

.container {
    color: $main-color;
}

css

.container {
    color: red;
}

$ 大部分語言都是變數的意思

運算

scss

$header-height: 70px;

#header {
    height: $header-height;
}

#wrap {
    padding-top: $header-height + 70;
}

css

#header {
    height: 70px;
}

#wrap  {
    padding-top: 140px;
}

巢狀 (Nesting)

scss

#header {
    background: yellow;

    &:hover {
        background: blue;
    }

    .title {
        color: red;
    }
}

css

#header {
    background: yellow;
}

#header:hover {
    background: blue;
}

#header .title {
    color: red;
}

預設為『後代』選擇器,& 代表自己

匯入 @import

將檔案內容複製進來

_reset.scss

*,
*::after,
*::before {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

scss

@import 'reset';

#header {
    background: yellow;
}

css

*,
*::after,
*::before {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

#header {
    background: yellow;
}
  • 使用 @import 匯入檔案
  • 匯入檔案名稱不需要加 _ 與副檔名 .scss
  • 被匯入檔案,檔案名稱最前面為 _ 可避免被編譯器編譯
  • 被匯入檔案,通常為共用屬性,不會單獨使用

混合 @mixin

scss

@mixin container {
    width: 1140px;
    margin: 10px auto;
}

#header {
    color: red;
    @include container;
}

css

#header {
    color: red;
    width: 1140px;
    margin: 10px auto;
}

@mixin 宣告

@include 使用

擴展 @extend

scss

#header {
    color: red;
    height: 300px;
}

#header2 {
    @extend #header;
    background: yellow;
    color: blue;
}

css

#header, #header2 {
    color: red;
    height: 300px;
}

#header2 {
    background: yellow;
    color: blue;
}

附加選擇器到 @extend 目的位置

函數 @function

scss

@function add-margin($value, $type: px) {
    @return $value + 30 + $type;
}

#header {
    background: red;
    margin: add-margin(20);
}

#wrap {
    margin: add-margin(10, em);
}

css

#header {
  background: red;
  margin: 50px;
}

#wrap {
  margin: 40em;
}

切版實戰

demo

素材下載

前置作業

參考 SCSS 建立專案

flowchart LR
    A[fa:fa-folder project root]
    A-->dist[fa:fa-folder dist]
    dist-->css[fa:fa-folder css]
    dist-->js[fa:fa-folder js]
    dist-->images[fa:fa-folder images]
    dist-->index[fa:fa-file index.html]
    css-->|fa:fa-ban 禁止異動| cssfile[fa:fa-file some.css]
    A-->src[fa:fa-folder src]
    src-->scss[fa:fa-folder scss]
    scss-->scssfile[fa:fa-file some.scss]
    scssfile-->|fa:fa-bolt 編譯產生| cssfile

建立 src/scss/_reset.scss

*,
*::after,
*::before {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

建立 src/scss/index.scss

@import 'reset';

使用 Watch Sass 編譯,看到 dist/css/index.css 表示前置作業完成

Step 1

建置 header 架構

dist/index.html

<div id="header">
    <div>Logo</div>
    <div>Menu</div>
</div>

demo

Step 2

編輯 header 樣式

看起來內容有局限在一定範圍內,所以先將 header 套入 .container

dist/index.html

<div id="header">
    <div class="container">
        <div class="header-content">
            <div>Logo</div>
            <div>Menu</div>
        </div>
    </div>
</div>

增加 .container 樣式

src/scss/_reset.scss

.container {
    max-width: 1000px;
    height: 100%;
    padding: 0 30px;
    position: relative;
    margin: auto;
}

加入 .header-content 樣式

src/scss/index.scss

#header {
    height: 80px;
    .header-content {
        height: 100%;
        display: flex;
        justify-content: space-between;
        align-items: center;
        background: rgb(240, 240, 240);
        padding: 0 15px;
        border-bottom-right-radius: 8px;
        border-bottom-left-radius: 8px;
    }
}

demo

Step 3

編輯 logomenu

dist/index.html

<div>
    <a href=""><img src="images/logo.svg" alt="appedu" /></a>
</div>
<div class="header-menu">
    <ul>
        <li>
            <a href="#">
                <div class="en">About Us</div>
                <div>關於赫綵</div>
            </a>
        </li>
        <li>
            <a href="#">
                <div class="en">Join Us</div>
                <div>加入赫綵</div>
            </a>
        </li>
        <li>
            <a href="#">
                <div class="en">Courses</div>
                <div>課程總覽</div>
            </a>
        </li>
    </ul>
</div>

加入 ula 的初始化

src/scss/_reset.scss

ul {
    list-style-type: none;
}

a {
    color: inherit;
    text-decoration: none;
}

加入 header-menu 樣式

src/scss/index.scss

#header {
    // ...

    .header-menu {
        ul {
            display: flex;

            li {
                font-weight: 700;
                text-align: center;
                margin-right: 30px;

                &:last-child {
                    margin-right: 0;
                }

                .en {
                    text-transform: uppercase;
                    font-size: 10px;
                }
            }
        }
    }
}

demo

Step 4

建立一頁式表單區塊

dist/index.html

<div id="join-form">
    <div class="container">
        <div>LEFT</div>
        <div>RIGHT</div>
    </div>
</div>

加入 #join-form 樣式

src/scss/index.scss

#join-form {
    min-height: 100vh;
    background-color: #000;
    background-image: url(../images/banner.jpeg);
    background-size: cover;
    color: #fff;
}

#header 設定為 fixed

src/scss/index.scss

#header {
    position: fixed;
}

此時發現區塊沒有滿版,調整一下

src/scss/index.scss

#header {
    position: fixed;
    width: 100%;
}

滿版了,但是點不到選單

src/scss/index.scss

#header {
    position: fixed;
    width: 100%;
    z-index: 9;
}

demo

Step 5

編輯表單內容區塊

dist/index.html

<div id="join-form">
    <div class="container">
        <div class="join-content">
            <div>LEFT</div>
            <div>RIGHT</div>
        </div>
    </div>
</div>

加入 .join-content 樣式

src/scss/index.scss

#join-form {
    // ...

    .join-content {
        width: 100%;
        height: 500px;
        background: rgba(10, 70, 161, 0.879);
    }
}

因為要讓區塊在畫面中間,使用 flex 水平垂直置中

增加 #join-form 樣式

src/scss/index.scss

#join-form {
    // ...

    display: flex;
    align-content: center;
    justify-content: center;

    // ...
}

此時發現 .join-content 沒有如預期的寬度滿版

發現是 .container 受到 flex 影響沒有滿版

增加 .container 樣式

src/scss/_reset.scss

.container {
    // ...
    flex: 1;
}

demo

Step 6

設計表單內容

dist/index.html

<div id="join-form">
    <div class="container">
        <div class="join-content">
            <div class="join-content-left">
                <h3 class="slogan">加入赫綵,人生精彩</h3>
                <div class="join-text">
                    就我個人來說,加入赫綵對我的意義,不能不說非常重大。一般來說,儘管如此,我們仍然需要對加入赫綵保持懷疑的態度。加入赫綵可以說是有著成為常識的趨勢。這必定是個前衛大膽的想法。培根說過一句很有意思的話,知識貧乏最能讓人生出許多懷疑。請諸位將這段話在心中默念三遍。儘管如此,別人往往卻不這麼想。
                </div>
            </div>
            <div class="join-content-right">
                <h3 class="slogan">加入赫綵,就是現在</h3>
                <div>
                    <input type="text" placeholder="姓名" class="form-control" />
                </div>
                <div>
                    <input type="text" placeholder="信箱" class="form-control" />
                </div>
                <div class="text-end">
                    <button class="btn">立即加入</button>
                </div>
            </div>
        </div>
    </div>
</div>

增加樣式

src/scss/index.scss

#join-form {
    // ..,
    .join-content {
        // ...

        display: flex;
        padding: 20px;

        > div {
            width: 50%;
        }

        .join-content-left {
            border-right: 2px dashed #fff;
            padding-right: 20px;
        }

        .join-content-right {
            padding-left: 20px;
        }
    }
}

加入 .slogan 通用樣式

src/scss/_reset.scss

.slogan {
    text-align: center;
    font-size: 26px;
    padding-top: 30px;
    padding-bottom: 10px;
    margin-bottom: 20px;
    position: relative;

    &::after {
        content: '';
        position: absolute;
        bottom: 0;
        left: calc(50% - 140px);
        width: 0;
        height: 3px;
        border-left: solid 100px #1072c2;
        border-right: solid 180px #fff;
    }
}

加入 .join-text 樣式

src/scss/index.scss

#join-form {
    // ..,

    .join-content {
        // ...

        .join-text {
            line-height: 30px;
            text-align: justify;
        }
    }
}

加入 .form-control.btn 樣式

src/scss/index.scss

#join-form {
    // ..,

    .join-content {
        // ...

        .form-control {
            width: 100%;
            height: 40px;
            margin-bottom: 20px;
            padding: 0 15px;
            font-size: 16px;
            border-radius: 3px;
            border: 0;
            outline: 0;
        }

        .btn {
            padding: 10px 15px;
            border: 0;
            outline: 0;
            background: rgb(64, 5, 165);
            color: #fff;
            font-size: 14px;
            font-weight: 700;
            letter-spacing: 1px;
            border-radius: 3px;
            cursor: pointer;

            &:hover {
                background: rgb(84, 31, 175);
            }
        }
    }
}

加入 .text-end 通用樣式

src/scss/_reset.scss

.text-end {
    text-align: right;
}

#join-content 高度改為為最小高度,讓內容自動擴增

src/scss/index.scss

#join-form {
    // ...

    .join-content { 
        // ...

        // height: 500px;
        min-height: 200px;

        // ...
    }
}

demo

Step 7

建立課程顧問頁面

dist/index.html

<div id="courses">
    <h3 class="slogan">課程顧問</h3>
</div>

加入 #courses 樣式

src/scss/index.scss

#courses {
    height: 50vh;
    background-image: url(../images/process.jpeg);
    background-size: cover;
    padding-top: 30px;
    padding-bottom: 30px;
    color: #fff;
}

demo

此時發現背景圖片白色與字體白色色調太近,文字可讀性低

Step 8

加入 .mask 樣式

src/scss/_reset.scss

.mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.438);
}

#courses 區塊調整

dist/index.html

<div id="courses">
    <div class="courses-content">
        <h3 class="slogan">課程顧問</h3>
    </div>
    <div class="mask"></div>
</div>

增加對應樣式

src/scss/index.scss

#courses {
    // ...

    position: relative;

    .courses-content {
        position: relative;
        z-index: 2;
    }
}

demo

Step 9

製作 timeline

dist/index.html

<div class="timeline">
    <div class="item">
        <div class="circle"></div>
        <div class="text">
            <h3>需求訪談</h3>
            <div>深度了解需求,給予最適合的建議</div>
        </div>
    </div>
</div>

增加 .timeline 樣式

src/scss/_reset.scss

.timeline {
    position: relative;
    width: 100%;
}

增加 .item 樣式

src/scss/_reset.scss

.timeline {
    // ...

    .item {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100%;
        position: relative;
        padding-top: 20px;
        padding-bottom: 20px;
        word-break: break-all;
    }
}

增加 .circle 樣式

src/scss/_reset.scss

.timeline {
    // ...

    .item {
        // ...

        .circle {
            width: 24px;
            height: 24px;
            border-radius: 24px;
            background: red;
        }
    }
}

此時 .circle 並沒有在中間,因為他與 .text 一同分享 .itemflex 空間

增加 .text 樣式

scr/scss/_reset.scss

.timeline {
    // ...

    .item {
        // ...

        .text {
            position: absolute;
            padding: 20px;
            background: red;
            border-radius: 8px;
        }
    }
}

透過 padding 產生內距,由內容稱出寬高,避免固定寬高破版

此時 .text 蓋在 .circle 上面,須將 .text 推到旁邊

.timeline {
    // ...

    .item {
        // ...

        .text {
            // ...

            left: calc(50% + 50px);
        }
    }
}

此時 .circle 因為 .text 設定為 position: absolute; 已經不佔據寬高,所以獨享 .item

calc+ 符號兩邊要留空白,不然編譯會出問題

增加白色虛線

src/scss/_reset.scss

.timeline {
    // ...

    &::after {
        content: '';
        position: absolute;
        border-left: 4px dashed #fff;
        height: 100%;
        left: calc(50% - 2px);
        top: 0;
    }

    // ...
}

此時 .circle 被白色虛線壓住了,調整一下

src/scss/_reset.scss

.timeline {
    // ...

    .item {
        // ...

        z-index: 2;
    }
}

增加第二個 .item

dist/index.html

<div class="timeline">
    <!-- ... -->

    <div class="item">
        <div class="circle"></div>
        <div class="text">
            <h3>課程搭配</h3>
            <div>提供專業課程配套方案</div>
        </div>
    </div>
</div>

將偶數 .item 內的 .text 往另一邊推

src/scss/_reset.scss

.timeline {
    // ..

    .item {
        // ...

        &:nth-child(even) {
            .text {
                right: calc(50% + 50px);
                text-align: right;
            }
        }
    }
}

看起來怪怪的,這是因為原先的 .text 已經有 left 屬性,在 CSS 覆寫疊加機制下,最終偶數 .item.text 會變成

.text {
    // ...

    left: calc(50% + 50px);
    right: calc(50% + 50px);
}

一下子要左邊一下子要右邊,搞得我好亂啊!

使用 unsetleft 取消設定

src/scss/_reset.scss

.timeline {
    // ..

    .item {
        // ...

        &:nth-child(even) {
            .text {
                // ...

                left: unset;
            }
        }
    }
}

將剩下的區塊補完

dist/index.html

<div class="timeline">
    <div class="item">
        <div class="circle" data-color="yellow"></div>
        <div class="text">
            <h3>需求訪談</h3>
            <div>深度了解需求,給予最適合的建議</div>
        </div>
    </div>
    <div class="item">
        <div class="circle"></div>
        <div class="text">
            <h3>課程搭配</h3>
            <div>提供專業課程配套方案</div>
        </div>
    </div>
    <div class="item">
        <div class="circle"></div>
        <div class="text">
            <h3>免費試聽</h3>
            <div>實際體驗課程狀況</div>
        </div>
    </div>
    <div class="item">
        <div class="circle"></div>
        <div class="text">
            <h3>分期付款</h3>
            <div>提供無壓力的付款方式</div>
        </div>
    </div>
    <div class="item">
        <div class="circle"></div>
        <div class="text">
            <h3>課後關懷</h3>
            <div>關心上課情況,給予適度調整</div>
        </div>
    </div>
    <div class="item">
        <div class="circle"></div>
        <div class="text">
            <h3>就業輔導</h3>
            <div>結訓後,提供就業媒合</div>
        </div>
    </div>
</div>

補完發現,內容跑去外面了(破版),這是因為 #courses 設定固定高度 height: 50vh;

src/scss/index.scss

#courses {
    // height: 50vh;
    min-height: 50vh;
}

.timeline 內的第一個 .item 與最後一個 .item 調整一下,讓線條可以突出

src/scss/_reset.scss

.timeline {
    // ...
    
    .item {
        // ...

        &:first-child {
            padding-top: 50px;
        }

        &:last-child {
            padding-bottom: 50px;
        }
    }
}

設定交錯顏色

src/scss/_reset.scss

.flow {
    // ...

    .item {
        // ...

        .circle {
            // ...

            background: rgb(10, 149, 151);
        }

        .text {
            // ...

            background: rgb(10, 149, 151);
        }

        &:nth-child(even) {
            .circle {
                background: blue;
            }
            .text {
                // ...

                background: blue;
            }
        }

        &:nth-child(3n) {
            .circle {
                background: red;
            }
            .text {
                background: red;
            }
        }

        &:nth-child(4n) {
            .circle {
                background: rgb(10, 149, 151);
            }
            .text {
                background: rgb(10, 149, 151);
            }
        }

        &:nth-child(5n) {
            .circle {
                background: blue;
            }
            .text {
                background: blue;
            }
        }
    }
}

demo

Step 10

製作頁尾 #footer

dist/index.html

 <div id="footer">
    <div class="container">
        <div class="footer-content">
            <div>
                <img src="images/logo.svg" alt="" />
            </div>
            <div>&copy; appedu 2022.</div>
        </div>
    </div>
</div>

增加 #footer 樣式

src/scss/index.scss

#footer {
    height: 100px;

    .footer-content {
        height: 100%;
        padding: 0 15px;
        display: flex;
        align-items: center;
        justify-content: space-between;
    }
}

demo

變形

將元素進行『旋轉』『位移』『縮放』『傾斜』等效果

使用 transform 屬性

旋轉

可將元素進行角度旋轉

使用 transform 屬性來定義 rotate,單位為 deg

宣告

transform: rotate(30deg);

html

<div class="block"></div>
<div class="block rotate"></div>

css

.block {
    width: 100px;
    height: 100px;
    background: #dedede;
    margin: 10px auto;
}

.rotate {
    transform: rotate(90deg);
}

demo

春字

demo

html

<div class="spring">春</div>

css

.spring {
    width: 100px;
    height: 100px;
    background-color: red;
    color: #fff;
    font-size: 20px;
    font-weight: 800;
    transform: rotate(135deg);
    margin: 50px auto;
    display: flex;
    align-items: center;
    justify-content: center;
}

此時文字因為旋轉的影響,沒有上下顛倒,而是斜了一邊

html 結構加工

 <div class="spring">
    <div class="text">春</div>
</div>

上下顛倒的角度為 180deg,目前旋轉 135deg,將剩下的 45deg 補給 .text

.spring .text {
    transform: rotate(45deg);
}

特價條

demo

html

<div class="product-discount">
    <div class="product-image">
        <div>Image</div>
        <div class="discount-bar">off 20%</div>
    </div>
    <div class="content">
        <h3 class="product-title">Product Name</h3>
        <div class="product-description">
            <p>Product description.</p>
            <p>Product description.</p>
        </div>
    </div>
</div>

scss

*,
*::after,
*::before {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

.product-discount {
    margin: 10px auto;
    height: 500px;
    width: 300px;
    background: #dedede;
    padding: 10px;

    .product-image {
        width: 100%;
        height: 200px;
        background: rgb(255, 127, 95);
        color: #fff;
        display: flex;
        justify-content: center;
        align-items: center;
        position: relative;
        overflow: hidden;

        .discount-bar {
            position: absolute;
            top: 25px;
            right: -120px;
            background: #fff;
            color: red;
            transform: rotate(45deg);
            width: 300px;
            text-align: center;
            font-size: 14px;
            font-weight: 800;
        }
    }

    .product-title {
        margin: 10px 0;
        font-size: 20px;
        font-weight: 700;
    }

    .product-description {
        line-height: 30px;
    }

}

原點

變形參考點,類似 position 的參考原點概念,預設原點為 x 50%, y 50% (中心點)

宣告

transform-origin: x y;

x 除了使用數值外,也可以使用 left, center, right

y 除了使用數值外,也可以使用 top, center, bottom

demo

位移

可將元素進行位置移動

使用 transform 屬性來定義 translate

宣告

transform: translate(x, y);

demo

與 position offset 差異

translate 會盡量使用 GPU 加速,製作移動影格時,使用 translate 效能會比較好

如果網頁需要支援 IE8 瀏覽器,就只能使用 position offset

兩者可互相搭配使用

縮放

可將元素進行比例縮放

使用 transform 屬性來定義 scale

宣告

transform: scale(2);

超小字

大部分的瀏覽器都有最小字體的限制,scale 可以突破此限制

.super-mini {
    font-size: 10px;
    transform: scale(0.5);
}

demo

特價條圖片放大

demo

.product-discount{
    &:hover {
        .product-image > div:first-child {
            transform: scale(2);
        }
    }
}

傾斜

可將元素進行角度傾斜

使用 transform 屬性來定義 skew

宣告

transform: skew(xdeg, ydeg);

demo

轉場

兩點之間變化的影格補禎

demo

限制

兩點必須是確切數值轉換

無效範例

.f1 {
    width: auto;

    &:hover {
        width: 100px;
    }
}

.f2 {
    display: block;

    &.hide {
        didplay: none;
    }
}

auto 並非確切的數值

display 顯示方式變化也無法補禎

補禎屬性

兩點變化可能包含許多屬性,此屬性可設定特定屬性進行補禎

宣告

transition-property: height; /* 單屬性 */
transition-property: height, width; /* 多屬性 */
transition-property: all; /* 預設 */
transition-property: none;

demo

補禎時間

兩點變化補禎時間

宣告

transition-duration: 0.5s;
transition-duration: 500ms;

延遲執行

兩點變化延遲執行

宣告

transition-delay: 0.5s;
transition-delay: 500ms;

速度曲線

兩點變化補禎速度曲線

宣告

transition-timing-function: linear; /* 均速 */
transition-timing-function: ease; /* 緩入,中間快,緩出,預設值 */
transition-timing-function: ease-in; /* 緩入 */
transition-timing-function: ease-out; /* 緩出 */
transition-timing-function: ease-in-out; /* 緩入緩出 */
transition-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); /* 貝茲曲線 */

特價條圖片滑順放大

demo

.product-discount{
    &:hover {
        .product-image > div:first-child {
            transform: scale(2);
            transition: all 1s;
        }
    }
}

動畫

搭配 @keyframes 使用

keyframes

宣告關鍵影格

demo

html

<div class="block"></div>

scss

.block {
    width: 100px;
    height: 100px;
    position: fixed;
    top: 10px;
    left: 10px;
    background: #000;
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;

    .run {
        animation-name: move;
        animation-duration: 3s;
    }
}


@keyframes move {
    from {
        left: 10px;
    }

    to {
        left: 300px;
    }
}

from 等同 0%

to 等同 100%

多關鍵影格

除了使用 fromto 來表示

也可以使用百分比的方式來將細節更完善

demo

scss

@keyframes move {
    0% {
        left: 10px;
    }

    20% {
        top: 100px;
        transform: rotate(180deg);
    }

    50% {
        top: 150px;
        transform: rotate(360deg) scale(2);
    }

    100% {
        left: 300px;
    }
}

動畫名稱

animation-name

使用 @keyframes

.run {
    animation-name: move;
}

動畫時間

animation-duration

動畫執行時間

.run {
    animation-duration: 3s;
    animation-duration: 3000ms;
}

動畫延遲

animation-delay

動畫延遲時間

.run {
    animation-delay: 3s;
    animation-delay: 3000ms;
}

動畫速度曲線

animation-timing-function

demo

.linear {
    animation-timing-function: linear;
}

.ease {
    animation-timing-function: ease;
}

.ease-in {
    animation-timing-function: ease-in;
}

.ease-out {
    animation-timing-function: ease-out;
}

.ease-in-out {
    animation-timing-function: ease-in-out;
}

.cubic-bezier {
    animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
}

.steps {
    animation-timing-function: steps(3, start);
}

.step-start {
    animation-timing-function: step-start;
}

.step-end {
    animation-timing-function: step-end;
}

.initial {
    animation-timing-function: initial;
}

.inherit {
    animation-timing-function: inherit;
}

執行次數

animation-iteration-count

預設一次,infinite 表示不停止

demo

播放方向

animation-direction

順向

0% -> 100%,預設值

.normal {
    animation-direction: normal;
}

逆向

100% -> 0%

.reverse {
    animation-direction: reverse;
}

demo

輪播

奇數 0% -> 100%,偶數 100% -> 0%

參考播放次數 animation-iteration-count

.alternate {
    animation-direction: alternate;
}

demo

逆向輪播

奇數 100% -> 0%,偶數 0% -> 100%

參考播放次數 animation-iteration-count

.alternate-reverse {
    animation-direction: alternate-reverse;
}

demo

模式

動畫結束後模式

預設

動畫停留在當下樣式

.none {
    animation-fill-mode: none;
}

最後影格

動畫停留在最後影格

.forwards {
    animation-fill-mode: forwards;
}

demo

開始影格

動畫停留在開始影格,搭配 animation-delay 才看得出效果

.backwards {
    animation-fill-mode: backwards;
}

demo

開始最後影格

結合 forwardsbackwards 特性

.both {
    animation-fill-mode: both;
}

demo

綜合練習

demo

藍天

body {
    background: rgb(49, 141, 216);
}

建立雲朵

.cloud {
    width: 500px;
    height: 120px;
    border-radius: 1000px;
    background: #fff;
    position: absolute;
    top: 200px;
    left: 0;

    &::after {
        content: '';
        position: absolute;
        width: 100px;
        height: 100px;
        border-radius: 100px;
        background: #fff;
        top: -50px;
        left: 100px;
    }

    &::before {
        content: '';
        position: absolute;
        width: 230px;
        height: 200px;
        border-radius: 300px;
        background: #fff;
        top: -100px;
        left: 190px;
    }
}

建立影格

@keyframes move {
    from {
        margin-left: 0;
    }

    to {
        margin-left: 100%;
    }
}

套用動畫

.cloud {
    // ...

    animation-name: move;
    animation-duration: 10s;
    animation-iteration-count: infinite;
    animation-direction: alternate;
    animation-timing-function: linear;

    // ...
}

大小雲朵


.xl {
    transform: scale(1.2);
}

.small {
    transform: scale(0.5);
}

時間差異

animation-duration

套件

animate.css

響應式網頁

解決跨裝置不同寬度顯示問題

RWD 與 AWD

RWDAWD
網址數量12
維護數量12
SEO重複內容與流量分散
載入速度更快
開發成本較低
維護成本較低
相容性較低

Fluid image

img 這個不太受控的元素,常常會導致實作 RWD 時破版

.fluid-image {
    width: 100%;
    height: auto;
}

由於在意的是寬度會不會破版,所以將 width 限制在 100%

高度設定 auto 是因為圖片有比例問題,如果高度也設定固定高度,會造成圖片比例失真

寬高其中一邊為固定,另外一邊必定為 auto

設定 width:100% 有個問題,如果圖片寬度不夠時,會被硬拉到滿版,此時也可能會造成圖片顯示失真,改為 max-width 解決此問題

.fluid-image {
    max-width: 100%;
    // width: 100%;
    height: auto;
}

Media Query

依據不同條件套用對應 CSS 樣式

@media <not | only> <media type> and (<media feature> <and | or | not> <media feature>){
    // CSS
}

media type

typedescription
all全部 (預設)
print印表機
braille點字機
screen視窗螢幕大小
handheld行動裝置
tv電視
projection投影機

常用的有 screenprint

列印學生成績

media feature

featuredescription
device-width裝置寬度
device-height裝置高度
width視窗寬度
height視窗高度
max-device-width最大裝置寬度
max-device-height最大裝置高度
max-width最大寬度
max-height最大高度
min-device-width最小裝置寬度
min-device-height最小裝置高度
min-width最小寬度
min-height最小高度
orientation裝置方向,portrait(直向) landscape(橫向)

常用的有 min-widthmax-width

min-width 大於等於時套用

max-width 小於等於時套用

一個網站 min-widthmax-width 不要同時使用,避免規則衝突

練習

  • 設定一個區塊,背景顏色黃色
  • 小於等於 800px 時變藍色
  • 小於等於 600px 時變紅色

此範例有什麼問題

Grid System

使用 class 讓頁面適應各種裝置寬度

格數 Column

每一列切割的數量,可自定義,常見為 12 格系統 (12 column grid system)

溝槽 Gutter

格與格之間的距離,可使用 marginpadding 實作

flexboxgrid

主流 CSS 框架都有實作 grid system,不用自己實作,知道原理即可

RWD實戰

切版實戰支援 RWD

Desktop

前置作業

先將原本的 src/scss/index.scss 改為 desktop.scss

建立 src/scss/mobile.scss

@import 'desktop';

dist/index.html 引入 css/mobile.css

Step 1

增加漢堡盒樣式

dist/index.html

<div id="header">
    <div class="container">
        <div class="header-content">
            <!-- ... -->
            
            <div class="mobile-menu">
                <span></span>
                <span></span>
                <span></span>
            </div>
        </div>
    </div>
</div>

src/scss/mobile.scss

.mobile-menu {
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    cursor: pointer;

    span {
        display: inline-block;
        width: 25px;
        height: 4px;
        margin-bottom: 4px;
        background: #000;

        &:last-child {
            margin-bottom: 0;
        }
    }
}

demo

Step 2

  • 預設漢堡盒隱藏
  • 當寬度小於 768px 時,隱藏選單,顯示漢堡盒

src/scss/mobile.scss

.mobile-menu {
    // ...
    // display: flex;
    display: none
}

@media screen and (max-width: 768px) {
    .mobile-menu {
        display: flex;
    }

    .header-menu {
        display: none;
    }
}

demo

Step 3

點擊漢堡盒時

  • 變換樣式為 X
  • 顏色為紅色

src/scss/mobile.scss

.mobile-menu {
    // ...

    span {
        // ...

        transition: all 0.3s
    }

    &:hover {
        span:first-child {
            transform: rotate(45deg);
            margin-top: 4px;
            position: absolute;
            background: red;
        }

        span:nth-child(2) {
            display: none;
        }

        span:last-child {
            transform: rotate(-45deg);
            background: red;
        }
    }
}

demo

Step 4

目前使用 :hover 來觸發

但是手機版沒有 :hover 可以用,所以要改用相鄰選擇器來處理狀態

增加 checkbox

dist/index.html

<div id="header">
    <div class="container">
        <div class="header-content">
            <input type="checkbox" id="mobile-switch" />
            <!-- ... -->
        </div>
    </div>
</div>                    

src/scss/mobile.scss

#mobile-switch {
    display: none;
}

選單改為 label 觸發 checkbox

dist/index.html

<div id="header">
    <div class="container">
        <div class="header-content">
            <!-- ... -->
            <label for="mobile-switch" class="mobile-menu">
                <span></span>
                <span></span>
                <span></span>
            </label>
        </div>
    </div>
</div>                    

將原本的 :hover 樣式搬移到相鄰選擇器

src/scss/mobile.scss

#mobile-switch {
    // ...

    &:checked ~ .mobile-menu {
        span:first-child {
            transform: rotate(45deg);
            margin-top: 4px;
            position: absolute;
            background: red;
        }

        span:nth-child(2) {
            display: none;
        }

        span:last-child {
            transform: rotate(-45deg);
            background: red;
        }
    }
}

demo

Step 5

checkbox 被勾選時,顯示選單

src/scss/mobile.scss

#mobile-switch {
    // ...

     &:checked ~ .header-menu {
        display: flex;
    }
}

手機版選單位置

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    .header-menu {
        // ...
        position: absolute;
        top: 70px;
        left: 0;
        width: 100%;
        background: inherit;
    }
}

發現寬度怪怪的

src/scss/mobile.scss

.header-content {
    position: relative;
}

手機版選單樣式

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    .header-menu {
        // ...

        justify-content: center;
        padding-top: 20px;

        ul {
            flex-direction: column;

            li {
                margin-bottom: 40px;
                margin-right: 0;
            }
        }
    }
}

選單歪一邊??

複寫 priority 問題

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    #header {
        .header-menu {
            // ...
        }
    }
}

demo

Step 6

加入表單手機版處理

翻轉區塊

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    #join-form {
        .join-content {
            flex-direction: column;
        }
    }
}

翻轉之後兩個區塊沒有滿版

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    #join-form {
        .join-content {
            // ...

            > div {
                width: 100%;
            }
        }
    }
}

左邊區塊去除右邊虛線與內距

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    #join-form {
        .join-content {
            // ...

            .join-content-left {
                padding: 0;
                border-right: unset;
            }
        }
    }
}

右邊區塊去除內距

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    #join-form {
        .join-content {
            // ...

            .join-content-right {
                padding: 0;
            }
        }
    }
}

demo

Step 7

timeline 手機版

將文字區塊統一放到右邊

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    .timeline {
        .item:nth-child(even) {
            .text {
                right: unset;
                left: calc(50% + 50px);
            }
        }
    }
}

文字區塊擠在一起了

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    .timeline {
        // ...

        .item {
            min-height: 130px;
        }
    }
}

將區塊靠左邊

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    .timeline {
        // ...

        .item {
            // ...

            justify-content: flex-start;
            padding-left: 20px;
        }
    }
}

虛線對齊

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    .timeline {
        // ...

        &::after {
            left: 30px;
        }
    }
}

文字區塊靠左

src/scss/mobile.scss

@media screen and (max-width: 768px) {
    // ...

    .timeline {
        .item:nth-child(even) {
            .text {
                right: unset;
                // left: calc(50% + 50px);
                left: calc(20px + 50px);
            }
        }

        .item {
            // ...

            .text {
                right: unset;
                left: calc(20px + 50px);
            }
        }
    }
}

demo

CSS 框架

優點

  • 規範一致
  • 快速開發
  • 維護便利

Bootstrap

優點

  • 完整度高
  • 學習曲線低
  • 可擴充 (SCSS)
  • 官方文件齊全
  • RWD 控制方便
  • 透過 class 組合,簡單的需求甚至不需要自己寫 css
  • 不需要編譯器編譯 (相對於 Tailwind CSS)

缺點

  • 只能滿足 90% 需求
  • component 構成複雜(善用文件)
  • 眾多 !important,覆寫彈性降低
  • 大量 class 組合,主體辨識度較低
  • 不支援 IE 11 以下瀏覽器

引用

官方網站

Breakpoints

BreakpointClass infixDimensions
Extra smallNone< 576px
Smallsm>= 576px
Mediummd>= 768px
Largelg>= 992px
Extra largexl>= 1200px
Extra extra largexxl>= 1400px

Containers

Extra smallSmallMediumLargeX-LargeXX-Large
.container100%540px720px960px1140px1320px
.container-sm100%540px720px960px1140px1320px
.container-md100%100%720px960px1140px1320px
.container-lg100%100%100%960px1140px1320px
.container-xl100%100%100%100%1140px1320px
.container-xxl100%100%100%100%100%1320px
.container-fluid100%100%100%100%100%100%

Grid

xssmmdlgxlxxl
Container100%540px720px960px1140px1320px
Class prefix.col-.col-sm-.col-md-.col-lg-.col-xl-.col-xxl-
# of columns121212121212
Gutter width1.5rem1.5rem1.5rem1.5rem1.5rem1.5rem

demo

Images

https://getbootstrap.com/docs/5.2/content/images/

Tables

https://getbootstrap.com/docs/5.2/content/tables/

Alerts

https://getbootstrap.com/docs/5.2/components/alerts/

Buttons

https://getbootstrap.com/docs/5.2/components/buttons/

Forms

https://getbootstrap.com/docs/5.2/forms/form-control/

Accordion

https://getbootstrap.com/docs/5.2/components/accordion/

Spinners

https://getbootstrap.com/docs/5.2/components/spinners/

挑戰

CSS 登入畫面

demo

<div class="d-flex justify-content-center align-items-center vh-100">
    <div class="p-5 bg-dark text-white">
        <h2 class="text-center ps-5 pe-5">System Login</h2>
        <hr />
        <div class="mb-3">
            <label for="login-account" class="form-label">帳號</label>
            <input type="text" class="form-control" id="login-account" />
        </div>
        <div class="mb-3">
            <label for="login-password" class="form-label">密碼</label>
            <input type="password" class="form-control" id="login-password" />
        </div>
        <div class="form-check mb-3">
            <input type="checkbox" class="form-check-input" id="login-remember" />
            <label for="login-remember" class="form-check-label">記住密碼</label>
        </div>
        <div class="text-end">
            <button class="btn btn-sm btn-danger">登入</button>
        </div>
    </div>
</div>

程式概論

語言類型

靜態動態
編譯需要不需要
型態強調(強型態)不強調(弱型態)
效能較好較差
代表語言C, C++, JavaJavaScript, PHP, Python, Ruby

型態

型態類型關鍵字位元數範圍
整數byte8-128 ~ 127
整數short16-32768 ~ 32767
整數int32-2147483648 ~ 2147483647
整數long64-9223372036854775808 ~ 9223372036854775807
浮點數float32依據 IEEE 754 標準
浮點數double64依據 IEEE 754 標準
布林值boolean1true, flase
字元char16'\u0000' - '\uffff'

攸關記憶體使用量

強型態

int hp;

hp = 100;
hp = 200;
hp = "一百"; // error

弱型態

let hp;

hp = 100;
hp = 200;
hp = "一百"; // success

事件驅動 Event-driven

傳統流程

flowchart LR
    A[客戶]
    B[店家]
    C[餐點]
    D[確認]
    E[客戶 N]

    E-->| 排隊 | A
    A-->| 1.點餐 | B
    B-->| 2.製作 | C
    C-->| 3.完成 | D
    D-->| 4.提供 | A

  • 客戶需要在原地等待餐點製作完成才可以離開
  • 店家一次也只能服務一個客戶

優點

  • 確保餐點提供無誤 (原子性)
    • 點餐
    • 確認
  • 依序提供餐點 (有序性)
    • 排隊
  • 可避免超賣 (可見性)
    • 完成時得知庫存

缺點

  • 無法有效消化大量人流

事件驅動

flowchart LR
    A[客戶]
    B[店家]
    C[餐點]
    D[確認]
    E[客戶 N]
    F[廚師 N]
    G[自由行動]

    E-->| 排隊 | A
    A-->| 1.點餐 | B
    B-->| 2.給呼叫器| A
    A-->| 3.離開 | G
    B-->| 3.派單 | F
    B-->| 3N.服務 | E
    E-->| 3N-1.點餐 | B
    B-->| 3N-2.給呼叫器| E
    F-->| 4.製作| C
    C-->| 5.完成 | D
    D-->| 6.呼叫 | A
    A-->| 7.取餐 | B

優點

  • 可快速消化人潮
  • 透過增加廚師理論上可以增加餐點提供數量

缺點

  • 餐點可能做錯
  • 餐點可能給錯
  • 收單後發現庫存不足

名詞補充

原子性 Atomic

  • 操作內所有的動作、變更都順利完成
  • 操作失敗,內部的動作、變更全部不存在

有序性 Ordering

  • FIFO (First In First Out)
  • Queue

可見性 Visibility

  • 當有異動時,其他人也會得知異動後的值

基本使用

腳本宣告

html 內

使用 <script></script> 元素宣告

<script>
    let age = 18;
</script>

獨立檔案

副檔名為 .js

使用 <script src="jsfile"></script> 引入

js/index.js

let age = 18;

index.html

<script src="js/index.js"></script>

終端機輸出

使用 console.log()

js/index.js

let age = 18;

console.log(age);

變數宣告

使用 let 進行宣告

let age = 18;

let age = 18; 宣告一個變數 age 內容為 18

變數名稱只能是英文跟 _ 開頭,不可用數字開頭以及特殊符號

變數型態

雖然 JavaScript 是弱型態語言,但不代表沒有型態

字串

使用 ' 或是 " 將內容包起來代表字串

let firstName = 'David';
let lastName = "Lin";
let fullName = firstName + ' ' + lastName;

使用 + 運算子執行字串串接

數字

整數或是浮點數(小數點)

let n1 = 1;
let n2 = 1.25;
let n3 = n1 + n2;

console.log(n3);

使用運算子來做處理

當數字遇到字串

let n1 = 1;
let s1 = '10';
let ns = n1 + s1;

console.log(ns);

只要運算過程遇到一個字串型態,全部會被轉為字串處理

顯示型態

typeof

let s1 = '10';

console.log(typeof s1);

型態轉換

字串轉為數字

parseInt(string, radix)

radix 進位系統,使用 10 進制轉換就輸入 10

let n1 = 1;
let s1 = '10';
let ns = parseInt(s1, 10) + n1;

console.log(typeof ns);

偷雞法

let n1 = 1;
let s1 = '10';
let ns = +s1 + n1;

console.log(typeof ns);

變數前面加上 + 轉型為數字(不要有空格)

數字轉文字

使用 .toString() 進行轉型

let n1 = 1;

console.log(typeof n1.toString());

布林 boolean

真(true)跟假(false)

let b1 = true;
let b2 = false;
let b3 = 1;
let b4 = 0;
let b5 = 's';
let b6 = '';
let b7 = -1;
let b8 = 2;

陣列 array

使用 [] 宣告

let a1 = []; // 宣告空陣列
let a2 = [1, 2, 3, 4]; // 宣告並給予初始值

console.log(a2[0]);

索引從 0 開始

增加

a2.push('aaa');

轉字串

a2.join(':');

: 分隔符號,可自訂

尋找

let index = a2.indexOf(2);

回傳符合內容的索引值,找不到時回傳 -1

刪除

.splice(index, delete_count)

a2.splice(0, 1);

從索引 0 刪除 1

物件 object

使用 {} 宣告

let student = {
    name: 'David',
    age: 18,
};

console.log(student);

student.name = 'John';
student.age = 20;

console.log(student);

函數

將事情給予名稱包裝

  • 好理解
  • 可重複使用
  • 彈性高 (可傳參數)

宣告

function add(num1, num2) {
    return num1 + num2;
}

function 宣告關鍵字

add 函數名稱

num1 第一個參數

num2 第二個參數

return num1 + num2 回傳數值

呼叫

函數名稱加上 () 進行呼叫

let calc = add(1, 10);

console.log(calc);

函數與變數

函數也是變數的一種

function add(num1, num2) {
    return num1 + num2;
}

console.log(add(1, 10));

add = 100;

console.log(add(1, 10));

add 將被覆寫為 100,呼叫失敗

關於參數

呼叫函數時,傳遞的參數會自動做變數宣告

flowchart LR
    A[add 1, 10]
    B[num1 = 1 num2 = 10]
    C[return num1 + num2]

    A-->| 初始化參數 | B
    B-->| 執行回傳 | C

DOM 操作

Document Object Model

HTML 上面的任何元素都是 DOM

DOM WIKI

抓取

單一

document.querySelector(selector)

單一抓取時,無論符合的 selector 有幾個,都只會回傳第一個

html

<div id="uid">UID</div>

<div class="student">Student 1</div>
<div class="student">Student 2</div>

js

let uid = document.querySelector('#uid');
let students = document.querySelector('.student');

console.log(uid);
console.log(students);

多選

document.querySelectorAll(selector)

單一抓取時,無論符合的 selector 有幾個,都會回傳陣列型態

html

<div id="uid">UID</div>

<div class="student">Student 1</div>
<div class="student">Student 2</div>

js

let uid = document.querySelectorAll('#uid');
let students = document.querySelectorAll('.student');

console.log(uid);
console.log(students);

控制

內容

dom.innerHTML

html

<div id="uid">UID</div>

js

let uid = document.querySelector('#uid');

console.log(uid.innerHTML);

uid.innerHTML = 'New UID';

數值

dom.value

html

<input type="text" value="David Lin" id="full-name">

js

let fullName = document.querySelector('#full-name');

console.log(fullName.value);

fullName.value = 'John Chen';

CSS

dom.style

遇到屬性有 - 連接的,使用小駝峰命名

background-color => backgroundColor

html

<div id="uid">UID</div>

js

let uid = document.querySelector('#uid');

uid.style.color = 'red';
uid.style.backgroundColor = 'yellow';

class

dom.classList

附加

dom.classList.add('class name')

重複附加也只會顯示一個

html

<div id="uid">UID</div>

js

let uid = document.querySelector('#uid');

uid.classList.add('some');
uid.classList.add('some');
uid.classList.add('some');

移除

dom.classList.remove('class name')

移除不存在的 class name 不會發生錯誤

html

<div id="uid">UID</div>

js

let uid = document.querySelector('#uid');

uid.classList.remove('some');
uid.classList.remove('some');
uid.classList.remove('some');

取得

dom.className

回傳字串,使用 String.split(' ') 切割成陣列

html

<div id="uid" class="first name full">UID</div>

js

let uid = document.querySelector('#uid');

let className = uid.className;

console.log(className);

let classes = className.split(' ');

console.log(classes);

確認名稱存在

dom.classList.contains('class name')

html

<div id="uid" class="first name full">UID</div>

js

let uid = document.querySelector('#uid');

let exists = uid.classList.contains('first');

console.log(exists);

dataset

dom.dataset 對應 data-* 屬性

html

<div id="uid" class="first name full" data-id="1">UID</div>

js

let uid = document.querySelector('#uid');

console.log(uid.dataset.id);

uid.dataset.id = 999;

事件監聽

dom.addEventListener(event, function)

event 事件類型

function 執行函數

html

<button id="btn">Click</button>

js

let btn = document.querySelector('#btn');

btn.addEventListener('click', function(e) {
    console.log(e);
})

click 點擊事件

e 事件物件,紀錄事件的詳細資料

互動初體驗

  1. 建立一顆按鈕
  2. 建立一姓名輸入匡
  3. 建立一個顯示區塊
  4. 當點擊按鈕時,顯示區塊顯示『Hi!, {姓名輸入匡內容}』

程式基本功

計算運算子

加法 +

let num1 = 10;
let num2 = 20;

let num3 = num1 + num2;

練習

  1. 建立兩個數字輸入匡 (A)(B)
  2. 建立一顆計算按鈕 (C)
  3. 建立一顯示區塊 (D)
  4. 當按下按鈕時,抓取兩數字輸入匡進行加法計算
  5. 將結果顯示在顯示區塊
    flowchart LR
    A[A]
    B[B]
    C[C]
    D[D]
    E[+]

    C-->| 抓取 | A
    C-->| 抓取 | B
    A-->E
    B-->E
    E-->| 顯示 | D

減法 -

let num1 = 10;
let num2 = 20;

let num3 = num1 - num2;

練習

  1. 建立兩個數字輸入匡 (A)(B)
  2. 建立一顆計算按鈕 (C)
  3. 建立一顯示區塊 (D)
  4. 當按下按鈕時,抓取兩數字輸入匡進行減法計算
  5. 將結果顯示在顯示區塊
    flowchart LR
    A[A]
    B[B]
    C[C]
    D[D]
    E[-]

    C-->| 抓取 | A
    C-->| 抓取 | B
    A-->E
    B-->E
    E-->| 顯示 | D

乘法 *

let num1 = 10;
let num2 = 20;

let num3 = num1 * num2;

練習

  1. 建立兩個數字輸入匡 (A)(B)
  2. 建立一顆計算按鈕 (C)
  3. 建立一顯示區塊 (D)
  4. 當按下按鈕時,抓取兩數字輸入匡進行乘法計算
  5. 將結果顯示在顯示區塊
    flowchart LR
    A[A]
    B[B]
    C[C]
    D[D]
    E[*]

    C-->| 抓取 | A
    C-->| 抓取 | B
    A-->E
    B-->E
    E-->| 顯示 | D

除法 /

let num1 = 10;
let num2 = 20;

let num3 = num1 / num2;

練習

  1. 建立兩個數字輸入匡 (A)(B)
  2. 建立一顆計算按鈕 (C)
  3. 建立一顯示區塊 (D)
  4. 當按下按鈕時,抓取兩數字輸入匡進行除法計算
  5. 將結果顯示在顯示區塊
    flowchart LR
    A[A]
    B[B]
    C[C]
    D[D]
    E[ / ]

    C-->| 抓取 | A
    C-->| 抓取 | B
    A-->E
    B-->E
    E-->| 顯示 | D

餘數 %

let num1 = 10;
let num2 = 20;

let num3 = num1 % num2;

練習

  1. 建立兩個數字輸入匡 (A)(B)
  2. 建立一顆計算按鈕 (C)
  3. 建立一顯示區塊 (D)
  4. 當按下按鈕時,抓取兩數字輸入匡進行餘數計算
  5. 將結果顯示在顯示區塊
    flowchart LR
    A[A]
    B[B]
    C[C]
    D[D]
    E[%]

    C-->| 抓取 | A
    C-->| 抓取 | B
    A-->E
    B-->E
    E-->| 顯示 | D

執行後遞增

let num1 = 1;

num++;

console.log(num1);

遞增後執行

let num1 = 1;

++num;

console.log(num1);

執行後遞減

let num1 = 1;

num--;

console.log(num1);

遞減後執行

let num1 = 1;

--num;

console.log(num1);

關於遞增遞減

let num1 = 1;
let num2 = num1++ + 1;
let num3 = ++num1 + 1;

num1, num2, num3 分別為多少?

判斷式

if

if (condition) {
    console.log('is true');
}

condition 必然是一個 boolean

if...else...

if (condition) {
    console.log('is true');
} else {
    console.log('is false');
}

if...else if...else...

if (condition) {
    console.log('is true');
} else if (condition2){
    console.log('condition2 is false');
} else {
    console.log('is false');
}

iif

let response = true ? 'is true' : 'is false';

三元判斷,適合用於單一動作時,同等於

if (true) {
    let response = 'is true';
} else {
    let response = 'is false';
}

switch

switch (some) {
    case 'match 1':
        console.log('is match 1');
        break;
    
    case 'match 2':
        console.log('is match 2');
        break;

    default:
        console.log('not match');
        break;
}

some 任意值

case match 1 some 等於 match 1

break 強制結束

判斷運算子

最終得到一個 boolean

等於

let num1 = 1;
let num2 = 10;

let condition = num1 == num2;

完全等於

let num1 = 10;
let num2 = '10';

let condition = num1 == num2;
let condition2 = num1 === num2;

連同型態一起比對

不等於

let num1 = 1;
let num2 = 10;

let condition = num1 != num2;

大於

let num1 = 10;
let num2 = 10;

let condition = num1 > num2;

大於等於

let num1 = 10;
let num2 = 10;

let condition = num1 >= num2;

小於

let num1 = 10;
let num2 = 10;

let condition = num1 < num2;

小於等於

let num1 = 10;
let num2 = 10;

let condition = num1 <= num2;

及閘 AND

let b1 = true;
let b2 = false;

let condition = b1 && $b2;

同時成立為真

b1b2condition
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

或閘 OR

let b1 = true;
let b2 = false;

let condition = b1 || $b2;

其一成立為真

b1b2condition
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

反閘 NOT

let b1 = true;

let condition = !b1;

真假反轉

b1condition
truefalse
falsetrue

迴圈

for

for (start; condition; next) {}

start 起始值

condition 滿足條件

next 每次執行完後執行動作

for (let i = 0; i < 10; i++) {
    console.log(i);
}

用於已知次數的重複執行

練習

乘法表

  1. 建立兩個數字輸入匡 (A)(B)
  2. 建立一顆按鈕 (C)
  3. 建立一張表格 (D)
  4. 按下按鈕後,抓取兩數字輸入匡內容
  5. 將兩數字使用 for 迴圈建立乘法表呈現在表格
    flowchart LR
    A[A]
    B[B]
    C[C]
    D[D]
    E[ for 迴圈]

    C-->| 抓取 | A
    C-->| 抓取 | B
    A-->E
    B-->E
    E-->| 顯示 | D

foreach

for (let key in object) {}

key 每次執行時的 object key

object 要執行迴圈的物件

let student = {
    name: 'David',
    age: 18
};

for (let field in student) {
    let value = student[field];
    console.log(field, value);
}

用於未知次數的重複執行

陣列內建

let scope = [100, 40, 20, 10, 0];

scope.forEach(function(value, index) {
    console.log(index, value);
});

while

while (condition) {}

condition 滿足條件,為 boolean 型態

持續執行,直到條件不滿足

while (true) {
    console.log('run');
}

注意無限迴圈

let num = 0;
while (num < 10) {
    console.log(num);
    num++;
}

等同於

for (let num = 0; num < 10; num++) {
    console.log(num);
}

do...while

do {} while (condition)

condition 滿足條件,為 boolean 型態

先執行一次,條件滿足時,持續執行

let num = 0;
do {
    console.log(num);
    num++;
} while (num < 10)

注意無限迴圈

生命週期

變數有效範圍

情境一

let num1 = 10;

function run() {
    let num2 = 20;
    num1 = 30;
    console.log(num1, num2);
}

run();
console.log(num1);
console.log(num2);

num2run 執行完畢後消失

情境二

let num1 = 10;

function run(num1) {
    let num2 = 20;
    num1 = 30;
    console.log(num1, num2);
}

run(num1);
console.log(num1);
console.log(num2);

num1 參數與外部的 num1 不同

情境三

let num1 = 10;

function run() {
    num2 = 20;
    num1 = 30;
    console.log(num1, num2);
}

run();
console.log(num1);
console.log(num2);

沒有使用 let 關鍵字宣告,預設為全域變數(Global)

Call by value

let num1 = 1;
let num2 = num;

num1++;

console.log(num1, num2);

num1 將內容複製一份給 num2,本質上是兩個不同的東西

Call by reference

let student = {
    name: 'David',
};

let student2 = student;

student.name = 'John';
console.log(student.name, student2.name);

student2.name = 'Dan';
console.log(student.name, student2.name);

student 將記憶體位置複製一份給 student2,所以兩個變數參考到同一份內容

具備此特性的類型

  • Object
  • Array

簡易型計算機實作

demo

事件使用

監聽

dom.addEventListener(event, function)

event 事件類型

function 執行函數

let dom = document.querySelector('#dom');

dom.addEventListener('click', function(e) {
    console.log(e);
});

e 事件氣泡,事件觸發時,系統自動傳遞

事件類型

click

點擊觸發

let btn = document.querySelector('#btn');

btn.addEventListener('click', function(e) {
    console.log(e);
});

change

焦點移出時且數值變化時觸發,用於輸入類型元素(有 value 屬性)

let name = document.querySelector('#name');

name.addEventListener('change', function(e) {
    console.log(name.value);
});

blur

焦點移出時觸發(focus -> unfocus)

let name = document.querySelector('#name');

name.addEventListener('blur', function(e) {
    console.log(name.value);
});

chagne 類似,差別在於無論數值有沒有變化都會觸發

keypress

鍵盤按下時觸發

let name = document.querySelector('#name');

name.addEventListener('keypress', function(e) {
    console.log(name.value);
});

keyup

鍵盤放開時觸發

let name = document.querySelector('#name');

name.addEventListener('keyup', function(e) {
    console.log(name.value);
});

補充

keypresskeyup 產生的 e 事件物件會附帶觸發的按鍵

使用 e.key 可以取得觸發的按鍵

let name = document.querySelector('#name');

name.addEventListener('keyup', function(e) {
    console.log(e.key);
});

練習

分數等級轉換

  1. 建立一個數字輸入匡
  2. 建立一個計算按鈕
  3. 當按下計算按鈕時,抓取數字輸入匡進行判斷
  4. 分數等級如下
    1. >= 90 甲
    2. >= 80 乙
    3. >= 70 丙
    4. >= 60 丁
    5. < 60 不及格
  5. 數字輸入匡按下 enter 時,也可以進行計算

demo

事件氣泡

事件監聽很好用,綁定都是針對單一元素處理,如果需要大量綁定,該如何處理?

html

<ul id="menu">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
</ul>

點擊每一個 li 都印出當下 li 內容

js

let lis = document.querySelectorAll('#menu li')

lis.forEach(function (li) {
    li.addEventListener('click', function (e) {
        console.log(li.innerHTML)
    })
})

問題

  1. 如果 li 有一萬個,會發生什麼事?
  2. 如果透過事件去增加 li,被新增的 li 被點擊時是否也會印出自己內容?

html

<ul id="menu">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
</ul>
<button id="add-btn">add li</button>

js

let lis = document.querySelectorAll('#menu li')

lis.forEach(function (li) {
    li.addEventListener('click', function (e) {
        console.log(li.innerHTML)
    })
})

let addBtn = document.querySelector('#add-btn')

addBtn.addEventListener('click', function (e) {
    let menu = document.querySelector('#menu')
    let li = document.createElement('li')
    li.innerHTML = 'new li'

    menu.appendChild(li)
})

觸發流程

先抓取,再冒泡

事件氣泡

了解事件觸發流程後,針對氣泡特性,改善先前 nli 的事件綁定問題

let menu = document.querySelector('#menu')

menu.addEventListener('click', function (e) {
    let li = e.target
    console.log(li.innerHTML)
})

e.target 事件氣泡觸發元素

有沒有機會觸發到 ul ?

let menu = document.querySelector('#menu')

menu.addEventListener('click', function (e) {
    let li = e.target
    if (e.tagName == 'LI') {
        console.log(li.innerHTML)
    }
})

e.tagName 觸發元素標籤,統一為大寫

中斷冒泡

事件氣泡無論節點是否有處理,都會往上冒泡。如果不想氣泡繼續往上,使用 e.stopPropagation() 中斷冒泡

中斷預設行為

特定元素無需事件監聽就會進行預設行為,例如 a 元素會進行網頁跳轉。如想要禁止這種預設行為執行,使用 e.preventDefault() 中斷預設行為

html

<a href="https://www.google.com" id="link">Google</a>
let link = document.querySelector('#link')
link.addEventListener('click', function (e) {
    e.preventDefault()
    console.log(e.target.innerHTML)
})

單次計時器

在特定秒數後,執行特定函數,只執行一次

宣告

setTimeout(function, ms)

setTimeout(function() {
    console.log('Run after 1000ms')
}, 1000)

1s = 1000ms

取消

clearTimeout(timer)

timersetTimeout 回傳的計時器編號

let timer = setTimeout(function() {
    console.log('Run after 1000ms')
}, 3000)

clearTimeout(timer)

重複計時器

在特定秒數後,執行特定函數,重複執行直到取消為止

宣告

setInterval(function, ms)

setInterval(function() {
    console.log('Run after 1000ms')
}, 1000)

1s = 1000ms

取消

clearInterval(timer)

timersetTimeout 回傳的計時器編號

let timer = setInterval(function() {
    console.log('Run after 1000ms')
}, 3000)

clearInterval(timer)

練習

電子鐘

  1. 建立 UI
  2. 每秒更新該 UI 時間

時間取得方式

let d = new Date();
let hh = d.getHours();
let mm = d.getMinutes();
let ss = d.getSeconds();

綜合練習

TODO List

  1. 建立文字輸入匡,與一顆新增按鈕
  2. 建立列表區塊,每個項目前面加入 checkbox,如打勾則開項目顯示刪除線樣式。(text-decoration: line-through;)
  3. 按下新增按鈕後,將目前文字輸入框內容新增至下方列表

永久性儲存

前端網頁重新整理後就會恢復到初始狀態,所以中間的數據異動無法被保存與還原

這時需要倚賴本地端儲存方法進行數據保存與還原

本地端儲存方式

特性cookielocalStoragesessionStorage
生命週期可設定失效時間,預設為關閉瀏覽器失效永久保存,手動清除關閉頁面後清除
可用容量4KB 左右5MB 左右5MB 左右
與 Server 溝通每次溝通都會附加,保存過多數據會帶來效能問題不參與溝通不參與溝通

瀏覽器與主機溝通架構

flowchart LR
    A[cookie id]
    B[service side seesion]
    C[session content]

    A-->| Send | B
    B-->| Read | C
    C-->| Response | A

為何網站登入後,關閉瀏覽器再打開,一樣在登入狀態?

查看資料

Chrome Devtool -> Application -> Storage

localStorage

寫入

localStorage.setItem(key, content)

key 數據識別值,如果寫入一樣的識別值,複寫

content 數據內容,只接受字串

localStorage.setItem('name', 'david');

let students = [];
students.push({name: 'David', age: 18});
students.push({name: 'John', age: 20});

localStorage.setItem('students', students);

查看 students 數據內容為何?

序列化

由於 localStorage 只接受字串類型的數據內容,所以 Array 或是 Object 這種非字串類型的數據,需要轉為字串,這個過程稱為序列化 (serialize)

JSON.stringify(object)

let students = [];
students.push({name: 'David', age: 18});
students.push({name: 'John', age: 20});

let studentsStr = JSON.stringify(students);

localStorage.setItem('students', studentsStr);

此時觀察數據內容發現已經改為字串方式存取

關於 JSON

JavaScript Object Notation

一種資料交換格式,對比 XML 資料交換格式而言,來的輕巧與方便存取

讀取

localStorage.getItem(key)

let students = localStorage.getItem('students');

console.log(students);

查看 students 數據內容為何?

反序列化

將被序列化的 ArrayObject 返回原始類型數據

JSON.parse(serialize)

let studentsStr = localStorage.getItem('students');

students = JSON.parse(studentsStr);

console.log(students);

刪除

localStorage.removeItem(key)

localStorage.removeItem('students');

練習

本地儲存版 Todo List

將上一個章節練習的 Todo List 擴充以下功能

  1. 進入時要求使用者輸入帳號作為儲存 key
  2. 新增儲存功能
  3. 新增還原功能
  4. 自動儲存功能

API & AJAX

API

Application Programming Interface

目的

  • 降低耦合性 (單一原則)
  • 跨系統串接 (不同作業系統,不同程式語言)
  • 簡化使用方式 (封裝)

URL

組成

https://www.google.com/?s=hello

https 通訊協定

www.google.com 網址

?s=hello 參數

請求方式

  • GET
  • POST
  • PUT (有定義未實作)
  • DELETE (有定義未實作)

對應 CRUD

類型CRUD
全名CreateReadUpdateDelete
HTTPPOST/PUTGETPOST/PUTDELETE
SQLINSERTSELECTUPDATEDELETE

Web API

每次都請求都是一個 API 的發起與完成

flowchart LR
    A[Browser]
    B[Google]
    C[Backend]

    A-->| Request Method With URL | B
    B-->| Run Process | C
    C-->| Return | B
    B-->| Response | A

Browser 這個視覺化的角色拿掉,發起 Request 就是大眾所謂的 API

通常 Response 會使用 JSON 格式回傳

初體驗

觀察兩個網址回應差別

https://book.niceinfos.com/frontend/api/

https://book.niceinfos.com/frontend/api/?action=demo

Ajax

Asnychronous JavaScript And XML

同步與非同步

flowchart LR
    A[Browser]
    B[Google]
    C[Backend]

    A-->| Request Method With URL | B
    B-->| Run Process | C
    C-->| Return after 10s | B
    B-->| Response | A

在等待的時候,還可以點畫面其他的互動嗎?

非同步請求

html

<button id="btn">AJAX</button>
<div id="response"></div>

js

let btn = document.querySelector('#btn')
let response = document.querySelector('#response')

btn.addEventListener('click', doAjax)

function doAjax() {
    let request = new XMLHttpRequest()
    request.addEventListener('load', () => {
        response.innerHTML = request.responseText
    })

    request.open('GET', 'https://book.niceinfos.com/frontend/api/?action=demo')
    request.send()
}

demo

透過此方法,就算請求需要時間,也不會影響主畫面的操作 (背景作業)

模擬主機端睡眠

  1. 使用上面的範例,將 url 改為 https://book.niceinfos.com/frontend/api/?action=sleep
  2. 觀察 開發者工具 > 網路

demo

fetch

原本的 XMLHttpRequest 使用上不夠直覺,ES6 之後推出包裝過後的 fetch 方便使用

js

function doAjax() {
    let api = 'https://book.niceinfos.com/frontend/api/?action=demo';

    fetch(api)
        .then((response) => {
            return response.text()
            // return response.json()
        })
        .then((data) => {
            console.log(data)
        })
}

response.text() 將回傳資料解析為純文字

response.json() 將回傳資料解析為 json

demo

POST 發送

js

function doAjax() {
    let api = 'https://book.niceinfos.com/frontend/api/'

    let params = {
        action: 'demo',
        data: { a: 1, b: 2 },
    }

    let options = {
        method: 'POST',
        body: JSON.stringify(params),
    }

    fetch(api, options)
        .then((response) => {
            return response.text()
            // return response.json()
        })
        .then((data) => {
            response.innerHTML = data
            console.log(data)
        })
}

params 傳送資料

options 設定選項 - method 請求方式,預設 GET - body 請求主體,GET 不可以使用此屬性,透過序列化將資料打為 json 格式發送

demo

關於 then

fetch 本身的包裝是希望讓開發者透過類同步的開發方式寫程式,所以使用 then 來等待非同步

底層使用的是 Promise 物件來處理

檔案上傳

html

<input type="file" id="file" />
<button id="btn">Upload</button>
<div id="response"></div>

js

let btn = document.querySelector('#btn')
let response = document.querySelector('#response')

btn.addEventListener('click', doAjax)

function doAjax() {
    let domFile = document.querySelector('#file')
    let file = domFile.files[0]
    if (!file) {
        return
    }

    let api = 'https://book.niceinfos.com/frontend/api/'

    let form = new FormData()
    form.append('action', 'upload')
    form.append('file', file)

    let options = {
        method: 'POST',
        body: form,
    }

    fetch(api, options)
        .then((response) => {
            return response.text()
            // return response.json()
        })
        .then((data) => {
            response.innerHTML = data
            console.log(data)
        })
}

使用 FormData 物件傳送檔案,因為檔案是二進制格式,不可序列化為文字

demo

練習

雲端儲存版 Todo List

將先前開發好的本地儲存版 Todo List 近一步改良

  1. 新增雲端儲存功能
  2. 新增雲端載入功能

tip

  1. 儲存功能對應請求方式為 POST,請求 APIhttps://book.niceinfos.com/frontend/api/
  2. 載入功能對應請求方式為 GET 請求 APIhttps://book.niceinfos.com/frontend/api/?action=todo&uid=your_uid

jQuery

官方網站

引用

<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js"></script>

介紹

使用 $ 代表 jQuery,提供簡單方便的使用方法

$ 也是一個變數,在某些複雜的專案下,可能被其他功能先使用,導致 $ 已非 jQuery

抓取 DOM

html

<input type="text" id="account">

<div class="item">Item 1</div>
<div class="item">Item 2</div>
<div class="item">Item 3</div>
<div class="item">Item 4</div>
<div class="item">Item 5</div>

js

let account = $('#account');
let items = $('.item');

console.log(account.val());
console.log(items.html());

html() 等同 innerHTML

val() 等同 value

無論是一個或是多個,都是統一使用 $(selector) 來抓取建立 jQuery 物件並自動整理成 Array

DOM each

將抓取到的 dom 做迴圈(iterate)一個一個讀取

items.each(function(index, item) {})

ArrayforEach(function(item, index){}) 不同,參數剛好是顛倒的

js

items.each((index, item) => {
    console.log(item, index);
    console.log($(item).html());
})

item 原生 dom

$(item) 將原生 dom 封裝為 jQuery 物件

取得特定 DOM

items.eq(0).html() 取得第一個 jQuery 物件

取得原生 DOM

items[0] 取得第一個原生 dom

事件綁定

items.on('click', function(e) {})

on 等同 addEventListener

自動將 items 內的 jQuery 物件都進行綁定

class 控制

附加

account.addClass('active')

移除

account.removeClass('active')

切換

account.toggleClass('active')

檢查存在

account.hasClass('active')

屬性控制

html

<input type="checkbox" id="agree">

js

let agree = $('#agree')

agree.prop('checked', true)
console.log(agree.prop('checked'))
agree.prop('checked', false)

沒有第二個參數表示 getter 取值

套件應用

WOWJS

結合 animate.css 依據捲軸位置呈現動畫效果

wowjs

animate.css

demo

swiperjs

swiperjs

demo

部署需求條件

網域 Domain

非買斷機制,以『年』為單位進行續約

一個網域可以設定 N 個網址(子網域)

註冊商

網域名稱服務 DNS

設定網址對應 IP 位置

服務商

雲端主機

提供網站服務與儲存空間

技術門檻彈性費用
自建主機最高最高
虛擬空間 hosting最低最低最低
虛擬主機 VPS
實體主機託管

虛擬空間

虛擬主機

服務架構

專案開發

部署工具

FTP

網頁檔案總管

一般租用 hosting,都會提供類似的工具給你上傳檔案

指令

  • git
  • 其他指令工具

Firebase Hosting

提供有限免費的雲端空間

安裝工具

  1. nodejs
  2. firebase-tools
npm install -g firebase-tools

部署 firebase hosting

firebase

  1. 建立專案(不要打中文跟特殊符號)
  2. 啟用 hosting
  3. 登入 firebase
    1. 終端機輸入 firebase login
    2. 網頁進行驗證
  4. 進入專案資料夾
    1. 終端機輸入 cd 專案資料夾位置
  5. 初始化專案
    1. 終端機輸入 firebase init
    2. 選擇 Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys
    3. Use an existing project
    4. 選擇專案
  6. 部署
    1. 終端機輸入 firebase deploy

部署的檔案會放在 public 資料夾內

部署的檔案無法下載,所以要留好原始檔案,建議搭配 git 管理

網址對應

  1. 新增自訂網域
  2. 輸入網址
  3. 前往 DNS 建立對應 TXTA 紀錄
  4. 等待生效

git

人生不能重來,git 可以!

demo

安裝

git

設定使用者

git config --global user.name "My Name"

設定信箱

git config --global user.email "myemail@example.com"

初始化

git init

提交第一個版本

先建立專案結構,參考 建議專案結構

建立完畢後,執行

git add .

將目前檔案加入 stage change 等待提交

執行提交

git commit -m "my first commit."

查看紀錄

git log

查看紀錄明細

git log --stat

圖形化顯示

git log --stat --pretty=short --graph

查看目前變化

git status

放棄目前變化

單一檔案

git restore <file>

全部檔案

git restore .

目前變化放入等待提交 stage change

單一檔案

git add <file>

全部檔案

git add .

提交

git commit -m "add reset style."

跳到特定版本

保留檔案跳版本,未納入版本的檔案會變成未提交狀態

git reset <commit hash>

不保留檔案跳版本,未納入版本的檔案會直接消失

連同提交紀錄也會消失,但是如果知道 commit hash 還是可以恢復

git reset --hard <commit hash>

提交線上資源庫

github

  1. 註冊
  2. 建立資源庫 (Respositories)

增加線上資源庫位置

git branch -M main
git remote add origin https://github.com/<account>/<repositories>.git

提交

git push -u origin main

過程會要求登入

建立 readme.md

說明專案使用,使用 markdown 格式

readme.md

# Frontend Project

This is my first frontend project.
git add .
git commit -m "add readme."
git push -u origin main

複製資源庫 clone

從現有資源庫複製一份出來,如無指定資料夾名稱,會自動產生 repositories 名稱的資料夾

不要自己建立資料夾!

git clone https://github.com/<account>/<repositories>.git <custom dir>

同步資源庫 pull

多人開發時,可能資源庫與你現在的有差異,這時候可以將線上資源庫同步下來

git pull origin main

Firebase Realtime

啟用

Realtime Database -> 建立資料庫

規則

Realtime Database -> 規則

預設都是 false,先改為 true 方便測試

實際上線需依據狀況設定為 false,避免資料安全性問題

建立應用程式

  1. 專案總覽 -> 專案設定 -> 網頁應用程式 </> 符號
  2. 輸入程式暱稱(英文數字) -> 不要 firebase 託管
  3. 複製 const firebaeConfig 設定值

初始化

引用 SDK

<script src="https://cdnjs.cloudflare.com/ajax/libs/firebase/8.10.0/firebase.js"
integrity="sha512-7NBxPO/qRUrKc+Wi9GbYz0YEzMpi2UMP3mtLcswnvzI0vUFP5Jb7HNVd1V8NmEhXpSe3ZLsoGEhwRHpAZDpYPQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

建立 firebase.js -> 將剛剛複製的 firebaseConfig 貼上

const firebaseConfig = {};

const model = firebase.initializeApp(firebaseConfig, firebaseConfig.appId);

async function write(value, path) {
    try {
        await model.database().ref(path).set(value)
        return true
    } catch (err) {
        return false
    }
}

async function read(path) {
    let snapshot = await model.database().ref(path).get()
    return snapshot.val()
}

function listen(path, callback) {
    model
        .database()
        .ref(path)
        .on('value', (snapshot) => {
            if (typeof callback === 'function') {
                callback(snapshot.val())
            }
        })
}

;(async () => {
    let result = await write('BBB', 'test')
    console.log(result)

    let response = await read('test')
    console.log(response)

    listen('test', (value) => {
        console.log(value)
    })
})()

demo