简单看看 React 脚手架中的 Webpack 做了什么?(1)

前言

React 项目的构建,是基于 Node.js 的。而 webpack 作为一个静态模块打包工具,在构建过程中发挥着至关重要的优化作用:模块化打包、性能优化、资源管理等。这大大简化了项目的构建流程,提高了开发效率和用户体验。

这次我们来看看 React 脚手架项目(通过 create-react-app 创建)中的 webpack 究竟做了哪些工作?

将脚手架中的 webpack 相关文件暴露出来后,我发现不管是配置文件还是脚本文件,里面都涉及到了 Node.js 的 API,像是 process, fs, path,出现的频率非常高。所以这篇文章,我们先了解一下 Node.js 相关的内容。

一、什么是 Node.js?

Node.js 是一个基于 V8 JavaScript 引擎 构建的 JavaScript 运行时。

这是官方对 Node.js 的描述。换句话说,Node.js 是一个可以运行 JavaScript 的环境。浏览器也是一个可以运行 JavaScript 的环境,这是我们平时接触得比较多的。

知道了什么是 Node.js,我们对 React 项目的构建过程就有了更深的理解:先是构建相关的代码在 Node.js 下运行 ,经过 webpack 的处理会输出一些部署项目所需的文件 。然后 webpack 会在本地起一个服务,并将这些文件部署在本地的服务器上,接着我们就可以在浏览器中运行项目了,项目中的代码则是运行在浏览器环境下的。

二、Node.js 相关的 API

简单了解一下构建命令和 webpack 配置中涉及到的 Node.js 的 API。

process

process 对象提供有关当前 Node.js 进程的信息并对其进行控制。

process.argv

process.argv 返回一个数组。

其中第1个元素是 process.execPath,表示 Node.js 进程的可执行文件的绝对路径名,可执行文件即 node.exe,也就是安装 Node.js 的绝对路径。

第2个元素是正在执行的 js 文件的绝对路径。如果 js 文件是被其它文件引用的,那么返回的是主动引用的 js 文件。

其余的元素则是命令行中携带的参数。例:

node 复制代码
    node start.js name=Toby age sex=1

携带的参数为:"name=Toby", "age", "sex=1"

process.cwd()

Node.js 进程的当前工作目录,即当前项目的根目录。

process.env

返回包含用户环境的对象,包括一些进程相关的信息和系统环境变量。

但在代码中我们通常很少用到这些环境变量,更多的是将其当作一个全局的对象来保存变量。

例如:

js 复制代码
    process.env.BABEL_ENV = 'development';
    process.env.NODE_ENV = 'development';

这里定义了两个环境变量,并将其设置为开发环境 。若是开发环境则设置为 production

除了在代码中显式地定义和设置变量,还需要将其注入到程序中,这样在业务代码中才能获取使用。

可以使用 webpack 的 DefinePlugin 插件进行注入。

也可以将变量以键值对的形式写在 .env 文件中,通过 dotenv 库读取文件内容,再通过 dotenv-expand 库将其添加到 process.env 对象中,再进行注入。

path

path 模块提供了用于处理文件和目录的实用工具。

path.basename(string path, string extension)

返回文件路径的最后一部分,即文件名 的部分。可选参数 extension,省略对应的后缀名,区分大小写。

path.dirname(string path)

basename 方法相对应,返回文件(夹)所在目录的路径。

path.isAbsolute(string path)

判断路径是否为绝对路径

path.join(string ...paths)

接收多个路径片段,用特定于平台的分隔符作为定界符将其连接在一起,并规范化生成的路径。

path.normalize(string path)

规范化给定的路径。

path.format(Object pathObj) 和 path.parse(string path)

两个相互对应的方法。前者将路径对象 转换为路径 ,后者将路径 转换为路径对象

对象属性示例如下:

js 复制代码
    {
        root: '/',
        dir: '/home/user/dir',
        base: 'file.txt',
        ext: '.txt',
        name: 'file'
    }

path.relative(string from, string to)

解析为从 from 到 to 的相对路径。若 from 和 to 相同,则返回空字符串。

path.resolve(string ...paths)

将路径或路径片段的序列解析为绝对路径。

接收若干个路径片段,从右往左进行拼接,直至生成一个绝对路径。若未能生成绝对路径,则使用当前工作目录的路径。

未传入路径 ,则返回当前工作目录的绝对路径

补充

关于路径,再补充几点:

. 表示当前目录

.. 表示父级目录

/ 表示文件系统的根目录

