匯率計算器 by React

先上一張圖片

GIF演示

功能實現

組件分割示意圖

既然React的賣點是組件化(話說昨天面試小姐姐問React除了組件化還有什麼,我竟然忘了聲明式,悔しい)。上圖是本身的劃分思路:html

  1. 首先粉色部分(①區)顯示的是實時匯率,初始值是美圓和歐元的匯率比,當改變④或⑤,或者點擊⑥時,①區根據當前狀態進行實時切換,而②和③區的改變是不會影響①區的;
  2. ②和③區能夠輸入數字,根據目前選定的匯率,來實時換算貨幣值,好比就上圖來講,在②區輸入10,③區會實時變成8.12304;
  3. ④和⑤區是整個組件的核心,它們的值決定着其餘各組件的方方面面;
  4. 當點擊⑥區時,④和⑤當前選定的值會交換,②區值不會變,但由於匯率發生了變化,③區的值將發生變更,此時①區的匯率也將發生反轉;
  5. 固然最外層的App類控制api的異步加載。

關於API

這裏必須得多說幾句,簡直炸毛。前端

首先是在fixer申請了key,還不錯,看下圖。然鵝坑爹的是,他們家不提供貨幣全稱API(好比USD對應United States Dollor;CNY對應Chinese Yuan醬紫)。雖然作這個項目只須要實時匯率就足夠了,可是隻顯示乾巴巴的簡稱確定是不友好滴。node

fixer匯率API

而後又去了currencylayer申請了key,貨幣全稱卻是有了,看下圖,但他家的匯率api超級坑爹,免費用戶只能以美金爲base貨幣,不像fixer無限制。遂兩家api一塊兒用...react

貨幣全稱api

具體實現

App類

名字懶得起了,就叫App好了,這個類主要用來異步加載兩個json文件,第一次用到了fetch,據說能代替傳統的Ajax,與之相似的還有什麼axois、superagent,忙過這幾天依次作一下研究。git

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {ratesObj: '', countriesObj: ''};
    }

    componentDidMount() {

        // 注意fetch的語法,其實跟Promise差很少少
        this.getCurrencyRates = fetch(this.props.api.getCurrencyRatesAPI).then(res => {
            res.json().then(resJSON => this.setState({ratesObj: resJSON.rates}));

        });

        this.getCountries = fetch(this.props.api.getCountriesAPI).then(res => {
            res.json().then(resJSON => this.setState({countriesObj: resJSON.currencies}));

        });
    }

    componentWillUnmount() {
        this.getCurrencyRates.abort();
        this.getCountries.abort();
    }

    render() {
        return (
            <div>
                <CountryChoice ratesObj={this.state.ratesObj} countriesObj={this.state.countriesObj}/>
            </div>
        )
    }
}

ReactDOM.render(
    // 這裏的api是個對象,存放上面所說的兩個url,這裏就不貼出來了
    <App api={api}/>,
    document.getElementById('root'),
);

實時匯率展現類

這個類就是個受,本身啥都幹不了。當select元素觸發onchange事件抑或button被點擊(觸發onclick事件)時,這裏就會發生變更。這個類只須要接受CountryChoice類(就是上面說到的核心類)的三個參數,分別是示意圖中④和⑤區正在被選中的那兩個幣種,還有就是兩個幣種之間的匯率。github

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

    render() {
        return (
            <h1>1 <span
                className="exchange-country-name">{this.props.firstSelectedCountry}</span> = {this.props.latestRates + ' '}
                <span className="exchange-country-name">{this.props.secondSelectedCountry}</span>
            </h1>
        );
    }
}

exchange按鈕類

這個類接受一個Boolean類型的flag,flag的定義一樣是在CountryChoice類裏面,每點擊一次按鈕,flag值就會從true和false之間切換,而後經過this.props.buttonToggle這個方法將實時的flag值傳遞迴CountryChoice類,固然this.props.buttonToggle方法定義在CountryChoice類裏,下面會說到。面試

// exchange按鈕交換兩個select,同時會改變 實時匯率展現 模塊
class ChangeCountry extends React.Component {
    constructor(props) {
        super(props);
        this.state = {currentFlag: this.props.currentFlag};
        this.buttonClick = this.buttonClick.bind(this);
    }

    buttonClick() {
        // 必定要把 !this.state.currentFlag 先存到一個變量,再把這個變量賦值到setState裏
        // 不然第一次點擊按鈕仍是true,第二次才變成false
        const currentFlag = !this.state.currentFlag;
        this.setState({
            currentFlag: currentFlag
        });
        this.props.buttonToggle(currentFlag);
    }

