輕鬆入門React和Webpack

最近在學習React.js,之前都是直接用最原生的方式去寫React代碼,發現組織起來特別麻煩,之前聽人說用Webpack組織React組件得心應手,就花了點時間學習了一下,收穫頗豐

說說React

一個組件,有自己的結構,有自己的邏輯,有自己的樣式,會依賴一些資源,會依賴某些其他組件。比如日常寫一個組件,比較常規的方式:

  • 通過前端模板引擎定義結構
  • JS文件中寫自己的邏輯
  • CSS中寫組件的樣式
  • 通過RequireJS、SeaJS這樣的庫來解決模塊之間的相互依賴

那麼在React中是什麼樣子呢?

結構和邏輯

在React的世界裏,結構和邏輯交由JSX文件組織,React將模板內嵌到邏輯內部,實現了一個JS代碼和HTML混合的JSX。

結構

在JSX文件中,可以直接通過React.createClass來定義組件:

var CustomComponent = React.creatClass({
    render: function(){
        return (<div className="custom-component"></div>);
    }
});

通過這種方式可以很方便的定義一個組件,組件的結構定義在render函數中,但這並不是簡單的模板引擎,我們可以通過js方便、直觀的操控組件結構,比如我想給組件增加幾個節點:

var CustomComponent = React.creatClass({
    render: function(){
        var $nodes = ['h','e','l','l','o'].map(function(str){
            return (<span>{str}</span>);
        });
        return (<div className="custom-component">{$nodes}</div>);
    }
});

通過這種方式,React使得組件擁有靈活的結構。那麼React又是如何處理邏輯的呢?

邏輯

寫過前端組件的人都知道,組件通常首先需要相應自身DOM事件,做一些處理。必要時候還需要暴露一些外部接口,那麼React組件要怎麼做到這兩點呢?

事件響應

比如我有個按鈕組件,點擊之後需要做一些處理邏輯,那麼React組件大致上長這樣:

var ButtonComponent = React.createClass({
    render: function(){
        return (<button>屠龍寶刀,點擊就送</button>);
    }
});

點擊按鈕應當觸發相應地邏輯,一種比較直觀的方式就是給button綁定一個onclick事件,裏面就是需要執行的邏輯了:

function getDragonKillingSword() {
    //送寶刀
}
var ButtonComponent = React.createClass({
    render: function(){
        return (<button onclick="getDragonKillingSword()">屠龍寶刀,點擊就送</button>);
    }
});

但事實上getDragonKillingSword()的邏輯屬於組件內部行爲,顯然應當包裝在組件內部,於是在React中就可以這麼寫:

var ButtonComponent = React.createClass({
    getDragonKillingSword: function(){
        //送寶刀
    },
    render: function(){
        return (<button onClick={this.getDragonKillingSword}>屠龍寶刀,點擊就送</button>);
    }
});

這樣就實現內部事件的響應了,那如果需要暴露接口怎麼辦呢?

暴露接口

事實上現在getDragonKillingSword已經是一個接口了,如果有一個父組件,想要調用這個接口怎麼辦呢?

父組件大概長這樣:

var ImDaddyComponent = React.createClass({
    render: function(){
        return (
            <div>
                //其他組件
                <ButtonComponent />
                //其他組件
            </div>
        );
    }
});

那麼如果想手動調用組件的方法,首先在ButtonComponent上設置一個ref=""屬性來標記一下,比如這裏把子組件設置成<ButtonComponent ref="getSwordButton"/>,那麼在父組件的邏輯裏,就可以在父組件自己的方法中通過這種方式來調用接口方法:

this.refs.getSwordButton.getDragonKillingSword();

看起來屌屌噠~那麼問題又來了,父組件希望自己能夠按鈕點擊時調用的方法,那該怎麼辦呢?

配置參數

父組件可以直接將需要執行的函數傳遞給子組件:

<ButtonComponent clickCallback={this.getSwordButtonClickCallback}/>

然後在子組件中調用父組件方法:

var ButtonComponent = React.createClass({
    render: function(){
        return (<button onClick={this.props.clickCallback}>屠龍寶刀,點擊就送</button>);
    }
});

子組件通過this.props能夠獲取在父組件創建子組件時傳入的任何參數,因此this.props也常被當做配置參數來使用

