over 3 years ago

將值從一或多個來源物件複製到目標物件
範例

var first = { name: "Bob" };
var last = { lastName: "Smith" };

var person = Object.assign(first, last);
console.log(person);

// Output:
// { name: "Bob", lastName: "Smith" } 

var obj = { person: "Bob Smith"};
var clone = Object.assign({}, obj);

來源網址
https://msdn.microsoft.com/zh-tw/library/dn858229v=vs.94).aspx(

 
over 3 years ago
function isBigEnough(element) {
  return element >= 10;
}
var filtered = [12, 5, 8, 130, 44].filter(isBigEnough);
// filtered is [12, 130, 44]
 
over 3 years ago
let state = {items: [
    { name: 'apple', value: 1 },
    { name: 'orange', value: 2 },
    { name: 'tomato', value: 3 },
]}
// 第一個參數 callback => function(previousValue, currentValue, currentIndex, array) { return... }
// 第二個參數 起始值 
let sum = items.reduce((acc, item) => acc + item.value, 0);
console.log("sum = " + sum); // sum = 6
// 0 為起始值 一開始 acc = 0

sum = items.reduce((acc, item) => acc + item.value, 10);
console.log("sum = " + sum); // sum = 16
// 10 為起始值 一開始 acc = 10
 
over 3 years ago

假設 Server 端的 port 為 8080
需設定 webpack-dev-server 的 port 為 8081

配置 webpack.config.js

module.exports = {
  ...
  output: {
    path: path.resolve(__dirname, "build"),
    publicPath: "http://localhost:8081/",
    filename: "bundle.js"
  },
  ...
};

於主頁面 index.html 加入 http://localhost:8081/bundle.js

<body>
  <div id="react"></div>
<script src="http://localhost:8081/bundle.js"></script>
</body>

執行

webpack-dev-server --progress --colors --history-api-fallback --host localhost --port 8081 --inline

開啟 Server 端的 URL

http://127.0.0.1:8080/(App路徑)/index.html
 
over 3 years ago

練習編寫 React 分頁 component

建立 Component\Pagination.js

import React, {Component, PropTypes} from 'react';

class Pagination extends Component {
  constructor(props) {
    super(props);
  }
  _bindFirstPage() {
    if (this.props.currentPage != 1 && this.props.isSetFirst == true) {
      return <a id={'page_1'}
                    style={this.props.linkStyle}
                    href="#"
                    onClick={this.props.onClick.bind(this, 1)}>{this.props.firstDesc}</a>;
    }
  }
  _bindLastPage() {
    if (this.props.currentPage != this.props.sumPages && this.props.isSetLast == true) {
      return <a id={'page_' + this.props.sumPages}
                    style={this.props.linkStyle}
                    href="#"
                    onClick={this.props.onClick.bind(this, this.props.sumPages)}>{this.props.lastDesc}</a>;
    }
  }
  _bindPrePage() {
    if (this.props.currentPage != 1 && this.props.isSetPre == true) {
        var pageNum = parseInt(this.props.currentPage) - 1;
        return <a id={'page_' + pageNum}
                      style={this.props.linkStyle}
                      href="#"
                      onClick={this.props.onClick.bind(this, pageNum)}>{this.props.preDesc}</a>;
    }
  }
  _bindNextPage() {
      if (this.props.currentPage != this.props.sumPages && this.props.isSetNext == true) {
          let pageNum = parseInt(this.props.currentPage) + 1;
          return <a id={'page_' + pageNum}
                        style={this.props.linkStyle}
                        href="#"
                        onClick={this.props.onClick.bind(this, pageNum)}>{this.props.nextDesc}</a>;
      }
  }
  _bindPreDesc(startPage) {
      if (startPage > 1 && this.props.isSetPreAndNextDesc == true) {
         return <span style={this.props.spanStyle}>{this.props.preAndNextDesc}</span>;
      }
  }
  _bindNextDesc(endPage) {
      if (endPage < this.props.sumPages && this.props.isSetPreAndNextDesc == true) {
          return <span style={this.props.spanStyle}>{this.props.preAndNextDesc}</span>;
      }
  }
  _bindPages(startPage, endPage) {
      let link = [];
      for (var pagenum = startPage; pagenum <= endPage; pagenum++) {
          if (pagenum == this.props.currentPage) {
             link.push(<span style={this.props.spanStyle}>{pagenum}</span>);
          } else {
            link.push(<a id={'page_' + pagenum}
                                style={this.props.linkStyle}
                                href="#"
                                onClick={this.props.onClick.bind(this, pagenum)}>{pagenum}</a>);
          }
      }
      return link;
  }
  render() {
    let startPage = 1;
    let endPage = 1;

    startPage = this.props.currentPage - parseInt(this.props.pageSize / 2);
    if (this.props.currentPage == this.props.sumPages) {
        startPage = this.props.sumPages - this.props.pageSize + 1;
    }
    if (this.props.currentPage == 1) {
        startPage = 1;
    }
    if (startPage < 1) {
        startPage = 1;
    }
    endPage = startPage + this.props.pageSize - 1;

    if (endPage > this.props.sumPages) {
        startPage -=  endPage - this.props.sumPages;
        endPage = this.props.sumPages;
    }

    if (startPage < 1) {
        startPage = 1;
    }
    let firstPage = this._bindFirstPage();
    let prePage = this._bindPrePage();
    let preDesc = this._bindPreDesc(startPage);
    let bindPages = this._bindPages(startPage, endPage);
    let nextDesc = this._bindNextDesc(endPage);
    let nextPage = this._bindNextPage();
    let lastPage = this._bindLastPage();
    return (
      <div>
        {firstPage}
        {prePage}
        {preDesc}
        {bindPages}
        {nextDesc}
        {nextPage}
        {lastPage}
        </div>
    );
  }
}

