2024年2月21日 星期三

[Gitlab]CI-流程建置

 

CI(Continuous Integration)是Devops的軟體開發流程,主要針對程式碼變更後的自動建置和測試之後,定期將變更合併置主要Repository,CI的關鍵目標是能更快發現和解決問題、改善軟體品質還有減少驗證和釋出軟體更新所需的時間。


此篇文章將教你如何建立Gitlab CI流程

環境說明:

  • Gitlab (self-managed)
  • Gitlab Runner
  • Docker (windows)


首先在Docker上安裝Gitlab-Runner,輸入以下指令安裝Docker

2024年2月16日 星期五

[React]環境部署


透過以下的簡易步驟,來部署React網站


以下依照不同的Server列出說明

項次

部署位置

地端/雲端

部署方式

說明

1

Windows Server(iis)

手動

本地VM,測試環境或者待管Server時會使用

2

Azure

手動

透過VS CodeFTP去部署

3

Azure

自動

透過CI/CD方式去部署

 

 

 

 

 


  • Window Server(iis)

1.先設定iis站台的URL Rewrite (SPA網站都可參考此設定)

//Web.config

更多資訊可參考保哥此篇文章說明

2.使用Terminal執行語法npm run build,以產生實體檔(檔案預設放在dist

3.手動搬移檔案至Server

  •  Azure (手動上傳)

    手動上傳還可區分成VS Code部署以及FTP部署兩種

    • VS Code部署

  1. 安裝Azure App Service Extension
  2. 使用語法npm run build產生實體檔(檔案預設放在dist資料夾)
  3. 使用滑鼠右鍵對選dist資料夾中的Deploy to Web App…
  4.  點選Sign in Azure
  5. 選擇登入Azure帳號
  6. 登入成功後選擇建立Azure App Service段落所建立App Service
  7. 點選Deploy以部署到 App Service
  8. 佈署成功可點選右下角的Browse WebSite查看(須過一段時間才能正常顯示)

    • FTP部署

  1. 使用語法npm run build產生實體檔(檔案預設放在dist資料夾)
  2. AzureApp Service頁面點選左側的Deployment center
  3.  點選FTP credentials就可以看到FTP的連線設定
  4. 使用FTP工具(.Filezilla)連線並將欲部署的檔案更新上去。

  •  Azure (CI/CD)
因CI/CD會被SourceControl的不同影響,待有實作時再回補說明


 













[React] 開發環境建立

  


依據以下步驟建立React開發環境,本篇文章使用的是vite來建立專案,而不是之前介紹的CRA。


先新建一個專案資料夾,並透過VS Code開啟


用Ctrl+`來開啟Terminal,並輸入以下Command

npm create vite@latest

再根據專案選項依序點選





接著用OpenFolder的方式開啟剛剛建立的專案資料夾


用Ctrl+`開啟Terminal,輸入以下指令安裝package.json內的相依套件

npm install

 安裝完成後,再輸入以下指令就可以偵錯網站

npm run dev


點擊網址,就可以看到網頁囉






2023年1月30日 星期一

[React]實作自定義的Component(密碼輸入框)

 


這篇主要說明如何開發自定義的Component,讓多個頁面都可同時使用,範例程式碼可參考這裡


最常見的自定義元件大概就是輸入框、讀取動畫或視窗...這類的。因為之前有實作過登入頁,那就以密碼輸入框來教大家如何實作。

因示範專案使用的是MUI,建議各位在開發任何元件前都可確認有沒有官方範例,有的話就可以省下大量的時間去實作。

而這功能確實有找到官方範例,那就可以直接抓程式碼下來使用!


首先先在src/components/text裡新增Password.tsx,並將官方範例貼上去。

Password.tsx

----------------------------------------------------------------------------------------------------------------

import { Visibility, VisibilityOff } from "@mui/icons-material";
import { IconButton, InputAdornment, TextField } from "@mui/material";
import { useState } from "react";
export default function Password(props: passwordProps) {
    //顯示密碼Flag
    const [showPassword, setShowPassword] = useState(false);
    //眼睛點選事件
    const handleClickShowPassword = () => setShowPassword((show) => !show);
    //取消預設點選事件
    const handleMouseDownPassword = (event: React.MouseEvent<HTMLButtonElement>) => {
        event.preventDefault();
    };
    return (
        <TextField
            error={props.ErrorMsg !== ''}
            margin="normal"
            required
            fullWidth
            name={props.Name}//"password"
            label={props.Label}//"Password"
            id={props.ID}//"password"
            autoComplete="current-password"
            helperText={props.ErrorMsg !== '' ? props.ErrorMsg : ""}
            InputProps={{
                type: showPassword ? 'text' : 'password',
                endAdornment:
                    <InputAdornment position="end" >
                        <IconButton
                            aria-label="toggle password visibility"
                            onClick={handleClickShowPassword}
                            onMouseDown={handleMouseDownPassword}
                            edge="end"
                        >
                            {showPassword ? <VisibilityOff /> : <Visibility />}
                        </IconButton>
                    </InputAdornment>
            }
            }
        />
    );
}
interface passwordProps {
    ID:string
    Name: string
    Label:string
    ErrorMsg?: string;
}

----------------------------------------------------------------------------------------------------------------


主要定義幾個屬性

ID:元件ID。
Name:元件Name,有包Form的可透過FormData.get({name})抓到值。
Label:元件Label。
ErrorMsg:顯示錯誤訊息。 

 

再將Login的原先密碼欄位改成使用<Password/>

Login.tsx

----------------------------------------------------------------------------------------------------------------

import Password from '../components/text/Password';

省略...

{/*原程式碼*/}
{/*<TextField*/}
{/*    error={LoginMsg !== ''}*/}
{/*    margin="normal"*/}
{/*    required*/}
{/*    fullWidth*/}
{/*    name="password"*/}
{/*    label={t('Login.Password')}*/}
{/*    type="password"*/}
{/*    id="password"*/}
{/*    autoComplete="current-password"*/}
{/*    helperText={LoginMsg !== '' ? LoginMsg:""}*/}
{/*/>*/}
 <Password ID="password" Name="password" Label={t('Login.Password')} ErrorMsg={LoginMsg} />

省略...

----------------------------------------------------------------------------------------------------------------


這樣就完成囉,未來如果有需要使用密碼欄位就可以同Login頁載入使用,下面Demo一下效果

初始為不顯示密碼

點選眼睛,就可以看到剛輸入的密碼。


看起來功能正常,這樣大家就可以開始來製作屬於自己的元件囉!


這篇功能感覺比前幾篇簡單許多,其實是各位在之前的教學都已經有作過了,讓大家猜猜看是什麼?


公布答案!其實各個頁面也是Component,只是載入的地方在APP.tsx內且透過React-Router去切換掉Content,所以如果有多個地方要顯示同樣的頁面,也可以使用此篇教的方法囉!















2023年1月4日 星期三

[React]Redux說明&導入

 

此篇主要說明Redux,範例程式碼可參考這裡


基本上Redux的功用就是管理狀態以及畫面與資料分開管理,與之前加上登入機制所使用的Context功能大同小異,優於Context的地方是在有多個state要管理時會優於React原生的Context。

p.s. Redux和Context API可混用,需頻繁操作的state可以用Redux,反之則用Context。

主要核心元件為redux和react-redux,redux是函式庫本體,react-redux則是React綁定使用套件。

Redux組成主要有三個元素

1.action

定義操作state方法,會傳入reducer。

2.reducer

保管State並針對傳入的action去對state做動作。

3.store

管理並整合reducer。

以下為Redux 的流程示意圖


接下來我們就直接用計數器範例來說明各元素的功能以及作用


開始前先用npm安裝套件語法如下

//安裝redux和react-redux

npm install --save redux react-redux


//安裝react-redux的Typescript套件

npm install --save --dev @types/react-redux


程式碼部分我們先建立reducer部分,新增reducers資料夾和counter檔案(src/reducers/counter.ts)

counter.ts裡面有Action類別定義、Actions和reducer本體,程式碼如下


counter.ts

-------------------------------------------------------------------------------------------------------------------

//初始state
const initState = {
    count: 0
};

// #region 定義Action類別
//增加
const INCREMENT = 'INCREMENT';
//減少
const DECREMENT = 'DECREMENT';
// #endregion 

// #region 匯出Action
//增加
export const countPlus = { type: INCREMENT }
//減少
export const countMinus = { type: DECREMENT }
// #endregion

// #region reducer本體
const counterReducer = (state = initState, action: any) => {
    //依照action的類型作判斷
    switch (action.type) {
        case INCREMENT:
            return { count: state.count + 1 };
        case DECREMENT:
            return { count: state.count - 1 };
        default:
            return state;
    }
};
// #endergion 

export default counterReducer;

-------------------------------------------------------------------------------------------------------------------

簡單說明,定義以及匯出Action主要是集中管理狀態的處理,對後續維護以及測試有許多幫助。

而reducer本體則是依照傳入的action去操作state並回傳最新的State,使之更新view。


再來建立集中管理reducer的store,新增stores資料夾和configureStore檔案(src/stores/configureStore.ts)


configureStore.ts

-------------------------------------------------------------------------------------------------------------------

import { createStore, combineReducers } from 'redux';
import counterReducer from '../reducers/counter';
const rootReducer = combineReducers({
    counterReducer,
});
const store = createStore(rootReducer);
export default store;

-------------------------------------------------------------------------------------------------------------------

如果只有一個reducer的可以直接createStore,但預想未來專案可能會有多個reducer,所以中間多一個combineReducers,有新增時在填入。


再來要修改index.tsx讓所有Component可以使用,需要使用react-redux的Provider來包覆住APP。


//index.tsx(紅字部分為新增)

-------------------------------------------------------------------------------------------------------------------

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './i18n';
import { Provider } from 'react-redux';
import store from './stores/configureStore';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
    <React.StrictMode>
        <Provider store={store}>
            <App />
        </Provider>
  </React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals(console.log);

-------------------------------------------------------------------------------------------------------------------

透過Provider的store讓全系統都可使用Redux。


前面這樣定義就完成了,開始撰寫計數器,新增counter.tsx頁面

//counter.tsx

-------------------------------------------------------------------------------------------------------------------

import * as React from 'react';
import { useSelector,useDispatch } from 'react-redux';
import { countMinus, countPlus } from '../reducers/counter';
export function Counter() {
    
    //透過useDispatch來使用action
    const dispatch = useDispatch();
    //透過useSelector去取得State
    const count: number = useSelector(state => (state as any).counterReducer.count);
    return (
        <React.Fragment>
            <h1>Counter</h1>
            <h3> Redux</h3>
            <p aria-live="polite">Current count: <strong>{count}</strong></p>
            <button style={{ margin: '5px' }}
                type="button"
                className="btn btn-primary btn-lg"
                onClick={() => { dispatch(countPlus); }}>
                Increment
            </button>
            <button style={{ margin: '5px' }}
                type="button"
                className="btn btn-primary btn-lg"
                onClick={() => { dispatch(countMinus); }}>
                Decrement
            </button>
        </React.Fragment>
    );
}

-------------------------------------------------------------------------------------------------------------------

說明一下程式碼,

操作部分使用useDispatch來取得dispatch,在按鈕時在使用dispatch傳入定義好的action操作state。

而畫面更新部分則是使用useSelector取代useState,如果state更新則有使用到useSelector的Component也會更新。


實際就可以來Run看看畫面,看起來是可以正常加減。



p.s.因為是全域的state,所以要注意如果切頁再回來值不會變回0。


這樣就完成了資料與畫面分離的部分,對於未來的維護上應該也就簡單許多,建議大家還是要使用Redux。


一樣在結尾也說明一下其實還有更多的應用沒有一起說明,例如操作資料庫或是用redux-logger來記錄每次操作state...等等,後續如果有用到時再分享給大家,謝謝!










2022年12月20日 星期二

[React]用 react-i18next 實現多國語言

此篇主要說明如何讓網站實現多國語言,範例程式碼可參考這裡


主要核心元件為react-i18next,透過設定的方式來實現多國語言的切換,想要知道更多的內容可以參考官網


那麼就教學開始!

首先需要先安裝i18Nextreact-i18Nexti18Next-http-backendi18Next-borwser-languagedetector

指令碼↓↓↓↓↓↓↓

npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector --save


再來看一下最後完工的檔案結構,紅色是要新增的部分

src
├─locales
│  ├─en
│  │  └─translation.json
│  └─zh
│    └─translation.json
│ 
├─app.tsx 
├─index.tsx (要import等等設定好的i18n) 
├─package.json
└─i18n.ts

先新增語言檔,分成en和zh(中文都只用繁體中文,所以不用在加-Tw)


---------------locales/en/translation.json------------------
{
  "SwitchLan": "Switch Language",
  "Login": {
    "SignIn": "Sign in",
    "Account": "Account",
    "Password": "Password",
    "Remember": "Remember me",
    "ForgetPassword": "Forgot password?"
  }
}

---------------locales/zh/translation.json------------------
{
  "SwitchLan": "切換語言",
  "Login": {
    "SignIn": "登入",
    "Account": "帳號",
    "Password": "密碼",
    "Remember": "記住我",
    "ForgetPassword": "忘記密碼?"
  }
}


再來新增i18n.ts(i18n設定檔)

--------------------------i18n.ts------------------------------
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import translationEN from './locales/en/translation.json';
import translationZH from './locales/zh-tw/translation.json';

const resources = {
    en: {
        translation:translationEN,
    },
    zh: {
        translation:translationZH
    }
};

i18n
    // load translation using http -> see /public/locales
    // learn more: https://github.com/i18next/i18next-http-backend
    .use(Backend)
    // detect user language
    // learn more: https://github.com/i18next/i18next-browser-languageDetector
    .use(LanguageDetector)
    // pass the i18n instance to react-i18next.
    .use(initReactI18next)
    // init i18next
    // for all options read: https://www.i18next.com/overview/configuration-options
    .init({
        resources,
        fallbackLng: 'en',
        lng:"zh",
        debug: true,
        interpolation: {
            escapeValue: false, // not needed for react as it escapes by default
        },
    });

export default i18n;
----------------------------------------------------------------

打開index.tsx,把i18n設定檔import進去(紅字為修改部分)


--------------------------index.tsx------------------------------
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './i18n';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

...
-------------------------------------------------------------------

這樣設定就完成了,再來就拿Login頁面來實作,我們使用的是useTranslation(Hook)方式

簡單來說就是使用const{t}=useTranslation()後在用t('Key')去抓取對應的文字
p.s. 如果要多層結構以.隔開,例如t('login.key')

下方紅字為登入頁異動部分(有...代表被省略,請勿直接複製貼上)


--------------------------Login.tsx------------------------------

...
import UserContext from '../contexts/UserContext';
import { useTranslation } from 'react-i18next';
...

export default function Login() {
    //登入訊息
    const [LoginMsg, setLoginMsg] = useState<string>('');
    //登入資訊Context
    const userContext:any = useContext(UserContext)
    //登入確認
    const handleSubmit = ...
    //i18n
    const { t,i18n } = useTranslation();

 return (
        <ThemeProvider theme={theme}>
            <Grid container component="main" sx={{ height: '100vh' }}>
                <CssBaseline />
                <Grid
                    item
                    xs={false}
                    sm={4}
                    md={7}
                    sx={{
                        backgroundImage: 'url(https://source.unsplash.com/random)',
                        backgroundRepeat: 'no-repeat',
                        backgroundColor: (t) =>
                            t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900],
                        backgroundSize: 'cover',
                        backgroundPosition: 'center',
                    }}
                />
                <Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
                    <Box
                        sx={{
                            my: 8,
                            mx: 4,
                            display: 'flex',
                            flexDirection: 'column',
                            alignItems: 'center',
                        }}
                    >
                        <Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
                            <LockOutlinedIcon />
                        </Avatar>
                        <Typography component="h1" variant="h5">
                            {t('Login.SignIn')}
                        </Typography>
                        <Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 1 }}>
                            <TextField
                                error={LoginMsg !== ''}
                                margin="normal"
                                required
                                fullWidth
                                id="account"
                                label={t('Login.Account')}
                                name="account"
                                autoFocus
                            />
                            <TextField
                                error={LoginMsg !== ''}
                                margin="normal"
                                required
                                fullWidth
                                name="password"
                                label={t('Login.Password')}
                                type="password"
                                id="password"
                                autoComplete="current-password"
                                helperText={LoginMsg !== '' ? LoginMsg:""}
                            />
                            <FormControlLabel
                                control={<Checkbox  value="remember" color="primary" />}
                                label={t('Login.Remember')}
                                name="remember"
                            />
                            <Button
                                type="submit"
                                fullWidth
                                variant="contained"
                                sx={{ mt: 3, mb: 2 }}
                            >
                                {t('Login.SignIn')}
                            </Button>
                            <Grid container>
                                <Grid item xs>
                                    <Link href="#" variant="body2">
                                        {t('Login.ForgetPassword')}
                                    </Link>
                                </Grid>
                                <Grid item>
                                    <Link href="#" variant="body2" onClick={() => { i18n.changeLanguage(i18n.language=="en"?"zh":"en") }}>
                                        {t('SwitchLan')}
                                    </Link>
                                </Grid>
                            </Grid>
                            <Copyright sx={{ mt: 5 }} />
                        </Box>
                    </Box>
                </Grid>
            </Grid>
        </ThemeProvider>
    );


