---
# System prepended metadata

title: 前端讀書會

---

# 前端讀書會

# 浮水印 watermark
##### (2024.11.29)
在網頁生成浮水印，可以用CSS，JS達到效果。Vue 可以使用Element Plus 提共的浮水印元件，或使用外掛，如watermarkify/vue-watermark，也可以自建元件。
製作浮水印須考量大小、位置、距離、圖層顯示的上下位置，如果是文字，還可以設定字型、顏色等效果。

## 介紹不同的浮水印的生成方法
### 一、Element Plus 浮水印元件 el-watermark
[Element Plus 官網說明](https://element-plus.org/en-US/component/watermark.html#watermark)

專案如果未安裝Element Plus，要先安裝，程式部分引用自官網，設定如下：
```javascript=
<script setup lang="ts">
  import { reactive, watch } from 'vue'
  import { isDark } from '~/composables/dark'

  const font = reactive({
    color: 'rgba(0, 0, 0, .15)',
  })

  watch(
    isDark,
    () => {
      font.color = isDark.value
        ? 'rgba(255, 255, 255, .15)'
        : 'rgba(0, 0, 0, .15)'
    },
    {
      immediate: true,
    }
  )
</script>

<template>
  <el-watermark :font="font">
    <div style="height: 500px" />
  </el-watermark>
</template>
```

浮水印生成的畫面
![image](https://hackmd.io/_uploads/r1cj5Hrmyl.png)

滾軸右移時，頁面不會有浮水印
![image](https://hackmd.io/_uploads/S1rTqSBXye.png)

要浮水印滿版，可以增加CSS style
```html
<template>
  <el-watermark :font="font" style="position: absolute;">
    <div style="height: 100vh" />
  </el-watermark>
</template>
```

浮水印滿版畫面
![image](https://hackmd.io/_uploads/HybRjSH71g.png)

### 二、watermarkify/vue-watermark
* [watermarkify/vue-watermark 使用手冊](https://github.com/watermarkify/vue-watermark)
* [功能展示連結](https://watermarkify.github.io/vue-watermark/)，從畫面實際操作，可以了解每個屬性設定的功能

程式設定步驟：
1. 在終端機安裝套件，以NPM為例，輸入`npm install @watermarkify/vue-watermark`
2. 設定程式：
```javascript=
<script setup lang="ts">
// step 1: import Watermark from @watermarkify/vue-watermark
import { Watermark } from '@watermarkify/vue-watermark'

// step 2: define watermark options
// see https://github.com/watermarkify/vue-watermark#options
const watermarkOptions = ref({
  content: 'watermark',
  gap: [20, 20],
  offset: [10, 10],
  zIndex: 5,
  rotate: -20,
})
</script>

<Watermark :options="watermarkOptions">
  <div>插入要加上浮水印的資料</div>
</Watermark>
```

實際生成的畫面，遇到跑版的還是會像Element Plus 一樣，浮水印只有生成在視窗可見的範圍內，如果拉動滾軸，就無法生成浮水印，但是正常沒跑版的頁面，是可以整個頁面生成浮水印，想要滿版的方式，和第一個做法一樣。另外，不知道為何浮水印尾端有被裁減。
![image](https://hackmd.io/_uploads/SJvHlPSmkg.png)

在終端機安裝時有跳出漏洞警示，使用時要考慮安全疑慮，以及功能是否失效。
![image](https://hackmd.io/_uploads/Sy7lCSrXye.png)


### 三、自建元件
監控螢幕長、寬，並除以浮水印的長、寬，再使用迴圈印出每個浮水印，達到滿版的效果。

1. 建立元件
:::spoiler
```javascript=
<template>
  <div class="watermark-container">
    <div
      v-for="(item, index) in watermarks"
      :key="index"
      class="watermark-item"
      :style="getWatermarkStyle(item.x, item.y)"
    >
      {{ props.text }}
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';

// 定義 props
const props = defineProps({
  text: {
    type: String,
    default: 'Confidential',
  },
  opacity: {
    type: Number,
    default: 0.1,
  },
  rotate: {
    type: Number,
    default: -30,
  },
  fontSize: {
    type: Number,
    default: 20,
  },
  color: {
    type: String,
    default: '#000000',
  },
  zIndex: {
    type: Number,
    default: 9999,
  },
});

const watermarks = ref([]);
let observer = null;

// 初始化浮水印
const initWatermark = () => {
  const screenWidth = window.innerWidth;
  const screenHeight = window.innerHeight;
  const spacing = {
    x: 250,
    y: 150,
  };

  const columns = Math.ceil(screenWidth / spacing.x);
  const rows = Math.ceil(screenHeight / spacing.y);

  const newWatermarks = [];

  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < columns; j++) {
      newWatermarks.push({
        x: j * spacing.x,
        y: i * spacing.y,
      });
    }
  }

  watermarks.value = newWatermarks;
};

// 獲取浮水印樣式
const getWatermarkStyle = (x, y) => {
  return {
    left: `${x}px`,
    top: `${y}px`,
    opacity: props.opacity,
    transform: `rotate(${props.rotate}deg)`,
    fontSize: `${props.fontSize}px`,
    color: props.color,
  };
};

// 處理視窗大小變化
const handleResize = () => {
  initWatermark();
};

// 處理 DOM 變化
const handleDomChange = (mutations) => {
  const needsUpdate = mutations.some(
    (mutation) =>
      mutation.type === 'attributes' || mutation.type === 'childList'
  );

  if (needsUpdate) {
    initWatermark();
  }
};

watch(props, () => {
  initWatermark();
});

onMounted(() => {
  initWatermark();

  window.addEventListener('resize', handleResize);

  // 設置 MutationObserver
  observer = new MutationObserver(handleDomChange);
  observer.observe(document.body, {
    attributes: true,
    childList: true,
    subtree: true,
  });
});

onBeforeUnmount(() => {
  window.removeEventListener('resize', handleResize);
  if (observer) {
    observer.disconnect();
  }
});
</script>

<style scoped>
.watermark-container {
  position: fixed;
  top: 0px;
  left: -40px;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: hidden;
}

.watermark-item {
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 300px;
  height: 100px;
  user-select: none;
  white-space: nowrap;
}
</style>
```
:::


2. 引入欲渲染的頁面
```html
<template>
  <div>這裡是資料...</div>
  <useWaterMark 
	:text=waterMark
	:opacity="0.1"
    :rotate="-30"
    :fontSize="20"
    color="#000000"
    :zIndex="9999"
  />
</template>
```

實際生成畫面
![image](https://hackmd.io/_uploads/ByaUS8Bm1x.png)

:::danger
須注意，以上三種由前端生成的浮水印，如果沒有轉成圖片檔或是設定防止刪除的機制，是可以從開發者模式刪除浮水印。
:::

---
# 非同步元件defineAsyncComponent 與 vite-plugin-federation

當有些客製化的功能，比如CSS樣式或是自訂的input 物件，除了自己專案用到，也可發布給其他專案使用時，就可以用到「非同步元件」[(defineAsyncComponent)](https://zh-hk.vuejs.org/guide/components/async.html)與 [vite-plugin-federation](https://github.com/originjs/vite-plugin-federation)

### 使用方式
#### 一、安裝套件
remote 和 host 都要安裝
`npm install @originjs/vite-plugin-federation --save-dev`
or 
`yarn add @originjs/vite-plugin-federation --dev`

#### 二、remote 端設定
1. vite.config.ts
在`exposes`設定要輸出的元件以及元件的路徑(exposes)， `./Watermark`是 host 要設定的元件名稱，`./src/components/Watermark.vue` 是這個元件在remote 專案的路徑

```javascript=
import federation from '@originjs/vite-plugin-federation'

export default {
  plugins: [
    federation({
      name: 'remote-app',
      filename: 'remoteEntry.js',
      // Modules to expose
      exposes: {
        './Watermark': './src/components/Watermark.vue',
      },
      shared: ['vue']
    })
  ]
}
```

2. 注意瀏覽器版本限制
如果發生`ERROR: Top-level await is not available in the configured target environment` 錯誤，要設定build
```
build: {
  target: "esnext"
}
```
或
```
build: {
  target: ["chrome89", "edge89", "firefox89", "safari15"]
}
```

3. 設定好要記得打包並讓專案運作

(一)、打包
`＄npm run build`

這樣引用的host 端才能接受到remote 端的元件

打包後會多了很多federation 相關的檔案
![image](https://hackmd.io/_uploads/Byz8gMLGJl.png)

想要即時打包，可在package.json 的build 設定 watch，只要檔案有異動，就會即刻打包
```javascript
"scripts": {
  "build": "vite build --watch",
}
```

(二)、啟動專案
要運行打包的檔案，要執行`＄npm run preview`
![image](https://hackmd.io/_uploads/H1sdNfIGJl.png)

#### 三、host 端設定
1. 設定remote_app 連接的domin name
```javascript=
import federation from '@originjs/vite-plugin-federation'

export default {
  plugins: [
    federation({
      name: 'host-app',
      remotes: {
        remote_app: "http://localhost:5001/assets/remoteEntry.js",
      },
      shared: ['vue']
    })
  ]
}
```

2. 在要使用async元件的檔案，引入remote 的元件
```javascript=
import { defineAsyncComponent } from 'vue'

const Watermark = defineAsyncComponent(() =>
  => import("remote_app/Watermark")
)
```

完成畫面
![image](https://hackmd.io/_uploads/HJCVXhHQye.png)

>[!Note]
如果host 端沒有出現remote 端的元件，可能需要重新打包(build)

:::danger
如果remote 掛掉，其他使用的也會跟著掛掉，所以要思考如何維護。
:::

參考：[[ 想入門，我陪你 ] Vue 3 宅家輕鬆玩｜Day 11：非同步組件與 vite federation 試玩](https://www.youtube.com/watch?v=KwopYEEPDgg&list=PLEfh-m_KG4dbjf0YCJ7i0FFGK3FtQpanL&index=14&ab_channel=Alex%E5%AE%85%E5%B9%B9%E5%98%9B)

---
# CORS(跨來源資源共用，Cross-Origin Resource Sharing)

## 一、前言：
網頁有分靜態網頁與動態網頁兩種。靜態網頁的內容是寫死在html 檔裡，而動態網頁的內容，是客戶端(Client)向伺服器(Server)發送請求(Request)，得到伺服器回應(Respones)的資料後，將資料渲染在網頁上。
當請求與回應的來源都是同一個來源(也可以說是同一個網址URL)時，瀏覽器就不會攔截伺服器回應的訊息，反之，瀏覽器就會報錯，這是瀏覽器**同源政策**(Same-origin policy)的安全機制。這個機制是防止其他來源隨意向伺服器發送請求，造成伺服器因為頻繁地回應而過載，或是伺服器內的機密資訊外流。

瀏覽器報錯畫面
![image](https://hackmd.io/_uploads/Hyd5JzYCC.png)

![image](https://hackmd.io/_uploads/HJBMOMK00.png)
[圖片來源](https://www.youtube.com/watch?v=4KHiSt0oLJ0)

## 二、介紹同源與不同源
同源是指同樣的通訊協定(Protocol，又稱Scheme),網域(Domain Name),埠號(Port)，以下舉例：

### 1. 同源
範例一
* `http://example.com/index.html`
* `http://example.com/product.html`

範例二
* `https://example.com:443`
* `https://example.com`

:::info
http 預設埠號是80
https 預設埠號是443
:::

### 2. 不同源
範例一，不同的通訊協定
* `http://example.com/index.html`
* `https://example.com/product.html`

範例二，不同的網域
* `http://globe.example.com`
* `http://example.com`

範例三，不同的埠號
* `https://example.com:5000`
* `https://example.com`

## 三、CORS 設定
為了解決跨來源請求的問題，就必須設定CORS。而這個設定須由<font style="color:#f00">**後端**</font>處理，請後端回應時，在header，加入Access-Control-Allow-Origin的來源，通常來源設定有兩種：

1. 加上與伺服器同樣的來源
`Access-Control-Allow-Origin: https://example.com`

2. 設定為允許任何來源
`Access-Control-Allow-Origin: *`

### 後端的作法，使用UseCors()----以 Asp.Net CORE 6為例
```csharp=
if (!WebSiteEnvironment.IsProduction && 
    !WebSiteEnvironment.IsSIT && 
    !WebSiteEnvironment.IsUAT)
{
    app.UseCors(x =>
    {
        x.AllowAnyOrigin().
          AllowAnyHeader().
          AllowAnyMethod().
          SetPreflightMaxAge(new TimeSpan(1, 0, 0));
    });
}
```

HTTP標頭
![Header](https://hackmd.io/_uploads/rJO-8dKCR.png)

請求標頭
![image](https://hackmd.io/_uploads/S1qyBOtRC.png)

回應標頭
![image](https://hackmd.io/_uploads/HJOMH_FA0.png)

### 前端的作法(參考，可以不設定)
以axios 為例：

```javascript=
const http = axios.create({
  baseURL: baseUrl + '/',
  headers: {},
});

http.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    config.headers['Access-Control-Allow-Origin'] = '*';
    config.headers['Content-Type'] = 'application/json;charset=utf-8';
    return config;
  }
);
```

### CORS外掛：
* [CORS unblock](https://chromewebstore.google.com/detail/cors-unblock/lfhmikememgdcahcdlaciloancbhjino)
* [Allow CORS](https://chromewebstore.google.com/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf)
* [CORS anywhere](https://github.com/Rob--W/cors-anywhere)

:::danger 
`Access-Control-Allow-Origin: *` 的設定可能會導致安全漏洞
:::

<br/>

---
# 地理定位(Geolocation)
是一個HTML5 的功能，透過Geolocation API 可以取得地理定位的資訊
請用瀏覽器開啟右側網 https://alonso1331.github.io/Geolocation/

```javascript=
const compass = document.querySelector('svg');
const speed = document.querySelector('.speed');

if ('geolocation' in navigator){
  // watchPosition() 觀看者目前位置
  navigator.geolocation.watchPosition((position) => {
    // 緯度, 經度
    position.coords.latitude, position.coords.longitude;
    console.log(position);

    // 移動角度 deg
    const deg = position.coords.heading;

    compass.style.transform = `rotate(${deg}deg)`;

    // 移動速度 m/s 轉換成習慣的用法 km/h
    const speeds = (position.coords.speed * 3.6).toFixed(2);
    speed.innerHTML = `${speeds}`;
  });
} else {
    // 取消觀察者位置
    navigator.geolocation.clearWatch();
}
```

<font style="color:#f00">watchPosition()</font>是觀察者目前的位置
使用navigator.geolocation.watchPosition()，會回傳經度、緯度、速度，移動的角度等資訊，將所需的資料寫到html 裡

下圖為回傳的物件內容
![image](https://hackmd.io/_uploads/HJHJQsV2R.png)

---
# 邏輯炸彈(Logic Booms)
程式設計師在系統中寫入一段程式，當他的人事資料從公司的人事系統中被刪除時，造成公司的檔案或程式毀損。

---
# MySQL 的觸發器(trigger) 設定
1. 點選畫面左側的資料庫後，在右上方選擇觸發器
![trigger](https://hackmd.io/_uploads/SJgxlq42C.png)

2. 新增一個觸發器
![create new trigger](https://hackmd.io/_uploads/rJALg9N3R.png)

3. 選擇好資料表，要再確定觸發器是要在事件發生前或後，以及什麼事件時(新增、修改、刪除)觸發
![image](https://hackmd.io/_uploads/ryYC0K43C.png)

4. 定義者設定
就是設定這個觸發器是由哪位管理者設置的，有些會設定為最高權限的管理者，如下圖所示
:::info
root@localhost
:::


---
# Base64 的應用

### 一、動機
在此次專案開發中，有一項聯徵報送的功能，玉山向聯合徵信中心調查債務人信用資料，聯徵中心會回傳HTML格式資料給玉山，後端則將回傳的資料存放在資料庫裡。後端將HTML格式用Base64編碼傳送到前端，然後前端將接到的Base64解碼為文字，並渲染在前端畫面。

突然有一天這個功能失效了，究竟是甚麼原因？

---
### 二、Base64 簡介
Base64是一種二進位的編碼，因為編碼的索引表總共有64個，所以被稱為Base64。通常用於資料的傳輸與儲存。
[維基百科 Base64](https://zh.wikipedia.org/wiki/Base64)

前端應用Base64 的時機在於傳遞或儲存檔案的URL 或是圖片。Base64解碼URL 後，通常以字元符號的方式呈現，而圖片因為其特性，解碼後的字元則不同於URL 的形式。

文字的編碼
:::info
Hello World => SGVsbG8gV29ybGQ=
:::

圖片的編碼
:::info
![image](https://hackmd.io/_uploads/r14F_18_A.png)

data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/7QB4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAD8cAVoAAxslRxwCAAACAAIcAj8ABjAwMjAyORwCPgAIMjAyMzAzMTkcAjcACDIwMjMwMzE5HAI8AAYwMDIwMjkAOEJJTQQlAAAAAAAQHD8NJrYj4BjmLvQVHPbdp//bAEMAAgEBAgEBAgICAgICAgIDBQMDAwMDBgQEAwUHBgcHB
中間略
wct/8AHaKKA2P/2Q==
:::

線上編碼轉換：
* https://products.aspose.app/imaging/zh-hant/conversion/base64-to-image
* 轉換純文字 https://zh-tw.rakko.tools/tools/24/
* 轉換圖片 https://www.toolnb.com/tools-lang-zh-TW/ImageToBase64.html

---
### 三、DEBUG 過程
#### 1. 找出問題
先把資料庫的HTML格式貼到HTML檔，看格式是否有誤？ 結果 :heavy_check_mark:
檢查前端的編譯程式，是否解碼出錯了？ 結果 :heavy_check_mark:
:::success
:information_source: 額外說明
JS有2個函別編碼與解碼Base64的方法，參見[W3School](https://www.w3schools.com/jsref/met_win_atob.asp)
* 編碼 window.atob()
* 解碼 window.btoa()

atob 的意思就是ASCII to Binary，反之就是Binary to ASCII
:::

把後端給的Base64編碼放到可解碼的網站，看是否可以解出文字？結果 :x:

#### <font style="color:#f00">問題就出在後端回傳的不是文字，而是圖片。</font>

所以原先放入字元符號的做法就不能用了
![image](https://hackmd.io/_uploads/rk8kqyLd0.png)

#### 2. 解決方法

圖片的作法，以VUE 為例：

* 路徑的用法：
在vue 使用<img/>標籤，通常只要綁定，再加上路徑，就可以了
![image](https://hackmd.io/_uploads/SkX3jyIO0.png)
* 圖片格式的用法：
但是如果src 不是路徑，是圖片編碼的base64，就要用下列的方法了
![image](https://hackmd.io/_uploads/HJDENcD8C.png)

----
### 免費網頁素材：
* 免費素材
[かわいいフリー素材集 いらすとや](https://www.irasutoya.com/search/label/%E4%BC%9A%E7%A4%BE?updated-max=2020-11-21T13:00:00%2B09:00&max-results=20&start=20&by-date=false)
* 調色盤
[Adobe](https://color.adobe.com/zh/)
[colourcontrast.cc](https://colourcontrast.cc/?background=3b36a2&foreground=352222)
* 圖表
[Chart.js](https://www.chartjs.org/docs/latest/)
* 特效
[simpleparallax](https://simpleparallax.com/)
[transition style](https://www.transition.style/)

---
# MVC 架構 ---- 以Laravel為例


### 一、MVC 架構概述：
#### 以往的程式開發是將使用者操作的畫面，業務邏輯和連接資料庫的設定，放在同一個檔案。


#### 為了能有效管理上述三個類別，於是產生了MVC、MVP、MVVM等不同的框架。

#### 而MVC是其中較為普遍的框架。

* Model 負責聯繫資料庫的部分，有資料寫入資料庫時，要把關什麼可以寫入什麼不行。
* View 是指使用者操作的畫面。也是前端將後端的資料渲染的所在。
* Controller 是接收前端使用者的CRUD等業務邏輯，並傳給Model，以便Model向資料庫取得資料。接到Model回傳資料庫的資料後，再拋回到View。


### 二、傳統開發網頁的內容：

如果沒有使用具有套版的工具開發，必須重複在不頁面放同樣的程式

首頁
![image](https://hackmd.io/_uploads/rJAKTx-IA.png)

其他頁面
![image](https://hackmd.io/_uploads/r140pxWLC.png)

------
前後端並存在同一個頁面
![image](https://hackmd.io/_uploads/rJg91W-UA.png)

------


### 三、laravel 架構簡易說明

MVC 架構示意圖
![image](https://hackmd.io/_uploads/H1o4Wfk80.png)


laravel專案目錄架構
![laravel專案目錄架構](https://hackmd.io/_uploads/Bkk0iTarR.png)


Model 畫面
![image](https://hackmd.io/_uploads/ry9jzMyIA.png)


Controller 畫面
![image](https://hackmd.io/_uploads/S1kPffJU0.png)

View 畫面
![image](https://hackmd.io/_uploads/B1DOmzkUC.png)

JS 區塊
![image](https://hackmd.io/_uploads/BJISAj-I0.png)





