一鍋燴了那些React項目開發繞不過去的配置

寫 React 已經有一段時間了,從開始到現在都是一路摸爬滾打過來的,踩過不少坑。雖然學習 React 已經很久了,但是和真正寫還是有很大區別的,今天就從頭開始記錄下使用過程的點點滴滴,權且當作學習筆記吧,也希望能給將要入坑的小伙伴一點點啟示吧!

前言

習慣了使用Vue開發的小伙伴,如果突然切換到React技術棧,剛開始肯定會有些手足無措,因為相比Vue的腳手架Vue-cliReact官方提供的腳手架工具create-react-app則顯得沒有那么友好。在Vue-cli中,如果想要擴展webpack配置,只需要在項目的根目錄新建一個vue.config.js文件,在該文件中進行配置即可。但是create-react-app則沒有那么簡單,它把所有的webpack配置都集成在了react-scripts的依賴包中,我們無法直接修改的,官方提供了一個npm run eject命令可以執行腳本將react-scripts配置釋放到本地,來讓我們進行配置,但這種方式也會有缺陷,具體后面會詳細說明,同時,社區也提供了一種方案,可以使用react-app-rewired包來重寫webpack配置。接下來我們就詳細來說明這兩種方式,幫你一舉掃清環境配置的各種障礙。

使用create-react-app初始化項目

  • 創建項目
npx create-react-app react-demo
  • 啟動項目
cd react-demo
npm run start

至此,一個簡單的react項目就創建和啟動完畢了。

扒一扒create-react-app背后的東西

通過項目創建大概有小伙伴會說這比vue-cli創建vue項目簡單,但是兩個命令背后發生了什么應該也會有疑問,接下來我們就一點點扒開它的真實面目。

兩個疑問

1、為什么使用npx創建項目?

在使用create-react-app創建項目時,通常是使用如下命令:

// 全局安裝create-react-app
npm install create-react-app -g
// 使用create-react-app創建項目
create-react-app react-demo

但是我們這里為什么使用了 npx 就可以直接創建項目呢 ?
那就要從認識npx說起了。
npxnpm提供的一個命令,它可以不用全局安裝包文件,就可以執行命令,在使用npx時它會先從下載一個臨時包文件,等使用完成之后就會自動刪除,以避免額外的占用磁盤空間。

2、執行npm run start后發生了什么?

相比很多的小伙伴大概也會跟我一樣,對于執行一個命令就啟動起來項目了,這背后到底發生了什么很好奇?那么現在我們就來一起一探究竟。

首先,查看項目根目錄的package.json文件:

{
  "name": "react-demo",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-scripts": "4.0.1",
    "web-vitals": "^0.2.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": ["react-app", "react-app/jest"]
  },
  "browserslist": {
    "production": [">0.2%", "not dead", "not op_mini all"],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

查看package.json中的scripts配置,我們知道了,執行npm run start命令其實是執行了react-scripts start,那么react-scripts又是何方圣神呢?
其實npm執行的命令默認都會去項目的node_modules下面.bin目錄下找可執行腳本。

其中react-scripts.cmdwindows系統下的可執行命令,react-scriptlinux環境下的命令。扒開文件內容我們可以看到:

@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe"  "%~dp0\..\react-scripts\bin\react-scripts.js" %*
) ELSE (
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  node  "%~dp0\..\react-scripts\bin\react-scripts.js" %*
)

通過上面命令我們就知道了腳手架的webpack配置入口文件是react-scripts包中bin目錄下的react-scripts.js文件。

react-scripts-package.png

然后在入口文件中又根據執行命令(參數)不同加載scripts目錄下不同的文件。

entry.png

由入口文件可知,如果執行的是npm run start就會執行scripts/start.js文件。

webpack.config.png

start.js文件中引入了config目錄下webpack.config.js文件,那么這個文件就是項目腳手架中核心的webpack配置文件了。

扒到這里,大概大家就明白了,服務是如何啟動的了,以及腳手架背后的webpack是如何配置的了。

那么,接下來我們就來說一說在日常項目中我們通常都怎樣在腳手架的基礎上自定義webpack配置的。

自定義webpack配置

通常,腳手架生成的默認配置是很難滿足我們項目需要的,因此自定義配置項目的webpack是我們無法避免的。接下來我就介紹兩種自定義webpack的方式。以及日常開發中常用到的配置。

一、使用eject,釋放默認webpack配置

package.json文件的scripts命令中,有一個npm run eject的命令,這個是create-react-app官方提供了一個可以釋放默認webpack的命令。