--------------------------------------------------------------------


這樣登入頁就有多國語系了,這次在按鈕的右下有加入一個連結來實現切換功能。

因預設zh,所以會先顯示中文


點選"切換語言",就會變成英文頁面。


這樣就完成了多國語言的實作,只要在需要的頁面使用useTransaltion()就可以使用

而且i18next還有許多優點以及功能,例如自動偵測瀏覽器語言(需載入i18next-browser-languagedetector)、下次進入時會抓取上次的語言設定等等,未來如果有更多實作會在持續更新。






















2022年12月16日 星期五

[React & .Net Core]環境佈署

 


此篇主要說明如何佈署至測試以及正式環境,範例程式碼可參考這裡


此次佈署的教學先以Windows IIS以及Azure App Services來做教學,且需要搭配Visual Studio的publish功能,使用Scripts的方式就後續有使用VS Code開發時再做說明。

1.Window IIS

前言:

因有些WebForm、WebService等等舊專案仍會用到,所以現有客戶的測試或正式環境都會是Windows Server,但未來如果升級.Net Core就可不設限。

環境準備:

(1)安裝.Net 6 Runtime(傳送門)

(2)安裝Node.js(傳送門,版本選擇當下建議的下載即可)


佈署步驟:

先對專案點右鍵,再點"Publish..."