Pagination.defaultProps = {
  sumPages: 30,
  currentPage: 5,
  pageSize:5,
  firstDesc: "第一頁",
  lastDesc: "最末頁",
  preDesc: "上一頁",
  nextDesc: "下一頁",
  preAndNextDesc: "...",
  isSetFirst: true,
  isSetLast: true,
  isSetPre: true,
  isSetNext: true,
  isSetPreAndNextDesc: true,
  linkStyle: {'marginRight':5},
  spanStyle: {'marginRight':5}
}

Pagination.propTypes = {
  sumPages: PropTypes.number.isRequired,
  currentPage: PropTypes.number.isRequired,
  pageSize:PropTypes.number.isRequired,
  firstDesc: PropTypes.string.isRequired,
  lastDesc: PropTypes.string.isRequired,
  preDesc: PropTypes.string.isRequired,
  nextDesc: PropTypes.string.isRequired,
  preAndNextDesc: PropTypes.string.isRequired,
  isSetFirst: PropTypes.bool.isRequired,
  isSetLast: PropTypes.bool.isRequired,
  isSetPre:PropTypes.bool.isRequired,
  isSetNext: PropTypes.bool.isRequired,
  isSetPreAndNextDesc: PropTypes.bool.isRequired,
  linkStyle: PropTypes.object.isRequired,
  spanStyle: PropTypes.object.isRequired
}

export default Pagination;

使用方式
於主 main 建立 _onClickLink 函數來控制 Pagination 的分頁狀態

import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';
import Pagination from './Component/Pagination';

class Main extends Component {
  constructor(props) {
    super(props);
    this.state = {currentPage: 5, sumPages: 60};
    this._onClickLink =  this._onClickLink.bind(this);
  }
  _onClickLink(num, e) {
    // 接收 Pagination 所觸發的頁碼
    this.setState({currentPage: num, sumPages: 60});
  }
  render() {
    return(
      <div>
      <div><Pagination ref="Pagination" onClick={this._onClickLink} sumPages={this.state.sumPages}  currentPage={this.state.currentPage} /></div>
      <div><Pagination ref="Pagination2" onClick={this._onClickLink} pageSize={20} sumPages={this.state.sumPages}  currentPage={this.state.currentPage} /></div>
      </div>
    );
  }
}

ReactDOM.render(
    <Main />,
  document.getElementById('demo')
);

執行效果

 
over 3 years ago

於 webpack.config.js 加入 devtool: 'source-map'

module.exports = {
  .
  ..
  ...
  devtool: 'source-map'
};

執行

webpack-dev-server  --progress --colors --hot

開啟 http://localhost:8080/webpack-dev-server/
於 chrome 開發人員工具 Sources 介面按 F1 進入 Settings 介面
並開啟 javascript source maps 模式

重新整理後
再於 Sources 介面即可查看開發時的 js 檔案來 debug

 
over 3 years ago

由於 ES6 的 React.Component 方式不支援 Mixins 語法
為啥不採用 Mixins 請查看 Mixins Are Dead. Long Live Composition
並建議採用 Higher order component 組合的方式來實現 Mixins 功能

何謂 Higher order component (HOC)

其實就是一個函數
接收某個現有的 Component
並 return 一個 class extends React.Component
且在內部 render() return 接收的 Component

範例如下

const HOC = (Component) => class extends React.Component {
  constructor(props) {
    super(props);
  }
  // do whatever you may want
  render() {
    // pass new properties to wrapped component
    return <Component {...this.props} {...this.state} />
  }
};

