React/Webpack 入門教程
Learn React & Webpack by building the Hacker News front page
這是一篇給初學者的教程, 在這篇教程中我們將通過構建一個 Hacker News 的前端頁面來學習 React 與 Webpack . 它不會覆蓋所有的技術細節, 因此它不會使一個初學者變成大師, 但希望能給初學者一個大致印象.
注意: 這篇文章已經有部分過時,需要等待更新.文章寫成時 webpack 版本使用的是 1.x,現在 webpack 默認安裝 2.x.
準備工作
-
安裝 webpack
在此之前你應該已經安裝了 node.js .
npm install webpack -g
參數 -g 表示我們將全局(global)安裝 webpack, 這樣你就能使用 webpack 命令了.
webpack 也有一個 web 服務器 webpack-dev-server, 我們也安裝上
npm install webpack-dev-server -g
-
webpack 配置文件
webpack 使用一個名為 webpack.config.js 的配置文件, 現在在你的項目根目錄下創建該文件. 我們假設我們的工程有一個入口文件 app.js , 該文件位于 app/ 目錄下, 并且希望 webpack 將它打包輸出為 build/ 目錄下的 bundle.js 文件. webpack.config.js 配置如下:
var path = require('path');
module.exports = { entry: path.resolve(dirname, 'app/app.js'), output: { path: path.resolve(dirname, 'build'), filename: 'bundle.js' } }</pre>
現在讓我們測試一下, 創建 app/app.js 文件, 填入一下內容:
document.write('It works');
創建 build/index.html , 填入以下內容:
<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>Hacker News Front Page</title> </head> <body> <script src="./bundle.js"></script> </body> </html>
其中 script 引入了 bundle.js , 這是 webpack 打包后的輸出文件.
運行 webpack 打包, 運行 webpack-dev-server 啟動服務器. 訪問 http://localhost:8080/build/index.html , 如果一切順利, 你會看到打印出了 It works .
</li> -
配置 package.json
在項目根目錄下運行 npm init -y 會按照默認設置生成 package.json , 修改 scripts 的鍵值如下:
"scripts": { "start": "webpack-dev-server", "build": "webpack" }
現在執行 npm run build 相當于 webpack , npm run start 相當于 webpack-dev-server . 當項目變得相當復雜時, 你可以使用這種技巧隱藏其中的細節.
-
安裝依賴
安裝 React:
npm install react react-dom --save
為了簡化 AJAX 請求代碼, 這里引入 jQuery:
npm install jquery --save
安裝 Babel 的 loader 以支持 ES6 語法:
npm install babel-core babel-loader babel-preset-es2015 babel-preset-react --save-dev
然后配置 webpack.config.js 來使用安裝的 loader.
// webpack.config.js
var path = require('path');
module.exports = { entry: path.resolve(dirname, 'app/app.js'), output: { path: path.resolve(dirname, 'build'), filename: 'bundle.js' }, module: { loaders: [ { test: /.jsx?$/, exclude: /node_modules/, loader: 'babel', query: { presets: ['es2015','react'] } }, ] } };</pre>
接下來測試一下開發環境是否搭建完成.
打開 app.js , 修改內容為:
// app.js
import $ from 'jquery'; import React from 'react'; import { render } from 'react-dom';
class HelloWorld extends React.Component { render() { return ( <div>Hello World</div> ); } }
render(<HelloWorld />, $('#content')[0]);</pre>
這里組件 HelloWorld 被裝載到 id 為 content 的 DOM 元素, 所以相應的需要修改 index.html .
... <body> <div id="content"></div> <script src="./bundle.js"></script> </body> ...
再次打包運行, 訪問 http://localhost:8080/build/index.html 如果可以看到打印出了 Hello World 那么開發環境就算搭建完成了.
</li> </ul>在開始寫第一個組件之前 ...
在開始寫第一個組件之前, 讓我們分析一下到底我們需要哪些組件.
上圖是的最終效果的部分截圖, 我們將其分為幾個組件, 用不同顏色的線框標出
我們可以看出其中包含以下幾個組件.
- NewsList (藍色): 所有組件的容器.
- NewsHeader (綠色): Logo, 標題, 導航欄等.
- NewsItem (紅色): 對應每條資訊.
把這些組成為一棵組件樹.
- NewsList
- NewsHeader
- NewsItem * n
編寫大致的模板
上一節我們知道了整個頁面的組件結構, 在這一節我們根據這些結果編寫出一個大致的模板. 先在 app 目錄下為每個組件建立文件, NewsList.js , NewsHeader.js 和 NewsItem.js .
cd app touch NewsList.js NewsHeader.js NewsItem.js
編輯 NewsHeader.js
// NewsHeader.js
import React from 'react';
export default class NewsHeader extends React.Component { render() { return ( <div className="newsHeader"> I am NewsHeader. </div> ); } }</pre>
NewsHeader 組件就先這樣, 具體的實現現在先不去考慮, 記得我們現在只是編寫一個大致的結構.
同樣的, 編輯 NewsItem.js
// NewsItem.js
import React from 'react';
export default class NewsItem extends React.Component { render() { return ( <div className="newsItem"> I am NewsItem. </div> ); } }</pre>
接著是 NewsList.js , 因為 NewsList 是前兩個組件的容器, 所以我們需要引入它們
// NewsList.js
import React from 'react'; import NewsHeader from './NewsHeader.js'; import NewsItem from './NewsItem.js';
export default class NewsList extends React.Component { render() { return ( <div className="newsList"> <NewsHeader /> <NewsItem /> </div> ); } }</pre>
最后修改入口文件 app.js
// app.js
import React from 'react' import { render } from 'react-dom'; import $ from 'jquery'; import NewsList from './NewsList.js';
render(<NewsList />, $('#content')[0]);</pre>
這時訪問 http://localhost:8080/build/index.html 可以看到下圖的效果
接下來讓我們逐步完善各個組件.
NewsHeader
整個 NewsHeader 包含了三個部分, 左邊的Logo和標題, 中間的導航欄和右邊的登錄入口.
讓我們先從左邊的 Logo 和標題開始.
-
Logo 和標題
先下載 Logo
在 NewsHeader 組件里新增一個方法 getLogo , 就像下面這樣.
// NewsHeader.js
// ...
export default class NewsHeader extends React.Component { //... getLogo() { / Do Something / } //... }</pre>
這個方法返回一個包含 Logo 的子組件.
getLogo() { return ( <div className="newsHeader-logo"> <a ><img src="PATH_TO_IMAGE" /></a> </div> ); }
這里遇到一個問題, img 的 src 填什么?
在前面幾節的開發中, 還記得你是怎么引入其他的 js 文件的嗎? import . 實際上這是 ES6 的模塊系統, 這里的 js 文件作為模塊被其他模塊引入. 但除了 js 文件, 在開發時我們還會涉及其他的資源文件, 如圖像, 字體, 樣式等, 它們也需要被模塊化. 在這里, 如果 Logo 圖片也能被模塊化然后引入該多好. 我們需要再次配置 Webpack.
安裝對應的 loader:
npm install url-loader file-loader --save-dev
配置 webpack.config.js
//...
loaders: [ { test: /.jsx?$/, exclude: /node_modules/, loader: 'babel', query: { presets: ['es2015','react'] } }, { test: /.(png|jpg|gif)$/, loader: 'url-loader?limit=8192' // 這里的 limit=8192 表示用 base64 編碼 <= 8K 的圖像 } ]
//...</pre>
然后回到 NewsHeader.js
這時候你就可以使用 import 引入圖片了.
import imageLogo from './y18.gif';
然后像這樣使用.
<img src={imageLogo} />
注意這里用 {} 包起來, 這樣其中的內容會作為表達式.
getLogo 方法完成了, 再寫一個 getTitle 方法.
getTitle() { return ( <div className="newsHeader-title"> <a className="newsHeader-textLink" >Hacker News</a> </div> ); }
修改 render 方法, 引用我們剛剛寫好的那兩個方法.
render() { return ( <div className="newsHeader"> {this.getLogo()} {this.getTitle()} </div> ); }
還是一樣別忘了 {} .
最后我們需要添加樣式, 還是回到剛剛的問題, 怎么引入樣式?
我們也需要將樣式模塊化.
安裝相應的 loader:
npm install css-loader style-loader --save-dev
css-loader 處理 css 文件中的 url() 表達式.
style-loader 將 css 代碼插入頁面中的 style 標簽中.
在 webpack.config.js 中配置新的 loader.
{ test: /\.css$/, loader: 'style!css' }
新建一個 css 文件 NewsHeader.css
.newsHeader { align-items: center; background: #ff6600; color: black; display: flex; font-size: 10pt; padding: 2px; }
.newsHeader-logo { border: 1px solid white; flex-basis: 18px; height: 18px; }
.newsHeader-textLink { color: black; text-decoration: none; }
.newsHeader-title { font-weight: bold; margin-left: 4px; }</pre>
然后在 NewsHeader.js 中引入它
import './NewsHeader.css';
再建立一個全局的 css 文件 app.css
body { font-family: Verdana, sans-serif; }
然后在 app.js 中引入
import './app.css'
打包運行看看吧.
</li> -
導航欄 接下來是導航欄, 也就是中間那部分.
回到 NewsHeader.js , 增加一個 getNav 方法.
getNav() { var navLinks = [ { name: 'new', url: 'newest' }, { name: 'comments', url: 'newcomments' }, { name: 'show', url: 'show' }, { name: 'ask', url: 'ask' }, { name: 'jobs', url: 'jobs' }, { name: 'submit', url: 'submit' } ];
return ( <div className="newsHeader-nav"> { navLinks.map(function(navLink) { return ( <a key={navLink.url} className="newsHeader-navLink newsHeader-textLink" href={"同樣, 記得在 render 方法中引用
render() { return ( <div className="newsHeader"> {this.getLogo()} {this.getTitle()} {this.getNav()} </div> ); }
添加樣式.
.newsHeader-nav { flex-grow: 1; margin-left: 10px; }
.newsHeader-navLink:not(:first-child)::before { content: ' | '; }</pre> </li>
-
登錄入口
增加一個 getLogin 方法.
getLogin() { return ( <div className="newsHeader-login"> <a className="newsHeader-textLink" >login</a> </div> ); }
在 render 中引用
render() { return ( <div className="newsHeader"> {this.getLogo()} {this.getTitle()} {this.getNav()} {this.getLogin()} </div> ); }
更新樣式
.newsHeader-login { margin-right: 5px; }
</ol>
-
NewsItem 標題
先來簡單點的, 第一步我們只獲取并顯示標題.
修改 NewsList.js
render() { var testData = { "by" : "bane", "descendants" : 49, "id" : 11600137, "kids" : [ 11600476, 11600473, 11600501, 11600463, 11600452, 11600528, 11600421, 11600577, 11600483 ], "score" : 56, "time" : 1461985332, "title" : "Yahoo's Marissa Mayer could get $55M in severance pay", "type" : "story", "url" : "
return ( <div className="newsList"> <NewsHeader /> <NewsItem item={testData} rank={1} /> </div> ); }</pre>
這里我們聲明一個 testData 作為測試數據傳入 NewsItem.
修改 NewsItem.js
render: function () { return ( <div className="newsItem"> <a className="newsItem-titleLink" href={this.props.item.url}>{this.props.item.title}</a> </div> ); }
在這里使用 this.props.item 訪問 item 屬性.
建立 NewsItem.css
.newsItem { color: #828282; margin-top: 5px; align-items: baseline; display: flex;
}.newsItem-titleLink { color: black; font-size: 10pt; text-decoration: none; }</pre>
在 NewsItem.js 中引入
import './NewsItem.css';
運行看看效果, 顯示的標題應該和傳入的測試數據中的一樣.
</li>NewsItem 來源地址
我們現在添加來源地址到標題的末尾. 先在 NewsItem.js 中引入 url 模塊
import URL from 'url';
然后增加一個 getDomain 方法.
getDomain() { return URL.parse(this.props.item.url).hostname; }
然后再增加一個 getTitle 方法, 這個方法會返回一個包含了標題(我們上一節做的事)和地址的組件.
getTitle() { return ( <div className="newsItem-title"> <a className="newsItem-titleLink" href={this.props.item.url}>{this.props.item.title}</a> <span className="newsItem-domain"><a href={'https://news.ycombinator.com/from?site=' + this.getDomain()}>({this.getDomain()})</a></span> </div> ); }
修改 render
render() { return ( <div className="newsItem"> <div className="newsItem-itemText"> {this.getTitle()} </div> </div> ); }
增加樣式
.newsItem-itemText { flex-grow: 1; }
.newsItem-domain { font-size: 8pt; margin-left: 5px; }
.newsItem-domain > a { color: #828282; text-decoration: none; }</pre>
好了, 看起來不錯, 但是有個問題, 這個項目最終需要從 Hacker News 的 API 取得資訊數據, 而其中有些是沒有 url 屬性的, 看看我們的 getTitle() 方法, 我們似乎忽略了這個特例, 讓我們做些修改.
getTitle() { return ( <div className="newsItem-title"> <a className="newsItem-titleLink" href={this.props.item.url ? this.props.item.url : 'https://news.ycombinator.com/item?id=' + this.props.item.id}>{this.props.item.title}</a> { this.props.item.url && <span className="newsItem-domain"><a href={'https://news.ycombinator.com/from?site=' + this.getDomain()}>({this.getDomain()})</a></span> } </div> ); }
試著去掉 testData 的 url 屬性, 看看是不是一切正常.
</li>NewsItem 其余部分
我們現在加上其余部分, 你已經看過了前兩節, 這節應該是沒有什么難度的, 我們快速帶過.
下載 grayarrow.gif , 在 NewsItem.js 中引入
import ImageGrayArrow from './grayarrow.gif';
修改 NewsItem.js
getCommentLink() { // 評論鏈接 var commentText = 'discuss'; if(this.props.item.kids && this.props.item.kids.length) { commentText = this.props.item.kids.length + ' comment'; }
return ( <a href={'
getSubtext() { // 分數, 作者, 時間, 評論數 return ( <div className="newsItem-subtext"> {this.props.item.score} points by <a href={'
getRank() { // 序號 return ( <div className="newsItem-rank"> {this.props.rank}. </div> ); }
getVote() { // 投票 return ( <div className="newsItem-vote"> <a href={' this.props.item.id + '&dir=up&goto=news'}> <img src={ImageGrayArrow} width="10" /> </a> </div> ); }
render() { return ( <div className="newsItem"> {this.getRank()} {this.getVote()} <div className="newsItem-itemText"> {this.getTitle()} {this.getSubtext()} </div> </div> ); }</pre>
這里計算時間間距我們使用了 Moment , 如果你要使用你需要安裝并引入它, 或者使用你喜歡的實現方法.
NewItem.css
.newsItem-rank { flex-basis: 25px; font-size: 10pt; text-align: right; }
.newsItem-vote { flex-basis: 15px; text-align: center; }
.newsItem-subtext { font-size: 7pt; }
.newsItem-subtext > a { color: #828282; text-decoration: none; }
.newsItem-subtext > a:hover { text-decoration: underline; }</pre> </li> </ol>
NewsList
上一節中為了測試 NewsItem , 我們定義了一個測試數據 testData , NewsList 中也只有一個 NewsItem , 而真實的情況不會只有一條資訊, 而應該是一組資訊, 每一條對應有一個 NewsItem , 本節中我們來實現這個功能.
首先我們確定傳入的數據是一個數組, 其中每一個元素都是一條資訊, 至于這個數據由哪里傳入, 怎么生成我們先不關心, 但我們可以用 this.props.items 獲取到. NewsList 對于其中的每一個元素都生成一個 NewsItem .
下面是修改完的 render
render() { return ( <div className="newsList"> <NewsHeader /> <div className="newsList-newsItem"> { (this.props.items).map(function(item, index) { return ( <NewsItem key={item.id} item={item} rank={index+1} /> ); }) } </div> </div> ); }
新建樣式 NewsList.css
.newsList { background: #f6f6ef; margin-left: auto; margin-right: auto; width: 85%; }
目前的代碼是沒法運行的, 我們還沒有取得數據傳入給 NewsList , 這將在下一節完善.
Hacker News API
本節中我們使用 Hacker News API 來獲取數據, 具體請參考 API 文檔.
app.js
function get(url) { return Promise.resolve($.ajax(url)); }
get(' function(stories) { return Promise.all(stories.slice(0, 30).map(itemId => get('items 就是處理完后的數據, 一個由資訊數據組成的數組, 我們將它作為屬性傳入 NewsList .
至此, 你已經完成了 Hacker News Front Page, 就像開頭所說的, 這篇教程不會使你精通, 但你應該對 React / Webpack / 模塊化 有了大概的了解.
下一步
現在你已經對 React&Webpack 有了基本的了解了, 下面有一些閱讀資源可以幫助你進一步深入.
至此整個 NewsHeader 就完成了, 你應該能看到如本節初所展示的效果.
NewsItem
如圖, 每條資訊對應著這樣一個 NewsItem , 本節我們將編寫 NewsItem 組件.
可以看到, 一個 NewsItem 包含了資訊的標題, 來源地址, 什么時候發布的以及評論數等等. 它依賴于傳入的數據, 那么怎么傳入數據呢?
對于父子組件間的通信, 可以使用屬性傳遞. 子組件可以使用 this.props 訪問到父組件傳入的屬性數據.
回到 NewsList 組件, 它作為 NewsItem 的父組件可以使用如下方式傳入數據.
<NewsItem item={data} />
NewsItem 中可以使用 this.props.item 訪問 item 屬性.
像這樣我們只需要將資訊數據作為屬性傳入, 在 NewsItem 中就能獲取到了. 讓我們開始做吧!