2022/07/04

認知無線電 Cognitive Radio, CR

1999年Joseph Mitola博士提出認知無線電 Cognitive Radio 的概念,最初定義為「一種能感知周圍環境並能適應環境的無線電」,CR具有學習能力,能與周圍環境交換資訊,感知和利用在該空間的可用頻譜,並限制和降低衝突。

認知無線電網路在認知過程中主要完成四個功能:頻譜感知、頻譜管理、頻譜共享和頻譜移動。Mitola的認知無線電是以軟體無線電爲平台,並讓軟體無線電智能化,強調有「學習」能力。

美國 FCC 提出了 CR 的簡化版本,建議任何具有動態根據環境改變發射參數的無線電,都可稱為 CR。無線電裝置能夠自動判斷頻道的狀態,並切換到可用的頻道。

兩個或多個具有認知無線電功能的無線電設備要建立一個通訊通道,必須先有一個同意採用什麼頻率、功率和調變機制的方法。一種技術是使用基地台在參與設備之間傳遞訊息。基地台只用於交換連線資料,然後認知無線電設備切換到協商一致的設定上。

在通話過程中,認知無線電設備必須連續監測通道和其它頻率,因為其它(非認知無線電)授權用戶可能突然啟動通話。他們比認知無線電設備的優先順序高,必須避免有害的干擾。在這種可能發生的情形中,認知無線電的作用是檢測授權用戶,建立並同意另一個閒置頻率,然後中斷當時的連接設定。

應用

  1. WRAN

    目的就是使用CR技術將分配給電視廣播的VHF/UHF頻帶(北美為54~862MHz)的頻率用作寬帶訪問線路,有效利用空閒頻道。IEEE 802.22 於2005年9月完成了對WRAN的功能需求和信道模型文檔,並於2006年3月形成了最終的合併提案作為編寫標準的基礎。

  2. UWB

    UWB技術產生於20世紀60年代,當時主要應用於脈衝雷達(ImpulseRadar),美國軍方利用其進行安全通信中的精確定位和成像。至20世紀90年代之前,UWB主要應用於軍事領域,之後UWB技術開始應用於民用領域。UWB由於具有傳輸速率高、系統容量大、抵抗多徑能力強、功耗低、成本低等優點,被認為是下一代無線通信的革命性技術,而且是未來多媒體寬帶無線通信中最具潛力的技術。

  3. WLAN

    IEEE802.11 標準為基礎的無線技術已經成為WLAN技術的主流,認知無線電技術能通過不斷掃描頻譜段,獲得這些可用頻道的環境和質量的認知資訊,自適應地接入較好的通訊頻道,這正是解決WLAN頻段擁擠問題的方法。

  4. Mesh

    無線Mesh網絡是近年來出現的具有一種無線多跳(Multi-hop)的網絡結構。在Mesh網絡中,每個節點可以和一個或者多個對等節點直接通信;同時也能模擬路由器的功能,從鄰近節點接收消息並進行中繼轉發。Mesh網絡通過鄰近節點之間的低功率傳輸取代了遠距離節點間的大功率傳輸,實現了低成本的通訊。網絡中所有節點之間是相互協作的,如果Mesh網絡中的一條鏈路失效了,網絡可以通過替代鏈路將信息路由到目的地,優化了頻譜的使用。

    認知無線電和無線Mesh結合,是在增大網絡密度和提高服務吞吐量的發展趨勢下提出來的,適用於可能有嚴重的線路競爭情況的人口密集城市的無線寬頻。認知Mesh網絡通過中繼方式可以有效地擴展網路覆蓋範圍,當一個無線Mesh網的骨幹網絡是由認知接入點和固定中繼點組成時,無線Mesh網的覆蓋範圍能夠大大增加。

  5. Ad-Hoc

    一般的 Ad-hoc網絡在發送資料前會先確定路由。認知無線電技術能夠即時收集資訊並向各方通知尚未使用的頻率信息,適用於具有不可提前預測的頻譜使用模式的應用場景。當認知無線電技術應用於低功耗多跳Ad-hoc網絡,能夠滿足分佈式認知用户之間的通信需求。基於認知無線電技術的Ad-hoc網絡需要新的支持分佈式頻率共享的MAC協定和路由協定。

DFS

802.11a工作於5GHz頻帶(在美國為U-NII頻段:5.15-5.25GHz、5.25-5.35GHz、5.725-5.825GHz),它採用OFDM(正交分頻多工)技術。802.11a支援的資料速率最高可達54Mbps。

但在歐洲軍方的雷達系統廣泛運用這一頻率(其中探測隱型飛機的雷達就使用這一頻率)。在歐洲出售的WLAN產品必須具備TPS和DFS這兩個功能,即發射功率控制和動態頻率選擇。TPS是為了防止無線產品發放過大的功率來干擾軍方雷達。DFS是為了使無線產品主動探測軍方使用的頻率,並主動選擇另一個頻率,以避開軍方頻率。通過這種方式,也可以避免其它WLAN干擾。

References

2022/06/27

安裝project-open

project-open 有提供 docker image,也可以直接安裝,以下記錄安裝 project-open 的過程。

CentOS7

參考網頁