點選Folder方式(如果有權限可以直接連到IIS,也可選擇WebServer)

再來輸入發佈位置(也可以直接輸入網路磁碟路徑),先用預設就好,再點選"Finish"


就將發佈檔設定好了,實體檔案會在Properties/PublishProfiles內,現在只要點選"Publish"就可發佈至設定的資料夾內,檔案結構如圖二

圖一

圖二

接下來就將這些檔案放到IIS站台對應的資料夾內就OK囉。


2.Azure App Service

前言:

雲端的服務越來越完善且簡單設置,當然要有雲端的教學阿!


環境準備:

1.Azure 帳號開一個App Service(建立步驟可直接上網查,有很多教學資源)


佈署方式可以分成兩種,分別是手動佈署和自動佈署:

而手動佈署也可以分成兩種,一種是用VS直接發佈至Azure,另一種是用FTP去上傳實體檔案。

(1)VS直接部屬至Azure


一樣使用VS對專案右鍵點選後在點選"publish"

如果原本已經有佈署過則會先顯示上次佈署的設定,,再來點選"+New"。

Target請選擇"Azure"
Specific target請選擇Azure App Service(.NetFramework只能選擇Windows,.Net則不限制)


登入Azure帳戶,登入後就可以顯示出Azure上已有的AppService。