因此可以將一些固定的邏輯放至 Higher order component 中
來變動 props 及 state 傳遞給接收的 Component

實作範例

建立 Hoc\HOC.js

import React from 'react';

const HOC = (Component, state, intervalFn) => class extends React.Component {
  constructor(props) {
    super(props);
    this.state = state;
  }

  componentWillMount() {
    this.intervals = [];
  }

  componentWillUnmount() {
    // Unmount 時清除所有 clearInterval
    this.intervals.forEach(clearInterval);
  }

  componentDidMount() {
   // DidMount時執行 setInterval 觸發 tick()
    this.setInterval(this.tick.bind(this), 1000);
  }

  setInterval() {
    // 堆疊 setInterval
    this.intervals.push(setInterval.apply(null, arguments));
  }

  tick() {
    // 觸發變動 state 函數 intervalFn
    this.setState(intervalFn(this.state));
  }

  render() {
    // 配置 props 及 state
    return <Component {...this.props} {...this.state} />
  }
};

export default HOC

建立 Component\TickTock.js

import React from 'react';

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

  render() {
    return (
        <p>
          React has been running for {this.props.seconds} seconds.
        </p>
    );
  }
}

TickTock.propTypes = {
  seconds: React.PropTypes.number.isRequired,
}

export default TickTock

建立 main.js

import React from 'react';
import ReactDOM from 'react-dom';
import HOC from './Hoc/HOC';
import TickTock from './Component/TickTock';

const intervalFn = (state) => {
  return {seconds: state.seconds + 1};
};

const Wrapped = HOC(TickTock, { seconds: 0 }, intervalFn);
const Wrapped2 = HOC(TickTock, { seconds: 3 }, intervalFn);
const Wrapped3 = HOC(TickTock, { seconds: 6 }, intervalFn);

ReactDOM.render(
  <div>
    <Wrapped />
    <Wrapped2 />
    <Wrapped3 />
  </div>,
  document.getElementById('demo')
);

利用 HOC 傳入 TickTock 及 預設的 state 狀態和做加總的 intervalFn 函數
模擬 Mixins 實現擴充 TickTock 內的功能
由於每次呼叫都會執行 return class extends React.Component
所以能避免 Component 重複使用 Mixins 的陷阱

執行效果

componentWillMount 與 componentDidMount 的先後順序

來源網址
http://www.darul.io/post/2016-01-05_react-higher-order-components

 
over 3 years ago

React.createElement 使用成本很高
babel-plugin-transform-react-inline-elements
可以將 React.createElement 轉換為 object literals
類似 {type: 'div', props: ...}

於專案資料夾執行

npm install --save-dev babel-plugin-transform-react-inline-elements

建立 .babelrc
內容如下

{
  "plugins": ["transform-react-inline-elements"]
}

轉換的 Component render() 內容如下

return(
  <div>
    <PopUp ref="PopUp">
      <div style={{"width":"150px","height":"180px","background-color":"white"}}>
      123
      465
      789
      </div>
    </PopUp>
    <input type="button" value="showBox" onClick={this.showBoxOne.bind(this)} />
    <PopUp ref="PopUp2">
    <ImgMask src="images/test.png" rectRadius={25} imgHeight={400} rectHeight={350}  rectWidth={250} />
    </PopUp>
    <input type="button" value="showBox2" onClick={this.showBoxTwo.bind(this)} />
  </div>
)

先前的轉換js


配置 plugin 轉換後js

如上圖可以看到此 plugin 可以將大部分的 React.createElement
轉換為更有效率的 object literals 宣告

 
over 3 years ago

React Redux 使用 redux-thunk 做 Async Actions

準備 package.json
使用 react-redux 及 redux-thunk

{
  "name": "reduxthunk",
  "version": "0.0.1",
  "description": "a todo app that you'd never want to use",
  "main": "index.js",
  "scripts": {
    "test": " ",
    "start": "webpack-dev-server --devtool eval --progress --colors --hot --inline"
  },
  "keywords": [
    "test"
  ],
  "author": "Jason Wang",
  "license": "ISC",
  "dependencies": {
    "react": "^0.14.2",
    "react-dom": "^0.14.2",
    "react-redux": "^4.0.0",
    "redux-thunk": "^2.0.1",
    "redux": "^3.0.4",
    "webpack": "^1.12.6",
    "webpack-dev-server": "^1.12.1"
  },
  "devDependencies": {
    "babel-core": "^6.1.21",
    "babel-loader": "^6.2.0",
    "babel-preset-es2015": "^6.1.18",
    "babel-preset-react": "^6.1.18"
  }
}

