更骚的create-react-app开发环境配置craco

背景

使用 CRA 脚手架创建的项目,如果想要修改编译配置,通常可能会选择 npm run eject 弹出配置后魔改。但是,eject 是不可逆操作,弹出配置后,你将无法跟随官方的脚步去升级项目的 react-script 版本。

如果想要无 eject 重写 CRA 配置,目前成熟的是下面这几种方式

通过 CRA 官方支持的--scripts-version参数,创建项目时使用自己重写过的 react-scripts 包

使用react-app-rewired+customize-cra组合覆盖配置

使用craco覆盖配置

第二种方式相对第三种略复杂一些,我自己很有体会并且我们注意到最新的AntDesign4官方也开始推荐craco了,那我们还等什么还不快行动起来,今天主要在这里详细讨论一下craco的使用,也方便大家给出更好的建议。

配置步骤

首先,使用create-react-app创建一个项目,这里我们命名为my-project

npx create-react-app my-project

进入项目目录,安装基本依赖

yarn add antd @craco/craco craco-less @babel/plugin-proposal-decorators babel-plugin-import -D

3、修改package.json中的scripts

{  "scripts":{    "start": "set PORT=5000 && craco start FAST_REFRESH=true",    "build": "set GENERATE_SOURCEMAP=false && craco build",    "analyzer": "env NODE_ENV=production BUILD_ANALYZER=true yarn start",    "test": "craco test"  }}

4、项目根目录创建 craco.config.js 文件

/* craco.config.js */module.exports = {  ...}

上面用到了几个环境变量:

PORT 启动端口

GENERATE_SOURCEMAP 打包时是否生成 sourceMap

BUILD_ANALYZER文件方式输出编译分析

基础的配置到此完成了,接下来是处理各种配置的覆盖,完整的 craco.config.js 配置文件结构,可以在 craco 官方的文档中详细查询:Configuration Overview 。

扩展 babel 配置

虽然可以在 configure 中定义 babel 配置,但 craco 也提供了快捷的方式单独去书写,添加 @babel/preset-env 配置示例如下:

/* craco.config.js */module.exports = {  babel: {    presets: [      [        '@babel/preset-env',        {          modules: false, // 对ES6的模块文件不做转化,以便使用tree shaking、sideEffects等          useBuiltIns: 'entry', // browserslist环境不支持的所有垫片都导入          // https://babeljs.io/docs/en/babel-preset-env#usebuiltins          // https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md          corejs: {            version: 3, // 使用core-js@3            proposals: true,          },        },      ],    ],    plugins: [        // 配置 babel-plugin-import        ['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd'],      // 配置解析器      ["@babel/plugin-proposal-decorators", { "legacy": true }],        ["@babel/plugin-proposal-class-properties", { "loose": true }],      ["babel-plugin-styled-components", { "displayName": true }]    ],      loaderOptions: {},      loaderOptions: (babelLoaderOptions, { env, paths }) => { return babelLoaderOptions; }      },}

检测模块编译情况

new WebpackBar({ profile: true }),new CircularDependencyPlugin({  exclude: /node_modules/,  include: /src/,  failOnError: true,  allowAsyncCycles: false,  cwd: process.cwd()})

观察打包进度

const SimpleProgressWebpackPlugin = require('simple-progress-webpack-plugin')module export = {  webpack: {    plugins: [      // 查看打包的进度      new SimpleProgressWebpackPlugin()    ]  }}

修改打包输出目录

module.exports = {  webpack: {    configure: (webpackConfig, {      env, paths    }) => {      // paths.appPath='public'      paths.appBuild = 'dist'      webpackConfig.output = {        ...webpackConfig.output,          // ...{          //  filename: whenDev(() => 'static/js/bundle.js', 'static/js/[name].js'),          //  chunkFilename: 'static/js/[name].js'          // },          path: path.resolve(__dirname, 'dist'), // 修改输出文件目录          publicPath: '/'      }      return webpackConfig    }  }}

如果觉得繁琐也可以直接使用webpack进行configure覆盖、webpackConfig的信息大概有这么多:

热更新Hot-loader扩展

启动热更新如何避免频繁刷新

常用的热更新方案 react-hot-loader、craco也帮我们提供了两种craco-plugin-react-hot-reload、craco-fast-refresh

react-hot-loader配置如下(传送门)

step1:webpack.config.js中引入别名配置解除相关警告yarn add @hot-loader/react-dommodule.exports = {  // ...  resolve: {    alias: {      'react-dom': '@hot-loader/react-dom',    },  },};step2:注入引用App.jsimport { hot } from 'react-hot-loader/root'function App {    return (    <div>ceshi</div>    )}export default hot(App)

craco-plugin-react-hot-reload配置如下(传送门)

/* craco.config.js */const reactHotReloadPlugin = require('craco-plugin-react-hot-reload')const reactHotReloadPlugin = require('craco-plugin-react-hot-reload')module.exports = {  plugins: [{    plugin: reactHotReloadPlugin  }]}

craco-fast-refresh  配置如下(传送门)

这是最近发现的新 craco plugin,相对于 react-hot-loader 好用得多,零配置,不需要修改项目代码,据说性能也更好。

step1:增加插件/* craco.config.js */const FastRefreshCracoPlugin = require('craco-fast-refresh')module.exports = () => {  return {    plugins: [{      plugin: FastRefreshCracoPlugin    }],  };};step2: 注入引用App.jsimport React from 'react'import { hot } from 'react-hot-loader'const App = () => <div>Hello World!</div>export default hot(module)(App)

Antd自定义主题配置

配置antd主题颜色可随意对以下方案就行选取

结合lessOptions

step1:运行 yarn add craco-lessstep2:引入 const CracoLessPlugin = require('craco-less')step3:配置{  plugin: CracoLessPlugin,  options: {    lessLoaderOptions: {      lessOptions: {        modifyVars: {          '@primary-color': '#1DA57A'        },        javascriptEnabled: true      }    }  }}

同时craco 也提供了专门的 plugin 来处理 antd 的集成(传送门)配置方式有区别

Craco自定义支持

craco-antd includes:

Less (provided by craco-less)

babel-plugin-import to only import the required CSS, instead of everything

An easy way to customize the theme. Set your custom variables in ./antd.customize.less

step1: yarn add craco-antdstep2: const CracoAntDesignPlugin = require('craco-antd')step3 {  plugin: CracoAntDesignPlugin,  options: {    customizeTheme: {      '@primary-color': '#FF061C'    }  }}

针对customizeTheme如果想单独抽离可采取如下方案

step1: 新建antd.customize.less文件---------@primary-color: #FF061C;---------step2:读取模式{  plugin: CracoAntDesignPlugin,  options: {  customizeThemeLessPath: path.join(__dirname,"antd.customize.less")} }

相对来讲使用会更简洁一些,推荐使用。

总结

确实能够在不 eject 弹出配置的情况下,能够自定义所有的 cra 构建配置,之前进行了详细的说明,有这方面的需求可以去看看(传送门)。因此在后续的编码中,我们可以随便使用这两种方式构建自己的webpack配置。

注意:configure配置和craco配置会互斥谨慎使用

以下,是我整理完整的 craco.config.js 配置,相应的demo方便参照

craco 还提供一些其他 plugin具体根据实际情况自行加入(传送门)

/* craco.config.js *//** * TODO: 区分环境 —— NODE_ENV * - whenDev ☞ process.env.NODE_ENV === 'development' * - whenTest ☞ process.env.NODE_ENV === 'test' * - whenProd ☞ process.env.NODE_ENV === 'production' */const {  when, whenDev, whenProd, whenTest, ESLINT_MODES, POSTCSS_MODES} = require('@craco/craco')const webpack = require('webpack')const CracoLessPlugin = require('craco-less')const CracoAntDesignPlugin = require('craco-antd')const CracoVtkPlugin = require('craco-vtk')const WebpackBar = require('webpackbar')const CircularDependencyPlugin = require('circular-dependency-plugin')const FastRefreshCracoPlugin = require('craco-fast-refresh')const TerserPlugin = require('terser-webpack-plugin')const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin')const {  BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')const CompressionWebpackPlugin = require('compression-webpack-plugin')const DashboardPlugin = require('webpack-dashboard/plugin')const SimpleProgressWebpackPlugin = require('simple-progress-webpack-plugin')const path = require('path')// 判断编译环境是否为生产const isBuildAnalyzer = process.env.BUILD_ANALYZER === 'true'const pathResolve = pathUrl => path.join(__dirname, pathUrl)module.exports = {  webpack: {    // 别名配置    alias: {      '@': pathResolve('.'),      src: pathResolve('src'),      assets: pathResolve('src/assets'),      common: pathResolve('src/common'),      components: pathResolve('src/components'),      hooks: pathResolve('src/hooks'),      pages: pathResolve('src/pages'),      store: pathResolve('src/store'),      utils: pathResolve('src/utils')        // 此处是一个示例,实际可根据各自需求配置    },    plugins: [      // webpack构建进度条      new WebpackBar({        profile: true      }),      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),      // 查看打包的进度      new SimpleProgressWebpackPlugin(),      // 时间转换工具采取day替换moment      new AntdDayjsWebpackPlugin(),      // // 新增模块循环依赖检测插件      ...whenDev(        () => [          new CircularDependencyPlugin({            exclude: /node_modules/,            include: /src/,            failOnError: true,            allowAsyncCycles: false,            cwd: process.cwd()          }),          // webpack-dev-server 强化插件          new DashboardPlugin(),          new webpack.HotModuleReplacementPlugin()        ], []      ),      /**      * 编译产物分析      *  - https://www.npmjs.com/package/webpack-bundle-analyzer      * 新增打包产物分析插件      */      ...when(        isBuildAnalyzer, () => [          new BundleAnalyzerPlugin({            analyzerMode: 'static', // html 文件方式输出编译分析            openAnalyzer: false,            reportFilename: path.resolve(__dirname, `analyzer/index.html`)          })        ], []      ),      ...whenProd(        () => [          // new TerserPlugin({          //  // sourceMap: true, // Must be set to true if using source-maps in production          //  terserOptions: {          //    ecma: undefined,          //    parse: {},          //    compress: {          //      warnings: false,          //      drop_console: true, // 生产环境下移除控制台所有的内容          //      drop_debugger: true, // 移除断点          //      pure_funcs: ['console.log'] // 生产环境下移除console          //    }          //  }          // }),          // 打压缩包          new CompressionWebpackPlugin({            algorithm: 'gzip',            test: new RegExp('\\.(' + ['js', 'css'].join('|') + ')$'),            threshold: 1024,            minRatio: 0.8          })        ], []      )    ],    //抽离公用模块    optimization: {      splitChunks: {        cacheGroups: {          commons: {            chunks: 'initial',            minChunks: 2,            maxInitialRequests: 5,            minSize: 0          },          vendor: {            test: /node_modules/,            chunks: 'initial',            name: 'vendor',            priority: 10,            enforce: true          }        }      }    },    /**    * 重写 webpack 任意配置    *  - configure 能够重写 webpack 相关的所有配置,但是,仍然推荐你优先阅读 craco 提供的快捷配置,把解决不了的配置放到 configure 里解决;    *  - 这里选择配置为函数,与直接定义 configure 对象方式互斥;    */    configure: (webpackConfig, {      env, paths    }) => {      // paths.appPath='public'      paths.appBuild = 'dist' // 配合输出打包修改文件目录        // webpackConfig中可以解构出你想要的参数比如mode、devtool、entry等等,更多信息请查看webpackConfig.json文件        /**        * 修改 output        */      webpackConfig.output = {          ...webpackConfig.output,            // ...{            //  filename: whenDev(() => 'static/js/bundle.js', 'static/js/[name].js'),            //  chunkFilename: 'static/js/[name].js'            // },            path: path.resolve(__dirname, 'dist'), // 修改输出文件目录            publicPath: '/'        }        /**        * webpack split chunks        */        // webpackConfig.optimization.splitChunks = {        //  ...webpackConfig.optimization.splitChunks,        //  ...{        //    chunks: 'all',        //    name: true        //  }        // }        // 返回重写后的新配置      return webpackConfig    }  },  babel: {    presets: [],    plugins: [      // AntDesign 按需加载      ['import', {        libraryName: 'antd',        libraryDirectory: 'es',        style: true      }, 'antd'],      ['@babel/plugin-proposal-decorators', {        legacy: true      }] // 用来支持装饰器    ],    loaderOptions: {},    loaderOptions: (babelLoaderOptions, {      env, paths    }) => {      return babelLoaderOptions    }  },  /**  * 新增 craco 提供的 plugin  */  plugins: [    // 热更新    ...whenDev(      () => [{        plugin: FastRefreshCracoPlugin      }, {        plugin: CracoVtkPlugin()      }, {        plugin: new AntdDayjsWebpackPlugin()      }], []    ),    // 方案1、配置Antd主题less    // {    //  plugin: CracoLessPlugin,    //  options: {    //    lessLoaderOptions: {    //      lessOptions: {    //        modifyVars: { '@primary-color': '#1DA57A' },    //        javascriptEnabled: true    //      }    //    }    //  }    // },    // 方案2、配置Antd主题    // {    //  plugin: CracoAntDesignPlugin,    //  options: {    //    customizeTheme: {    //      '@primary-color': '#FF061C'    //    }    //  }    // },    // 方案3、配置Antd主题    {      plugin: CracoAntDesignPlugin,      options: {        customizeThemeLessPath: path.join(          __dirname,          "antd.customize.less"        ),      },    },  ],  devServer: {    port: 9000,    proxy: {      '/api': {        target: 'https://placeholder.com/',        changeOrigin: true,        secure: false,        xfwd: false,      }    }  }}

同时我们也可以看一下官方给我们暴露了哪些Api

const { when, whenDev, whenProd, whenTest, ESLINT_MODES, POSTCSS_MODES } = require("@craco/craco");module.exports = {    reactScriptsVersion: "react-scripts" /* (default value) */,    style: {        modules: {            localIdentName: ""        },        css: {            loaderOptions: { /* Any css-loader configuration options: https://github.com/webpack-contrib/css-loader. */ },            loaderOptions: (cssLoaderOptions, { env, paths }) => { return cssLoaderOptions; }        },        sass: {            loaderOptions: { /* Any sass-loader configuration options: https://github.com/webpack-contrib/sass-loader. */ },            loaderOptions: (sassLoaderOptions, { env, paths }) => { return sassLoaderOptions; }        },        postcss: {            mode: "extends" /* (default value) */ || "file",            plugins: [],            env: {                autoprefixer: { /* Any autoprefixer options: https://github.com/postcss/autoprefixer#options */ },                stage: 3, /* Any valid stages: https://cssdb.org/#staging-process. */                features: { /* Any CSS features: https://preset-env.cssdb.org/features. */ }            },            loaderOptions: { /* Any postcss-loader configuration options: https://github.com/postcss/postcss-loader. */ },            loaderOptions: (postcssLoaderOptions, { env, paths }) => { return postcssLoaderOptions; }        }    },    eslint: {        enable: true /* (default value) */,        mode: "extends" /* (default value) */ || "file",        configure: { /* Any eslint configuration options: https://eslint.org/docs/user-guide/configuring */ },        configure: (eslintConfig, { env, paths }) => { return eslintConfig; },        loaderOptions: { /* Any eslint-loader configuration options: https://github.com/webpack-contrib/eslint-loader. */ },        loaderOptions: (eslintOptions, { env, paths }) => { return eslintOptions; }    },    babel: {        presets: [],        plugins: [],        loaderOptions: { /* Any babel-loader configuration options: https://github.com/babel/babel-loader. */ },        loaderOptions: (babelLoaderOptions, { env, paths }) => { return babelLoaderOptions; }    },    typescript: {        enableTypeChecking: true /* (default value)  */    },    webpack: {        alias: {},        plugins: [],        configure: { /* Any webpack configuration options: https://webpack.js.org/configuration */ },        configure: (webpackConfig, { env, paths }) => { return webpackConfig; }    },    jest: {        babel: {            addPresets: true, /* (default value) */            addPlugins: true  /* (default value) */        },        configure: { /* Any Jest configuration options: https://jestjs.io/docs/en/configuration. */ },        configure: (jestConfig, { env, paths, resolve, rootDir }) => { return jestConfig; }    },    devServer: { /* Any devServer configuration options: https://webpack.js.org/configuration/dev-server/#devserver. */ },    devServer: (devServerConfig, { env, paths, proxy, allowedHost }) => { return devServerConfig; },    plugins: [        {            plugin: {                overrideCracoConfig: ({ cracoConfig, pluginOptions, context: { env, paths } }) => { return cracoConfig; },                overrideWebpackConfig: ({ webpackConfig, cracoConfig, pluginOptions, context: { env, paths } }) => { return webpackConfig; },                overrideDevServerConfig: ({ devServerConfig, cracoConfig, pluginOptions, context: { env, paths, proxy, allowedHost } }) => { return devServerConfig; },                overrideJestConfig: ({ jestConfig, cracoConfig, pluginOptions, context: { env, paths, resolve, rootDir } }) => { return jestConfig },            },            options: {}        }    ]};

这么多的信息使用起来是不是很爽,想探索的赶快行动起来共同进步啦

参考

craco 配置

less-loader 官方文档

ant design 官方文档

craco实践

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容