前言
搭一個腳手架真不是一件容易的事,之前為了學習webpack是怎么配置的選擇自己搭建開發環境,折騰了好幾天總算對入口文件、打包輸出、JSX, es6編譯成es5、css加載、壓縮代碼等這些基礎的東西有了一個大體的了解。后來有一次組內分享技術,我作死的選擇了webpack,為了看起來高大上又去折騰它的按需加載、怎樣處理第三方插件、拆分CSS文件、利用Happypack實現多進程打包等等。徹底把自己搞暈了。再后來接手了一個緊急的項目,實在來不及去折騰webpack了,就選擇使用react官方推薦的腳手架create-react-app,這個腳手架確實搭的非常完善,幾乎不需要我們修改配置,我也研究了一下它的配置,準備從零開始搭建一個react+webpack的開發環境,配置從簡單到復雜,由于內容較多,我將分為幾篇文章講述,這是第一篇。
另外,熱更新我單獨寫成一篇文章了,當你修改一次代碼就需要手動啟動服務器,然后你煩了的時候,你可以先去把熱更新配置好再回來繼續:開始一個React項目(二) 徹底弄懂webpack-dev-server的熱更新
------------------------- start 2017/12/25更 --------------------------
Yarn自稱是比npm快十倍的包管理工具,并且還有一些很贊的特性,應該是2017年前端的一個驚喜,根據我的親身體驗,真的是快到飛起,所以強烈建議各位小伙伴也用起來吧!使用方式很簡單,用你的npm 安裝它:
npm install yarn -g
然后切換為淘寶源你才能感受到速度:
yarn config set registry https://registry.npm.taobao.org --global
yarn config set disturl https://npm.taobao.org/dist --global
好了,真的很簡單,它的命令與npm幾乎一樣:
- 初始化:
yarn init
- 安裝一個包:
yarn add 包名
- 更新一個包:
yarn upgrade 包名
- 刪除一個包:
yarn remove 包名
- 安裝所有包:
yarn
或者yarn install
Yarn是沒有全局安裝的,所以安裝還是用npm。
另外,Yarn的包依賴也是寫在package.json文件里的,所以你可以在已經使用npm的項目里使用Yarn。
我在配置這個項目的時候用的是npm,因為我懶所以我就不改啦。
------------------------- end 2017/12/25更 --------------------------
初始化
先貼出項目結構
my-app/
|
--- README.md
--- package.json
--- webpack.config.js
--- public/
|
--- index.html(模板文件)
--- favicon.ico(網站圖標)
--- src/(項目文件都在這里)
|
--- index.js(入口文件)
--- pages/ (頁面)
--- components/(抽離的公用組件)
--- css/
--- js/
--- images/
一開始最重要的需要你建好的文件是public/index.html
和src/index.js
。
新建一個項目,使用npm init
初始化生成一個package.json文件。可以全部回車,后面反正是可以修改的。
安裝webpack: npm install webpack --save-dev
全局安裝: npm install webpack -g
(全局安裝以后才可以直接在命令行使用webpack)
一個最簡單的webpack.config.js文件可以只有entry(入口文件)和output(打包輸出路徑)
新建webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js', //相對路徑
output: {
path: path.resolve(__dirname, 'build'), //打包文件的輸出路徑
filename: 'bundle.js' //打包文件名
}
}
新建入口文件 src/index.js
function hello() {
console.log('hello world');
}
好了這就夠了,我們已經可以運行這個項目了,打開命令窗口試一下:webpack
編譯成功了,項目根目錄下已經生成好build/bundle.js文件了,bundle.js文件前面的幾十行代碼其實就是webpack對怎么加載文件,怎么處理文件依賴做的一個聲明。
我們可以將啟動wepback的命令寫到package.json中并添加一些有用的參數:
package.json
"scripts": {
"start": "webpack --progress --watch --hot"
},
progress
是顯示打包進程,watch
是監聽文件變化,hot
是啟動熱加載,更多命令行參數詳見:webpack cli
以后只需要執行npm start
就可以了。
添加模板文件index.html
配置react項目最重要的兩個文件是入口文件(這里是src/index.js)和html模板文件(這里是public/index.html),入口文件是整個項目開始運行的地方,模板文件是構建DOM樹的地方,相信有一部分小伙伴在網上看到的教程是直接在打包路徑build里面建一個index.html,然后手動或者使用html-webpack-plugin
將打包后的js引入,這樣存在一個問題是build本身是打包路徑,而這個路徑的所有文件都不應該是我們手動去添加的,甚至包括這個文件夾也不是我們事先建好的。所以最好是按照create-react-app
的方式,將這類不需要被webpack編譯的文件放到public路徑下。
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
現在要讓webpack知道這就是我們的html入口文件,并且我們不需要手動引入打包后的js文件,需要安裝html-webpack-plugin
:
npm install html-webpack-plugin --save-dev
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html', //指定模板路徑
filename: 'index.html', //指定文件名
})
]
}
plugins是用于擴展webpack功能的,它幫助webpack構建項目,比如上面的
html-webpack-plugin
自動生成模板文件,以及后面用到的分離CSS等。
這里提示一下生成的HTML路徑就是output.path
指定的路徑,不僅如此,像extract-text-webpack-plugin
分離CSS文件打包的文件路徑也是這個路徑。
重新運行一下:npm start
現在可以看到build路徑下已經生成好了一個index.html文件,并且這個文件已經引入了bundle.js文件了。
開始React項目
安裝: npm install react react-dom --save-dev
,現在來修改我們的入口文件
src/index.js
import React, { Component } from 'react';
import ReactDom from 'react-dom';
class App extends Component {
render() {
return <h1> Hello, world! </h1>
}
}
ReactDom.render(
<App />,
document.getElementById('root')
)
別著急運行,react里面的JSX語法普通瀏覽器可解析不了,需要安裝babel來解析:
npm install babel babel-cli babel-loader --save-dev
再安裝兩個分別用于解析es6和jsx語法的插件:
npm install babel-preset-env babel-preset-react --save-dev
注:以前編譯es6以上語法用的是
babel-preset-es2015
,現在是時候說再見了,babel-preset-env
是一個更定制化的插件,你可以指定你要兼容的瀏覽器版本,這樣babel會選擇編譯該版本不支持的語法而不是全部編譯成舊的語法,具體配置參見:babel-preset-env
webpack.config.js
module.exports = {
...
module: {
loaders: [ //配置加載器
{
test: /\.js$/, //配置要處理的文件格式,一般使用正則表達式匹配
loader: 'babel-loader', //使用的加載器名稱
query: { //babel的配置參數,可以寫在.babelrc文件里也可以寫在這里
presets: ['env', 'react']
}
}
]
}
}
webpack最重要的配置都在modules(模塊)里,loaders(加載器)是處理源文件的,后面你會看到,loader可以處理不同的js(jsx, es6等)編譯成js,less等編譯成css,將項目中引用的圖片等靜態資源路徑處理成打包以后可以正確識別的路徑等。
現在試著運行一下,沒有報錯的話,直接雙擊打開build/index.html
就可以看到hello world!
了。
加載和解析CSS樣式
我們以前寫CSS大致是兩種方式,一寫在html里,二寫在CSS文件里。現在我們沒有html只有JSX了,JSX通俗一點理解就是可以在js里面寫html,所以我們如果要在jsx里面寫css,就是在js里面寫css,寫過RN的小伙伴應該對這種寫法比較熟悉。
//方式一:
const styles = {
container: {
textAlign: 'center',
marginTop: '50px'
},
containerBtn: {
margin: '0 20px',
backgroundColor: '#dde18e'
}
}
//使用:
<div style={styles.container}>
<button style={styles.containerBtn}>count+1</button>
</div>
//方式二:
<div style={{textAlign: 'center', marginTop: '50px'}}>
</div>
而如果想在JSX文件里面像我們以前的用法一樣去引入CSS文件,就只能使用import語句,但是import引入的都會被當做js處理,所以,我們需要對css文件做一個轉換。這就需要css-loader
和style-loader
,其中css-loader
用于將css轉換成對象,而style-loader
則將解析后的樣式嵌入js代碼。
安裝:npm install style-loader css-loader --save
webpack.config.js:
loaders: [
{
test: /\.css/,
loader: 'style-loader!css-loader'
},
]
使用方式三:
//index.css
.container {
text-align: center;
margin-top: 40px;
}
//index.js
import '../css/index.css
<div className="container">
</div>
需要注意用className
而不是class
。
單獨編譯CSS文件(只在生產環境配置)
一般發布到線上以后,為了加載速度更快會把CSS和JS打包到不同的文件中,使用extract-text-webpack-plugin
插件可以分離CSS。而其實,開發的時候是不需要單獨編譯CSS文件的。如果你在開發環境加了這個,又配置了熱更新,那么你會發現CSS發生變化時熱更新用不了了,所以建議開發環境就不要配置這個了。
npm install extract-text-webpack-plugin --save
webpack.config.js
文件:
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');
module.exports = {
//...
module: {
loaders: [
{
test: /\.css/,
use: ExtractTextWebpackPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html'
}),
new ExtractTextWebpackPlugin("bundle.css")
],
};
使用PostCSS或者Less, Sass等CSS工具
Less或Sass想必大家都非常熟悉了,PostCSS可能有的小伙伴沒有用過,我也是在create-react-app
的配置里第一次見到,然后就去搜了一下,發現它挺強大的。它不是什么預處理后處理語言,而是一個平臺,這就像Node一直宣稱的那樣:我是平臺!我是平臺!我是平臺!既然是個平臺,那我們就可以在平臺上做很多事情,比如說:檢查CSS(像eslint檢查js那樣)、添加瀏覽器前綴(該平臺目前最火的插件)、使用未來的CSS語法(大概就像現在的花唄??)、函數式語法(類似Sass語法)等等。目前像阿里爸爸,維基百科等都在使用它。我覺得雖然官方介紹了很多插件,但我們用其中的幾個就可以了。
之前寫過Sass或Less的童鞋估計會問:既然它是個平臺那我可以在它上面寫Sass(Less)嗎?答案是可以的,另外,它也有一個類似于Sass語法的插件,在它上面配置起來非常容易,所以,怎么選擇看你咯。
安裝:npm install postcss-loader --save
安裝一些你需要的工具:npm install autoprefixer precss postcss-flexbugs-fixes --save
注:autoprefixer是自動添加瀏覽器前綴的插件,precss是類似Sass語法的css插件,postcss-flexbugs-fixes是修復了flex布局bug的插件,具體會有哪些bug你可以自行查看。
webpack.config.js
文件:
{
test: /\.css$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
}
},
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer'),
require('precss'),
require('postcss-flexbugs-fixes')
]
}
}
]
},
測一下配置生效了沒有
src/css/style.css
:
$mainColor: #8ce7b4;
$fontSize: 1rem;
@keyframes rotate {
0% {transform: rotate(0deg);left:0px;}
100% {transform: rotate(360deg);left:0px;}
}
button {
background: $mainColor;
font-size: calc(1.5 * $fontSize);
}
.container .logo {
animation: rotate 10s linear 0s infinite;
}
可以看到已經自動幫我們添加了前綴以及可以使用sass語法了,而且這是在css文件里直接寫,不需要建其他后綴的文件。
如果你非要用less或者sass,也可以,但我還是會建議你保留postcss,畢竟它有很多有用的插件,只是去掉precss即可。安裝:
npm less less-loader --save
{
test: /\.(css|less)$/,
use: [
//...
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer'),
require('postcss-flexbugs-fixes')
]
}
},
{
loader: 'less-loader',
}
]
},
好了,現在你可以寫less了。
加載圖片資源
我們知道webpack打包會將所有模塊打包成一個文件,而我們在開發項目時引入圖片資源的時候是相對于當前文件的路徑,打包以后這個路徑是錯誤的路徑,會導致引入圖片失敗,所以我們需要一個處理靜態資源的加載器,url-loader和file-loader。我看到網上說url-loader是包含了file-loader的功能的,所以我們只需要下載url-loader就可以了,但是我下載完以后它卻提示我url-loader依賴file-loader,并且運行項目會報錯,所以我又下載了file-loader。url-loader有一個功能是對于指定大小以下的圖片會轉成base64,這樣可以有效減少http請求。
安裝:npm install file-loader url-loader --save
webpack.config.js
{
test: [/\.gif$/, /\.jpe?g$/, /\.png$/],
loader: 'url-loader',
options: {
limit: 10000, //1w字節以下大小的圖片會自動轉成base64
},
}
使用圖片有兩種情況,一在CSS里面設置背景,二在JSX里面設置背景或<src>
,
CSS里面和以前的使用方式一樣,假如你的目錄結構長這樣:
src
|
---pages/
--- css/
--- images/
那么在CSS文件里引入背景圖的路徑就為:
.container {
background: url('../images/typescript.jpeg') no-repeat center / contain;
}
在JSX里面引入圖片有幾種方式:(該頁面在pages/下)
//方式一:
import tsIcon from '../images/typescript.jpeg';
//方式二:
const tsIcon = require('../images/typescript.jpeg');
//使用方式一:
<img src={tsIcon} alt="" />
//使用方式二:
<div style={{background: `url(${tsIcon}) no-repeat`}}></div>
//使用方式三:
const styles = {
test: {
background: `url(${tsIcon}) no-repeat center`
}
}
render() {
return (
<div style={styles.test}></div>
)
}
另外,你也可以測試一些小的icon,看看是不是轉換成了很長的一串字符串。
配置ESLint
js的松散真的是體現在方方面面,現在除了有TypeScript這種一心想用C#風格幫助js走上人生巔峰的語言,也有ESLint這種控制規則從娃娃抓起的工具,ESLint的創始人可是js紅皮書的作者,所以,趕緊用起來吧,這樣你就完全不必和隊友爭論到底Tab鍵設置為4還是為2,加不加分號等一系列問題啦。
安裝:npm install eslint eslint-plugin-react eslint-loader --save
注:可以全局安裝eslint,這樣你才可以直接在命令行使用
eslint xxx
,如果選擇全局安裝eslint那么你使用的任何插件或可分享的配置也都必須在全局安裝。如果選擇本地安裝,命令行使用應該是./node_modules/.bin/eslint xxx
eslint-plugin-react是檢查react項目的插件,eslint-loader是webpack需要的加載器插件。
webpack.config.js
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['env', 'react'], //babel編譯es6以上語法以及jsx語法
plugins: ["react-hot-loader/babel"]
}
},
{
test: /\.js$/,
enforce: 'pre', //加載器的執行順序,不設置為正常執行,pre(前)|post(后),eslint是檢查代碼規范,應該在編譯前就執行
loader: 'eslint-loader',
},
]
接下來需要配置ESLint規則,為了滿足大家“稀奇古怪”的代碼風格,ESLint遵循各種規則自定義的原則,所以,前面我說有了它就可以避免因代碼風格不同而與隊友發生爭執的問題是不準確的,因為,我們還是要制定規則啊。對于我等小菜鳥來說,遵守別人的規則會比自己制定好一些,因為,怕你對自己太好了。
我們先來看看如何配置吧,采用命令行來初始化:eslint --init
,如果是本地安裝的話:./node_modules/.bin/eslint --init
遇到的第一個問題:你喜歡怎么配置你的ESLint呢?
- 根據你喜歡的方式來制定規則,你需要回答一些問題。
- 使用當下流行的代碼規則。
- 根據你的js文件生成一些規則。
使用當下流行的代碼規則就是用大公司制定的一套規則啦,這里只有三個選項:
如果你都不喜歡,沒關系,這里有很多款式供你選擇,比如eslint-config-react-app就是create-react-app
的代碼規則。據說目前最常用的是Airbnb,它也被稱為史上最嚴規則,選擇這個回車,接著回答問題:是否使用React,希望生成的eslint文件格式是什么,我選擇的是javascript,Airbnb需要安裝一些插件,耐心等待就好。
好了,運行一下代碼,沒有意外的話你的代碼肯定有很多報錯,反正我一共就兩js文件,加起來七十多行代碼,報了九十多個錯,哈哈哈哈,讓我先冷靜冷靜換個姿勢,選擇自定義規則,別誤會,我是打算另外寫一篇文章專門來解決我那九十多行的報錯的,寫在這里篇幅有點太長了。
現在選擇第一種方式,自定義代碼規則,ESLint附帶了一些默認規則幫你起步,具體查看默認規則列表,打勾的表示已經啟用的規則,然后另外還需要你回答一些問題:
這里有幾個要注意的選項:
注意事項一:indentation(縮進)是用tab還是空格
意思不是你縮進的時候是按空格鍵縮進還是tab鍵縮進,或者說很多時候縮進是編輯器做的事情,我們要做的是告訴編輯器是用哪種方式,而怎么看的出來用的是什么呢?以sublime編輯器為例,當你選中代碼的時候,縮進的樣式就出來了:
其中,..是空格,—是tab,如何修改文件的縮進風格呢?sublime編輯器是選擇View--> Indentation,首先,你可以選擇tab的寬度是多少,一般是2,4,然后,如果你想修改當前已有的文件縮進風格,選擇 Convert Indentation to Tabs或者 Convert Indentation to Spaces,這樣,整個文件的縮進風格就統一了,并且,以后你新建的文件也會按照這個風格,但是!你已有的文件風格不會變,你需要手動去每個文件下修改。所以,配置這種檢查工具肯定是越早越好,等你寫了一大半了再跑回來配置看到幾千個錯誤都是很正常的,而這時候,估計你會選擇放棄。
注意事項二:line endings 選擇
了解Windows和Unix系統的童鞋都知道,這倆系統的行尾結束符不一樣,Unix的行尾是兩個字符:"\r\n",Windows的行尾是一個字符:"\n",而如果假如你在mac里寫代碼這里卻選擇了Windows,你就會看見Expected linebreaks to be 'CRLF' but found 'LF'
,同理反過來也一樣,所以不要選錯了。
注意事項三:semicolons(分號)的選擇
分號在js里面實在是個很隨意的東西,有的人喜歡打,有的人不喜歡打,有的人喜歡打一部分,但是這個選擇卻引起了很多爭議,有人認為,雖然js有自動分號插入 (ASI)的功能,但總是依賴js去幫我們打分號是不可靠的,首先,js也會累是吧,其次,有些地方js也不知道該打在哪里,然后它就會亂打,然后你懂的,你就會被噴。有人認為,不打分號可以節省編譯時間,而且,看起來很棒。然后就有一群和事佬跳出來說:我可以在可能會引起js困惑的地方打分號,在js可以自動引入分號的地方省略,嗯,應該說我們大部分人都屬于這一類人。
在react里面,如果選擇了始終使用分號就會有一些比較困惑的地方,比如:
add() {
this.setState((preState) => {
return{
count: preState.count + 1
}
})
}
render() {
return (
<div>hello </div>
)
}
那么,ESLint會報return{}后缺少分號,this.setState({})后缺少分號,甚至render里的return()后也缺少分號,而這應該是大部分人都不會選擇加分號的地方,所以,始終使用分號這個選項在react項目里恐怕不是那么適用。不過,你是自由的,你的代碼風格由你決定。
現在你應該有能力解決Missing semicolon (或Extra semicolon )
,Expected indentation of 1 tab but found 4 spaces
,Mixed spaces and tabs
等這種問題了。
但是ESLint依然對react非常不友好啊,比如:'React' is defined but never used
,或者是引入其他組件也會報這個警告,當然,引入了又沒有用或者根本沒有引入某個組件,報了警告是非常正常的,但是我們明明用到了引入的東西它還是報警告這就說不過去了,修改.eslintrc.js文件:
"extends": [
"eslint:recommended",
"plugin:react/recommended" //增加
],
這絕對是最萬能的解決辦法!!相信大家以前用過這種寫法:
"rules": {
//...
"react/jsx-uses-react": 1,
}
但這個只能解決'React' is defined but never used
這個錯誤,如果是引入其他組件比如import Home from './pages/Home'
依然會報'Home' is defined but never used
,而這時候你還需要添加一句:"no-unused-vars": 1
才可以,所以用第一種方法是最好的。
------------------------- start 2017/12/25更 --------------------------
箭頭函數及綁定this
React里面函數綁定this有三種方式:
//方式一
<button onClick={() => this.add()}>count+1</button>
//方式二
<button onClick={this.add.bind(this)}>count+1</button>
//方式三,官方推薦使用
constructor(props) {
super(props)
this.add = this.add.bind(this);
}
<button onClick={this.add)}>count+1</button>
最近我又看到了一種新寫法:
add = () => {
this.setState((preState) => {
return{
count: preState.count + 1
}
})
}
<button onClick={this.add)}>count+1</button>
直接將綁定的函數定義成箭頭函數,就無需綁定this了。這幾種方法略有區別,這里我就不展開了。然后要注意的是,直接這樣寫是會報錯的,我查了一下,因為這個還處于草案階段,也就是stage-2,不過不用擔心,據可靠消息,進入到這個階段的提案最后成為標準幾乎是板上釘釘的事情了,我們現在讓babel可以認識這種寫法:
yarn add babel-preset-stage-2
修改webpack.config.js
module.exports = {
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['env', 'react', 'stage-2'], //babel編譯es6以上語法以及jsx語法
}
},
}
------------------------- end 2017/12/25更 --------------------------
------------------------- start 2017/12/28更 --------------------------
使用async報錯:Uncaught ReferenceError: regeneratorRuntime is not defined
異步函數又新增一員大軍:async和await,寫法及其簡潔:
async asyncAdd () {
let aaa = await this.asyncAddAchieve();
console.log('hello, 我是異步后面的console.log')
}
asyncAddAchieve = () => {
setTimeout(() => {
console.log('hello, 我是異步里面的東西')
this.setState((preState) => {
return{
count: preState.count - 1
}
})
}, 2000)
}
然而普通的babel拯救不了它,會報Uncaught ReferenceError: regeneratorRuntime is not defined
錯,很奇怪的是,這是進入stage-3階段的提案,我安裝了stage-2都沒用,后來找到了解決辦法就是安裝babel-polyfill,polyfill是個比較強大的工具,又興趣的小伙伴可以去了解一下,安裝:
yarn add babel-polyfill
接著配置一下:
//webpack.config.js
entry: ['babel-polyfill', path.resolve(__dirname, './src/index.js')],
------------------------- end 2017/12/28更 --------------------------
寫在最后
我把該項目放在github上了。雖然這是webpack配置的第一篇,但是為了開發方便,我把webpack-dev-server的熱更新配置也放進來了,如果你對熱更新有疑問可以閱讀開始一個React項目(二) 徹底弄懂webpack-dev-server的熱更新。