http://www.project-open.com/en/install-rhel-7

  1. system tools

    timedatectl set-timezone Asia/Taipei
    
    yum -y install system-config-*
    
    yum -y group install "System Administration Tools" --setopt=group_package_types=mandatory,default,optional
  2. KDE Plasma Workspaces Install "KDE Plasma Workspaces" Graphical Environment GNOME 3 on CentOS 7 has a number of important issues, so the ]po[ team recommends to switch to KDE:

    #yum -y group install "KDE Plasma Workspaces" "X Window System"
    #yum -y group install "Graphical Administration Tools" --setopt=group_package_types=mandatory,default,optional
    yum -y install net-tools setools policycoreutils-python
    #ln -sf /lib/systemd/system/graphical.target /etc/systemd/system/default.target
  3. Development Tools

    yum -y group install "Development Tools" --setopt=group_package_types=mandatory,default,optional
    yum -y group install "Compatibility Libraries"
    yum -y install vim emacs-nox
    yum -y install cvs svn git wget libXaw expat expat-devel pango graphviz graphviz-devel ImageMagick
    yum -y install libdbi-dbd-pgsql openldap-clients openldap-devel mlocate sharutils psmisc
  4. Perl Libraries

    yum -y install graphviz-perl perl perl-Archive-Tar perl-Archive-Zip perl-CGI perl-CGI-Session
    yum -y install perl-CPAN perl-CPAN-Changes perl-CPAN-Meta perl-CPAN-Meta-Requirements perl-CPAN-Meta-YAML
    yum -y install perl-Carp perl-Compress-Raw-Bzip2 perl-Crypt-DES perl-Crypt-OpenSSL-RSA
    yum -y install perl-Crypt-OpenSSL-Random perl-Crypt-PasswdMD5 perl-Crypt-SSLeay perl-DBD-Pg
    yum -y install perl-DBD-Pg-tests perl-DBI perl-Data-Dumper perl-DateTime perl-Digest-MD5
    yum -y install perl-Encode perl-File-Slurp perl-GSSAPI perl-IO-Socket-IP perl-IO-Socket-SSL
    yum -y install perl-JSON perl-LDAP perl-LWP-MediaTypes perl-LWP-Protocol-https perl-Net-DNS
    yum -y install perl-Net-HTTP perl-Net-SSLeay perl-Params-Check perl-Params-Util perl-Params-Validate
    yum -y install perl-Socket perl-TimeDate perl-WWW-Curl perl-YAML perl-core perl-devel perl-gettext
    yum -y install perl-libs perl-libwww-perl rrdtool-perl perl-YAML
  5. OpenOffce/LibreOffice

    yum -y install libreoffice libreoffice-headless
  6. 將 projop 加入 wheel

    vi /etc/group
    
    wheel:x:10:projop
  7. 修改 hosts

    vi /etc/hosts
    
    127.0.0.1 localhost
    10.140.0.3 po
  8. download files

    wget http://sourceforge.net/projects/project-open/files/project-open/Support%20Files/naviserver-4.99.8.tgz 
    wget http://sourceforge.net/projects/project-open/files/project-open/Support%20Files/web_projop-aux-files.5.0.0.0.0.tgz 
    wget http://sourceforge.net/projects/project-open/files/project-open/V5.0/update/project-open-Update-5.0.3.0.0.tgz 
  9. po installer

    groupadd projop                                                              # create a group called "projop"
    mkdir /web/                                                                  # super-directory for all Web servers /web/ by default
    useradd -d /web/projop -g projop projop                                      # create user "projop" with home directory /web/projop
    cd /web/projop/
    tar xzf /usr/src/web_projop-aux-files.5.0.0.0.0.tgz                          # extract auxillary files
    tar xzf /usr/src/project-open-Update-5.0.3.0.0.tgz                           # extract the ]po[ product source code - latest
    chown -R projop:projop /web/projop                                           # set ownership to all files
    
    cd /usr/local
    tar xzf /usr/src/naviserver-4.99.8.tgz                                       # extract the NaviServer binary 64 bit
  10. Setup PostgreSQL 9.2

    yum -y install postgresql postgresql-server postgresql-contrib postgresql-devel postgresql-odbc postgresql-docs
    
    systemctl enable postgresql

    init PostgreSQL

    /usr/bin/postgresql-setup initdb
    systemctl start postgresql
  11. Database

    su - postgres -c "createuser -s projop"                             # database user "projop" with admin rights
    su - projop -c "createdb --encoding=utf8 --owner=projop projop"     # new database
    su - projop -c "createlang plpgsql projop"                          # enable PlPg/SQL, may already be installed
  12. verify

    su - projop -c psql
    
    # Enter "\q" or press Ctrl-D to exit

    匯入 DB sql

    # import db
    
    su - projop
    psql -f ~/pg_dump.5.0.3.0.0.sql > import.log 2>&1
    
    # verify
    
    psql -c "select count(*) from users"
  13. 修改 postgresql.conf

    vi /var/lib/pgsql/data/postgresql.conf
    
    listen_addresses = '*'
    max_connections = 100
    
    #shared_buffers = 512MB
    shared_buffers = 256MB
    
    work_mem = 64MB
    maintenance_work_mem = 16MB
    
    checkpoint_segments = 64
    
    log_timezone = 'Asia/Taipei'
    timezone = 'Asia/Taipei'
  14. update pg_hba.conf

    vi /var/lib/pgsql/data/pg_hba.conf
    
    local   all             all                                     peer
    host    all             all             127.0.0.1/32            trust
    host    all             all             ::1/128                 ident
  15. update po config

    vi /web/projop/etc/config.tcl
    
    set httpport            8000
    set httpsport           8443
    
    set servername   "maxkit \]project-open\[ Server"
    set homedir      /usr/local/ns
  16. manual startup

    /usr/local/ns/bin/nsd -f -t /web/projop/etc/config.tcl -u projop -g projop
  17. service

    vi /usr/lib/systemd/system/projop.service
    
    [Unit]
    Description=NaviServer Web Server as user projop
    After=postgresql.service network.target
    Wants=postgresql.service
    
    [Service]
    Type=forking
    PIDFile=/web/projop/log/nsd.pid
    
    ExecStartPre=/usr/bin/rm -f /web/projop/log/nsd.pid
    ExecStart=/usr/local/ns/bin/nsd -t /web/projop/etc/config.tcl -u projop -g projop &
    ExecReload=/bin/kill -s HUP $MAINPID
    ExecStop=/bin/kill -s 9 $MAINPID
    
    Restart=always
    
    # Restart=on-abort
    
    # Restart=on-abnormal
    
    KillMode=process
    
    [Install]
    WantedBy=multi-user.target
    # init service
    chmod 755 /usr/lib/systemd/system/projop.service
    systemctl daemon-reload
    systemctl enable projop.service
    systemctl start projop.service
    
    tail -f /web/projop/log/error.log
  18. nginx

    yum -y install epel-release
    yum -y install nginx
    vi /etc/nginx/nginx.conf
    
    user nginx;
    worker_processes auto;
    error_log /var/log/nginx/error.log;
    pid /run/nginx.pid;
    
    events {
       worker_connections 1024;
    }
    
    http {
       log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                         '$status $body_bytes_sent "$http_referer" '
                         '"$http_user_agent" "$http_x_forwarded_for"';
       access_log          /var/log/nginx/access.log  main;
       sendfile            on;
       tcp_nopush          on;
       tcp_nodelay         on;
       keepalive_timeout   601;
       types_hash_max_size 2048;
       default_type        application/octet-stream;
       include             /etc/nginx/mime.types;
       include             /etc/nginx/conf.d/*.conf;
    
       server {
           listen 80;
           location / {
               # pass all communication to NaviServer on port 8000
               proxy_pass           http://127.0.0.1:8000;
               # add information about the original IP
               proxy_set_header     X-Forwarded-For $remote_addr;
               # upload files to file storage up to 1G
               client_max_body_size 1024M;
           }
    
           # error_page    500 502 503 504 /err/50x.html;
           # error_page    404             /err/404.html;
           # location /err/ {
           #     root /usr/share/nginx/html;
           # }
    
       }
    
    }

    先用 port 80 瀏覽一次網頁

    cat /var/log/audit/audit.log | grep nginx | grep denied | audit2allow -M mynginx
    semodule -i mynginx.pp

Add Swap to CentOS 7

ref: How to Add Swap Space on CentOS 7 - Google Cloud

```
# 1G

dd if=/dev/zero of=/swapfile bs=1024 count=1048576

# 2G

dd if=/dev/zero of=/swapfile bs=1024 count=2097152

# 4G

dd if=/dev/zero of=/swapfile bs=1024 count=4194304

chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

swapon --show

```

```
vi /etc/fstab

/swapfile swap swap defaults 0 0

```

Docker

ref: http://www.project-open.com/en/install-docker-centos7

  1. CentOS 7安裝 docker

    yum install -y yum-utils device-mapper-persistent-data lvm2
    yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    yum install docker-ce
    
    systemctl enable docker
    systemctl start docker
  2. 下載 project-open

    mkdir -p download/project-open
    
    cd download/project-open
    
    wget --no-check-certificate https://sourceforge.net/projects/project-open/files/project-open/V5.0/older/project-open-Docker-Community-5.0.2.4.0.beta5.zip/download -O project-open-Docker-Community-5.0.2.4.0.beta5.zip
    
    unzip project-open-Docker-Community-5.0.2.4.0.beta5.zip
    cd po5-centos7
  3. 修改 setup line 24

    if ! wget --no-check-certificate -nv "$url" ; then
  4. 安裝

    ./setup
    ./build
    
    docker images
    
    REPOSITORY TAG IMAGE ID CREATED SIZE
    local/po5-centos7 latest acc4736e6a0c 7 minutes ago 1.81GB
  5. 啟動

    ./run
    
    docker start po5_centos7
    docker update --restart unless-stopped po5_centos7

2022/06/13

C# Thread

Thread 相關類別

有三個相關的類別:Thread, ThreadStart, ParameterizedThreadStart

  • ThreadStart

    宣告的 function會被 thread 執行,ThreadStart 負責建立無參數的委派函式

  • ParameterizedThreadStart

    宣告的 function會被 thread 執行,ThreadStart 負責建立有參數的委派函式

  • Thread

    建立、控制、管理 Thread,常用的屬性:

    屬性 說明 回傳資料型別
    CurrentThread 目前正在執行的 thread Thread
    IsAlive thread 的執行狀態 Boolean
    IsBackground thread 是否為背景執行緒 Boolean
    ManagedThreadId thread 的識別號碼 Integer
    Name thread 的名稱 String 或 null
    ThreadState thread 的狀態 ThreadState (enum)

    常用 method

    method 說明
    Abort() 停止 thread,停止後,無法重新啟動
    BeginCriticalRegion() 設定 Critical region
    EndCriticalRegion() 結束 Critical region
    Interrupt() 中斷 WaitSleepJoin 狀態的 thread
    Join([a]) 封鎖執行緒直到停止執行為止,a 為 integer (ms) 或 TimeSpan
    ResetAbort() 取消正在要求的 Abort()
    Sleep(a) 暫停執行緒 a (ms) 或 TimeSpan
    Start([a]) 啟動 thread,a為委派函式的參數,object 型別

    Note: 使用 Abort() 不保證一定能停止 thread,有可能會發生 exception。ex: SecurityException: 沒有權限停止 thread,ThreadStateException: 停止已暫停的 thread

  • ThreadState (enum)

    位於 System.Threading namespace

    列舉常數 value 說明
    Aborted 256 執行緒目前無作用,但狀態尚未變更為 Stopped
    AbortRequested 128 已收到 Abort()
    Background 4 背景執行
    Running 0 正在執行
    Stopped 16 已停止
    StopRequested 1 已被要求停止中
    Suspended 64 已暫停
    SuspendedRequested 2 已被要求暫停中
    Unstarted 8 還沒開始執行,未被呼叫 Start()
    WaitSleepJoin 32 已被封鎖

    列出呼叫哪個 method 會導致狀態改變

    method state
    建立 thread Unstarted
    Start() Running
    Sleep() WaitSleepJoin
    對另一個物件呼叫 Monitor.Wait() WaitSleepJoin
    Join WaitSleepJoin
    Interrupt() Runing
    Suspend() SuspendedRequested
    回應 Suspend() Suspended
    Resume() Running
    Abort() AbortRequested
    回應 Abort() Stopped
    thread 已終止 Stopped

建立 thread

  1. 建立委派的 method

  2. 使用 ThreadStart 建立委派物件

  3. 使用委派物件建立 Thread 型別的 Thread 物件

  4. Start()

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        bool fgDone;
        Int32 sum;
        int guess;

        public Form1()
        {
            InitializeComponent();
        }

        void count()
        {            
            while (sum < int.MaxValue)
                sum++;

            fgDone = true;
        }

        void count_param(object num)
        {
            Random rd = new Random();

            while(guess!=(int)num)
            {
                guess = rd.Next(1, 101);
                Thread.Sleep(100);
            }                     

            fgDone = true;
        }

        // 沒有用 thread,計算過程中,視窗會卡住,無法使用
        private void Button1_Click(object sender, EventArgs e)
        {          
            textBox1.AppendText("開始計算\r\n");
            sum = 0;

            while (sum < int.MaxValue)
                sum++;

            textBox1.AppendText("計算完畢,sum= " + 
                sum.ToString()+"\r\n");
        }

        // 用 ThreadStart 產生 Thread
        private void button2_Click(object sender, EventArgs e)
        {
            ThreadStart thdStart = new ThreadStart(count);
            Thread thd = new Thread(thdStart);

            sum = 0;
            fgDone = false;
            textBox1.AppendText("執行緒開始執行\r\n");

            thd.Start();
            timer1.Enabled = true;
        }

        // 用 ParameterizedThreadStart 產生 Thread
        private void button3_Click(object sender, EventArgs e)
        {
            int num = 78;
            ParameterizedThreadStart paramStart = 
                new ParameterizedThreadStart(count_param);
            Thread thd = new Thread(paramStart);

            fgDone = false;
            guess = -1;
            thd.Start(num);
            timer2.Enabled = true;
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (fgDone)
            {
                timer1.Enabled = false;
                textBox1.AppendText("計算完畢,sum= " +
                    sum.ToString()+"\r\n");
            }
        }

        private void timer2_Tick(object sender, EventArgs e)
        {
            textBox1.AppendText(guess.ToString() + "\r\n");
            if (fgDone)
            {
                timer2.Enabled = false;
                textBox1.AppendText("找到了\r\n");
            }
        }
    }
}

取得 thread 執行結果

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        Int32 sum;

        public Form1()
        {
            InitializeComponent();
        }

        void count()
        {
            while (sum < int.MaxValue)
                sum++;           
        }

        void count_param(object param)
        {
            Int32[] pp = (Int32[])param;

            while (pp[0] < int.MaxValue)
                pp[0]++;           
        }

        // 透過 sum 全域變數,儲存 thread 的執行結果
        private void button1_Click(object sender, EventArgs e)
        {
            Thread thd = new Thread(count);

            sum = 0;
            textBox1.AppendText("執行緒開始執行\r\n");

            thd.Start();
            textBox1.AppendText("使用Join()方法,"+"" +
                "所以必須等待執行緒執行結束...\r\n");
            thd.Join();
            textBox1.AppendText("sum=" + sum.ToString() + 
                "\r\n");            
        }

        // 透過傳入 thread 的參數,儲存 thread 的執行結果
        // 因為該參數是 array,傳入是 call by value
        // 會將 sum1[0] 的記憶體位址傳給該 method
        private void button2_Click(object sender, EventArgs e)
        {
            Thread thd = new Thread(count_param);
            Int32 []sum1 = { 0 };

            textBox1.AppendText("執行緒開始執行\r\n");
            thd.Start(sum1);
            textBox1.AppendText("使用Join()方法," + "" +
                "所以必須等待執行緒執行結束...\r\n");
            thd.Join();
            textBox1.AppendText("sum1=" + sum1[0].ToString() + 
                "\r\n");
        }
    }
}

thread 生命週期

Thread 提供 Abort(), Join(), Interrupt(), Sleep(), Suspend(), Resume(),其中 Suspend(), Resume() 已經建議不要使用。

Join() 會等待 thread 結束,呼叫 Join() 的 thread 會持續等待無法回應。

結束 Thread 的方法,是讓該 Thread 完工後自行結束,如果是持續工作的 Thread,就要用 Abort(),或是用全域變數判斷要不要繼續執行。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        bool fgDone_a, fgDone_b;
        int guess_a, guess_b;
        Thread th_a=null;
        bool fgRun;
        int num_a = 78, num_b=50;

        public Form1()
        {
            InitializeComponent();
        }

        // 亂數產生數字,直到該數字為 78
        // 結束時,設定 fgDone_a,並停止 timer1
        void count_a()
        {
            Random rd = new Random();
            try
            {
                while (guess_a != num_a)
                {
                    guess_a = rd.Next(1, 101);
                    Thread.Sleep(100);
                }

                fgDone_a = true;
            }
            catch (ThreadAbortException ex)
            {
                timer1.Enabled = false;
            }
            catch (ThreadInterruptedException ex)
            {
                timer1.Enabled = false;
            }
        }

        void count_b()
        {
            Random rd = new Random();

            while (guess_b != num_b && fgRun)
            {
                guess_b = rd.Next(1, 101);
                Thread.Sleep(100);
            }

            fgDone_b = true;
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            textBox1.AppendText(guess_a.ToString() + "\r\n");
            if (fgDone_a)
            {
                timer1.Enabled = false;
                textBox1.AppendText("找到了\r\n");
            }
        }

        // 用 count_a 產生 Thread th_a
        // 同時啟動 timer1,timer1 會呼叫 timer1_Tick
        private void button1_Click(object sender, EventArgs e)
        {
            Thread thd = new Thread(count_a);

            th_a = thd;
            fgDone_a = false;
            guess_a = -1;
            thd.Start();
            timer1.Enabled = true;
        }

        // 呼叫 th_a.Abort()
        private void button2_Click(object sender, EventArgs e)
        {
            textBox1.AppendText("使用Abort()中止執行緒\r\n");
            th_a.Abort();
        }

        // 呼叫 th_a.Interrupt()
        private void button3_Click(object sender, EventArgs e)
        {
            textBox1.AppendText("Interrupt()中斷執行緒\r\n");
            th_a.Interrupt();
        }

        // 用 count_b 產生 Thread th_b
        // 將全域變數 fgRun 設定為 true
        // 同時啟動 timer2,timer2 會呼叫 timer2_Tick
        private void button4_Click(object sender, EventArgs e)
        {
            Thread thd = new Thread(count_b);

            fgDone_b = false;
            guess_b = -1;
            fgRun = true;
            thd.Start();
            timer2.Enabled = true;
        }

        // 將全域變數 fgRun 設定為 false
        private void button5_Click(object sender, EventArgs e)
        {
            fgRun = false;
        }

        private void timer2_Tick(object sender, EventArgs e)
        {
            textBox2.AppendText(guess_b.ToString() + "\r\n");
            if (fgDone_b)
            {
                timer2.Enabled = false;
                if(!fgRun)
                    textBox2.AppendText("結束執行緒\r\n");
                else
                    textBox2.AppendText("找到了\r\n");
            }
        }

        // 關閉 Form1,要停止 th_a,fgRun 設定為 false
        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if(th_a!=null)
                th_a.Abort();

            fgRun = false;
        }        
    }
}

Thead 存取表單控制項

Form UI 控制項是由 UI Thread 控制,如果自己產生的 Thread 要跨到 UI thread 存取控制項,會發生錯誤。

前面的例子,是在 Thread 裡面計算後,將過程記錄在全域變數中,然後在 UI Thread 以 Timer 定時將資料顯示在 UI 上,這種方法比較麻煩。

透過控制項的 InvokeRequired 屬性及 Invoke(),可讓 Thread 直接存取控制項。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {        
        delegate void SafeCall(string str);
        Thread []myThreads= { null, null };
        int sum = 0;

        public Form1()
        {
            InitializeComponent();
        }

        void safeControl() //無參數
        {
            if (textBox1.InvokeRequired)
            {
                sum++;
                if (sum > int.MaxValue)
                    sum = 0;
                MethodInvoker ivk = new MethodInvoker(safeControl);
                textBox1.Invoke(ivk, new object[] { });
            }
            else
                textBox1.AppendText("myThreads[1]: " + 
                    sum.ToString() + "\r\n");
        }

        void myFunc()
        {
            while (true)
            {                    
               safeControl();
               Thread.Sleep(500);
            }          
        }

        // 用 InvokeRequired 判斷 textBox1 存取權
        // 如果是 true,就表示現在是 UI thread 在控制
        // 就要由 textBox1 呼叫一次 SafeCall
        // false 就表示為外部 thread 存取,可直接使用 textBox1
        void safeControl_param(string str) //有參數
        {
            if (textBox1.InvokeRequired)
            {
                SafeCall ivk = new SafeCall(safeControl_param);
                textBox1.Invoke(ivk, new object[] { str });
            }
            else
                textBox1.AppendText(str + "\r\n");
        }

        // myThreads[0] 的 method
        // 呼叫 safeControl_param
        void myFunc_param()
        {
            Random rd = new Random();
            int num;

            while (true)
            {
                num = rd.Next(1, 101);
                safeControl_param("myThreads[0]: " + num.ToString());
                Thread.Sleep(500);
            }                                
        }

        // 用 button1 產生 myThreads[0]
        private void button1_Click(object sender, EventArgs e)
        {
            Thread thd;

           // 如果 myThreads[0] 已經存在,就要停止 myThreads[0]
           // 用 myFunc_param 重新產生一個 myThreads[0],並啟動
           if (myThreads[0] != null)
            {
                myThreads[0].Abort();
                myThreads[0].Join();
            }
            thd = new Thread(new ThreadStart(myFunc_param));
            myThreads[0] = thd;

            thd.Start();
        }

        // 用 button2 產生 myThreads[1]
        private void button2_Click(object sender, EventArgs e)
        {
            Thread thd;

            if (myThreads[1] != null)
            {
                myThreads[1].Abort();
                myThreads[1].Join();
            }
            thd = new Thread(new ThreadStart(myFunc));
            myThreads[1] = thd;

            thd.Start();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            foreach(var item in myThreads)
                if (item != null)
                    item.Abort();           
        }        
    }
}

另一種寫法

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        Thread[] myThreads = { null, null };
        int sum = 0;

        public Form1()
        {
            InitializeComponent();
        }

        void safeControl_param(string str) //有參數
        {
            textBox1.Invoke(new Action (() =>
            {
               textBox1.AppendText(str);
            }
            ));
        }

        // this.Invoke 配合 new Action 
        void myFunc_param()
        {
            Random rd = new Random();
            int num;

            while (true)
            {
                num = rd.Next(1, 101);

                //也能寫成函式來來呼叫:safeControl_param(num.ToString());
                this.Invoke(new Action(() =>
                {
                    textBox1.AppendText("myThreads[0]: "+num.ToString()+"\r\n");
                }
             ));

            Thread.Sleep(500);
            }
        }

        void safeControl() //無參數
        {
            sum++;
            if (sum > int.MaxValue)
                sum = 0;

            this.Invoke((MethodInvoker)delegate 
            { textBox1.AppendText( "myThreads[1]: " +
                    sum.ToString() + "\r\n"); }
            );
        }

        void myFunc()
        {
            while (true)
            {
                ////此段程式也能寫成函式來來呼叫:safeControl();
                sum++;
                if (sum > int.MaxValue)
                    sum = 0;

                this.Invoke((MethodInvoker)delegate
                {
                    textBox1.AppendText("myThreads[1]: " +
                          sum.ToString() + "\r\n");
                }
                );

                Thread.Sleep(500);
            }
        }


        // 有參數的 myFunc_param Thread
        private void button1_Click(object sender, EventArgs e)
        {
            Thread thd;

            if (myThreads[0] != null)
            {
                myThreads[0].Abort();
                myThreads[0].Join();
            }
            thd = new Thread(new ThreadStart(myFunc_param));
            myThreads[0] = thd;

            thd.Start();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            foreach (var item in myThreads)
                if (item != null)
                    item.Abort();
        }

        //  沒有參數的 myFunc Thread
        private void button2_Click(object sender, EventArgs e)
        {
            Thread thd;

            if (myThreads[1] != null)
            {
                myThreads[1].Abort();
                myThreads[1].Join();
            }
            thd = new Thread(new ThreadStart(myFunc));
            myThreads[1] = thd;

            thd.Start();
        }
    }
}

第三種寫法

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        delegate void SafeCall(string str);
        Thread []myThread= { null, null };
        int sum;

        public Form1()
        {
            InitializeComponent();
        }

        void myFunc2()
        {
            MethodInvoker ivk = new MethodInvoker(safeControl);

            while (true)
            {
                sum++;
                if (sum > int.MaxValue)
                    sum = 0;
                this.Invoke(ivk, new object[] { });
                Thread.Sleep(500);
            }
        }

        void safeControl() //無參數
        {
            textBox1.AppendText(sum.ToString() + "\r\n");
            label1.Text = sum.ToString() ;

        }

        void myFunc1()
        {
            SafeCall ivk = new SafeCall(safeControl_param);
            Random rd = new Random();
            int num;

            while (true)
            {
                num = rd.Next(1, 101);
                this.Invoke(ivk, new Object[] { num.ToString()+"\r\n" });
                Thread.Sleep(500);
            }
        }

        private void safeControl_param(string str)
        {
            textBox1.AppendText(str);
            label1.Text = str;

        }

        private void button1_Click(object sender, EventArgs e)
        {
            Thread thd = new Thread(myFunc1);
            myThread[0] = thd;

            thd.Start();

        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            foreach (var item in myThread)
                if (item != null)
                    item.Abort();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Thread thd = new Thread(myFunc2);
            myThread[1] = thd;

            sum = 0;
            thd.Start();
        }
    }
}

執行緒同步

如果有多個 thread 會同時存取相同的資源,會造成內容一致性的問題。有四個方法,可處理 synchronization 問題

  • synchronized code region -> 最常用

    當 thread A 執行時,thread B 必須等待 A 完成後,才能執行該區塊

  • manual synchronization

    使用 .Net Framework 的 Mutex, Semaphore, EventWaitHandle, AutoResetEvent, ManualResetEvent

  • synchronized contexts

    使用 SynchronizationAttribute 設定 ContextBoundObject

    當物件進入或離開由 ContextBoundObject 定義的內容時,會強制執行規則

  • 使用 System.Collections.Concurrent 的集合與類別

Critical Section: thread 存取的共用資源(物件、資料、變數、設備),同一時間只有一個 thread 能使用

lock()

互斥鎖: public object locker = new object();

lock(locker) 以 locker 鎖定 critical section

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        Thread thd1=null, thd2=null;
        object locker = new object();
        bool fg1 = false, fg2=false;

        public Form1()
        {
            InitializeComponent();
        }

        // 當 Form1 載入時,就啟動兩個 thread
        private void Form1_Load(object sender, EventArgs e)
        {
            thd1 = new Thread(func1);
            thd2 = new Thread(func2);

            thd1.Start();
            thd2.Start();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (thd1 != null)
                thd1.Abort();

            if (thd2 != null)
                thd2.Abort();
        }

        // 無窮迴圈
        // 當 fg1 為 true,就 lock locker 物件,執行 critical section 區塊
        // 完成後將 fg1 改為 false
        void func1()
        {
            while(true)
            {
                if(fg1)
                    lock(locker)
                    {
                        for(int i=0;i<10;i++)
                        {
                            textBox1.Invoke(new Action(() =>
                            {
                                textBox1.AppendText("Thread 1\r\n");
                            }));
                            Thread.Sleep(500);
                        }
                        fg1 = false;
                    }
            }
        }

        void func2()
        {
            while (true)
            {               
                if (fg2)
                    lock (locker)
                    {
                        textBox1.Invoke(new Action(() =>
                        {
                            textBox1.AppendText("Thread 2\r\n");
                        }));
                        fg2 = false;
                    }
            }        
        }

        // 將 fg1 改為 true
        private void button1_Click(object sender, EventArgs e)
        {                        
            if (!fg1)
                fg1 = true;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (!fg2)
                fg2 = true;
        }     
    }
}

Monitor

method 說明
Enter(a, [b]) 取得並鎖定互斥鎖 a,a為 Object,b 為 Boolean。當 b 為 true,表示已經取得互斥鎖 a,否則為 false
Exit(a) 釋放互斥鎖 a
IsEntered(a) 判斷 thread 是否已經取得互斥鎖 a
Pulse(a) 通知等候的 thread,互斥鎖 a 已改變狀態
PulseAll(a) 通知等候 queu 的所有 threads,互斥鎖 a 已改變狀態
TryEnter(a,b,c) 嘗試在時間 b 取得互斥鎖 a,並回傳結果 c。
a: Object, b: Int32 or TimeSpan, c: Boolean
TryEnter(a[,b]) 嘗試在時間 b 取得互斥鎖 a。
a: Object, b: Int32 or TimeSpan
TryEnter(a[,b]) 嘗試取得互斥鎖 a,並回傳結果 b。
a: Object, b: Boolean
Wait(a[,b[,c]]) 釋放互斥鎖 a ,並在時間 b 內,嘗試重新取得互斥鎖 a。如果無法取得,就會進入等候 queue
a: Object, b: Int32 or TimeSpan, c: Boolean

Enter, Exit 要搭配使用,否則會造成 a 無法釋放或 deadlock

Pulse() 只能被正在鎖定互斥鎖的 thread 呼叫

critical section

Monitor.Enter(locker);
.
.
.
Monitor.Exit(locker);
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        Thread thd1 = null, thd2 = null;
        object locker = new object();
        bool fg1 = false, fg2 = false;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            thd1 = new Thread(func1);
            thd2 = new Thread(func2);

            thd1.Start();
            thd2.Start();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (thd1 != null)
                thd1.Abort();

            if (thd2 != null)
                thd2.Abort();
        }

        void func1()
        {
            while (true)
            {
                if (fg1)                    
                {
                    Monitor.Enter(locker);
                    for (int i = 0; i < 10; i++)
                    {
                        textBox1.Invoke(new Action(() =>
                        {
                                textBox1.AppendText("Thread 1\r\n");
                        }));
                        Thread.Sleep(500);
                    }
                    Monitor.Exit(locker);
                    fg1 = false;
                }
            }
        }

        void func2()
        {
            while (true)
            {
                if (fg2)                    
                {
                    Monitor.Enter(locker);
                    textBox1.Invoke(new Action(() =>
                    {
                        textBox1.AppendText("Thread 2\r\n");
                    }));
                    Monitor.Exit(locker);
                    fg2 = false;
                }
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (!fg1)
                fg1 = true;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (!fg2)
                fg2 = true;
        }
    }
}

Semaphore

5 人去店裡繳費,但只有 3 個櫃檯

一般來說臨櫃劉成為:取號碼牌、有空的櫃檯時叫號、沒有空的櫃檯就等待

Semaphore 提供機制協調多個 thread 的同步處理

// 有三個號誌數量,最多可接受3個 thread 要求號誌 
Semaphore smphore = new Semaphore(3,3);


// 有0個號誌數量,最多可接受3個 thread 要求號誌
// 一開始,所有 thread 都在等待 
Semaphore smphore = new Semaphore(0,3);

// 釋放 3 個號誌
smphore.Release(3);

// 沒有參數,表示釋放先前取得的號誌
smphore.Release();
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        Semaphore smphore;
        Thread[] thds=new Thread[5];
        delegate void SafeCall(string str);

        public Form1()
        {
            InitializeComponent();
        }

        void safeControl(string str)
        {
            textBox1.AppendText(str);
        }

        void func(object param)
        {
            int no = (int)param;
            string str;
            SafeCall ivk = new SafeCall(safeControl);

            str = String.Format("第{0}位在排隊...\r\n", no);
            textBox1.Invoke(ivk, new Object[] { str});

            // 所有 threads 會停在這裡等待 semaphore
            smphore.WaitOne();

            str = String.Format("第{0}位正在繳費...\r\n", no);
            textBox1.Invoke(ivk, new Object[] { str });
            Thread.Sleep(1000);

            str = String.Format("第{0}位繳費結束...\r\n", no);
            textBox1.Invoke(ivk, new Object[] { str });
            smphore.Release();
            // 結束時,釋放 semaphore
        }        

        private void Form1_Load(object sender, EventArgs e)
        {
            // 載入時,產生 semaphore
            smphore = new Semaphore(0, 3);
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 產生並啟動 5 個 threads
            for (int i = 0; i < thds.Length; i++)
                thds[i] = new Thread(
                    new ParameterizedThreadStart(func));

            for (int i = 0; i < thds.Length; i++)
                thds[i].Start(i + 1);

            textBox1.AppendText("尚未開始營業...\r\n");
            Thread.Sleep(3000);

            textBox1.AppendText("開始營業...\r\n");
            smphore.Release(3);
        }
    }
}

References

博客來-C#程式設計從入門到專業(下):職場C#進階應用技術

2022/06/06

acmesh

因原本使用的 Let's Encrypt 提供的 certbot,在舊版的 CentOS 7 會遇到 python 及 kernel 更新的問題,現在改用 GitHub - acmesh-official/acme.sh: A pure Unix shell script implementing ACME client protocol 做免費的 SSL 憑證。

安裝,會安裝到 /root/.acme.sh

curl https://get.acme.sh | sh -s email=charley@maxkit.com.tw

設定自動更新 acme.sh

acme.sh --upgrade --auto-upgrade

在 crontab 可發現自動更新的 script

# crontab -e
59 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

申請 zone.larzio.com 的憑證

acme.sh --issue -d zone.larzio.com --webroot /var/www/html/

申請成功後,憑證相關檔案會在

  • 憑證 /root/.acme.sh/zone.larzio.com/zone.larzio.com.cer

  • key /root/.acme.sh/zone.larzio.com/zone.larzio.com.key

  • intermediate ca cert /root/.acme.sh/zone.larzio.com/ca.cer

  • full chain certs /root/.acme.sh/zone.larzio.com/fullchain.cer

將憑證複製給 haproxy 使用: /etc/haproxy/ssl/server.pem

acme.sh --installcert -d zone.larzio.com \
 --cert-file /etc/haproxy/ssl/zone.larzio.com.cer \
 --key-file /etc/haproxy/ssl/zone.larzio.com.key \
 --fullchain-file /etc/haproxy/ssl/zone.larzio.com.fullchain.cer \
 --reloadcmd "cat /etc/haproxy/ssl/zone.larzio.com.cer /etc/haproxy/ssl/zone.larzio.com.key | tee /etc/haproxy/ssl/server.pem"

References

HAProxy 實現 h2 到 h2c 的解析 - 台部落

Centos7 下使用acme.sh以DNS方式申請免費SSL證書 - 台部落

2022/05/30

RSocket + WebSocket

Reactive Programming

這是一種以資料流為主的程式開發模型,目前比較流行的 Vue, React 也是類似這個概念,畫面上顯示的運算結果或呈現的資料,會隨時跟著資料異動而自動更新。

傳統的程式設計是用 Interative Programming 方法,例如

b:=2;
c:=3;
a:=b+c
// a 為 5
b:=3;
// 當 b 的值異動後,必須重新運算一次 b+c,才能更新 a

Excel 就是使用 Reactive Programming,當在某個儲存格 C1 填寫為 C1=A1+B1,C1 的值,就會隨著 A1 與 B1 異動而自動更新。

RSocket

rsocket 是類似 gRPC 的訊息傳遞協定,支援 TCP, WebSocket, Aeron(UDP),主要有四種互動的方式

Interaction Model Behavior
fire-and-forget 不需要 response
request-and-response one-to-one 傳統通訊,一個 request,一個response,然後不斷重複
request-response-stream one-to-many 發一個訊息,可連續收到多個 response
channel many-to-many bi-directional stream

Sample

ref: RSocket + WebSocket + Spring Boot = Real Time Application

這個網頁用 spring boot 做了stream 及 channel 兩個 sample server

client 部分是用 rsocket-js,透過 nodejs 啟動

Server

用 Java Sprint Boot 撰寫

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.vinsguru</groupId>
    <artifactId>rsocket-websocket</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rsocket-websocket</name>
    <description>Demo project for Spring Boot, RSocket WebSocket</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-rsocket</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

resources/application.properties

spring.rsocket.server.port=6565
spring.rsocket.server.mapping-path=/rsocket
spring.rsocket.server.transport=websocket

RsocketWebsocketApplication

package com.vinsguru.rsocketwebsocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RsocketWebsocketApplication {

    public static void main(String[] args) {
        SpringApplication.run(RsocketWebsocketApplication.class, args);
    }

}

RSocketController

package com.vinsguru.rsocketwebsocket.controller;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;

import java.time.Duration;

@Controller
public class RSocketController {

    @MessageMapping("number.stream")
    public Flux<Integer> responseStream(Integer number) {
        return Flux.range(1, number)
                .delayElements(Duration.ofSeconds(1));
    }

    @MessageMapping("number.channel")
    public Flux<Long> biDirectionalStream(Flux<Long> numberFlux) {
        return numberFlux
                .map(n -> n * n)
                .onErrorReturn(-1L);
    }

}
  1. number.stream 就是 request-response-stream,client 會發送一個 Integer 給 Server,Server 會定時回傳 1, 2 ..... 直到該 Integer 停止
  2. number.channel 是 many-to-many 的傳輸,可連續發送多個 Long 給 Server,Server 會回傳該數的平方

Client

最主要的是 index.js

有兩個部分,分別是 number.stream 與 number.channel

import { RSocketClient, JsonSerializer, IdentitySerializer } from 'rsocket-core';
import RSocketWebSocketClient from 'rsocket-websocket-client';
import {FlowableProcessor} from 'rsocket-flowable';

// backend ws endpoint
const wsURL = 'ws://localhost:6565/rsocket';

// rsocket client
const client = new RSocketClient({
    serializers: {
        data: JsonSerializer,
        metadata: IdentitySerializer
    },
    setup: {
        keepAlive: 60000,
        lifetime: 180000,
        dataMimeType: 'application/json',
        metadataMimeType: 'message/x.rsocket.routing.v0',
    },
    transport: new RSocketWebSocketClient({
        url: wsURL
    })
});
// error handler
const errorHanlder = (e) => console.log(e);

// response handler
const responseHanlder = (payload) => {
    const li = document.createElement('li');
    li.innerText = payload.data;
    li.classList.add('list-group-item', 'small')
    document.getElementById('result').appendChild(li);
}

/////////
//  number.stream
////////
/*

const numberRequester = (socket, value) => {
    socket.requestStream({
        data: value,
        metadata: String.fromCharCode('number.stream'.length) + 'number.stream'
    }).subscribe({
        onError: errorHanlder,
        onNext: responseHanlder,
        onSubscribe: subscription => {
            subscription.request(100); // set it to some max value
        }
    })
}

client.connect().then(sock => {
    document.getElementById('n').addEventListener('change', ({srcElement}) => {
        numberRequester(sock, parseInt(srcElement.value));
    })
}, errorHanlder);
*/

/////////
//  number.channel
////////
// reactive stream processor
const processor = new FlowableProcessor(sub => {});

const numberRequester = (socket, processor) => {
    socket.requestChannel(processor.map(i => {
        return {
            data: i,
            metadata: String.fromCharCode('number.channel'.length) + 'number.channel'
        }
    })).subscribe({
        onError: errorHanlder,
        onNext: responseHanlder,
        onSubscribe: subscription => {
            subscription.request(100); // set it to some max value
        }
    })
}

client.connect().then(sock => {
    numberRequester(sock, processor);
    document.getElementById('n').addEventListener('keyup', ({srcElement}) => {
        if(srcElement.value.length > 0){
            processor.onNext(parseInt(srcElement.value))
        }
    })
}, errorHanlder);

安裝 node_modules

npm install

設定

npm run build

啟動 client

npm run serve

問題

一直找不到如何單獨使用 rsocket-js 的方法,就是不透過 nodejs 提供 網頁 service 的方法。現在很多 js library 只有用在 nodejs 的 sample,但傳統的網頁,應該只要透過 webserver 直接載入 javascript file,就可以使用才對。如果可以這樣用,就可以將 html, js 合併到 sping boot 裡面,也不需要兩個 service port。

References

RSocket 革命,為了 Reactive Programming 而生的高效率通訊協定

RSocket With Spring Boot

2022/05/23

Annotations in Spring Boot

Spring 在 2.5 以後,不用集中式的 xml 設定檔,改用 annotation 定義,傳統的 XML 設定冗長,改用 annotation 這個方法後有好有壞,這讓開發者專注在程式開發,但會有不知道為什麼這樣設定就能運作的錯覺。

以下是 Spring Application 能看到的 annotations

Core Spring Framework

@Autowired

可用在 fields, setter methods, constructors,能夠直接 inject object dependency

fields: 透過 property name 自動設定 field value

public class Customer {
    @Autowired                               
    private Person person;                   
    private int type;
}

setter: 告訴 Spring 要在 init bean 時,呼叫 setter method

public class Customer {                                                                                         
    private Person person;
    @Autowired                                                                                                      
    public void setPerson(Person person) {
       this.person=person;
    }
}

constructor: 產生物件時,做 injection,只能有一個 constructor 帶有 @Autowired annotation

@Component
public class Customer {
    private Person person;
    @Autowired
    public Customer (Person person) {
        this.person=person;
    }
}

@Qualifier

跟 @Autowired 一起使用,當該 @Bean 有多個 instance 時,可透過 @Qualifier 指定 instance

如果有兩個 BeanInterface 的 instances

@Component
public class BeanB1 implements BeanInterface {
    //
}
@Component
public class BeanB2 implements BeanInterface {
    //
}

透過 @Qualifier("beanB2") 指定 beanB2

@Component
public class BeanA {
    @Autowired
    @Qualifier("beanB2")
    private IBean dependency;
    ...
}

@Configuration

類似以往的 XML config file (applicationContext.xml),用來設定 Spring IoC beans

ex:

@Configuartion
public class DataConfig {
    @Bean
    public DataSource source() {
        DataSource source = new OracleDataSource();
        source.setURL();
        source.setUser();
        return source;
    }
    @Bean
    public PlatformTransactionManager manager() {
        PlatformTransactionManager manager = new BasicDataSourceTransactionManager();
        manager.setDataSource(source());
        return manager;
    }
}

@SpringBootApplication 本身就包含了 @Configuartion,可直接在裡面使用 @Bean

ex:

@SpringBootApplication
public class SpringBootBeanDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootBeanDemoApplication.class, args);
    }

    @Bean
    public FooService fooService() {
        return new FooService();
    }

}

