搭建基于 webpack2 的 react 腳手架

  • 腳手架現(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)問題后也可以從容解決。

參考資料

完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容