2022/12/05

From Docker Virtual Network Connect To Solid MySQL Server

假設我們的 server 只有一台,PHP 環境為 docker container,而資料庫為實體機安裝,資料庫 3306 防火牆防堵不對外,但要允許從 Docker 環境內的服務存取,最簡單的方式就是防火牆讓 Docker 產生的 network interface 通過,下面我們來實做這件事。

今天示範環境為使用 Vagrant 起一台 Ubuntu jammy64,然後在裡面進行防火牆的測試。

sudo service ufw start
sudo ufw enable
sudo ufw allow ssh

記得開啟防火牆要先把 ssh 打開,不然就悲劇了,接著安裝 MySQL,docker 以及 docker compose 部分就麻煩自行安裝。

sudo apt install -y mariadb-server mariadb-client

安裝完畢後,我們來建立測試用資料庫以及帳號。

sudo mysql
create database laravel;
GRANT ALL PRIVILEGES ON `laravel`.* TO 'homestead'@'%' IDENTIFIED BY 'secret' WITH GRANT OPTION;

這邊權限開 % 沒關係,因為我們只會允許 docker network 指定的 network interface 進入而已。

docker-compose.yml
version: "3.9"

services:
  fpm:
    container_name: fpm
    build: .
    restart: always
    working_dir: /www
    extra_hosts:
      - "host.docker.internal:host-gateway"
    networks:
      - front_end
    volumes:
      - ./www/:/www

networks:
  front_end:
    driver_opts:
      com.docker.network.bridge.name: br-fpm
Dockerfile
FROM php:8.1-fpm

RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip

ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/

RUN chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions @composer pdo_mysql mbstring exif pcntl bcmath gd

WORKDIR /www

這是要安裝 Laravel 的最小環境,我們今天只是要測試連線所以其他的設定就先不理會了。

sudo docker compose up -d

等 image build 完以及 service 起來以後,我們來利用指令安裝一個 Laravel 的環境。

sudo docker compose exec fpm composer create-project laravel/laravel .

接著我們打開 .env 檔案設定資料庫連線內容。

DB_CONNECTION=mysql
DB_HOST=host.docker.internal
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=homestead
DB_PASSWORD=secret

這時來測試一下連線。

sudo docker compose exec fpm php artisan migrate

因為防火牆擋住了,所以 connection 會 hang 住,最終失敗,接著來開始防火牆設定。

sudo ufw allow in on br-fpm to any port 3306

接著再執行一次 migrate 的指令。

sudo docker compose exec fpm php artisan migrate

   INFO  Preparing database.

  Creating migration table ............................................................................................................... 10ms DONE

   INFO  Running migrations.

  2014_10_12_000000_create_users_table .................................................................................................... 9ms DONE
  2014_10_12_100000_create_password_resets_table .......................................................................................... 2ms DONE
  2019_08_19_000000_create_failed_jobs_table .............................................................................................. 3ms DONE
  2019_12_14_000001_create_personal_access_tokens_table ................................................................................... 4ms DONE

可以看到 migration 順利成功了,如果發生 connection refuse 的錯誤的話,記得把 MySQL 的 bind-address 設定為 0.0.0.0

iptable 的話用下方的指令應可達到一樣的效果。

iptables -A INPUT -i br-fpm -p tcp --dport 3306 -j ACCEPT
iptables -A INPUT -p tcp --dport 3306 -j DROP

2022/11/21

Test API Server With Express.js

測試 API 結果時常常需要 fake data 做實際測試,架設 http server 有點小題大作,我們可以利用 express.js 迅速搭建測試用的 API server。

首先安裝相關套件:

npm i --save express cors

接著建立一支 server.js,程式如下:

const express = require('express');
const app = express();
const port = 8080;
const fs = require("fs");
const path = require("path");
const cors = require("cors");

app.use(cors());

function getFile(fileName) {
    return JSON.parse(fs.readFileSync(path.join(__dirname, "data", `${fileName}.json`)));
}