    render() {
        return (
            <button className="button" onClick={this.buttonClick}>Exchange</button>
        )
    }
}

金額輸入類

首先先寫一個isNumber方法,是爲了阻止用戶輸入非數字,且只能輸入一個小數點。由於有兩個input元素,因此先給兩個元素命名。由於在任意一個input中輸入值都會實時影響到另外一個,因此這裏就涉及到了<mark>狀態提高</mark>問題,能夠去研究官方文檔-狀態提高的這個例子。json

// 命名兩個input輸入框
const inputNames = {f: 'firstInput', s: 'secondInput'};

function isNumber(input) {
    if (/^[0-9]+([.][0-9]*)?$/.test(input) === false) {
        return input.slice(0, -1)
    } else {
        return input
    }
}

class MoneyInput extends React.Component {

    constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(ev) {
        this.props.onInputChange(isNumber(ev.target.value));
    }

    render() {
        const inputName = this.props.inputName;
        const inputValue = this.props.inputValue;

        return (
            // 這裏用到了ES6的計算屬性名
            <input name={inputNames[inputName]} className="input-value" type="text" value={inputValue}
                   placeholder="0" onChange={this.handleChange}/>
        );
    }
}

幣種(國家)選擇類

高潮來了,額,重點來了。這個類控制着幣種的選擇,解釋都放在了註釋裏,代碼寫得有點兒繞,都是exchange按鈕惹的禍。寫下來發現React的一個重點就是React 組件間通信:segmentfault

  • 【父組件】向【子組件】傳值;
  • 【子組件】向【父組件】傳值;
  • 沒有任何嵌套關係的組件之間傳值(如:兄弟組件之間傳值)

這裏推薦兩篇文章,一個是淘寶前端團隊的React 組件間通信;另外一個是在SegmentFault的一篇文章React 組件之間如何交流,把人家演示的例子敲一敲,基本上就能理解了。api

// 貨幣選擇:初始化第一個選中的是美刀,第二個選中的歐元
// 貨幣選擇的變化影響着 匯率展現 和 匯率計算
class CountryChoice extends React.Component {
    constructor(props) {
        super(props);

        // 初始化狀態
        this.state = {
            firstSelectedCountry: 'USD',// 第一個默認被選中的幣種是美金
            secondSelectedCountry: 'EUR',// 第二個默認被選中的幣種是歐元
            flag: true,// 立一個flag,初始是true,button被點擊時在true和false之間切換
            inputName: 'f',// 默認選中的是第一個input標籤
            inputValue: ''// 默認input標籤的值是空
        };

        this.handleChange = this.handleChange.bind(this);
        this.buttonToggle = this.buttonToggle.bind(this);
        this.firstInputChange = this.firstInputChange.bind(this);
        this.secondInputChange = this.secondInputChange.bind(this);
    }

    // 經過ChangeCountry類傳遞過來的flag值來設置成當前狀態
    buttonToggle(flag) {
        this.setState({flag: flag})
    }

    // 設置select標籤的狀態

    // 當flag是true時,把firstSelectedCountry的狀態設置爲name屬性爲「first-select」的value,
    // 把secondSelectedCountry的狀態設置爲name屬性爲「second-select」的value

    // 當flag是true時,把firstSelectedCountry的狀態設置爲name屬性爲「first-select」的value,
    // 把secondSelectedCountry的狀態設置爲name屬性爲「second-select」的value

    // 也就是說當flag是false時,此時name屬性爲「first-select」的select標籤控制的是name屬性爲「second-select」的select標籤
    // 固然我這裏設計的不合理,應該經過動態修改name值纔好,放在下一次的項目迭代吧,留個坑先
    handleChange(ev) {
        const target_name = ev.target.name;

        if (this.state.flag) {
            if (target_name === 'first-select') {
                this.setState({firstSelectedCountry: ev.target.value});
            } else if (target_name === 'second-select') {
                this.setState({secondSelectedCountry: ev.target.value});
            }

        } else {
            if (target_name === 'first-select') {
                this.setState({secondSelectedCountry: ev.target.value});
            } else if (target_name === 'second-select') {
                this.setState({firstSelectedCountry: ev.target.value});
            }
        }
    }

    // 獲取第一個input輸入的值
    firstInputChange(inputValue) {
        this.setState({inputName: 'f', inputValue});
    }

    // 獲取第二個input輸入的值
    secondInputChange(inputValue) {
        this.setState({inputName: 's', inputValue});
    }


