2022年9月29日 星期四

Vue3 with composition api 學習心得

import 相關物件

import { ref, readonly, computed, watch, watchEffect, onMounted, provide} from 'vue';
import * as common from "@/utils/common"; // @ = clientapp/src


基本用法

setup() {
  const itemRefs = []; // 定義內部使用物件
  const orders = ref([]); // 定義HTML可使用物件
  const obj = readonly(ref({value:0,text:'123'})); // 定義唯讀物件
  const no = computed(() => `$No.{obj.value}`); // 定義計算物件
  const f1= (p1, p2) => { // 定義函數
    orders.value=xxx; // 設定物件值
  }
  onMounted(() => { // html rendered
  ...
  })
  ... // 預先執行程式
  return {orders, f1} // 回傳HTML可存取的物件和函數
}

等待 dom 更新完畢

import { nextTick } from 'vue'
用法1:
const xxx = async () => {
  await nextTick(); // 等待 dom 更新完畢
  ...
}
用法2:
const xxx = () => {
  nextTick(() => ...);  // 設定 dom 更新完畢後要執行的程式
}

使用 props 和 emits

props: ['id'], 
emits: ['done'],
setup(props, context) {
  console.log(props.id) // 取得 props
  context.emit('done'); // 回拋事件

若要修改 props 物件內容,需要另外建立參考物件
const objref = ref(props.obj)
objref.name='123'

使用 provide 和 inject

上層元件(例如 app.vue)
provide("userName", userName) // 設定子元件可讀取寫入的物件
provide('book', readonly(book)) // 設定子元件可讀取的物件
provide("hasRight", { hasRight }) // 設定子元件可呼叫的函數

子元件
const userName = inject("userName"); // 注入上層元件的物件
const { hasRight } = inject("hasRight"); // 注入上層元件的函數


設定watch

watch(id, (newValue, oldValue) => { // 監聽變數
});
watch(()=>obj.id, (newValue, oldValue) => { // 監聽物件屬性
});
watch(()=>obj, (newValue, oldValue) => { // 監聽物件所有屬性
},{deep:true});
watchEffect(() => { // 自動監控使用到的變數
   fetchData(); // 只要 props.id 有變化就會自動呼叫 fetchData(),但若 props.id 包在 setTimeout 裡面則不會自動監控
})
const fetchData = () => {
if (props.id)
axios.get(...)
.then(response => {
...
});
}


v-for 搭配 ref

<div v-for="order in orders" :key="order.id" :ref="setItemRef">
let itemRefs = [];
const setItemRef = el => {
if (el) {
itemRefs.push(el) // 將 v-for 每次產生出來的元件放入陣列
}
if (itemRefs.length == orders.value.length) { // 當元件全部產生後執行
const index = orders.value.findIndex(a => a.id == props.id); // 取得特定元件
itemRefs[index].scrollIntoView(); // 讓特定元件顯示在最上面
}
}
onBeforeUpdate(() => {
                itemRefs = []
            })

避免 import .vue 出現 typescript 編譯錯誤

修改 src/shims-vue.d.ts
declare module '*.vue' {
    import Vue from 'vue'
    export default Vue
}

異步載入元件

import { defineAsyncComponent } from 'vue';
export default {
components: {
xxx: defineAsyncComponent(() =>
import('./xxx.vue')
),
},

DOM出現於視野內時觸發執行動作

import { useIntersectionObserver } from '@vueuse/core'
export default {
        setup(props) {
            const targetEl = ref();
const { stop } = useIntersectionObserver(
                targetEl,
                ([{ isIntersecting }]) => {
                    if (isIntersecting) {                        
                        ...
                        stop(); // 避免重複觸發
                    }
                }
            );
    return { targetEl }
        },
    };
<template>
    <div ref="targetEl"></div>
</template>

強制更新元件內容

例如屬性值是動態判斷且會參考 props,但 props 改變時並不會更新屬性值
設定元件的 key 並更新其值會強制更新元件內容
html
====
<WaitSchedule :key="updatekey"></WaitSchedule>
javascript
====
import WaitSchedule from './xxx.vue';
    export default {
        components: {
            WaitSchedule
        },
        setup() {
            const updatekey=ref(1)
            watch(() => xxx, () => {
                updatekey.value++;
            });


設定日期可選最大值為今天

<input class="form-control" type="date" :max="(new Date()).toISOString().split('T')[0]">

import self

須設定元件名稱否則會跳警告且無法正確載入第二個自己
export default {
    name: 'name1',

動態css

綁定 css 的值為某個屬性,以達到動態設定目的
<style>
    .cv-week {
        min-height: v-bind(weekheight);
    }
</style>
 export default {
     setup() {
        const maxevents=ref()
        const weekheight = computed(() => `${1 + maxevents.value * 2}em`)
        return {weekheight }

※單檔元件才有作用 (javascript 寫在同一個 .vue)


※vue3 v-on:change & v-on:input 會抓到舊值,寫法可改為 @change="xxx($event)",$event 放的是新值
※作為 component 必須有唯一的root element否則會有異常狀況


讓容器內的元件出現在容器可見區域最上方

xxx.scrollIntoView(); 

使用情境 : 容器設定 overflow-y: auto 時,內部有多個元件且超過可見區域範圍而出現垂直捲軸,指定讓某個元件顯示在容器可見區域最上方

參考來源

2022年9月23日 星期五

導入 ESLint 學習心得

若要允許 html 使用全形空白及 import xxx.vue 檔名使用中文命名,修改 package.json 如下
"eslintConfig": {
  "rules": {
    "no-irregular-whitespace": 0 <= 為了使用全形空白
    "vue/multi-word-component-names": 0 <= 為了使用中文命名
  }
}

2022年9月13日 星期二

例外處理機制

目的 : 讓前端呼叫者可以顯示易懂的錯誤訊息

步驟 :
1. 自訂 ActionResult
public class CustomError : IActionResult
{
private readonly HttpStatusCode _status;
private readonly string _errorMessage;

public CustomError(HttpStatusCode status, string errorMessage)
{
_status = status;
_errorMessage = errorMessage;
}
public async Task ExecuteResultAsync(ActionContext context)
{
var objectResult = new ObjectResult(new
{
errorMessage = _errorMessage
})
{
StatusCode = (int)_status,
};
context.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = _errorMessage;
await objectResult.ExecuteResultAsync(context);
}
}

2. 自訂 ExceptionFilter
public class BaseExceptionFilter : IExceptionFilter
{
private readonly IHostEnvironment _hostEnvironment;

public BaseExceptionFilter(IHostEnvironment hostEnvironment) =>
_hostEnvironment = hostEnvironment;

public void OnException(ExceptionContext context)
{
if (_hostEnvironment.IsDevelopment())
{
// 顯示英文訊息方便google
System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
}

string controllerName = context.RouteData.Values["controller"].ToString();
string actionName = context.RouteData.Values["action"].ToString();
var request = context.HttpContext.Request;
var requestUrl = request.Scheme + "://" + request.Host + request.Path;
                var username = context.HttpContext.User?.Identity.Name;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.AppendLine("xxx發生未預期錯誤");
sb.AppendLine($"錯誤訊息: {context.Exception.詳細訊息()}");
sb.AppendLine($"堆疊追蹤: {context.Exception.StackTrace.Replace("<","<").Replace(">",">")}"); // 若使用skype 通知則不接受<和>
sb.AppendLine($"網址: {requestUrl}");
         sb.AppendLine($"系統使用者: {username}");
sb.AppendLine($"Controller: {controllerName}");
sb.AppendLine($"Action: {actionName}");
                if (_hostEnvironment.IsDevelopment()) {
                    // 直接顯示除錯訊息
                    context.Result = new CustomError(HttpStatusCode.InternalServerError, sb.ToString());
                }
                else {
                    // todo: 使用 skype、line、email 等方式發送除錯訊息給資訊人員
                     context.Result =  new CustomError(HttpStatusCode.InternalServerError, "發生未預期錯誤,請洽資訊部");
                }
}
}

3.1. 建立頂層 Controller,遇到未攔截的錯誤時透過自訂 ExceptionFilter 回傳自訂 ActionResult
[TypeFilter(typeof(BaseExceptionFilter))]
public class BaseController : ControllerBase

3.2. Controller Action 若要回傳錯誤則透過自訂ActionResult
ex. return new CustomError(HttpStatusCode.BadRequest, "請輸入收貨地址");

4. 前端顯示錯誤訊息
ex.
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
console.error(error);
if (error.response.status == 401) {
alert('登入逾時');
setTimeout(() => {
window.location.reload();
}, 2000);
}
else {
if (error.response.data.errorMessage) alert(error.response.data.errorMessage);
else alert(error.message);
}
return Promise.reject(error);
});

非 core 版後端寫法

public class BaseExceptionFilter : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext context)
{
// 顯示英文訊息方便google
System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");

string controllerName = context.RouteData.Values["controller"].ToString();
string actionName = context.RouteData.Values["action"].ToString();
var requestUrl = context.HttpContext.Request.Url.AbsoluteUri;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.AppendLine("xxx發生未預期錯誤");
sb.AppendLine($"錯誤訊息: {context.Exception.詳細訊息()}");
sb.AppendLine($"堆疊追蹤: {context.Exception.StackTrace.Replace("<", "<").Replace(">", ">")}");
sb.AppendLine($"網址: {requestUrl}");
sb.AppendLine($"系統使用者: {xxx}");
sb.AppendLine($"Controller: {controllerName}");
sb.AppendLine($"Action: {actionName}");
// todo: 使用 skype、line、email 等方式發送除錯訊息給資訊人員
context.ExceptionHandled = true;
context.HttpContext.Response.TrySkipIisCustomErrors = true;
context.HttpContext.Response.Write("發生未預期錯誤,請洽資訊部");
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}

頂層Controller
[BaseExceptionFilter]
public class BaseController : Controller
{
  
}
※若要回傳可預期錯誤給使用者看,則用以下方式
async public Task<IHttpActionResult> xxxAsync()
{
if (xxx)
{
HttpContext.Current.Response.Write(xxx);
HttpContext.Current.Response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest;
return default;
}

input 連結 datalist 用程式控制彈出選項

範例: nextTick(() => document.querySelector('input').showPicker());  ※僅支援現代瀏覽器