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 init
2) 安裝 webpack
npm install webpack -g
3) 安裝 React
npm install react react-dom --save-dev
4) 安裝加載器
本項目使用到的有 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>