webpack全面解析(2) - 学习 creat-react-app 的 webpack 配置

1,244 阅读3分钟

1. 创建一个 react 的项目

首先,我们使用 npx create-react-app study-webpack-from-cra 创建一个 react 的项目;

然后,我们进入项目:cd study-webpack-from-cra

使用 npm run eject 的命令,使 webpack 相关的配置完全暴露出来;

执行完成后,我们会得到如下的部分目录结构:

// 此处仅列举了部分的文件夹和文件
|--config
    |-- env.js
    |-- paths.js
    |-- webpack.config.js
    |-- webpackDevServer.config.js
|-- scripts
    |-- build.js
    |-- start.js

2. 解析 webpack 配置

npm run build

我们先来看看我们比较常用的打包命令 npm run build,它对应到执行 scripts 文件夹下的 build 文件,我们将该文件贴出主要代码,并逐行注释讲解:

'use strict';
​
// 设置是进行生产模式处理,在 webpack.config.js 文件里会大量用到。
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
​
// 确保环境被读到。
// 这里值得说明的是,CommonJS 模块是运行时加载。当不同的文件共用process.env.NODE_ENV时,加载的时序就变得很重要。
// 如果将process.env.NODE_ENV = 'production' 的设置放在这句话之后,那么就会在env.js 抛出一个 NODE_ENV 不存在的错误。
require('../config/env');
​
// require(...)
// require(...)
// require(...)// 一些关于文件大小的测试、Yarn使用、测试 Node.js 是否在终端上下文中运行等
const measureFileSizesBeforeBuild =
  FileSizeReporter.measureFileSizesBeforeBuild;
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
const useYarn = fs.existsSync(paths.yarnLockFile);
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
  process.exit(1);
}
​
// 是否需要生成 stat json
const argv = process.argv.slice(2);
const writeStatsJson = argv.indexOf('--stats') !== -1;
​
// 从这里,正文开始了。
// 1. 拿到 webpack 在 production 模式下的配置。
const config = configFactory('production');
​
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
  .then(() => {
   return measureFileSizesBeforeBuild(paths.appBuild);
  })
  .then(previousFileSizes => {
    // 2. 检查浏览器、文件大小,并执行 build 
    return build(previousFileSizes);
  })
  
function build(previousFileSizes) {
  console.log('Creating an optimized production build...');
​
// 3. 进行打包
  const compiler = webpack(config);
  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
        // ………………
      return resolve(resolveArgs);
    });
  });
}

这里我删减了很多代码,并不是这些代码不重要,而是我们先把流程理解下,其中的部分代码,我们后面会详细讲解。

这里面主要就是三步:

  1. 拿到 webpackproduction 模式下的配置:const config = configFactory('production');
  2. 检查浏览器相关信息,并执行 build() 函数:checkBrowsers(paths.appPath, isInteractive);
  3. build函数中执行 webapckconst compiler = webpack(config);

我们通过断点,也能看到 config 中的 webpack 配置:

2.png

稍后在第三节中,我们会根据在 package.json 中传入不同的参数,来实现不同的打包方案。

npm run start

npm run start 命令的执行和 npm run build类似,只是要加入对应的 dev-server 的相关配置。 主要的区别也是在start.js文件中的checkBrowsers()函数下:

checkBrowsers(paths.appPath, isInteractive)
  .then(() => {
    // 给出端口占用的判断,也就是我们在启动时经常看到的端口占用是的提示
    // 该函数在react-dev-utils/WebpackDevServerUtils中定义
    return choosePort(HOST, DEFAULT_PORT);
  })
  .then(port => {
    // 获取配置
    const config = configFactory('development');
    // 获取协议
    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
    const appName = require(paths.appPackageJson).name;

    const useTypeScript = fs.existsSync(paths.appTsConfig);
    const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
    const urls = prepareUrls(
      protocol,
      HOST,
      port,
      paths.publicUrlOrPath.slice(0, -1)
    );
    const devSocket = {
      warnings: warnings =>
        devServer.sockWrite(devServer.sockets, 'warnings', warnings),
      errors: errors =>
        devServer.sockWrite(devServer.sockets, 'errors', errors),
    };
    // 创建一个 webpack 的 compiler
    // 这里主要是为了在 react-dev-utils/WebpackDevServerUtils.js 文件中定义一些钩子,
    const compiler = createCompiler({
      appName,
      config,
      devSocket,
      urls,
      useYarn,
      useTypeScript,
      tscCompileOnError,
      webpack,
    });
    // 一些配置
    const proxySetting = require(paths.appPackageJson).proxy;
    const proxyConfig = prepareProxy(
      proxySetting,
      paths.appPublic,
      paths.publicUrlOrPath
    );
    const serverConfig = createDevServerConfig(
      proxyConfig,
      urls.lanUrlForConfig
    );
    // 执行。
    const devServer = new WebpackDevServer(compiler, serverConfig);
    // Launch WebpackDevServer.
    devServer.listen(port, HOST, err => {
      if (err) {
        return console.log(err);
      }
      if (isInteractive) {
        clearConsole();
      }

      if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
        console.log(
          chalk.yellow(
            `Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
          )
        );
      }

      console.log(chalk.cyan('Starting the development server...\n'));
      openBrowser(urls.localUrlForBrowser);
    });

    ['SIGINT', 'SIGTERM'].forEach(function (sig) {
      process.on(sig, function () {
        devServer.close();
        process.exit();
      });
    });

    if (process.env.CI !== 'true') {
      // Gracefully exit when stdin ends
      process.stdin.on('end', function () {
        devServer.close();
        process.exit();
      });
    }
  })
  .catch(err => {
    if (err && err.message) {
      console.log(err.message);
    }
    process.exit(1);
  });

这里,我们通过命令行的方式,执行了 webpack-dev-server的服务。

主要的逻辑在于这几行:

var compiler = webpack(config);
const devServer = new WebpackDevServer(compiler, serverConfig);
devServer.listen(port, () => {
    
})