@ComponentScan

跟著 @Configuration 一起使用

告訴 Spring 去哪一個 package 掃描 annotated components

@ComponentScan("com.test.config")
@Configuration
public class AppConfig {
}

可以指定 basePackageClasses 或 basePackages,取代直接設定 package。這樣可避免 class package 異動時,還要修改 @ComponentScan 的 package name

@ComponentScan(basePackageClasses = ApplicationController.class)
@Configuration
public class AppConfig {
}

@Bean

只能用在 method,跟 @Configuration 一起使用,告訴 Spring 產生哪些 bean

@Configuration
public class AppConfig {
    @Bean
    public Person person() {
        return new Person(address());
    }
    @Bean
    public Address address() {
        return new Address();
    }
}

@Lazy

用在 component class

autowired dependencies 預設會在啟動時,自動 created & configured

加上 @Lazy,表示該 bean 會在第一次被使用時,才被 created

@Lazy 也可以用在 @Configuration classes,表示所有的 @Bean 都會被延遲建立

@Value

用在 field, constructor parameter, method parameter

表示該 field/parameter 的初始值

通常 Spring 會自動載入 application.properties 的設定,如果是自訂 properties file,要用 @PropertySource 指定

ex:

#application.properties
user.name=admin

#my.properties
user.password=pwd123
@PropertySource("classpath:my.properties")
public class ValueController {
    @Value("${user.name}")
    private String name;

