> Source: https://www.vultr.com/docs/setup-letsencrypt-on-linux ### Introduction Let’s Encrypt is an automated, open certificate authority that offers free TLS/SSL certificates for the public’s benefit. The service is provided by the Internet Security Research Group (ISRG). This tutorial shows how to install a Let's Encrypt SSL certificate on One-Click LAMP & LEMP apps using the certbot installation wizard. After completing this tutorial, the server will have a valid certificate and redirect all HTTP requests to HTTPS. ### Prerequisites This tutorial assumes that you have deployed a Vultr One-Click LAMP (Apache) or One-Click LEMP (Nginx) VPS, have a domain name pointing to your server IP address, and you are logged in as root. ### 1. Install certbot Install certbot with `apt`. - One-Click LAMP (Apache) ```sh # apt update && apt install certbot python-certbot-apache -y ``` - One-Click LEMP (Nginx) ```sh # apt update && apt install certbot python-certbot-nginx -y ``` ### 2. Install Certificate Run `certbot` to install the certificate. Full examples are below, here are descriptions of the command line options: - --apache - - Use the Apache web server - --nginx - - Use the nginx web server - --redirect - - Redirect all HTTP requests to HTTPS. - -d example.com -d www.example.com - - Install a multiple domain (SAN) certificate. You may use up to 100 -d domain entries. - -m admin@example.com - - The notification email address for this certificate. - --agree-tos - - Agree to the terms of service. Use `certbot --help` for more information. ### Example: One-Click LAMP (Apache) Run certbot for Apache. ```sh # certbot --apache --redirect -d example.com -d www.example.com -m admin@example.com --agree-tos ``` ### Example: One-Click LEMP (Nginx) 1. Before running certbot, make sure server_name is set properly. Edit your Nginx configuration: ```sh # nano /etc/nginx/conf.d/default.conf ``` 2. Update server_name to include your domain name. ```sh server { server_name example.com www.example.com; ``` 3. Save and exit the file. 4. Run certbot for Nginx. ```sh # certbot --nginx --redirect -d example.com -d www.example.com -m admin@example.com --agree-tos ``` ### 3. Verify Automatic Renewal Let's Encrypt certificates are valid for 90 days. The certbot wizard updates the systemd timers and crontab to automatically renew your certificate. 1. Verify the timer is active. ```sh # systemctl list-timers | grep 'certbot\|ACTIVATES' ``` 2. Verify the crontab entry exists. ```sh # ls -l /etc/cron.d/certbot ``` 3. Verify the renewal process works with a dry run. ```sh # certbot renew --dry-run ``` ### Summary Installing a free Let's Encrypt certificate is simple with certbot. For more information, see the official certbot installation documentation.
> Source: https://www.vultr.com/docs/update-ubuntu-server-best-practices ### Introduction It is a best practice to update your server on a regular schedule for security and stability. Use this guide to keep your Ubuntu server updated. Supported Versions This guide applies to: - Ubuntu 20.04 LTS - Ubuntu 19.10 - Ubuntu 18.04 LTS - Ubuntu 16.04 LTS ### Make a Backup Always make a backup before updating your system. ### 1. Update the Package Lists This command updates the package lists from the enabled repositories. ```sh $ sudo apt update ``` ### 2. List the Upgradable Packages This step is optional. To view the upgradable packages before performing the upgrade, use the apt list command. ```sh $ sudo apt list --upgradable ``` ### 3. Upgrade Packages This command will upgrade all the upgradeable packages. ```sh $ sudo apt upgrade ``` ### 4. Restart the Server ```sh $ sudo reboot ``` ### One Line Upgrade If you want to accept all the defaults and perform the upgrade without intervention, use this command: ```sh $ sudo apt update && sudo apt upgrade -y ``` ### Optional - Autoremove Use apt to remove old packages and dependencies automatically. ```sh $ sudo apt autoremove ```
> Source: https://www.vultr.com/docs/create-a-sudo-user-on-ubuntu-best-practices ### Introduction Performing server administration as a non-root user is a best practice for security. After launching your Vultr VPS, your first task as root should be to set up a non-root user with sudo access. This guide applies to the following versions: - Ubuntu 20.04 LTS - Ubuntu 19.10 - Ubuntu 18.04 LTS - Ubuntu 16.04 LTS ### 1. Add a New User Account Create a new user account with the `adduser` command. Use a strong password for the new user. You can enter values for the user information, or press `ENTER` to leave those fields blank. ```sh # adduser example_user ``` ### 2. Add the User to the Sudo Group Add the new user to the sudo group with usermod. ```sh # usermod -aG sudo example_user ``` ### 3. Test Switch to the new user. ```sh # su - example_user ``` Verify you are the new user with whoami, then test sudo access with sudo whoami, which should return root. ```sh $ whoami example_user $ sudo whoami [sudo] password for example_user: root ```
### Bundle 在使用 React 時是一定會使用到 Webpack 等等工具來把 JSX、CSS、IMAGES... 等等進行打包。如果你的 APP 已經建立得非常大時,產生的 Bundle 檔案也會非常巨大。使用者在下載時就會花上很多時間了喔 ! ### 解決方法 我們可以通過使用 React 內建的 Lazy loading 來解決一次過下載巨大 Bundle 檔案的問題。它的原理是把單一的 Bundle 檔案依不同的 package 來分割開,然後在使用者實際使用時才動態載入。 ### 內裏是 Promise 我們有以下的 module ,作用把兩個傳入的數字相加。 ```js // math.js export default sum(a, b) => a + b; ``` 在傳統的使用方法,我們是以下這樣 : ```js import sum from './math.js'; console.log(sum(1, 2)); // 3 ``` 使用 Lazy Loading 的話,載入的 object 就會變成了 Promise 物件: ```js import('./math.js') .then(sum => console.log(sum(1, 2))); // 3 ``` ### 實際使用 在實際使用上,我們可以通過使用 `React.lazy` 方法來簡單化整個載入的流程。 ```js // 載入 SomeComponent const SomeComponent = React.lazy(() => import('./SomeComponent.jsx')); ``` 然可以作為一般的 Component 來使用,但是需要放在 React.Suspense 元件內。 ```js <React.Suspense fallback={<div>Loading...</div>}> <SomeComponent /> </React.Suspense> ``` React.Suspense 是處理其子 Component 如果有 Lazy Loading 動作時,就會劃出 fallback 的元件出來,等待 Lazy Load 完成後,就會顯示元件。 ### 筆者推介使用方法 因為在 React 內得多元件都可以重用,但是如果有使用 React Router 的話,不同 URL 之間的元件是一定不會重用的,所以我們可以簡單把 React.Suspense 包住整個 Router。然後把不同 Route 內的 Component 都轉成使用 Lazy Load,這樣就已經可以簡單快速地分割出合適的 Bundle 檔案了。 ### 官方文件 官方文件 : https://zh-hant.reactjs.org/docs/code-splitting.html
### Image slider / Carousels 在網站上很多時都會使用流水式的 Banner 來告訴使用者最近的事情。  ### 選擇現成的 Library 初時筆者找到了 `react-alice-carousel` 這個套件,好像很簡單易用,所以後快速的試了一下。不過發現有個問題到現時最新的 Version 都好像還沒有修正好,就是在圖片滑動的中途,如果整個 Component 出現 unmount 的情況,就會出現 Error : Updated on unmounted component。 雖然在它們的 github 上好像沒有人 report 過這樣的 issue。不過筆者確實是遇到了,所以還是選擇另家的好了。 ### nuka-carousel 這是另一家大神寫出來的 Image slider,使用上也是非常之簡單。 Github: https://github.com/FormidableLabs/nuka-carousel 下面會簡單記錄一下使用的方法。 ### 安裝 可以使用 npm 來安裝。 ```sh $ npm install nuka-carousel ``` ### 使用 使用上也是十分之簡單,只要用 Carousel 包住想要用來 Slide 的內容就可以了,高度是自動的,內容可以用 Array 填入就可以。配上 Div 使用 background 來使用就非常得心應手。 ```js import React from 'react'; import Carousel from 'nuka-carousel'; export default class extends React.Component { render() { return ( <Carousel> <img src="https://via.placeholder.com/400/ffffff/c0392b/&text=slide1" /> <img src="https://via.placeholder.com/400/ffffff/c0392b/&text=slide2" /> <img src="https://via.placeholder.com/400/ffffff/c0392b/&text=slide3" /> <img src="https://via.placeholder.com/400/ffffff/c0392b/&text=slide4" /> <img src="https://via.placeholder.com/400/ffffff/c0392b/&text=slide5" /> <img src="https://via.placeholder.com/400/ffffff/c0392b/&text=slide6" /> </Carousel> ); } } ``` 送上 div 顯示圖片的咒語: ```js <div style={{ background: '#FFFFFF url(https://via.placeholder.com/400/ffffff/c0392b/&text=slide1) center center / cover no-repeat', height: '0px', paddingBottom: '30%' }} /> ``` 以下是筆者使用的 Config: ```js <Carousel autoplay={ true } autoplayInterval={ 4000 } withoutControls={ false } wrapAround={ true } speed={ 1000 } renderCenterLeftControls={ () => {} } renderCenterRightControls={ () => {} } > {items} </Carousel> ```
### 設定 Nginx 要在 nginx 上取得 proxy 的 client address,需要事先在設定 nginx 時就要加入下 `--with-http_realip_module` 的設定。如果安裝的 nginx 是由 apt 上下載的,就可以使用以下指令來查看你的 nginx 有沒有設定 `--with-http_realip_module` 模組。 ```sh $ nginx -V nginx version: nginx/1.18.0 (Ubuntu) built with OpenSSL 1.1.1f 31 Mar 2020 TLS SNI support enabled configure arguments: --with-cc-opt='-g -O2 -fdebug-prefix-map=/build/nginx-5J5hor/nginx-1.18.0=. -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-debug --with-compat --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module=dynamic --with-http_sub_module --with-http_xslt_module=dynamic --with-stream=dynamic --with-stream_ssl_module --with-mail=dynamic --with-mail_ssl_module ``` 如果在輸出的結看看到了模組的名稱,就代表已經設定好了。 ### 設定來自 Cloudflare 的 IP address 然後需要把來自 Cloudflare 的 IP address 設定到 nginx.conf 檔案內。 參考 Cloudflare 的網站 : https://support.cloudflare.com/hc/en-us/articles/200170786-Restoring-original-visitor-IPs-Logging-visitor-IP-addresses-with-mod-cloudflare- ```txt set_real_ip_from 103.21.244.0/22; set_real_ip_from 103.22.200.0/22; set_real_ip_from 103.31.4.0/22; set_real_ip_from 104.16.0.0/12; set_real_ip_from 108.162.192.0/18; set_real_ip_from 131.0.72.0/22; set_real_ip_from 141.101.64.0/18; set_real_ip_from 162.158.0.0/15; set_real_ip_from 172.64.0.0/13; set_real_ip_from 173.245.48.0/20; set_real_ip_from 188.114.96.0/20; set_real_ip_from 190.93.240.0/20; set_real_ip_from 197.234.240.0/22; set_real_ip_from 198.41.128.0/17; set_real_ip_from 2400:cb00::/32; set_real_ip_from 2606:4700::/32; set_real_ip_from 2803:f800::/32; set_real_ip_from 2405:b500::/32; set_real_ip_from 2405:8100::/32; set_real_ip_from 2c0f:f248::/32; set_real_ip_from 2a06:98c0::/29; ``` 再設定 Cloudflare 的 IP 寫入 header 欄位名稱。 ```txt real_ip_header CF-Connecting-IP; #real_ip_header X-Forwarded-For; ``` 設定好後,在 Nginx 的 Virtual Host Config 檔案內使用變數 $remote_addr 時,就可以取得 Cloudflare 傳入的真正地址了。 下面是 Virtual Host Config 檔案使用 Reverse proxy 的例子 : ```txt location / { proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Host $http_host; proxy_pass https://localhost:81; } ```
由於不同之間的 browser 都有差異,所以如果要保險起見,需要以多種的語法來最最得 scroll top 值來確保安全。而已經有大神寫好了這個簡單的語法 ! ```js window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0 ```
### 什麼是 favicon.ico favicon 是網站的圖案,在使用 Chrome 的情況下,favicon.ico 會顯示在 chrome tab 上。  在網路上有很多不同的網站可以將 png 變成為 ico 檔案。我們只需要把喜歡的圖片使用這些工具變為 ico 檔案就可以直接使用。 ### 使用 HTMLWebpackPlugin 在使用 webpack 時,我們可能要使用到 HTMLWebpackPlugin 來建立 html 的進入點 (特別是 react js)。我們可以通過設定 HTMLWebpackPlugin 的參數來設定 favicon。 ```js new HtmlWebpackPlugin({ favicon: Path.join(__dirname, 'src', 'favicon.ico') }) ``` 有關更多的使用方法,可以去 plugin 的官方 github 去查看 : https://github.com/jantimon/html-webpack-plugin
### Nginx 的設定檔 很多時我們都需要更新 nginx 的設定檔來設置新的功能到 nginx 上,但是在 production 的環境下,如果設定檔出錯可能會出現很大的狀況 !! 想安安全全的確保重新啟動 nginx 時不會出問題,最好是先檢查一下設定檔有沒有打錯的地方。 ### 檢查 我們可以通過使用以下的 command 來檢查 nginx 設定檔 : ```sh $ nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` 如果成功沒有錯的話就會告訴你 syntax is ok,出錯的話則會提示你在那裏出現問題 ! 絕對是變更設定檔後,重新啟動 nginx 是必要的動作之 1 啊 !
### Reverse proxy (反向代理服務器) Reverse proxy 是指由代理伺服器接收了來自 internet 的訪問,然後會把訪問轉發給內聯網上的伺服器,然後把伺服器回傳的結果返回給 internet 的使用者。而這個代理伺服器的工作就是 reverse proxy 了。 ### 為什麼要使用 reverse proxy 很多時我們的服務器是不會直接連上 internet 的,而是由一個 firewall 來分隔開 internet 和 intranet,以確保服務器的安全。所以我們需要一個 reverse proxy 來為各種不同服務的請求分流到不同的伺服器上。 ### Nginx Nginx 除是可以成為網站伺服器外,還可以作為一個 reverse proxy。只需要在 config 檔案內設定好就可以了。 設定檔位置 : `/etc/nginx/site-availables/default` 先來看一個最普通的設定: ```text # 設定 1 個 server 設置 server { # 監聽 port 80 listen 80 default_server; listen [::]:80 default_server; # 監聽 port 443 (使用 ssl 設定) listen 443 ssl default_server; listen [::]:443 ssl default_server; # 設定 ssl 使用的證書及密匙 ssl_certificate /etc/nginx/ssl/nginx.crt; ssl_certificate_key /etc/nginx/ssl/nginx.key; # 設定監聽的 domain (可以多於 1 個) server_name _; # 設定文件的根目錄 root /var/www/html/; # 設定預設要存取的文件 index index.htm index.html index.php index.nginx-debian.html; } ``` 我們把上面的設定檔修改一下,刪除下面這兩個設定 : ```text root /var/www/html/; index index.htm index.html index.php index.nginx-debian.html; ``` 然後在原有的位置加入下面的設定 : ```text # 指定收到 url 的工作 location / { # 加入額外的 http header proxy_set_header X-Forwarded-For $remote_addr; # 加入額外的 http header proxy_set_header Host $http_host; # 把訪問移交到目標伺服器 proxy_pass https://localhost:8080; } ``` 下面是完整的設定檔 : ```text # 設定 1 個 server 設置 server { # 監聽 port 80 listen 80 default_server; listen [::]:80 default_server; # 監聽 port 443 (使用 ssl 設定) listen 443 ssl default_server; listen [::]:443 ssl default_server; # 設定 ssl 使用的證書及密匙 ssl_certificate /etc/nginx/ssl/nginx.crt; ssl_certificate_key /etc/nginx/ssl/nginx.key; # 設定監聽的 domain (可以多於 1 個) server_name _; # 指定收到 url 的工作 location / { # 加入額外的 http header proxy_set_header X-Forwarded-For $remote_addr; # 加入額外的 http header proxy_set_header Host $http_host; # 把訪問移交到目標伺服器 proxy_pass https://localhost:8080; } } ``` 經過上面的設定,就可以把來自 port 80 的訪問轉發到同一主機的 port 8080 上去了。
### Nginx Nginx 除了可以用來作為 Web 伺服器外,它還可以用來設定為 proxy,如果配上 virtual host 的話就可以在一台主機上部署多個不同的 applications。 ### Virtual host 像 apache2 一樣,nginx 也可以以不同的 domain 來分別處理的動作,只需要在 config 檔案內設定好就可以了。 設定檔位置 : `/etc/nginx/site-availables/default` 先來看一個最普通的設定: ```text # 設定 1 個 server 設置 server { # 監聽 port 80 listen 80 default_server; listen [::]:80 default_server; # 監聽 port 443 (使用 ssl 設定) listen 443 ssl default_server; listen [::]:443 ssl default_server; # 設定 ssl 使用的證書及密匙 ssl_certificate /etc/nginx/ssl/nginx.crt; ssl_certificate_key /etc/nginx/ssl/nginx.key; # 設定監聽的 domain (可以多於 1 個) server_name _; # 設定文件的根目錄 root /var/www/html/; # 設定預設要存取的文件 index index.htm index.html index.php index.nginx-debian.html; } ``` 在 `server_name` 的設定內,`_` 是指預設的情況,即是沒有配合到合適的 domain name 情況。 如果我們在 `server_name` 後加入了 domain name,就會指定這個 server 的設定為監聽指定的 domain name 訪問。例如 : ```text # 監聽 domain name 為 19site.net 及 www.19site.net 的訪問 server_name 19site.net www.19site.net; ``` 通過以上的設定就可以依 domain name 來設定多個不同的服務在相同的 port 上。
Canvas 在 html 5 面世已經有一段時間,在使用上有很多大神門寫出了出色的 library 來把 canvas 的圖像 model 化。最近要要處理一個 project 是需要使用 canvas 來畫圖層。這次筆者就選擇了用 fabric.js 來開發這個功能。 ### fabric.js 先到官方網站看看 : http://fabricjs.com 由於沒有太多的時間來寫教學,所以都是主要記錄一下開發時遇到想要記低的事情。在寫這個文章時使用的是版本 4.1.0。 ### 安裝 可以使用 npm 來安裝,在 react js 內使用。 ```js import { fabric } from 'fabric'; ```
### Remove duplicated elements in array in JS (ES6) We have the following case: ```js // we have an array with some duplicated elements let data = [1, 2, 3, 1, 4, 2, 6, 5, 6, 2]; // we want to remove duplicated elements and get the result like below [1, 2, 3, 4, 6, 5]; ``` ### Set By using `Set`and spread syntax, we can easily achieve this: ```js // using set and spread syntax data = [ ...new Set(data) ]; console.log(data); > [1, 2, 3, 4, 6, 5]; ```
在網站開發工作上,Dialog 是一個很常會使用得到的元件。我們可以使用現成別人寫好的來達成,不需要自動重新寫一個。 ### SweetAlert2 SweetAlert2 是一個 Dialog 的 Library,用來在網站上顯示 Dialog。內建有很多使用情況及預設的 template 可以直接使用。  無論是純 Javascript 或是使用 ReactJS 都可以使用。 官方網站 : https://sweetalert2.github.io
### 自動提示 現今的瀏覽器很多時都會儲存起使用者輸入過的資料,以方便下次使用者需要再用時不需要重新載入。但是有時這樣的設定反而會引起麻煩,例如如果電腦是公用的,就可能會看到很多不同人輸入過的結果。 ### 關閉功能 我們可以通過對 text field 加入參數來獨立設定每一個欄位是否出現提示。 ```text autocomplete='off' ``` 實際使用 : ```html <input type='text' name='username' autocomplete='off' /> ``` 這樣在輸入文字時就不會出現提示字的選單了。
### 為何防止短時間內重覆執行功能? 現時得多的應用程式都是建立在 web service 上,在使用者介面及服務器之間的溝通,很多都會使用非同步來進行,以提升使用者的體驗。為免使用者可以重覆 submit 表單,在設計使用者介面時需要防止這個情況。例如把 submit disable 以防止使用者重覆按下。 ### Delay Call 要處理需要短時間內需要重覆執行的功能,但是又想限制在特定時間後才一次過累積執行 (例如即時提供 suggestion 的 combo box),就可以使用 Delay Call 的方法。 ### 實作方法 以下會使用 JS 來作個例子 : ```js // variable for saving delay call state let delayId = undefined; // function want to prevent multiple call in short moment const requestJson = () => { // do ajax call }; // find dom element and add click handler document.getElementById('myButton').addEventListener('click', evt => { // check delay call state if( typeof delayId === 'undefined') { // assign delay call id (timeout id) delayId = setTimeout(() => { // clear delay id delayId = undefined; // call function requestJson(); }, 1000); } }); ``` 以上的代碼可以防止 `requestJson` 在 1 秒在被重覆執行。
### Apache Apache (httpd) is a web server application that can turn you machine into a web server. ### Change apache settings Open file `/etc/apache2/site-available/000-default.conf. You may see the following content:  This is the default setting of Apache, you may copy these setting to create a new one.  Change port and document root. Then open file `/etc/apache2/ports.conf`.  Add `Listen 81` setting to config file. ### Check configuration file is valid Before restart Apache service, better check the config files syntax is valid. ```sh $ apachectl configtest ``` ### Restart service Restart Apache to apply new settings. ```sh $ sudo service apache2 restart ```
### 環境變數 使用過 NodeJS 有一段時間的朋友仔對環境變數應該都不會陌生,使用時只需要使用 `process.env` 物件就可以取得資料。 還可以通過使用 `delete` 來把變數刪除呢 ! ```js process.env.PRODUCTION = false; console.log(process.env.PRODUCTION); > false delete process.env.PRODUCTION; console.log(process.env.PRODUCTION); > undefined ``` 還是這樣來設定環境變數好像有點不切實際,變數都好像 hard code 在代碼中。 如有一個方法可以像 laravel 一樣載入 `.env` 檔案到變數就更加方便了。 ### 使用 dotenv 套件 已經有大神想到這一點了呢 ! 通過使用 `dotenv` 套件就可以達成。 #### 安裝套件 ```sh $ npm install dotenv ``` #### 使用 建立 `.env` 檔案。記要住 `.env` 檔案不要加入到 git 內喔 ! 如果需要為 `.env` 記錄參數的變更,可以另外再加入一個 `.env.example` 來加入 git。 ```text DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASS=password DB_NAME=my_database ``` 然後在程式的進入點 (app.js),載入套件。 ```js require('dotenv').config(); console.log(process.env["DB_HOST"]); > localhost console.log(process.env["DB_PORT"]); > 3306 console.log(process.env["DB_USER"]); > root console.log(process.env["DB_PASS"]); > password console.log(process.env["DB_NAME"]); > my_database ``` 這樣就可以了 !
### RTMP 這個 library 可能可以成功把影片以 RTMP 方式 push 到伺服器上,等待測試中。 https://github.com/begeekmyfriend/yasea ### YASEA 下載 Project 然後使用 Android Studio 啟動並 Complie,成功後填入 RTMP 伺服器的地址。  然後使用 VLC 播放器播放 RTMP 伺服器的串流。 
### VLC 要播放 RTMP 影片資料,最簡單可以使用 VLC 播放器。 Step 1, 下載並啟動 VLC 播放器, 官方網址 : https://www.videolan.org/vlc  Step 2, 按下 Media > Open Network Stream  Step 3, 輸入 RTMP 的地址  Step 4, 成功載入串流影片, 完成 
### Nginx 安裝 nginx 及 rtmp 模組 : ```sh $ sudo apt-get update $ sudo apt-get upgrade $ sudo apt-get install nginx -y $ sudo apt-get install libnginx-mod-rtmp -y ``` ### 修改 Config File ```sh $ sudo vi /etc/nginx/nginx.conf ``` 把以下的內容加入到 Config File 入面 : ```sh rtmp { server { listen 1935; chunk_size 4096; application live { live on; record off; } } } ``` ### 重新啟動 Nginx ```sh $ sudo systemctl restart nginx ```
我們可以通過 ffmpeg 來推送檔案的 data 到 rtmp server: ```sh $ ffmpeg -re -i /home/video.mp4 -vcodec copy -acodec copy -f flv rtmp://localhost/live/your_video_key ```  而 rtmp server 可以使用 nginx 和 nginx-rtmp 模組來建立。
### Sorting 排序資料很多時都會在程式中出現,今天要記下的事是在 Android (Java) 把 Array 排序。 排序的概念大約是把 Array 內的 Elements 依一個特定的規則來排好,例如數字由大至細。 下是是 Javascript 的例子 : ```javascript // data var data = [5, 2, 3, 4, 1]; // data sort data.sort(); // print data console.log(data); > 1,2,3,4,5 ``` 預設的 Sort 大約會有以上的效果,它可以自動理解 data type 然後進行排序。如果遇到不會處理的 Data type,就只好使用 `toString()` 方法來取出字串。 ### 自訂排序的方法 上面提到我們可以自訂排序的方法,可以透過傳入一個 function 來處理排序的邏輯。 ```javascript // data var data = [5, 2, 3, 4, 1]; // data sort data.sort((a, b) => { if( a > b ) { return 1; } if( a < b ) { return -1; } else { return 0; } }); // print data console.log(data); > 1,2,3,4,5 ``` 我們傳入的方法只需要告訴排序程式當遇到兩者要比較時的結果,而回傳就通常時 -1, 0, 和 1 三個值。 ### 在 Java 上實作 以下的代碼是在 Java 上實作的例子 : ```java // array data int[] data = new int[]{4, 2, 5, 1, 3}; // do sort data Arrays.sort(data, new Comparator<Integer>() { @Override public int compare(int a, int b) { if( a > b ) { return 1; } if( a < b ) { return -1; } else { return 0; } } }); ``` 透過使用 interface (Comparator) 來告訴 sort 是怎樣進行的。
### Bitmap 在 Android 內使用 Bitmap 是十分常見的事,有時我們可能需要把 Bitmap 存為 JPEG 檔案,應該要怎麼做呢? ### Bitmap Compress 原來 Bitmap 自己本身已經寫好了有關的方法,我們只需要使用就可以了。 ```java // file output stream FileOutputStream fos = new FileOutputStream(new File("PATH_TO_FILE")); // compress bitmap as jpeg format with 85% quality bitmap.compress(Bitmap.CompressFormat.JPEG, 85, fos); // flush stream fos.flush(); // recycle bitmap memory bitmap.recycle(); ``` 真的是非常之方便 !
### Gallery Images / Videos 在 Android 上要取出 Gallery 的相片 / 影片,在以前我們可以使用 media query 來達成,但是在 API 的變更後,現在要改為使用 content resolver 來達成了。 ### Content Resolver 其實就是簡單的一句可以取得 content resolver,和以前 media query 比起來只差一點點。 ```java context.getContentResolver(); ``` 以下是一個完整的代碼來用取出 Gallery 內的圖片。 ```java // select image data Uri uri = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI; // columns for each row String[] projection = { MediaStore.MediaColumns.DATA }; // run query Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); // get column index int index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA); // do each cursor while (cursor.moveToNext()) { // get the file path String path = cursor.getString(index); // do something with the file path } ``` ### URI 不同的 URI 是指對應不同的資料表 (Table),下面會列出兩個最常用的 Uri : 指向圖片的資料表 > android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI 指向影片的資料表 > android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI
這個五六月真的是兩個很忙的月分,讀書考試及工作上的事情都弄到透不過氣來。 今天終於都有時間定下來可以寫一下 19 Site。 今年修讀的三個 OU 科目 (COMPS224, 332, 368) 都已經全部完成考試,現在也就是等放成績的時間。因為肺炎疫情關係,所有的考試都變為了 Take-Home Exam / Take-Home Assignment 來進行,當然也變得有辣有唔辣。 辣的地方是因為變成為了 Open Book Exam,所以題目的出題方法變得非常 Open Ended。除非已經深入了解課文後的原理,否則靠死背書是必死的。因為寫 Definition 的題目只餘下大約 10 分左右,用來拉及格是可以的,但是靠它來取分就必死了。而唔辣的地方就當然是不同背書,可以在書中 Copy & Paste 需要的內容就可以,大大減少寫字的時間。 總的來說這次的考試安排都唔錯,系統順暢,不過在 Submit 考卷時卡住。開發人員值得一讚,也辛苦了 CC's 及各 Tutor's 了。 不知道是不是因為疫情的影響,公司的客戶常常想到新的些法去改變現有的東西,東改改西改改,常常要花時間去了解客戶想要的真正意思。真的花掉了很多時間。
長話短說,先看一看代碼再繼續 : ```js const copyToClipboard = str => { const el = document.createElement('textarea'); el.value = str; document.body.appendChild(el); el.select(); document.execCommand('copy'); document.body.removeChild(el); }; ``` ### 原理 以下會解釋上面代碼的原理 : 1. 建立一個 `<textarea>` 元件 2. 把 `<textarea>` 元件的內容設定為目標的文字 3. 把 `<textarea>` 元件加入到 `document` 底下 4. 利用 `select()` 方法把 `<textarea>` 元件的內容選擇起來 5. 利用 `document.execCommand('copy')` 把選取的內容複制到剪貼簿 6. 把`<textarea>` 元件從 `document` 中移除 非常簡單但是十分有用。
連接 DB 是一件必定要進行的事,在 Laravel 可以使用 ORM 去完成,而在 NodeJS 上,筆者用得最多是使用 npm mysql 去完成。雖然 NodeJS 上的 mysql library 是一個寫得很成熟的東西,但是在建立 query 時還是有很多不足的地方。最明顯的地方是在建立 conditional where 時。 ### 例子 以下是一個壞的例子 : ```js var sqlParams = []; var where = []; // conditional where case if( typeof id === 'number' ) { where.push('id = ?'); sqlParams.push(id) } // sql var sql = 'select * from users where ' + where.join(' and '); // execute query db.query(sql, sqlParams, callback); ``` 相信這樣的 SQL Statement 大家都有可能寫過 / 見識過。雖然上面的寫法還不致於十分混亂,但是讀起來也不是太方便。如果又要同時用上 AND 及 OR 時,可能就會出現比較混亂的情況了 ! ### Knex.js - A SQL Query Builder for Javascript 這是 Knex.js 自己介紹自己的,它是一個 SQL Query Builder。 不過除此之後它還會自行管理 Connection 的生命週期,使用 Connection Pool 的話還會自行 release connection,算是一個非常便利及強勁的 Database adapter。 官方網站 : http://knexjs.org 以下是使用 Knex 的方法來重寫一次上面的代碼 : ```js // query var query = knex('users'); // conditional where case if( typeof id === 'number' ) { query.where('id', id); } // execute query var result = await query; ``` 很神奇吧。
在大多數的情況下,如果需要對調兩個變數的值,就需要使用到第三個變數來記錄暫存的值 : ```js temp = x; x = y; y = temp; ``` 然而,其實是可以使用 XOR 來耍點技巧來達成,不需要使用到 temp 變數 : ```js x = x xor y; y = x xor y; x = x xor y; ``` 不相信 ? 以下會進行一個實際的例子。 ```js // init values var x = 34; // 34 var y = 78; // 78 // do the swap x = x xor y; // 108 y = x xor y; // 34 x = x xor y; // 78 // final values y; // 34 x; // 78 ``` ### 什麼是 XOR XOR 是對 bit 的運算,以下例子可以說明 XOR 的動作 : |x|y|x xor y| |---| |0|0|0| |0|1|1| |1|0|1| |1|1|0| ### 動作分析 我們把上面的操作,使用不同的變數儲存起來,就會比較容易看得出分別。 ```js x1 = x xor y y1 = x1 xor y x2 = x1 xor y1 ``` 使用上面的數字 ( x = 34, y = 78 ),我們使用二進為計算一下。 #### 第一步 ```js x1 = x xor y ``` 我們使用二進位來計算說明 : ||||||||| |---| |||1|0|0|0|1|0| |xor|1|0|0|1|1|1|0| ||1|1|0|1|1|0|0| 而二進位 1101100 就是十進位的 108。 #### 第二步 ```js y1 = x1 xor y ``` 我們使用二進位來計算說明 : ||||||||| |---| ||1|1|0|1|1|0|0| |xor|1|0|0|1|1|1|0| ||0|1|0|0|0|1|0| 而二進位 100010 就是十進位的 34,已經把 x 的值移到 y 去了。 #### 最後一步 ```js x2 = x1 xor y1 ``` 我們使用二進位來計算說明 : ||||||||| |---| ||1|1|0|1|1|0|0| |xor||1|0|0|0|1|0| ||1|0|0|1|1|1|0| 而二進位 1001110 就是十進位的 78,已經把 y 的值移到 x 去了。 這樣就已經完成把變數 x 和 y 互換了 ! ### 使用純數的角度去證明 以下是證明 x2 = y 的算式 : ```js x2 = x1 xor y1; x2 = x1 xor (x1 xor y); // replace y1 x2 = (x1 xor x1) xor y; // associative law x2 = 0 xor y; // a xor a => 0 x2 = y; // 0 xor a => a; x2 now has y's original value ``` 以下是證明 y1 = x 的算式 : ```js y1 = x1 xor y y1 = (x xor y) xor y y1 = x xor (y xor y) y1 = x xor 0 y1 = x ``` 很有趣吧 !
在 ExtJS 內,我們會用 Proxy 來載入 Store 內容。以下是 Proxy 會自動加入的參數 : - start - (number) 像 SQL 的 offset - limit - (number) 像 SQL 的 limit - page - (number) 頁數 - sort - (string) JSON format 的字串,用來排列結果 但是有時我們可能需要加入更多的參數才可以抽出資料,那應該要怎樣做呢? ### 動態方法 可以通過使用 `Proxy.setExtraParams()` 來動態設定額外的參數。 ```js grid.getStore().getProxy().setExtraParams({ foo: 'bar' }); grid.getStore().load(); ``` ### 靜態方法 可以通過加入 `extraParams` 到 Proxy 來設定額外的參數。 ```js new Ext.data.Store({ autoLoad: true, proxy: { url: 'users.php', type: 'ajax', extraParams: { foo: 'bar' } } }); ```
使用 ExtJS 就一定會自定義一些 Component 來使用,而自定義的 Component 如果要把 UI 的互動動作傳出去的話,就需要自定義 Event 了。 ### 自定義 Component 看看下面例子 : ```js Ext.define('MyApp.CustomForm', { extend: 'Ext.form.Panel', layout: { type: 'vbox' }, items:[{ xtype: 'textfield', name: 'name' }, { xtype: 'button', text: 'submit' }] }); ``` 上面定義了一個 `MyApp.CustomForm` 元件,入面定義了一個 submit button。如果要把 submit button 的 click 事件傳出去給 `MyApp.CustomForm` 接收,應該要怎樣做呢? ### 使用 `.fireEvent()` 方法 我們可以通過使用 `.fireEvent()` 方法來對元作發動事件 : ```js { xtype: 'button', text: 'submit', listeners: { click: function(ele) { ele.up('form').fireEvent('submitclick', 'message'); } } } ``` 上面的代碼是使用 `MyApp.CustomForm`發動 `submitclick` 事件,並傳入 'message' 字串作為參數。 在生成 `MyApp.CustomForm` 元件時,我們可以設定 `submitclick` 事件的 listener。 ```js var form = Ext.create('MyApp.CustomForm'); form.on('submitclick', function(message) { console.log(message); }); ``` 這樣當 `MyApp.CustomForm` 內的 `button` 按下時,`MyApp.CustomForm` 就會發動 `submitclick` 事件所連結的 listeners。
JSON 的 Structure 可以像一個樹一樣去表達資料,但是傳統的 Relational Database 只可以使用一張張的 Table 來儲存資料,如果要把數張 Table 的內容合併為一個 JSON 來使用的話,可以通過使用 mapping 的方法把關聯的 ID 值串合起來。 ### 例子 考慮到有以下的資料: Table: users |id|name|age| |---| |1|peter|11| |2|tom|12| |3|mary|13| Table: user_fruits |id|user_id|fruit| |---| |1|1|apple| |2|1|orange| |3|2|apple| |3|2|banana| |3|3|orange| |3|3|banana| |3|3|apple| 考慮到要抽出以下的 JSON 格式 : ```json [ {"name":"peter", "age": 11, fruits:["apple","orange"]}, {"name":"tom", "age": 12, fruits:["apple","banana"]}, {"name":"mary", "age": 13, fruits:["orange","banana","apple"]} ] ``` 應該要如何轉換呢? ### 解決方法 使用 Loop 是必需要的,因為而一個個 Data Object 互相 Mapping。不過在語法上可以使用 JS 一句內完成的。 ```js users.forEach(user => user.fruits = userFruits.filter(userFruit => user.id === userFruit.user_id).map(userFruit => userFruit.fruit)); ``` 以下是一個完整的示範 : ```js // get users var users = await db.query(`select * from users`); // fetch user id as array var userIds = users.map(user => user.id); // check users is empty if( users.length > 0 ) { // fetch user fruits by user id var userFruits = await db.query(`select * from user_fruits where user_id in (?)`, [userIds]); // map user fruit data to users users.forEach(user => user.fruits = userFruits.filter(userFruit => user.id === userFruit.user_id).map(userFruit => userFruit.fruit)); } ```
ExtJS 的 Combobox 方便好用,配上 Proxy 後可以自動載入 Ajax 的 JSON 內容。不過 API 內就好像沒有一個方法設定自動選擇第一選項。那應該要如何達成呢? ### 使用 Store 的 load 事件 我們可以通過使用 Store 的 load 事件,把 load 入 Store 的記錄抽取出來,然後使用 Combobox 的 `select(r)` 來設定選擇項目。 ```js /** * on load event listener callback */ function load(this, records, successful, operation, eOpts) {} ``` 可以透過 `load` 事件來取得 `records` 資料。 不過我們無法從 Store 取得 Combobox 元件,因為一個 Store 可以 Attach 到很多不同的元件,所以我們需要在第三方的元件上設定好關聯事件。這最好是 Combobox 的 top parent 元件。 ### 設定方法 以下時通過使用 `load` 事件來自動選擇第一選項目的例子 : ```js combobox.getStore().on('load', function(ele, records) { if( records.length > 0 ) { combobox.select(records[0]); } }}; ``` 而這段代碼運行的最佳時機,可以是 combobox 元件的 top parent 元件的 `initComponent()` 方法內。
 要達到可以在 Grid 內即時修改 Cell 的內容,我們要為 Grid 加入 Plugin 設定才可以。 ### 為 Grid Panel 加入 Plugin 設定 我們需要為 Grid Panel 加入以下的設定 : ```js { selModel: 'cellmodel', plugins: { ptype: 'cellediting', clicksToEdit: 1 } } ``` ### 參數解說 - selModel : 選擇 Grid 內容時所以使用的 Model - ptype : Plugin 的種類 - clicksToEdit : 按多少下滑鼠可以進入修改模式 ### Column 設定 除了要設定好 Grid 外,還需要為你目標相修改的 column 進行設定 : ```js { columns: [ {header: 'name', dataIndex: 'name', editor: 'textfield'}, {header: 'name', dataIndex: 'name', editor: { completeOnEnter: false, field: { xtype: 'textfield', allowBlank: false } }, ] } ``` ### 參數解說 - editor (String) - 可以直接使用 xtype 來指定修改器的種類 - editor (Object) - 可以通過使用 field 方法來指定修改器的種類,更適合像 Combobox 一類的 Component。 ### 例子 以下例子是從 ExtJS 的官方說明文件中節錄出來 : https://docs.sencha.com/extjs/6.0.2/classic/Ext.grid.plugin.CellEditing.html ```js Ext.create('Ext.data.Store', { storeId: 'simpsonsStore', fields:[ 'name', 'email', 'phone'], data: [ { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224' }, { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234' }, { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' }, { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' } ] }); Ext.create('Ext.grid.Panel', { title: 'Simpsons', store: Ext.data.StoreManager.lookup('simpsonsStore'), columns: [ {header: 'Name', dataIndex: 'name', editor: 'textfield'}, {header: 'Email', dataIndex: 'email', flex:1, editor: { completeOnEnter: false, // If the editor config contains a field property, then // the editor config is used to create the Ext.grid.CellEditor // and the field property is used to create the editing input field. field: { xtype: 'textfield', allowBlank: false } } }, {header: 'Phone', dataIndex: 'phone'} ], selModel: 'cellmodel', plugins: { ptype: 'cellediting', clicksToEdit: 1 }, height: 200, width: 400, renderTo: Ext.getBody() }); ```
 ExtJS 的 Form 很方便,有時我們需要從伺服器上載入 JSON 內容,然後把 JSON 的內容放回入 Form 內供使用者修改,然後再儲存回到伺服器上。 ### 伺服器 AJAX 回傳的 JSON 假設呼叫伺服器一個 `users.php` 會回傳以下的 JSON 資料: ```json { "success":true, "user":{ "name":"19site", "age":20 } } ``` ### Ext.form.Panel 元件 另外有一個 `Ext.form.Panel` 元件 : ```json { xtype: 'form', layout: { type: 'vbox' }, items:[{ name: 'name', fieldLabel: '名稱' }, { name: 'age', fieldLabel: '年齡' }] } ``` ### 實作方法 我們使用 `Ext.Ajax.request` 呼叫 `users.php` 後,可以使用以下方法把 JSON 直接載入到 `Ext.form.Panel` 內 : ```js form.getForm.setValues(data.data); ``` 我們是可以使用 `form.getForm.setValues(data)` 來把 JSON 直接定到到 Form Panel 內,JSON 的 KEY 會對應 Form Panel 內 Text Field 的 name 來配對,設定相應的 value。 ```js var form = view.down('selector of form'); Ext.Ajax.request({ url: 'users.php', success: function(res) { var data = JSON.parse(res.responseText); form.getForm.setValues(data.data); } }); ```
