從一個例子中體會React的基本面

weixin_34321977發表於2019-01-14

【起初的準備工作】


npm init
npm install --save react react-dom
npm install --save-dev html-webpack-plugin webpack webpack-dev-server babel-core babel-loader babel-preset-react
  • react react-dom是有關react
  • html-webpack-plugin:用來把原始檔,比如把src/index.html複製到dest/中的index.html中,並引用經webpack捆綁後的js檔案
  • webpack:不多說
  • webpack-dev-server:搭建一個本地伺服器
  • babel-core, babel-loader用來把jsx轉換成js檔案
  • babel-preset-react:在babel中設定react


【檔案結構】


app/
.....index.html
.....index.js
.babelrc
package.json
webpack.config.js
  • .babelrc:在其中設定react
  • webpack.config.js:webpack的配置檔案


【使用webpack需求與實現】

  • 用webpack把js檔案捆綁到根目錄下的dist資料夾下的index_bundle.js
  • 複製app/index.html檔案,在根目錄下的dist資料夾下生成一個index.html檔案,並引用index_bundle.js
  • 執行npm run 某某名稱,來執行webpack -p命令


也就是:

app/
.....index.html
.....index.js
dist/
.....index.html
.....index_bundle.js
.babelrc
package.json
webpack.config.js


webpack.config.js, 這個webpack的配置檔案中大致包括:接受原始檔、放到目標資料夾、使用babel把jsx檔案轉換成js檔案、對原始檔進行復制,等等。


var HtmlWebpackPlugin = require('html-webpack-plugin');
var HtmlWebpackPluginConfig = new HtmlWebpackPlugin({
    template: __dirname + '/app/index.html',
    filename: 'index.html',
    inject: 'body'
})

module.exports = {
    entry: [
        './app/index.js'
    ],
    output: {
        path: __dirname + '/dist',
        filename: "index_bundle.js"
    },
    module: {
        loaders: [
            {test: /\.js$/, exclude: /node_modules/, loader: "babel-loader"}
        ]
    },
    plugins: [
        HtmlWebpackPluginConfig
    ]
}
  • HtmlWebpackPlugin:用來檔案複製,template表示原始檔的具體地址, filename表示複製到目標資料夾後的檔名稱, inject:'body'表示把經webpack生成的index_bundle.js檔案被引用到body中
  • module中的loaders使用babel進行jsx到js檔案的轉換, test使用正規表示式對需要被轉換的檔案進行限定,exclude是babel在進行轉換時需要排除的原始檔夾
  • entry:babel在這裡找入口原始檔
  • output:經babel轉換後的檔案儲存位置
  • plugins:使用HtmlWebpackPlugin,用來檔案複製


.babelrc檔案中,babel需要對react的jsx進行轉換,配置如下:


{
    "presets": [
        "react"
    ]
}


最後一點,如何執行npm run 某某名稱,來執行webpack -p命令呢?


需要在package.json中配置


{
  "name": "reactjspracticeswithtyler",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "production": "webpack -p"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^15.1.0",
    "react-dom": "^15.1.0"
  },
  "devDependencies": {
    "babel-core": "^6.9.0",
    "babel-loader": "^6.2.4",
    "babel-preset-react": "^6.5.0",
    "bootstrap": "^3.3.6",
    "html-webpack-plugin": "^2.17.0",
    "webpack": "^1.13.1",
    "webpack-dev-server": "^1.14.1"
  }
}

以上,在scripts下的配置,意思是說執行npm run 某某名稱,實際是執行webpack -p命令。


【第一個React元件】


app/index.html


<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Untitled Document</title>
    <link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
</head>
<body>

    <div id="app"></div>

</body>
</html>

以上,我們即將要把React元件插入到id為app的div中。


app/index.js,在這裡建立元件


var React = require('react');
var ReactDOM = require('react-dom');