    @Value("${user.password}")
    private String password;
}

也可以指定 List, Array

tools=car,train,airplane
@Value("${tools}")
private String[] toolArray;

@Value("${tools}")
private List<String> toolList;

Spring Framework Stereotype Annotations

@Component

用在 class,代表一個 Spring component

@Component 可讓 Java class 在 component scanning 時被放入 application context

@Controller

代表 Spring controller

用在 Spring MVC 或 Spring WebFlux 的 controller

@Service

用在 class,讓 java class 提供某個 service,ex: 執行 business logic、計算、呼叫外部 API,這是一種用在 service layer 特殊的 @Component

@Repository

用在 Java class 可直接存取 DB,也就是 Data Access Object 的角色

有 auto translation 功能,ex: 發生 exception 時,會有 handler 處理該 exception,不需要加上 try-catch block

Spring Boot Annotations

@EnableAutoConfiguration

用在 main application class

會自動定義一個 base "search package",告訴 Spring Boot,根據 classpath 設定, perperties 設定,加入beans

會檢查所有 sub-packages & class

@Configuration
@EnableAutoConfiguration
public class EmployeeApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(EmployeeApplication.class, args);
        // ...
    }
}

排除 JdbcTemplateAutoConfiguration.class

@Configuration
@EnableAutoConfiguration(exclude={JdbcTemplateAutoConfiguration.class})
public class EmployeeApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(EmployeeApplication.class, args);
        // ...
    }
}