當 Android 由 Action Bar 到變成使用 Toolbar 後,雖然兩者大體上是差不多的,不過都有少少地方是有不同,例如如果要在 Toolbar 上顯示 Back Arrow 應該要怎樣做呢?  ### 把 Toolbar 設定為 ActionBar 我們可以把 Toolbar 設定為 ActionBar,這樣就可以使用以前的方法來把返回的按鈕設定出來。 把 Toolbar 設為 support action bar : ```java Toolbar toolbar = (Toolbar) findViewById(R.id.my_toolbar); setSupportActionBar(toolbar); ``` 然後就可以用以前的方法來設定顯示返回按鈕 : ```java etSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); ``` 記住要 override 入下面的方法才可以處理按下的事件 : ```java @Override public boolean onSupportNavigateUp() { onBackPressed(); return true; } ```
### Component 大小 使用 XML 可以為 Layout 內的 Component 設定高度大小等,用的單位可以自行選擇,只要鍵入在數字後便可以。 ```xml <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center|center" android:text="text" android:textSize="20sp" /> ``` ### 使用 Programming 方法設定 我們也可以使用 Programming 的方法來生成一個 TextView。 ```java // layout params ViewGroup.LayoutParams mLayoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); // create text view TextView mTextView = new TextView(context); mTextView.setLayoutParams(mLayoutParam); mTextView.setGravity(Gravity.CENTER|Gravity.CENTER); mTextView.setText("text"); mTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); ``` 上面的代碼應該也能生成一個和上面 XML 相同的 TextView。我們再修改一下上面的 XML : ```xml <TextView android:layout_width="match_parent" android:layout_height="100dp" android:gravity="center|center" android:text="text" android:textSize="20sp" /> ``` 這樣可以生成一個高度 100dp 的 TextView,於是我們又改一下 Java Program : ```java // layout params ViewGroup.LayoutParams mLayoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 100); // create text view TextView mTextView = new TextView(context); mTextView.setLayoutParams(mLayoutParam); mTextView.setGravity(Gravity.CENTER|Gravity.CENTER); mTextView.setText("text"); mTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); ``` 這樣能生成一個 100dp 的 TextView 嗎? 答案是不行的,因為 LayoutParams 沒有像 setTextSize() 一樣能接受一個 Unit (單位) 的值傳入去,所以上面傳入去的 100 只是一個不知道什麽單位的數字,要達到使用 dp 單位的效果,需要加多一點計算才可以啊 ! ### 算出 dp 值 通過下面的方法,我們可以計算出不同單位的數值 : ```java // get resources Resources r = getResources(); // target height in dp int height = 100; // calculate number float heightInDp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, r.getDisplayMetrics()); ``` 然後就可以把計算出來的值放入 LayoutParams 內,就可以設定出相應的單位。 ```java // get resources Resources r = getResources(); // target height in dp int height = 100; // calculate number float heightInDp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, r.getDisplayMetrics()); // layout params ViewGroup.LayoutParams mLayoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, heightInDp); // create text view TextView mTextView = new TextView(context); mTextView.setLayoutParams(mLayoutParam); mTextView.setGravity(Gravity.CENTER|Gravity.CENTER); mTextView.setText("text"); mTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); ``` ### TypedValue 的種類 TypedValue 有以下的類型 (不同的單位) : |常數|說明| |---| |TypedValue.COMPLEX_UNIT_PX|Pixels| |TypedValue.COMPLEX_UNIT_SP|Scaled Pixels| |TypedValue.COMPLEX_UNIT_DIP|Device Independent Pixels|
### new vs newInstance() 我們有一個 Class 叫 MyFragment.java,內容如下 : ```java /** * my fragment */ public class MyFragment extends Fragment { public static MyFragment newInstance() { return new MyFragment(); } } ``` 我們可以通過下面的代碼來生成 MyFragment 的 Object : ```java // create fragment using new Fragment f1 = new MyFragment(); // create fragment using static method Fragment f2 = MyFragment.newInstance(); ``` 接觸過 Android Programming 的朋友都會見過上面兩種方法,那一種才是正確的方法呢 ? ### Android 的生命週期 上面的答案和 Android 的生命週期有極大的關係,因為 Android 會把不在面前 (顯示中) 的 Fragment Recycle 掉,然後在 Fragment 再出現 (例如使用者按了 Back 制) 時重新建立。這時,Android 是會使用 new Fragment() 的方法建立新的 Fragment 重新顯示。 ### 產生問題 如果 Fragment 是沒有參數的話,是沒有任何問題的,像上面的 MyFragment 一樣。我們改寫一下 MyFragment 的內容,讓它可以接收參數 : ```java /** * my fragment */ public class MyFragment extends Fragment { private int id; public static MyFragment newInstance() { return new MyFragment(); } public MyFragment() { super(); } public MyFragment(int id) { super(); this.id = id; } } ``` 有了新的 Constructor 後,我們可以在 `new MyFragment()` 時傳入 id 參數了 ! ```java // create fragment with arguments Fragment f1 = new MyFragment(1); ``` 這樣很完美 ! 但是我們忘記了 Android 可是會因為節省記憶體而把 Fragment 斬掉,然後需要再顯示時以 new Fragment() 重新建立,那我們傳入的 id 不就會一同被斬掉了嗎 ? 答案是對的,id 會隨著 Fragment 斬掉而一同遠去 ... ### 解決方法 如果有看過另一篇文章講解過 [Android 為 Fragment 傳入參數](https://cdn.19site.net/posts/118)的朋友,會知道我們可以使用 Bundle 來為 Fragment 傳入參數 ! Bundle 不會因為 Fragment 的斬掉而清走,當 Android 重新建立 Fragment 時,先前的 Bundle 會重新連接上,故可以使用 `getArguments()` 取得 Bundle 物件進行設定動作。而 newInstance 的作用就是為了把傳入到 Fragment 的參數規範起來。 我們再把上面的 `MyFragment.java` 改寫一下 : ```java /** * my fragment */ public class MyFragment extends Fragment { private int id; public static MyFragment newInstance(int id) { Bundle bundle = new Bundle(); bundle.putInt("id", id); MyFragment fragment = new MyFragment() fragment.setArguments(bundle); return fragment ; } } ``` 使用這樣的方法來傳入參數,就可以確保 Fragment 重新建立時不會有缺失了 ! ```java // create fragment with arguments Fragment f1 = MyFragment.newInstance(1); ``` 官方說明文件 https://developer.android.com/reference/android/app/Fragment.html#Fragment()
在使用 Fragment Transaction 時,我們可以為 Fragment 傳入一些參數,來改變 Fragment 的起始設定。 ### 使用 Bundle 在 Android 中,Bundle 就像一個 JSONObject 物件,可以使用 Key-Value 的方式來放入 / 取出一些常用的資料型別。我們可以通過 Bundle 來把資料傳入到 Fragment 內。 ### 例子 以下代碼會建立一個 Bundle Object,然後設定 id 及 name 值,再把 Bundle Object 傳入到 MyFragment 內 : ```java // create bundle object Bundle bundle = new Bundle(); bundle.putInt("id", 1); bundle.putString("name", "19Site"); // create fragment object Fragment fragment = new MyFragment(); // set bundle as fragment arguments fragment.setArguments(bundle); // prepare fragment transaction FragmentTransaction mFragmentTransaction = getSupportFragmentManager().beginTransaction(); mFragmentTransaction.addToBackStack(null); mFragmentTransaction.replace(R.id.frame_layout, fragment); mFragmentTransaction.commit(); ``` 在檔案 `MyFragment.java` 內,我們可以使用 `getArguments()` 來取得傳入的 Bundle Object : ```java // get arguments Bundle bundle = getArguments(); // get values from bundle int id = bundle.getInt("id", 0); String name = bundle.getString("name", null); ```
### FileProvider 先前有一篇文章提及到為什麼會有 FileProvider 的出現,內容可以參考那篇文章。 [Android Intent 透過 FileProvider 分享檔案的使用權限](https://cdn.19site.net/posts/113) 主要也是為了加強對檔案的安全性管理及加強對內容檔案的操作強化。 ### 如何設定 FileProvider 我們要先在檔案 `AndroidManifest.xml` 一段對 Provider 的定義 : ```xml <provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:enabled="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> ``` 然後在 `res/xml/file_paths.xml` 加入以下內容,沒有檔案不存在就建立一個新檔案 : ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <external-files-path name="external_files" path="." /> </paths> ``` 依照官方的說明文件,這這檔案可以設定更多的內容,使這個 APK 能支援更多的檔案輸出路徑。 https://developer.android.com/reference/androidx/core/content/FileProvider 以下是 XML 設定對應的 Java 方法表格 : |XML 設定|Java 方法| |---| |files-path|Context.getFilesDir()| |cache-path|Context.getCacheDir()| |external-path|Environment.getExternalStorageDirectory()| |external-files-path|Context#getExternalFilesDir(String), Context.getExternalFilesDir(null)| |external-cache-path|Context.getExternalCacheDir()| |external-media-path|Context.getExternalMediaDirs()| 只要設定好對的內容,就可以讓 Intent 的目標讀取得到 URI 的內容。 ### 實例 以下是把一個儲存在 APP 自己 DATA 內的檔案想透過 FileProvider 讓外部可以讀取。 檔案 `AndroidManifest.xml` 有以下設定 : ```xml <provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:enabled="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> ``` 檔案 `res/xml/file_paths.xml` 有以下設定 : ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <files-path name="my_videos" path="videos/" /> <files-path name="my_images" path="images/" /> </paths> ``` 然後在 Java 程式內使用 FileProvider 取得檔案的 Uri : ```java // video file path File mFilePath = new File(context.getFilesDir(), "videos"); // file from application directory File mFile = new File(mFilePath, "myvideo.mp4"); // get uri via file provider Uri mUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", mFile); mUri.toString(); >>> content://com.example.fileprovider/my_videos/myvideo.mp4 ``` 我們會發現 FileProvider 還可以改寫來自不同 Directory 的名稱,甚至可以用來限制只輸出某一個 sub directory 的檔案,達到進一步的安全效果。 再看看下一個例子 : ```java // video file path File mFilePath = new File(context.getFilesDir(), "images"); // file from application directory File mFile = new File(mFilePath, "myimage.jpg"); // get uri via file provider Uri mUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", mFile); mUri.toString(); >>> content://com.example.fileprovider/my_images/myimage.jpg ``` 從輸出的效果我們可以推算出 `res/xml/file_paths.xml` 設定對 FileProvider 輸出的影響。 ### 秘技 功能愈多設定也會愈複雜,要一個個路徑設定到 `file_paths.xml` 實在是費時又易出錯,所以在普通的情況下,大家可以使用以下的設定來減輕功夫。 ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="external" path="." /> <external-files-path name="external_files" path="." /> <cache-path name="cache" path="." /> <external-cache-path name="external_cache" path="." /> <files-path name="files" path="." /> </paths> ``` 要注意,以上的設定可以把你 App 內的所有檔案透過 FileProvider 分享出去。不過也不必太擔心,因為只能透過 FileProvider 主動分享出去,對算對方 App 知道 content uri 也好,也是無法讀取到檔案的。
### Android FileProvider Android 使用 FileProvided 的時候,可能會遇到這個問題。 ```text FATAL EXCEPTION: main Process: com.example.your.andrroidapp java.lang.IllegalArgumentException: Failed to find configured root that contains /path/to/your.file ... error stacks ``` ### 問題源因 遇到這個問題的原因,是因為沒有設定好 FileProvider 要存取的路徑。 假設在 `res/xml/file_paths.xml` 有以下內容 : ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <external-files-path name="external_files" path="." /> </paths> ``` 然後用以下的代碼運行 : ```java // file from application directory File mFile = new File(context.getFilesDir(), "myvideo.mp4"); // get uri via file provider Uri mUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", mFile); ``` 如果我們使用上面的代碼來取得檔案 URI 的話,就會出現錯誤。因為 `context.getFilesDir()` 對應的 XML 是 `files-path`,但是在 `res/xml/file_paths.xml` 內沒有相關的設定。 有關 file_paths 的設定可以參考這篇文章 : [Android FileProvider (API 24+)](https://cdn.19site.net/posts/117) ### 解決方法 修改一下上面的 XML 設定 : ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <files-path name="files" path="." /> <external-files-path name="external_files" path="." /> </paths> ``` 再運行一次這段代碼 : ```java // file from application directory File mFile = new File(context.getFilesDir(), "myvideo.mp4"); // get uri via file provider Uri mUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", mFile); mUri.toString(); >>> content://com.example.fileprovider/files/myvideo.mp4 ``` 這樣就應該能成功運作了。
RecyclerView 是 Android 一個不可以缺少的 Component,雖然初學時用起上來非常之複雜,但是學會了後是一個絕對強勁的東東。 ### 設定 Adapter 在使用 RecyclerView 時也是需要設定 Adapter 才能夠使用,像從前的 ListView 一樣 : ```java // check adapter has initialized if( adapter == null ) { // new adapter adapter = new YourAdapter(); // set adapter to recycler view mRecyclerView.setAdapter(adapter); } ``` ### 問題產生 上面的 Code 應該是日常用來設定 RecyclerView Adapter 時用到。但是原來以上的 Code 會出現一個問題 !! 就是當 RecyclerView 重新建立過後,但上一個 adapter 沒有變成 null 時,新的 RecyclerView 就永遠不會設定 adapter 了 !! 這個情況會當使用者離開了這個 Fragment 後,在下一個 Fragment 使用 Back press 退行時發生,因為在後排的 View 已經 destroy 了,所以在載入時會運行一次 `onCreateView()` 重新畫出 UI,但是先前的 Adapter 卻沒有清空變成 null 的,所以 if 入面的條件是永遠不能進入。 ### 解決方法 我們應該雖要檢查 RecyclerView 的 Adapter 是否有設定。 ``` // check adapter has initialized if( adapter == null ) { // new adapter adapter = new YourAdapter(); } // check recycler view has set adapter if( mRecyclerView.getAdapter() == null ) { // set adapter to recycler view mRecyclerView.setAdapter(adapter); } ``` 簡單加入檢查,就可以確保萬無一失了 !
在 Java 上,我們會很常使用到 `instanceof` 來檢查某一個 Object 是否由特定的 Class 生成出來。除此之後我們還可能會比對一下兩個 Object 是否由同一個 Class 生成出來。可是應該要怎樣做呢? ### 比對兩個 Object 是否由同一個 Class 生成 我們可以通過使用 `.getClass()` 方法從 Object 中取得 Class 值。 ```java String a = "foo"; a.getClass(); ``` 既然能夠取得 Class 值,我們只要對比一下 Class 是否相同就可以判定是否由同一個 Class 生成。 ```java String a = "foo"; String b = "bar"; boolean result = a.getClass().equals(b.getClass()); ```
 自從 Android 上左 API 24 後,所有使用 Intent 進出的檔案路徑都要使用 FileProvider 的封裝才可以讓第三方的 APK 讀取得到,政策實行已久但是常常會也記不起語法,所以記錄一下。 ### 以前的方法 API 24- 下面是一個例子示範,以先前 ( API 24 之前 ) 的方法去把影片傳給第三方 APK 來播放 : ```java Uri.fromFile(new File("path/to/your/file")); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, getContentResolver().getType(uri)); startActivity(intent); ``` ### 現在的方法 API 24+ 現在需要使用 FileProvider 來封裝 URI,讓第三方的 APK 不知道真實檔案的位置 : ```js Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", new File("path/to/your/file")); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, getContentResolver().getType(uri)); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent); ``` 有一點要記住記住記住 (重要的事要說三次),必定要加以下這句才可以成功運行的,不然目標的 Intent Receiver 會讀不到檔案。 ```java intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); ``` 有關 FileProvider 的設定,我們在第二篇文章講解。