//1、生產Component
//這裡是jsx的語法
var HelloWorld = React.createClass({
    render: function(){
        //console.log(this.props);
        return (
            <div>Hello {this.props.name}</div>
        )
    }
});

//2、渲染出來
ReactDOM.render(
    <HelloWorld name="darren" anySortData={29}/>,
    document.getElementById('app')
);
  • React.createClass建立元件, render必不可少否則報錯,return返回的語法就是jsx語法,將來需要用Babel轉換成js檔案
  • 元件的名稱,像這裡的HelloWord,第一個字母一般大寫
  • ReactDOM.Render用來把元件渲染到DOM上
  • 元件的表現形式就像html元素,比如這裡的<HelloWorld name="darren" anySortData={29}/>,這裡,對name和anySortData的賦值,實際上會賦值到元件的this.props.name和this.props.anySortData中


執行npm run production,因為我們在package.json中的scripts下已經有了設定,相當於執行webpack -p命令,接下來會根據webpack.config.js中的設定,找到app/index.js,使用babel把index.js中的jsx部分轉換成js儲存到dist/index_bundle.js中;這時候,html-webpack-plugin開始工作了,把app/index.html複製儲存到dist/index.html中,並把index_bundle.js中注入到dist/index.html的body中。


還有一個問題需要解決,當在瀏覽器中輸入localhost:8080的時候,能瀏覽到網頁。怎麼做呢?


需要在package.json中進行設定,在scripts中進行如下設定


{
  "name": "reactjspracticeswithtyler",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "production": "webpack -p",
    "start":"webpack-dev-server"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^15.1.0",
    "react-dom": "^15.1.0"
  },
  "devDependencies": {
    "babel-core": "^6.9.0",
    "babel-loader": "^6.2.4",
    "babel-preset-react": "^6.5.0",
    "bootstrap": "^3.3.6",
    "html-webpack-plugin": "^2.17.0",
    "webpack": "^1.13.1",
    "webpack-dev-server": "^1.14.1"
  }
}

以上,在scripts下新增了"start":"webpack-dev-server"部分。


執行npm run start命令,在瀏覽器中輸入localhost:8080就可以看到內容。


【元件巢狀】


var USER_DATA = {
    name: 'darren',
    username: 'DarrenActive',
    image: 'https://avatars0.githubusercontent.com/u/2933430?v=38s=460'
};


var React = require('react');
var ReactDOM = require('react-dom');

//被巢狀元件
var ProfilePic = React.createClass({
    render: function(){
        return <img src={this.props.imageUrl} style={{height: 100, width: 100}} />
    }
})

//被巢狀元件
var ProfileLink = React.createClass({
    render: function(){
        return (
            <div>
                <a href={'https://www.github.com/' + this.props.username}>
                    {this.props.username}
                </a>
            </div>
        )
    }
});

//被巢狀元件
var ProfileName = React.createClass({
    render: function(){
        return (
            <div>{this.props.name}</div>
        )
    }
})

//巢狀元件
var Avatar = React.createClass({
    render: function(){
        return (
            <div>
                <ProfilePic imageUrl={this.props.user.image} />
                <ProfileName name={this.props.user.name} />
                <ProfileLink username={this.props.user.username} />
            </div>
        )
    }
})

ReactDOM.render(
    <Avatar user={USER_DATA} />,
    document.getElementById('app')
);

巢狀元件和被巢狀元件的關係就像河流的上下游,這裡的巢狀元件Avatar在河流的上游,這裡的被巢狀元件ProfileName在河流的下游,Avatar元件通過user來獲取外界的賦值,user接受到值後往下游的ProfileName流動,user中的值再賦值給ProfileName的name,依次類推。


所以,React的資料流動是單向的,由外向內的流動。


【元件元素巢狀】


元件巢狀和元件元素巢狀不一樣。元件巢狀大致是:

var Room = React.createClass({
    render: function(){
        return (
            <Table />
            <Chair />
            <Clock />
        )
    }
});


元件元素巢狀大致是:

