前言
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 start.js name=Toby age sex=1
携带的参数为:"name=Toby", "age", "sex=1"
process.cwd()
Node.js 进程的当前工作目录,即当前项目的根目录。
process.env
返回包含用户环境的对象,包括一些进程相关的信息和系统环境变量。
但在代码中我们通常很少用到这些环境变量,更多的是将其当作一个全局的对象来保存变量。
例如:
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)
两个相互对应的方法。前者将路径对象转换为路径,后者将路径转换为路径对象。
对象属性示例如下:
{
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.js
和 env.js
。
paths.js
paths.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 方法
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
获取当前项目根目录的绝对路径,将传入的文件或目录的相对路径拼接到根目录上,返回一个绝对路径。
resolveModule 方法
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 方法
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
方法具体做了什么。
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
,为后续注入进程序做准备。
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
的值相关的配置文件,看起来是动态选取配置文件的逻辑。
接下来,借助 dotenv
和 dotenv-expand
库,将配置文件中的变量注入到 Node.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
中注册的路径中寻找。
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.js
和env.js
。
下一篇文章,将介绍 webpack
的配置代码以及项目构建时 webpack
是如何进行工作的。