執行npm run eject命令會將react-scripts釋放到本地項目中,可以通過修改對應的文件完成配置。同時會刪除react-scripts依賴包,修改package.json中命令。

eject.png

命令執行完畢以后,我們就會看到本地項目中多出了scriptsconfig兩個目錄文件。同時,package.json文件命令也被修改了。

"scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js"
}

此時,如果在執行啟動或者打包命令,就是直接執行的scripts目錄下對應的文件了。我們可以很方便的根據需要修改對應的文件配置即可。

但值得注意的是,npm run eject命令是不可逆的,即執行之后不可恢復,這就造成了如果后續我們想通過腳手架的react-scripts包增加新的特性,比如PWA支持,是不可行的,因此,請根據需要謹慎釋放。

說完如何釋放腳手架的webpack配置,接下來我們就來說一下如何通過釋放默認配置的方式進行一些常用的自定義配置

添加less支持

  • 安裝lessless-loader
npm install less less-loader
  • config/webpack.config.js文件中做如下配置:

1、添加匹配.less.module.less結尾的文件的正則

less-regex.png

2、在modulerules中配置less-loader規則

less-loader.png

至此,對less文件的支持已經配置好了,在項目中創建less樣式文件并引入,就可以生效了。

  • 拓展

    看了上面的配置,可能有些人會有疑問:

    為什么要在配置中匹配.module.less文件,并進行單獨的解析?

    這是因為在create-react-app創建的項目中,組件中的樣式沒有作用域隔離的概念,不像是vue中給style添加scoped屬性,樣式文件只針對當前組件有效,不會影響到子組件。所以在react項目中如果不進行處理很容易造成全局樣式污染的問題。

    針對此,通常有兩種解決方案:

    1、每個組件在最外層標簽上都定義一個唯一的類名,所有的樣式寫在這個類下面。但是該方式當項目大了,參入人員多了就很難把控。

    2、定義組件樣式文件為組件名.module.less的形式,并以模塊的方式引入。

    style.module.png

解析到頁面的時候就會給對應標簽添加一個唯一標志的類名,這樣就可以有效的避免樣式污染問題。


style.page.png

按需引入 Antd 組件

antd組件是使用React技術棧使用最為普遍的 UI 組件庫,如果使用css書寫樣式,那么按照官方文檔配置使用即可:

antd-css.png

但是,如果我們使用less作為樣式書寫方式,使用上面的方式,antd的樣式文件會不生效,如果將在App.less頂部引入的antd樣式文件改為antd.less則會報錯,因為在antddist目錄下根本就找不到antd.less文件。

那么,我們就需要借助babel-plugin-import這個加插件了,使用這個插件在按需引入組件的時候會自動引入.js.css|.less文件。

babel-import.png

接下來,就需要安裝和配置babel-plugin-import

  • 安裝
npm install babel-plugin-import
  • 配置

package.json文件中配置babel:

"babel": {
    "presets": [
      "react-app"
    ],
    "plugins": [["import",{
        "libraryName": "antd",
        "libraryDirectory": "es",
        "style": true
    }]]
  }

或者在根目錄創建.babelrl文件,配置:

// .babelrl
{
  "babel": {
    "presets": [
      "react-app"
    ],
    "plugins": [
      ["import", {
        "libraryName": "antd",
        "libraryDirectory": "es",
        // css
        // "style": "css"
        //less
        "style": true
      }]
    ]
}

以上兩種配置babel的方式取一種即可。

另外需要提及一下的是,以上配置style取值不同,加載的樣式文件類型也不一樣。

babel-import-style.png

  • 踩坑

    配置style: true時,如果使用lessless-loader較高版本時會報錯,在一番嘗試后,將版本降級即可。我這里使用如下版本:

  "less": "2.7.3",
  "less-loader": "5.0.0",

當然,經過探索,使用高版本的less-loader也是可以的,但是less的版本要匹配,我這里使用了less-loader最新版本7.1.0,通過查看less-loader配置文件,發現依賴的less版本是3.5.0,因此我將less版本降為了對應版本??晒膺@樣還是不夠,還需要對webpackless-loader進行對應的配置。

less-loader-config.png

自定義 Antd 主題

搞定上面的less-loader配置以后,自定義主題就非常容易了。antd組件樣式是使用less開發的,我們只需要使用less提供的 modifyVars 的方式進行覆蓋變量即可。

less-loader-theme.png

配置跨域

跨域是前后端分離的開發方式繞不過去的問題,因此也是我們進行項目開發時首先要考慮的,React開發的跨域配置有別于Vue,下面我們就來詳細的看看。
通過閱讀create-react-app官方文檔可知,有兩種方式可以配置跨域:

1、在package.json中,proxy字段進行配置:

"proxy": "http://192.168.0.0/api"

這時,如果訪問后端接口本地的localhost就會被代理到http://192.168.0.0/api上。

但是,這種方式存在一個缺陷,如果你想同時訪問多臺服務器這種方式就沒辦法滿足了,類似如下配置:

// 偽代碼
"proxy": {
    "/api": {
        "target": "http://192.168.0.0",
    },
    "/abc": {
        "target": "http://192.168.0.1",
    }
}

此時,啟動就會報錯,提示proxy字段只接收字符串,不接受對象。

2、針對第一種的缺陷,官方比較推薦使用 http-proxy-middleware插件的方式

proxy.png

具體使用如下:

  • 安裝http-proxy-middleware
yarn add http-proxy-middleware
  • 在項目的src目錄下創建setupProxy.js,并進行配置
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
    app.use(
        '/api',
        createProxyMiddleware({
            target: 'http://172.16.0.0:8082',
            changeOrigin: true,
        })
    ),
    app.use(
        '/abc',
        createProxyMiddleware({
            target: 'http://172.16.0.1:8082',
            changeOrigin: true,
        })
    ),
}

