Create React App無eject配置(react-app-rewired 和 customize-cra)

Run丘位元發表於2019-12-09

前言

Create React App(以下簡稱 CRA)是建立 React 應用的一個腳手架,它與其他腳手架不同的一個地方就是將一些複雜工具(比如 webpack)的配置封裝了起來,讓使用者不用關心這些工具的具體配置,從而降低了工具的使用難度。

使用 create-react-app 生成專案,不會有 webpack 的配置項,要匯出 webpack,必須使用 react-script eject,但這是一個單向操作,eject 後,就無法恢復了。如果只是修改一些簡單的配置,eject 是沒有必要的。

但是對於一些熟悉 webpack 的開發者來說,他們可能想對 webpack 配置做一些修改,這個時候應該怎麼辦呢?

其實我們可以通過以下幾種方式來修改 webpack 的配置:

  • 專案 eject
  • 替換 react-scripts 包
  • 使用 react-app-rewired
  • scripts 包 + override 組合

如果你正在為如何處理react配置項而煩惱,以下內容可以帶你走出一片藍天。接下來對這幾種方式分別進行介紹下,配置是一個痛苦的過程,認真品味,如果對你有幫助請點贊轉發哦!!

專案 eject

使用 CRA 建立完專案以後,專案在package.json裡面提供了這樣一個命令:

{
  ...
  "scripts": {
    "eject": "react-scripts eject"
  },
  ...
}
複製程式碼複製程式碼

執行完這個命令——yarn run eject後會將封裝在 CRA 中的配置全部反編譯到當前專案,這樣使用者就可以完全取得 webpack 檔案的控制權,想怎麼修改就怎麼修改了。

# eject 後專案根目錄下會出現 config 資料夾,裡面就包含了 webpack 配置
config
├── env.js
├── jest
│   ├── cssTransform.js
│   └── fileTransform.js
├── paths.js
├── polyfills.js
├── webpack.config.dev.js // 開發環境配置
├── webpack.config.prod.js // 生產環境配置
└── webpackDevServer.config.js
複製程式碼複製程式碼

CRA 與其他腳手架不同的另一個地方,就是可以通過升級其中的react-scripts包來升級 CRA 的特性。比如用老版本 CRA 建立了一個專案,這個專案不具備 PWA 功能,但只要專案升級了react-scripts包的版本就可以具備 PWA 的功能,專案本身的程式碼不需要做任何修改。

但如果我們使用了eject命令,就再也享受不到 CRA 升級帶來的好處了,因為react-scripts已經是以檔案的形式存在於你的專案,而不是以包的形式,所以無法對其升級。

替換 react-scripts 包

react-scripts 是 CRA 的一個核心包,一些指令碼和工具的預設配置都整合在裡面,使用 CRA 建立專案預設就是使用這個包,但是 CRA 還提供了另外一種方式來建立 CRA 專案,即使用自定義 scripts 包的方式。

# 預設方式
$ create-react-app foo
# 自定義 scripts 包方式
$ create-react-app foo --scripts-version 自定義包
複製程式碼複製程式碼

自定義包可以是下面幾種形式:

  • react-scripts包的版本號,比如0.8.2,這種形式可以用來安裝低版本的react-scripts包。
  • 一個已經發布到 npm 倉庫上的包的名字,比如your-scripts,裡面包含了修改過的 webpack 配置。
  • 一個 tgz 格式的壓縮檔案,比如/your/local/scripts.tgz,通常是未釋出到 npm 倉庫的自定義 scripts 包,可以用 npm pack 命令生成。

這種方式相對於之前的eject是一種更靈活地修改 webpack 配置的方式,而且可以做到和 CRA 一樣,通過升級 scrips 包來升級專案特性。

自定義 scripts 包的結構可以參照react-scripts包的結構,只要修改對應的 webpack 配置檔案,並安裝上所需的 webpack loader 或 plugin 包就可以了。

使用 react-app-rewired 自定義配置

注意

react-app-rewired 1.x 配合 create-react-app 1.x

react-app-rewired 2.x 配合 create-react-app 2.x