配置 webpack.config.js

var path = require("path");
module.exports = {
  entry: './components/app.js',
  output: {
    path: path.resolve(__dirname, "build"),
    publicPath: "/",
    filename: "bundle.js"
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules)/,
        loader: 'babel',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  },
  resolve: {
    extensions: ['', '.js', '.jsx']
  }
};

安裝

npm install

建立 action \actions\index.js

export function addReposAction(jsonResult) {
  return {
    type: "ADD_TWEETS",
    repos: jsonResult
  }
}
export function userChangedAction(value) {
  return {
    type: "USER_CHANGED",
    value: value
  }
}
export function loadingChangedAction(isLoading) {
  return {
    type: "IS_LOADING",
    isLoading: isLoading
  }
}
export function loadReposAction() {
  return function(dispatch, getState) {
    var state = getState();
    var url = "https://api.github.com/users/" + state.user + "/repos";
    dispatch(loadingChangedAction(true));

    return fetch(url)
      .then(function(result) {
        dispatch(loadingChangedAction(false));

        if (result.status === 200) {
          return result.json();
        }

        throw "request failed";
      })
      .then(function(jsonResult) {
        dispatch(addReposAction(jsonResult));
      })
      .catch(function(err) {
        alert("Oops...Couldn't fetch repos for user: " + state.user, "error");
      });
  }
}

thunk function -> loadReposAction()
使用 fetch 如果瀏覽器不支援可改用 isomorphic-fetch
呼叫 Api "https://api.github.com/users/" + state.user + "/repos" 取得 user 的 github repos
並於於內部執行 dispatch(loadingChangedAction(...)) 及 dispatch(addReposAction(jsonResult)) 來更新 component

建立 \reducers\rootReducer.js

function initialState() {
  return {
    user: "",
    repos: [],
    isLoading: false
  }
}
export default (state, action)  => {
  var previousState = (state ? state : initialState());
  switch (action.type) {
    case "ADD_TWEETS":
      return addTweets(previousState, action);
      break;
    case "USER_CHANGED":
      return userChanged(previousState, action);
      break;
    case "IS_LOADING":
      return isLoadingChanged(previousState, action);
    default:
      return previousState;
  }
}

function addTweets(state, action) {
  return {
    isLoading: state.isLoading,
    user: state.user,
    repos: action.repos
  }
}

function userChanged(state, action) {
  return {
    user: action.value,
    repos: [],
    isLoading: state.isLoading
  };
}

function isLoadingChanged(state, action) {
  return {
    user: state.user,
    repos: state.repos,
    isLoading: action.isLoading
  };
}

建立 store.js

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/rootReducer';

let store =  applyMiddleware(thunk)(createStore)(rootReducer);
export default store;

使用 applyMiddleware 綁定 reducers

建立 \components\rootComponent.js

import React , { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import * as Actions from '../actions';

class rootComponent extends Component {
  constructor(props) {
    super(props);
    this.loadRepos = this.loadRepos.bind(this);
  }
  loadRepos() {
     const {state, actions} = this.props;
     let user = this.refs.user.value;
     actions.userChangedAction(user);
     actions.loadReposAction();
  }
  render() {
    const {state, actions} = this.props;
    return (
      <div>
        <input ref="user" id="user" type="text"  />
        <input type="button" value="load" onClick={this.loadRepos} />
        {(state.isLoading) ? <div>load....</div> : state.repos.map((repo, index) => <div key={index}>{repo.name}</div>)}
      </div>
    )
  }
}
function mapStateToProps(state) {
  return {
    state
  }
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(Actions, dispatch)
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(rootComponent);

於 rootComponent->loadRepos() 中依序執行
actions.userChangedAction(user);
actions.loadReposAction();

建立 \components\app.js

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import store from '../store'
import RootComponent from './rootComponent'

let reactElement = document.getElementById('react')
render(
  <Provider store={store}>
    <RootComponent />
  </Provider>,
  reactElement
)

執行

npm start

結果




參考網址
http://nojaf.com/2015/12/06/redux-thunk/
https://davidwalsh.name/fetch
https://camsong.github.io/redux-in-chinese/docs/advanced/AsyncActions.html

 
over 3 years ago

於 Chrome 安裝 Redux DevTools

設定 store 此範例是搭配 react-router-redux 於 Redux createStore 加入 window.devToolsExtension

export default createStore(
  combineReducers({
    todos,
    routing: routerReducer
  }),
  window.devToolsExtension ? window.devToolsExtension() : undefined
)

效果

額外功能請參考
redux-devtools-extension