ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

搞懂 ejected 源码,吃透 React 高频面试题

搞懂 ejected 源码,吃透 React 高频面试题 搞懂 ejected 源码,吃透 React 高频面试题 官方文档那几页纸,根本不够看。你想改 webpack 配置,结果发现 react-scripts 把构建流程锁死了。这时候 ejected 命令成了救命稻草,也是面试里那道让你卡壳的高频面试题。别被这个词吓住,它其实就是把黑盒变成白盒的过程。 今天咱们不背八股文,直接拆开 react-scripts 的源码,看看 ejected 到底干了什么,为什么现在大家都不推荐用了,以及如何在面试中优雅地回答这个问题。 入口定位:ejected 命令到底在哪? 很多初学者以为 ejected 是 React 自带的命令,其实不是。它属于 react-scripts 这个 NPM 官方包。你去 npmjs.com 看一眼就知道,react-scripts 是 Create React App (CRA) 的核心引擎。 当你运行 npx react-scripts eject 时,程序执行流会进入 react-scripts 的 scripts/eject.js 文件。这是整个过程的起点。 这个脚本非常短,核心逻辑就是调用 eject 模块。我们来看这段代码: // react-scripts/scripts/eject.js const path = require('path'); const fs = require('fs-extra'); const chalk = require('chalk'); const semver = require('semver'); const os = require('os'); const execSync = require('child_process').execSync; const {checkDeps,getDependency,getDependencyVersion,requiredConfigFiles, } = require('../config/paths'); const {resolveModule,resolveModuleForRelativePath, } = require('../config/resolveModule');function printGreeting() {console.log(chalk.yellow('NOTE: Create React App now packages Webpack and Babel configurations as well.'));console.log(chalk.yellow('You will now be able to make changes to these files, but you will be on your own when upgrading this package in the future.'));console.log(chalk.yellow('Refactoring is not automatic. If you need to upgrade, please check the changelog to see what changed.')); }function printWarning() {console.log(chalk.red.bold('\nAre you sure you want to eject? This action is permanent.'));console.log(chalk.yellow('Press ctrl+c to exit. Press enter to continue.')); }function eject() {const appPackageJson = require(paths.appPackageJson);const dependencies = appPackageJson.dependencies;// 检查是否已经 eject 过if (fs.existsSync(paths.appBuildFile)) {console.log(chalk.red('This project has already been ejected. Aborting.'));process.exit(1);}// 复制配置文件const files = {'public/index.html': 'public/index.html','src/index.js': 'src/index.js','src/App.js': 'src/App.js','src/index.css': 'src/index.css','src/App.css': 'src/App.css','src/App.test.js': 'src/App.test.js','src/logo.svg': 'src/logo.svg',};// 执行复制逻辑... }// 主流程 printGreeting(); printWarning(); // ... 后续逻辑这段代码乍一看很复杂,但核心就两件事:警告用户和复制文件。 注意 printWarning 函数,它打印了红色加粗的警告,并让用户按回车确认。这是一个防呆设计,防止新手误操作。因为一旦 eject,你就无法再使用 react-scripts 提供的自动更新功能了。 核心片段:配置文件的搬运工 eject 的核心工作是把 react-scripts 内部的配置文件“搬运”到你的项目根目录。这些文件包括 webpack.config.js、babel.config.js、jest.config.js 等等。 让我们看 react-scripts/config/webpack.config.js 的部分源码。这是一个巨大的文件,但我们可以截取核心部分来看它是如何被引用的: // react-scripts/config/webpack.config.js (片段) const paths = require('./paths'); const modules = require('./modules'); const plugins = require('./plugins');module.exports = function (webpackEnv) {const isEnvDevelopment = webpackEnv === 'development';const isEnvProduction = webpackEnv === 'production';// 定义一些通用配置const commonConfig = {// 入口文件entry: paths.appIndexJs,// 输出配置output: {// 输出路径path: paths.appBuild,// 文件名规则filename: isEnvProduction ? 'static/js/[name].[contenthash:8].js' : 'static/js/bundle.js',// 公共路径publicPath: paths.publicUrlOrPath,// 其他配置...},// 模块解析配置resolve: {// 模块解析的扩展名extensions: paths.moduleFileExtensions,// 别名配置alias: {// 设置 @ 指向 src 目录'@': paths.appSrc,// 设置 components 指向 src/componentscomponents: paths.appComponents,},},// 加载器配置module: {strictExportPresence: true,rules: [// 禁用默认的 .mjs 解析{test: /\.mjs$/,include: /node_modules/,type: 'javascript/auto',},// 处理 JavaScript 文件{test: /\.(js|mjs|jsx|ts|tsx)$/,include: paths.appSrc,loader: require.resolve('babel-loader'),options: {customize: require.resolve('babel-preset-react-app/webpack-overrides'),presets: [[require.resolve('babel-preset-react-app'),{runtime: hasJsxRuntime ? 'automatic' : 'classic',},],],},},// 处理 CSS 文件{test: /\.css$/,use: [isEnvProduction MiniCssExtractPlugin.loader,require.resolve('css-loader'),require.resolve('postcss-loader'),].filter(Boolean),},],},// 插件配置plugins: [// 生成 HTML 文件new HtmlWebpackPlugin({inject: true,template: paths.appHtml,}),// 其他插件...],};// 根据环境返回不同的配置if (isEnvDevelopment) {return {...commonConfig,mode: 'development',// 开发环境特有配置devServer: {client: {overlay: false,},},};}if (isEnvProduction) {return {...commonConfig,mode: 'production',// 生产环境特有配置optimization: {minimize: true,},};} };这段代码展示了 react-scripts 如何构建 webpack 配置。它区分了开发环境和生产环境,并使用了不同的加载器和插件。 当 eject 发生时,这个 webpack.config.js 文件会被复制到你的项目根目录。此时,你就拥有了完全的控制权。你可以修改 entry、output、module.rules 等任何配置。 但这里有个坑:你复制过去的文件是静态的。react-scripts 内部可能会根据 Node 版本、操作系统等因素动态调整配置,但这些逻辑在复制后的文件中是丢失的。你需要自己处理这些边界情况。 设计思想:为什么 CRA 要设计 eject? ejected 的设计思想是渐进式复杂度。 CRA 的初衷是让开发者专注于业务逻辑,而不是构建工具。它通过封装 webpack、babel、eslint 等工具,提供了一套“开箱即用”的方案。但总有开发者需要定制化的配置,比如添加自定义的 loader、修改输出路径、配置代理等等。 eject 就是为这部分开发者准备的“逃生舱”。它允许你在需要时跳出封装,直接操作底层配置。 这种设计的优点在于:降低入门门槛:新手不需要懂 webpack,直接 create-react-app 就能开始写代码。 保留灵活性:当遇到瓶颈时,可以通过 eject 获取完全的控制权。 隔离风险:react-scripts 的更新不会直接影响已 eject 的项目,因为你已经拥有了独立的配置文件。但缺点也很明显:不可逆:一旦 eject,就无法回到 react-scripts 的管理下。 维护成本高:你需要自己维护 webpack、babel 等配置的兼容性。 社区支持减弱:CRA 社区主要支持未 eject 的项目,eject 后的问题往往需要自己解决。这也是为什么现在很多团队推荐使用 craco 或 react-app-rewired 这类库。它们允许你在不 eject 的情况下修改 react-scripts 的配置,兼顾了灵活性和维护性。 手写简化版:实现一个 mini-eject 为了彻底理解 eject 的原理,我们来手写一个简化版。假设我们的项目结构如下: my-app/ ├── node_modules/ ├── src/ │ ├── index.js │ ├── App.js │ └── index.css ├── public/ │ └── index.html ├── package.json └── mini-eject.js我们的 mini-eject.js 脚本将执行以下操作:检查是否已经 eject 过。 从 react-scripts 中复制 webpack.config.js 和 babel.config.js 到项目根目录。 修改 package.json 中的 scripts,将 start、build、test 命令指向本地的配置文件。// mini-eject.js const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process');const projectRoot = process.cwd(); const reactScriptsPath = path.resolve(projectRoot, 'node_modules', 'react-scripts');// 1. 检查是否已经 eject 过 if (fs.existsSync(path.join(projectRoot, 'webpack.config.js'))) {console.log('Project has already been ejected.');process.exit(0); }// 2. 复制配置文件 const filesToCopy = [{from: path.join(reactScriptsPath, 'config', 'webpack.config.js'),to: path.join(projectRoot, 'webpack.config.js'),},{from: path.join(reactScriptsPath, 'config', 'babel.config.js'),to: path.join(projectRoot, 'babel.config.js'),}, ];filesToCopy.forEach(({ from, to }) = {console.log(`Copying ${from} to ${to}...`);fs.copyFileSync(from, to); });// 3. 修改 package.json const packageJsonPath = path.join(projectRoot, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));packageJson.scripts = {...packageJson.scripts,start: 'react-scripts start --config webpack.config.js',build: 'react-scripts build --config webpack.config.js',test: 'react-scripts test --config babel.config.js', };fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));console.log('Ejection complete!'); console.log('Remember to update your dependencies if you modify the configs.');这个简化版脚本展示了 eject 的核心逻辑:复制文件和修改脚本。 实际运行 react-scripts eject 时,逻辑会更复杂,包括处理依赖关系、生成 README 文件、更新 package.json 中的依赖版本等。但核心思想是一致的。 应用场景:什么时候该用 eject? 虽然不推荐轻易 eject,但在某些场景下,它是必要的:深度定制构建流程:比如你需要自定义 webpack 的 target、externals、optimization 等配置,且 craco 无法满足需求。 迁移到 Vite:有些团队选择 eject 后,将 webpack 配置迁移到 Vite,以获得更快的构建速度。 学习构建工具:通过阅读 react-scripts 的源码,理解 webpack 和 babel 的工作机制,是提升工程化能力的好方法。但更常见的做法是使用 craco 或 react-app-rewired。它们允许你通过配置文件修改 react-scripts 的行为,而不需要复制整个配置文件。例如,使用 craco 你可以这样修改 webpack 配置: // craco.config.js module.exports = {webpack: {alias: {'@': path.resolve(__dirname, 'src'),},plugins: [new HtmlWebpackPlugin({inject: true,template: path.resolve(__dirname, 'public/index.html'),}),],}, };这种方式更轻量,也更容易维护。 在面试中,如果问到 ejected,你可以这样回答:ejected 是 Create React App 提供的一个命令,用于将 react-scripts 的构建配置复制到项目根目录,从而允许开发者完全自定义 webpack、babel 等配置。这个过程是不可逆的,一旦 eject,就需要自己维护这些配置的兼容性。虽然提供了灵活性,但增加了维护成本。因此,在实际项目中,我们更倾向于使用 craco 或 react-app-rewired 这类库,在不 eject 的情况下修改配置,兼顾了灵活性和可维护性。这个回答既展示了你对 ejected 的理解,也体现了你对工程化实践的思考。 结尾互动 ejected 是 CRA 历史上的一个重要节点,它反映了前端构建工具从“黑盒”到“白盒”的演变过程。虽然现在使用频率降低了,但理解它的原理,对于掌握前端工程化至关重要。 你在项目中遇到过必须 eject 的情况吗?或者你有更好的替代方案?还有什么不懂的?评论区留言挨个回。
返回列表