- 腳手架現(xiàn)已發(fā)布到 NPM,歡迎大家踴躍下載,多提意見。
最近公司的后臺(tái)管理項(xiàng)目,技術(shù)選型的時(shí)候決定采用 react 技術(shù)棧。在開發(fā)之前就想要一個(gè)腳手架,在熱門的腳手架中,create-react-app 的熱替換不太好用,而 react-starter-kit 又太復(fù)雜,下載下來后發(fā)現(xiàn)完全看不懂……于是干脆自己搭建一個(gè),在搭建此腳手架的過程中,也踩了不少坑,特地分享給大家,希望一起學(xué)習(xí)。
本文不會(huì)從零開始講解 webpack 的知識(shí),如果你對(duì) webpack 還不甚了解,可以先了解相關(guān)知識(shí)再來查看本文。
特性
此腳手架有如下一些特性:
- react
- redux
- react-redux
- react-router
- redux-thunk
- hmr
- fetch
- es6
- css modules
- scss
- postcss
安裝
1. npm install build-react-app -g
2. build-react-app <directory>
3. cd <directory>
4. npm install
用法
1. npm run dev 代碼熱替換模式
2. npm run build 生產(chǎn)環(huán)境構(gòu)建
3. npm run serve 在服務(wù)器環(huán)境運(yùn)行構(gòu)建后的代碼
目錄結(jié)構(gòu)
│ .babelrc —— babel 配置文件
│ .npmignore
│ index.html —— 入口頁面
│ package.json
│ webpack.config.js —— webpack 的入口文件
│
├─config —— webpack 的主要配置放在此目錄
│ config.js —— 常用配置文件,如端口等
│ webpack.base.js —— 基礎(chǔ)配置文件
│ webpack.dev.js —— 開發(fā)環(huán)境配置
│ webpack.prod.js —— 生產(chǎn)環(huán)境配置
│
├─src —— 項(xiàng)目源文件
│ │ index.html
│ │ main.js
│ │
│ ├─actions —— 存放所有的 action
│ │ │ index.js
│ │ │
│ │ └─allActions
│ │ counterAction.js
│ │
│ ├─components —— 組件文件夾
│ │ │ App.js
│ │ │
│ │ ├─Counter
│ │ │ Counter.js
│ │ │ index.js
│ │ │
│ │ └─Welcome
│ │ index.js
│ │ style.scss
│ │
│ ├─reducer —— 存放所有的 reducer
│ │ │ index.js
│ │ │ reducers.js
│ │ │
│ │ └─allReducers
│ │ counter-reducer.js
│ │
│ ├─router —— 存放路由
│ │ components.js
│ │ index.js
│ │
│ └─store —— 存放 store
│ index.js
│
└─static —— 靜態(tài)資源文件
common.scss
normalize.scss
上面是腳手架生成的目錄樹,除了 config 目錄,都是與 react 相關(guān)的,不了解 react 也沒關(guān)系,我們主要講的是 webpack 這一塊,也就是 config 目錄中的三個(gè)文件。
思路
由于配置的腳手架功能較多,因此采取將配置文件分離的策略,便于后期的維護(hù)和擴(kuò)展。因此我們將文件分為:
- 主入口文件
- 基礎(chǔ)配置文件
- 開發(fā)環(huán)境配置文件
- 生產(chǎn)環(huán)境配置文件
- 通用配置文件
下面依次進(jìn)行講解。
如何區(qū)分開發(fā)環(huán)境和生產(chǎn)環(huán)境
我們可以采用定義 Node 運(yùn)行時(shí)環(huán)境變量的方法進(jìn)行區(qū)分。在 package.json 定義如下命令:
"scripts": {
"dev": "set NODE_ENV=dev && webpack-dev-server --profile",
"build": "rimraf dist && set NODE_ENV=prod && webpack",
"serve": "http-server ./dist -p 8888 -o"
}
其中 set NODE_ENV=xxx 就是設(shè)置相應(yīng)的環(huán)境變量,以區(qū)分生產(chǎn)和開發(fā)環(huán)境。在開發(fā)環(huán)境設(shè)置為 dev,生產(chǎn)環(huán)境設(shè)置為 prod。
說說其他幾個(gè)命令:
rimraf dist // 在 build 時(shí)先移除舊有的 dist 文件夾,此命令不是必須的
http-server ./dist -p 8888 -o // 查看構(gòu)建后的項(xiàng)目,-o 表示打開瀏覽器
以上兩個(gè)包都需要自行進(jìn)行安裝:
npm install rimraf http-server --save-dev
wbpack.config.js
運(yùn)行 webpack 時(shí),其默認(rèn)的配置文件就是 webpack.config.js,在此腳手架中,它只是一個(gè)入口文件,根據(jù)環(huán)境變量來引入相應(yīng)的配置文件。
下面是 webpack.config.js 的代碼:
// 獲取 NODE 運(yùn)行時(shí)的環(huán)境變量
const env = process.env.NODE_ENV.replace(/(\s*$)|(^\s*)/ig,"");
// 根據(jù)環(huán)境變量引入相應(yīng)的配置文件
const config = require(`./config/webpack.${env}.js`)(env);
// 導(dǎo)出此模塊
module.exports = config;
上面的 config 是一個(gè)函數(shù)調(diào)用的結(jié)果,也就是說 webpack.dev.js 和 webpack.prod.js 兩個(gè)暴露出的是函數(shù)而不是對(duì)象,這樣做的原因是方便對(duì)配置文件進(jìn)行 merge。
webpack.base.js
這是基礎(chǔ)的配置文件,包括了最基礎(chǔ)的功能。
下面是 webpack.base.js 的代碼:
const path = require("path")
const webpack = require("webpack")
// 導(dǎo)入配置文件
const config = require("./config");
const publicPath = config.publicPath;
module.exports = function(env){
return{
// 入口文件:src目錄下的 main.js
entry:{
main:path.resolve(__dirname,"../src/main.js"),
},
// 輸出配置
output: {
// 輸出路徑:dist 目錄
path:path.resolve(__dirname,"../dist"),
// sourceMap 名稱
sourceMapFilename: "[name].map",
// 根據(jù)環(huán)境變量確定輸出文件的名稱
// 如是生產(chǎn)環(huán)境,則文件名中帶有一個(gè)長度為 16 的 hash 值
filename:(env === "dev")?"[name].js":"[name].[hash:16].js",
publicPath,
},
resolve: {
extensions: [".ts", ".js", ".json"],
modules: [path.join(__dirname, "../src"), "node_modules"]
},
module:{
loaders:[
{
test:/\.jsx?$/,
use:["babel-loader"],
exclude:"/node_modules/"
},
{
test: /\.(png|jpg|gif)$/,
use: ["url-loader?limit=20000&name=images/[hash:16].[ext]"],
exclude: "/node_modules/"
},
// 個(gè)人比較偏愛 scss,因此采用 scss 作為樣式文件
{
test: /\.scss$/,
// sass-loader:處理 .scss 后綴的文件
// postcss-loader:對(duì)樣式文件自動(dòng)化處理,如自動(dòng)添加前綴
// css-loader?modules:將 scss 文件轉(zhuǎn)換成 css 文件,并開啟模塊化
use: ["style-loader","css-loader?modules","postcss-loader","sass-loader"],
// 我們只想模塊化 src 中的 scss 文件,并不想對(duì)所有的樣式文件進(jìn)行模塊化
exclude: ["/node_modules/",path.resolve(__dirname,"../static")]
},
{
test: /\.scss$/,
// 對(duì) static 目錄中的 scss 文件進(jìn)行處理,我們不想此文件中的樣式文件被模塊化
use: ["style-loader","css-loader","postcss-loader","sass-loader"],
// 該處理只包含 static 目錄中的文件
include: [path.resolve(__dirname,"../static")]
},
],
},
}
}
由于項(xiàng)目中可能存在兩種樣式文件:組件本身的樣式和外部的樣式文件(或公共樣式文件),我們?cè)谶M(jìn)行 CSS 模塊化的時(shí)候,可以選擇只模塊化 src 目錄中的文件,而 static 目錄中的文件不進(jìn)行模塊化,用來存放一些公共的樣式。因此上面對(duì) scss 文件進(jìn)行了兩次處理的原因就在這里。
webpack.dev.js
此文件用來存放開發(fā)環(huán)境的配置文件。
下面是 webpack.dev.js 的代碼:
const path = require("path");
const webpack = require("webpack")
// 此插件用來合并 webpack 配置
const webpackMerge = require("webpack-merge");
// 打開瀏覽器插件
const OpenBrowserPlugin = require("open-browser-webpack-plugin");
// postcss 的自動(dòng)補(bǔ)全插件
const autoprefixer = require("autoprefixer");
const precss = require("precss");
// 引入基礎(chǔ)配置文件
const baseConfig = require("./webpack.base.js");
const config = require("./config");
const port = config.port;
module.exports = function(env){
console.log(`
#################################################
Server is listening at: http://localhost:${config.port}
#################################################
`);
// 合并文件
return webpackMerge(baseConfig(env),{
entry:[
"react-hot-loader/patch",
"webpack-dev-server/client?http://localhost:" + port,
"webpack/hot/only-dev-server",
path.resolve(__dirname,"../src/main.js"),
],
devtool: "cheap-module-source-map",
plugins:[
// 開啟熱替換
new webpack.HotModuleReplacementPlugin(),
// 編譯完成后自動(dòng)打開瀏覽器
new OpenBrowserPlugin({ url: "http://localhost:" + port }),
// 配置 postcss
new webpack.LoaderOptionsPlugin({
options:{
postcss(){
return[precss, autoprefixer];
}
}
})
],
// 配置 webpack-dev-server
devServer:{
hot:true,
port:config.port,
historyApiFallback:true,
}
})
}
webpack-merge 插件用來合并 webpack 的配置,我們這里將 webpack.base.js 和新定義的配置進(jìn)行合并。
webpack.prod.js
此文件用來存放生產(chǎn)環(huán)境的配置文件。
下面是 webpack.prod.js 的代碼:
const path = require("path");
const webpack = require("webpack");
const webpackMerge = require("webpack-merge");
// 抽取公共代碼
const ExtractTextPlugin = require("extract-text-webpack-plugin");
// HTML 模板插件
const HTMLWebpackPlugin = require("html-webpack-plugin");
const autoprefixer = require("autoprefixer");
const precss = require("precss");
const baseConfig = require("./webpack.base.js");
const config = require("./config.js");
// 項(xiàng)目中用到的第三方庫
const vendor = config.vendor;
module.exports = function(env){
return webpackMerge(baseConfig(env),{
entry:{
main:path.resolve(__dirname,"../src/main.js"),
vendor,
},
module:{
rules:[
{
test:/\.jsx?$/,
use:["babel-loader"],
exclude:"/node_modules/"
},
{
test: /\.(png|jpg|gif)$/,
use: ["url-loader?limit=20000&name=images/[hash:16].[ext]"],
exclude: "/node_modules/"
},
// 抽取 css 文件,使用 extract-text-webpack-plugin 插件完成
// 配置方式和 webpack.base.js 中相似,即在抽取過程中只對(duì) src 中的樣式文件進(jìn)行模塊化
{
test: /\.s?css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: [
"css-loader?minimize&modules&importLoaders=1&localIdentName=[name]_[local]_[hash:base64:5]",
"sass-loader",
"postcss-loader"
]
}),
exclude: ["/node_modules/",path.resolve(__dirname,"../static")]
},
{
test: /\.s?css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: [
"css-loader?minimize",
"sass-loader",
"postcss-loader"
]
}),
include: [path.resolve(__dirname,"../static")]
}
],
},
plugins:[
// 壓縮 js,以及一些選項(xiàng)
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true,
conditionals: true,
unused: true,
comparisons: true,
sequences: true,
dead_code: true,
evaluate: true,
if_return: true,
join_vars: true,
},
output: {
comments: false,
},
}),
// 定義抽取出的 css 的文件名
new ExtractTextPlugin({
filename:"style.[contenthash:16].css",
disable:false,
allChunks:true,
}),
// 自動(dòng)生成 HTML 文件,需要指定一個(gè)模板
new HTMLWebpackPlugin({
template:"src/index.html"
}),
// 抽取公共的庫
new webpack.optimize.CommonsChunkPlugin({
name: ["vendor","manifest"]
}),
// 定義環(huán)境變量
// 在打包 react 時(shí)會(huì)根據(jù)此環(huán)境變量進(jìn)行優(yōu)化
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("production")
}
}),
new webpack.LoaderOptionsPlugin({
options:{
postcss(){
return[precss, autoprefixer];
},
sassLoader: {
sourceMap: true
},
}
})
]
})
}
config.js
此文件主要用來存放公共的配置,如公共的第三方庫,端口,publicPath 等,方便管理。
以下是該文件的代碼:
module.exports = {
port:8080,
vendor:[
"react",
"react-dom",
"react-hot-loader",
"react-router",
"redux",
"react-redux",
"prop-types",
"isomorphic-fetch",
"es6-promise",
"redux-thunk",
"classnames",
],
publicPath:"/",
}
總結(jié)
當(dāng)你學(xué)會(huì)自己搭建腳手架,了解了腳手架的基本套路后,就可以定制各種各樣的配置了,根據(jù)自己的需求隨意搭配。再也不用在開發(fā)前為選用腳手架而煩惱了,自己搭過一次后,即使采用別人的腳手架,在出現(xiàn)問題后也可以從容解決。
參考資料
完。