2020年12月31日 星期四

React 練習 - 使用 css, scss, img loader, image 壓縮

自己寫的 React, Webpack 練習,

練習了以下 webpack loader:


sass-loader : 

用來處理編譯 sass 檔案。


css-loader : 

用來處理 css 檔案。


style-loader : 

用來將 css 寫進網頁中 (不用在網頁中另寫 <link rel="stylesheet" href="xxx.css" />)。


image-webpack-loader : 

用來處理圖片檔案,有圖片壓縮等功能。


url-loader :

用來將圖片的 url 寫進網頁中,內部使用了 file-loader,所以可使用 file-loader 的選項參數,

有能把圖片的 url 轉成 base64 url 的等功能。


mini-css-extract-plugin : 

用來把處理好的 css 輸出成最後版本 css 檔案的功能,

網頁要使用 css 需自行加入 css style include, Ex:

<link rel="stylesheet" href="xxx.css" />)


實作了一個簡易的 tab 選單當範例來練習:

1. 有三個選單 tab,分別對應三個內容 (暫稱 content),內容中有各自的文字及各放置了不同的圖片。

2. 當滑鼠滑入 tab 中時  (mouseenter) ,下方會切換到對應的 Content。

檔案結構如下圖所示:


重要的程式碼如下 (以下根目錄以從 package.json 檔所在位置,也就是 WebContent 資料夾算起):

/index.html :
<!DOCTYPE html>
<html>
    <head>
        
    </head>
    <body>
        <div id="root"></div>
        <script src="dist/js/index_bundle.js"></script>
    </body>
</html>
/dev/js/index.jsx :
import React from "react"
import ReactDOM from "react-dom"
import {Main} from "./components/Main.jsx"

import "@projectDirPath/dev/css/style.scss"

ReactDOM.render(<Main/>, document.getElementById("root"));
/dev/js/components/Main.jsx :
import React from "react"
import {Tab} from "./Tab.jsx"
import {Content} from "./Content.jsx"
import dogImgSrc from "@projectDirPath/dev/img/dog.jpg"
import catImgSrc from "@projectDirPath/dev/img/cat.jpg"
import bananaCatImgSrc from "@projectDirPath/dev/img/bananaCat.jpg"

function Main(){
    const [itemList, setItemList] = React.useState([
                                                        {
                                                            title: "title1",
                                                            desc: "desc1",
                                                            img: dogImgSrc,
                                                            isActive: true
                                                        },
                                                        {
                                                            title: "title2",
                                                            desc: "desc2",
                                                            img: catImgSrc,
                                                            isActive: false
                                                        },
                                                        {
                                                            title: "title3",
                                                            desc: "desc3",
                                                            img: bananaCatImgSrc,
                                                            isActive: false
                                                        }
                                                    ]);

    function setItemActive(index){
        var updatedItemList = itemList.map((item, i) => {
            item.isActive = (i == index);
            return item;
        });

        setItemList(updatedItemList);
    }

    return (
        <React.Fragment>
            <div className="tab">
                {
                    itemList.map((item, index) => {
                        return <Tab item={item} key={index} index={index} setItemActive={setItemActive}/>;
                    })
                }
            </div>
            <div className="content">
                {
                    itemList.map((item, index) => {
                        return <Content item={item} key={index}/>;
                    })
                }
            </div>
        </React.Fragment>
    )
}

export {Main}
/dev/js/components/Content.jsx :
import React from "react"

function Content({item}){
    return (
        <div className={item.isActive ? "active" : ""}>{item.desc}<img src={item.img}/></div>
    );
}

export {Content}
/dev/js/components/Tab.jsx :
import React from "react"

function Tab({item, index, setItemActive}){

    function handleMouseEnter(){
        setItemActive(index);
    }

    return (
        <div className={item.isActive ? "active" : ""} onMouseEnter={handleMouseEnter}>{item.title}</div>
    );
}

export {Tab}
/dev/css/style.scss :
.tab {
    div {
        display: inline-block;
        width: 30%;
        height: 100px;
        background-color: green;
        background-color: yellow;
        border-bottom: 3px solid black;
    }

    div.active {
        background-color: yellow;
        border-left: 3px solid black;
        border-right: 3px solid black;
        border-top: 3px solid black;
        border-bottom: 3px solid yellow;
    }
}
  
.content {
    background-color: yellow;
    border-left: 3px solid black;
    border-right: 3px solid black;
    border-bottom: 3px solid black;

    div {
        display: none;
        padding: 3px;
    }

    div.active {
        display: block;
    }

    img {
        width : 30%;
    }
 }


webpack.config.js:

  var path = require('path');
//const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
 entry : "./dev/js/index.jsx",
 output : {
  path : path.resolve(__dirname, "dist"),
  filename : "js/index_bundle.js",
  publicPath : "dist/"
 },
 resolve: {
    alias: {
        '@projectDirPath': __dirname
    }
},
 watch : false,
 devtool : 'source-map',
 mode : "development", //"production",
 module : {
  rules : [ {
   test : /\.jsx?$/,
   use : {
    loader : 'babel-loader',
    options : {
     presets : [ '@babel/preset-react', '@babel/preset-env' ]
    }
   }
  },
  {
    test: /\.(png|jpg|gif|jpe?g|svg)$/,
    use: [
      {
        loader: 'url-loader',
        options: {
          limit: 50 * 1024,
          outputPath: "img"
        }        
      },
      "image-webpack-loader"      
    ]
  },/*
  {
      test : /\.css$/,
      use : ["style-loader", "css-loader"]
  },*/
  {
    test: /\.s[ac]ss$/,
    use: [        
         "style-loader", //MiniCssExtractPlugin.loader,
         "css-loader", "sass-loader"
        ]
  }]
 }
 //, plugins: [new MiniCssExtractPlugin()]
}
  

package.json

  {
  "name": "reacttest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "runServer": "lite-server"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.7.5",
    "@babel/preset-env": "^7.7.5",
    "@babel/preset-react": "^7.7.4",
    "babel-loader": "^8.0.6",
    "css-loader": "^5.0.1",
    "file-loader": "^6.2.0",
    "image-webpack-loader": "^7.0.1",
    "lite-server": "^2.6.1",
    "mini-css-extract-plugin": "^1.3.3",
    "node-sass": "^5.0.0",
    "sass-loader": "^10.1.0",
    "style-loader": "^2.0.0",
    "url-loader": "^4.1.1",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10"
  },
  "dependencies": {
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-redux": "^7.2.2",
    "redux": "^4.0.5"
  }
}

  

原始碼分享下載:

ReactTest2.7z


參考:

  1. Webpack 4 筆記 (3)
  2. 新手向 Webpack 完全攻略 (7) – 編譯圖片?略懂略懂
  3. Webpack 前端打包工具 - 使用 image-webpack-loader 壓縮圖片
  4. Webpack 前端打包工具 - 使用 sass-loader 編譯 Sass/SCSS 預處理器
  5. Webpack 前端打包工具 - 使用 mini-css-extract-plugin 把 CSS 抽離出來
  6. NPM – Webpack – 使用 import 的絕對路徑方法

2020年12月2日 星期三

Java EE - JSP 自訂標籤 - 自訂 EL 函式

紀錄一下 在 JAVA EE JSP 中,

如何建立客制的、自己所使用的 "自訂EL 函式 (or 自訂 JSTL tag 標籤函式)" 。


首先先設定下範例需求:

1. 想在 JSP 中,使用如下程式碼來使用,

<%@ taglib prefix="customEL" uri="/WEB-INF/customTld.tld"%>

${customEL:contains(list, value)}

