顯示具有 ReactJs 標籤的文章。 顯示所有文章
顯示具有 ReactJs 標籤的文章。 顯示所有文章

2018年5月14日 星期一

React & ASP.NET Core 2.0 Upload File

一個新增檔案的Form含有上傳圖片的功能並可預覽圖片,圖片與表單內容一起傳送到後端,資料新增後再存檔圖片

建立一個選取圖片的子Component
import React from 'react';
 
class ImageUpload extends React.Component {
    constructor(props) {
        super(props);
        this.state = { file: '', imagePreviewUrl: '' };
    }
 
    handleImageChange(e) {
        e.preventDefault();
 
        let reader = new FileReader();
        let file = e.target.files[0];
 
        reader.onloadend = () => {
            this.setState({
                file: file,
                imagePreviewUrl: reader.result
            });
        };
 
        reader.readAsDataURL(file);
        this.props.onImageChange(file);
    }
 
    render() {
        let { imagePreviewUrl } = this.state;
        let $imagePreview = null;
        if (imagePreviewUrl) {
            $imagePreview = <img src={imagePreviewUrl} />;
        } else {
            $imagePreview = (
                <div className="previewText">
                    Please select an Image for Preview
                </div>
            );
        }
 
        return (
            <div className="previewComponent form-group">
                <label htmlFor="file">{this.props.label}</label>
                <input
                    id="file"
                    type="file"
                    className="fileInput form-control"
                    accept="image/*"
                    onChange={e => this.handleImageChange(e)}
                />
                <div className="imgPreview">
                    {$imagePreview}
                </div>
            </div>
        );
    }
}
 
ImageUpload.defaultProps = {
    label: '上傳圖片',
    onImageChange: (file) => {
        console.log('default onImageChange() has been called', file);
    }
};
 
export default ImageUpload;

建立新增資料的Form的父Component,兩個輸入欄位的type分別為text和file
import React from 'react';
import axios from 'axios';
 
import ImageUpload from './ImageUpload';
 
class CreateProduct extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            product: '',
            file: '',
        }
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleChange = this.handleChange.bind(this);
        this.handleImageChange = this.handleImageChange.bind(this);
    }
    handleSubmit(e) {
        e.preventDefault();
        const formData = new FormData();
        const keys = Object.keys(this.state);
        keys.forEach(key => {
            formData.append(key, this.state[key]);
        });
        const headers = { 'Content-Type''multipart/form-data' };
        axios.post('api/product', formData, { headers: headers }).then(response => {
            console.log('create success', response);
        });
    }
    handleChange(e) {
        this.setState({
            [e.target.id]: e.target.value
        });
    }
    handleImageChange(file) {
        this.setState({ file });
    }
    render() {
        const { product } = this.state;
        return (
            <div>
                <form onSubmit={this.handleSubmit}>
                    <div>
                        <label htmlFor="url">Product</label>
                        <input
                            type="text"
                            id="product"
                            value={product}
                            onChange={this.handleChange}
                        />
                    </div>
                    <ImageUpload onImageChange={this.handleImageChange} />
                    <button type="submit">Submit</button>
                </form>
            </div>
        );
    }
}
 
export default CreateProduct;

後端接收資料資料的參數類型
public class ProductCreationModel
{
    public string Product{ getset; }
    public IFormFile File { getset; }
}

處理上傳的後端程式
public class AppService
{
    private readonly IHostingEnvironment _environment;
 