app.get('/api', (req, res) => {
    res.json(getFile('index'));
});

app.listen(port, () => {
    console.log(`Example app listening on port ${port}`);
})

index.json 內容如下:

{
    "status": true,
    "data": "hello world"
}

有時候 json 的內容很龐大,我們不希望負責 route 的檔案長度過長不好維護,所以我這邊 parse 了 data 目錄裡指定的 .json 檔案返回,接著只要執行 node server.js 後就可以在 http://localhost:8080/api 看到正確的內容了。

2022/11/17

CSS Practice - Learn CSS Grid

最近在大量練習 CSS 3 切版,看到有趣的版面就拿來時做練習,Learn CSS Grid 這個炸開的效果挺有趣的,複製了 html 架構後,除了一些顏色跟間距數值以外的 css 就憑想像手刻了,除了炸裂的位置只抓個大概以外,排版的部分盡量一致。

2022/10/18

Reload Nginx And Apache Config In Docker

如果在本機安裝 apache2 或 nginx 時想要 reload config 的話我們通常會使用下列的指令:

# nginx
nginx -s reload

# apache2
service httpd reload

但在 docker 的環境下就沒有那麼簡單,像 apache 的 reload 是把 script 寫在 service 的 config 裡面,不一定每個 image 裡面都有包含這個指令,CentOS 的我記得好像就沒有,為了要兼容所有的環境,我們可以利用 kill -USR1 這個方式套用到每個我們想要 reload without restart 的專案上面。

apache2

首先在根目錄建立兩個網頁檔:

  • ./www/1/index.html
  • ./www/2/index.html

各別會在網頁印出 1 以及 2 的內容識別。

docker-compose.yml

version: "3.9"

services:
  http:
    ports:
      - "80:80"
    container_name: httpd
    image: httpd
    restart: always
    volumes:
      - "./www:/www"
      - "./config/httpd:/usr/local/apache2/conf:ro"

我們建立了一個名為 http 的 container,使用 httpd 的 image,並且把 www 目錄掛進去,我事先把 httpd 的 config 複製出來以便在外面修改內容,接下來跑 docker compose up -d,打上 IP 後可以看到 httpd 傳統的首頁內容 It works!

接著我們修改 httpd.conf

DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">

# 改為

DocumentRoot "/www/1"
<Directory "/www/1">

我們先使用 docker compose restart 測試,執行後 refresh 網頁可以看到我們要的 1,接著再把 config 改成:

DocumentRoot "/www/2"
<Directory "/www/2">

這次我們使用 docker compose kill -s USR1 http ,此時我們可以看到 container 活得好好的,但 refresh 頁面後得到我們想要的 2 了,他的原理就是呼叫 container 內的 httpd daemon 去重新整理他的 service。

nginx

docker-compose.yml

version: "3.9"

services:
  http:
    ports:
      - "80:80"
    container_name: nginx
    image: nginx
    restart: always
    volumes:
      - "./www:/www"
      - "./config/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro"

一樣,我們事前複製了 nginx 的 config 出來重新掛載回去,啟動 container 以後看見 nginx 的預設字樣 Welcome to nginx!

接著我們修改 defalut.conf

root   /usr/share/nginx/html;

# 改為

root   /www/1;

跟 apache2 一樣我們先使用 docker compose restart,可以順利看到 1 的結果,接著我們將 config 修改為:

root   /www/2;

使用 docker compose exec http nginx -s reload,我們就可以順利看到 2 了,nginx 跟 apache2 的 container 啟動方式不太一樣,所以不能用 kill 的方法,但事實上在 nginx 的 logrotate config 裡面他使用的方式也是:

kill -USR1 `cat /var/run/nginx.pid`

上面介紹了兩個主流網頁伺服器的重取 config 的目的是在當我們把 server 的 log 掛到我們本機目錄時,我們可能會需要依照情況在本機執行 logrotate 的動作,這樣就可以把 reload 腳本寫在本機的 logrotate config 的 postrotate/endscript 裡面了。