第一行使引入自行定義的 TLD 檔,

第二行是使用了在自訂的 TLD 檔裡,被設定了的一個 function, "contains(list, value)",

作用為:傳入一個 Collection 的物件, "list" 和一個 Object 的物件  (或int 之類的簡單型別), "value",

然後傳回一個 boolean 值來表示在 list 中有無存在 value ,有的話回 true、否則回 false。

底層其實就是呼叫 Collection 的 contains() ,即 return list.contains(value);


2. 開始制作自訂EL函式,首先先實作其邏輯,就是一個簡單的 Class 和其內的 method:

CustomElFunction.java:

package com.xxx.tld;

import java.util.Collection;

public class CustomElFunction {
	public static boolean contains(Collection collection, Object object) {
		return collection.contains(object);
	}
}

可以看到只是一個很普通的 Class,裡面設定了一個呼叫 Collection.contains() 的 method,要注意的是 method 必須為 static 的。
因為之後會在 TLD 檔來以 package path + Class Name + Method 簽名的方式設定 自訂EL 函式,
所以如果有想要建立多個 自訂EL函式,可以在一個 Class 裡加入多個 method,
或是再建一個 Class 加入 method 都可以。

3. 再來是建立自己的 TLD 檔案,內容如下:

customTld.tld:

<?xml version="1.0" encoding="UTF-8" ?>

<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
  version="2.0">
    
  <description>Custom JSTL functions library</description>
  <display-name>Custom JSTL functions</display-name>
  <tlib-version>1.0</tlib-version>
  <short-name>customEL</short-name>
  <uri>http://xxx.xxx.com/jsp/jstl/functions</uri>

  <function>
    <description>
      Returns true if this collection contains the specified element
    </description>
    <name>contains</name>
    <function-class>com.xxx.tld.CustomElFunction</function-class>
    <function-signature>boolean contains(java.util.Collection, java.lang.Object)</function-signature>
    <example>
      ${customEL:contains(list, value)}
    </example>  
  </function>

</taglib>

<descriptin>, <display-name>, <tlib-version>, <short-name> 等比較不重要可隨便填。
<uri> 只有在想用在 JSP 中用 uri 的方式時引入才需要,如是直接指定專案路徑下的相對位置,例如 /WEB-INF/customTld.tld , 則可以隨便填。

重要的是 <name>, <function-class>, <function-signature>,

<name> 是在 JSP 中呼叫自訂EL函式時要使用的函式名稱,
即 
${customEL:contains(list, value)}
中的 contains

<function-class> 為指定自訂EL函式所在的 Class path

<function-signature> 設定了自訂EL函式的函式方法簽名

最後,把 TLD 檔放到自己知道地方,從 JSP 中引入,就可以開始使用了,
例如如下:

xxx.jsp:
<%@ taglib prefix="customEL" uri="/WEB-INF/customTld.tld"%>

${customEL:contains(list, value)}

參考資料:

2020年11月18日 星期三

AES 加密 - JAVA 程式碼範例

紀錄一下 Java 使用 AES (Advanced Encryption Standard)  加解密的範例程式碼:

import java.io.UnsupportedEncodingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class AESTest {

	public static void main(String[] args) {
		try {
			String seedForRandom = "TestSeed";
			String plaintext = "TestPlaintext";
			
			KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
			// 設定特定的隨機數種子,
			// 如隨機數種子不變,就會得到相同的隨機數,
			// 這裡相當於每次都使用同樣的金鑰
			SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
			secureRandom.setSeed(seedForRandom.getBytes("UTF-8"));
			//產生 AES 金鑰
			keyGenerator.init(256, secureRandom);
			SecretKey secretKey = keyGenerator.generateKey();
			SecretKey AES_secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
			// 取得 AES 加解密器
			Cipher cipher = Cipher.getInstance("AES");
			
			//進行加密
			cipher.init(Cipher.ENCRYPT_MODE, AES_secretKey);
			byte[] encryptedText_byteArray = cipher.doFinal(plaintext.getBytes("UTF-8"));
			String encryptedText = new String(encryptedText_byteArray, "UTF-8");
			System.out.println(encryptedText); // W?\?? ???	U]?
			
			//對加密後的 byte[] 進行解密 (再取得一個新的 AES 加解密器來做)
			cipher = Cipher.getInstance("AES");
			cipher.init(Cipher.DECRYPT_MODE, AES_secretKey);
			byte[] decryptedText_byteArray = cipher.doFinal(encryptedText_byteArray);
			String decryptedText = new String(decryptedText_byteArray, "UTF-8");
			System.out.println(decryptedText); // TestPlaintext
			
			//注意!
			//加密後的 byte[] (decryptedText_byteArray) 不能與 String 互轉,
			//會流失一些東西,即上述的 encryptedText 會有轉不回 decryptedText_byteArray 的情況,
			//如果直接拿 encryptedText 來解密會出錯,例如以下例子會產生如下錯誤: 
			//javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
			//cipher = Cipher.getInstance("AES");
			//cipher.init(Cipher.DECRYPT_MODE, AES_secretKey);
			//byte[] decryptedText_byteArray2 = cipher.doFinal(encryptedText.getBytes("UTF-8"));
			// ==> Execption : javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
			
			//如上所述,替代方案可使用 base64 編碼,
			//因為加密後的 byte[] (即 decryptedText_byteArray) 可用 base64 編碼來在 byte[] 和 String 之間互轉,
			//如下所示:
			String base64Encoded_encryptedTextcryptedText = Base64.encodeBase64String(encryptedText_byteArray);
			System.out.println(base64Encoded_encryptedTextcryptedText); // V79c5vQIIP/R2wcWCVVdqA==
			
			cipher = Cipher.getInstance("AES");
			cipher.init(Cipher.DECRYPT_MODE, AES_secretKey);
			byte[] decryptedText_byteArray2 = cipher.doFinal(Base64.decodeBase64(base64Encoded_encryptedTextcryptedText));
			String decryptedText2 = new String(decryptedText_byteArray2, "UTF-8");
			System.out.println(decryptedText2); // TestPlaintext
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BadPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}


參考:

  1. 進階加密標準(Advanced Encryption Standard,AES)
  2. JAVA AES加密與解密
  3. Input length must be multiple of 16 when decrypting with padded cipher

2020年10月27日 星期二

Maven - 使用 profile 功能為不同環境設置不同內容的檔案 - 以 web.xml, proxool.xml 為例

 Maven 可以使用 profile 的功能來設定多個環境,

並對不同環境使用不同的設定,非常方便,

舉個例來說:

通常程式專案可能會有幾個開發階段,

例如開發、測試、正式上線等,

而在不同的階段,我們可能會想使用不同的設定,

例如 Java EE Web 專案的 web.xml 可能在不同的階段會想使用不同的內容設定、

或者是 DB connection pool, Proxool 的 proxool.xml 可能在開發階段想連本地端或測試機的資料庫 (Database),但在正式上線時想連正式機的資料庫等。


這篇文主要是來紀錄使用 Maven 的 profile 功能,來為一個 Jave EE Web 專案使用

不同的 web.xml 和 proxool.xml。

我們希望在使用 maven 產生 war 檔

(命令列指令:

mvn clean install -P{active 的 profile},例如:

mvn clean install -Pdev)

時,能夠依不同的 profile 使用不同的設定。


在這篇文的例子中,先說明幾個比較特別的地方:

  1. 此篇文的 Java EE Web 專案,是由沒有使用 Maven 的舊 Java EE Web 專案轉成的有使用 Maven 的新專案,故檔案結構跟一開始就使用 Maven + webapp archetype 不一樣。
  2. 此篇文使用 Maven 的 maven-war-plugin 來處理 war 檔的發佈,其中 web.xml 可以用 <webXml> 參數來設置。
    而 proxool.xml 用 <webResource> 參數來設置。


如果舊專案是用 Eclipse 產生出來的,可以對專案按右鍵,選 

Configure --> Convert to Maven Project

來將其轉成 Maven 專案,如下圖:



檔案結構如圖所示,其中我們在專案的根目錄下 (也是 pom.xml 所在的位置),建立了一個 config 資料夾,並在其中建立了三個不同環境的資料夾 (dev, demo, production),在各個環境的資料夾中都放置了一份 web.xml 和 proxool.xml,如下圖所示:






首先在 pom.xml 中設定 profile,例如在這我們分三個階段,開發 (dev)、測試 (demo)、正式上線 (production):

<profiles>
  	<profile>			
		<id>dev</id> 
		<properties>
			<profiles.active>dev</profiles.active>	
			<jdk>11</jdk>			
		</properties>
		<activation>
			<activeByDefault>true</activeByDefault>
		</activation>
	</profile>
	<profile>			
		<id>demo</id>
		<properties>
			<profiles.active>demo</profiles.active>			
			<jdk>11</jdk>			
		</properties>		
	</profile>
	<profile>			
		<id>production</id>
		<properties>
			<profiles.active>production</profiles.active>			
			<jdk>11</jdk>			
		</properties>		
	</profile>
</profiles>

接著來設定 web.xml 和 proxool.xml,我們使用了 maven-war-plugin (詳細參數) 來處理,
首先設定 warSourceDirectory 到 WebContent 資料夾 (平常預設是 ${basedir}/src/main/webapp),
然後用 webXml 設定 web.xml 所在的位置 (相對 pom.xml 所在的路徑),
最後用 webResource 設定 proxool.xml :
<plugin>
	<artifactId>maven-war-plugin</artifactId>
	<version>3.2.1</version>
	<configuration>
		<warSourceDirectory>WebContent</warSourceDirectory>
		<webXml>config/${profiles.active}/web.xml</webXml>
		<webResources>
			<webResource>
 				<directory>config/${profiles.active}</directory>
			  	<includes>
			  		<include>proxool.xml</include>
			  	</includes>
			  	<targetPath>WEB-INF</targetPath>
			</webResource>
		</webResources>
	</configuration>
</plugin>
在這邊,${profiles.active} 代表要發佈時所要使用的 profile,即 dev, demo 或 production,
profile 可以在
mvn clean install -P{要的 profile} 
指令中指定,例如如要使用 demo,指令就為:
mvn clean install -Pdemo,
如果沒有在命令中指定 profile (即 -P 參數),就會使用 profile 裡有設定 activeByDefault = true 的 profile (例如此篇文就是設定 dev)
<activation>
	<activeByDefault>true</activeByDefault>
</activation>

在 webResource 設定裡, targetPath 的位置是相對於 warSourceDirectory 的位置,
在這篇文中,就是 WebContent 資料夾

因為在這篇文中,我們沒有把 web.xml 和 proxool.xml 放在專案的 resource directory 之中 (config 資料夾不在 resource directory 裡),
也沒有放在 warSourceDirectory 的設定資料夾中,
也就是 web.xml 和 proxool.xml 不會被發佈到 war 檔裡面,
所以不需要特別對其做 resource excludes 的動作,
例如 resource 的 exclude 
或 <warSourceExcludes> (值為相對於 warSourceDirectory 設定的位置)


參考資料:

Maven - 引用本地端的 jar

這篇文主要來紀錄我使用 Maven 引用本地端 jar 檔的經驗。

在使用 Maven 時,如果想要加入到 library 的 jar 檔在 Maven 的 Repository 找不到、

例如:

  1. 因為某些授權的原因,jar 檔的作者不願意將之放到 Maven Repository 上面,
    或就是沒有人把其 jar 檔放到 Maven Repository 上面。
  2. 可能 jar 檔是來自於合作的第三方,例如跟公司合作的夥伴提供的專屬 jar 檔。
  3. 或是本來原本官方的 jar 檔,有被自己以手動的方式做二次修改的 jar 檔 ,可能是為了相容問題或修正某些官方 bug,例如:Proxool + MS SQL Server DB Driver的相容问题 (參考:
    JAVA+ Proxool+ SQLserver 2008 “signer information does not match signer information of other classe...
    Proxool+SQLserver2008(sqljdbc4.jar) integration problem
  4. 自己寫的,例如公司內部用的 jar 檔,或是想要把舊專案轉成 Maven 時,因年代已久無法考證 jar 檔來原,所以乾脆直接繼續拿來用的 jar 檔。

這時我們就必須要引用本地端 (local) 的 jar 檔,

Maven 大概有幾種作法:

  1. 在 pom.xml 使用 <scope>system</scope> 和 <systemPath> 來引用本地 jar 檔檔案路徑
  2. 將本地 jar 檔放到 local repository (使用命令列指令)
  3. 將本地 jar 檔放到 local repository (在 pom.xml 中加入設定)


首先先講我在實務上希望的需求是:

  1. 能夠在 pom.xml 中容易地看出哪些是自訂的 local jar dependency。
  2. 能夠看得出及容易找出 local jar 檔放在哪裡。
  3. 能夠在不下命令列指令的情況下成功地匯入 jar 檔到 local repository。
  4. 能夠跟 IDE 配合,例如: Eclipse Maven Plugin + Tomcat 

再來在以下分述這三種作法及我實務上的例子,及我最後的選擇偏好:


1. 在 pom.xml 使用 <scope>system</scope> 和 <systemPath> 來引用本地 jar 檔檔案路徑

第一種方法 (稱為 System Dependencies) 在使用上非常簡單,直接在 pom.xml 上指定 local jar 檔的檔案路徑,

例如:

<dependency>
        <groupId>myGroupId</groupId>
        <artifactId>myArtifactId</artifactId>
        <version>1.0.0</version>
	<systemPath>D:\myJar.jar</systemPath>
	<scope>system</scope>
</dependency>

其中 <systemPath> 中也可使用用一些 Maven 自己的參數,例如表示專案路徑的 ${basedir}

此方法有幾個缺點:

  1. Maven 官網說 <scope>system</scope> 的用法 (System Dependencies)已經 deprecated 了 (來源)
  2. 在要使用 maven 指令部屬專案時,例如要產生 war 檔時, mvn clean install 產生出的 war 檔裡並不會把用 System Dependencies 設定的 local jar 檔包進生產的專案中。
  3. 如果使用 Eclipse Maven plugin + Tomcat 來開發,用 System Dependencies 設定的 local jar 檔不會被部屬至 server 上 (例如:  C:\Users\xxxUser\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\xxProject\WEB-INF\lib)


2. 將本地 jar 檔放到 local repository (使用命令列指令)

此方法是使用 Maven 的指令直接將本地端的 jar 檔放進 local repository 中,命令如下 (大括弧 {} 中的值可自由填寫):

mvn install:install-file -Dfile={本地端 jar 檔的檔案位置} -DgroupId={自訂的 group id} -DartifactId={自訂的 artifact id} -Dversion={自訂的 version}

例如:

mvn install:install-file -Dfile=D:\myJar-1.0.0.jar -DgroupId=myGroupId -DartifactId=myJar

-Dversion=1.0.0

注意 jar 檔的檔名要改成符合 maven 格式,就是 artifactId-version 這種格式。

執行完命令後,可以到自己本地上的 local respository (通常位置在C:/Users/xxxUser/.m2/repository) 去看是否有成功匯入 jar 檔。

最後在 pom.xml 中加入引用 jar 檔的設定如下:

<dependency>
          <groupId>myGroupId</groupId>
          <artifactId>myArtifactId</artifactId>
          <version>1.0.0</version>
</dependency>


不過個人認為這種方法有一個缺點,

就是如果今天我們換了一台電腦或換了一個開發者要開發時,

必須要對 local jar 檔再一次執行上述的命令例指令以匯入 jar 檔 local repository,

並且單看 pom.xml 很難辨認出哪個 dependency 是使用了自訂的 local jar 檔。

對比我上述的需求,此方法:

  1. 無法在 pom.xml 中容易地看出哪些是自訂的 local jar dependency (除非把命令列指令存起來,再對照去找 local jar 檔位置)。
  2. 單看 pom.xml 看不出 local jar 檔放在哪裡 (除非例如把要下的命令列指令放到 project 中的某一個 file 上)。
  3. 需下命令列指令來匯入 jar 檔到 local repository。
  4. 能夠跟 IDE 配合,例如: Eclipse Maven Plugin + Tomcat 


3. 將本地 jar 檔放到 local repository (在 pom.xml 中加入設定)
補充:不確定是不是 Maven 3 版本以上才需要,發現除了本地 jar 檔,還需要再放一個本地 pom 檔 (除了附檔名,檔名跟本地 jar 檔一樣)

此方法是我目前最喜歡的方法,此方法跟第二種方法一樣,

都是把本地 jar 檔放到 local repository 中,不過差在不是執行命令列指令,

而是把要的設定寫在 pom.xml 中。

首先我們要在 pom.xml 中,設定自訂的 repository,例如:

<repository>
	<id>localLibRepository</id>
	<name>localLibRepository</name>
	<url>file:${basedir}/localLibRepository</url>
</repository>

其中 ${basedir} 為 Maven 的參數,代表專案的根目錄位置,

localLibRepository 為我自己建立的資料夾,用來存放 local jar。


設定好後,接著要來把 local jar 檔放到相應的位置去,

首先像上述第二種方法一樣,把 local jar 檔命名成 maven 的格式,例如: 

myArtifactId-1.0.0.jar

然後仿照 Maven Repository 的檔案路徑格式,即 

{repositoryPath}/{groupId}/{artifactId}/{version}/{jarFileName}

的規則建立資料夾 (如同 java 的 package 一樣,如 com.xxx.yyyy,應為 com, xxx, yyy 三個資料夾),並將 local jar 檔放到正確的位置上,例如:

D:\myProject\localLibRepository\myGroupId\myArtifactId\1.0.0\myArtifactId-1.0.0.jar

接著在同一個位置增加 pom 檔,例如:

D:\myProject\localLibRepository\myGroupId\myArtifactId\1.0.0\myArtifactId-1.0.0.pom

內容範例如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>myGroupId</groupId>
  <artifactId>myArtifactId</artifactId>
  <version>1.0.0</version>
</project>

最後如第二種方法一樣,在 pom.xml中引入 local jar 檔的 dependency,例如:

<dependency>
          <groupId>myGroupId</groupId>
          <artifactId>myArtifactId</artifactId>
          <version>1.0.0</version>
</dependency>

之所以我最後選擇這個方法的原因是,它完全符合我的需求:

  1. 比較能夠在 pom.xml 中容易地看出哪些是自訂的 local jar dependency
    (對照 pom.xml 中自訂 repository 的設定,打開 repository 的資料夾即可得知)。
  2. 能夠看得出及容易找出 local jar 檔放在哪裡 (看 pom.xml 中即可得知自訂 repository 的位置)。
  3. 能夠在不下命令列指令的情況下成功地匯入 jar 檔到 local repository。
  4. 能夠跟 IDE 配合,例如: Eclipse Maven Plugin + Tomcat 


參考資料:

  1. 3 ways to add local jar to maven project
  2. Adding dependencies to local .jar files in pom.xml
  3. How to include local jar files in Maven project [duplicate]
  4. Maven私服上传pom和jar实操

2020年10月21日 星期三

React - Redux + Hook 練習 - ToDo List (待辦事項) 簡易實作

Hook 是 React v16.8 的新功能,讓我們在撰寫 React Component 時,

可以讓我們用 function 的寫法來取代 Class 的寫法,

來避免在 Class 中常會碰到的問題,例如:

1. Class 中的 this 意義常讓人無法容易地理解。

2. Component 上的事件 (event) 綁定,常須要多寫 bind() 的步驟,並且涵義也容易讓人不容易了解,例如:

class TextInput extends React.Component{

	constructor(props){

		super(props);

		this.onHandleTextChanged = this.onHandleTextChanged.bind(this);

	}

	onHandleTextChanged(e){

		this.props.handleTextChanged(e.target.value);

	}



	render(){

		return (

			<div>

					<input type="text" value={this.props.text} onChange={this.onHandleTextChanged} />

			</div>

		);

	}

}

而因為 function 不像 Class 可以儲存保存自己的成員變數,

為了可以實現像 Class 那樣保存、修改依賴於 Component 的成員變數,

React 使用了 useState() 這個 function,其可以以一個初始值做為輸入,

並返回一個包含 變數、修改變數的 function 的 array,

Ex:

import { useState } from "react"

const [text, setText] = useState("initial value");


React Redux 也推出了使用 React Hook 的版本,

其提供了 useSelector() 和 useDispatch() ,

useSelector() 可以讓我們方便得取得 Reducer Storde 裡 state 的值,

而不用像以前一樣使用

connect(mapStateToProps, actionCreators)

this.props.XXX

等的方法來取得 state 值,

Ex:

import {useSelector} from 'react-redux'

const toDoList = useSelector(state => state.toDoListReducer.toDoList);


useDispatch() 可以讓我們方便得送入要傳給 Reducer 的 Action 來觸發 Component render 元件繪製的行為,

而不用像以前一樣使用

connect(mapStateToProps, actionCreators)

this.props.xxxAction()

等的方法來傳遞 Action 給 Reducer,

Ex:

import {useDispatch} from 'react-redux'

import * as actionCreators from './action'

const dispatch = useDispatch();

function handleDeleteToDo(){

dispatch(actionCreators.deleteToDo(toDo.id));

}


這篇文要用 React Hook 來改寫上一篇,

React - Redux 練習 - ToDo List (待辦事項) 簡易實作

做為練習,並比較沒用 Hook 及有用 Hook 兩者程式碼的差異。


跟之前的例子比,有改動的檔案只有:

Main.jsx

TextDisplayer.jsx

TextInpupt.jsx

ToDoList.jsx

ToDoRow.jsx

如下圖所示


接著來看程式碼的部份:

Main.jsx :

import React, { useState } from "react"
import {TextDisplayer} from "./TextDisplayer.jsx"
import TextInput from "./TextInput.jsx"
import ToDoList from "./ToDoList.jsx"

function Main(){
	const [text, setText] = useState("");	

	return (
		<React.Fragment>
			<TextInput text={text} handleTextChanged={setText}/>
			<TextInput text={text} handleTextChanged={setText}/>
			<TextDisplayer text={text}/>
			<ToDoList/>
		</React.Fragment>
	);
}

export default Main

說明:

可以看到原本的 Main 不用 Class ,而改用了 function 去改寫,

因為只是 function ,所以連 constructor() 也不用了。

原本在用 Class Component 時,Component 所管理的 text 這個成員屬性,

是用 this.state = {text : ""} 的方式指定初始值的。

改用 Function Component 時,我們使用了 useState(初始值) 的方式來獲得設定了初始值的 text 變數,並且還得到了一個可以改變 text 的 function,即 setText() ,

const [text, setText] = useState("");

中的 text 及 setText() ,就跟以前一樣,

可以使用在 Main Component 中做UI的顯示,

也可以傳遞給 Child Component ,讓下層元件可以顯示及修改 text。

例如在 Main Component 中,我們把 text 及  setText() 傳遞給 TextInput、TextDisplayer 及 ToDoList。


TextDisplayer.jsx :

import React from "react"

function TextDisplayer({text}){
    return (<div>Text you typed : {text}</div>);
}

export {TextDisplayer}

說明:

TextDisplayer Component  也寫成了 Hook 的 Function Component 的形式,

此時接收上層元件的值改由從 Function Component 的輸入值傳進來,

輸入會是一個 Object, key 及 value 由上層元件傳進來的時候指定,

例如 在 Main.jsx 中用 text={text} 的方式傳 text 進 TextDisplayer 元件,

TextDisplayer 的 function component 輸入就會是:

{text : text的值} 


TextInpupt.jsx :

import React from "react"

import {useDispatch} from 'react-redux'
import * as actionCreators from './action'

function TextInput({text, handleTextChanged}){	
	const dispatch = useDispatch();

	function onHandleTextChanged(e){
		handleTextChanged(e.target.value);
	}

	function onHandleAddToDo(e){
		dispatch(actionCreators.addToDo(new Date().getTime(), text));
		handleTextChanged("");
		e.preventDefault();
	}

	return (
		<div>
			<form onSubmit={onHandleAddToDo}>
				<input type="text" value={text} onChange={onHandleTextChanged} />
				<input type="submit" value="addToDo"/>
			</form>
		</div>
	);
}

export default TextInput

說明:

TextInput Component 也被改寫成了 Hook 的 Function Component 形式,

此時不像 Class Component 時使用 this.props 的方式來取得上層元件傳進來的值,

而是直接從 Function Component 的 Input 輸入取得,

輸入會是一個 Object,其中會包括由上層元件傳進來的值,例如此例就是 Main.jsx 傳進來的

text 及 handleTextChanged 。

可以注意到的是, onChane 上面綁定的 onHandleTextChanged(),

不用再像以前 Class Component 那樣須要使用 bind() ,例如以前的寫法:

this.onHandleAddToDo = this.onHandleAddToDo.bind(this);

另一個可以注意到的是,我們沒有使用到如

export default connect(mapStateToProps, actionCreators)(TextInput)

的語法去得到 Reducer Store 裡的變數及函式,

那我們要怎麼把 Action 傳遞給 Reducer 呢?

答案是使用 Redux Hook 提供的 useDispatch() ,

其他傳回一個函式 dispatch(要送給 Reducer 的 Action) ,

我們可以直接把要傳遞的 Actoin 傳給 Reducer。


ToDoList.jsx :

import React from "react"
import ToDoRow from "./ToDoRow.jsx"

import {useSelector} from 'react-redux'

function ToDoList(){
	const toDoList = useSelector(state => state.toDoListReducer.toDoList);

	return (
		<ul>
			{	
				toDoList.map(function(toDo){
					return (
						<ToDoRow toDo={toDo} key={toDo.id}/>
					);
				})
			}
		</ul>
	);
}

export default ToDoList

說明:

ToDoList 元件跟 TextInput 元件一樣,沒有使用 connect() 來從 Reducer Store 取得資料,

那要怎麼取得 Reducer Store 中取得資料呢? 例如此例是要取得 toDoList。

答案是使用 Redux Hook 提供的 useSelector(Reducer 的 state) ,useSelector()會有 Redcuer 的 state 作為 callback 輸入,這樣我們就能取得 Reducer State 裡的資料,

在這裡因為我們之前有在 reducer.js 裡使用了 combineReducers() 去 combine toDoListReducer,所以取得 toDoList 的方式為 state.toDoListReducer.toDoList ,

如果沒有用 combineReducers() 的話,就會改成用

state.toDoList 直接取得值。


ToDoRow.jsx :

import React from "react"

import {useDispatch} from 'react-redux'
import * as actionCreators from './action'

function ToDoRow({toDo}){
	const dispatch = useDispatch();

	function handleDeleteToDo(){
		dispatch(actionCreators.deleteToDo(toDo.id));
	}

	return (
		<li>
			{toDo.toDoText}
			<button onClick={handleDeleteToDo}>Delete</button>
		</li>
	);
}

export default ToDoRow

說明:

ToDoRow 元件從本身 Function Component 的輸入中取得由 ToDoList 元件來的 toDo,

並且跟 TextInput 元件一樣使用了 dispatch() 來傳遞要送給 Reducer 的 Action,此例為 actionCreators.deleteToDo(toDo.id)

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

最後我們可以大概總結一下在這篇改用 React Hook 的文中,跟

React - Redux 練習 - ToDo List (待辦事項) 簡易實作

這篇沒使用 React Hook 文不一樣的地方:

  1. Component 元件全部由 Class 寫法改成 Function 寫法,
    故少了 Class 特性的語法,例如: constructor()、 this 的用法、 bind() 的用法。
  2. Component 自己管理的 state (例如 this.state = {text}),因為無法使用 Class 的 this 寫法,
    所以改成用 Hook 來管理,使用方法為 [變數名, 可修改變數的 function] = useState(初始值)。
  3. 上層把變數傳給下層的方法跟以前一樣,只是下層 Hook Component 因為不是 Class,
    所以不用 this.props ,改用從 Function Component 本身的輸入去取得,例如: 
    function ChildComponent({inputValue1, inputValue2, ...})
  4. 在取得 Reducer 管理的 state 方面,不使用 connect() 的方法來將 reducer state 及 action 綁到 this.props 上 ,而是使用 Redux Hook 所提供的 useSelector() 來取得,例如:
    var 要取得的 state value 變數 = useSelector(function(reducer的state){return 要的 value 變數});
  5. 在傳遞 Action 給 Reducer 方面,不使用 connect() 的方法來將 reducer state 及 action 綁到 this.props 上,而是使用 Redux Hook 所提供的 useDispatch() ,
    useDispatch() 可以回傳一個 dispatch(),
    dispatch() 可以輸入一個 action,其會將 action 傳遞給 Reducer,例如:
    var dispatch = useDispatch();
    dispatch(要傳遞的 action)。


最後要注意的地方是,

useState() 、useSelector()、useDispatch() 等

都要在 Function Component 的最上層呼叫,

不要在迴圈、條件式或是巢狀的 function 內呼叫 Hook (參考)。

要理解這個,我們可以從 Hook 實作的原理去理解。


首先是,為什麼元件寫成 function ,卻可以如 Class 搬擁有自己可管理的 state (例如 text),

因為 function 裡的所有變數應該都只是區域變數而己 (local variable),

應該無法長期保存其值 (不管有沒有被修改)。


React Hook 的實作方式可以簡單理解為 (**不代表真實實作的細節,只是類似,可以從這想法大致理解 Hook 的概念),

使用了兩個(或多個,端看 React Hook 的詳細實作)陣列 (或鍵結串列等類似有序串列的資料結構) 來儲存了所有使用

useState() 設定的 值 (暫且稱作 state value) 及 可修改值的function (暫且稱作 setState function),例如:

[value1, value2, value3, ...]  (暫且稱作 state array)

[setValu1, setValu2, setValu3, ...]  (暫且稱作 setState array)

在 React 要重新繪製 (Render) 所有元件時,就會依序執行所有function component 的 function,

如果是第一次繪製,呼叫 useState() 時,各 state value 會用初始值儲存在 state array 中,

並且在相應的 index 中存放可修改 value 的函式在 setState array 中 (所以函式會知道它要去修改哪個 state value)。

如果是之後的繪製,呼叫 useState() 時,各 state value 會以同樣順序的方式從 state array 中取出,

因為 state array 只是個陣列,所以每次取出時必須要照順序,因為它並不知道要取出的 state value 是給哪個 Component 用的,

這樣就能理解,為什麼在 Component 中,只能一次的,在最上層呼叫 useState(),

因為這樣才能確保取出 state value 時,每次都是照一樣的順序



源碼分享下載:
ReactReduxTest.7z

本Blog的其他相關文章:



參考資料:

  1. 使用 State Hook
  2. 打造你自己的 Hook
  3. React Redux | 小孩子才做選擇! Hooks 和 Redux 我全都要!
  4. 原始碼解析 React Hook 構建過程:沒有設計就是最好的設計
  5. React Hooks 不是黑魔法,只是陣列
  6. [译] 深入 React Hook 系统的原理
  7. Hooks
  8. How to pass a React Hook to a child component

2020年7月13日 星期一

使用 JACOB Java-COM Bridge 操作 Microsoft Outlook

Java 無法直接執行作業系統的部份函式庫,
例如無法直接執行並操作如 Microsoft Outlook, Word, Excel 等跟作業系統綁的較緊的軟體,
如果真得要使用 Java 來操作,
則必須要使用例如 JNI 或 Java-COM Bridge 等技術 (例如 JACOB, Jawin 等)來實現。

Java Outlook Connector 是一個提供 Java 控制本地端 Outlook 的 Java Lib,
將底層的 JNO、 COM、MAPI 等複雜的實現進行了封裝,
不過只有試用版 (每次執行都會跳出建議購買的視窗) 及付費版。

這裡要介紹的是另一個方案, JACOB,
為一個免費的 Java-COM Bridge ,
這邊要演示的是使用 JACOB 這個 Java-COM Bridge 來操作 Outlook 的範例。
要注意的是,這裡只是直接操作電腦上的 Outlook 軟體,
並非使用 JavaMail 來連接 mail server,
所以並不需要進行登入連接等動作,
可以想成類似直接使用 VBscript 來控制 Outlook。

首先先去 JACOB 官網下載所需的 JAR 檔和 DLL 檔,
裡面有提供最新版放在 Sourceforge.net 上的檔案下載頁面連結
下載完解壓縮後可以看到所需的 jacob.jar、jacob-1.19-x64.dll  (或著是 32 位元,依執行環境自行選擇),也有附上 Javadoc 的說明文檔。

引入 jacob.jar、jacob-1.19-x64.dll  (例如設定環境變數 path、或放到System32 資料夾下、或放到程式同目錄等,
可參考
source of com.jacob.com.LibraryLoader
System.loadLibrary() 的使用方法彙總
) 後,
就可以開始撰寫程式。

範例程式碼如下,
其中讀取了 Outlook 的 INBOX 收件夾,
找出特定 Subject 的信,並將信上的附件檔存到本地資料夾。

package outlookReaderTest;

import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.Dispatch;

public class JacobTest {

 public static void main(String[] args) {
  // 指定 INBOX 收件夾,參考 https://docs.microsoft.com/zh-tw/office/vba/api/outlook.oldefaultfolders
  int olFolderInbox = 6; 
  ActiveXComponent outlookApplication = new ActiveXComponent("Outlook.Application");
  Dispatch outlookObject = outlookApplication.getObject();
  Dispatch myNamespace = Dispatch.call(outlookObject, "GetNamespace","MAPI").toDispatch();
  Dispatch myFolder = Dispatch.call(myNamespace,"GetDefaultFolder", Integer.valueOf(olFolderInbox)).toDispatch();
  Dispatch mails = Dispatch.get(myFolder, "Items").toDispatch();
  
  /*
   * 上述程式碼等同於 VBscript 的:
   * Dim outlookObject, myNamespace, myFolder, mails
   * Set outlookObject = CreateObject("Outlook.Application")
   * Set myNamespace = outlookObject.GetNamespace("MAPI")
   * Set myFolder = myNamespace.GetDefaultFolder(6)
   * Set mails = myFolder.Items
   * 下面的程式碼跟 VBscript 的對照可以此類推
   */

  int count = Dispatch.call(mails, "Count").getInt();
  System.out.println("Total Mail Count: " + count);
  for (int i = 1; i <= count; i++)
  {
   Dispatch mail = Dispatch.call(mails, "Item", Integer.valueOf(i)).toDispatch();
   String subject = Dispatch.call(mail, "Subject").getString();
 
   if ("This is a test mail".equals(subject)) {
    System.out.println(i);
    System.out.println(subject);
    
    String saveFolderPath = "D:\\tempTestFoler";
    
    //Save attachments
    Dispatch attachs = Dispatch.call(mail, "Attachments").toDispatch();
    int attachCount = Dispatch.call(attachs, "Count").getInt();
    System.out.println("attachCount: " + attachCount);
    for (int j = 1; j <= attachCount; j++) {
     Dispatch attach = Dispatch.call(attachs, "Item", Integer.valueOf(j)).toDispatch();
     String attachName = Dispatch.call(attach, "FileName").getString();     
     Dispatch.call(attach, "SaveAsFile", saveFolderPath + "\\" + attachName);
     System.out.println(attachName + " -- saved");
     //Dispatch.call(mail, "Delete");
    }
   }
  }
 }

}





參考:

  1. Open source java library to read outlook emails, calendar etc
  2. 【JACOB】Java-COM Bridge 介紹
  3. COM - Microsoft物件導向開發架構的基礎
  4. Java Outlook Connector 

2020年5月3日 星期日

將 java 的 Excpetion.printStackTrace() 輸出內容寫到 String 中

將 java 的 Excpetion.printStackTrace() 輸出內容寫到 String 中

import java.io.PrintWriter;
import java.io.StringWriter;

public class Test {

 public static void main(String[] args) {
  try{
   //some code
  }catch(Exception e){
   StringWriter stringWriter = new StringWriter();
   e.printStackTrace(new PrintWriter(stringWriter));
   String exceptionString = stringWriter.toString();
   System.out.println(exceptionString);
  }
 }

}


參考:
  1. [程式][Java] Exception StackTrace to String


2020年3月16日 星期一

ffmpeg 常用命令

ffmpeg 基本參數:
-y : 如果輸出檔案已存在,不詢問直接覆蓋
-n : 如果輸出檔案已存在,不詢問直接忽略不覆蓋

注意:
如果輸出檔案跟輸入檔案相同,
在為 video 檔案時,-y 會失效,
會印出
FFmpeg cannot edit existing files in-place.
錯誤

ffmpeg 常用命令:
=============================================================
將圖或影片 Resize 成特定尺寸:
Ex : 將圖或影片 resize 成 320x240
D:\\ffmpeg\\bin\\ffmpeg -i "input.jpg" -vf scale=320:240 "output_320x240.png"
** 注意:
因為 h264 encoder 規定輸出影片長寬須為偶數,
如指定奇數會有 "ffmpeg height not divisible by 2" 的錯誤訊息,可將指令改成:
D:\\ffmpeg\\bin\\ffmpeg -i "input.jpg" -vf scale=floor(321/2)*2:floor(241/2)*2 "output_321x241.png"

=============================================================
將圖或影片在保持長寬比 ration 的情況下 Resize 到能塞到特別尺寸的矩形中,
如原圖檢來就可塞得下,就不 resize
Ex: 將圖或影片保持長寬比並 resize 至塞得下 300x500 的矩形中
D:\ffmpeg\bin\ffmpeg -i "input.jpg" -vf "scale='min(iw, 300)':'min(ih, 500)':force_original_aspect_ratio=decrease" "output_boundedBy_300x500.png"
** 注意:
因為 h264 encoder 規定輸出影片長寬須為偶數,
如指定奇數會有 "ffmpeg height not divisible by 2" 的錯誤訊息,可將指令改成:
D:\ffmpeg\bin\ffmpeg -i "input.jpg" -vf "scale='floor(iw*min(1,min(301/iw,501/ih))/2)*2':'floor(ih*min(1,min(301/iw,501/ih))/2)*2'" "output_boundedBy_301x501.png"

參考:

  1. FFMPEG (libx264) “height not divisible by 2”
  2. ffmpeg转码遇见错误height not divisible by 2的解决方案
  3. [FFmpeg] error about width or height not divisible by 2
=============================================================
將影片壓縮
D:\\ffmpeg\\bin\\ffmpeg -i "inputVideo.mp4" -r 10 -b:a 32k "outputVideo.mp4"
=============================================================
得到影片中,特定秒數幀(frame)的圖片
Ex: 取得 22:11:00 的 frame
D:\\ffmpeg\\bin\\ffmpeg -i "inputVideo.mp4" -y -f  image2  -ss 22:11:00 -vframes 1 "outputFrame.jpg"
=============================================================
將影片中的 "影片資訊 (moov box 或稱 movie box)" 放到檔案二進位較前的位置,讓例如瀏覽器能夠先讀到馬上開始邊播放邊下載影片 (通常瀏覽器需得到如影片長度等資訊後才會開始播放影片),
而不用等到讀到 "影片資訊 (moov box)" 後才開始播放
參考 : MP4 moov box解析
D:\\ffmpeg\\bin\\ffmpeg -i "inputVideo.mp4" -movflags faststart -acodec copy -vcodec copy "outputVideo.mp4"
=============================================================
取得資源 (影片檔或圖片檔,也可為外部網址 url),的資訊,並以 json 格式輸出,
例如長、寬、影片長度等
D:\\ffmpeg\\bin\\ffprobe -show_streams -print_format json "imageOrVideoUrl"
=============================================================
對圖片或影片進行截剪
Ex: 將圖片在 x=135, y=75 的位置(以左上為原點,向右向下為正),以width=270, height = 150 進行截剪
D:\\ffmpeg\\bin\\ffmpeg -i "input.jpg" -vf crop=270:150:135:75 "out.jpg"
=============================================================
將GIF動圖的某個幀(frame)取出,
或者是將所有幀全數取出 (輸出檔名用 %d ,之後會得到多個圖,檔名中的 %d 會被幀數代替 )
Ex:
D:\\ffmpeg\\bin\\ffmpeg -i "input.gif" -vsync 0 "output.png"
D:\\ffmpeg\\bin\\ffmpeg -i "input.gif" -vsync 0 "output_%d.png"
=============================================================
旋轉或翻轉圖片和影片
D:\\ffmpeg\\bin\\ffmpeg -i -vf "transpose=1" output.mp4
Note:
transpose = 0 : 順時鐘團 90 度後,水平左右翻轉
transpose = 1 : 順時鐘團 90 度
transpose = 2 : 逆時鐘團 90 度
transpose = 3 : 順時鐘團 90 度,水平左右翻轉

可以連續旋轉翻轉,用transpose 指令用雙引號(")包住並用逗號(,)隔開,例如
D:\\ffmpeg\\bin\\ffmpeg -i -vf "transpose=1,transpose=1" output.mp4
參考:
How To Rotate Videos Using FFMpeg From Commandline


2020年3月7日 星期六

React - Redux 練習 - ToDo List (待辦事項) 簡易實作

React 如果使用的元件嵌套過於多層,
需要用到的 state 傳值就會必須由上層元件一路傳到下層子元件,
當傳值層數過多的時候,會造成每一層的子元件都要撰寫傳值的程式碼,
而最下層如果要改變某個 state 值,要呼叫 root 元件的某 function 時,
又需要把 function 從 root 元件一路傳到子元件。

例如:
<Element1>
          <Element2 attr={this.state.attr} func1={this.func1}></Element2>
</Element1>

<Element2>
          <Element3 attr={this.props.attr} func1={this.props.func1}></Element3>
</Element2>

<Element3>
          <Element4 attr={this.props.attr} func1={this.props.func1}></Element4>
</Element3>

<Element4>...............</Element4>

而且一路傳值的中間,有的元件可能根本不需要用到某些值。

為了處理上述的問題,
有了 Redux 這樣的一個工具可以幫助我們,
Redux 並不完全是為 React 而開發的,
不過它的概念可以幫助我們。

我們希望可以把 React 的各層元件中,
所用到的 state, function 等都集中在最上層的某個地方來管理,
並且把 state, function 一路傳到 Root 元件上的各子元件中以供利用,
但又不想把傳值的程式碼寫在各子元件上 。

Redux 的想法跟我們很像,
它用 store 來管理 state,state 只會被叫做 action 的 function 來改變,
並且我們可以把 callback function 註冊  (subscribe) 給 store,
只要呼叫 action 來改變 state 時,被註冊的 callback function就會被執行。

對應 React 的概念是,
用 Redux 來管理 state,
用 action 來改變 state,
將 React Render 繪製元件的 function (相當於 render() ) 註冊到 store 上,
將 store 的 state, action 一路傳給所有子元件,
子元件可以從 store 中取得 state 來顯示,
也可取得 action 來改變 state,
而action 被執行時就會執行被註冊的 callback function 來繪製 React 元件。

不過通常我們不會直接使用 redux 在 react 中,
而是使用 react-redux,
react-redux  幫我們處理並優化了 react 與 redux 合作時的細節部份,
並且已經註冊了 react render 繪制 function,
我們只要專注使用 action 來改變 state,
react-redux 就會幫我們自動重繪元件

在這篇文中,我修改了
這篇
React 練習 - ToDo List (待辦事項) 簡易實作
,將它用 Redux + React 的方式改寫

首先安裝 redux 和 react-redux
npm install redux
npm install react-redux



使用 Redux 管理一個統一的 state,其可在各 component 內之間使用,
state 中包括一般 state 及可跟 reducer 配合的 actionCreator

reducer :
Redux 用來決來決定其管理 state 的依據,
為一個 function,輸入為"前一個 state" 和 "要做的行為 action (通常為一個物件,內含純文字的 type 和 action 需要的參數)",返回值為更新後的 state:
Ex :
newState reducer(oldState, action){
     var newState = defaultState;
     switch (action.type){
          case "action1" :
              newState  = .........;
          break;
     }
     return newState;
}

actionCreator :
為多個 function 的集合,共通點是返回 reducer function 所需的 action 物件,
actionCreator 中的各 function 會傳遞給各 React 子元件中以供使用
Ex:
returnAction addToDo(id, text){
     return {id : id, text : text};
}

store :
Redux 所管理的 state

跟之前沒有使用 redux 的範例比較,
我們增加了 action.js 及 reducer.js


/src/components/main/reducer.js :
import { combineReducers } from 'redux'

function toDoListReducer(state = {toDoList: []}, action) {
  /*
  state : {
 toDoList : [{id : new Date().getTime(),toDoText : ""}]]
  }
  */
  switch (action.type) {
    case 'ADD_TODO':
      {
  //action.payload : [{id : new Date().getTime(),toDoText : ""}]  
  if (action.payload.id && action.payload.toDoText.trim()){
   return {
    toDoList : state.toDoList.concat([action.payload])
   };
  }else{
   return state;
  }
      }

    case 'DEL_TODO':
      {
  //action.payload : {id : toDoId}
  return {
   toDoList : state.toDoList.filter(function(toDo){
    return toDo.id != action.payload.id;
   })
  }
      }

    default:
      return state
  }
}

const reducers = combineReducers({
  toDoListReducer
})

export default reducers

說明:
在 reducer.js 中,宣告了一個 toDoListReducer() 來處理 state 及 action 的變化,
根據輸入的 actions 和上一次的 state ,回傳不同的新 state,
state 及 action 的物件格式可以由自己決定,
在這裡, state 裡管理一個 toDoList 陣例,其中存放各個待辦事項的 id 及文字內容。
而 action 裡有 type 及 payload 兩個屬性,type 用來表示是哪種 action,
而 payload 存放此 action 所需的其他資訊。

可以注意到的是, reducer function 可以不只一個 (雖然本例只有一個),
當有多個 reducer function 時,redux 提供了 combinReducers 來將所有的 reducer function
包起來一起 export 出去。

/src/components/main/action.js :
export const addToDo = function(id, toDoText){
 //action.payload : [{id : new Date().getTime(),toDoText : ""}]  
 return { 
  type: 'ADD_TODO',
  payload : {
   id : id,
   toDoText : toDoText
  }
 };
}

export const deleteToDo = function(id){
 //action.payload : {id : toDoId}
 return { 
  type: 'DEL_TODO',
  payload : {
   id : id
  }
 };
}

說明:
在 action.js  中,宣告了想要改變 toDoList 值的各種行為 function,
這些 function 之後會用 redux 傳進各元件中以供使用,
當各元件呼叫這些 action function 時, action function 會將其轉成 reducer function 需要的格式,例如此例中的
{
      type: "someAction",
      payload : { /* some infomation */}
}

/src/index.jsx :
import React from "react"
import ReactDOM from "react-dom"
import Main from "./components/main/Main.jsx"

import {Provider} from 'react-redux'
import {createStore} from 'redux'
import reducers from './components/main/reducer'

//below code if used for Chrome plugin, REDUX_DEVTOOLS
//const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
const store = createStore(reducers);

ReactDOM.render(
 <Provider store={store}>
  <Main/>
 </Provider>, 
 document.getElementById('root')
);

說明:
接下來,在 index.jsx 中,即在最頂層的 root element 中,
我們先利用 react 提供的 createStore() 結合之前建立的 reducer 來得到交結 redux 管理的 store,
再利用了 react-redux 提供的 <Provider> 元件,
來將 store 傳給所有子元件。
需要注意的是,我們只是將 store 傳給子元件,只包含了 state 及 action 的關係。
之後再各子元件中,可依各自的需要將 store 的 state 及 action 取出來使用。
也就是不用再像沒有 redux 時,一定要用 this.props 從父元件獲取,
父元件也不用特別寫傳值的程式碼。

題外:
REDUX_DEVTOOLS 是一個 Chrome 的 Redux 外掛,安裝後,在使用 conntect() 時用以下的方式:
const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
即可以在 Chrome 中對 Redux 進行 debug,例如觀察 state 的變化。
REDUX_DEVTOOLS 應用程式商店
REDUX_DEVTOOLS 官網

我們這次只有利用 redux 來管理待辦事項,
沒有委託 redux 來管理 User 在輸入框中即時輸入的字串,
所以 TextDisplayer.jsx 完全不需要修改,還是使用 react 本來管理的方式 。

/src/components/main/Main.jsx:
import React from "react"
import {TextDisplayer} from "./TextDisplayer.jsx"
import TextInput from "./TextInput.jsx"
import ToDoList from "./ToDoList.jsx"

class Main extends React.Component{
	constructor(props){
		super(props);
		
		this.state = {
			text : ""
		};
		
		this.handleTextChanged = this.handleTextChanged.bind(this);
	}
	
	handleTextChanged(text){
		this.setState({text : text});
	}
	
    render(){
        return (
        	
        		
        		
        		
				
        	
    	);
    }
}

export default Main


/src/components/main/TextInput.jsx :
import React from "react"

import {connect} from 'react-redux'
import * as actionCreators from './action'

class TextInput extends React.Component{
 constructor(props){
  super(props);
  
  this.onHandleTextChanged = this.onHandleTextChanged.bind(this);
  this.onHandleAddToDo = this.onHandleAddToDo.bind(this);
 }
 
 onHandleTextChanged(e){
  this.props.handleTextChanged(e.target.value);
 }

 onHandleAddToDo(e){
  // addToDo() is set to this.props by connect(mapStateToProps, actionCreators)(TextInput)
  this.props.addToDo(new Date().getTime(), this.props.text);
  this.props.handleTextChanged("");
  e.preventDefault();
 }

 render(){
  return (
   <div>
    <form onSubmit={this.onHandleAddToDo}>
     <input type="text" value={this.props.text} onChange={this.onHandleTextChanged} />
     <input type="submit" value="addToDo"/>
    </form>
   </div>
  );
 }
}

const mapStateToProps = function(store){
 return { 
  storeState: store.toDoListReducer
 };
}
export default connect(mapStateToProps, actionCreators)(TextInput)

在 TextInput.jsx 中,我們需要取得 addToDo() 來添加待辦事項到 toDoList 裡。
我們使用了 react-redux 的 connect(mapStateToProps, actionCreators)(someElement)
來將 redux 管理的 state 、 action 並結合  store 中的 reducer 一起取出來放到 TextInput 元件的 props上。
在 TextInput 元件中,就可以使用 this.props.addToDo() 來增加待辦事項。
(為了方便,我們在 TextInput 中取出了所有的 state 及 action,但其實可以只取出部份的 state 及 action,例如 deleteToDo() 其實可以不用取出,因為沒有用到 )

接下來其他的元件: ToDoList.jsx、ToDoRow.jsx
也是用一樣的方式取出所需的 state, action,
用 this.props.someStateOrAction 來使用。
以下為程式嗎:

/src/components/main/ToDoList.jsx :
import React from "react"
import ToDoRow from "./ToDoRow.jsx"

import {connect} from 'react-redux'
import * as actionCreators from './action'

class ToDoList extends React.Component{ 
 render(){
  return (
   <ul>
    {  
     // toDoList is set to this.props.storeState by connect(mapStateToProps, actionCreators)(ToDoList)
     this.props.storeState.toDoList.map(function(toDo){
      return (
       <ToDoRow toDo={toDo} key={toDo.id}/>
      );
     })
    }
   </ul>
  );
 }
}

const mapStateToProps = function(store){
 return { 
  storeState: store.toDoListReducer
 };
}
export default connect(mapStateToProps, actionCreators)(ToDoList)

/src/components/main/ToDoRow.jsx :
import React from "react"

import {connect} from 'react-redux'
import * as actionCreators from './action'

class ToDoRow extends React.Component{
 constructor(props){
  super(props);

  this.handleDeleteToDo = this.handleDeleteToDo.bind(this);
 }

 handleDeleteToDo(){
  //deleteToDo() is set to this.props by connect(mapStateToProps, actionCreators)(ToDoRow)
  this.props.deleteToDo(this.props.toDo.id);
 }

 render(){
  return (
   <li>
    {this.props.toDo.toDoText}
    <button onClick={this.handleDeleteToDo}>Delete</button>
   </li>
  );
 }
}

const mapStateToProps = function(store){
 return { 
  storeState: store.toDoListReducer
 };
}
export default connect(mapStateToProps, actionCreators)(ToDoRow)

源碼下載