@SpringBootApplication

用在 application class,設定 Spring Boot project

會自動掃描 sub-packages,ex:

會掃描 com.example 的所有 sub-packages

package com.example;

@SpringBootApplication
public class EmployeeApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(EmployeeApplication.class, args);
        // ...
    }
}

使用 @SpringBootApplication 就等於使用了

  • @Configuration
  • @EnableAutoConfiguration
  • @ComponentScan

Spring MVC and REST Annotations

@Controller

會自動偵測 classpath 的 component classes,並自動註冊 bean definitions

如果要偵測 annotated controllers,必須加入 component scanning 到 configuration。

@Controller 可處理多個 request mappings

@Controller 用在 Spring MVC 及 Spring WebFlux

@RequestMapping

可用在 class 及 method

用來 map web request 到某個 class / handler methods

用在 class level 時,會產生該 controler 的 base URI,然後就能使用所有 handler methods

如果要限制 HTTP method,可在 handler method 上加上 @RequestMapping,ex:

只接受 GET method request 發送到 /welcome

@Controller
@RequestMapping("/welcome")
public class WelcomeController {
    @RequestMapping(method = RequestMethod.GET)
    public String welcomeAll() {
        return "welcome all";
    }
}

用在 Spring MVC 及 Spring WebFlux

@CookieValue

用在 method parameter level

用在 request mapping method 的參數,透過 cookie name 綁定

ex:

當有個 http request 夾帶 JSESSIONID 的 cookie,可透過 @CookieValue 取得 JSESSIONID 的值

JSESSIONID=418AB76CD83EF94U85YD34W
@ReuestMapping("/cookieValue")
    public void getCookieValue(@CookieValue "JSESSIONID" String cookie){
}

@CrossOrigin

用在 class 及 method level,可 enable cross-origin requests

如果可讓 js 從不同 host 取得資料,就要用 @CrossOrigin 啟用 cross-origin resource sharing

@CrossOrigin 預設允許所有 origins, headers,maxAge 為 30 min

ex:

getMessage 與 getNot 都會是 maxAge of 3600 seconds

getMessage 只允許來自 http://example.com 的 request

getNote 允許所有 hosts

@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin(origins = "http://example.com")
    @RequestMapping("/message")
    public Message getMessage() {
        // ...
    }

    @RequestMapping("/note")
    public Note getNote() {
        // ...
    }
}

Composed @RequestMapping Variants

用在 Spring MVC 及 WebFlux

@GetMapping

只接受 Get method request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.GET)

@PostMapping

只接受 Postmethod request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.POST)

@PutMapping

只接受 PUT method request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.PUT)

@PatchMapping

只接受 Patch method request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.PATCH)

@DeleteMapping

只接受 DELETE method request

跟以下 @RequestMapping 意義一樣

@RequestMapping(method = RequestMethod.DELETE)

@ExceptionHandler

處理 controller level 的 exception,定義該 class 要 catch 的 exception class

@ExceptionHandler 的 value 可以是 array of Expception types

@InitBinder

method-level

識別用來初始化 WebDataBinder 的 methods

WebDataBinder 是可將 request parameter 綁定 JavaBean objects 的 DataBinder

可在 controller 使用 @InitBinder annotated method

@InitBinder annotated methods 會在每一次 http request 被呼叫。用來驗證參數是否符合規則

ex: 如果有一個 request form 是 Student 資料

先產生 Student Java Bean

public class Student{
 private String Id;
 private String firstName;

 @NotNull(message="is required")
 @Size(min=1,message="is required")
 private String lastName; // validation done to check if lastname is NULL

 @Max(value=10,message="Value should between 0 and 10")
 @Min(value=0,message="Value should between 0 and 10")
 private String standard;
 private String Age;
}

用 initBinder 註冊 StringTrimmerEditor

@InitBinder
public void initBinder(WebDataBinder dataBinder) {
  StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
    dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);
}

@Mappings and @Mapping

用在 fields

@Mappings 是 web mapping annotation,可對應 source field 到 target field

@MaxtrixVariable

用在 request handler method arguments

可 inject relevant bits of a maxtri URI

maxtrix variables 會以 semicolon 區隔

如果 URL 包含 matrix variables,request mapping pattern 會以 URI template 表示

http://localhost:8080/spring/employees/John;beginContactNumber=22001

用以下 method