屠龍寶刀每個人只能領取一把,按鈕點擊一下就應該灰掉,應當在子組件中增加一個是否點擊過的狀態,這又應當處理呢?

組件狀態

在React中,每個組件都有自己的狀態,可以在自身的方法中通過this.state取到,而初始狀態則通過getInitialState()方法來定義,比如這個屠龍寶刀按鈕組件,它的初始狀態應該是沒有點擊過,所以getInitialState方法裏面應當定義初始狀態clicked: false。而在點擊執行的方法中,應當修改這個狀態值爲click: true

var ButtonComponent = React.createClass({
    getInitialState: function(){
        //確定初始狀態
        return {
            clicked: false
        };
    },
    getDragonKillingSword: function(){
        //送寶刀

        //修改點擊狀態
        this.setState({
            clicked: true
        });
    },
    render: function(){
        return (<button onClick={this.getDragonKillingSword}>屠龍寶刀,點擊就送</button>);
    }
});

這樣點擊狀態的維護就完成了,那麼render函數中也應當根據狀態來維護節點的樣式,比如這裏將按鈕設置爲disabled,那麼render函數就要添加相應的判斷邏輯:

render: function(){
    var clicked = this.state.clicked;
    if(clicked)
        return (<button disabled="disabled" onClick={this.getDragonKillingSword}>屠龍寶刀,點擊就送</button>);
    else 
        return (<button onClick={this.getDragonKillingSword}>屠龍寶刀,點擊就送</button>);
}

小節

這裏簡單介紹了通過JSX來管理組件的結構和邏輯,事實上React給組件還定義了很多方法,以及組件自身的生命週期,這些都使得組件的邏輯處理更加強大

資源加載

CSS文件定義了組件的樣式,現在的模塊加載器通常都能夠加載CSS文件,如果不能一般也提供了相應的插件。事實上CSS、圖片可以看做是一種資源,因爲加載過來後一般不需要做什麼處理。

React對這一方面並沒有做特別的處理,雖然它提供了Inline Style的方式把CSS寫在JSX裏面,但估計沒有多少人會去嘗試,畢竟現在CSS樣式已經不再只是簡單的CSS文件了,通常都會去用Less、Sass等預處理,然後再用像postcss、myth、autoprefixer、cssmin等等後處理。資源加載一般也就簡單粗暴地使用模塊加載器完成了

組件依賴

組件依賴的處理一般分爲兩個部分:組件加載和組件使用

組件加載

React沒有提供相關的組件加載方法,依舊需要通過<script>標籤引入,或者使用模塊加載器加載組件的JSX和資源文件。

組件使用

如果細心,就會發現其實之前已經有使用的例子了,要想在一個組件中使用另外一個組件,比如在ParentComponent中使用ChildComponent,就只需要在ParentComponentrender()方法中寫上<ChildComponent />就行了,必要的時候還可以傳些參數。

疑問

到這裏就會發現一個問題,React除了只處理了結構和邏輯,資源也不管,依賴也不管。是的,React將近兩萬行代碼,連個模塊加載器都沒有提供,更與Angularjs,jQuery等不同的是,他還不帶啥腳手架...沒有Ajax庫,沒有Promise庫,要啥啥沒有...

虛擬DOM

那它爲啥這麼大?因爲它實現了一個虛擬DOM(Virtual DOM)。虛擬DOM是幹什麼的?這就要從瀏覽器本身講起

如我們所知,在瀏覽器渲染網頁的過程中,加載到HTML文檔後,會將文檔解析並構建DOM樹,然後將其與解析CSS生成的CSSOM樹一起結合產生愛的結晶——RenderObject樹,然後將RenderObject樹渲染成頁面(當然中間可能會有一些優化,比如RenderLayer樹)。這些過程都存在與渲染引擎之中,渲染引擎在瀏覽器中是於JavaScript引擎(JavaScriptCore也好V8也好)分離開的,但爲了方便JS操作DOM結構,渲染引擎會暴露一些接口供JavaScript調用。由於這兩塊相互分離,通信是需要付出代價的,因此JavaScript調用DOM提供的接口性能不咋地。各種性能優化的最佳實踐也都在儘可能的減少DOM操作次數。