版本升級導致互不相容,另外,react-app-rewired 2.x 應該是社群維護了。

react-app-rewired@^2.0.0+ 版本需要搭配 customize-cra 使用

在 react-app-rewired 1.x 的版本中,它除了提供覆蓋配置的方法,還體用了一些 helpers,例如 rewireLess、rewirePreact 等,2.x 版本只保留了核心功能。

From README:

Version 2.0 removes the rewire helper functions

All helper functions:

  • injectBabelPlugin
  • compose
  • getBabelLoader
  • getLoader
  • babelLoaderMatcher
  • loaderNameMatches

have been removed with commit 0848602

另外一個工具幫我們實現了這些,customize-cra,這次我們將使用 react-app-rewiredcustomize-cra 一起倒騰。

雖然有這兩種方式可以擴充套件 webpack 配置,但是很多開發者還是覺得太麻煩,有沒有一種方式可以既不用eject專案又不用建立自己的 scripts 包呢?答案是肯定的,react-app-rewired 是 react 社群開源的一個修改 CRA 配置的工具。

react-app-rewired 的方式自定義配置,參考 Extended Configuration Options 文件。

這次我們使用 customize-cra 協助自定義,參考 Using the plugins 文件。

customize-cra提供了一些簡遍的api(customize-cra/api.md),通常就可以滿足大部分的開發需求

原始碼(customize-cra/src/customizes/webpack.js)(比如當前版本github.com/arackaf/cus…)

在 CRA 建立的專案中安裝了react-app-rewired後,可以通過建立一個config-overrides.js 檔案來對 webpack 配置進行擴充套件。具體操作如下:

config-overrides.js配置

1、修改webpack配置檔案,需要安裝 react-app-rewired customize-cra

yarn add react-app-rewired customize-cra -D複製程式碼

2、修改package.json檔案

"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test --env=jsdom",
    "eject": "react-scripts eject"
  },複製程式碼

3、在專案根目錄新建config-overrides.js

const { override } = require('customize-cra');
module.exports = {};複製程式碼

4、新增配置,跨域設定、增加less支援、px轉rem、ant-design按需載入、打包壓縮js和css

// 安裝less less-loader
yarn add less less-loader -D
// 安裝compression-webpack-plugin 壓縮js為gzip
yarn add compression-webpack-plugin -D

const { override, overrideDevServer, addLessLoader, addPostcssPlugins, fixBabelImports } = require('customize-cra');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
// 打包配置
const addCustomize = () => config => {
  if (process.env.NODE_ENV === 'production') {
    // 關閉sourceMap
    config.devtool = false;
    // 配置打包後的檔案位置
    config.output.path = __dirname + '../dist/demo/';
    config.output.publicPath = './demo';
    // 新增js打包gzip配置
    config.plugins.push(
      new CompressionWebpackPlugin({
        test: /\.js$|\.css$/,
        threshold: 1024,
      }),
    )
  }
  return config;
}
// 跨域配置
const devServerConfig = () => config => {
  return {
    ...config,
    // 服務開啟gzip
    compress: true,
    proxy: {
      '/api': {
        target: 'xxx',
        changeOrigin: true,
        pathRewrite: {
          '^/api': '/api',
        },
      }
    }
  }
}
module.exports = {
  webpack: override(
    fixBabelImports('import', {
      libraryName: 'antd-mobile',
      style: 'css',
    }),
    addLessLoader(),
    addPostcssPlugins([require('postcss-pxtorem')({ rootValue: 75, propList: ['*'], minPixelValue: 2, selectorBlackList: ['am-'] })]),
    addCustomize(),
  ),
  devServer: overrideDevServer(
    devServerConfig()
  )
}
複製程式碼

5、antd

假設我們要使用 antd,參考 高階配置 文件。

npm i --save-dev babel-plugin-import
npm i --save antd複製程式碼
const { override, fixBabelImports, addLessLoader } = require('customize-cra');
module.exports = override(
    fixBabelImports('import', {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: true,
    }),    
    addLessLoader({
        javascriptEnabled: true,
        modifyVars: { '@primary-color': '#1DA57A' },
        localIdentName: '[local]--[hash:base64:5]' // 自定義 CSS Modules 的 localIdentName
    }),
);複製程式碼