var Room = React.createClass({
    render: function(){
        return (
            <Table />
            <Chair />
            <Clock>
                <Time />
                <Period />
            </Clock>
        )
    }
});

也就是說,元件元素巢狀中的被巢狀元件,這裡的<Time /><Period />不是被放在一個外層的元件中,而是放在了一個元件元素<Clock></Clock>。Clock元件的顯示依賴於<Time /><Period />。那麼,在定義Clock元件的時候,如何把<Time /><Period />顯示出來呢?


像這種需要顯示元件元素內的被巢狀元件,就需要this.props.children的幫忙。


像在本專案的Avatar元件的寫法還是不變:

var Avatar = React.createClass({
    render: function(){
        return (
            <div>
                <ProfilePic imageUrl={this.props.user.image} />
                <ProfileName name={this.props.user.name} />
                <ProfileLink username={this.props.user.username} />
            </div>
        )
    }
})


ProfileLink元件,現在想把它作為另外一個元件元素內的被巢狀元件,這樣定義ProfileLink元件:

var ProfileLink = React.createClass({
    render: function(){
        return (
            <div>
                <Link href={'https://www.github.com/' + this.props.username}>
                    {this.props.username}
                </Link>
            </div>
        )
    }
});

以上,{this.props.username}拿到的值是通過ProfileLink元件元件獲取到的,如何傳遞給Link元件呢?


var Link = React.createClass({
    
    changeURL: function(){
        window.location.replace(this.props.href);
    },
    
    render: function(){
        return (
            <span 
                style={{color: 'blue', cursor: 'pointer'}}
                onClick={this.changeURL}>
                {this.props.children}
            </span>
        )
    }
});

可見,在Link元件中,通過this.props.children獲取到ProfileLink元件這個元件元素內的元件。值得注意的是:this.props.children獲取到的是包含在元件元素內的所有被巢狀元件。


【路由】


React的路由連線了component和url。


npm install --save react-router@2.0.0-rc5


在app資料夾下建立config資料夾,並建立routes.js檔案;在app資料夾下建立Home.js和Main.js,現在檔案結構變為:


app/
.....config/
..........routes.js
.....components/
..........Home.js
..........Main.js
.....index.html
.....index.js
.babelrc
package.json
webpack.config.js


在Home.js中建立一個名稱為Home的元件


var React = require('react');

var Home = React.createClass({
    render: function(){
        return (
            <div>Hello From Home!</div>
        )
    }
});

module.exports = Home;


在Main.js中建立一個名稱為Main的元件


var React = require('react');

var Main = React.createClass({
    render: function(){
        return (
            <div>
                Hello From Main!
                {this.props.children}
            </div>
        )
    }
});

module.exports = Main;


配置config/routes.js中的路由,其實就是配對url和元件的對映關係。


var React = require('react');
var ReactRouter = require('react-router');
var Router = ReactRouter.Router;
var Route = ReactRouter.Route;
var IndexRouter = ReactRouter.IndexRoute;

var Main = require('../components/Main');
var Home = require('../components/Home');

var routes = (
    <Router>
        <Route path='/' component={Main}>
            <Route path='/home' component={Home} />
        </Route>
    </Router>
);

module.exports = routes;

以上,路由從本質上說也是元件。<Route path='/' component={Main}>...</Route>依賴於<Route path='/home' component={Home} />,當url為/的時候,只會顯示Main元件內容,當url為/home的時候會同時顯示Main元件和Home元件的內容。


在app/index.js中,把路由這個特殊的元件載入起來。


var React = require('react');
var ReactDOM = require('react-dom');
var routes = require('./config/routes');

ReactDOM.render(
    routes,
    document.getElementById('app')
);


npm run start


在瀏覽器中輸入:localhost:8080


url變成:http://localhost:8080/#/?_k=dhjswq
內容為:Hello From Main!