Deployment Type選擇Publish,點選Finish就完成Publish的設定。


完成可以看到一些發佈的設定,再點選Publish按鈕就可以發佈至Azure上。



(2)用FTP上傳實體佈署檔


前面步驟與IIS發佈類似,只是要先發佈至Local。


打開Azure App Service的Deployment Center,再選擇FTPS credentials。

頁面上可以看到FTP的一些相關資訊,使用FileZilla連線到Azure資料夾。


之後將剛剛發佈到Local檔案透過FileZilla上傳到wwwroot,就成功佈署了。

p.s.以上的手動佈署都有可能遇到佈署時會出現"檔案使用中"的錯誤訊息,這時可以先在資料夾內新增一個檔名是app_offline.htm的檔案,這樣就會強制關掉站台,待更新後再將此檔案Rename或Remove站台就會啟動了。


而自動佈署則是會用Azure的CI功能。(前提是要用它可以選到的版控,內部Gitlab不行)

此範例是用Github來教學。

打開Azure的Deployment Center,在Setting頁籤的Source點選Github。



登入Github,填入Github的相關設定


上面設定的Branch會針對設定的Branch去偵測Commit,只要有Commit就會自動佈署。
(正式環境建議拆成獨立分支,例如Release)

設定完之後,在Github的Action頁籤就可以看到一個yml檔案


試著Commit&push,就可以看到有一個workflow在run

點進去就可以看到目前CI的流程以及正在執行的步驟。

過一陣子就可以看到執行完成囉。


點選deploy網址就可以看到成功佈署於Azure上了(未來再也不用手動更版了!!!!!)
也可以查看Azure 的Deplotmeny Center的Logs看到發佈紀錄




總結一下, 佈署方式還是要依照客戶需求去選擇,但CI/CD是未來不可不學的一部分,如果環境許可的話都一律建議使用自動佈署搭配雲端的方式。













[ASP.Net]GridView自訂分頁處理

  ASP.Net的GirdView在每次分頁處理時都會在重新抓取所有資料在進行分頁,遇到大量資料或是設計不良的情況下,在切換時頁面會卡住,造成使用者體驗不佳。 可以透過GirdView內建的參數以及SQL OFFSET語法來替代原先的分頁處理,以下為步驟說明 1.asp...