@RequestMapping(value = "/employees/{name}", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<List<Employee>> getEmployeeByNameAndBeginContactNumber(
  @PathVariable String name, @MatrixVariable String beginContactNumber) {
    List<Employee> employeesList = new ArrayList<Employee>();
    ...
    return new ResponseEntity<>(employeesList, HttpStatus.OK);
}

URI

http://localhost:8080/spring/employees/id=1;name=John;contactNumber=2200112334
@GetMapping("employees/{employee}")
@ResponseBody
public ResponseEntity<Map<String, String>> getEmployeeData(
  @MatrixVariable Map<String, String> matrixVars) {
    return new ResponseEntity<>(matrixVars, HttpStatus.OK);
}

@PathVariable

用在 method arguments

處理 dynamic changes in URI

可以用 regular expression

ex:

@GetMapping("/api/employees/{id}/{name}")
@ResponseBody
public String getEmployeesByIdAndName(@PathVariable String id, @PathVariable String name) {
    return "ID: " + id + ", name: " + name;
}

@RequestAttribute

綁定 request attribute 到 handler method paratmeter

ex: 計算 visit counter

Interceptor 增加 request attribute

public class MyCounterInterceptor extends HandlerInterceptorAdapter {
  private AtomicInteger counter = new AtomicInteger(0);

  @Override
  public boolean preHandle (HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) throws Exception {

      request.setAttribute("visitorCounter", counter.incrementAndGet());
      return true;
  }
}

controller

@Controller
public class ExampleController {

  @RequestMapping("/")
  @ResponseBody
  public String handle (@RequestAttribute("visitorCounter") Integer counter) {
      return String.format("Visitor number: %d", counter);
  }
}

JavaConfig

@EnableWebMvc
@ComponentScan("com.example")
public class AppConfig extends WebMvcConfigurerAdapter {

  @Override
  public void addInterceptors (InterceptorRegistry registry) {
      registry.addInterceptor(new MyCounterInterceptor());
  }
}

ref: https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/request-attribute.html

@RequestBody

一般用於處理非 Content-Type: application/x-www-form-urlencoded 編碼格式的資料,例如:application/json、application/xml

@RequestMapping(path = "/something", method = RequestMethod.PUT)
public void handle(@RequestBody String body, Writer writer) throws IOException {
    writer.write(body);
}

@ResponseStatus(value = HttpStatus.OK)
@PostMapping(value="/user", consumes = MediaType.APPLICATION_JSON_VALUE)
public void process2(@RequestBody User user) {
  logger.info("User: {}", user);
}

@RequestHeader

將 request header 裡面的 name 對應到 handler method parameter

@RequestParam

在 controller 中,跟 @RequestMapping 一起使用,綁定 request parameter 到 method parameter

@RequestPart

取代 @RequestParam

可取得特定 multipart 的 content 到 method parameter

Content-type 必須要是 multipart

@ResponseBody

用在 handler methods,類似 @RequestBody

spring 會將物件透過 HttpMessageConverter 轉換為 JSON/XML 寫入response body

@ResponseStatus

用在 methods, exception class,指定回傳的 status code

@ControllerAdvice

class level

可定義@ExceptionHandler, @InitBinder, and @ModelAttribute methods ,就會套用在所有 @RequestMapping methods

如果在 @ControllerAdvice class 的某個 method 加上 @ExceptionHandler,會被套用在所有 controllers

@RestController

將 class 標記為 controller

使用 @RestController 就不需要在所有 RequestMapping methods 加上 @ResponseBody,這表示不會在 response 回傳 html

@RestControllerAdvice

等同 @ControllerAdvice 與 @ResponseBody,再加上 @ExceptionHandler 處理 exceptions

@SessionAttribute

method parameter level

綁定 method parameter to a session attribute

@SessionAttributes

type level

要跟 @ModelAttribute 一起使用

可以將 JavaBean object 增加到 session 中

@ModelAttribute("person")
public Person getPerson() {}
// within the same controller as above snippet
@Controller
@SeesionAttributes(value = "person", types = {
    Person.class
})
public class PersonController {}

Spring Cloud Annotations

@EnableConfigServer

class level

當 project 有多個 services,需要集中設定所有 services

centralized config server 的優點是不需要知道 components 在哪裏

@EnableEurekaServer

用在 class

使用 microservices 時,很難知道每一個 service 相關的 addresses,必須要有 service discovery 方法

Netflix’s Eureka 是 discovery server 的實作

@EnableDiscoveryClient

用在 java class

通知哪一個 application 要註冊到 Eureka,會將 host, port 註冊到 Eureka

@EnableCircuitBreaker

用在 java class

circuit breaker pattern 讓 microservice 在相關 service fails 還能持續運作,避免 error 擴散

讓 service 有 recover 的時間

@HystrixCommand

method level

Netflix’s Hystrix library 是 Circuit Breaker pattern 的實作

當 method 套用 circuit breaker,Hystrix 會監控 failure of the method。當錯誤發生,Hystrix 會打開 circuit,後續的呼叫會 fail,Hystrix 會 redirect calls 到這個 method

ex:

@Service
public class BookService {
    private final RestTemplate restTemplate;
    public BookService(RestTemplate rest) {
        this.restTemplate = rest;
    }
    @HystrixCommand(fallbackMethod = "newList") public String bookList() {
        URI uri = URI.create("http://localhost:8081/recommended");
        return this.restTemplate.getForObject(uri, String.class);
    }
    public String newList() {
        return "Cloud native Java";
    }
}

Spring Framework DataAccess Annotations

@Transactional

放在 interface 定義, interface 的 method, class 定義, class 的 public method 前面

@Transactional 只是一個 metadata,讓 beans 設定 tranactional behavior

Cache-Based Annotations

@Cacheable

用在 methods

可將 return 的資料以 addresses 儲存到 cache

@Cacheable("addresses")
public String getAddress(Book book){...}

每次該 method 被呼叫時,會先檢查 cache

@CachePut

當要 update cache 時,可用 @CachePut

@CachePut("addresses")
public String getAddress(Book book){...}

不建議將 @CachePut, @Cacheable 合併使用

@CacheEvict

用在 method

可將 cache 清空,allEntries 就是清除所有 values

@CacheEvict(value="addresses", allEntries="true")
public String getAddress(Book book){...}

@CacheConfig

class level

可儲存 cache configuration

Task Execution and Scheduling Annotations

@Scheduled

method level

該 method 必須回傳 void,且不能有任何參數

// 等 5s 後開始執行,會等前一次結束後再跑第二次
@Scheduled(fixedDelay=5000)
public void doSomething() {
    // something that should execute periodically   
}

// 每 5s 執行一次,不管前一次有沒有跑完
@Scheduled(fixedRate=5000)
public void doSomething2() { 
    // something that should execute periodically 
}

@Scheduled(initialDelay=1000,fixedRate=5000)
public void doSomething3() { 
   // something that should execute periodically after an initial delay  
}

@Async

method level

每個 method 都在不同 thread 執行

@Async 如果有 return value,必須要是 Future-typed

Spring Framework Testing Annotations

@BootstrapWith

class level

設定 Spring TestContext Framework 如何啟動

@ContextConfiguration

class level

宣告要載入 context 的 class,及設定的 xml

@ContextConfiguration(locations={"example/test-context.xml", loader = Custom ContextLoader.class})

@WebAppConfiguration

class level

ApplicationContext 要載入的必須要是 WebApplicationContext

@WebAppConfiguration 可產生 web version of the application context

default root of web app 為 src/main/webapp

@Timed

method

該method 必須在限定時間內執行結束。超時就代表測試失敗

@Timed(millis=10000)
public void testLongRunningProcess() {  ... }

@Repeat

用在 test method

重複執行

@Repeat(10)
@Test
public void testProcessRepeatedly() {  ... }

@Commit

class level or method level

執行後,transaction of the transactional test method 會被 commit

@RollBack

class level or method level

@Rollback(true), the transaction is rolled back

@DirtiesContext

class, method level

表示 ApplicationContext 已被 modified / corrupted

@BeforeTransaction

在 @Transactional 前執行

@AfterTransaction

在 @Transactional 後

@Sql

run SQL scripts against a database

@SqlConfig

決定 parse and execute SQL scripts 的方法

@SqlGroup

method level

a container annotation that can hold several @Sql annotations

@SpringBootTest

start the Spring context for integration tests

@DataJpaTest

test Spring Data JPA using an in-memory database such as H2

@DataMongoTest

@WebMVCTest

a mock servlet context for testing the MVC layer

@AutoConfigureMockMVC

@MockBean

產生並 inject Mockito Mock

@JsonTest

限制測試處理 JSON 的 auto-configuration of Spring Boot 的 components

@TestPropertySource

class level

property sources for the test class

References

A Guide to Spring Framework Annotations

太厲害了!終於有人把Spring Boot常用註釋講明白了!

Top 10 Spring Framework Annotations for Java Programmers

Spring Annotation Note

2022/05/16

GraphQL with SpringBoot & MySQL

這是使用 SpringBoot 並將資料放在 MySQL 的 GraphQL sample

建立專案

Spring Initializer 產生 maven project,在 Dependencies 的部分增加

  • Lombok
  • MySQL Driver
  • Spring Data JPA
  • Spring Web

取得 project 後,再增加 Graphql dependencies

              <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-spring-boot-starter</artifactId>
            <version>5.0.2</version>
        </dependency>
        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java-tools</artifactId>
            <version>5.2.4</version>
        </dependency>

Data Model

因應 MySQL 的 Table,需要先產生 ORM 的 Data Model

Post.java

package com.example.writeup.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "USER")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "USER_ID")
    private Integer userId;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Column(name = "DOB")
    private Date dob;

    @Column(name = "ADDRESS")
    private String address;

    @Column(name = "POST_ID")
    private Integer postId;

    public User(String firstName, String lastName, Date dob, String address, Integer postId) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
        this.address = address;
        this.postId = postId;
    }
}

User.java

package com.example.writeup.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "USER")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "USER_ID")
    private Integer userId;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Column(name = "DOB")
    private Date dob;

    @Column(name = "ADDRESS")
    private String address;

    @Column(name = "POST_ID")
    private Integer postId;

    public User(String firstName, String lastName, Date dob, String address, Integer postId) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
        this.address = address;
        this.postId = postId;
    }
}

Repository

這是用來跟資料庫建立跟剛剛的 Data Model 的關聯

PostRepository.java

package com.example.writeup.repository;

import com.example.writeup.model.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post,Integer> {
}

UserRepository.java

package com.example.writeup.repository;

import com.example.writeup.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

    @Transactional
    @Modifying
    @Query(value = "UPDATE user SET address = ?1 WHERE user_id = ?2 ", nativeQuery = true)
    int updateUserAddress(String address, Integer user_id);

}

Note: 原文說可以改為繼承 CrudRepository,但 find 會回傳一般的 List,但 JpaRepository 會回傳 iterable list ref: What is difference between CrudRepository and JpaRepository interfaces in Spring Data JPA?

DataLoader

用在 init project 時,會自動產生 MySQL table 及測試資料

DataLoader.java

package com.example.writeup.service;

import com.example.writeup.model.Post;
import com.example.writeup.model.User;
import com.example.writeup.repository.PostRepository;
import com.example.writeup.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;

@Service
public class DataLoader {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PostRepository postRepository;

    @PostConstruct
    public void loadData(){

        User user1 = new User("Yasas" ,"Sandeepa",DataLoader.getRandomDate(),"Mount Pleasant Estate Galle",1);
        User user2 = new User("Sahan" ,"Rambukkna",DataLoader.getRandomDate(),"Delkanda Nugegoda",2);
        User user3 = new User("Ranuk" ,"Silva",DataLoader.getRandomDate(),"Yalawatta gampaha",3);

        Post post1 = new Post("Graphql with SpringBoot",DataLoader.getRandomDate());
        Post post2 = new Post("Flutter with Firebase",DataLoader.getRandomDate());
        Post post3 = new Post("Nodejs Authentication with JWT",DataLoader.getRandomDate());

        postRepository.save(post1);
        postRepository.save(post2);
        postRepository.save(post3);

        userRepository.save(user1);
        userRepository.save(user2);
        userRepository.save(user3);
    }

    public static Date getRandomDate(){
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, 1990);
        calendar.set(Calendar.MONTH, 1);
        calendar.set(Calendar.DATE, 2);
        Date date1 = calendar.getTime();
        calendar.set(Calendar.YEAR, 1996);
        Date date2 = calendar.getTime();
        long startMillis = date1.getTime();
        long endMillis = date2.getTime();
        long randomMillisSinceEpoch = ThreadLocalRandom
                .current()
                .nextLong(startMillis, endMillis);

        return new Date(randomMillisSinceEpoch);
    }
}

application.properties

產生設定檔

server.port=7000

#mysql properties
spring.jpa.generate-ddl=true
spring.datasource.url=jdbc:mysql://localhost/writeup
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create

#graphql properties
graphql.servlet.corsEnabled=true
graphql.servlet.mapping=/graphql
graphql.servlet.enabled=true

GraphQL

  • 一開始的 pom.xml 已增加了相關 dependencies
  • 剛剛的 application.properties 增加 graphql.servlet 的設定
  • 建立 GraphQL schema

schema.graphqls

schema {
    query: Query,
    mutation: Mutation,
}

type Query{
    # Fetch All Users
    getAllUsers:[User]

}

type Mutation {
    # Update the user address
    updateUserAddress(userId:Int,address:String): User
}

type User {
    userId : ID!,
    firstName :String,
    lastName :String,
    dob:String,
    address:String,
    postId : Int,
}
  • 產生 Service

UserService.java

package com.example.writeup.service;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.example.writeup.model.User;
import com.example.writeup.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService implements GraphQLQueryResolver, GraphQLMutationResolver {

    @Autowired
    private UserRepository userRepository;

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    public User updateUserAddress(Integer userId, String address) throws Exception {
        try {
            userRepository.updateUserAddress(address, userId);
            User user = userRepository.findById(userId).get();
            return user;
        } catch (Exception e) {
            throw new Exception(e);
        }
    }
}

WriteupApplication

package com.example.writeup;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WriteupApplication {

    public static void main(String[] args) {
        SpringApplication.run(WriteupApplication.class, args);
    }

}

Altair

這是 GraphQL 的測試工具,這邊安裝了 Chrome Extension 版本

網址為 http://localhost:7000/graphql/ ,點 Docs 可取得 Query 及 Mutation 兩個介面

References

[譯]使用 SpringBoot 和 MySQL 構建 GraphQL 服務端應用程式

Build a GraphQL Server With Spring Boot and MySQL

WriteUp github

Altair Altair GraphQL Client helps you debug GraphQL queries and implementations

2022/05/09

原生 vs 跨平台

現今軟體應用程式有四種使用者介面:終端機(Command Line Interface)、網頁、Mobile APP、Desktop APP。在為應用程式考量要提供哪一種使用者介面時,如果面對一般使用者,通常會以 網頁 -> Mobile -> Dekstop -> CLI 這樣的順序考慮。但每一種類型的使用者介面,又通常因為運作的 OS 平台不同而有不同的差異,導致開發的方法跟經驗,無法跨出熟悉的領域。

為了節省開發耗費的資源,跨平台成了開發者的首要考量,就如同 Java 一開始的宏願:Write Once, Run Everywhere!,這個目標,Java 並沒有取得全面的成功,但 Java 至少統一了過去數十年一部分的 Application 的開發。Java 在嵌入式環境、Mobile 行動平台、Desktop、網頁 Applet 直到現在都已經確認是失敗的。