因為,當url為/的時候,路由設定顯示Main元件,雖然在Main元件定義的時候有{this.props.children},但因為Main元件元素巢狀的Home元件沒有顯示,所有隻顯示Main元件的內容。


在瀏覽器中輸入:http://localhost:8080/#/home?_k=dhjswq


內容:
Hello From Main!
Hello From Home!


也很好理解,因為Home元件是被巢狀在Main這個元件元素中的,當url為Home路由的時候,不僅把Home元件顯示了出來,還把Main元件顯示了出來。


如果想始終都顯示Home這個元件呢?


需要在app/config/routes.js按如下配置


var React = require('react');
var ReactRouter = require('react-router');
var Router = ReactRouter.Router;
var Route = ReactRouter.Route;
var hashHistory = ReactRouter.hashHistory;
var IndexRouter = ReactRouter.IndexRoute;

var Main = require('../components/Main');
var Home = require('../components/Home');

var routes = (
    <Router history={hashHistory}>
        <Route path='/' component={Main}>
            <IndexRouter  component={Home} />
        </Route>
    </Router>
);

module.exports = routes;

以上,IndexRouter表示始終都顯示的路由。


【無狀態函式式宣告元件】


現在,已經習慣了按這樣的方式宣告元件:

var HelloWorld = React.createClass({
    render: function(){
        return (
            <div>Hello {this.props.name}</div>
        )
    }
});

ReactDOM.render(<HelloWorld name='darren' />, document.getElementById('app'));


除了上面的宣告方式,還有一種"無狀態函式式宣告方式"。當React.createClass中只有render方法的時候就可以按如下方式來替代。

function HelloWorld(props){
    return (
        <div>Hello {props.name}</div>
    )
}

ReactDOM.render(<HelloWorld name='darren' />, document.getElementById('app'));



【元件中的變數型別約定】


定義元件的時候經常用到變數,這些變數的型別可以約定嗎?
--答案是可以的,使用propTypes


var React = require('react');
var PropTypes = React.PropTypes;
var Icon = React.createClass({
    propTypes: {
        name: PropTypes.string.isRequired,
        size: PropTypes.number.isRequired,
        color: PropTypes.string.isRequired,
        style: PropTypes.object
    },
    render: ...
});


【生命週期事件】


所有的元件在生命週期內有一些共同的事件。有些事件在元件與DOM的繫結或解除繫結的時候發生,有些事件在元件接受資料的時候發生。


給元件設定一些預設屬性:


var Loading = React.createClass({
    getDefaultProps: function(){
        return {
            text: 'Loading'
        }
    },
    render: function(){
        ...
    }
});

以上,通過getDefaultProps為元件設定了一些預設屬性和其對應的值。


設定元件的初始狀態:


var Login = React.createClass({
    getInitialState: function(){
        return {
            email: '',
            password:''
        }
    },
    render: function(){
        ...
    }
});

以上,通過getInitialState設定元件的初始狀態。


元件繫結到DOM時觸發的事件


比如,當元件繫結到DOM時從遠端獲取一些資料:

var FriendsList = React.createClass({
    componentDidMount: functioin(){
        return Axios.get(this.props.url).then(this.props.callback);
    },
    render: function(){
        ...
    }
});

以上,通過componentDidMount在元件繫結到DOM上後發生事件。


比如,當元件繫結到DOM時設定監聽:

var FriendsList = React.createClass({
    componenetDidMount: function(){
        ref.on('value', function(snapshot){
            this.setState({
                friends: snapshot.val()
            });
        })
    },
    render:...
});


元件與DOM解除繫結時觸發的事件


var FriendList = React.createClass({
    componentWillUnmount: function(){
        ref.off();
    },
    render:...
});

以上,通過componentWillUnmount來觸發當元件與DOM解除繫結時的事件。


當元件接受到新的屬性值時觸發的事件:componentWillReceiveProps


決定元件是否需要渲染的事件:shouldComponentUpdate


所有的事件大致如下圖:


417212-20160530135449414-2064794439.png


