Vite Pwa 漸進式網路應用程式 安裝與設定

Vite Pwa漸進式網路應用程式 安裝與設定

Vite 的Pwa功能是在web情況下,開發web就能製作成App的效果,不需要上架app store / google play 也不需要使用安裝 exe檔案,只需要連結就能增加的手機畫面,
Vite PWA
Vite 和生態系統的 PWA 整合零配置和與框架無關的 Vite PWA 插件
做好了網站以後,執行以下指令:

1
npm i -D vite-plugin-pwa

vite.config.js增加

  • 引入vite-plugin-pwa解構VitePWA
  • plugins內新增VitePWA({ ... })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { VitePWA } from "vite-plugin-pwa"
export default defineConfig({
plugins: [
VitePWA({ registerType: 'autoUpdate' })
<!-- VitePWA({
srcDir: "/",
filename: "vite_pwa.js",
registerType: 'autoUpdate',
devOptions: {
enabled: true,
type: 'module',
},
strategies: "injectManifest",
injectRegister: false,
manifest: false,
injectManifest: {
injectionPoint: undefined,
},
}), -->
],
server: {
hmr: {
overlay: false
}
},
})
產生 PWA 資產產生和圖像

PWA 資產產生器

安裝PWA 資產產生和圖像

1
npm install --global vue-pwa-asset-generator

執行指令
剛剛製作的icon路徑產生到public/img/icons檔案夾內

1
2
3
//剛剛製作的icon路徑 =>(產生) => icons
npx vue-pwa-asset-generator -a ./public/logo.png -o ./public/img/icons

終端機回應:產生資料到/public/

產生的檔案內會有一個manifest.json檔

index.html載入manifest.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lara Huang 出勤系統</title>
<link rel="apple-touch-icon" sizes="180x180" href="/img/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png">
<!--manifest.json 注意路徑--->
<link rel="manifest" href="/img/icons/manifest.json">
<link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

產生Bug
主要原因是位址路徑錯誤