然后重啟項目,就可以了。

設置路徑別名

config目錄下找到webpack.config.js文件,做如下配置即可:

alias.png

關閉生產環境 sourcemap

通常在開發的時候,我們會配置開發環境生成 sourcemap 文件,方便調試,但是在生產環境都要關閉,避免代碼泄露和打包多余文件,體積太大。通過查看config/webpack.config.js文件的devtool配置如下:

sourcemap.png

因此,可以通過cross-env插件在打包命令中添加全局變量。

  • 安裝cross-env
yarn add cross-env
  • 在打包命令中配置
"scripts": {
    "build": "cross-env GENERATE_SOURCEMAP=false node scripts/build.js",
  },

打包開啟 gzip 壓縮

前端代碼打包開啟gzip壓縮是前端性能優化重要的策略之一,因此這里也說下eject后如何配置打包開啟gzip壓縮。開啟打包代碼壓縮需要使用到插件compression-webpack-plugin。

  • 安裝
yarn add compression-webpack-plugin
  • 配置

config/webpack.config.js文件下做如下配置:

const CompressionPlugin = require("compression-webpack-plugin");
// 在命令中添加一個GENERATE_GZIP環境變量控制是否開啟打包壓縮
const isGzip = process.env.GENERATE_GZIP === "true";
plugins: [
  // ...
  isEnvProduction &&
    isGzip &&
    new CompressionPlugin({
      // 匹配開啟gzip壓縮的文件
      test: /\.(js|css|html|png|jpg)$/,
      // 壓縮后靜態資源文件名
      filename: "[path][base].gz",
      // 使用的壓縮算法
      algorithm: "gzip",
      // 只處理10M以上的資源壓縮
      threshold: 10240,
      // 只處理壓縮率小于0.8的資源
      minRatio: 0.8,
      // 壓縮后是否刪除源文件
      deleteOriginalAssets: true,
    }),
];

  • 在命令腳本設置是否開啟壓縮打包變量
"scripts": {
    "test": "cross-env GENERATE_GZIP=true node scripts/build.js",
    "prod": "cross-env GENERATE_GZIP=true node scripts/build.js"
  },

配置全局環境變量

create-react-app官方文檔中提供了通過.env文件來配置全局環境變量的方式,但是只提供了開發環境和生產環境兩種,但是我們日常開發中,必定會有測試環境,甚至是體驗環境,因此僅僅使用官方提供的無法滿足我們的需求,因此需要做些小的修改。

需求:

打包后的文件名通過.env.xx環境變量REACT_APP_MODE來決定。

步驟:

  1. 在項目根目錄創建三個文件:.env.env.test、.env.prod。內容如下:
// .env 開發環境環境變量
NODE_ENV = development;
REACT_APP_MODE = dev;
// .env.test 測試環境環境變量
NODE_ENV = production;
REACT_APP_MODE = test;
// .env.prod 生產環境環境變量
NODE_ENV = production;
REACT_APP_MODE = prod;
  1. 修改package.json文件命令腳本
"scripts": {
    "start": "node scripts/start.js",
    "test": "cross-env MODE_ENV=test node scripts/build.js",
    "prod": "cross-env MODE_ENV=prod node scripts/build.js"
  },
  1. 修改 config/env.js 文件


    mode_env.png
  1. 修改config/paths.js文件
    fileName.png

