Webpack + React 開發之路
來自: https://segmentfault.com/a/1190000004506929
雜七雜八的想法
記得大二的時候剛學習 Java,我做的第一個圖形化用戶界面是一個仿QQ的登錄窗口,其實就是一些輸入框和按鈕,但是記得當時覺得超級有成就感,于是后來開始喜歡上寫 Java,還做了很多小游戲像飛機大戰、坦克大戰啥的,自己還覺得特別有意思。
后來開始學前端,其實想想也是做圖形化用戶界面,不過是換了一個運行環境而已。但是寫著寫著發現很不順手,和用 Java 寫感覺很不一樣,到底哪不對呢。
用 Java 寫界面的時候,按鈕是按鈕,輸入框是輸入框,我做登錄窗口的時候,只要定義一個登錄窗口類,然后設置布局、把按鈕、輸入框加進去,一個登錄窗口就出來了。
反觀前端的實現,要寫一個登錄窗口,得先在 html 里定義結構,在 css 里制定樣式,然后在 js 里添加行為,最頭疼的是 js 里不僅僅只是這個登錄窗口的行為,還有頁面初始化的代碼、別的按鈕的監聽等等等等一大堆亂七八糟的代碼(作為菜鳥的自我吐槽)
其實我理解的以上問題的關鍵詞就是 組件化 ,之所以以前寫的那么別扭,很大程度上是自己帶著組件化的思想,但是寫不出組件化的代碼。
</div>
直到現在使用上 React,真是感覺眼前一亮。當然還有很多很多需要學習的地方,就從現在開始,配合著 Webpack,踏上 React 的開發之路吧。
制作一個微博發送表單
下面通過 React 編寫一個簡單的例子,就是常用的微博發送的表單。
一、新建項目
項目目錄如下:
/js -- /components ---- /Publisher ------ Publish.css ------ Publish.jsx -- app.js /css -- base.css index.html webpack.config.js
-
js/components 目錄存放所有的組件,比如 Publisher 是我們的表單組件,里面存放這個表單的子組件(如果有的話)、組件的 jsx 文件以及組件自己的樣式。
-
js/app.js 是入口文件
-
css 存放全局樣式
-
index.html 主頁
-
webpack.config.js webpack 的配置文件
</ul>
二、配置 Webpack
編輯 webpack.config.js
var webpack = require('webpack');module.exports = { entry: './js/app.js', output: { path: __dirname, filename: 'bundle.js' }, module: { loaders: [ { test: /.jsx?$/, loader: 'babel', query: { presets: ['react', 'es2015'] } }, { test: /.css$/, loader: 'style!css' } ] }, plugins: [ new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) ] }</pre>
上一篇文章 里是使用 webpack 進行 ES6 開發,其實不管是 ES6 也好,React 也好,webpack 起到的是一個打包器的作用,配置項和這里大致相似,就不再贅述。
不同的是在 babel-loader 里增加了 react 的轉碼規則。
另外這里使用到了 webpack 的一個內置插件 UglifyJsPlugin ,通過他可以對生成的文件進行壓縮。詳細的介紹請看 這里 。
三、安裝一系列東東
首先保證安裝了 nodejs 。
1) 初始化項目
npm init2) 安裝 webpack
npm install webpack -g3) 安裝 React
npm install react react-dom --save-dev4) 安裝加載器
本項目使用到的有 babel-loader、css-loader、style-loader。
-
babel-loader 進行轉碼
-
css-loader 對 css 文件進行打包
-
style-loader 將樣式添加進 DOM 中
詳細請看 這里 。
npm install babel-loader css-loader style-loader --save-dev
5) 安裝轉碼規則
npm install babel-preset-es2015 babel-preset-react --save-dev
四、碼代碼
index.html 中,引用的 js 文件是通過 webpack 生成的 bundle.js , css 文件是寫在 /css 目錄下的 base.css 。
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <link rel="stylesheet" href="css/base.css"> </head> <body> <div id="container"></div> <script src="bundle.js"></script> </body> </html>
/css/base.css
base.css 里面存放的是全局樣式,也就是與組件無關的。
html, body, textarea { padding: 0; margin: 0; }body { font: 12px/1.3 'Arial','Microsoft YaHei'; background: #73a2b0; }
textarea { resize: none; }
a { color: #368da7; text-decoration: none; }</pre>
/js/app.js
/js/app.js 是入口文件,引入了 Publisher 組件
import React from 'react'; import ReactDOM from 'react-dom'; import Publisher from './components/Publisher/Publisher.jsx';ReactDOM.render( <Publisher />, document.getElementById('container') );</pre>
/js/components/Publisher/Publisher.jsx
好的,下面開始編寫組件,首先,確定這個組件的組成部分,因為是一個簡單的表單,所以不需要繼續劃分子組件
表單分為上中下三部分, title 里面包含熱門微博和剩余字數的提示, textElDiv 包含輸入框, btnWrap 包含發布按鈕。
import React from 'react';class Publisher extends React.Component { constructor(...args) { super(...args); }
render() { return ( <div className="publisher"> <div className="title"> <div> <a href="#">網友曝光兩女孩蹲著等地鐵,稱沒教養,你怎么看(投票)</a> </div> <div className="tips"> <span>還可以輸入</span><strong>140</strong>字 </div> </div> <div className="textElDiv"> <textarea></textarea> </div> <div className="btnWrap"> <a className="publishBtn" href="javascript:void(0)">發布</a> </div> </div> ); }
}
export default Publisher;</pre>
我們暫時通過 className 給組件定義了樣式名,但還沒有實際寫樣式代碼,因為要保證組件的封裝性,所以我們不希望組件的樣式編寫到全局中去以免影響其他組件,最好像我們的目錄劃分一樣,組件自己的樣式跟著組件自己走,而且這個樣式不影響其他組件。這里就需要用到 css-loader 了。
css-loader 可以將 css 文件進行打包,而且可以對 css 文件里的 局部 className 進行哈希編碼。這意味著可以這樣寫樣式文件:
/ xxx.css /:local(.className) { background: red; } :local .className { color: green; } :local(.className .subClass) { color: green; } :local .className .subClass :global(.global-class-name) { color: blue; }</pre>
經過處理之后,則變成:
._23_aKvs-b8bW2Vg3fwHozO { background: red; } ._23_aKvs-b8bW2Vg3fwHozO { color: green; } ._23_aKvs-b8bW2Vg3fwHozO ._13LGdX8RMStbBE9w-t0gZ1 { color: green; } ._23_aKvs-b8bW2Vg3fwHozO ._13LGdX8RMStbBE9w-t0gZ1 .global-class-name { color: blue; }也就是我們可以在不同的組件樣式中定義 .btn 的樣式名,但是經過打包之后,在全局里面就被轉成了不同的哈希編碼,由此解決了 css 全局命名沖突的問題。
關于 css-loader 更詳細的使用,請參考 這里 。
那么 Publisher 的樣式如下:
/js/components/Publisher/Publisher.css
:local .publisher{ width: 600px; margin: 10px auto; background: #ffffff; box-shadow: 0 0 2px rgba(0,0,0,0.15); border-radius: 2px; padding: 15px 10px 10px; height: 140px; position: relative; font-size: 12px; }:local .title{ position: relative; }
:local .title div { position: absolute; right: 0; top: 2px; }
:local .tips { color: #919191; display: none; }
:local .textElDiv { border: 1px #cccccc solid; height: 68px; margin: 25px 0 0; padding: 5px; box-shadow: 0px 0px 3px 0px rgba(0,0,0,0.15) inset; }
:local .textElDiv textarea { border: none; border: 0px; font-size: 14px; word-wrap: break-word; line-height: 18px; overflow-y: auto; overflow-x: hidden; outline: none; background: transparent; width: 100%; height: 68px; }
:local .btnWrap { float: right; padding: 5px 0 0; }
:local .publishBtn { display: inline-block; height: 28px; line-height: 29px; width: 60px; font-size: 14px; background: #ff8140; border: 1px solid #f77c3d; border-radius: 2px; color: #fff; box-shadow: 0px 1px 2px rgba(0,0,0,0.25); padding: 0 10px 0 10px; text-align: center; outline: none; }
:local .publishBtn.disabled { background: #ffc09f; color: #fff; border: 1px solid #fbbd9e; box-shadow: none; cursor: default; }</pre>
然后就可以在 Publisher.jsx 中這樣使用了
import React from 'react'; import style from './Publisher.css';class Publisher extends React.Component { constructor(...args) { super(...args); }
render() { return ( <div className={style.publisher}> <div className={style.title}> <div> <a href="#">網友曝光兩女孩蹲著等地鐵,稱沒教養,你怎么看(投票)</a> </div> <div className={style.tips}> <span>還可以輸入</span><strong>140</strong>字 </div> </div> <div className={style.textElDiv}> <textarea></textarea> </div> <div className={style.btnWrap}> <a className={style.publishBtn>發布</a> </div> </div> ); }
}
export default Publisher;</pre>
這樣組件的樣式已經添加進去了,接下來就純粹是進行 React 開發了。
編寫 Publisher.jsx
表單的需求如下:
輸入框獲取焦點時,輸入框邊框變為橙色,右上角顯示剩余字數的提示;輸入框失去焦點時,輸入框邊框變為灰色,右上角顯示熱門微博。
輸入字數小于且等于140字時,提示顯示剩余可輸入字數;輸入字數大于140時,提示顯示已經超過字數。
輸入字數大于0且不大于140字時,按鈕為亮橙色且可點擊,否則為淺橙色且不可點擊。
首先,給 textarea 添加 onFocus 、 onBlur 、 onChange 事件,通過 handleFocus 、 handleBlur 、 handleChange 來處理輸入框獲取焦點、失去焦點和輸入。
然后將輸入的內容保存在 state 里,這樣每當內容發生變化時,就能方便的對變化進行處理。
對于按鈕的變化、熱門微博和提示之間的轉換,根據 state 中內容的變化來切換樣式就能輕松地做到。
完整代碼如下:
import React from 'react'; import style from './Publisher.css';class Publisher extends React.Component { constructor(...args) { super(...args); // 定義 state this.state = { content: '' } }
/** * 獲取焦點 **/ handleFocus() { // 改變邊框顏色 this.refs.textElDiv.style.borderColor = '#fa7d3c'; // 切換右上角內容 this.refs.hot.style.display = 'none'; this.refs.tips.style.display = 'block'; } /** * 失去焦點 **/ handleBlur() { // 改變邊框顏色 this.refs.textElDiv.style.borderColor = '#cccccc'; // 切換右上角內容 this.refs.hot.style.display = 'block'; this.refs.tips.style.display = 'none'; } /** * 輸入框內容發生變化 **/ handleChange(e) { // 改變狀態值 this.setState({ content: e.target.value }); } render() { return ( <div className={style.publisher}> <div className={style.title}> <div ref="hot"> <a href="#">網友曝光兩女孩蹲著等地鐵,稱沒教養,你怎么看(投票)</a> </div> <div className={style.tips} ref="tips"> <span>{this.state.content.length > 140 ? '已超出' : '還可以輸入'}</span><strong>{this.state.content.length > 140 ? this.state.content.length - 140 : 140 - this.state.content.length}</strong>字 </div> </div> <div className={style.textElDiv} ref="textElDiv"> <textarea onFocus={this.handleFocus.bind(this)} onBlur={this.handleBlur.bind(this)} onChange={this.handleChange.bind(this)}></textarea> </div> <div className={style.btnWrap}> <a className={style.publishBtn + ((this.state.content.length > 0 && this.state.content.length <= 140) ? '' : ' ' + style.disabled)} href="javascript:void(0)">發布</a> </div> </div> ); }
}
export default Publisher;</pre>
五、運行
-
通過 --display-error-detail 可以顯示 webpack 出現錯誤的中間過程,方便在出錯時進行查看。
-
--progress --colors 可以顯示進度
-
--watch 可以監視文件的變化并在變化后重新加載
運如如下:
webpack --display-error-detail --progress --colors --watch</div>