    public AppService(IHostingEnvironment environment)
    {
        _environment = 
            environment ?? throw new ArgumentNullException(nameof(environment));
    }
    public async Task<string> Upload(IFormFile file, string name = null)
    {
        if (string.IsNullOrWhiteSpace(_environment.WebRootPath))
        {
            _environment.WebRootPath = Path.Combine(
                Directory.GetCurrentDirectory(), "wwwroot"
                );
        }
        var dir = Path.Combine(_environment.WebRootPath, "uploads");
        if (!Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }
        var fileName = file.FileName;
        var path = Path.Combine(dir, fileName);
        if (!string.IsNullOrEmpty(name)) // 使用自訂的檔案名稱
        {
            var extension = Path.GetExtension(path);
            fileName = $"{name}{extension}";
            path = path.Replace(file.FileName, fileName);
        }
        if (file.Length > 0)
        {
            using (var fileStream = new FileStream(path, FileMode.Create))
            {
                try
                {
                    await file.CopyToAsync(fileStream);
                    return fileName;
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
        }
        else
        {
            throw new Exception("file is empty");
        }
    }
}




2018年3月15日 星期四

快速建立一個完整的React Application專案 (有路由 & 可呼叫後端API & 開發階段用webpack server)


整合以下連結內的教學建立一個專案:
1.[Webpack 4 Tutorial: from 0 Conf to Production Mode]
2.[React Redux Tutorial for Beginners: learning Redux in 2018]
3.[React Router v4 官方文件: Quick Start ]
4.[Redux Async Actions - Redux Tutorial #6]

安裝:
$ npm install --save-dev webpack webpack-cli webpack-dev-server babel-core babel-loader babel-preset-env babel-preset-react babel-plugin-transform-object-rest-spread html-webpack-plugin html-loader
$ npm install --save react react-dom react-router-dom prop-types redux react-redux redux-thunk redux-promise-middleware redux-logger axios

配置:
webpack.config.js
const HtmlWebPackPlugin = require('html-webpack-plugin');
module.exports = {
    devtool: 'inline-source-map',
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.html$/,
                use: [
                    {
                        loader: 'html-loader',
                        options: { minimize: true }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: './src/index.html',
            filename: './index.html'
        })
    ]
};

.babelrc
{
    "presets": ["env", "react"],
    "plugins": ["transform-object-rest-spread"]
}

package.jsonscript
    "scripts": {
        "start": "webpack-dev-server --mode development --open",
        "build": "webpack --mode production"
    },

專案結構:
[src]
          [js]
                   [actions]
                   [components]
                   [constants]
                   [reducers]
                   [store]
          index.js
          index.html

備註:
想關掉webpack server時按下Ctrl+C,但是還是在running,要真的停止必須執行:
taskkill /F /IM node.exe

Redux 的   Middleware 套件說明:
1.      reduxdispatch只接受plain object,可使用redux-thunk來讓dispatch也可以接受function
2.      使用redux-promise-middleware可以讓action.payloadPromise類型來達到簡化設計三種狀態:Penging, Fulfilled, Rejected
3.      redux-promise-middleware可以和redux-thunk結合起來鍊接action
範例:使用者登入後取得他的文章
const mapDispatchToProps = dispatch => {
    return {
        login: (email, password) => dispatch(login(email, password))
    };
};

export const login = (email, password) => dispatch => {
    dispatch({
        type: 'LOGIN',
        payload: axios.post('http://localhost:5000/api/Token', {
            email,
            password
        })
    }).then(reponse => {
        dispatch({
            type: 'FETCH_POSTS',
            payload: axios.get('http://localhost:5000/api/posts')
        });
    });
};



2017年2月18日 星期六

Redux的createStore實作

建立一個計數器範例,點擊頁面會執行累加計數
直接使用 createStore:
import { createStore } from 'redux';

const counter = (state = 0, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
}

const store = createStore(counter);
console.log(`initial state : ${store.getState()}`);

store.subscribe(() => {
    document.body.innerHTML = store.getState();
})

document.addEventListener('click', () => {
    store.dispatch({ type: 'INCREMENT' });

});

現在 counter 方法直接沿用,另外建立一個 createStore 方法:
const createStore = (reducer) => {
    // 這個 store 持有 state 變數
    let state;

    const getState = () => state;

    const dispatch = (action) => {

    };

    const subscribe = (listener) => {

    };

    // 回傳的物件被稱為 Redux store
    return { getState, dispatch, subscribe }

};

因為 subscribe() 可以被呼叫很多次,所以需要紀錄這些 listener:
    let listeners = [];

    const subscribe = (listener) => {
        listeners.push(listener)
    };

dispatch() 是唯一可以改變內部 state 的:
    const dispatch = (action) => {
        // 使用當前的 state 和 被 dispatch 的 action 物件當參數呼叫 reducer 計算出新的 state
        state = reducer(state, action);
        // 執行 listeners
        listeners.forEach((listener) => { listener() });
    };

還沒有實作 unsubscribe() 方法,先使用替代方案,在subscribe() 寫一個回傳一個方法:
        // 回傳一個方法
        // 使用方式:
        //     var s1 = store.subscribe(()=>{});
        //     s1(); // unsubscribe
        return () => {
            listeners = listeners.filter(l => l !== listener);
        }

最後在 createStore() 回傳前加入 dispatch({});
為了得到初始 state

createStore 完整範例:
const createStore = (reducer) => {
    let state;
    let listeners = [];

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action);
        listeners.forEach((listener) => { listener() });
    };

    const subscribe = (listener) => {
        listeners.push(listener);
        return () => {
            listeners = listeners.filter(l => l !== listener);
        }
    };

    dispatch({});

    return { getState, dispatch, subscribe }

};










2017年2月13日 星期一

用最少的套件安裝React應用程式並啟動

1.  建立資料夾,在該資料下開啟終端機並執行:
    $ npm init -y
 
    參考:
    https://docs.npmjs.com/cli/init

2. 安裝 React 套件
    $ npm install --save react react-dom

3. 安裝轉譯套件
    $ npm install --global babel-cli
    $ npm install --save-dev babel-preset-es2015 babel-preset-react

4. 建立 .babelrc
    {
        "presets" : ["es2015","react"]
    }

5. 安裝 webpack
    $ npm install --save-dev webpack

6. 建立 webpack.config.js
var path = require('path');

module.exports = {
  entry: './app/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};    
    參考:https://webpack.js.org/guides/get-started/

7. 執行 babel 和 webpack
    $ babel js/source -d js/build --watch
    $ webpack --config webpack.config.js --watch --colors --progress -d
    --watch : 程式有異動就執行webpack
    --colors : 顯示一些顏色
    --progress : 顯示執行進度
    -d : 加入 Source Map (方便 debug)
    -p : production code (不斷行也不空白的很醜的程式碼)

後續:
想使用babel-loader卻一直失敗,後來發現網路上很多教學文章都是用webpack 1,而我安裝的是2,
配置React的Babel和Webpack2環境:
https://segmentfault.com/a/1190000007000131
https://blog.madewithenvy.com/getting-started-with-webpack-2-ed2b86c68783#.zg5j3c46z
https://www.smashingmagazine.com/2017/02/a-detailed-introduction-to-webpack/?utm_source=javascriptweekly&utm_medium=email
簡單設定:
module.exports = {
    // ...其他設定
    module : {
        rules : [
            {
                test : /\.js$/,
                use : 'babel-loader',
                exclude : /node_modules/
            }
        ]
    }
}
指定路徑下的終端機輸入
$ code .
會運行Visual Studio Code並開啟指定目錄