失敗的重要原因在於效能及耗費的系統資源量,因為跨平台的要求,會需要將平台相關的功能包裝起來,再用另一個標準提供開發者介面,但這個封裝可能會造成系統資源的耗費,畢竟原生的函式庫會越貼近作業系統,資源的使用會比較少,效能也比較好。瀏覽器之間的差異相較於作業系統,差異是很小的,因此 Web 會是使用者介面的首選。

但為什麼還是會有跨平台的需求?畢竟不是每一個提供服務的廠商,都有足夠的工程師配額,能夠處理不同平台的開發。只要能夠節省成本,就永遠都會有跨平台的需求。

Web -> Mobile -> Dekstop -> CLI

一般使用者都能夠使用圖形介面 GUI 的應用程式,但終端機 Command Line Interface 就不同了,沒有經過 Linux OS 洗禮的使用者,通常無法適應一個字一個字敲打的指令列,通常也只有開發者才會去接觸 CLI 的程式。

在這幾種使用介面中,以網頁是最符合跨平台的開發方法,因為網頁的標準規格,各家瀏覽器都要遵循這些規格去開發,再加上有眾多框架與函式庫,解決了不同瀏覽器之間的些微差異問題,因此為應用提供網頁使用者介面,基本上是最常見的做法。

但實際上還是有很多應用程式,是沒辦法放在網頁裡面運作的,尤其是使用到 OS 原生的資源的應用。另外由於手機這種手持裝置的流行,人手一機,在面對一般使用者提供服務時,首先就會考慮提供手機 Mobile APP。

失敗經驗

過去曾經有使用過 Titanium 開發 iOS, Android 的失敗經驗,記得失敗的原因是 APP 只能做比較簡單的畫面,太複雜客製,非標準的 UI 元件,無法自由地透過 Titanium 產生,也無法產生具有不同平台特性的 UI 元件。

因此目前在 Mobile APP 比較常見的作法,是分開實作的,業界也分別需要 iOS 與 Android 的開發人員。也許當時我們是使用了一個封裝得不好的 Library 去開發,才導致失敗的結果。

不代表 iOS, Android 就一定要用原生開發,像 Game 的領域中,Unity 作為 Game Engine,也跨平台的 Game 開發做得很好。

Microsoft 也曾經想要統一 Desktop 與 Mobile 的使用者體驗,用 Tiles 做了 Windows Phone 及 Windows 10。現在大家已經知道,Windows Phone 已經完全在市場消失。而 Windows 11 的設計方式也已經向競爭對手看齊。

跨平台

是永久的需求及目標,成功與失敗的案例都有,但也都要經過嘗試,才能在一段時間後知道結果如何。

如果公司能夠負擔足夠的開發成本,或是專案運作的目標限制在某些平台上,當然就在不同平台去開發。要使用跨平台,如果能先用小的專案去測試,並先了解問題點,做先行測試,會是比較安全的作法。跨平台的效能跟資源耗費,必須要在可接受的範圍內。

另外也要考慮平台升級的問題,一但使用了跨平台的函式庫,就表示當核心升級後,會需要等待一段時間,才能取得跨平台函式庫的更新,這一段時間會是空窗期,遇到問題是完全無法解決的,也只能靜靜等待更新與修正。

References

2021 年加速開發的 8 個最佳跨平台框架

放弃坚持 15 年的原生开发,1Password 用 Electron 重写了全部代码

Top 10 Best Cross Platform App Development Frameworks in 2021

2022/04/25

電商營收數據指標

商品成交金額 GMV

商品成交金額(Gross Merchandise Volume,簡稱GMV

  • GMV = (來客數) 流量 × 購買轉換率 × 平均客單價

來客數、流量

可再區分裝置 (PC/Mobile/APP)、通路(自然流量/付費流量)

  • Unique Visitor

    不重複的來客數,實際上有多少訪客

  • Page View

    每一個頁面的瀏覽數量

  • Session

    使用者進入網站的次數,同一個使用者可能連接很多 Page。

  • MAU/WAU

    每月/週活躍用戶,檢視吸引用戶的能力

  • 下載量

    APP 下載數量

轉換率

從來店人流數量,轉換為真正購買的來客數量

  • CVR, Conversion Rate

    各頁面的轉換率

    從進入平台到結帳前,分析客戶是從哪個步驟跳開,分析產品的「訊息流」「任務流」

    訊息流 是從商品供給角度提供的商品內容與資訊,例如商品規格、評價、導購文章等

    任務流 是從用戶需求的角度去搜尋並找到他所需要的商品,例如搜尋篩選器、熱銷排行榜、推薦商品等

  • Bounce Rate

    CVR 的相反,了解是從哪裡跳開的

平均客單價 ABS、AOV

ABS (Average Basket Size) AOV (Average Order Value)

當客戶流量降低時,提升 ABS 是提升毛利的方法,常見的方法有:免運、折扣、跨銷、綁售

電商營運指標

流量指標

  • Session
  • Unique Visitor
  • Page View

轉化指標

  • CVR

用戶指標

  • 客單價 AOV

  • 用戶黏性

    • DAU (Daily Activited Users)

      日活躍用戶數

    • MAU (Monthly Activited Users)

      自統計之日算起一個月內登錄過APP的使用者總量

  • 用戶留存 Retention 回購

商品指標

  • 商品總數

    • SKU (Stock Keeping Unit)

      單品項管理、最小存貨單位

    • 庫存

  • 商品優勢

    • 個別商品轉化率&收入佔比、商品最低價比例

風險管控指標

  • 評價、投訴率、退貨率

拆解營收

  • 營收 = 來客數 * 購買轉換率 * 客單價

  • 營收 = 新客數 * 新客轉化率 * 新客的單價 + 舊客數 * 舊客回訪率 * 舊客轉換率 * 舊客單價

    區分新舊客戶數量

  • 營收 = 某某 channel 導流數 * 各自channel 轉化率 * 客單價

    區分網路流量來源,ex: EDM、LINE 官方帳號、搜尋流量、直接流量、網紅流量

  • 營收 = 品類一 * 銷售量 * 單價 + 品類二 * 銷售量 * 單價

    區分商品品項

  • 營收 = 通路一 * 銷售量 * 單價 + 通路二 * 銷售量 * 單價

    區分通路

  • 營收 = Campiagn 時期流量 * 轉化率 * 客單價 + 平常時期流量 * 轉化率 * 客單價

    區分週年慶時期

  • 營收 = 流量池導流數 * 轉化率 * 客單價 + 付費流量 * 轉化率 * 客單價 + 自然流量 * 轉化率 * 客單價

    LINE 官方帳號、APP 用戶,都被歸類在流量池

  • 營收 = 獲客數 * 回訪率 * 付費轉換率 * 付費頻率 * 客單價

    ex: 免費手遊

References

電商營收哪裡來?拆解各項重要數據指標

八種拆解營收的方法

電商 PM 都應了解的 5 大數據運營指標 -【數據乾貨大全】

電商人必備!68個常見電商專有名詞

2022/04/18

vuex

Vuex 是 state management pattern + library 工具,集中儲存所有 components,加上特定改變狀態的規則。

State Management Pattern

  • state: 目前 app 的狀態
  • view: 根據 state 產生的畫面
  • actions: 從 view 取得 user input,修改 state

如果有多個 components 共享 common state 會遇到的問題

  • multiple views 會由 the same piece of state 決定
  • 由不同的 views 產生的 actions,可改變 the same piece of state

Vuex 提出的方法是將 shared state 由 components 取出來,並用 global singleton 管理。

Vuex 可協助處理 shared state management,如果 app 很簡單,不是大型 SPA,就不需要 Vuex,只需要用 store pattern 即可

Store Pattern

如果有兩個 component 需要共享一個 state 時,可能會這樣寫

<div id="app-a">App A: {{ message }}</div>
<div id="app-b">App B: {{ message }}</div>

<script>
const { createApp, reactive } = Vue

const sourceOfTruth = reactive({
  message: 'Hello'
})

const appA = createApp({
  data() {
    return sourceOfTruth
  }
}).mount('#app-a')

const appB = createApp({
  data() {
    return sourceOfTruth
  },
  mounted() {
    sourceOfTruth.message = 'Goodbye' // both apps will render 'Goodbye' message now
  }
}).mount('#app-b')

</script>

畫面上兩個文字部分,都會變成 Goodbye

因為 sourceOfTruth 可以在程式中任意一個地方,被修改資料,當程式變多,會造成 debug 的難度。

這個問題就用 store pattern 處理。

store 類似 java 的 data object,透過 set method 修改資料內容,資料以 reactive 通知 Vue 處理異動。

<div id="app-a">{{sharedState.message}}</div>
<div id="app-b">{{sharedState.message}}</div>

<script>
const { createApp, reactive } = Vue

const store = {
  debug: true,

  state: reactive({
    message: 'Hello!'
  }),

  setMessageAction(newValue) {
    if (this.debug) {
      console.log('setMessageAction triggered with', newValue)
    }

    this.state.message = newValue
  },

  clearMessageAction() {
    if (this.debug) {
      console.log('clearMessageAction triggered')
    }

    this.state.message = ''
  }
}

const appA = createApp({
  data() {
    return {
      privateState: {},
      sharedState: store.state
    }
  },
  mounted() {
    store.setMessageAction('Goodbye!')
  }
}).mount('#app-a')

const appB = createApp({
  data() {
    return {
      privateState: {},
      sharedState: store.state
    }
  }
}).mount('#app-b')

Simplest Store

Vuex app 的核心就是 store,用來儲存 app 的狀態,以下兩點,是 Vuex store 跟 global object 的差異

  1. Vuex stores 是 reactive,如果 Vue component 使用了 state,將會在 state 異動時,自動更新 component
  2. 無法直接修改 store 的 state,修改的方式是透過 committing mutations,可確保 state change 可被追蹤

透過 mutations methods 異動 state

<div id="app-a">
  {{sharedState.count}}
   <button @click="increment">increment</button>
</div>

<script>
// import { createApp } from 'vue'
// import { createStore } from 'vuex'
const { createApp, reactive } = Vue
const { createStore } = Vuex

// Create a new store instance.
const store = createStore({
  state () {
    return {
      count: 0
    }
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

const app = createApp({
  data() {
    return {
      privateState: {},
      sharedState: store.state
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
      console.log(this.$store.state.count)
    }
  }
})

app.mount('#app-a')
app.use(store)

</script>

State

Single State Tree

single state tree 就是包含 application 所有 state 的單一物件,也就是 "single sure of truth",每一個 application 都只有一個 store。單一物件容易使用部分 state 資料,也很容易 snapshot 目前的狀態值。

single state 並不會跟 modularity 概念衝突,後面會說明如何將 state 與 mutations 分割到 sub modules

store 儲存的 data 遵循 Vue instance 裡面的 data 的規則

Getting Vuex State into Vue Components

因為 Vuex store 是 reactive,最簡單的方法就是透過 computed property 取出部分 store state

以下產生一個 component,並將 store inject 到 component 中,透過 this.$store 存取

<div id="app">
  <counter></counter>
</div>

<script>
// import { createApp } from 'vue'
// import { createStore } from 'vuex'
const { createApp, reactive } = Vue
const { createStore } = Vuex

// Create a new store instance.
const store = createStore({
  state () {
    return {
      count: 0
    }
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})


const app = createApp({
  data() {
    return {
      privateState: {},
      sharedState: store.state
    }
  },

})

const Counter = {
  template: `<div>{{ count }}</div> <button @click="increment">increment</button>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
      console.log(this.$store.state.count)
    }
  }
}

app.use(store)
app.component('counter', Counter)

app.mount('#app')

</script>

mapState

當 component 需要使用多個 store state properties or getters,宣告多個 computed property 會很麻煩,Vuex 用 mapState 產生 computed getter functions

<div id="app">
  <counter></counter>
</div>

<script>
// import { createApp } from 'vue'
// import { createStore } from 'vuex'
const { createApp, reactive } = Vue
const { createStore, mapState } = Vuex

// Create a new store instance.
const store = createStore({
  state () {
    return {
      count: 0
    }
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})


const app = createApp({
  data() {
    return {
      privateState: {},
      sharedState: store.state
    }
  },

})

const Counter = {
  template: `<div>{{ count }}</div>
    <div>{{ countAlias }}</div>
    <div>{{ countPlusLocalState }}</div>
    <button @click="increment">increment</button>`,
  data() {
    return {
      localCount: 2,
    };
  },
  computed: mapState({
    // arrow functions can make the code very succinct!
    count: state => state.count,

    // passing the string value 'count' is same as `state => state.count`
    countAlias: 'count',

    // to access local state with `this`, a normal function must be used
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  }),
  methods: {
    increment() {
      this.$store.commit('increment')
      console.log(this.$store.state.count)
    }
  }
}

app.use(store)
app.component('counter', Counter)

app.mount('#app')

</script>

也可以直接傳入 string array 給 mapState,mapped computed property 的名稱要跟原本 state sub tree name 一樣

  computed: mapState([
      'count'
    ]),

Object Spread Operator

mapState 會回傳一個物件,如果要組合使用 local computed property,通常要用 utility 將多個物件 merge 在一起,再將該整合物件傳給 computed

利用 object spread operator 可簡化語法

<div id="app">
  <counter></counter>
</div>

<script>
// import { createApp } from 'vue'
// import { createStore } from 'vuex'
const { createApp, reactive } = Vue
const { createStore, mapState, mapGetters } = Vuex

// Create a new store instance.
const store = createStore({
  state () {
    return {
      count: 0,
      todos: [{
          id: 1,
          text: '...',
          done: true
        },
        {
          id: 2,
          text: '...',
          done: false
        }
      ]
    }
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
    doneTodosCount: (state,getters) => {
      return getters.doneTodos.length
    },
    getTodoById: (state) => (id) => {
      return state.todos.find(todo => todo.id === id)
    }
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})


const app = createApp({
  data() {
    return {
      privateState: {},
      sharedState: store.state
    }
  },

})

const Counter = {
  template: `<div>
    <div>{{count}}</div>
    <div>{{countAlias}}</div>
    <div>{{countPlusLocalState}}</div>

    <div>{{doneTodos}}</div>
    <div>{{doneTodosAlias}}</div>
    <div>{{doneTodosCount}}</div>
    <div>{{getTodoById}}</div>
  </div>
    <button @click="increment">increment</button>`,
  data() {
    return {
      localCount: 2,
    };
  },
  computed: {

    // 本地 computed
    getTodoById() {
      return this.$store.getters.getTodoById(2);
    },

    // 使用展開運算符將 mapState 混合到外部物件中
    ...mapState([
      'count',
    ]),
    ...mapState({
      countAlias: 'count',
      countPlusLocalState(state) {
        return state.count + this.localCount;
      },
    }),

    // 使用展開運算符將 mapGetters 混合到外部物件中
    ...mapGetters([
      'doneTodos',
      'doneTodosCount',
    ]),
    ...mapGetters({
      doneTodosAlias: 'doneTodos',
    }),
  },
  methods: {
    increment() {
      this.$store.commit('increment')
      console.log(this.$store.state.count)
    }
  }
}

app.use(store)
app.component('counter', Counter)

app.mount('#app')

</script>

Getters

有時候需要根據儲存的 state 計算出衍生的 state

ex:

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

如果有多個 component 需要這個 function,可以在 store 裡面定義 getters,第一個參數固定為 state

const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos (state) {
      return state.todos.filter(todo => todo.done)
    }
  }
})

Property-Style Access

getters 是透過 store.getters 物件使用

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

可接受其他 getters 為第二個參數

getters: {
  // ...
  doneTodosCount (state, getters) {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1

在 component 可這樣呼叫

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

Method-Style Access

可利用 return a function 傳給 getters 參數,這對於查詢 store 裡面的 array 很有用

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

mapGetters

map store getters 為 local computed properties

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
    // mix the getters into computed with object spread operator
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

可 mapping 為不同名稱

...mapGetters({
  // map `this.doneCount` to `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})

Mutations

修改 state 的方式是透過 committing a mutation

Vuex mutations 類似 events,每個 mutation 都有 string type 及 a handler

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // mutate state
      state.count++
    }
  }
})