而虛擬DOM幹了什麼?它直接用JavaScript實現了DOM樹(大致上)。組件的HTML結構並不會直接生成DOM,而是映射生成虛擬的JavaScript DOM結構,React又通過在這個虛擬DOM上實現了一個 diff 算法找出最小變更,再把這些變更寫入實際的DOM中。這個虛擬DOM以JS結構的形式存在,計算性能會比較好,而且由於減少了實際DOM操作次數,性能會有較大提升

道理我都懂,可是爲什麼我們沒有模塊加載器?

所以就需要Webpack了

說說Webpack

什麼是Webpack?

事實上它是一個打包工具,而不是像RequireJS或SeaJS這樣的模塊加載器,通過使用Webpack,能夠像Node.js一樣處理依賴關係,然後解析出模塊之間的依賴,將代碼打包

安裝Webpack

首先得有Node.js

然後通過npm install -g webpack安裝webpack,當然也可以通過gulp來處理webpack任務,如果使用gulp的話就npm install --save-dev gulp-webpack

配置Webpack

Webpack的構建過程需要一個配置文件,一個典型的配置文件大概就是這樣

var webpack = require('webpack');
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');

module.exports = {
    entry: {
        entry1: './entry/entry1.js',
        entry2: './entry/entry2.js'
    },
    output: {
        path: __dirname,
        filename: '[name].entry.js'
    },
    resolve: {
        extensions: ['', '.js', '.jsx']
    },
    module: {
        loaders: [{
            test: /\.js$/,
            loader: 'babel-loader'
        }, {
            test: /\.jsx$/,
            loader: 'babel-loader!jsx-loader?harmony'
        }]
    },
    plugins: [commonsPlugin]
};

這裏對Webpack的打包行爲做了配置,主要分爲幾個部分:

  • entry:指定打包的入口文件,每有一個鍵值對,就是一個入口文件
  • output:配置打包結果,path定義了輸出的文件夾,filename則定義了打包結果文件的名稱,filename裏面的[name]會由entry中的鍵(這裏是entry1和entry2)替換
  • resolve:定義瞭解析模塊路徑時的配置,常用的就是extensions,可以用來指定模塊的後綴,這樣在引入模塊時就不需要寫後綴了,會自動補全
  • module:定義了對模塊的處理邏輯,這裏可以用loaders定義了一系列的加載器,以及一些正則。當需要加載的文件匹配test的正則時,就會調用後面的loader對文件進行處理,這正是webpack強大的原因。比如這裏定義了凡是.js結尾的文件都是用babel-loader做處理,而.jsx結尾的文件會先經過jsx-loader處理,然後經過babel-loader處理。當然這些loader也需要通過npm install安裝
  • plugins: 這裏定義了需要使用的插件,比如commonsPlugin在打包多個入口文件時會提取出公用的部分,生成common.js

當然Webpack還有很多其他的配置,具體可以參照它的配置文檔

執行打包

如果通過npm install -g webpack方式安裝webpack的話,可以通過命令行直接執行打包命令,比如這樣:

$webpack --config webpack.config.js

這樣就會讀取當前目錄下的webpack.config.js作爲配置文件執行打包操作

如果是通過gulp插件gulp-webpack,則可以在gulpfile中寫上gulp任務:

var gulp = require('gulp');
var webpack = require('gulp-webpack');
var webpackConfig = require('./webpack.config');
gulp.task("webpack", function() {
    return gulp
        .src('./')
        .pipe(webpack(webpackConfig))
        .pipe(gulp.dest('./build'));
});

組件編寫

使用Babel提升逼格

Webpack使得我們可以使用Node.js的CommonJS規範來編寫模塊,比如一個簡單的Hello world模塊,就可以這麼處理:

var React = require('react');

var HelloWorldComponent = React.createClass({
    displayName: 'HelloWorldComponent',
    render: function() {
        return (

<div>Hello world</div>

);
    }
});

module.exports = HelloWorldComponent;

等等,這和之前的寫法沒啥差別啊,依舊沒有逼格...程序員敲碼要有geek範,要逼格than逼格,這太low了。現在都ES6了,React的代碼也要寫ES6,babel-loader就是幹這個的。Babel能夠將ES6代碼轉換成ES5。首先需要通過命令npm install --save-dev babel-loader來進行安裝,安裝完成後就可以使用了,一種使用方式是之前介紹的在webpack.config.js的loaders中配置,另一種是直接在代碼中使用,比如:

var HelloWorldComponent = require('!babel!jsx!./HelloWorldComponent');

那我們應當如何使用Babel提升代碼的逼格呢?改造一下之前的HelloWorld代碼吧:

import React from 'react';

export default class HelloWorldComponent extends React.Component {
    constructor() {
        super();
        this.state = {};
    }
    render() {
        return (

<div>Hello World</div>

);
    }
}

這樣在其他組件中需要引入HelloWorldComponent組件,就只要就可以了:

import HelloWorldComponent from './HelloWorldComponent'

怎麼樣是不是更有逼格了?通過import引入模塊,還可以直接定義類和類的繼承關係,這裏也不再需要getInitialState了,直接在構造函數constructor中用this.state = xxx就好了

Babel帶來的當然還不止這些,在其幫助下還能嘗試很多優秀的ES6特性,比如箭頭函數,箭頭函數的特點就是內部的this和外部保持一致,從此可以和that_this說再見了

['H', 'e', 'l', 'l', 'o'].map((c) => {
    return (<span>{c}</span>);
});

其他還有很多,具體可以參照Babel的學習文檔

樣式編寫

我是一個強烈地Less依賴患者,脫離了Less直接寫CSS就會出現四肢乏力、不想幹活、心情煩躁等現象,而且還不喜歡在寫Less時候加前綴,平常都是gulp+less+autoprefixer直接處理的,那麼在Webpack組織的React組件中要怎麼寫呢?

沒錯,依舊是使用loader

可以在webpack.config.js的loaders中增加Less的配置:

{
  test: /\.less$/,
  loader: 'style-loader!css-loader!autoprefixer-loader!less-loader'
}

通過這樣的配置,就可以直接在模塊代碼中引入Less樣式了:

import React from 'react';

require('./HelloWorldComponent.less');

export default class HelloWorldComponent extends React.Component {
    constructor() {
        super();
        this.state = {};
    }
    render() {
        return (

<div>Hello World</div>

);
    }
}

其他

Webpack的loader爲React組件化提供了很多幫助,像圖片也提供了相關的loader:

{ test: /\.png$/, loader: "url-loader?mimetype=image/png" }

更多地loader可以移步webpack的wiki

在Webpack下實時調試React組件

Webpack和React結合的另一個強大的地方就是,在修改了組件源碼之後,不刷新頁面就能把修改同步到頁面上。這裏需要用到兩個庫webpack-dev-serverreact-hot-loader

首先需要安裝這兩個庫,npm install --save-dev webpack-dev-server react-hot-loader

安裝完成後,就要開始配置了,首先需要修改entry配置:

entry: {
  helloworld: [
    'webpack-dev-server/client?http://localhost:3000',
    'webpack/hot/only-dev-server',
    './helloworld'
  ]
},

通過這種方式指定資源熱啓動對應的服務器,然後需要配置react-hot-loader到loaders的配置當中,比如我的所有組件代碼全部放在scripts文件夾下:

{
  test: /\.js?$/,
  loaders: ['react-hot', 'babel'],
  include: [path.join(__dirname, 'scripts')]
}

最後配置一下plugins,加上熱替換的插件和防止報錯的插件:

plugins: [
  new webpack.HotModuleReplacementPlugin(),
  new webpack.NoErrorsPlugin()
]

這樣配置就完成了,但是現在要調試需要啓動一個服務器,而且之前配置裏映射到http://localhost:3000,所以就在本地3000端口起個服務器吧,在項目根目錄下面建個server.js:

var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');

new WebpackDevServer(webpack(config), {
  publicPath: config.output.publicPath,
  hot: true,
  historyApiFallback: true
}).listen(3000, 'localhost', function (err, result) {
  if (err) console.log(err);
  console.log('Listening at localhost:3000');
});

這樣就可以在本地3000端口開啓調試服務器了,比如我的頁面是根目錄下地index.html,就可以直接通過http://localhost:3000/index.html訪問頁面,修改React組件後頁面也會被同步修改,這裏貌似使用了websocket來同步數據。圖是一個簡單的效果:

結束

React的組件化開發很有想法,而Webpack使得React組件編寫和管理更加方便,這裏只涉及到了React和Webpack得很小一部分,還有更多的最佳實踐有待在學習的路上不斷髮掘

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章