這邊附上 logrotate for docker 基本的配置:

nginx-docker

/YOUR_PATH_TO_LOG/*.log {
        daily
        missingok
        rotate 52
        compress
        delaycompress
        notifempty
        create 640 nginx adm
        sharedscripts
        postrotate
            sudo docker exec YOUR_CONTAINER_NAME nginx -s reload
        endscript
}

httpd-docker

/YOUR_PATH_TO_LOG/*.log {
        daily
        missingok
        rotate 14
        compress
        delaycompress
        notifempty
        sharedscripts
        postrotate
            sudo docker kill -s USR1 YOUR_CONTAINER_NAME 
        endscript
}

2022/10/10

Delete None Keeping Files By Command find

之前寫過 Delete None Keeping File 這篇,用來刪除固定數量的保存檔案,使用的是 ls,今天使用 find 來達成一樣的效果。

首先我們來建二十個以 0.05 秒間距不同的檔案:

for i in {1..20}; do touch "${i}.log"; sleep 0.05; done

列出檔案一下目前排序是這樣。

0 Oct 15 23:56 1.log
0 Oct 15 23:56 10.log
0 Oct 15 23:56 11.log
0 Oct 15 23:56 12.log
0 Oct 15 23:56 13.log
0 Oct 15 23:56 14.log
0 Oct 15 23:56 15.log
0 Oct 15 23:56 16.log
0 Oct 15 23:56 17.log
0 Oct 15 23:56 18.log
0 Oct 15 23:56 19.log
0 Oct 15 23:56 2.log
0 Oct 15 23:56 20.log
0 Oct 15 23:56 3.log
0 Oct 15 23:56 4.log
0 Oct 15 23:56 5.log
0 Oct 15 23:56 6.log
0 Oct 15 23:56 7.log
0 Oct 15 23:56 8.log
0 Oct 15 23:56 9.log

用預設的 find 查詢一下檔案內容。

find . -type f -name "*.log"

./10.log
./8.log
./19.log
./14.log
./13.log
./9.log
./2.log
./20.log
./1.log
./17.log
./6.log
./15.log
./12.log
./16.log
./4.log
./5.log
./11.log
./7.log
./18.log
./3.log

有人可能會說那利用 sort 不就可以自然排序正確的數字,沒錯,但那是因為我們的檔名自然有序,若遇到實際上沒有這樣結構的檔案,或者萬一臨時修改了檔名造成新排序不是我們要的結果就會出問題,所以最保險的方法還是利用日期去自然排序。

我們利用 -printf 這個指令來達到目的。

find . -type f -name "*.log" -printf "%T+ %p\n" | sort
2022-10-15+23:56:20.1777708840 ./1.log
2022-10-15+23:56:20.2297708850 ./2.log
2022-10-15+23:56:20.2817708850 ./3.log
2022-10-15+23:56:20.3337708860 ./4.log
2022-10-15+23:56:20.3857708860 ./5.log
2022-10-15+23:56:20.4457708870 ./6.log
2022-10-15+23:56:20.5057708880 ./7.log
2022-10-15+23:56:20.5697708880 ./8.log
2022-10-15+23:56:20.6337708890 ./9.log
2022-10-15+23:56:20.6817708890 ./10.log
2022-10-15+23:56:20.7377708900 ./11.log
2022-10-15+23:56:20.7977708910 ./12.log
2022-10-15+23:56:20.8457708910 ./13.log
2022-10-15+23:56:20.9057708920 ./14.log
2022-10-15+23:56:20.9577708920 ./15.log
2022-10-15+23:56:21.0217708930 ./16.log
2022-10-15+23:56:21.0977708940 ./17.log
2022-10-15+23:56:21.1617708950 ./18.log
2022-10-15+23:56:21.2137708950 ./19.log
2022-10-15+23:56:21.2617708960 ./20.log

printf 詳細用法可以參考 How to Use find -printf in Linux?,非常的強大,其他工作就可以接續 Delete None Keeping File 處理。

find . -type f -name "*.log" -printf "%T+ %p\n" | sort -r | awk '{print $NF}' | tail -n +11
./10.log
./9.log
./8.log
./7.log
./6.log
./5.log
./4.log
./3.log
./2.log
./1.log

上方指令搭配 xargs rm -f 就可以只保留最新的十份檔案。

2022/09/18

Supervisord

Supervisord 是 Linux 上常用的 daemon 監控工具,可以使用他來啟動並監控 daemon,若 daemon 因為任何的狀況消失,可以進行重啟並且記錄。

首先我們手動建立一個 daemon。

/tmp/test.sh
while true
do
    echo test
    sleep 1
done

接著來建立配置檔案。

/etc/supervisor/conf.d/test.conf
[program:test]
command = bash /tmp/test.sh
autostart = true
autorestart = true

配置完成後,執行相關指令使用。

$ sudo supervisorctl status
# 這個時候是空的

$ sudo supervisorctl reload
Restarted supervisord

$sudo supervisorctl status
test                             STARTING
# 我們可以看到剛才配置的 test.conf 已經生效了

$ ps -aux | grep test
root        5884  0.0  0.3   8696  3352 ?        S    01:27   0:00 bash /tmp/test.sh
# 利用 ps 來看他也確實在背景運作

$ sudo kill -9 5884
# 我們把這個 daemon process 砍掉

$ ps -aux | grep test
root        6107  0.0  0.3   8696  3468 ?        S    01:29   0:00 bash /tmp/test.sh
# 可以看到這個 daemon 重新啟動並且更換了 process id,表示原本的 process 有被我們砍掉,'但 supervisord 又把他啟動了

如果有添加新的 config,建議使用 sudo supervisorctl update 做單一配置更新,使用 reload 的話所有的 process 會重新啟動,如果是跑 queue 之類的 process 怕造成不可預期的後果。

配置內容

command=/bin/cat    ; 程式執行命令,建議使用絕對路徑。
process_name=%(program_name)s ; 程式名稱,可用的變數有 `group_name`, `host_node_name`, `process_num`, `program_name`, `here`(配置檔案目錄)。 一般程式需要執行多個副本的情況會使用。後面會有例子。
numprocs=1     ; 程式執行的副本個數,預設為1,如果值大於1,則`process_name` 必須包含 `%(process_num)s`
numprocs_start=0    ; `%(process_num)s`起始數字,預設為0
00=/tmp    ; 程式執行的所在目錄,相當於先cd到指定目錄,然後執行程式。
umask=022      ; umask for process (default None)
priority=999     ; 程式操作的的優先順序,例如在start all/stop all,高優先順序的程式會先關閉和重啟。
autostart=true    ; 在supervisord啟動時自動啟動,預設為true
startsecs=1     ; 程式啟動前等待時間等待時間。預設為1。
startretries=3    ; 嘗試重啟最大次數。預設為3。
autorestart=unexpected  ; 是否自動重啟,可選引數為 false, unexpected, true。如果為false則不自動重啟,如果為unexpected表示如果程式退出訊號不在 `exitcodes` 中,則自動重啟。預設為unexpected
exitcodes=0,2     ; 程式退出碼。配合`autorestart`使用。預設為 0,2
stopsignal=QUIT    ; 殺死程序是傳送的訊號,預設為TREM。
stopwaitsecs=10    ; 傳送SIGKILL訊號前最大等待時間。預設為10。
user       ; 以指定使用者身份啟動程式。預設為當前使用者。
stopasgroup=false    ; 是否向子程序傳送停止訊號,這對於Flask的debug模式很有用處,如果設定為true,則不向子程序傳送停止訊號。預設為false
killasgroup=false    ; 是否向子程序傳送kill訊號,預設為false
redirect_stderr=false   ; 將錯誤輸出定向到標準輸出,預設為false
stdout_logfile=/a/path  ; 標準輸出日誌路徑,可選引數為 `自定義` `AUTO` `NONE`,`自定義`將日誌寫到自定義路徑,可用的變數有`group_name`, `host_node_name`, `process_num`, `program_name`, `here`(配置檔案目錄);`NONE`不建立日誌;`AUTO` 又supervisord自動選擇路徑,並且當supervisord服務重新啟動時原來自動建立的日誌以及日誌的備份檔案會被刪除。預設為AUTO
stdout_logfile_maxbytes=1MB ; 標準輸出日誌單個檔案最大大小,如果超過指定大小會將日誌檔案備份,可用的單位 KB MB GB。如果設定為0則表示不限制檔案大小。預設為50MB
stdout_logfile_backups=10  ; 標準輸出日誌檔案最大備份數。預設為10
stdout_capture_maxbytes=1MB ; 當程序處於“stdout capture mode”模式下寫入到FIFO佇列最大位元組數,可用單位 KB MB GB。預設為0,詳細說明見[capture-mode](http://supervisord.org/logging.html#capture-mode)
stdout_events_enabled=false ; 
;以下配置項配置錯誤輸出的日誌引數。和上面標準輸出配置相同。
stderr_logfile=/a/path  ;
stderr_logfile_maxbytes=1MB ;
stderr_logfile_backups=10  ;
stderr_capture_maxbytes=1MB ;
stderr_events_enabled=false ;
environment=A="1",B="2"  ; 環境變數設定,可用的變數有 `group_name`, `host_node_name`, `process_num`, `program_name`, `here`。 預設為空。
serverurl=AUTO    ; override serverurl computation (childutils)

參考網站

2022/07/15

vagrant mount issue

Vagrant 在安裝幾個 redhat 相關 box 時常常會遇到 mount 出問題,像是:

Vagrant was unable to mount VirtualBox shared folders. This is usually
because the filesystem “vboxsf” is not available. This filesystem is
made available via the VirtualBox Guest Additions and kernel module.
Please verify that these guest additions are properly installed in the
guest. This is not a bug in Vagrant and is usually caused by a faulty
Vagrant box. For context, the command attempted was:

mount -t vboxsf -o uid=1000,gid=1000 keys /keys

這時候通常在 Vagrantfile 添加 vbguest 的指令就可以解了。

if Vagrant.has_plugin?("vagrant-vbguest")
    config.vbguest.installer_options = {
        enablerepo: true,
        allow_kernel_upgrade: true
    }
end

若有時候有其他問題,則可以加入下列語法:

if Vagrant.has_plugin?("vagrant-vbguest")
    config.vbguest.auto_update = false
    config.vbguest.no_remote = true
end

我在 ubuntu 21 上使用 NFS 時遇到無法 mount 的問題,加上了 nfs_udp: false 才解決,大部分狀況在官方文件都找的到 solution。

如果使用 vagrant 做為測試用途,而當前版本有些問題無法滿足,可以選擇其他人製作的版本來用,例如說我使用 rockylinux8 測試時,因為他的主磁碟只規劃 5GB,導致根本無法完成測試要用的套件安裝,試了很多方式調整 partition 都失敗,最後改用 bento/rockylinux-8 解決,換個風景看世界就開闊了 QQ。

2022/07/12

multiple checkbox filter for datatables

最近在練習 datatables 這個好用的工具,他可以搭配 jQueryBootstrap 讓你可以很快的建立有 RWD 的表格,他有非常豐富的 API 可以客製內容,今天示範一下怎麼使用 checkbox 去 filter 出符合的內容。

使用正規式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet"
          href="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/css/jquery.dataTables.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/jquery.dataTables.min.js"></script>
    <script>
        $(function() {
            let data = [
                [
                    "Chan",
                    "basketball"
                ],
                [
                    "Phoebe",
                    "volleyball"
                ],
                [
                    "Lucas",
                    "tennis"
                ],
                [
                    "LBJ",
                    "basketball"
                ],
            ];
            let table = $('#data_table').DataTable({
                data: data
            });

            $(':checkbox').on('click', function() {
                let options = $(':checkbox:checked').map(function() {
                    return $(this).val()
                }).get();

                if (options.length === 0) {
                    table.columns(1).search('').draw();
                } else {
                    let regex = options.join('|');

                    table.columns(1).search(regex, true, false).draw();
                }
            });
        });
    </script>
</head>
<body>
<div>
    <label> basketball
        <input type="checkbox" value="basketball">
    </label>
    <label> volleyball
        <input type="checkbox" value="volleyball">
    </label>
    <label> tennis
        <input type="checkbox" value="tennis">
    </label>
</div>
<table id="data_table">
    <thead>
    <tr>
        <th>name</th>
        <th>hobby</th>
    </tr>
    </thead>
    <tbody></tbody>
</table>
</body>
</html>

使用 extension

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet"
          href="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/css/jquery.dataTables.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.21/js/jquery.dataTables.min.js"></script>
    <script>
        $(function() {
            let data = [
                [
                    "Chan",
                    "basketball"
                ],
                [
                    "Phoebe",
                    "volleyball"
                ],
                [
                    "Lucas",
                    "tennis"
                ],
                [
                    "LBJ",
                    "basketball"
                ],
            ];
            let table = $('#data_table').DataTable({
                data: data
            });

            $.fn.dataTable.ext.search.push(function(settings, data) {
                let options = $(':checkbox:checked').map(function() {
                    return $(this).val()
                }).get();

                if (options.length === 0) {
                    return true;
                }

                return options.indexOf(data[1]) !== -1;
            });

            $(':checkbox').on('click', function() {
                table.draw();
            });
        });
    </script>
</head>
<body>
<div>
    <label> basketball
        <input type="checkbox" value="basketball">
    </label>
    <label> volleyball
        <input type="checkbox" value="volleyball">
    </label>
    <label> tennis
        <input type="checkbox" value="tennis">
    </label>
</div>
<table id="data_table">
    <thead>
    <tr>
        <th>name</th>
        <th>hobby</th>
    </tr>
    </thead>
    <tbody></tbody>
</table>
</body>
</html>

上面使用交集的方法篩選,稍微改寫一下就可以變聯集了。

2022/07/06

learning latteart opening

自學拉花也好幾年了,買過很多跟拉花相關的器材,但穩定性一直不是很好。

https://www.instagram.com/p/Ce-TmSGh8np/

這次終於痛下決心去找老師上課,survery 了坊間很久,沒想到自己的愛店 Shawn Coffee 就有開課,透過 IG 跟老師聯繫之後,約定好了上課日期(不太好排,太紅了),今天是我的第一天上課,我還蠻緊張的,都四十幾歲了還能夠重回學生時代的學習初心跟重溫未知的忐忑感我覺得很棒,Wish myself luck。

Compile Apache and OpenSSL

OS: CentOS 8
Target:

  • PHP 7.4
  • PHP-FPM
  • Apache 2.4.46
  • OpenSSL 1.1.1g

Start compile

sudo dnf install install -y gcc gcc-c++ make prec-devel libtool perl-core zlib-devel

# Compile apr
sudo ./configure --prefix=/usr/local/apr
sudo make
sudo make install

# Compile apr-util
sudo ./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr
sudo make
sudo make install

# Compile openssl
sudo ./config --prefix=/usr/local/openssl --openssldir=/usr/local/openssl shared zlib
sudo make
sudo make install

# Compile apache
sudo ./configure --prefix=/usr/local/apache2446 --enable-so --enable-ssl --enable-cgi --enable-rewrite --enable-modules=most --enable-mpms-shared=all --with-mpm=prefork --with-zlib --with-apr=/usr/local/apr --with-apr-util=/usr/local/apr-util --with-ssl=/usr/local/openssl
sudo make
sudo make install

# Apache system configuration
# Add Apache executables to PATH ( You can use nano instead of vi)
# create and open the following file
sudo vim /etc/profile.d/httpd.sh
  
# paste the following content, save and exit
pathmunge /usr/local/apache2446/bin

# Add Systemd entry

# create and open the following file
sudo vim /etc/systemd/system/httpd.service

paste the following content, save and exit.

[Unit]
Description=The Apache HTTP Server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/apache2446/bin/apachectl -k start
ExecReload=/usr/local/apache2446/bin/apachectl -k graceful
ExecStop=/usr/local/apache2446/bin/apachectl -k graceful-stop
PIDFile=/usr/local/apache2446/logs/httpd.pid
PrivateTmp=true

[Install]
WantedBy=multi-user.target
# reload the systemctl daemon
sudo systemctl daemon-reload

# start Apache httpd server (ignore the warnings at this stage)
sudo systemctl start httpd
sudo systemctl enable httpd

# Install PHP
sudo dnf install -y http://rpms.remirepo.net/enterprise/remi-release-8.rpm
dnf module reset php
dnf module install -y php:remi-7.4

sudo service php-fpm start
sudo systemctl enable php-fpm

Reference

2022/07/04

JsRender Sample

現在前端 framework 百家爭鳴,不過和後端 framework
遇到一樣的問題,版本更動超快寫法不相容,今日的當紅炸子機相隔一年後消失的無影無蹤時有所聞,後端比較沒有迅速隕落的問題,畢竟他的開發門檻還是稍高一些,也因此除了有玩一下 vuejs 以外對
front-end framework 真的沒有太高的興趣,即便我超愛寫 JavaScript,不過專案裡面還滿常用到 js render template 這件事情,所以我還是有使用一些 plugin
來實現這件事情,今天就來介紹 JsRender 這個 plugin

基礎功能

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JsRender</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrender/0.9.88/jsrender.min.js"></script>
    <script>
        $(function() {
            var data = {name: 'chan'};
            var html = $.templates('<strong>{{:name}}</strong>').render(data);
            $('#app').append(html);
        });
    </script>
</head>
<body>
<div id="app"></div>
</body>
</html>

JsRender 傳進去的資料為 JSON,上面的內容會得到:

<div id="app"><strong>chan</strong></div>

如果是複數的內容,JsRender 會自動幫你做 repeat。

script
$(function() {
    let data = [
        {name: 'chan'},
        {name: 'phoebe'}
    ];
    let html = $.templates('<strong>{{:name}}</strong>').render(data);
    $('#app').append(html);
});
output

<div id="app"><strong>chan</strong><strong>phoebe</strong></div>

樣板

如果將樣板寫在 script 裡面,往往會變得很難維護,字串也超長,所以 JsRender 有一個把樣板隔離出來的套用方式。

script
$(function() {
    let data = [
        {name: 'chan'},
        {name: 'phoebe'}
    ];
    let html = $.templates('#info').render(data);

    $('#app').append(html);
});
html
<script id="info" type="text/html">
    <strong>{{:name}}</strong>
</script>
output
<div id="app">
    <strong>chan</strong>
    <strong>phoebe</strong>
</div>

內建函式

{{> …}} (HTML-encode)

將輸出的部份加上 > 的話會將程式碼 encode。

script
$(function() {
    let data = {link: '<a href="http://www.google.com">google</a>'};
    let html = $.templates('#info').render(data);

    $('#app').append(html);
});
html
<script id="info" type="text/html">
    {{>link}}
</script>
output
&lt;a href&#61;&#34;http://www.google.com&#34;&gt;google&lt;/a&gt;

{{if …}}

判斷式。

script
$(function() {
    let data = [
        {name: 'chan', age: 37},
        {name: 'phoebe', age: 38},
        {name: 'lucas', age: 2}
    ];
    let html = $.templates('#info').render(data);

    $('#app').append(html);
});
html
<script id="info" type="text/html">
    {{:name}} is {{if age > 10}}adult{{else}}children{{/if}}<br>
</script>
output
<div id="app">
    chan is adult<br>
    phoebe is adult<br>
    lucas is children<br>
</div>

{{for …}}

迴圈。

script
$(function() {
    let data = [{
        name: 'chan',
        likes: [{
            name: 'music'
        }, {
            name: 'movie'
        }]
    }, {
        name: 'phoebe'
    }, {
        name: 'lucas',
        likes: [{
            name: 'cartoon'
        }]
    }];
    let html = $.templates('#info').render(data);

    $('#app').append(html);
});
html
<script id="info" type="text/html">
    <div>
        {{:#index}}: {{:name}}
        {{if likes}}
        <ul>
            {{for likes}}
            <li>{{:#index + 1}}: {{:name}}</li>
            {{/for}}
        </ul>
        {{else}}
        <div>no likes</div>
        {{/if}}
    </div>
</script>
output
<div id="app">
    <div>
        0: chan
        <ul>
            <li>1: music</li>
            <li>2: movie</li>
        </ul>
    </div>
    <div>
        1: phoebe
        <div>no likes</div>
    </div>
    <div>
        2: lucas
        <ul>
            <li>1: cartoon</li>
        </ul>
    </div>
</div>

{{include …}}

引入其他的樣板。

script
$(function() {
    let data = {
        name: 'test',
        data: [
            {
                name: 'chan',
                age: 42
            },
            {
                name: 'phoebe',
                age: 42
            }
        ]
    };

    console.log($('#main').render(data));
});

html
<script id="main" type="text/html">
    <h3>{{:name}}</h3>
    {{include tmpl="#sub"/}}
</script>
<script id="sub" type="text/html">
    <ol>
        {{for data}}
        <li>{{:name}}: {{:age}}</li>
        {{/for}}
    </ol>
</script>

output
<h3>test</h3>
<ol>
    <li>chan: 42</li>
    <li>phoebe: 42</li>
</ol>

{{props …}}

key value 式的迴圈。

script
$(function() {
    var data = [
        {
            name: 'Chan',
            likes: {
                name: 'a1',
                age: 33
            }
        },
        {
            name: 'Phoebe',
            likes: {
                food: 'steak',
                drink: 'juice'
            }
        }
    ]
    var html = $.templates('#info').render(data);

    $('#app').html(html);
});
html
<script id="info" type="text/html">
    <div>
        {{:name}}
        <ul>
            {{props likes}}
            <li>{{:key}}: {{:prop}}</li>
            {{/props}}
        </ul>
    </div>
</script>
output
<div id="app">
    <div>
        Chan
        <ul>
            <li>name: a1</li>
            <li>age: 33</li>
        </ul>
    </div>
    <div>
        Phoebe
        <ul>
            <li>food: steak</li>
            <li>drink: juice</li>
        </ul>
    </div>
</div>

helper

helper 也是一種 JSON 格式,可以是文字或者是 function。

script
$(function() {
    var helpers = {
        prefix: function(val) {
            return val + ' is cool';
        },
        version: '1.1'
    };
    var data = {
        name: 'chan'
    };
    var html = $.templates('#info').render(data, helpers);

    $('#app').append(html);
});
html

<script id="info" type="text/html">
    version: {{:~version}} name: {{:~prefix(name)}}
</script>
output
<div id="app">
    version: 1.1 name: chan is cool
</div>

helper 還有另一種 global 的傳入方式。

script
$(function() {
    let helpers = {
        prefix: function(val) {
            return val + ' is cool';
        },
        version: '1.1'
    };
    $.views.helpers(helpers);
    let data = {
        name: 'chan'
    };
    let html = $.templates('#info').render(data);

    $('#app').append(html);
});

以上是最基本的範例,不過應該可以應付大部分的情況了,若要使用其他更詳盡的功能可以前往他的官網 https://www.jsviews.com/