6、decorators

在 create-react-app 的 Can I Use Decorators 文件中說,當前它並不是一個文件的規範,預設不推薦使用,如果要使用,需要自己手動開啟。

npm i --save-dev @babel/plugin-proposal-decorators複製程式碼
const { override, fixBabelImports, addLessLoader, addDecoratorsLegacy } = require('customize-cra');
module.exports = override(
    addDecoratorsLegacy(),
    fixBabelImports('import', {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: true,
    }),    
    addLessLoader({
        javascriptEnabled: true,
        modifyVars: { '@primary-color': '#1DA57A' },
        localIdentName: '[local]--[hash:base64:5]' // 自定義 CSS Modules 的 localIdentName
    }),
);複製程式碼

7、新增別名

const { override, fixBabelImports, addLessLoader, addDecoratorsLegacy, addWebpackAlias } = require('customize-cra');
module.exports = override(
    addDecoratorsLegacy(),
    addWebpackAlias({
        ["ag-grid-react$"]: path.resolve(__dirname, "src/shared/agGridWrapper.js")
    }),
    fixBabelImports('import', {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: true,
    }),    
    addLessLoader({
        javascriptEnabled: true,
        modifyVars: { '@primary-color': '#1DA57A' },
        localIdentName: '[local]--[hash:base64:5]' // 自定義 CSS Modules 的 localIdentName
    }),
);複製程式碼

8、新增 react-hot-reloader、在根元件處開啟 react-hot-reloader

# https://www.npmjs.com/package/react-hot-loader
# https://github.com/cdharris/react-app-rewire-hot-loader

$ npm i react-hot-loader -D

$ npm i react-app-rewire-hot-loader @hot-loader/react-dom -D
複製程式碼

隨後在App.js中做如下設定

import React, { Component } from 'react'
import { hot } from 'react-hot-loader/root'
class App extends Component {
    render() {
    return (
            <>測試</>
        )
  }
}
const AppHot = process.env.NODE_ENV === 'development' ? hot(App) : App

export default AppHot
複製程式碼

9、關閉sourceMap

  • 方案一:修改package中scripts裡的build
"build": "GENERATE_SOURCEMAP=false react-app-rewired build"複製程式碼
  • 方案二:
const rewiredMap = () => config => {
  config.devtool = config.mode === 'development' ? 'cheap-module-source-map' : false

  return config
}複製程式碼

以上是一些簡單的配置已經好了。

整體配置檔案如下:

const {
  override,
  fixBabelImports,
  addLessLoader,
  addWebpackAlias,
  addBabelPlugins,
  addWebpackPlugin,
  useBabelRc,
  disableChunk,
  adjustWorkbox,
  setWebpackPublicPath,
  addBundleVisualizer,
  disableEsLint,
  addWebpackExternals
  // addBundleVisualizer
} = require('customize-cra')