fs

文件系统模块。

fs.existsSync(string path)

判断路径是否存在。

fs.realpathSync(string path)

通过解析 ., .. 和符号链接异步地计算规范路径名。

解析一个路径的真实路径(绝对路径)

补充

在 Node.js 中 ,当前工作目录不一定是当前文件所在的目录,而是在执行脚本时操作系统指定的默认目录(通常是启动脚本所在的目录)。

运行 start 脚本时,默认的当前工作目录是项目的根目录

三、两个重要的 js 文件

在正式介绍 webpack 相关配置之前,我们先来了解两个在相关代码中不可或缺的两个 js 文件:paths.jsenv.js

paths.js

paths.js 将项目中关键的文件和目录路径整合起来,方便调用。

js 复制代码
    module.exports = {
      dotenv: resolveApp('.env'),
      appPath: resolveApp('.'),
      appBuild: resolveApp(buildPath),
      appPublic: resolveApp('public'),
      appHtml: resolveApp('public/index.html'),
      appIndexJs: resolveModule(resolveApp, 'src/index'),
      appPackageJson: resolveApp('package.json'),
      appSrc: resolveApp('src'),
      appTsConfig: resolveApp('tsconfig.json'),
      appJsConfig: resolveApp('jsconfig.json'),
      yarnLockFile: resolveApp('yarn.lock'),
      testsSetup: resolveModule(resolveApp, 'src/setupTests'),
      proxySetup: resolveApp('src/setupProxy.js'),
      appNodeModules: resolveApp('node_modules'),
      appWebpackCache: resolveApp('node_modules/.cache'),
      appTsBuildInfoFile: resolveApp('node_modules/.cache/tsconfig.tsbuildinfo'),
      swSrc: resolveModule(resolveApp, 'src/service-worker'),
      publicUrlOrPath,
    };

resolveApp 方法

js 复制代码
    const appDirectory = fs.realpathSync(process.cwd());
    const resolveApp = relativePath => path.resolve(appDirectory, relativePath);

获取当前项目根目录的绝对路径,将传入的文件或目录的相对路径拼接到根目录上,返回一个绝对路径。

resolveModule 方法

js 复制代码
    const moduleFileExtensions = [
      'web.mjs',
      'mjs',
      'web.js',
      'js',
      'web.ts',
      'ts',
      'web.tsx',
      'tsx',
      'json',
      'web.jsx',
      'jsx',
    ];

    const resolveModule = (resolveFn, filePath) => {
      const extension = moduleFileExtensions.find(extension =>
        fs.existsSync(resolveFn(`${filePath}.${extension}`))
      );

      if (extension) {
        return resolveFn(`${filePath}.${extension}`);
      }

      return resolveFn(`${filePath}.js`);
    };

由于部分模块文件的后缀名不确定,可能为 js, ts 等,因此对各个后缀名进行遍历,确定真实的后缀名后调用 resolveApp 方法返回其绝对路径。

getPublicUrlOrPath 方法

js 复制代码
    const publicUrlOrPath = getPublicUrlOrPath(
      process.env.NODE_ENV === 'development',
      require(resolveApp('package.json')).homepage,
      process.env.PUBLIC_URL
    );

除此之外,还需要设置 public path,当浏览器加载页面的静态资源 时,便是根据 public path 去服务器上寻找对应的资源进行加载的。

在这里,从 react-dev-utils 库引入了 getPublicUrlOrPath 方法并传入3个参数:判断是否为开发环境;读取 package.json 中的 homepage 字段;读取环境变量中的 PUBLIC_URL

接下来,看看 getPublicUrlOrPath 方法具体做了什么。

js 复制代码
      const stubDomain = 'https://create-react-app.dev';
      
      if (homepage) {
        homepage = homepage.endsWith('/') ? homepage : homepage + '/';

        const validHomepagePathname = new URL(homepage, stubDomain).pathname;
        
        return isEnvDevelopment
          ? homepage.startsWith('.')
            ? '/'
            : validHomepagePathname
          :
          homepage.startsWith('.')
          ? homepage
          : validHomepagePathname;
      }

首先,补全路径末尾的斜杠,保证后续拼接路径正确。

接着,创建一个 URL 对象,取其有效路径 的部分(网址中主机名之后的部分,且不包括查询参数和哈希部分)。stubDomain 的作用是保证能够成功创建 URL,并且不影响到有效路径的部分。

经过这一步处理,将会得到一个正确的绝对路径