幾點說明

  • 在命令腳本設置環境變量使用了cross-env插件,因此需要先安裝。
  • 在命令腳本加入MODE_ENV環境變量是為了根據命令加載不同的.env.xx環境變量文件。
  • .env.xx配置的環境變量必須以REACT_APP_xx開頭。
  • .env.xx配置的環境變量在全局范圍內通過process.env.變量名獲取到。

二、不使用eject,重寫項目默認webpack配置

相比于eject釋放配置后,需要進行復雜的webpack配置不同,不使用eject則簡單很多,添加less支持、Antd組件按需引入、自定義主題跨域配置設置路徑別名、打包開啟 gzip 壓縮、生產環境關閉 sourceMap等都可以通過插件react-app-rewired、customize-cra在一個配置文件內完成,同時還支持自定義擴展,根據需求配置(添加或修改)webpackpluginsloader。具體操作如下:

安裝依賴

yarn add react-app-rewired customize-cra babel-plugin-import less less-loader antd

修改package.json命令配置

"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build"
}

在項目根目錄創建config-overrides.js文件,并進行如下配置:

const {
  override,
  overrideDevServer,
  fixBabelImports,
  addLessLoader,
  addWebpackAlias,
} = require("customize-cra");
const path = require("path");
const CompressionPlugin = require("compression-webpack-plugin");
const devServerConfig = () => (config) => {
  return {
    ...config,
    // 跨域配置
    proxy: {
      "/api": {
        target: "http://172.16.15.50:8082/api",
        secure: false,
        changeOrigin: true,
        pathRewrite: {
          "^/api": "",
        },
      },
      "/abc": {
        target: "http://172.16.0.1:9003",
        secure: false,
        changeOrigin: true,
        pathRewrite: {
          "^/abc": "",
        },
      },
    },
  };
};
// 自定義插件配置
const selfPlugins = [
  new CompressionPlugin({
    // 匹配開啟gzip壓縮的文件
    test: /\.(js|css|html|png|jpg)$/,
    // 壓縮后靜態資源文件名
    filename: "[path][base].gz",
    // 使用的壓縮算法
    algorithm: "gzip",
    // 只處理10M以上的資源壓縮
    threshold: 10240,
    // 只處理壓縮率小于0.8的資源
    minRatio: 0.8,
    // 壓縮后是否刪除源文件
    deleteOriginalAssets: true,
  }),
  // 更多自定義插件配置.....
];
module.exports = {
  // webpack配置
  webpack: override(
    // 按需引入antd組件
    fixBabelImports("import", {
      libraryName: "antd",
      libraryDirectory: "es",
      style: true,
    }),
    // 添加less支持和自定義主題
    addLessLoader({
      lessOptions: {
        modifyVars: { "@primary-color": "#1DA57A" },
        javascriptEnabled: true,
      },
    }),
    // 設置路徑別名
    addWebpackAlias({
      "@": path.resolve(__dirname, "./src"),
    }),
    /**暴露webpack的配置
     * 當插件內置方法不足以滿足我們的要求時,可以接收一個函數來暴露webpack所有配置,
     * 進行添加或修改loader,plugins,如此就可以滿足所有的需求了
     */
    (config) => {
      console.log("config", config);
      // 開發環境開啟source map,生產環境去掉map文件
      config.devtool =
        config.mode === "development" ? "cheap-module-source-map" : false;
      /**
       * 添加或修改plugins配置
       */
      if (process.env.NODE_ENV === "production") {
        // 生產環境開啟gzip壓縮
        config.plugins = [...config.plugins, ...selfPlugins];
      }
      /**
       * 添加和修改loader配置
       */
      // 獲取所有的loader信息:
      let loaders = config.module.rules.find((rule) =>
        Array.isArray(rule.oneOf)
      ).oneOf;
      // 獲取到所有的loader之后,根據需求添加或修改具體的某一項loader.....
      return config;
    }
  ),
  // 本地服務devServer配置
  devServer: overrideDevServer(devServerConfig()),
};

幾點說明:

  1. 添加less支持會根據lessless-loader版本不同會有所差異,我這里使用的版本是:"less": "3.5.0"、"less-loader": "^7.1.0"。
  2. 這種配置less支持的方式,不管是直接使用.less文件書寫樣式,還是通過.module.less文件的方式都可以支持。
  3. 跨域代理配置這并不是唯一的方法,也可以使用前面介紹到的http-proxy-middleware插件。
  4. 使用compression-webpack-plugin開啟gzip壓縮時,如果使用最新版本打包時會報錯,版本降為6.0.0后正常。
    gzip-error.png
  1. 這種方式,除了使用插件內置的方法配置外,還支持自定義擴展配置,通過暴露webpack的配置來完成自定義添加或修改pluginsloader,這樣就可以最大限度來滿足項目的多樣化需求了。