manifest.json
注意:icons/src路徑必須正確

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
{
"name": "Lara Huang",
"short_name": "Lara Huang",
"description":"描述",
"start_url":"/",
"icons": [
{
"src": "./android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "./android-chrome-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./android-chrome-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./apple-touch-icon-60x60.png",
"sizes": "60x60",
"type": "image/png"
},
{
"src": "./apple-touch-icon-76x76.png",
"sizes": "76x76",
"type": "image/png"
},
{
"src": "./apple-touch-icon-120x120.png",
"sizes": "120x120",
"type": "image/png"
},
{
"src": "./apple-touch-icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "./apple-touch-icon-180x180.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "./apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "./favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "./favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "./msapplication-icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "./mstile-150x150.png",
"sizes": "150x150",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

vite_pwa.js 這個檔案夾 不一定要使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self.addEventListener("notificationclick", function (event) {
console.log("開啟通知");
});

self.addEventListener("notificationclick", function (event) {
const channel = new BroadcastChannel("sw-mensajes");
if (event.action == "aceptar") {
channel.postMessage({ title: "接受" });
}

if (event.action == "rechazar") {
channel.postMessage({ title: "衰退" });
}
});

打包

1
npm run build

重新運行測試

1
npm run dev

畫面出現標示安裝

前端頁面部署方式的全面解析與比較

整理部署方式:

  1. FTP上傳: 這是最傳統的方式,將你的前端檔案(HTML,CSS,JavaScript等)上傳到你的web伺服器。大多數的web主機提供FTP訪問,這也是小型專案的快速部署方式。
  2. 使用Git: 如果你的伺服器支援Git,你可以設定一個Git倉庫,並使用**git push**指令部署你的前端頁面。這種方式方便版本控制,也支援多人協作。
  3. 使用SSH:可以透過SSH將你的檔案推送到你的伺服器,這個過程可以透過腳本自動化。
  4. 使用雲端服務: 像AWS S3,Google Cloud Storage,Azure Storage等服務可以託管靜態網站,通常它們提供了很好的效能和安全性。
  5. 使用專門的前端部署服務: 服務如Netlify,Vercel等,這些平台通常提供一站式服務,包括版本控制,持續集成,HTTPS和CDN等。
  6. 使用CDN服務: 這些服務可以將你的網站部署到全球的伺服器上,加速存取速度。例如,Cloudflare,Akamai等。
  7. 容器化部署: 使用如Docker等工具,將前端套用容器化,然後在Kubernetes等容器編排平台進行部署。

Nginx

1.將你的前端程式碼上傳到伺服器上的某個目錄,例如 /var/www/mywebsite。
2.建立Nginx設定檔:Nginx的設定檔通常位於 /etc/nginx/ 目錄下,特定路徑可能會因伺服器的不同而略有不同。
在該目錄下,你通常會找到一個叫做 sites-available 的目錄
在這個目錄下,你可以為每個網站建立一個設定檔。例如,你可以建立一個新的設定文件,命名為 mywebsite:sudo nano /etc/nginx/sites-available/mywebsite

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name your_domain_or_IP;

location / {
root /var/www/mywebsite;
index index.html;
try_files $uri $uri/ =404;
}
}

應該替換為你的程式碼所在的目錄:/var/www/mywebsite
應該替換為你的網域或IP位址:your_domain_or_IP
listen:此配置意味著Nginx將監聽80端口,並為所有指向你的網域或IP位址的請求提供服務
Nginx將傳回404狀態碼:如果請求的檔案不存在,Nginx將傳回404狀態碼。

4.啟用網站
建立並編輯完設定檔後,你需要建立一個到 sites-enabled 目錄的符號連結來啟用你的網站:

1
sudo ln -s /etc/nginx/sites-available/mywebsite /etc/nginx/sites-enabled/

5.檢查設定檔
重啟Nginx之前,檢查你的設定檔有沒有語法錯誤:

1
sudo nginx -t

如果沒有錯誤,你會看到類似這樣的輸出:

1
nginx: configuration file /etc/nginx/nginx.conf test is successful

6.重啟Nginx
需要重啟Nginx來讓你的設定生效:

1
sudo service nginx restart

前端頁面應該可以透過你的網域名稱或IP位址存取了。
注意,如果你的伺服器有防火牆,你可能需要設定防火牆來允許HTTP和HTTPS流量。

更完整的 Nginx 配置

首先,你需要為你的網域取得一個SSL證書,你可以使用Let’s Encrypt提供的免費證書。

設定檔可能如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
server {
listen 80;
server_name your_domain_or_IP;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl;

server_name your_domain_or_IP;

ssl_certificate /etc/letsencrypt/live/your_domain_or_IP/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain_or_IP/privkey.pem;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";

location / {
root /var/www/mywebsite;
index index.html;
try_files $uri $uri/ =404;
}

location ~* \\.(jpg|jpeg|png|gif|ico|css|js|pdf)$ {
expires 30d;
}

gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
gzip_disable "MSIE [1-6]\\.";
}
  • 第一個 server 區塊監聽80個端口,也就是HTTP的預設端口,然後把所有的請求重定向到HTTPS。
  • 第二個 server 區塊監聽443端口,也就是HTTPS的預設端口。
  • ssl_certificate 和 ssl_certificate_key 指向你的SSL憑證和私鑰的位置。
  • ssl_protocols 和 ssl_ciphers 定義了你伺服器支援的SSL協定和加密套件。
  • location / 區塊定義了你的網站的根目錄和預設文件,以及如何處理不存在的路徑。
  • location ~* \\.(jpg|jpeg|png|gif|ico|css|js|pdf)$ 區塊定義了對於靜態檔案如圖片,CSS文件,JavaScript檔案等,客戶端應該快取30天。
  • gzip 相關的行啟用了gzip壓縮,並定義了哪些類型的檔案應該被壓縮。

這只是一個基本的配置範例,你可以根據你的需求進行修改。
有許多其他的選項可以配置,例如負載平衡,
反向代理,HTTP/2支援等。

負載平衡

使用 Nginx 來做負載平衡的實作非常直接。以下是一個簡單的範例來說明如何設定 Nginx 來實現對多個伺服器的負載平衡。

假設你有兩個或更多的應用程式伺服器實例,它們分別託管在不同的 IP 位址或主機名稱下**。你可以在 Nginx 設定檔中設定一個 upstream 模組,然後在 server 模組中將請求反向代理到這個 upstream。 **

首先,你需要在你的 Nginx 設定檔(一般位於 /etc/nginx/nginx.conf 或 /etc/nginx/conf.d/ 目錄下的某個檔案)中定義一個 upstream。例如:

1
2
3
4
5
6
7
http {
upstream myapp {
server app1.example.com;
server app2.example.com;
}
...
}

在這個例子中,myapp 是你定義的 upstream 的名字,app1.example.com 和 app2.example.com 是你的應用程式伺服器的位址。

然後,在 server 模組中,你可以將來自客戶端的請求代理到這個 upstream:

1
2
3
4
5
6
7
server {
listen 80;

location / {
proxy_pass <http://myapp>;
}
}

在這個範例中,所有來自客戶端的 HTTP 請求(即 :80 連接埠的請求)都會被 Nginx 轉送到定義的 upstream(即 myapp)。 Nginx 預設使用輪詢(round-robin)演算法將請求指派給 upstream 中的伺服器,你也可以設定其他的負載平衡演算法,如最少連接(least_conn)或 IP 雜湊(ip_hash)。

這樣,Nginx 就會把請求平衡地轉送到你的各個應用程式伺服器實例上,實現負載平衡。注意這個配置對於前端靜態頁面來說可能不太必要,因為靜態頁面通常不會對伺服器產生太大負載,但對於動態應用來說是非常有用的。

Docker

使用 Docker 來部署前端應用程式是一種很好的方式,因為它可以將你的應用程式及其依賴項打包到一個獨立、可移植的容器中,可以在任何安裝了 Docker 的環境中運行。

以下是使用 Docker 部署一個基本的靜態前端頁面的步驟:

1.建立 Dockerfile

在你的專案根目錄中,建立一個名為”Dockerfile”的檔案。這個檔案是用來定義你的 Docker 映像的,可以看作是一個自動化的腳本。以下是一個簡單的 Dockerfile 範例,其中使用了官方的 Nginx 映像來部署靜態頁面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用官方的 Nginx 镜像
FROM nginx:latest

# 删除 Nginx 默认的静态资源
RUN rm -rf /usr/share/nginx/html/*

# 将你的静态资源复制到 Nginx 的静态资源目录下
COPY ./dist /usr/share/nginx/html

# 暴露 80 端口
EXPOSE 80

# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]

2.建置 Docker 映像

在專案根目錄下,你可以使用 docker build 指令來根據 Dockerfile 建立 Docker 映像:

1
docker build -t my-frontend-app .

其中,-t my-frontend-app 參數用於為您的 Docker 映像命名,. 指定了 Dockerfile 所在的位置(在這裡,它在目前目錄下)。

3:运行 Docker 容器

你现在可以使用 docker run 命令来运行你刚刚构建的 Docker 镜像:

1
docker run -p 80:80 -d my-frontend-app

參數指定了將容器的 80 端口映射到宿主機的 80 端口,d 參數指定了以後台模式運行容器。

現在你應該可以在你的瀏覽器中透過造訪 http://localhost 來查看你的前端頁面了。

如果你的前端應用更複雜,或者需要與後端服務進行交互,你可能需要進行更複雜的配置,例如使用 Docker Compose 來管理多個服務,或使用環境變數來配置你的應用程式。

負載平衡
如果你的前端應用使用 Docker 部署,有多種方式可以實現負載平衡。以下是一些常見的方法:

  1. 使用 Docker Swarm:

Docker Swarm 是 Docker 的原生叢集管理和編排工具。在 Swarm 叢集中,你可以建立一個服務,然後指定服務的副本數量。 Swarm 會自動在叢集的節點間分配這些副本,實現負載平衡。

以下是一個簡單的 Docker Swarm 服務建立指令:

1
docker service create --name my-frontend-app --publish 80:80 --replicas 3 my-frontend-app

在這個命令中,–name my-frontend-app 指定了服務的名稱,–publish 80:80 指定了將容器的80 端口映射到宿主機的80 端口,–replicas 3 指定了創建3 個副本,my-frontend-app 是你的Docker 映像的名稱。

2.使用 Kubernetes:
Kubernetes 是一個更強大的容器編排平台,它也支援負載平衡。在 Kubernetes 中,你可以建立一個 Deployment 和一個 Service,Kubernetes 會自動為你的應用提供負載平衡。

以下是一個簡單的 Kubernetes Deployment 和 Service 的定義:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-frontend-app
spec:
replicas: 3
selector:
matchLabels:
app: my-frontend-app
template:
metadata:
labels:
app: my-frontend-app
spec:
containers:
- name: my-frontend-app
image: my-frontend-app
ports:
- containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
name: my-frontend-app
spec:
type: LoadBalancer
ports:
- port: 80
selector:
app: my-frontend-app

在這個定義中,Deployment 建立了 3 個前端應用的副本,Service 為這些副本提供了一個負載平衡器。

  1. 使用雲端服務提供者的負載平衡器:

如果你的應用程式部署在雲端環境(如 AWS, Google Cloud, Azure 等),你可以使用雲端服務供應商的負載平衡服務。這些負載平衡服務通常提供了更強大的功能,例如自動擴縮容,健康檢查,SSL 終結等。

以上都是實現 Docker 負載平衡的一些方式,你可以根據你的特定需求和環境選擇最合適的方式。

PM2

PM2 主要用於管理和維護 Node.js 應用,但你也可以使用 PM2 來運行一個靜態伺服器,用來部署你的前端頁面。
透過使用像 http-server 這樣的簡單的靜態檔案伺服器來完成
1.安装 http-server

1
npm install http-server --save

2.建立一個 PM2 的設定文件
你需要建立一個 PM2 的設定文件,例如 ecosystem.config.js。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
apps: [
{
name: 'frontend',
script: 'node_modules/http-server/bin/http-server',
args: './dist -p 8080', // 這裡的 ./dist 是你的前端頁面的路徑,-p 8080 是你要監聽的端口
instances: 'max',
exec_mode: 'cluster',
watch: true, // 如果你希望在檔案改變時自動重新啟動伺服器,可以設定 watch 為 true
env: {
NODE_ENV: 'production',
},
},
],
};

3.使用 PM2 啟動你的前端頁面
利用 PM2 提供的特性,例如應用的自動重啟和負載平衡。

1
pm2 start ecosystem.config.js

http-server 是一個非常簡單的靜態檔案伺服器,
如果你需要更複雜的功能(例如SSL、反向代理等),你可能需要考慮使用更強大的伺服器軟體,例如Nginx 或Express .js。

技術 優點 缺點 出現時間 適合場景 不適合場景
Nginx 輕量級,高效能,可作為反向代理和負載平衡器 需要手動配置和管理 2004年 任何需要靜態頁面服務的項目 需要動態伺服器端渲染的項目
PM2 Serve 可以使用PM2來管理靜態服務程序 不包含反向代理或負載平衡器 PM2:2014年 需簡單的靜態檔案服務 需複雜的HTTP請求處理或負載平衡的項目
Docker 容器化,跨平台 需要手動管理容器和映像 2013年 任何需要跨平台部署或以統一方式部署多種服務的項目
小規模或簡單的項目可能,
引入不必要的複雜性
GitHub Pages 免費,支援自訂域名,整合GitHub 功能有限,僅支援靜態內容 2008 年 簡單的個人、專案或組織頁面 複雜的、需要後端的應用
Netlify/Vercel 提供自動部署,函數即服務等 免費版有一些限制 Netlify:2014年,
Vercel:2015年
靜態網站和Jamstack應用 大規模、複雜的後端應用

Node express Vercel部署
參考資料

Vue Fetch

Fetch API基本

1
2
3
4
5
6
7
8
fetch(url, options)
.then(response => response.json())
.then(data => {
// 处理返回的数据
})
.catch(error => {
// 处理请求错误
});

封裝請求

在專案src目錄下新增request.js的文件並且 export

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
export default {
// GET請求
get(url, params = {}) {
if (params) {
//
url += '?' + Object.keys(params).map(key => `${key}=${params[key]}`).join('&');
}
return fetch(url)
.then(response => response.json())
.catch(error => {
console.error('Request failed:', error);
throw new Error(error);
});
},

// POST請求
post(url, data) {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.catch(error => {
console.error('Request failed:', error);
throw new Error(error);
});
},

// PUT請求
put(url, data) {
return fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.catch(error => {
console.error('Request failed:', error);
throw new Error(error);
});
},

// DELETE請求
delete(url) {
return fetch(url, {
method: 'DELETE',
})
.then(response => response.json())
.catch(error => {
console.error('Request failed:', error);
throw new Error(error);
});
},
};

引入頁面使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<template>
<div>
<!-- 展示数据 -->
</div>
</template>

<script>
import request from '@/api/request.js';

export default {
name: 'ExampleComponent',
data() {
return {
data: [],
};
},
mounted() {
this.getData();
},
methods: {
getData() {
//
request.get('/api/data')
.then(response => {
this.data = response;
})
.catch(error => {
console.error('error);
});
},
postData() {
const data = {
// 提交的数据
};
request.post('/api/data', data)
.then(response => {
// 处理返回结果
})
.catch(error => {
console.error(error);
});
},

},
};
</script>

單元測試 describe & it

Unit Test官網
Unit Test官網
unit test VUE 3 Composition API methods?

元件測試的目標

  • data
  • props
  • slot
  • provide
  • directive
  • Event(瀏覽器中的互動行為)
  • API response(模擬回應)
  • 元件測試:針對 Vue 元件所進行的測試
  • 單元測試:針對元件引入函式、類別等 utils、helper 與 composable 的測試。
  • 工具本身:Pinia 測試、Vue Router 測試。
  • 容器(Wrapper)

    方法 mount
    import HelloWorld from ‘../HelloWorld.vue’
    const wrapper = mount(HelloWorld, { props: { msg: ‘Hello Vitest’ } })
    expect(1 + 1).toBe(2)
    expect(wrapper.text()).toContain(‘Hello Vitest’)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import { describe, it, expect } from 'vitest'
    import { mount } from '@vue/test-utils'

    describe('HelloWorld', () =>
    {
    //1+1 應該是2
    it('1 + 1 should be 2', () => {
    expect(1 + 1).toBe(2)
    })
    //正確渲染
    it('正確渲染', () => {
    const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
    expect(wrapper.text()).toContain('Hello Vitest')
    })
    //在此測試檔案中使用 jsdom
    it('在此測試檔案中使用 jsdom', () => {
    const element = document.createElement('div')
    element.innerHTML = '<p>Hello, HTML!</p>'
    expect(element.innerHTML).toBe('<p>Hello, HTML!</p>')
    })
    })

    Nav

    檢查元件的可見

    檢查元件的可見

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    import { describe, it, expect } from 'vitest'
    import { mount } from '@vue/test-utils'
    import Nav from '../Nav.vue'

    describe('Nav.vue gets the following array from the parent element frontLayout.vue', () =>
    {
    const navLists = [
    { id_set: '001', title: '出勤打卡', href: '/' },
    { id_set: '002', title: '出勤設定', href: '/set_check' },
    { id_set: '003', title: '出勤列表', href: '/work_check_lists' },
    { id_set:'004', title:'出勤行事曆', href:'/work_calendar'}
    ]
    const pageNameActive = '001';
    const wrapper = mount(Nav, {
    props: {
    navLists: navLists,
    pageNameActive:pageNameActive
    }
    })

    it('Is navLists data correctly rendering information? ', () => {
    const items = wrapper.findAll('li');
    expect(items.length).toBe(4);
    expect(items[0].text()).toBe('出勤打卡');
    expect(items[1].text()).toBe('出勤設定');
    expect(items[2].text()).toBe('出勤列表');
    expect(items[3].text()).toBe('出勤行事曆');

    });
    it('pageNameActive is rendered when the initial value is passed', () => {
    expect(pageNameActive).toMatch(pageNameActive);
    })

    it('Emits the event of the current data when clicked', () =>
    {
    const items = wrapper.findAll('li');
    const button = wrapper.find('li');
    button.trigger('click');
    expect(pageNameActive).toMatch(items[0].id_set);
    expect(pageNameActive).toMatch(items[1].id_set)
    expect(pageNameActive).toMatch(items[2].id_set)
    expect(pageNameActive).toMatch(items[3].id_set)
    expect(button.find('li.isActive'));
    })
    //emitted ,toHaveProperty 點擊時取出當筆數據的事件
    it('Event to retrieve the current data when clicked', async () =>
    {
    await wrapper.get('[data-test="button"]').trigger('click')
    expect(wrapper.emitted()).toHaveProperty('sendMenuClickActive')
    })
    })

    form

    單元測試 describe & it

    describe & it 輔助 API

    describe

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import { describe, it} from 'vitest'
    import { mount } from "@vue/test-utils"

    describe('登入元件', () => {
    // Happy Path
    it('輸入正確帳號密碼,應登入到OO頁面', () => {
    // ...
    })
    // Sad Path
    it('只輸入帳號,應該顯示請輸入密碼', () => {
    // ...
    })
    it('只輸入密碼,應該顯示請輸入帳號', () => {
    // ...
    })
    it('輸入錯誤帳號密碼,應該顯示登入資訊錯誤', () => {
    // ...
    })
    })

    允許巢狀的方式來建構測試

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    describe('父層情境', () => {
    describe('基於父層情境的情境一', () => {
    it('測試案例', () => {
    // ...
    })
    })
    describe('基於父層情境的情境二', () => {
    it('測試案例', () => {
    // ...
    })
    it('出現提示寄件者姓名與電話將直接註冊成會員', () => {
    // ...
    })
    })
    })

    describe & it 輔助 API

    • .only:測試情境、測試案例皆可使用
    • .skip:測試情境、測試案例皆可使用
    • .todo:測試情境、測試案例皆可使用
    • .fails:測試案例才能使用
    only
    若在測試情境用了此指令,則在同個測試檔案中只會執行帶有 .only 的測試情境,而其餘測試情境底下所有的測試案例將會被跳過(skipped):
    1
    2
    3
    describe.only('測試情境 1', () => { /* */ })
    describe('測試情境 2', () => { /* */ }) // skipped
    describe('測試情境 3', () => { /* */ }) // skipped
    若在測試案例中使用,則除了帶有 .only 之外的測試案例都將會被跳過:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    describe('測試情境 1', () => {
    it.only('測試案例', () => { /* */ })
    it('測試案例', () => { /* */ }) // skipped
    })

    describe('測試情境 2', () => {
    it.only('測試案例', () => { /* */ })
    it('測試案例', () => { /* */ }) // skipped
    })

    describe('測試情境 3', () => { /* */ }) // skipped

    skip

    測試情境或測試案例被標注時,將自動跳過該範疇內的測試案例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    describe.skip('測試情境 1', () => {
    it('測試案例', () => { /* */ }) skipped
    it('測試案例', () => { /* */ }) // skipped
    })

    describe('測試情境 2', () => {
    it('測試案例', () => { /* */ })
    it.skip('測試案例', () => { /* */ }) // skipped
    })

    todo

    測試情境或測試案例被標注時,同樣將自動跳過該範疇內的測試案例,但 todo 含義比較接近待加入測試的區塊,並且將來若產出報告時也會特別整理出還有哪些地方需要補上測試。

    fails

    最後一個介紹的是測試案例才能使用的輔助 API,還記得列測試案例時的 sad path 嗎?當測試案例應該要失敗的時候就可以透過 fails 顯性標註他們:
    1
    2
    3
    4
    it.fails(`'1' + '1' should not to be '11'`, () => {
    const add = (x, y) => Number(x) + Number(y)
    expect(add('1', '1')).toBe('11')
    })
    當然你也可以單純藉由斷言中的 .not 達到同樣的效果:
    1
    2
    3
    4
    it(`'1' + '1' should not to be '11'`, () => {
    const add = (x, y) => Number(x) + Number(y)
    expect(add('1', '1')).not.toBe('11')
    })
    準備(Setup)與清理(Teardown)
    • beforeEach:在每個測試案例執行前呼叫一次
    • beforeAll:在所有測試案例執行前呼叫一次
    • afterEach:在每個測試案例執行後呼叫一次
    • afterAll:在所有測試案例執行後呼叫一次
    1
    2
    3
    4
    beforeEach(() => {
    // 針對測試案例重新初始化
    initTestEnv()
    })
    Setup & Teardown API 的範疇
    Setup & Teardown API 「所有」的定義是根據當下的範疇(context)來決定,除了測試檔案本身之外,使用 describe 來定義測試情境也會形成一個 context,因此假如測試情境有巢狀的情況如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    const history = []
    describe('父層情境', () => {
    beforeAll(() => {
    history.push('beforeAll - 父層情境')
    })
    beforeEach(() => {
    history.push('beforeEach - 父層情境')
    })
    afterAll(() => {
    history.push('afterAll - 父層情境')
    })
    afterEach(() => {
    history.push('afterEach - 父層情境')
    })
    describe('子層情境 A', () => {
    beforeAll(() => {
    history.push('beforeAll - 子層情境 A')
    })
    beforeEach(() => {
    history.push('beforeEach - 子層情境 A')
    })
    afterAll(() => {
    history.push('afterAll - 子層情境 A')
    })
    afterEach(() => {
    history.push('afterEach - 子層情境 A')
    })
    it('案例 1', () => {
    history.push('子層情境 A 案例 1')
    })
    it('案例 2', () => {
    history.push('子層情境 A 案例 2')
    })
    })
    describe('子層情境 B', () => {
    beforeAll(() => {
    history.push('beforeAll - 子層情境 B')
    })
    beforeEach(() => {
    history.push('beforeEach - 子層情境 B')
    })
    afterAll(() => {
    history.push('afterAll - 子層情境 B')
    })
    afterEach(() => {
    history.push('afterEach - 子層情境 B')
    })
    it('案例 1', () => {
    history.push('子層情境 B 案例 1')
    })
    it('案例 2', () => {
    history.push('子層情境 B 案例 2')
    })
    })
    })

    此時將透過 console.log(history) 查看並歸納整理就能得到以下結果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    --- 進入測試程式碼本身的 Context
    --- 進入父層情境的 Context
    beforeAll - 父層情境
    --- 進入子層情境 A 的 Context
    beforeAll - 子層情境 A
    beforeEach - 父層情境
    beforeEach - 子層情境 A
    子層情境 A 案例 1 // 執行 情境 A 案例 1 的時間點
    afterEach - 子層情境 A
    afterEach - 父層情境
    beforeEach - 父層情境
    beforeEach - 子層情境 A
    子層情境 A 案例 2 // 執行 情境 A 案例 2 的時間點
    afterEach - 子層情境 A
    afterEach - 父層情境
    afterAll - 子層情境 A
    --- 離開子層情境 A 的 Context

    --- 進入子層情境 B 的 Context
    beforeAll - 子層情境 B
    beforeEach - 父層情境
    beforeEach - 子層情境 B
    子層情境 B 案例 1 // 執行 情境 B 案例 1 的時間點
    afterEach - 子層情境 B
    afterEach - 父層情境
    beforeEach - 父層情境
    beforeEach - 子層情境 B
    子層情境 B 案例 2 // 執行 情境 B 案例 2 的時間點
    afterEach - 子層情境 B
    afterEach - 父層情境
    afterAll - 子層情境 B
    --- 離開子層情境 B 的 Context
    afterAll - 父層情境
    --- 離開父層情境的 Context
    --- 離開測試程式碼本身的 Context
    避免誤區:在 expect 後做清掃處理
    除了上面的用法,有時候你可能會認為既然要清掃,那我何不在斷言後處理就好呢:
    1
    2
    3
    4
    5
    6
    7
    describe('', () => {
    it('', () => {
    expect().toBe()
    // 在這裡做清除
    resetTestingEnv()
    })
    })
    這麼做當你在測試案例都是通過的情況下都沒有問題,但是一但某個測試案例發生了錯誤,由於測試案例就會在斷言時拋出 AssertionError 後停止,因此很有可能因為一個測試案例壞了導致接下來所有測試都受到影響:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    describe('', () => {
    it('', () => {
    expect().toBe() // AssertionError,這個測試案例就停在這了
    resetTestingEnv()
    })
    it('', () => {
    // 在沒有經過 `resetTestingEnv()` 下進行測試
    })
    it('', () => {
    // 在沒有經過 `resetTestingEnv()` 下進行測試
    })
    it('', () => {
    // 在沒有經過 `resetTestingEnv()` 下進行測試
    })
    it('', () => {
    // 在沒有經過 `resetTestingEnv()` 下進行測試
    })
    })
    因此較佳的作法還是使用 Setup & Teardown API 來處理會比較好:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    describe('', () => {
    beforeEach('', () => {
    setupTestingEnv()
    })
    afterEach('', () => {
    resetTestingEnv()
    })
    it('', () => {})
    it('', () => {})
    it('', () => {})
    it('', () => {})
    it('', () => {})
    })

    Vite Vue SSr 專案

    安裝

    Vite 需要Node.js版本 14.18+、16+。但是,某些模板需要更高的 Node.js 版本才能運作
    官網
    參考資料

    1
    2
    3
    4
    5
    6
    7
    npm create vite-extra@latest 專案名稱

    yarn create vite-extra 專案名稱

    pnpm create vite-extra 專案名稱

    deno run -A npm:create-vite-extra

    Streaming 串流媒體
    Non-streaming非串流媒體

    index.html//為 Vite的進入點
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    ├── node_modules // 安裝的所有套件
    ├── package.json //整個專案的設定檔
    ├── package-lock.json //整個專案的設定檔
    ├── public
    │ └── favicon.ico
    ├── env 環境變數資料夾
    │ └── .env.development 開發中環境變數
    │ └── .env.production 正式環境變數
    │ └── .env.staging 測試環境變數

    ├── README.md
    ├── src
    │ ├── App.vue
    │ ├── entry-client.ts //客戶端
    │ ├── entry-server.ts//伺服器
    │ ├── assets //放靜態資源
    │ ├── components //子元件
    │ │──layouts //佈局
    ├── main.ts //進入點
    │ ├── router//路由
    │ │ └── router.ts //路由
    │ ├── stores//Pinina 狀態管理庫
    │ │
    │ └── views
    │ └── type //屬性設定
    │ ├── engineering.ts
    │ └── filter //過濾

    └── vite.config.js // Vite 的設定檔

    main.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { createSSRApp } from 'vue'
    import App from './App.vue'

    // SSR requires a fresh app instance per request, therefore we export a function
    //每個請求都需要一個新的應用程式實例,因此我們匯出一個函數
    // that creates a fresh app instance. If using Vuex, we'd also be creating a
    // fresh store here.

    export function createApp() {
    const app = createSSRApp(App)
    return { app }
    }

    package.json啟動指令

    1
    2
    3
    4
    "scripts": {
    "build:client": "vite build --ssrManifest --outDir dist/client",
    "build:server": "vite build --ssr src/entry-server.ts --outDir dist/server",
    }

    啟動指令

    1
    2
    npm run build:client =>dist/client會多出這個資料夾
    npm run build:server =>dist/server"會多出這個資料夾

    注意:server.js Constants 的port 如果重複就會出現白幕(完全沒顯示任何…,

    Flutter 安裝

    Flutter 官網介紹 Windows 安裝方式:
    https://flutter.dev/docs/get-started/install/windows
    Flutter 官網介紹 Linux 安裝方式:
    https://flutter.dev/docs/get-started/install/linux
    所以需要安裝多種軟體

    • Flutter SDK
    • Android Studio
    • Xcode

    到官網下載 Mac flutter SDK
    Flutter SDK For MacOS 官方下載點:flutter_macos_2.2.1-stable.zip

    1
    2
    3
    4
    5
    6
    7
    8
    # 在桌面建立一個 development 資料夾
    mkdir ~/Desktop/development

    # 進入 development 目錄
    cd ~/Desktop/development

    # 解壓縮 Flutter.zip 壓縮檔到此目錄
    unzip ~/Downloads/flutter_macos_2.2.1-stable.zip

    Set3 : 設定Flutter指令路徑

    1
    2
    3
    4
    export PATH=$PATH:[Flutter目錄路徑]/flutter/bin
    //執行
    export PATH=$PATH:/Users/larahuang/Desktop/development/flutter/bin

    永久開啟

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 使用 nano 文字編輯器,開啟 「.bash_profile」
    nano .bash_profile

    # 在「.bash_profile」檔案中加入 Flutter 指令路徑:
    export PATH=$PATH:/Users/CalvinHuo/Desktop/development/flutter/bin

    # ctrl + x 存檔離開,將永久可使用 flutter 指令,不會因會關閉終端機,下次進來就無法使用 flutter 指令

    #新增的設定檔,需要重開終端機才會生效,若不想重開,可以執行以下指令
    source ~/.bash_profile

    檢查安裝(必須在設定Flutter指令路徑下)

    1
    2
    3
    4
    5
    6
    flutter doctor

    //查看位元
    uname -a
    RELEASE_X86_64 x86_64

    下載android studio

    android studio安裝說明

    1
    flutter doctor android-licenses

    參考資料

    純javascript 輪播

    1. 綁定每個輪播的區域:
    2. 設置輪播圖容器:
    3. 定義輪播的區域有幾個與索引從第0個開始:
    4. 設置一定時間重複setinterval函式:獲取當前索引,獲取下一個輪播圖的索引,取於防止超出範圍,讓當前輪播圖項左移出屏幕,讓下一個輪播圖向左進入屏幕;style.transition與style.transform=>translateX 的使用
    5. 移動結束後,設置下一張輪播圖位置,準備下一次移動setTimeout 函式的使用
    1
    2
    3
    4
    5
    6
    7
    8
    //html
    <div class="carousel">
    <div class="carousel_inner">
    <div class="carousel_item" style="background-color: aqua;"></div>
    <div class="carousel_item"
    style="background-color: yellow;"></div>
    </div>
    </div>

    Css

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    body{
    margin:0;
    padding:0;
    }

    .carousel_inner{
    width: 100vw;
    overflow: hidden;
    }

    .carousel_item{
    width: 100vw;
    height: 100vw;
    }

    JavaScript.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 綁定carouselItem
    let carouselItem =document.getElementsByClassName("carousel_item");
    // 取得寬度
    let width = carouselItem[0].clientWidth;
    // 算出carouselItem有幾個
    let lenth = carouselItem.length;
    // 容器
    let carouselInner = document.getElementsByClassName("carousel-inner");
    carouselInner[0].style.width =`${ width * length }px`;

    let i = 0;

    // 每個輪播元素一定時間重複
    setInterval(() =>
    // 獲取當前索引
    let current = i % lenth;
    console.log('獲取當前索引',current);
    let next = (i + 1) % lenth;
    console.log('獲取下一個輪播圖的索引',next);
    // 塞入當前的 translateX位置
    carouselItem[current].style.transform = `translateX(${0 - (current + 1) * width}px)`;
    // 塞入下一頁的 translateX位置
    carouselItem[next].style.transform = `translateX(${0 - ((current + 1) % lenth) * width}px)`;
    i++;
    // 移動結束後,設置下一張輪播圖位置,準備下一次移動
    setTimeout(() => {
    carouselItem[current].style.transition = "";
    carouselItem[next].style.transition = "";

    // 輪播每個元素
    for(let j = 0; j < lenth; j++){
    // 當前元素不變
    if(j == i % lenth) continue;
    if(j < i % lenth) {
    // 当前元素左邊的元素按順序移動放置到列表的后面做進行準備
    carouselItem[j].style.transform = `translateX(${(lenth - j - i % lenth) * width}px)`;
    }
    else{
    // 当前元素右邊的元素移動到當前元素的後面,防止輪播圖切換时中間有留白
    carouselItem[j].style.transform = `translateX(${0 - ((current + 1) % lenth) * width}px)`;
    }
    }
    i %= lenth;
    },800)
    },2000)

    js 輪播 加左右鍵與引導按鈕

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //html
    <div class="banner_container">
    <ul class="img_box">
    <li><img src="https://cdn.pixabay.com/photo/2024/04/13/11/29/muffins-8693748_1280.jpg" alt=""></li>
    <li><img src="https://cdn.pixabay.com/photo/2023/07/04/17/13/mallow-8106680_1280.jpg" alt=""></li>
    <li><img src="https://cdn.pixabay.com/photo/2024/01/04/09/01/bird-8486864_1280.jpg" alt=""></li>
    <li><img src="https://cdn.pixabay.com/photo/2024/05/15/07/59/flowers-8763039_1280.jpg" alt=""></li>
    <li><img src="https://cdn.pixabay.com/photo/2020/06/24/19/50/garden-5337535_1280.jpg" alt=""></li>
    <li><img src="https://cdn.pixabay.com/photo/2024/05/26/10/15/bird-8788491_1280.jpg" alt=""></li>
    </ul>
    <!--引導-->
    <ul class="sel_box">
    <li data-index="0"></li>
    <li data-index="1"></li>
    <li data-index="2"></li>
    <li data-index="3"></li>
    </ul>
    <!--左右鍵-->
    <div class="left_btn"><</div>
    <div class="right_btn">></div>
    </div>

    Css

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    * {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
    }

    .banner_container {
    position: relative;
    margin: 100px;
    width: 400px;
    height: 250px;
    overflow: hidden;
    }

    ul.img_box {
    position: absolute;
    left: 0;
    top: 0;
    transform: translateX(-400px);
    }

    .img_box li {
    float: left;
    list-style: none;
    }


    .img_box li img {
    width: 400px;
    }


    .sel_box {
    position: absolute;
    bottom: 15px;
    left: 50%;
    transform: translateX(-50%);
    }

    .sel_box li {
    list-style: none;

    display: inline-block;
    width: 10px;
    height: 10px;
    background-color: pink;
    margin-right: 3px;
    border-radius: 5px;
    transition: all 0.4s;
    }


    .left_btn {
    position: absolute;
    top: 50%;
    left: 0;
    transform: translateY(-50%);
    width: 25px;
    height: 30px;
    background-color: #fff;
    line-height: 30px;
    padding-left: 3px;

    cursor: pointer;
    }

    .right_btn {
    position: absolute;
    top: 50%;
    left: 375px;
    transform: translateY(-50%);
    width: 25px;
    height: 30px;
    background-color: #fff;
    line-height: 30px;
    padding-left: 5px;
    cursor: pointer;
    }



    .sel_box .cur {
    background-color: red!important;
    transform: scale(1.3);
    }

    JavaScript

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    //綁定圖片Box
    let img_box = document.querySelector('.img_box');
    let imgs = document.querySelectorAll('img');

    //綁定引導Box
    let sel_box = document.querySelector('.sel_box')
    let lis = sel_box.querySelectorAll('li');

    //左右按鈕
    let left_btn = document.querySelector('.left_btn');
    let right_btn = document.querySelector('.right_btn');

    //索引第幾張
    let index = 0;
    let timer = null;

    //設置容器大小
    let imgContainerW = img_box.offsetWidth
    img_box.style.width = imgContainerW * imgs.length + 'px'
    //設置容器位置
    img_box.style.left = 0 + 'px';

    //設置第一個小圖為當前按鈕
    lis[0].className = 'cur'

    //輪播切換
    const swapImg =() =>{

    img_box.style.left = -index * imgContainerW + 'px';

    for (let i = 0; i < lis.length; i++) {
    lis[i].className = '';
    }

    lis[index].className = 'cur'
    }

    const swapFormat =() =>{
    index++;

    if (index >= 4) {

    index = 4;
    img_box.style.transition = 'all, linear, 1s';
    img_box.style.left = -index * imgContainerW + 'px';
    for (let i = 0; i < lis.length; i++) {
    lis[i].className = '';
    }

    lis[0].className = 'cur'

    //通過定時器立馬切換到第一張
    setTimeout(()=>{
    index = 0;
    img_box.style.transition = '';
    swapImg();
    }, 1500)


    } else {
    img_box.style.transition = 'all, linear, 1.5s';
    swapImg();
    }
    }
    //
    timer = setInterval(swapFormat, 3000)

    //右按鈕 =>自動播放
    right_btn.addEventListener('click', ()=>{
    swapFormat();
    })

    //左按鈕
    left_btn.addEventListener('click', () =>{
    index--;

    if (index < 0) {
    index = -1
    img_box.style.transition = 'all, linear, 1.5s';
    img_box.style.left = -index * imgContainerW + 'px';
    for (let i = 0; i < lis.length; i++) {
    lis[i].className = '';
    }

    lis[3].className = 'cur'


    setTimeout(()=>{
    index = 3
    img_box.style.transition = '';
    swapImg();
    }, 1000)

    } else {
    img_box.style.transition = 'all, linear, 1.5s';
    swapImg();
    }
    })


    img_box.addEventListener('mouseover', () =>{
    clearInterval(timer)
    })

    right_btn.addEventListener('mouseover', () =>{
    clearInterval(timer)
    })

    left_btn.addEventListener('mouseover', () =>{
    clearInterval(timer)
    })


    img_box.addEventListener('mouseout', ()=> {
    timer = setInterval(swapFormat, 3000)
    })

    left_btn.addEventListener('mouseout', ()=> {
    timer = setInterval(swapFormat, 3000)
    })

    right_btn.addEventListener('mouseout', () =>{
    timer = setInterval(swapFormat, 3000)
    })

    javascript 計算總和的幾種方法

    for迴圈加總

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const SumFor=(arr)=>{
    var sum=0;
    for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
    };
    return sum;
    }
    var data= [1, 1, 1];
    console.log(SumFor(data));

    forEach遍歷 加總

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const sumforEach = (arr) =>{
    var sum=0;
    arr.forEach((el)=>{
    sum+=el;
    });
    return sum;
    }
    var data= [1, 1, 1];
    console.log(sumforEach(data));

    reduce() 方法將一個累加器及陣列中每項元素(由左至右)傳入回呼函式,將陣列化為單一值。

    1
    2
    3
    4
    var sum = [1, 1, 1].reduce( (a, b)=> {
    return a + b;
    }, 0);
    console.log(sum);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const array =[
    {name:'apple',price:25,qty:1},
    {name:'orange',price:10,qty:1},
    {name:'piniaApple',price:45,qty:1},
    ]

    //將map價格取出=>reduce累加=> 簡化以後
    const SumReduce = (arr)=>{
    return arr.map(el=>el.price).reduce((a,b)=>a+b);
    }
    console.log(SumReduce(array));