最后,根据项目环境的不同以及 homepage 的值返回对应的 public path

在目前的项目环境下,homepage 的值为 .,最终得到的 public path/,即为项目根目录。

这是关于 homepage 的处理,关于 PUBLIC_URL 的处理逻辑也大同小异的,就不再赘述了。

env.js

env.js 处理了进程的环境变量 process.env,为后续注入进程序做准备。

js 复制代码
    const dotenvFiles = [
      `${paths.dotenv}.${NODE_ENV}.local`,
      NODE_ENV !== 'test' && `${paths.dotenv}.local`,
      `${paths.dotenv}.${NODE_ENV}`,
      paths.dotenv,
    ].filter(Boolean);

    dotenvFiles.forEach(dotenvFile => {
      if (fs.existsSync(dotenvFile)) {
        require('dotenv-expand')(
          require('dotenv').config({
            path: dotenvFile,
          })
        );
      }
    });

首先,构建一个环境变量配置文件路径的数组 。这里面包括最常见的 .env 文件,还有与 NODE_ENV 的值相关的配置文件,看起来是动态选取配置文件的逻辑。

接下来,借助 dotenvdotenv-expand 库,将配置文件中的变量注入Node.js 进程的环境变量中去。

js 复制代码
    const appDirectory = fs.realpathSync(process.cwd());
    process.env.NODE_PATH = (process.env.NODE_PATH || '')
      .split(path.delimiter)
      .filter(folder => folder && !path.isAbsolute(folder))
      .map(folder => path.resolve(appDirectory, folder))
      .join(path.delimiter);

这一段代码的作用是将环境变量 NODE_PATH 中的相对路径都转换为绝对路径

关于 NODE_PATH 的作用,是用于注册模块所提供的路径的环境变量。举个例子:

比如说,在项目中引入一个模块时,会先寻找项目根目录的 node_modules 目录,再寻找文件系统根目录的 node_modules,最终会去到 NODE_PATH 中注册的路径中寻找。

js 复制代码
    const REACT_APP = /^REACT_APP_/i;
    function getClientEnvironment(publicUrl) {
      const raw = Object.keys(process.env)
        .filter(key => REACT_APP.test(key))
        .reduce(
          (env, key) => {
            env[key] = process.env[key];
            return env;
          },
          {
            NODE_ENV: process.env.NODE_ENV || 'development',
            PUBLIC_URL: publicUrl,
            WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
            WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
            WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
            FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
          }
        );

      const stringified = {
        'process.env': Object.keys(raw).reduce((env, key) => {
          env[key] = JSON.stringify(raw[key]);
          return env;
        }, {}),
      };

      return { raw, stringified };
    }

最后,暴露出一个 getClientEnvironment 方法。

这个方法将 process.env 中以 REACT_APP_ 开头的环境变量筛选出来,并添加了几个例如 NODE_ENV, PUBLIC_PATH 的环境变量整合在一起。另外将上述环境变量对象转换为字符串形式 ,便于通过 DefinePlugin 注入到程序之中。

总结

在这篇文章中,我们了解了 Node.js 的基本概念和常用 API,以及两个在 webpack 配置中将会用到的 js 文件paths.jsenv.js

下一篇文章,将介绍 webpack 的配置代码以及项目构建时 webpack 是如何进行工作的。

相关推荐
Y学院5 分钟前
vue的组件通信
前端·javascript·vue.js
PairsNightRain8 分钟前
React Concurrent Mode 是什么?怎么使用?
前端·react.js·前端框架
小岛前端22 分钟前
React 剧变!
前端·react.js·前端框架
teeeeeeemo33 分钟前
Webpack 模块联邦(Module Federation)
开发语言·前端·javascript·笔记·webpack·node.js
岁月宁静1 小时前
AI聊天系统 实战:打造优雅的聊天记录复制与批量下载功能
前端·vue.js·人工智能
小小弯_Shelby1 小时前
uniApp App内嵌H5打开内部链接,返回手势(左滑右滑页面)会直接关闭H5项目
前端·uni-app
IT_陈寒1 小时前
SpringBoot性能飞跃:5个关键优化让你的应用吞吐量提升300%
前端·人工智能·后端
加洛斯2 小时前
Vue 知识篇(2):浅谈Vue中的DOM与VNode
前端·javascript·vue.js
kunge1v52 小时前
学习爬虫第三天:数据提取
前端·爬虫·python·学习
可爱的秋秋啊2 小时前
简单网站编写
开发语言·前端