    render() {

        const inputName = this.state.inputName;
        const inputValue = this.state.inputValue;

        // 由於要用到用戶輸入的值乘以匯率來進行計算
        // 當用戶清空某個input標籤的值時,這裏就成了NaN
        // 這個函數就是當檢測到輸入的值爲空時,自動設爲數字0
        // 啊啊啊,確定有更好的方法
        function formatInputValue(inputValue) {
            if (inputValue === '') {
                inputValue = 0;
                return inputValue
            } else {
                return parseFloat(inputValue)
            }
        }

        // 這邊就寫的很笨重了,匯率是根據flag的狀態定的
        // 若是是true,匯率是第二個select標籤選中的值除以第一個select
        // 假設當前在第一個input輸入數值,那麼下面的 inputName === 'f' 就是true, 因此第二個input的值(sI)就會被實時計算
        // 反正就是很繞,若是不加exchange按鈕要省不少事兒,一切都是爲了學習...
        const fI = inputName === 's' ? formatInputValue(inputValue) * (!this.state.flag ? this.props.ratesObj[this.state.secondSelectedCountry] / this.props.ratesObj[this.state.firstSelectedCountry] : this.props.ratesObj[this.state.firstSelectedCountry] / this.props.ratesObj[this.state.secondSelectedCountry]) : inputValue;
        const sI = inputName === 'f' ? formatInputValue(inputValue) * (this.state.flag ? this.props.ratesObj[this.state.secondSelectedCountry] / this.props.ratesObj[this.state.firstSelectedCountry] : this.props.ratesObj[this.state.firstSelectedCountry] / this.props.ratesObj[this.state.secondSelectedCountry]) : inputValue;

        return (
            <div className="container">
                {/*這邊就是把當前狀態(兩個被選中的貨幣全稱和之間的匯率)傳遞給①區來顯示*/}
                <CurrentExchangeRate
                    firstSelectedCountry={this.state.flag ? this.props.countriesObj[this.state.firstSelectedCountry] : this.props.countriesObj[this.state.secondSelectedCountry]}
                    secondSelectedCountry={!this.state.flag ? this.props.countriesObj[this.state.firstSelectedCountry] : this.props.countriesObj[this.state.secondSelectedCountry]}
                    latestRates={this.state.flag ? this.props.ratesObj[this.state.secondSelectedCountry] / this.props.ratesObj[this.state.firstSelectedCountry] : this.props.ratesObj[this.state.firstSelectedCountry] / this.props.ratesObj[this.state.secondSelectedCountry]}
                />

                {/*當在第二個input輸入數字時,換算出來的值會實時顯示在第一個input裏*/}
                <div className="item">
                    <MoneyInput
                        inputName='f'
                        inputValue={fI}
                        onInputChange={this.firstInputChange}
                    />

                    {/*傳統設置默認選項是在option標籤設置selected=selected, 如今放在select標籤裏,固然還有個Select的三方庫*/}
                    {/*經過map將option標籤循環添加到第一個select標籤裏面*/}
                    <select className="select" name="first-select"
                            value={this.state.flag ? this.state.firstSelectedCountry : this.state.secondSelectedCountry}
                            onChange={this.handleChange}>
                        {
                            Object.keys(this.props.ratesObj).map((key) => (
                                <option key={key.toString()}
                                        value={key}>{key} - {this.props.countriesObj[key]}</option>))
                        }
                    </select>
                </div>
                <div className="item">
                    // 當在第一個input輸入數字時,換算出來的值會實時顯示在第二個input裏
                    <MoneyInput
                        inputName='s'
                        inputValue={sI}
                        onInputChange={this.secondInputChange}
                    />

                    {/*經過map將option標籤循環添加到第一個select標籤裏面*/}
                    <select className="select" name="second-select"
                            value={!this.state.flag ? this.state.firstSelectedCountry : this.state.secondSelectedCountry}
                            onChange={this.handleChange}>
                        {
                            Object.keys(this.props.ratesObj).map((key) => (
                                <option key={key.toString()}
                                        value={key}>{key} - {this.props.countriesObj[key]}</option>))
                        }
                    </select>

                    {/*exchange按鈕 將當前flag值傳遞給ChangeCountry類,同時將ChangeCountry類改變的flag值做爲參數,經過buttonToggle方法傳回當前這個類*/}
                    <ChangeCountry currentFlag={this.state.flag} buttonToggle={flag => this.buttonToggle(flag)}/>
                </div>

            </div>
        )
    }
}

 寫在最後

項目寫的很醜,隨着學習的深刻還要進行迭代,加油吧。項目傳到了github上了,沒有勞駕node服務器,fork下來直接打開html文件就能使用了。

至於爲何當時想到了作一個匯率計算器,可能就是下圖吧:

哦

以上、よろしく。

相關文章
相關標籤/搜索