const path = require('path')
const paths = require('react-scripts/config/paths')
const rewireReactHotLoader = require('react-app-rewire-hot-loader')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
// const rewireCompressionPlugin = require('react-app-rewire-compression-plugin')
const rewireUglifyjs = require('react-app-rewire-uglifyjs')
const FilterWarningsPlugin = require('webpack-filter-warnings-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 補充:對開發友好,打包完成桌面提醒
const WebpackBuildNotifierPlugin = require('webpack-build-notifier')

const webpackConfig = require('./webpack.config.js')
// const ProgressBarPlugin = require('progress-bar-webpack-plugin')

// const Dashboard = require('webpack-dashboard')
// const DashboardPlugin = require('webpack-dashboard/plugin')
// const dashboard = new Dashboard()

const theme = require('./theme')
// SKIP_PREFLIGHT_CHECK = true

/**
 * 生產環境是否打包 Source Map 兩種方法
 *
 */
const rewiredMap = () => config => {
  config.devtool = config.mode === 'development' ? 'cheap-module-source-map' : false

  return config
}
process.env.PORT = 3006

process.env.GENERATE_SOURCEMAP !== 'false'

console.log(process.env.NODE_ENV)

// const addWebpackModules = () => config => {
//   const loaders = config.module.rules.find(rule => Array.isArray(rule.oneOf)).oneOf
//   loaders[loaders.length - 4] = Object.assign(
//     loaders[loaders.length - 4],
//     webpackConfig.module.rules[0]
//   )
//   return config
// }

// path
const resolveAlias = dir => path.join(__dirname, '.', dir)
// 熱跟新
const hotLoader = () => (config, env) => {
  config = rewireReactHotLoader(config, env)
  return config
}
// build--->prod --->檔案設定
const appBuildPathFile = () => config => {
  if (config.mode === 'development') {
    console.log('evn is development, skip build path change...')
  } else if (config.mode === 'production') {
    console.log('evn is production, change build path...')
    // 關閉sourceMap
    config.devtool = false
    //  // 配置打包後的檔案位置修改path目錄
    paths.appBuild = path.join(path.dirname(paths.appBuild), 'dist')
    config.output.path = path.join(path.dirname(config.output.path), 'dist')
    // 新增js打包gzip配置
    // config.plugins.push(
    //   new CompressionWebpackPlugin({
    //     test: /\.js$|\.css$/,
    //     threshold: 1024
    //   })
    // )
    // 更改生產模式輸出的檔名
    // config.output.filename = 'static/js/[name].js?_v=[chunkhash:8]'
    // config.output.chunkFilename = 'static/js/[name].chunk.js?_v=[chunkhash:8]'
  }
  return config
}
//生產環境去除console.* functions
const dropConsole = () => {
  return config => {
    if (config.optimization.minimizer) {
      config.optimization.minimizer.forEach(minimizer => {
        if (minimizer.constructor.name === 'TerserPlugin') {
          minimizer.options.terserOptions.compress.drop_console = true
        }
      })
    }
    return config
  }
}
/**
 *
 * @description 解決打包的時候如下報錯
 * @url{https://github.com/ant-design/ant-design/issues/15696}
 * https://blog.csdn.net/peade/article/details/84890399
chunk 3 [mini-css-extract-plugin]
Conflicting order between:
 * css ./node_modules/css-loader/dist/cjs.js??ref--6-oneOf-7-1!./node_modules/postcss-loader/src??postcss!./node_modules/less-loader/dist/cjs.js??ref--6-oneOf-7-3!./node_modules/antd/es/input/style/index.less
 * css ./node_modules/css-loader/dist/cjs.js??ref--6-oneOf-7-1!./node_modules/postcss-loader/src??postcss!./node_modules/less-loader/dist/cjs.js??ref--6-oneOf-7-3!./node_modules/antd/es/message/style/index.less
 */
const delConflictingOrder = () => {
  return config => {
    for (let i = 0; i < config.plugins.length; i++) {
      const p = config.plugins[i]
      if (!!p.constructor && p.constructor.name === MiniCssExtractPlugin.name) {
        const miniCssExtractOptions = { ...p.options, ignoreOrder: true }
        config.plugins[i] = new MiniCssExtractPlugin(miniCssExtractOptions)
        break
      }
    }
  }
}

const addMiniCssExtractPlugin = () => {
  return config => {
    config.plugins.unshift(
      new FilterWarningsPlugin({
        // exclude: /any-warnings-matching-this-will-be-hidden/
        // exclude: /mini-css-extract-plugin[^]*Conflicting order between:/
        exclude: /\[mini-css-extract-plugin\][^]*Conflicting order between:/
      })
    )
  }
}

const proxyApi = {
  '/api': {
    // target: '', // prod
    changeOrigin: true,
    secure: false,
    xfwd: false,
    pathRewrite: {
      '^/api': '/'
    }
  },
  '/store': {
    // target: '', // staging
    changeOrigin: true,
    secure: false,
    xfwd: false,
    pathRewrite: {
      '^/store': '/'
    }
  }
}

module.exports = {
  webpack: override(
    fixBabelImports('import', {
      libraryName: 'antd',
      libraryDirectory: 'es',
      style: true
    }),
    addLessLoader({
      // strictMath: true,
      noIeCompat: true,
      javascriptEnabled: true,
      modifyVars: { ...theme }
      // localIdentName: '[local]--[hash:base64:5]', // 自定義 CSS Modules 的 localIdentName
    }),
    setWebpackPublicPath('/hostsec'), // 修改 publicPath
    addWebpackExternals({
      React: 'React',
      lodash: 'Lodash'
    }),
    // addWebpackModules(),
    addWebpackAlias({
      '@': resolveAlias('src'),
      lib: resolveAlias('src/lib'),
      components: resolveAlias('src/components'),
      images: resolveAlias('src/assets/images'),
      styled: resolveAlias('src/assets/styled'),
      views: resolveAlias('src/views'),
      store: resolveAlias('src/store'),
      router: resolveAlias('src/router'),
      locale: resolveAlias('src/locale'),
      // 處理警告  React-Hot-Loader: react-?-dom patch is not detected. React 16.6+ features may not work.
      'react-dom': '@hot-loader/react-dom'
      // 解決antd 的icon圖示打包體積大
      // '@ant-design/icons': 'purched-antd-icons'
    }),

    disableEsLint(),
    appBuildPathFile(),
    disableChunk(),
    dropConsole(),
    // 關閉mapSource
    rewiredMap(),
    // 熱跟新
    hotLoader(),
    // 配置babel解析器
    addBabelPlugins(['@babel/plugin-proposal-decorators', { legacy: true }]),
    //啟用ES7的修改器語法(babel 7)
    // ['@babel/plugin-proposal-decorators', {legacy: true}],
    // ['@babel/plugin-proposal-class-properties', {loose: true}],
    // 打包編譯完成提醒
    addWebpackPlugin(
      new WebpackBuildNotifierPlugin({
        title: '',
        logo: path.resolve('./public/logo.svg'),
        suppressSuccess: true
      }),
      new MiniCssExtractPlugin({
        filename: 'static/css/[name].[contenthash].css',
        chunkFilename: 'static/css/[id].[contenthash].css',
        ignoreOrder: false
        // moduleFilename: ({ name }) => `${name.replace('/js/', '/css/')}.css`
      }),
      // 美化控制檯
      // new DashboardPlugin(dashboard.setData),
      // 進度條
      // new ProgressBarPlugin(),
      delConflictingOrder(),
      addMiniCssExtractPlugin()
    ),
    rewireUglifyjs,
    // rewireCompressionPlugin,
    // 允許使用.babelrc檔案進行Babel配置。
    useBabelRc(),
    // add webpack bundle visualizer if BUNDLE_VISUALIZE flag is enabled
    process.env.BUNDLE_VISUALIZE == 1 && addBundleVisualizer(),

    adjustWorkbox(wb =>
      Object.assign(wb, {
        skipWaiting: true,
        exclude: (wb.exclude || []).concat('index.html')
      })
    )
    // addDecoratorsLegacy() // 解析器,
  ),
  // 配置devServer
  // devServer: overrideDevServer(
  //   // dev server plugin
  //   watchAll(),
  // ),
  // 配置devServer
  devServer: configFunction => (proxy, allowedHost) => {
    proxy = process.env.NODE_ENV === 'development' ? proxyApi : null
    // allowedHost: 新增額外的地址
    const config = configFunction(proxy, allowedHost)
    return config
  }
}
複製程式碼

㊗️注意:如果還要增加配置可以自行插入,具體支援的方法如圖:

image.png

傳送門api.md

同時,歡迎小夥伴們加微信群一起探討:

微訊號

image.png

Create React App無eject配置(react-app-rewired 和 customize-cra)

總結

CRA 是一個非常棒的 React 腳手架工具,但你如果不滿足於它的 webpack 預設配置,你可以通過上述幾種方式來擴充套件自己專案的 webpack 配置,這幾種方式各有優缺點,可以結合具體的使用場景來選擇合適自己的方式。

文件參考 :


相關文章