輕松入門React和Webpack
最近在學(xué)習(xí)React.js,之前都是直接用最原生的方式去寫React代碼,發(fā)現(xiàn)組織起來特別麻煩,之前聽人說用Webpack組織React組件得心應(yīng)手,就花了點時間學(xué)習(xí)了一下,收獲頗豐。
說說React
一個組件,有自己的結(jié)構(gòu),有自己的邏輯,有自己的樣式,會依賴一些資源,會依賴某些其他組件。比如日常寫一個組件,比較常規(guī)的方式:
- 通過前端模板引擎定義結(jié)構(gòu)
- JS文件中寫自己的邏輯
- CSS中寫組件的樣式
- 通過RequireJS、SeaJS這樣的庫來解決模塊之間的相互依賴,
那么在React中是什么樣子呢?
結(jié)構(gòu)和邏輯
在React的世界里,結(jié)構(gòu)和邏輯交由JSX文件組織,React將模板內(nèi)嵌到邏輯內(nèi)部,實現(xiàn)了一個JS代碼和HTML混合的JSX。
結(jié)構(gòu)
在JSX文件中,可以直接通過React.createClass來定義組件:
- var CustomComponent = React.creatClass({
- render: function(){
- return ();
- }
- });
通過這種方式可以很方便的定義一個組件,組件的結(jié)構(gòu)定義在render函數(shù)中,但這并不是簡單的模板引擎,我們可以通過js方便、直觀的操控組件結(jié)構(gòu),比如我想給組件增加幾個節(jié)點:
通過這種方式,React使得組件擁有靈活的結(jié)構(gòu)。那么React又是如何處理邏輯的呢?
邏輯
寫過前端組件的人都知道,組件通常首先需要相應(yīng)自身DOM事件,做一些處理。必要時候還需要暴露一些外部接口,那么React組件要怎么做到這兩點呢?
事件響應(yīng)
比如我有個按鈕組件,點擊之后需要做一些處理邏輯,那么React組件大致上長這樣:
- var ButtonComponent = React.createClass({
- render: function(){
- return (屠龍寶刀,點擊就送);
- }
- });
點擊按鈕應(yīng)當(dāng)觸發(fā)相應(yīng)地邏輯,一種比較直觀的方式就是給button綁定一個onclick事件,里面就是需要執(zhí)行的邏輯了:
- function getDragonKillingSword() {
- //送寶刀
- }
- var ButtonComponent = React.createClass({
- render: function(){
- return (屠龍寶刀,點擊就送);
- }
- });
這樣就實現(xiàn)內(nèi)部事件的響應(yīng)了,那如果需要暴露接口怎么辦呢?
暴露接口
事實上現(xiàn)在getDragonKilling
- <br>
Sword已經(jīng)是一個接口了,如果有一個父組件,想要調(diào)用這個接口怎么辦呢?
父組件大概長這樣:
- var ImDaddyComponent = React.createClass({
- render: function(){
- return (
- //其他組件
- //其他組件
- );
- }
- });
那么如果想手動調(diào)用組件的方法,首先在ButtonComponent上設(shè)置一個ref=""屬性來標(biāo)記一下,比如這里把子組件設(shè)置成
- this.refs.getSwordButton.getDragonKillingSword();
看起來屌屌噠~那么問題又來了,父組件希望自己能夠按鈕點擊時調(diào)用的方法,那該怎么辦呢?
配置參數(shù)
父組件可以直接將需要執(zhí)行的函數(shù)傳遞給子組件:
然后在子組件中調(diào)用父組件方法:
- var ButtonComponent = React.createClass({
- render: function(){
- return (屠龍寶刀,點擊就送);
- }
- });
子組件通過this.props能夠獲取在父組件創(chuàng)建子組件時傳入的任何參數(shù),因此this.props也常被當(dāng)做配置參數(shù)來使用。
屠龍寶刀每個人只能領(lǐng)取一把,按鈕點擊一下就應(yīng)該灰掉,應(yīng)當(dāng)在子組件中增加一個是否點擊過的狀態(tài),這又應(yīng)當(dāng)處理呢?
組件狀態(tài)
在React中,每個組件都有自己的狀態(tài),可以在自身的方法中通過this.state取到,而初始狀態(tài)則通過getInitialState()方法來定義,比如這個屠龍寶刀按鈕組件,它的初始狀態(tài)應(yīng)該是沒有點擊過,所以getInitialState方法里面應(yīng)當(dāng)定義初始狀態(tài)clicked: false。而在點擊執(zhí)行的方法中,應(yīng)當(dāng)修改這個狀態(tài)值為click: true:
- var ButtonComponent = React.createClass({
- getInitialState: function(){
- //確定初始狀態(tài)
- return {
- clicked: false
- };
- },
- getDragonKillingSword: function(){
- //送寶刀
- //修改點擊狀態(tài)
- this.setState({
- clicked: true
- });
- },
- render: function(){
- return (屠龍寶刀,點擊就送);
- }
- });
這樣點擊狀態(tài)的維護(hù)就完成了,那么render函數(shù)中也應(yīng)當(dāng)根據(jù)狀態(tài)來維護(hù)節(jié)點的樣式,比如這里將按鈕設(shè)置為disabled,那么render函數(shù)就要添加相應(yīng)的判斷邏輯:
- render: function(){
- var clicked = this.state.clicked;
- if(clicked)
- return (屠龍寶刀,點擊就送);
- else
- return (屠龍寶刀,點擊就送);
- }
小節(jié)
這里簡單介紹了通過JSX來管理組件的結(jié)構(gòu)和邏輯,事實上React給組件還定義了很多方法,以及組件自身的生命周期,這些都使得組件的邏輯處理更加強大
#p#
資源加載
CSS文件定義了組件的樣式,現(xiàn)在的模塊加載器通常都能夠加載CSS文件,如果不能一般也提供了相應(yīng)的插件。事實上CSS、圖片可以看做是一種資源,因為加載過來后一般不需要做什么處理。
React對這一方面并沒有做特別的處理,雖然它提供了Inline Style的方式把CSS寫在JSX里面,但估計沒有多少人會去嘗試,畢竟現(xiàn)在CSS樣式已經(jīng)不再只是簡單的CSS文件了,通常都會去用Less、Sass等預(yù)處理,然后再用像postcss、myth、autoprefixer、cssmin等等后處理。資源加載一般也就簡單粗暴地使用模塊加載器完成了
組件依賴
組件依賴的處理一般分為兩個部分:組件加載和組件使用。
組件加載
React沒有提供相關(guān)的組件加載方法,依舊需要通過<script>標(biāo)簽引入,或者使用模塊加載器加載組件的JSX和資源文件。
組件使用
如果細(xì)心,就會發(fā)現(xiàn)其實之前已經(jīng)有使用的例子了,要想在一個組件中使用另外一個組件,比如在ParentComponent中使用ChildComponent,就只需要在ParentComponent的render()方法中寫上<ChildComponent />就行了,必要的時候還可以傳些參數(shù)。
疑問
到這里就會發(fā)現(xiàn)一個問題,React除了只處理了結(jié)構(gòu)和邏輯,資源也不管,依賴也不管。是的,React將近兩萬行代碼,連個模塊加載器都沒有提供,更與Angularjs,jQuery等不同的是,他還不帶啥腳手架...沒有Ajax庫,沒有Promise庫,要啥啥沒有...
虛擬DOM
那它為啥這么大?因為它實現(xiàn)了一個虛擬DOM(Virtual DOM)。虛擬DOM是干什么的?這就要從瀏覽器本身講起
如我們所知,在瀏覽器渲染網(wǎng)頁的過程中,加載到HTML文檔后,會將文檔解析并構(gòu)建DOM樹,然后將其與解析CSS生成的CSSOM樹一起結(jié)合產(chǎn)生愛的結(jié)晶——RenderObject樹,然后將RenderObject樹渲染成頁面(當(dāng)然中間可能會有一些優(yōu)化,比如RenderLayer樹)。這些過程都存在與渲染引擎之中,渲染引擎在瀏覽器中是于JavaScript引擎(JavaScriptCore也好V8也好)分離開的,但為了方便JS操作DOM結(jié)構(gòu),渲染引擎會暴露一些接口供JavaScript調(diào)用。由于這兩塊相互分離,通信是需要付出代價的,因此JavaScript調(diào)用DOM提供的接口性能不咋地。各種性能優(yōu)化的最佳實踐也都在盡可能的減少DOM操作次數(shù)。
而虛擬DOM干了什么?它直接用JavaScript實現(xiàn)了DOM樹(大致上)。組件的HTML結(jié)構(gòu)并不會直接生成DOM,而是映射生成虛擬的JavaScript DOM結(jié)構(gòu),React又通過在這個虛擬DOM上實現(xiàn)了一個 diff 算法找出最小變更,再把這些變更寫入實際的DOM中。這個虛擬DOM以JS結(jié)構(gòu)的形式存在,計算性能會比較好,而且由于減少了實際DOM操作次數(shù),性能會有較大提升
道理我都懂,可是為什么我們沒有模塊加載器?
所以就需要Webpack了
說說Webpack
什么是Webpack?
事實上它是一個打包工具,而不是像RequireJS或SeaJS這樣的模塊加載器,通過使用Webpack,能夠像Node.js一樣處理依賴關(guān)系,然后解析出模塊之間的依賴,將代碼打包
安裝Webpack
首先得有Node.js
然后通過npm install -g webpack安裝webpack,當(dāng)然也可以通過gulp來處理webpack任務(wù),如果使用gulp的話就npm install --save-dev gulp-webpack
配置Webpack
Webpack的構(gòu)建過程需要一個配置文件,一個典型的配置文件大概就是這樣
- 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:配置打包結(jié)果,path定義了輸出的文件夾,filename則定義了打包結(jié)果文件的名稱,filename里面的[name]會由entry中的鍵(這里是entry1和entry2)替換
- resolve:定義了解析模塊路徑時的配置,常用的就是extensions,可以用來指定模塊的后綴,這樣在引入模塊時就不需要寫后綴了,會自動補全
- module:定義了對模塊的處理邏輯,這里可以用loaders定義了一系列的加載器,以及一些正則。當(dāng)需要加載的文件匹配test的正則時,就會調(diào)用后面的loader對文件進(jìn)行處理,這正是webpack強大的原因。比如這里定義了凡是.js結(jié)尾的文件都是用babel-loader做處理,而.jsx結(jié)尾的文件會先經(jīng)過jsx-loader處理,然后經(jīng)過babel-loader處理。當(dāng)然這些loader也需要通過npm install安裝
- plugins: 這里定義了需要使用的插件,比如commonsPlugin在打包多個入口文件時會提取出公用的部分,生成common.js
當(dāng)然Webpack還有很多其他的配置,具體可以參照它的配置文檔
執(zhí)行打包
如果通過npm install -g webpack方式安裝webpack的話,可以通過命令行直接執(zhí)行打包命令,比如這樣:
- $webpack --config webpack.config.js
這樣就會讀取當(dāng)前目錄下的webpack.config.js作為配置文件執(zhí)行打包操作
如果是通過gulp插件gulp-webpack,則可以在gulpfile中寫上gulp任務(wù):
- 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'));
- });
#p#
組件編寫
使用Babel提升逼格
Webpack使得我們可以使用Node.js的CommonJS規(guī)范來編寫模塊,比如一個簡單的Hello world模塊,就可以這么處理:
- var React = require('react');
- var HelloWorldComponent = React.createClass({
- displayName: 'HelloWorldComponent',
- render: function() {
- return (<div>Hello world</div>);
- }
- });
- module.exports = HelloWorldComponent;
等等,這和之前的寫法沒啥差別啊,依舊沒有逼格...程序員敲碼要有g(shù)eek范,要逼格than逼格,這太low了?,F(xiàn)在都ES6了,React的代碼也要寫ES6,babel-loader就是干這個的。Babel能夠?qū)S6代碼轉(zhuǎn)換成ES5。首先需要通過命令npm install --save-dev babel-loader來進(jìn)行安裝,安裝完成后就可以使用了,一種使用方式是之前介紹的在webpack.config.js的loaders中配置,另一種是直接在代碼中使用,比如:
- var HelloWorldComponent = require('!babel!jsx!./HelloWorldComponent');
那我們應(yīng)當(dāng)如何使用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引入模塊,還可以直接定義類和類的繼承關(guān)系,這里也不再需要getInitialState了,直接在構(gòu)造函數(shù)constructor中用this.state = xxx就好了
Babel帶來的當(dāng)然還不止這些,在其幫助下還能嘗試很多優(yōu)秀的ES6特性,比如箭頭函數(shù),箭頭函數(shù)的特點就是內(nèi)部的this和外部保持一致,從此可以和that、_this說再見了
- ['H', 'e', 'l', 'l', 'o'].map((c) => {
- return (<span>{c}</span>);
- });
其他還有很多,具體可以參照Babel的學(xué)習(xí)文檔
樣式編寫
我是一個強烈地Less依賴患者,脫離了Less直接寫CSS就會出現(xiàn)四肢乏力、不想干活、心情煩躁等現(xiàn)象,而且還不喜歡在寫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組件化提供了很多幫助,像圖片也提供了相關(guān)的loader:
- { test: /\.png$/, loader: "url-loader?mimetype=image/png" }
更多地loader可以移步webpack的wiki
##在Webpack下實時調(diào)試React組件
Webpack和React結(jié)合的另一個強大的地方就是,在修改了組件源碼之后,不刷新頁面就能把修改同步到頁面上。這里需要用到兩個庫webpack-dev-server和react-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'
- ]
- },
通過這種方式指定資源熱啟動對應(yīng)的服務(wù)器,然后需要配置react-hot-loader到loaders的配置當(dāng)中,比如我的所有組件代碼全部放在scripts文件夾下:
- {
- test: /\.js?$/,
- loaders: ['react-hot', 'babel'],
- include: [path.join(__dirname, 'scripts')]
- }
最后配置一下plugins,加上熱替換的插件和防止報錯的插件:
- plugins: [
- new webpack.HotModuleReplacementPlugin(),
- new webpack.NoErrorsPlugin()
- ]
這樣配置就完成了,但是現(xiàn)在要調(diào)試需要啟動一個服務(wù)器,而且之前配置里映射到http://localhost:3000,所以就在本地3000端口起個服務(wù)器吧,在項目根目錄下面建個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端口開啟調(diào)試服務(wù)器了,比如我的頁面是根目錄下地index.html,就可以直接通過http://localhost:3000/index.html訪問頁面,修改React組件后頁面也會被同步修改,這里貌似使用了websocket來同步數(shù)據(jù)。圖是一個簡單的效果:
結(jié)束
React的組件化開發(fā)很有想法,而Webpack使得React組件編寫和管理更加方便,這里只涉及到了React和Webpack得很小一部分,還有更多的最佳實踐有待在學(xué)習(xí)的路上不斷發(fā)掘