不能直接呼叫 mutation handler,必須這樣呼叫

store.commit('increment')

Commit with Payload

傳送新增的參數給 store.commit 稱為 mutation 的 payload

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}

呼叫

store.commit('increment', 10)

通常 payload 會是一個 object,裡面有多個欄位

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

呼叫

store.commit('increment', {
  amount: 10
})

Object-Style Commit

commit a mutation 的另一個方式

store.commit({
  type: 'increment',
  amount: 10
})

這時候,整個物件會成為 payload,故 handler 不變

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

Using Constants for Mutation Types

常見到在 Flux 會使用 constants 為 mutation types,優點是可將所有 constants 集中放在一個檔案裡面,可快速知道整個 applicaiton 的 mutations

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = createStore({
  state: { ... },
  mutations: {
    // we can use the ES2015 computed property name feature
    // to use a constant as the function name
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

Mutations Must Be Synchronous

mutation handler functions must be synchronous

如果這樣寫,當 commit mutation 時 callback 無法被呼叫。devtool 無法得知什麼時候被呼叫了 callback

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

Committing Mutations in Components

可用 this.$store.commit('xxx') 或是 mapMutations helper

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // map `this.increment()` to `this.$store.commit('increment')`

      // `mapMutations` also supports payloads:
      'incrementBy' // map `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // map `this.add()` to `this.$store.commit('increment')`
    })
  }
}

Vuex 的 mutations 是 synchronous transactions

store.commit('increment')
// any state change that the "increment" mutation may cause
// should be done at this moment.

如果需要用到 asynchronous opertions,要使用 Actions


Actions

類似 mutations,差別:

  • actions commit mutations,而不是 mutating the state
  • actions 可封裝任意非同步 operations

這是簡單的 actions 例子

const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

action handler 以 context 為參數,裡面是 store instance 的 methods/properties,故能呼叫 context.commit commit a mutation,context.statecontext.getters

也能用 context.dispatch 呼叫其他 actions

只使用 commit 的時候,可這樣簡化寫法

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

Dispatching Actions

store.dispatch 會驅動 actions

store.dispatch('increment')

因為 mutations 必須要為 synchronous,故如要處理 asynchronous operations,而不是直接呼叫 store.commit('increment')

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

actions 支援 payload format & object-style dispatch

// dispatch with a payload
store.dispatch('incrementAsync', {
  amount: 10
})

// dispatch with an object
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

這是更真實的例子:checkout a shopping cart

actions: {
  checkout ({ commit, state }, products) {
    // save the items currently in the cart
    const savedCartItems = [...state.cart.added]
    // send out checkout request, and optimistically
    // clear the cart
    commit(types.CHECKOUT_REQUEST)
    // the shop API accepts a success callback and a failure callback
    shop.buyProducts(
      products,
      // handle success
      () => commit(types.CHECKOUT_SUCCESS),
      // handle failure
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

Dispatching Actions in Components

可使用 this.$store.dispatch('xxx')mapActions helper 在 component 中 dispatch actions

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // map `this.increment()` to `this.$store.dispatch('increment')`

      // `mapActions` also supports payloads:
      'incrementBy' // map `this.incrementBy(amount)` to `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // map `this.add()` to `this.$store.dispatch('increment')`
    })
  }
}

Composing Actions

因 action 是非同步的,可利用 Promise 得知 action 已完成

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

現在就能這樣呼叫

store.dispatch('actionA').then(() => {
  // ...
})

////// 在另一個 action 可這樣呼叫
actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

可利用 async/await 撰寫 actions

// assuming `getData()` and `getOtherData()` return Promises

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // wait for `actionA` to finish
    commit('gotOtherData', await getOtherData())
  }
}

Modules

因使用 single state tree,application 的所有 states 集中在一個物件中,如果 application 很大,store 也會很大

Vuex 可將 store 切割為 modules,每個 module 有各自的 state, mutations, actions, getters, nested modules

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

Module Local State

在 module 的 mutations 與 getters,第一個參數為 module 的 local state

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // `state` is the local module state
      state.count++
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

在 module action,透過 context.state存取 local state,透過 context.rootState 存取 root state

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

在 module getter,rootState 是第三個參數

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

Namespacing

actions, mutations, getters 預設註冊為 global namespace

可用 namespaces:true ,自動加上 module name

const store = createStore({
  modules: {
    account: {
      namespaced: true,

      // module assets
      state: () => ({ ... }), // module state is already nested and not affected by namespace option
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // nested modules
      modules: {
        // inherits the namespace from parent module
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // further nest the namespace
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})
  • Accessing Global Assets in Namespaced Modules

rootStaterootGetters 有傳入 getter function 作為第三、四個參數,且可透過 context 物件使用 properties

如果要使用 global namespace 的 actions, mutations,要在 dispatch, commit 傳入 {root:true}

modules: {
  foo: {
    namespaced: true,

    getters: {
      // `getters` is localized to this module's getters
      // you can use rootGetters via 4th argument of getters
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
        rootGetters['bar/someOtherGetter'] // -> 'bar/someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // dispatch and commit are also localized for this module
      // they will accept `root` option for the root dispatch/commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'
        rootGetters['bar/someGetter'] // -> 'bar/someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}
  • register global actions in namespaces modules
{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}
  • binding helpers with namespace

如果要呼叫 nested module 的 getters, action 會比較麻煩

computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  }),
  ...mapGetters([
    'some/nested/module/someGetter', // -> this['some/nested/module/someGetter']
    'some/nested/module/someOtherGetter', // -> this['some/nested/module/someOtherGetter']
  ])
},
methods: {
  ...mapActions([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}

可用 module namespace string 作為第一個參數鎚入 helpers

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  }),
  ...mapGetters('some/nested/module', [
    'someGetter', // -> this.someGetter
    'someOtherGetter', // -> this.someOtherGetter
  ])
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

也可以用 createNamespacedHelpers

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // look up in `some/nested/module`
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // look up in `some/nested/module`
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}
  • caveat for plugin developers

如果有 plugin 提供 module,並讓使用者加入 vuex store,如果 plugin user 把 module 加入某個 namespaced module,會讓使用者的 module 也被 namespaced

可透過 plugin option 的 namedspace 參數解決此問題

// get namespace value via plugin option
// and returns Vuex plugin function
export function createPlugin (options = {}) {
  return function (store) {
    // add namespace to plugin module's types
    const namespace = options.namespace || ''
    store.dispatch(namespace + 'pluginAction')
  }
}

Dynamic Module Registration

可在 store 產生後,再透過 store.registerModule 註冊 module

import { createStore } from 'vuex'

const store = createStore({ /* options */ })

// register a module `myModule`
store.registerModule('myModule', {
  // ...
})

// register a nested module `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

module 的 state 為 store.state.myModule and store.state.nested.myModule

動態註冊的 module,可用 store.unregisterModule(moduleName) 移除

可用 store.hasModule(moduleName) 檢查是否有被註冊

  • Preserving state

註冊新的 module 時,可用 preserveState option: store.registerModule('a', module, { preserveState: true }) 保留 state

Module Reuse

有時候需要產生 module 的多個 instance,ex:

  • 用一個 module 產生多個 store
  • 在一個 store 重複註冊某個 module

如果用 plain object 宣告 state of the module,state object 會以 reference 方式被分享,如果 mutated 時,會造成 cross store/module state pollution

解決方法:use a function for declaring module state

const MyReusableModule = {
  state: () => ({
    foo: 'bar'
  }),
  // mutations, actions, getters...
}

References

Vuex