環境變量配置差異

相較于釋放webpack配置配置環境變量,重寫webpack配置設置環境變量則有所不同,我們無法在config-overrides.js中配置根據環境引入不同的.env.xx文件。因此只能借助工具來完成,通過dotenv插件我們可以在命令腳本配置引入哪個.env.xx文件。我們用同樣的需求演示。

需求:

打包后的文件名通過.env.xx環境變量REACT_APP_MODE來決定。

步驟:

  • 安裝dotenv-cli
yarn add dotenv-cli
  • 在項目根目錄下創建.env、.env.test、.env.prod文件,并進行如下配置:
// .env 開發環境環境變量
REACT_APP_MODE = dev;
// .env.test 測試環境環境變量
REACT_APP_MODE = test;
// .env.prod 生產環境環境變量
REACT_APP_MODE = prod;
  • 修改package.json文件命令腳本
"scripts": {
    "start": "react-app-rewired start",
    "test": "dotenv -e .env.test react-app-rewired build",
    "prod": "dotenv -e .env.prod react-app-rewired build"
}
  • config-overrides.js中修改默認打包后的文件名
    override_path.png

完整配置文件

const {
  override,
  overrideDevServer,
  fixBabelImports,
  addLessLoader,
  addWebpackAlias,
} = require("customize-cra");
const path = require("path");
const CompressionPlugin = require("compression-webpack-plugin");
const devServerConfig = () => (config) => {
  return {
    ...config,
    // 跨域配置
    proxy: {
      "/api": {
        target: "http://172.16.15.50:8082/api",
        secure: false,
        changeOrigin: true,
        pathRewrite: {
          "^/api": "",
        },
      },
      "/abc": {
        target: "http://172.16.0.1:9003",
        secure: false,
        changeOrigin: true,
        pathRewrite: {
          "^/abc": "",
        },
      },
    },
  };
};
// 自定義插件配置
const selfPlugins = [
  new CompressionPlugin({
    // 匹配開啟gzip壓縮的文件
    test: /\.(js|css|html|png|jpg)$/,
    // 壓縮后靜態資源文件名
    filename: "[path][base].gz",
    // 使用的壓縮算法
    algorithm: "gzip",
    // 只處理10M以上的資源壓縮
    threshold: 10240,
    // 只處理壓縮率小于0.8的資源
    minRatio: 0.8,
    // 壓縮后是否刪除源文件
    deleteOriginalAssets: true,
  }),
  // 更多自定義插件配置.....
];
module.exports = {
  /**
   * 路徑信息配置
   *  要修改什么路徑,打印出來paths,更改對應的位置即可
   */
  paths: function (paths) {
    // 打包后的文件名
    paths.appBuild = path.join(__dirname, `./${process.env.REACT_APP_MODE}`);
    console.log(paths);
    return paths;
  },
  // webpack配置
  webpack: override(
    // 按需引入antd組件
    fixBabelImports("import", {
      libraryName: "antd",
      libraryDirectory: "es",
      style: true,
    }),
    // 添加less支持和自定義主題
    addLessLoader({
      lessOptions: {
        modifyVars: { "@primary-color": "#1DA57A" },
        javascriptEnabled: true,
      },
    }),
    // 設置路徑別名
    addWebpackAlias({
      "@": path.resolve(__dirname, "./src"),
    }),
    /**暴露webpack的配置
     * 當插件內置方法不足以滿足我們的要求時,可以接收一個函數來暴露webpack所有配置,
     * 進行添加或修改loader,plugins,如此就可以滿足所有的需求了
     */
    (config) => {
      // console.log('config', config)
      // 開發環境開啟source map,生產環境去掉map文件
      config.devtool =
        config.mode === "development" ? "cheap-module-source-map" : false;
      /**
       * 添加或修改plugins配置
       */
      if (process.env.NODE_ENV === "production") {
        // 生產環境開啟gzip壓縮
        config.plugins = [...config.plugins, ...selfPlugins];
      }
      /**
       * 添加和修改loader配置
       */
      // 獲取所有的loader信息,
      let loaders = config.module.rules.find((rule) =>
        Array.isArray(rule.oneOf)
      ).oneOf;
      // 獲取到所有的loader之后,根據需求添加或修改具體的某一項loader.....
      return config;
    }
  ),
  // 本地服務devServer配置
  devServer: overrideDevServer(devServerConfig()),
};

至此,項目開發中的常用配置就已經分享完了,搭建好環境后就可以愉快的擼碼了。

碼字不易,且行且珍惜!?。?!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容