【this關鍵字】


隱式繫結


var me = {
    name: 'Darren',
    age:25,
    sayName: function(){
        console.log(this.name);
    }
};

me.sayName();

以上,當呼叫sayName的時候,隱式用到了this,這裡的this指的是點左邊的me.



來看一個在巢狀函式中使用this關鍵字的例子。

var sayNameMixin = function(obj){
    obj.sayName = function(){
        console.log(this.name);
    };
}

var me = {
    name: 'Darren',
    age: 25
};

var you = {
    name: 'Joey',
    age: 21
};

sayNameMixin(me);
sayNameMixin(you);

me.sayName();//Darren
you.sayName();//Joey

以上,sayNameMixin接受me這個引數,然後在其內部給me定義了一個sayName方法,這裡的this也是值me。


再來看用建構函式建立物件,使用this關鍵字的例子。

var Person = function(name, age){
    return {
        name: name,
        age: age,
        sayName: function(){
            console.log(this.name);
        }
    }
};

var jim = Person('Jim', 42);
jim.sayName();

以上,呼叫sayName方法的時候隱式用到了this,這裡的this還是指的是點左邊的jim。


所以,這裡關於隱式繫結的的總結是:當隱式呼叫this的時候,this通常指的是點左側的那個變數。這裡的this在定義的時候就很明確。


顯式繫結



1、使用call方法顯式指定this關鍵字。

//在定義的時候this指的誰並不明確
var sayName = function(){
    console.log('My name is ' + this.name);
};

var stacy = {
    name: 'Stacy',
    age: 34
};

sayName.call(stacy);

以上,在定義sayName方法的時候並沒有明確定義this指的誰。使用call方法的時候把stacy傳值給了this關鍵字。


call方法不僅可以指向this,還可以傳遞引數。

var sayName = function(lang1, lang2, lang3){
    console.log('My name is ' + this.name + ' and I know ' + lang1 + ', ' + lang2 + ', and ' + lang3);
};

var stacy = {
    name: 'Stacey',
    age:28
};

var languages = ['JavaScript', 'Ruby', 'Pythos'];

sayName.call(stacey, languages[0], languages[1], languages[2]);

以上,通過呼叫call方法不僅指定了this關鍵字,還傳遞了sayName方法所需的引數。


2、使用apply方法顯式繫結this關鍵字

var sayName = function(lang1, lang2, lang3){
    console.log('My name is ' + this.name + ' and I know ' + lang1 + ', ' + lang2 + ', and ' + lang3);
};

var stacy = {
    name: 'Stacey',
    age:28
};

var languages = ['JavaScript', 'Ruby', 'Pythos'];

sayName.apply(stacey, languages);

以上,apply和call的區別可見一斑,apply接受的實引數組。


3、使用bind方法顯式繫結this關鍵字

var sayName = function(lang1, lang2, lang3){
    console.log('My name is ' + this.name + ' and I know ' + lang1 + ', ' + lang2 + ', and ' + lang3);
};

var stacy = {
    name: 'Stacey',
    age:28
};

var languages = ['JavaScript', 'Ruby', 'Pythos'];

var newFn = sayName.bind(stacey, languages[0], languages[1], languages[2]);

newFn();

以上,使用bind方法可以產生一個新的函式,再呼叫該函式。


New Binding


var Animal = function(color, name, type){
    //this = {}
    this.color = color;
    this.name = name;
    this.type = type;
}

var zebra = new Animal('black and white', 'Zorro', 'Zebra');

以上,當定義Animal這個函式的時候,此時的this指的是一個空物件,當例項化Animal的時候this就有值了。


Window Binding


var sayAge = function(){
    console.log(this.age);
}

var me = {
    age: 25
};

sayAge(); //undefined
window.age = 35;
sayAge(); //35

可見,當沒有給this顯式繫結的時候,this指的是window。


接下來,會從一個例子來體會React的方方面面......

相關文章