### Mime-Types Mime-Types 也就是 Content-Types,是指網際網路媒體類型。說白一點就是傳送過來的資料是什麼類型,好讓 Browser 去正確解讀檔案。以下會列出幾個最常見的 mime-type。 |副檔名|對應 Mime-Types| |---| |html|text/html| |txt|text/plain| |js|application/javascript |jpg|image/jpeg| |png|image/png| |mp4|video/mp4| ### 實際應用 處理 HTTP 上載檔案時,難免要處理一些檔案的 mime-types 事情。現實中是很難記得每一種檔案的 mime-types,由其是 AWS S3 API 在 `upload` 時是必需要指定上載檔案的 mime-types,所以我們必需要找個好幫手去代勞 !! NPM 上已經有大神寫好了程式庫,只要學會用就可以輕鬆取得檔案對應的 mime-types 了。 ```sh $ npm i mime-types ``` 程式試調 : ```js // import mime-types var mime = require('mime-types'); // loopup mime.lookup('json'); // application/json mime.lookup('.md'); // text/markdown mime.lookup('file.html'); // text/html mime.lookup('folder/file.js'); // application/javascript mime.lookup('folder/.htaccess'); // false mime.lookup('cats'); // false ``` 還有一個強勁功能,就是能把 mime-types 取得對應的 file extension。 ```js mime.extension('image/jpeg'); // jpeg mime.extension('text/html'); // html ``` 這樣就可以輕鬆處理好 mime-types 的轉換工作。
### 清空 ImageView 為什麼要清空 ImageView 呢? 在 Inflate XML 時 ImageView 不是已經空白的嗎? 對的,在最初 Inflate XML 時 ImageView 的確空白的,但是如果在程式中動態載入外部的資料更新到 ImageView 顯示,然後又要使用相同的 ImageView 顯示第二筆的資料,那麼就需要先把 ImageView 先清空,才能安心把第二筆資料的圖片顯示到 ImageView 內。 最好的例子是 RecyclerView ,因為 RecyclerView 會把用過的 ViewHolder 重新使用,就會有機會殘留著上一筆資料的 "殘留物"。必需要清空才能確保資料正確。 ### 實作 其實也不是一個很複雜的事情,只需要利用到 Android 原生的 color 設定就可以。 ```java // get image view from view holder viewHolder.mImageView // set image resources to transparent color .setImageResources(android.R.color.transparent); ``` 另外,坊間有些人會提及第二個方法,就是把 `android.R.color.transparent` 取代為 0 值。但是有一部份人嘗試過是不成功的,大家最好還是使用上面的方法較好。
在使用 EditText 時,預設是可以使用 Enter 鍵來換行的。換行的高度會反映在 UI 上。如果想限制 EditText 只可以得一行的話,我們需要在 XML 內加一點設定。 ### 設定 EditText 我們需要設定以下兩參數 : - 設定最高行高 `android:maxLines="1"` - 設定輸入的種類 `android:inputType="text"` ```xml <EditText android:id="@+id/et" android:layout_width="match_parent" android:layout_height="wrap_content" android:maxLines="1" android:inputType="text" /> ``` 這樣就可以把 EditText 鎖定為 1 行高了。
### Picasso Picasso 是一個用來載入圖片的程式庫,可以用來處理從 HTTP 的檔案,能夠輕鬆處理 RecyclerView 的 Async 圖片載入行為。 > Picasso 網址 : https://square.github.io/picasso ### 問題 在使用 Picasso 載入圖片時,可能會遇到以下這個問題 : ```java Picasso.get() .load("https://example.com/image.jpg") .centerCrop() .into(mImageView); ``` 在 Runtime 時會出以下的 Exception : ```text Center inside requires calling resize with positive width and height ``` 原因是因為 Picasso 要把圖片載入到 Image View 時,要先把 Bitmap resize 為一個正數的數值,才能夠使用 `center inside` 或 `center crop`。但是我們應該要 resize 做什麼數值才好呢? ### 解決方法 通過使用 `.resize()` 方法,可以把 Bitmap resize 為一個正數值的大小。 ```java Picasso.get() .load("https://example.com/image.jpg") .resize(800, 800) .centerCrop() .into(mImageView); ``` 以上的代碼是能夠運行,但是好像欠缺彈性。我們可以使用 `.fit()` 方法來讓 Picasso 幫我們自動 resize 為 Image View 的大小,這讓我們就不用 hard code 數值到 `.resize()` 了。 ```java Picasso.get() .load("https://example.com/image.jpg") .fit() .centerCrop() .into(mImageView); ```
在疫情下學校好多時都會使用 ZOOM 進行直播教學,教學完結時可以把影片留在 ZOOM 上翻看,但是如果想要下載影片到本的話,就好像沒有辧法。 ### 使用 Chrome Developer Tool 把 Video 連結抽出 我們可以通過使用 Chrome Developer Tool 把 Video 的路徑抽出來,然後下載影片。因為 ZOOM 有使用 cookies 保護 URL 的來源,如果把 Video 的路徑直接貼到 Browser 上的話,會得到 HTTP 403。 ```js (function(window) { var video = document.getElementsByTagName('video')[0]; var src = video.src; var a = document.createElement('a'); a.setAttribute('href', src); a.innerText = 'right click and save target'; a.style.position = 'absolute'; a.style.border = 'solid 1px #AAAAAA'; a.style.top = '20%'; a.style.left = '20%'; a.style.right = '20%'; a.style.textAlign = 'center'; a.style.display = 'block'; a.style.lineHeight = '200px'; a.style.fontSize = '30px'; a.style.backgroundColor = '#FFFFFF'; document.body.append(a); })(window); ``` 我們可以把上面的代碼貼到 Chrome Developer Tool Console 上,然後按 ENTER。  畫面上就會出現 'right click and save target' 的方塊,我可以使用滑鼠 Right Click 然後按另存連結,就可以把影片儲存到本機了。 
由 19Site 開發的項目 FBucket 上線啦 ! 這個項目主要是發開一個檔案的網站服務器,並會提供 Rest API 及 NodeJS API 以提供檔案的上傳及公開。 GitHub: https://github.com/19Site/FBucket NPM: https://www.npmjs.com/package/fbucket
在處理伺服器的工作時,為了能自動化完成一些常常會進行的工作,最常用到就是 Linux 的 Shell Script。 ### Linux Shell Linux Shell 就好像的 Window CMD (用 DOS 更為貼切),不同的是 Window CMD 比較像一個軟體,運行在 Window 之上,而 Shell 本身就是的外殼 (所以叫 Shell),它的底下就是 OS 本身了 ! Shell 有好多種,最常見到可能會是 `sh`、`bash`。`bash` 是 `sh` 的子集,並基於 `sh` 上加入了好多功能。 ### 使用 Shell Script 讀取使用者 Input String 我們可以通過使用 `read` 來讀取使用者在 shell 鍵入的字串。我們使用以下的指令建立 `test_command.sh`。 ```sh $ vi test_command.sh ``` 然後輸入以下內容 : ```sh #! /bin/sh printf "Enter you name: " read VARIABLE_NANE printf "\n\nYour name: ${VARIABLE_NANE}" ``` 完成後儲存,並試試運行 : ```sh $ chmod 755 ./test_command.sh $ ./test_command.sh Enter your name: 19Site Your name: 19Site ``` ### 收集 Password 有時我們可能要輸入 Password 來進行動作,但是不又想在畫面上顯示出 Password 的內容,要如何做呢? 我們可以透過修改 Shell 的設定,來把使用者鍵入的字符設為不顯示。我休修改一下上面的 Script。 ```sh #! /bin/sh stty -echo printf "Enter password: " read VARIABLE_NANE stty echo printf "\n\nYour password: ${VARIABLE_NANE}" ``` 我們可以透過使用 `stty` 指令來變更 Shell 的 `echo` 設定,從而達成把輸入隱藏目的。 ```sh $ chmod 755 ./test_command.sh $ ./test_command.sh Enter your password: Your password: 19Site ```
### 網站上的自動填入 一般的情況下,瀏覽器會記住用戶通過網站上的 `<input>` 所提交的信息。 這使瀏覽器能夠提供自動完成功能 (建議用戶已開始鍵入文字時的可能字眼) 或自動填充 (加載時預填充某些字段)。 這些功能通常預設情況下處於啟用狀態,但對於用戶來說可能是隱私問題,因此瀏覽器可以讓用戶禁用它們。 但是,以表格形式提交的某些數據將來可能不再有用 (例如一次性密碼),或者包含敏感信息 (例如信用卡安全碼)。作為網站的作者,即使啟用了瀏覽器的自動完成功能,您也可能希望瀏覽器不記住這些字段的值。 ### 實行方法 通過加入 `autocomplete='off'`,可以把瀏覽器的自動完成功能不套用到該 Element。 ```html <!-- disable form auto complete --> <form autocomplete='off'> [...] </form> <!-- disable input:text auto complete --> <input type='text' autocomplete='off' /> ```
### Regular Expression 我們常常會使用到 Regular Expression 來驗證字串的格式,由日期、電話號碼到特定的格式都可以用 Regular Expression 表達出來。而在 Javascript 中使用者可以使用 `new RegExp()` 來建立 Regular Expression 物件。 ```js // create regexp object var re1 = /^\d{4}-\d{2}-\d{2}$/i; // create regexp object var re2 = new RegExp('^\d{4}-\d{2}-\d{2}$', 'i'); console.log(re1.test('2019-11-22')); // true console.log(re2.test('2019-11-22')); // true ``` 它們的運作是一樣功能的。由上面的代碼推算出,我們可以透過使用 `new RegExp()` 來動態建立一個 Regular Expression Object。 ```js // create regex by providing pattern string const createRegExp = string => new RegExp('/^' + string + '$/'); ``` ### 產生問題 上面代碼雖然很方便,但是如果有使用者輸入了和 Regular Expression 相同的保留字,就會有可能把你的程式弄壞了 !! ```js // create regex var re1 = createRegExp('*^*^*$*'); ``` 這樣就會把程式弄壞了。要防止這個情況,我們需要為傳入的參數 Escape 走 Regular Expression 的保留字。例如以下字符 : ```txt \ ^ $ * + ? . ( ) | { } [ ] ``` 我們可以寫一個 Function 把上面的字符 Escape,然後回傳出去。 ```js /** * escape regexp */ const escapeRegExp = string => { // $& means the whole matched string return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // example escapeRegExp("All of these should be escaped: \ ^ $ * + ? . ( ) | { } [ ]"); // result >>> "All of these should be escaped: \\ \^ \$ \* \+ \? \. \( \) \| \{ \} \[ \]" ``` 經過 Escape 後的字串就可以放心放到 `new RegExp()` 中使用,以產生 Regular Expression Object。 > 參考網址 : https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex ### NPM Package 另外已經有大神把 Function 打包,放到 NPM 上去了,只要使用 NPM 下載就可以直接使用。 ```sh # npm install escape-string-regexp ```
### TL;DR 使用以下的 CSS 來把 Focus 時的邊框取消。 ```css input:focus, select:focus, textarea:focus, button:focus { outline: none; } ``` ### Focus Outline Focus Outline 大概是 Chrome 最先有的東西 (筆者自己經驗),就是當使用者的 Cursor 放到 Text Input Field 上,該 DOM Element 的邊框就會有一個藍色的 Highlight 出現,像亮了燈一樣。  當 Search Box 被 Focus 時,就會有一個藍色的邊框包著,像亮了燈一樣。 ### 把 Focus 的 Border 消除 我們可以透過使用以的 CSS 來把 Focus 的 Border 消除。 ```css input:focus, select:focus, textarea:focus, button:focus { outline: none; } ```
使用 Laravel 時,我們都會用到 `url()` 產生超連結 (hyper link),使用 `url()`所產生的 URL 會是完整的 URL,即包括有 Protocol、Domain、Port、Path 及 Query String 所有資料。 ### 完整的 URL 以下是一個完整的 URL ```text https://19site.com:80/posts?page=2 ``` - https - Protocol - 19site.com - Domain - :80 - Port number - /posts - Path - ?page=2 - Query String 使用 Laravel 的 `url('/posts')` 產生的 URL 會是以下的樣子。 ```php // laravel url() helper function echo url('/posts'); // https://19site.com/posts ``` ### 相對的 URL 如果我們要用 `url()` 來產生相對 (relative) 的 URL,最直接是不使用 `url()` 就可以解決了 ! 但如果我們硬是要使用 `url()` 的方式來產生 URL 的話,就需要自己再外建 helper function 了。 ```php /** * create relative url */ function relativeUrl($value='') { return str_replace(url(''), '', url($value)); } // use new helper function echo relativeUrl('/posts'); // /posts ```
在使用 DB 的時候,有時需要先從 DB 內抽出記錄,然後經過一些運算後,才會寫入到 DB 。但是如果在運算中的時間,有其他的 Thread 來 DB 修改記錄的話,就可能會有互相覆寫的情況出現。 ### 實例 想像以下的情況 : Table : fruits |id|name|quantity| |---|---|---| |1|apple|10| |2|orange|5| 使用以下的代碼進行運算,目的是把水果的數量減去一定數量,減去後水果數量如果少於 0 ,就要拒絕。代碼如下 : ```js // generate a random number var random = Math.round(Math.random() * 8); // fetch data from db var rows = await db.query(`select id, name, quantity from fruits where name = 'apple'`); // get first record data var {id, quantity} = rows[0]; // check quantity is enough if( quantity >= random ) { // new quantity var newQuantity = quantity - random; // update db record await db.query(`update fruits set quantity = ? where id = ?`, [newQuantity, id]); } else { // not enough quantity throw new Error('not enough quantity'); } ``` 以上的代碼在單一的 Thread 上運行是沒有問題的,但是如果是在 Multi Thread 的情況下,就可能會有機會出現覆寫舊值的情況。 ### 覆寫舊值 當 Thread 1 和 Thread 2 同時啟動這段代碼時,就會有機同時在 DB 內抽取到相同的值 (因為互相還未寫入新值到 DB)。 然後分別寫入各自減去 `Random` 的數值入 DB 內,這樣 DB 內的數量就會不正確。 ### 解決方法 要解決就需要進行阻塞 (Blocking),要後執行的 Thread 等待先執行的執行完成,才會開始進入執行階段,像是 Java 內的 Synchronized Method 一樣。 在 MySQL 內我們可以通過使用 `lock table` 方法來達成。 以下這句可以防止其他 Connection 寫入表格 fruits : ```sql lock tables fruits read; ``` 以下這句可以防止其他 Connection 讀取及寫入表格 fruits : ```sql lock tables fruits write; ``` 記住 `lock table` 後一定要 `unlock table` 才行,不然一直 lock 住就會變成為 Dead Lock 了。 ```sql unlock tables; ``` ### 改寫代碼 我們把上面的代碼改寫一下 : ```js // generate a random number var random = Math.round(Math.random() * 8); // lock tables await db.query(`lock tables fruits write`); // fetch data from db var rows = await db.query(`select id, name, quantity from fruits where name = 'apple'`); // get first record data var {id, quantity} = rows[0]; // check quantity is enough if( quantity >= random ) { // new quantity var newQuantity = quantity - random; // update db record await db.query(`update fruits set quantity = ? where id = ?`, [newQuantity, id]); // unlock tables await db.query(`unlock tables`); } else { // unlock tables await db.query(`unlock tables`); // not enough quantity throw new Error('not enough quantity'); } ``` 只需要使用 Lock Table 功能把需要讀出運算及寫入的邏輯包裝起來,就可以防止其他的 DB Connection 在中間讀取未更新的參數。 *** 但是 Lock Table 在 Transaction 中會引發無法 Rollback 的情況,需要使用第二種方法來處理,在第二篇文章再介紹。
今天工作剛好要劃些少 ER 來告訴別人工程師,正在設計中的 Object 關係,所以記錄一下到這裏。 ### 什麼是 ER Diagram ER Diagram 可以用來描述 Database 內 Table (Entity) 和 Table 之間的關係 (Relation),當中有些符號是劃在連接線上的,用來表達關係。 ### 連接線 我們可以使用連接線把兩個 Entity 連起來,並依照他們的關連數量來加入符號到兩端的線頭。  而整個 ER Diagram 大約就是這樣的 !  > 參考網址 : https://softwareengineering.stackexchange.com/questions/345709/erd-many-vs-zero-or-many-one-or-many-crowfoot-notation
### How to read a text file in Java? In short, Scanner. ### Demo code Code below showing how to use `Scanner` class to read a text file line by line in Java. ```java try { // init file File mFile = new File("/root/example.txt"); // init scanner Scanner mScanner = new Scanner(mFile); // while has next line while( mScanner.hasNextLine() ) { // get line string String mLine = mScanner.nextLine(); // do something } } catch ( FileNotFoundException e ) { // trace error e.printStackTrace() } ```
又是 NetBean 的問題了 !!! 這次遇到的是 Unpacking index for Central Repository on Netbeans ... 等了很久沒完沒了。安裝個 Dependencies 等到天荒地老了,而且間中還會不段的重新來 Transfer Index !! 真是惡夢 !! 於是又找了 GOOGLE 大神求救 !! 找到了以下的提問 : https://stackoverflow.com/questions/50933665/unpacking-index-for-central-repository-on-netbeans 看來大家都有遇到這個問題,經過筆者的了解後,筆者遇到的是 Run out of disk spaces ... 應該是 temp folder 的 Disk 用完空間了。( 筆者用的是 4GB Ram Drive) 結果要把 temp folder 指回去 C Drive 才能完成過程 ...
今日遇到了一個有關於 NetBean 8.2 配上 Maven 的問題 ... 話說工作需要使用到 NetBean 8.2 ... 所以把原先有的 NetBean 11.2 砍掉了,找了個 NetBean 8.2 回來安裝,一切都很順利。直到要 Create Maven Web Application 就出事了。 ### 事件一 ```text Cannot run program "cmd" (in directory "C:\projects\open"): Malformed argument has embedded quote: "C:\Program Files\NetBeans-11.1\netbeans\java\maven\bin\mvn.cmd" ``` 每一次 Create Project 到了最後階段要 Finish 時,都會彈出這樣的問題。在網上查找了一會後發現都有很多人遇到了這個問題,大約是因為 Security Fix 所以有些動作變成不准許了,要解決的話可以修改一下 NetBean 的 Config 檔案。 1. 打開檔案 `<netbeans-dir>\etc\netbeans.conf` 2. 找出 `netbeans_default_options` 的設定 3. 把 `-J-Djdk.lang.Process.allowAmbiguousCommands=true` 加入到設定的後面 經過上面的動作,解決了上面的問題 !! > 參考網站 : https://stackoverflow.com/questions/58411279/java-with-maven-wouldnt-build-cannot-run-program-cmd-malformed-argument-has ### 事件二 完後成上的事件後,Maven 好像運作得不錯地下載相依的檔案,突然又出現了第二個問題 !! ```text [ERROR] No plugin found for prefix 'archetype' in the current project and in the plugin groups [org.apache.maven.plugins, org.codehaus.mojo] available from the repositories [local (C:\Documents and Settings\ccen\.m2\repository), central (http://repo1.maven.org/maven2)] -> [Help 1] ``` 又卡住了 !!! 這次的問題大約是因為 NetBean 在 Request http://repo1.maven.org/maven2 時出了問題。又在網上找了找看看有沒人遇到相同的問題,後快就找到了 ! > 參考網站 : https://stackoverflow.com/questions/6472782/mvn-archetypegenerate-does-not-work-no-plugin-found-for-prefix-archetype 內文有說到可能是因為 Firewall 等等因素,可以直接試試在 Browser 上貼上 http://repo1.maven.org/maven2 看看能否連上。筆者立即試一試,結果給了以下的回覆 !! ```text 501 HTTPS Required. Use https://repo1.maven.org/maven2/ More information at https://links.sonatype.com/central/501-https-required ``` 原因是因為改用了 HTTPS 了,舊有的 HTTP 服務終結了。正當想看看能否改變 Maven2 的 URL 由 HTTP 如何變為 HTTPS 時,看到有其他使用者留言 :  NetBean 8.2 或以下就會遇到這個問題了 ... 看來還是乖乖砍掉 NetBean 8.2 重新下載 NetBean 11.2 吧 ... 這文章就是在安裝 NetBean 11.2 時趁著等待的時間記錄下來 ...
### Android AsyncTask 今日收到一位同事問起,如果 HTTP Request 是多於一個時,要如何 Handle Progress 的生命週期 ? 雖然筆者沒有在網上再找找解決方法,不過以經驗去理解應該和處理一個 HTTP Request 也差不多,以下寫了一段 DEMO CODE 來實作這個事情 (沒有測試過能否運行的)。 另外也放到了 GIST : https://gist.github.com/19Site/39a6d1c40794de16dfd97f2da7ef9f16 ### Demo Code 以下是 Demo Code ```java public static void main(String... args) { AsyncTask<String, Integer, Boolean> task = new AsyncTask<String, Integer, Boolean>() { // loading dialog AlertDialog dialog = (new AlertDialog.Builder(context)).setCancelable(false).setMessage("loading").create(); @Override protected Boolean doInBackground(String... strings) { publishProgress(1); doHttp1(); publishProgress(2); doHttp2(); publishProgress(3); doHttp3(); publishProgress(4); return true; } @Override protected void onPostExecute(Boolean aBoolean) { super.onPostExecute(aBoolean); // todo } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); switch (values[0]) { case 1: dialog.show(); case 2: case 3: dialog.setMessage("progress " + values[0]); break; case 4: default: dialog.dismiss(); } } private void doHttp1() { // do http request } private void doHttp2() { // do http request } private void doHttp3() { // do http request } }; } ```
Home Office 了一段時間因為有點 Server 問題回到公司去解決,但誰知道在沒有動 Server 一段時間後在連上 SSH 時發生問題 !! ### 發現問題 大約是因為這台 Server 的 Fingerprint 和先前連上時不同了,為了防止第三方的人士 "扮" 是連線目標的主機,所以引發這個問題。 ```sh $ ssh root@192.168.1.1 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdropping on you right now (man-in-the-middle attack)! It is also possible that a host key has just been changed. The fingerprint for the ECDSA key sent by the remote host is SHA256:HDjXJcb0VYXWF+MCBDjSGn6FQmg/+x7vV0ljJvIDas0. Please contact your system administrator. Add correct host key in /root/.ssh/known_hosts to get rid of this message. Offending ECDSA key in /root/.ssh/known_hosts:46 ECDSA host key for 192.168.1.1 has changed and you have requested strict checking. Host key verification failed. ``` 說明了是 Host key changed ```sh ECDSA host key for 192.168.1.1 has changed and you have requested strict checking. Host key verification failed. ``` Host key Changed 可能有好多原因,最有可能是因為主網重新安裝了 ... 果然 ... 目標的伺服器是更新了。 ### 解決方法 針對因為 Host Key Changed 的問題,只需要把本機的 Host Key 記錄清除。 ```sh $ ssh-keygen -R 192.168.1.1 ``` 這樣就可以把 `/root/.ssh/known_hosts` 的有關 `192.168.1.1` 的 Key 記錄清除,這樣下次連線是就會重新查問是否連線到 `192.168.1.1`。之後回答 `yes` 便可以了。
進入 2020 年的最大單事情,暫是應該是武漢肺炎對全球的影響了。對香港這邊影響很大呢 ... 農曆年後完全停課到現在。很多公司也選擇 Home Office 來繼續工作,每天的新聞也是一個一個的確診個案,待在家中 Home Office 久了真的在懷疑人生。 Home Office 不斷找舊的 Project 出來番新一下,及維護一下現有的系統。寫 Script 進行 Automation Backup 等動作,多出來的時間就做下 OU 功課 ... 電視台還天天播著沒用的飛機歌唱 x x 加油,還不如播多一點教大家洗手注意衛生好過。 希望疫情快點可以受控,病者早日康復 ... 這病毒感覺已經到了化武級數。
我們可以通過變更 `state` 或 `props` 的內容來更新 render 的結果。但有些情況如果當 Component 又可以通過 `state` 來更新,又同時可以通過 `props` 來更新的話,那麼需要怎樣做呢? ### 使用 `state` 更新 render 先看看下面的 Code,我們可以通過把 `props` 傳的的資料 assign 到 `state`,然後在 render 時讀取 state 便可了。這樣傳入的 `props` 可以從 `state` 反映出來。 ```js /** * my component */ class MyComponent extends React.Component { constructor(props) { super(props); this.state = { text: this.props.text }; } render() { return this.state.text; } } ``` 不過上面有一個缺點,就是 `constructor` 只會在建立 Component 時運行一次,在往後的 `props` 更新時,Component 並不會把 `props` 的內容再一次 assign 到 `state`。這讓便不能反映出日後 `props`變更的值。 ### 解決方法 我們可以加入一個方法,來把 `props` 的變更重新 assign 到 `state`,使 render 出來的結果合乎預期。 ```js /** * my component */ class MyComponent extends React.Component { constructor(props) { super(props); this.state = { text: this.props.text }; } componentDidUpdate(oldProps) { if( oldProps.text !== this.props.text ) { this.setState(state => ({ ...state, text: this.props.text })); } } render() { return this.state.text; } } ``` 我們可以通過在 `componentDidUpdate` 中檢查需要的 `props` 內容有沒有變更,從而決定是否發動 `setState`,這樣就可以把 `props` 的更新反映出來。 在 React 的官方網站對這個動作也有特別說明 : https://zh-hant.reactjs.org/docs/react-component.html#constructor
要驗證使用者自行輸入的日期時間,使用 Regular Expression 來驗證是最方便的方法。 ### 什麼是 Regular Expression Regular Expression 能夠描述出字串的出現模式,其中一個用途是可以用來驗證字串是否符合某個特定的 Pattern。例如 : ```js // define a string var str1= 'Hello World'; var str2= 'World Hello'; // define regex pattern var regex = /^Hello World$/; console.log(regex.test(str1)); // true console.log(regex.test(str2)); // false ``` ### 驗證日期字串 例如我們要限制使用者輸入的日期格式為 `YYYY-MM-DD` 的話,那麼以下的日期格式是能通過的。 ```js // valid dates var validDates = [ '2020-01-02', '2020-02-29', '2020-12-31' ]; // invalid dates var invalidDates = [ '02-02-2020, '31-01-2020', '04-30-2020' ]; ``` 要使用 Regular Expression 來表達這個格式的話,可以使用以下語法 : ```js // regex for checking date string var regex = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/; ``` ### 解說 下面會為上面的 Regular Expression 進行解說。 |符號|解釋| |---| |`^`|字串的開始| |`[0-9]`|字符為 0 到 9 之間的數字| |`{4}`|上一個字符重覆 4 次| |-|出現 "-" 字符 1 次| |`[0-9]`|字符為 0 到 9 之間的數字| |`{2}`|上一個字符重覆 2 次| |-|出現 "-" 字符 1 次| |`[0-9]`|字符為 0 到 9 之間的數字| |`{2}`|上一個字符重覆 2 次| |`$`|字串的結尾| 用這樣的 Regular Expression 便能測試出字串是否符合特定的日期格式。 ```js // regex for checking date string var regex = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/; console.log(regex.test('2020-01-02')); // true console.log(regex.test('2020-11-22')); // true console.log(regex.test('2020-33-99')); // true ``` 從上面的結果可以看到,雖然可以正常檢查到日期格式,但是會連 `2020-33-99` 這種不合法的日期都會能通過。如果要加強度合法日期的檢查,則需要再修改一下 Regular Expression 內容。 ### 驗查日期字串加強版 我們使用上面的 Regular Expression 再加以修改一下。 ```js // regex for checking date string var regex = /^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/; ``` 經過修改後的 Regular Expression 就可以檢查出月份在 1 到 12 之間及日期在 1 到 31 之間,但是如果要能驗證出 31 日及 30 日的月份甚至是潤年的話,就需要更多的邏輯檢查了。
如果要在 MySQL 中的 `DATETIME` 欄位中抽出 DATE 的話,可以使用 `DATE()` function 來達成。 ### 日期時間 在 MySQL 中 `DATETIME` 欄位會儲存著日期時間。格式如下 : ```sql # select query select created_at from table; # 2020-01-22 12:11:23 ``` ### 使用 `DATE()` Function 透過使用 `DATE()` function,可以只抽出當中的日期值 : ```sql # select query select DATE(created_at) as created_at from table; # 2020-01-22 ```
雖然每個月的日子都差不多是固定的,但是如果能使用 Program 計算出日期相關的數值,就更為方便易用。 ### TL;DR 我們可以使用 Date Object 來快速計算出每個月份的最後一日。 ```js // year var year = 2020; // month var month = 0; // get date var date = new Date(year, month + 1, 0); ``` ### 潤年 Leap Year 在小學時都會有教過 : 一月大、二月小、三月大、四月小、五月大、六月小、七月大、八月大、九月小、十月大、十一月小、十二月大。 實質上上面的意思是 : 1 月有 31 日;2 月有 28 或 29 日;3 月有 31 日;4 月有 30 日;5 月有 31 日;6 月有 30 日;7 月有 31 日;8 月有 31 日;9 月有 30 日;10 月有 31 日;11 月有 30 日;12 月有 31 日。 潤年是指二月有 29 日的年份,而潤年是有一條公式可以計算的。 ### 潤年定義方法 年份是 4 的倍數及年份不是 100 的倍數但年份是 400 的倍數。 以上的規則可以使用以下的邏輯判別出來。 ```js /** * check year is leap year */ function isLeapYear(year) { return ((year % 4) === 0 && (year % 100) !== 0) || (year % 400) === 0; } ``` ### 找出每個月份的最後一日 由以上的資料,我們可以總結出下面的邏輯用來計算每個月份的最後一日。 ```js /** * get last date of month */ function getDaysOfMonth(year, month) { // get date by month switch (month) { case 2: return isLeapYear(year) ? 29 : 28; case 4: case 6: case 9: case 11: return 30; default: return 31; } } ``` 但是... 其實 Javascript 入面的 Date Object 已經可以快速計算出來。 ```js // year var year = 2020; // month var month = 0; // get date var date = new Date(year, month + 1, 0); ``` 上面的 `new Date()` 方法是把目前的月份加上 1,然後再把日子設定為 0,這樣 Date Object 便會設定為下一個月的 0 日,但因為 0 日是一個不合法的日期,所以 Date Object 會把 0 設定為 1 的前一日,也即是設定月份的最後一日。 不過有人回報過使用 `new Date()` 的速度很慢,所以用那一個方法來取得每月最後一,可以因應情況自行決定。
今日收到個問題,是關於把顏色變成一段數線。這個問題令筆者想起其實光也是波段,不同的波段可以反映出不同的顏色。 在網上找了一會,原來已經有科學人實作了出來。筆者把內裏的 Javascript 代碼節錄出來。 ```js /** * takes wavelength in nm and returns an rgba value */ function wavelengthToColor(wavelength) { var r, g, b, alpha, colorSpace, wl = wavelength, gamma = 1; if (wl >= 380 && wl < 440) { r = -1 * (wl - 440) / (440 - 380); g = 0; b = 1; } else if (wl >= 440 && wl < 490) { r = 0; g = (wl - 440) / (490 - 440); b = 1; } else if (wl >= 490 && wl < 510) { r = 0; g = 1; b = -1 * (wl - 510) / (510 - 490); } else if (wl >= 510 && wl < 580) { r = (wl - 510) / (580 - 510); g = 1; b = 0; } else if (wl >= 580 && wl < 645) { r = 1; g = -1 * (wl - 645) / (645 - 580); b = 0.0; } else if (wl >= 645 && wl <= 780) { r = 1; g = 0; b = 0; } else { r = 0; g = 0; b = 0; } // intensty is lower at the edges of the visible spectrum. if (wl > 780 || wl < 380) { alpha = 0; } else if (wl > 700) { alpha = (780 - wl) / (780 - 700); } else if (wl < 420) { alpha = (wl - 380) / (420 - 380); } else { alpha = 1; } colorSpace = ['rgba(' + (r * 100) + '%,' + (g * 100) + '%,' + (b * 100) + '%, ' + alpha + ')', r, g, b, alpha] // colorSpace is an array with 5 elements. // The first element is the complete code as a string. // Use colorSpace[0] as is to display the desired color. // use the last four elements alone or together to access each of the individual r, g, b and a channels. return colorSpace; } ``` 上面 function 是把光的波長數值變換成 HTML 格式的顏色數值。另外還會獨立輸出 RGB 及 Alpha 值。 由上面的 Code 可見,如果把 380 - 780 的數值拉一條線用 0 - 100% 包裝起來,就可以用 0 - 100 中任一數值來表示每一個不同的顏色。 ```js /** * get color 1 to 100 */ function getColor(value) { // get the weight var weight = Math.floor(380 + ((780 - 380) / 100 * value)); // return color return wavelengthToColor(weight); } ``` 另外在 GitHub 上有其他的實作版本,不過筆者就未有嘗試。 https://gist.github.com/mlocati/7210513 > 參考網址 : https://scienceprimer.com/javascript-code-convert-light-wavelength-color
2019 年的冬天來曼谷的 ICON SIAM !!! 這個地點是先前來曼谷時沒有到過的,因為在 Youtube 上看到 "冲遊泰國" 系列的其介一隻,介紹了一家好吃的 Harbour 自助餐,所以專程來這邊試試。 筆者這次是 GRAB 車來的,所以遇上了很塞的交通情況。如果下次有機會再來的話,一定會試試坐船來,可以看到更多的水上風景。和在河上看到 ICON SIAM 建築特色。 ### 交通 和去河濱夜市一樣,可以先坐 BTS Saphan Taksin 2號出口,再行去 Sathorn Pier 碼頭轉乘船隻抵達 ICON Siam。  ### 時間 早上 09:00 到晚上 23:00,大約 10 分鐘一班船。 ### 商場特色 商場的定位是高級商場,有各國名店進駐,但是最大的特色是底層是一個水上市場,模擬了泰國傳統船家的生活及交易方式。 除了水上市場外,商場內還有很多特色的手作小店,售賣當地的手工藝品及傳統食物,而且價格相宜,能把高級名店和特色小店包裝在一起,又不會有違和感。   
Harbour 自助餐位於 ICON Siam 商場的 6 樓,是一家專門服務自助餐的餐廳。 ### 交通 和去河濱夜市一樣,可以先坐 BTS Saphan Taksin 2號出口,再行去 Sathorn Pier 碼頭轉乘船隻抵達 ICON Siam。  ### 時間 早上 09:00 到晚上 23:00,大約 10 分鐘一班船。 ### Harbour 位於 ICON Siam 6 樓的 Harbour。       ### 筆者點評 食物種類很多,以中西日較為主道,台資公司。餐廳的地方很闊,不過有逼的感覺。食物質數 OK,熱食為佳,達到水準。海鮮冷食相對較差,新鮮程度一般。甜品超多選擇,飲品也很多款。總體來說性價比高,值得一試再試。
曼谷遊的第七天,行程來到最後一天了,今天下午 5:00 就要到達機場坐 7:20 的機回去了 !!! 今天會去一個曼谷相對比較新的地方,ICON Siam。雖然地方是叫 ICON Siam,但是一點都不近 Siam BTS 站的,它是位處河濱夜市的對岸。 ### ICON Siam ICON Siam 附近的交通真是十分之差,塞了非非非常之久才到達 !  ICON Siam 的特色是它的最下層是一個大型的水上市集,內裏有很多小店,吃的穿的都有,價格也平民。   而上層的就是普通的大型商場,生活百貨。   ### Harbour 還沒有吃早午餐的我們就得先解決一下腸胃。所以選擇去了頂樓的 Harbour 自助餐吃個飽 !!   因為當日為 12 月31日,所以這個格目不適用,入坐 2 小時 1,300 BAHT 一位。價錢也不算貴啊 !       ### 水上市場 吃完後我們又回到地下再逛逛水上市場。     因為時間關係不能逛太久,交通情況很難預計得到,所以就提早了叫車回酒店再叫車去機場。  這次泰國之旅圓滿結束 !!
2019 年冬天,來到泰國吃了一餐自助午餐。 ### 曼谷洲際酒店 這家酒店每次出 Chit Lom BTS 站都會看到,每次都說下次一定要來試一試,而這次終於都合時機 BOOK 來試試 !! ### 地點 由 Chit Lom BTS 站行三分鐘就到達。     ### Espresso Espresso 位處酒店二樓,在大堂走上一層便可以到達,星期日的自助午餐大約 1,000 BATH 一位。   ### 餐廳內部 餐廳內部坐位十分之多,地方很多,不會有很逼的感覺。   不過食物的種類不是很多,主要是以熱食為主,在午市自助餐沒有鮮等食物提供。 當時目測光顧的人不是很多,大約有 10 枱客左右。 ### 筆者點評 總的來說筆者覺得相同的價格可以在曼谷找到性價比更高的自助餐,整體感覺一般。食物味道還可以,沒有超水準的表現,也沒有特別差的。環境感覺起來普通,可能因為日光滲入不足,所以覺得暗暗的。有個服務員很利害的能說一點點廣東話,在他國能聽到當地人說廣東話感覺很親切。
曼谷遊的第六天,這是今次旅程的最後一個一整日了。但是因為體力關係,所以今天的行程也是一樣簡簡單單行逛為主。 這天訓到日上三竿,不過今天會去一家大酒店吃自助午餐 !! 就是曼谷的洲際酒店 (Intercontinental Hotel)。 ### 曼谷洲際酒店 曼谷洲際酒店位於 Chit Lom BTS 站,每次出站必定會見到身影。  在天橋右手邊的樓梯下去,就可以到達。    ### Espresso 進入酒店後上二樓,就可以到達自助餐的餐廳 Espresso。       吃了大約兩個小時,四個人埋單 5,200 BAHT 左右。食完後又分開各自行,筆者和太太去了 Siam 站附近周圍走走。 > 在另一個 POST 會有影片介紹 : https://19site.net/posts/85 ### Siam 貴為曼谷中心地帶的中心區,Siam 商場名店臨立,是旅人必到的地方之一。筆者雖然來了數次,但是也必定要來一來,為的是一家海南雞飯。  Siam Paragon,這個名字自從 2013 年第一次和女朋友 (當是太太還是女朋友) 來泰國時,聽到導遊介紹時就被洗腦了。導遊哥哥 (他自稱叫阿譚) 介紹說 Paragon 是解作 Mall 的意思,並說大家如果失散了可以在 Siam Paragon 的正門口等。 不過這次要先去找筆者想吃那家海南雞飯,所以目標為本,立即進發。   我們穿過了 Siam Square ONE 商場。      很難想像休假的第 1 日貼的出 A4 紙可以 "巢" 成這樣 !!! 唯有返 Siam Square ONE 行下。    ### Siam Center Siam Center 位於 Siam Paragon 的側邊,行 1 分鐘就到。入面的格區不像 Siam Paragon 走貴要路線,而是較為中級的商場,有吃的也有買的,比起 Siam Paragon 更易吸引大眾。   ### 河濱夜市 走了一整個下午都有點累了,筆者和太太先回飯店休息一會,再一行四人 GRAB 車到河濱夜市 (先前都是坐船去的,今次試一下叫車去)。  由於塞車的關係,在上車一邊塞時日以落了 !!  由於實在太多車的關係,所以司機停了在夜市很遠的地方讓我們走進去 .... 還真是很遠呢,走了差不多十五分鐘才到。 到達碼頭時,日已落 ..  行進去一點,就會看到地標性的摩天輪。     夜市的另外一邊有像貨食一樣的市集區。   > 在另一個 POST 會有影片介紹 : https://19site.net/posts/78 ### 晚飯時間 雖然夜市內有很多餐廳,不過好像也沒有合心意的,所以最後還是回去 Asok 附近,吃飯店樓下一家的餐廳 (平常晚上回去酒店時超多人在吃)。          總共九個菜,食到好飽 !! 吃飽後就回酒店休息去 !! 明天就是最後一天的行程,今天晚上要執 GIP 了。
很多時我們也會在程式用上 Random String,用來作為暫時的 ID 或者用來生產 TOKEN。 ### TL;DR 我們可以使用隨機數字來生產隨機的字串。以下 Script 可以產生 8 個位的隨機字串 : ```js // create random string Math.random().toString(36).slice(2, 10); ``` ### 原理 以上的 Script 其實是把使用 `Math.ramdom()` 生產出來的隨機數字變成 36 進位的字串。由此推理出,也可以生成其他進位的字串 : ```js // hexadecimal Math.random().toString(16).slice(2); // octadecimal Math.random().toString(8).slice(2); // binary Math.random().toString(2).slice(2); ``` ### 實作 不過以上都只可以當作為快速用途,生成的字符都是在 a-z 及 0-9 之間,如果要加入更多的字符,就要用較為複雜的邏輯了。 ```js /** * generate random string */ function randStr(length) { // result container var result = []; // characters pool var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; // create random string for (var i = 0; i < length; i++) { // get random position character result.push(chars.charAt(Math.floor(Math.random() * chars.length))); } // return result return result.join(''); } // print random string with 8 characters console.log(randStr(8)); ``` > 參考資料 : https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript
製作網站時小不免會使用到 ICON 用來標示功能,但是如果每一個 ICON 都使用一幅圖片來處理的話,在載入網站時便會使用大量時間。後來為了減少載入圖片的時間,使用了 ICON MAP 來解決。即是把所有的 ICON 都合併在一張圖片,然後使用 background-image 及 background-position 來決定顯示圖片的那一個位置。  ### 文字 ICON 在未開始前先看看這個網站 : https://fontawesome.com 這個網站提供了字型 ICON,供開發者免費或者付費使用。只需要匯入文字到項目,然後在相應的地方加入 CSS 就可以顯示文字 ICON 了。 ### 用法 只需要在 HTML 中插入特定元素並設定好 `class` 就可以使用。 ```html <!-- use i element to reference to icon --> <i class='fa fa-star'></i> <!-- use span element to reference to icon --> <span class='fa fa-star'></span> ``` 詳細的情況可以查看這裏 : https://fontawesome.com/how-to-use/on-the-web/referencing-icons/basic-use ### 在 React 上使用 要在 React 上使用先要使用 npm 安裝套件。 ```sh npm install @fortawesome/fontawesome-free ``` 然後在 JS 或 JSX 檔案內載入套件。 ```js import '@fortawesome/fontawesome-free/css/all.css'; ``` 最後就可以使用上面的語法正常使用。 ```html <!-- use i element to reference to icon --> <i className='fa fa-star' /> <!-- use span element to reference to icon --> <span className='fa fa-star' /> ```
很多時我們使用 Http 回傳 JSON 到客戶應用程式時都會忽視了一點,就是告訴客戶應用程式 (多數是指 Browser 或 Http Agent) 你回傳的是 JSON 字串。 得多比較新的 Http Agent 都會有 Auto-detect 回傳的內容是什麽而去決定為回傳的結果 "加工"。例如回傳的是 JSON String,則會進行 `JSON.stringify()` 事前處理,然後才會回傳到使用者。所以如果伺服器可以告訴客戶要傳送的內容種類,就可以省去客戶分析內容的時間了 ! ### Header 伺服器可以透過設定回應檔頭 (Response Header),來告訴客戶端回傳的內容是什麼格式。在 PHP 上我們可以使用 `header()` 功能來達成。 ```php // send json string header('Content-Type: application/json'); // send json string with charset header('Content-Type: application/json; charset=utf-8'); ``` 以上就是設定回應檔頭的語法,而 `Content-Type: application/json` 就是要告訴客戶程式,將會收到的東西是 `application/json`。以下是其他檔頭的例子 : ```php // send html header('Content-Type: text/html'); // send plain text header('Content-Type: text/plain'); // send xml header('Content-Type: text/xml'); ``` ### Chrome Developer Toolbar 以下是使用 Chrome 開發人員工具,可以看到回應檔頭的內容。  還可以設定內容的編碼 (charset encoding)。
在先前有篇文章是提及使用 Javascript 把 JSON 的格式對齊好 : https://19site.net/posts/76 其實相類似的功能在 PHP 下也是有的 !! ### PHP 上的 JSON 在 PHP 上我們常常使用到 `json_encode()` 及 `json_decode()` 把資料和 JSON 之間進行互換。 自從 PHP 5.4 後更提供了 `JSON_PRETTY_PRINT` 參數,來把 JSON 對齊好 ! http://php.net/manual/en/function.json-encode.php ``` <?php // data $data = [ 'name' => 'Peter', 'age' => 21 ]; // $data to json $json_string = json_encode($data, JSON_PRETTY_PRINT); ```
在開發網站時 (特別是 ReactJS 時),網站主機 (Web Hosting Server) 和應用程式主機 (Application Server) 很多時都不會是同一台主機。如果在沒有設定 proxy 的情況下,在網站使用 Ajax 載入外部資料時便會出現 `Access-Control-Allow-Origin` 例外。 > Access to XMLHttpRequest at 'https://19site.net/files/7d/79/7d790b71-7af2-4b0b-890d-65c513f4077e.jpeg' from origin http://192.168.1.147:8000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. ### 源因 這是因為安全性的因素,瀏覽器預設不允許 JavaScript 任意存取其他伺服器的資源。以免因為 Session 而能夠載入先前登入過的網站,然後把資料經過惡意網站上載到第三方地址,達到偷取個人資料的功能。  ### 解決方法 解決方法有二 : - 修改 Application Server 的應用程式,使回應的檔頭 (Response Header) 加入 `Access-Control-Allow-Origin` 並設定為相應的 IP 地址。 - 關閉 Chrome 的網站安全設定。 (本文提及的方法) 注意 : 關閉 Chrome 的網站安全設定隻能生效在本機上,如果正式 Production 的話記得到設定好 Server Application 才對啊 ! 另外要留意是關閉了網站安全設定代表你的 Chrome 並不是在安全環境下運行著,不要用這個 Session 的 Chrome 來作為日常使用 ! 不然便會有機會被惡意網站偷取個人資料了 ! ### 進入正題 實際的操作是在啟動 Chrome 時加入參數 `--disable-web-security`。 在 Window 上,我們可以建立以下 Shortcut : > "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --user-data-dir="C:/Chrome-dev-session" --disable-web-security  然後我們可以使用新建立的連結來啟動 Chrome。 會看到在啟動時會有警告的字句,提醒使用者使用 `--disable-web-security` 會危害穩定及安全性。  再次提醒一次,不要用這個 Session 的 Chrome 來作為日常使用 ! 不然便會有機會被惡意網站偷取個人資料了 ! > 參考資料 : https://stackoverflow.com/questions/20035101/why-does-my-javascript-code-get-a-no-access-control-allow-origin-header-is-pr
泰國河濱夜市是一個位於曼谷的夜市,在 2013 年第一次來泰國時 19 筆者到過這裏,那是還是一個初開發地方,只有室內的小店鋪,還沒有發展到碼頭的位置。到2016 年再來時已經發展到有摩天輪的及完善飲食的大型市集了。 這次 2019 再來,拍了一段小影片遊走在市集之間。 ### 交通資訊 乘坐 BTS 坐到 Saphan Taksin 站,然後到沙吞碼頭轉搭免費接駁船。  到達碼頭後在左邊有一條專門是去 Aisatique 或是 Icon Siam 的隊,小心不要上錯船,這裏有些船是到沿岸酒店的。 ### 營業時間 夜市的營業時間為每天 16:00pm 到 00:00am ### 場內介紹 如果是乘坐船來的話,下船後最先可以在碼頭走走,時間剛好的話還可以觀看日落呢 !   走過了碼頭,行進入一點就會看到像倉庫一樣的商店。  當然不少得地標性的摩天輪。  再走進一點是餐廳及市隻了啊 !!   文字介紹先到這裏,可以看看影片啊 !
今天在工作時找到了關於 NodeJS DES-ECB 的例子代碼,在這裏先記錄下來。 ### NodeJS 中的 DES 加解密 Node.js 自帶強大的加密功能 Crypto,它是基於 OpenSSL 庫實現的加密技術。 DES 是一種對稱加密算法,密匙長度必須是 8 的整數倍,在一些簡單的應用場景經常被使用。 為了網絡上信息傳輸的安全(防止第三方竊取信息看到明文),發送發和接收方分別進行加密和解密,這樣信息在網絡上傳輸的時候就是相對安全的。 DES 加密模式有: Electronic Codebook (ECB) , Cipher Block Chaining (CBC) , Cipher Feedback (CFB) , Output Feedback (OFB)。這裏以密文分組鏈接模式 CBC 為例,使用了相同的 key 和 iv (Initialization Vector)。 ```js const crypto = require('crypto') // DES 加密 function desEncrypt (message, key) { key = key.length >= 8 ? key.slice(0, 8) : key.concat('0'.repeat(8 - key.length)); const keyHex = new Buffer(key); const cipher = crypto.createCipheriv('des-cbc', keyHex, keyHex); let c = cipher.update(message, 'utf8', 'base64'); c += cipher.final('base64'); return c; } // DES 解密 function desDecrypt (text, key) { key = key.length >= 8 ? key.slice(0, 8) : key.concat('0'.repeat(8 - key.length)); const keyHex = new Buffer(key); const cipher = crypto.createDecipheriv('des-cbc', keyHex, keyHex); let c = cipher.update(text, 'base64', 'utf8'); c += cipher.final('utf8'); return c; } ``` ### NodeJS DES-ECB 上面的代碼情況是使用 DES-CBC 的,如果是要改為使用 DES-ECB 的話,則要把 `keyHex` 換為 `null` 值。 還要注意讀入的字串是什麼格式,即是 `base64` > `utf8` 等等,不然搞半天也隻會搞出亂碼來。 以下代碼是已經把上面的改為使用 DES-ECB Algorithm : ```js // import const crypto = require('crypto'); // DES-CBC Encrypt function desEncrypt (text, key) { key = key.length >= 8 ? key.slice(0, 8) : key.concat('0'.repeat(8 - key.length)); const keyHex = new Buffer(key); const cipher = crypto.createCipheriv('des-ecb', keyHex, null); let c = cipher.update(text, 'utf8', 'base64'); c += cipher.final('base64'); return c; } // DES-CBC Decrypt function desDecrypt (text, key) { key = key.length >= 8 ? key.slice(0, 8) : key.concat('0'.repeat(8 - key.length)); const keyHex = new Buffer(key); const cipher = crypto.createDecipheriv('des-ecb', keyHex, null); let c = cipher.update(text, 'base64', 'utf8'); c += cipher.final('utf8'); return c; } ``` 這段 Code 省掉了 19 筆者半日時間。 > 參考網址 : https://blog.niceue.com/front-end-development/des-encryption-and-decryption-in-nodejs.html
要美觀的 JSON 排列,大概是因為看 API 或者是 Debug 時才會用上。 ### JSON JSON 全名是 JavaScript Object Notation,也就是我們可以使用 JSON 的格式來使用文字的方式定義一個 Object 有什麽。 以下是一條隨機生成的 JSON 字串 : ```json [{"id":"5e18484f4e25d29c4c67233f","index":0,"guid":"29c564e9-138c-4cfe-9504-b0e84c9561f3","isActive":false,"balance":"$1,900.03","picture":"http://placehold.it/32x32","age":32,"eyeColor":"brown","name":"Erma Butler","gender":"female","company":"KNOWLYSIS","email":"ermabutler@knowlysis.com","phone":"+1 (959) 489-2209","address":"566 Gold Street, Cherokee, District Of Columbia, 3647","about":"Quis enim sunt cupidatat et fugiat enim id incididunt consectetur in. Consectetur dolore pariatur elit ullamco veniam. Exercitation fugiat elit ex fugiat. Aute ad nisi aute cupidatat.\r\n","registered":"2016-02-07T01:22:26 -08:00","latitude":79.400117,"longitude":81.521948,"tags":["consectetur","laboris","irure","in","sit","ad","nisi"],"friends":[{"id":0,"name":"Golden Small"},{"id":1,"name":"Felicia Gilmore"},{"id":2,"name":"Frankie Curry"}],"greeting":"Hello, Erma Butler! You have 5 unread messages.","favoriteFruit":"apple"}] ``` 下面這條 JSON 內容和上面是完全一樣的,不過就使用特定方法排列好 : ```json [ { "id": "5e18484f4e25d29c4c67233f", "index": 0, "guid": "29c564e9-138c-4cfe-9504-b0e84c9561f3", "isActive": false, "balance": "$1,900.03", "picture": "http://placehold.it/32x32", "age": 32, "eyeColor": "brown", "name": "Erma Butler", "gender": "female", "company": "KNOWLYSIS", "email": "ermabutler@knowlysis.com", "phone": "+1 (959) 489-2209", "address": "566 Gold Street, Cherokee, District Of Columbia, 3647", "about": "Quis enim sunt cupidatat et fugiat enim id incididunt consectetur in. Consectetur dolore pariatur elit ullamco veniam. Exercitation fugiat elit ex fugiat. Aute ad nisi aute cupidatat.\r\n", "registered": "2016-02-07T01:22:26 -08:00", "latitude": 79.400117, "longitude": 81.521948, "tags": [ "consectetur", "laboris", "irure", "in", "sit", "ad", "nisi" ], "friends": [ { "id": 0, "name": "Golden Small" }, { "id": 1, "name": "Felicia Gilmore" }, { "id": 2, "name": "Frankie Curry" } ], "greeting": "Hello, Erma Butler! You have 5 unread messages.", "favoriteFruit": "apple" } ] ``` 可讀性立即增加上 N 倍了 !!!! ### JSON.stringify() 如果是 JS 的関發者相信對 `JSON.stringify()` 也應該不會佰生,要把 Object 或是 Array 變成為 JSON 字串最正統就是用這個方法。原來我們只要在後面加入多兩個參數就可以把 JSON 輸出為已 Format 版本 !! ```js // data var data = { name: 'foo', pass: 'bar' }; // data to json string var jsonString = JSON.stringify(data, null, 4); // print json string console.log(jsonString); ``` 就會有以下輸出 : ```js // output { name: 'foo', pass: 'bar' } ``` > 在 PHP 上也有類似的動作可以把 JSON 對齊好 : https://19site.net/posts/80
我們會常常看到 HTML Form 要填上 `enctype` 才可以上傳檔案,究竟 HTML Form 的 `enctype` 是什麼來的呢? ### TL;DR 如果需要文件上傳,你只可以使用 `form-data`。 。 與 `x-www-form-urlencoded` 相比,`form-data` 是一種更高級的數據編碼方式。 您可以將 `x-www-form-urlencode` 視為 `txt` 文件,並將 `form-data` 視為 `html` 文件。最終,它們都提供了一些 `http` 的 payload。 ### Content Type ||content-type| |---| |x-www-form-urlencoded|application/x-www-form-urlencoded| |form-data|multipart/form-data; boundary={boundary string}| 瀏灠器通常會自己生成一個分隔的字串,用來分隔每筆傳動的資料,格式通常會是 : ```txt ----WebKitFormBoundaryFasdfWEfawEF3 ``` 這個字串使用者是可以自訂的。 ### Request Payload 假如有以下登入資料 : |fields|values| |---| |username|foo| |password|bar| 如果以上的 payload 使用 `x-www-form-urlencoded` 方法去表示的話,資料會使用 `encodeURIComponent()` 編碼並變成以下這樣 : ```text username=foo&password=bar ``` 而如果使用 `form-data` 的話,每一組資料 (key, value) 都會有自己的一個 section,並會使用 `{boundary string}` 為作分隔。以下會以 `form-data` 方法再把上面的資料整理一次 : ```text --{boundary string} Content-Disposition: form-data; name="username", foo --{boundary string} Content-Disposition: form-data; name="password", bar --{boundary string}-- ``` 如果 `form` 中有上傳檔案的話,內容會像以下 : ```text --{boundary string} Content-Disposition: form-data; name="username", foo --{boundary string} Content-Disposition: form-data; name="password", bar --{boundary string} Content-Disposition: form-data; name="file"; filename="image.jpg" Content-Type: image/jpeg, binary data... --{boundary string}-- ``` 我們把上面的 Body 分成為 4 部份,以下會為每一部份講解 : ### 第 1 部份 這裏儲存著資料 `username` 為 `foo`。 ```text --{boundary string} Content-Disposition: form-data; name="username", foo ``` ### 第 2 部份 這裏儲存著資料 `password` 為 `bar`。 ```text --{boundary string} Content-Disposition: form-data; name="password", bar ``` ### 第 3 部份 這裏儲存著上傳檔案的資料,欄位名為 `file`,檔案名為 `image.jpg`。 ```text --{boundary string} Content-Disposition: form-data; name="file"; filename="image.jpg" Content-Type: image/jpeg, binary data... ``` ### 第 4 部份 Body 的完結。 ```text --{boundary string}-- ```
曼谷遊的第五天,經過了昨天非常消耗體力的行程,這天我們決定會輕鬆一點的。早上在床上過了很久也沒法起床因為腳太痛了,到出門口時已經差不多12時。 我們決定再到 Terminal 21 找吃的,但是不會再吃 Pier 21 了 ... 好像已經吃了很多次。到達 Terminal 21 乘坐電梯到 4 樓時,看到 MK 火鍋的指示廣告板,就決定今天的午餐在 MK 火鍋解決。 ### MK火鍋 對 MK 火鍋的認識,是因為在2018年冬天來泰國的時候,參加了一個去華欣的本地旅行團。中午的時候汽車駛到路程的一半,好導遊帶我們到一個商場,說今天的午飯大家在這裡自費安排。並說這裏有一家MK火鍋是非常出名的,但是如果中午吃火鍋的話你們可能會趕不上車哦。 那時候我和太太邊行邊走,就看到導遊說那間 MK火鍋。在火鍋的門口那排隊的凳子上,竟然看到有兩個團友正在坐著。看來他們真的有自信可以一個小時內吃完火鍋呢 ! 然後我和太太便去了另一家吃泰菜的餐廳。吃完泰菜之後看時間也差不多,想預留少少時間去樓上的超市逛逛。當再次經過 MK 火鍋的時候那兩位團友還在裏面努力中。當然我們逛完超市後那兩位團友剛好吃完了,真是厲害,真的可以在一個小時內完成火鍋午餐。 那次之後便想,如果有機會必然會到 MK火鍋吃一餐。終於機會來了 !!!  在科技的加持下,點菜不再是難度了 !!!     MK 火鍋係有燒鴨叫的,不過比起港式的燒鴨,這裏的口味可能不太岩港人胃口,因為那個汁是好像用花生醬弄成的。味道不是不好,就是有怪不相襯。   四個人埋單大約 2,300 BAHT 左右,比起當地物價唔算是平,不過也是一個值得一試的地方,湯低弄得不錯。 ### Platinum Fashion Mall 在下午的時間我們決定分頭事,筆者會和老婆到 Platinum Fashion Mall 逛逛街。這是女士來曼谷必到的地方之一,在那裏筆者認為是曼谷的 "旺中"。衣服款式多,價格也相宜。   筆者在上年來的時間,Platinum 商場還只是個 "Platinum 商場",一年之差已經變為了 "Platinum Fashion Mall",其實這個商場一直也是在賣女裝衣服為主,早在第一次來泰國時已經是。不過經過重新命名後,更為專業。   逛完後再由原路走回去。   ### 晚餐 由於行得太累,所以先回飯店休息一會,然後到 Terminal 21 的 Foot Court 隨使吃了點東西就回去休息了。
2019 泰國遊這次跟了 KKDAY 的一日團遊泰國大城,探索一下泰國的歷史古都 ! 如果想看看筆者的大城遊記,可以到下面這篇文章 : 2019 曼谷遊 - Day4 https://19site.net/posts/66 ### 大成府 面積約為 2,556.6 平方公里,距首都曼谷約76公里。其疆域北連紅統府及華富里府;東瀕沙拉武里府,南毗巴吞他尼府,西臨素攀府及暖武里府。 大城府為廣闊平原,河道縱橫,是三條河流的匯合處。農業發達,為全泰國最大產米區。另外,水產豐富,且有不少大、中、小型工廠。 ### 歷史資料 1563年,緬甸東吁王朝攻滅阿瑜陀耶王朝,及後復國並延續30多年。 1767年,緬甸貢榜王朝軍隊攻陷大城,阿瑜陀耶王朝正式滅亡。後鄭昭重建王國,將首都南遷至吞武裏。 原王城遺址現為阿瑜陀耶遺蹟公園,被列為聯合國教科文組織世界遺產。該府作為大城王國時代的首都時,其文化、藝術、國際貿易均非常發達,很可惜,此古城遭入侵緬甸軍人縱火徹底破壞,現只剩下部份宮殿遺跡、珍貴佛像和精美雕刻等供人憑弔。 ### 參觀景點 ##### 邦芭茵夏宮   建於 17 世紀的泰皇夏日行宮,鄭王將首都遷至曼谷吞武里區後,邦芭茵夏宮漸漸被荒廢,隨著時間的流逝,年久失修的木製宮殿逐漸腐爛,後在19世紀開始重建,因當時西方文化的傳入,從邦芭茵夏宮的建築可以看出多種元素的融合,包含維多莉亞式、哥德式、中國式,當然還有泰國本身特有的建築風格。在此可以欣賞精心打理的園藝,享受宛如貴族的悠閒氣氛。 ##### 大城水上市場   乾淨整齊的大城水上市場,不同於丹能莎朵的喧鬧,是許多泰國本地人的景點名單之一。避開炎熱的太陽,沿著河岸兩旁建立的商家,探索濃厚古城特色的玩物,品嚐各樣經典泰式小點。若逛到腳酸,不妨體驗道地的泰式按摩。 ##### 崖差蒙空寺  保留完整的崖差蒙空寺,14世紀為紀念戰勝緬甸軍隊而修建,以戶外臥佛跟寶塔最為知名。高聳雄偉的錫蘭式高塔、連綿壯觀的佛像,你可以爬上塔頂,將硬幣投入許願井中,或是把硬幣貼在臥佛的腳底,許下心願、祈求好運。 ##### 瑪哈泰寺   位在遺址中心的瑪哈泰寺是城內最古老的廟宇,「樹中佛陀」更為大城必看景點之一。相傳當時緬甸軍人砍下這尊佛像時,落下的佛首滾到菩提樹旁,被樹根團團包圍保護,緬甸軍因此奇景被嚇退,才得以保有現在所看到的佛寺景象。 ##### 帕席桑碧寺 & 帕蒙空博大皇宮&涅槃寺   主要用來舉行皇家儀式和典禮的帕席桑碧寺,分別存放國王及皇室成員的骨灰在三座鐘型佛塔。即使遭遇戰亂的洗劫,這些佛塔至今仍昂然聳立在寺廟內, 一排排主殿屋簷的圓柱上仍保留姿態千萬的蓮花雕刻,向世人展示大城王朝歷久不衰的傲氣。 > 參考資料 : Wikipedia - https://zh.wikipedia.org/wiki/%E5%A4%A7%E5%9F%8E%E5%BA%9C > 參考資料 : KKDAY - https://www.kkday.com/zh-hk/product/18532