前置知识
monorepo管理
- Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)
- monorepo 最主要的好处是统一的工作流和代码共享
- Lerna是一个管理多个 npm 模块的工具,优化维护多包的工作流,解决多个包互相依赖,且发布需要手动维护多个包的问题
- yarn
monorepo优势
-
Monorepo
最主要的好处是统一的工作流和代码共享 -
目前大多数开源库都使用Monorepo进行管理,如react、vue-next、create-react-app
monorepo劣势
- 体积庞大。
babel
仓库下存放了所有相关代码,clone
到本地也需要耗费不少时间。 - 不适合用于公司项目。各个业务线仓库代码基本都是独立的,如果堆放到一起,理解和维护成本将会相当大
Lerna
安装
npm i lerna -g
初始化
lerna init
package.json
package.json
{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^3.22.1"
}
}
lerna.json
lerna.json
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}
yarn workspace
yarn workspace
允许我们使用monorepo
的形式来管理项目- 在安装 node_modules 的时候它不会安装到每个子项目的 node_modules 里面,而是直接安装到根目录下面,这样每个子项目都可以读取到根目录的 node_modules
- 整个项目只有根目录下面会有一份
yarn.lock
文件。子项目也会被link
到node_modules
里面,这样就允许我们就可以直接用 import 导入对应的项目 yarn.lock
文件是自动生成的,也完全Yarn来处理.yarn.lock
锁定你安装的每个依赖项的版本,这可以确保你不会意外获得不良依赖
开启workspace
package.json
{
"name": "root",
"private": true, // 私有的,用来管理整个项目,不会被发布到npm
+ "workspaces": [
+ "packages/*"
+ ],
"devDependencies": {
"lerna": "^3.22.1"
}
}
创建子项目
lerna create create-react-app3
lerna create react-scripts3
lerna create cra-template3
添加依赖
设置加速镜像
yarn config get registry
yarn config set registry http://registry.npm.taobao.org/
yarn config set registry http://registry.npmjs.org/
作用 | 命令 |
---|---|
查看工作空间信息 | yarn workspaces info |
给根空间添加依赖 | yarn add chalk cross-spawn fs-extra --ignore-workspace-root-check |
给某个项目添加依赖 | yarn workspace create-react-app3 add commander |
删除所有的 node_modules | lerna clean 等于 yarn workspaces run clean |
安装和link | yarn install 等于 lerna bootstrap --npm-client yarn --use-workspaces |
重新获取所有的 node_modules | yarn install --force |
查看缓存目录 | yarn cache dir |
清除本地缓存 | yarn cache clean |
commander
- chalk可以在终端显示颜色
- commander是一个完整的
node.js
命令行解决方案 - version方法可以设置版本,其默认选项为
-V
和--version
- 通过.arguments可以为最顶层命令指定参数,对子命令而言,参数都包括在.command调用之中了。尖括号(例如)意味着必选,而方括号(例如[optional])则代表可选
- 通过
usage
选项可以修改帮助信息的首行提示
const chalk = require('chalk');
const {Command} = require('commander');
console.log('process.argv',process.argv);
new Command('create-react-app')
.version('1.0.0')
.arguments('<must1> <must2> [optional]')
.usage(`${chalk.green('<must1> <must2>')} [optional]`)
.action((must1,must2,optional,...args) => {
console.log(must1,must2,optional,args);
})
.parse(process.argv);
cross-spawn
- cross-spawn是node的
spawn
和spawnSync
的跨平台解决方案 - inherit表示将相应的
stdio
流传给父进程或从父进程传入
const spawn = require('cross-spawn');
const child = spawn('node', ['script.js','one','two','three'], { stdio: 'inherit' });
child.on('close',()=>{
console.log('child is done!');
});
const result = spawn.sync('node', ['script.js','one','two','three'], { stdio: 'inherit' });
console.log(result);
dotenv
- 使用dotenv,只需要将程序的环境变量配置写在
.env
文件中
.env
MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DB=test
MONGODB_URI=mongodb://${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DB}
const dotenvFile = '.env';
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
})
);
console.log(process.env.MONGODB_HOST);
console.log(process.env.MONGODB_PORT);
console.log(process.env.MONGODB_DB);
console.log(process.env.MONGODB_URI);
NODE_PATH
- NODE_PATH 就是NODE中用来寻找模块所提供的路径注册环境变量
let fs = require('fs');
let path = require('path');
const appDirectory = fs.realpathSync(process.cwd());
//NODE_PATH folder1;folder2
process.env.NODE_PATH = [
'./a',
path.resolve(appDirectory, 'b'),
'./c',
].join(path.delimiter);
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)//windows ; mac :
.filter(folder => folder && !path.isAbsolute(folder))//不是绝对路径的删除
.map(folder => path.resolve(appDirectory, folder))//只能是相对目录
.join(path.delimiter);//再连接在一起
console.log(process.env.NODE_PATH);
semver-regex
- 匹配semver版本的正则表达式
const semverRegex = require('semver-regex');
console.log(semverRegex().test('v1.0.0'));
//=> true
console.log(semverRegex().test('1.2.3-alpha.10.beta.0+build.unicorn.rainbow'));
//=> true
console.log(semverRegex().exec('unicorn 1.0.0 rainbow')[0]);
//=> '1.0.0'
console.log('unicorn 1.0.0 and rainbow 2.1.3'.match(semverRegex()));
//=> ['1.0.0', '2.1.3']
const semver = require('semver')
console.log(semver.valid('1.2.3')); // '1.2.3'
console.log(semver.valid('a.b.c')); // null
console.log(semver.clean(' =v1.2.3 ')); // '1.2.3'
console.log(semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')); // true
console.log(semver.gt('1.2.3', '9.8.7')); // false
console.log(semver.lt('1.2.3', '9.8.7')); // true
console.log(semver.valid(semver.coerce('v2'))); // '2.0.0'
console.log(semver.valid(semver.coerce('42.6.7.9.3-alpha'))); // '42.6.7'
globby
- globby是基于 glob,并进一步得到了增强
var globby = require('globby');
(async () => {
const paths = await globby(['images','photos'],{
expandDirectories: true
});
console.log(paths);
})();
globalThis
- globalThis提供统一的机制来访问全局对象
- Web 浏览器 window
- Node.js global
- Web Worker self
// 浏览器环境
console.log(globalThis); // => Window {...}
// node.js 环境
console.log(globalThis); // => Object [global] {...}
// web worker 环境
console.log(globalThis); // => DedicatedWorkerGlobalScope {...}
bfj
const bfj = require('bfj');
bfj.read('big.json')
.then(data => {
console.log(data);
})
.catch(error => {
console.log(error)
});
pnp
yarn install
- 1.将依赖包的版本区间解析为某个具体的版本号
- 2.下载对应版本依赖的 tar 包到本地离线镜像
- 3.将依赖从离线镜像解压到本地缓存
- 4.将依赖从缓存拷贝到当前目录的 node_modules 目录
Pnp
- PnP工作原理是作为把依赖从缓存拷贝到 node_modules 的替代方案
使用
启用
#create-react-app 已经集成了对 PnP 的支持。只需在创建项目时添加 --use-pnp 参数
npx create-react-app react-project --use-pnp
# 在已有项目中开启 PnP
yarn --pnp
yarn add uuid
package.json
- 只要 installConfig.pnp 的值是一个真值且当前版本的 Yarn 支持,PnP 特性就会被启用
{
"installConfig": {
"pnp": true
}
}
uuid.js
uuid.js
let uuid = require('uuid');
console.log(uuid.v4());
运行
- 由于在开启了 PnP 的项目中不再有 node_modules 目录,所有的依赖引用都必须由 .pnp.js 中的 resolver 处理
- 因此不论是执行 script 还是用 node 直接执行一个 JS 文件,都必须经由 Yarn 处理
yarn run build
yarn node uuid.js
npx安装依赖
npm 5.2.0之后提供了npx命令,当调用npx <command>
,而你的$PATH
没有<command>
时,将会自动安装这个名字的包,并执行它。执行完后,这个安装下来的包会被清理掉。 使用npx执行cra,能够保证你执行的脚手架始终是最新的版本。关于npx具体的文档可以参考这里blog.npmjs.org/post/162869…
This feature is ideal for things like generators, too. Tools like
yeoman
orcreate-react-app
only ever get called once in a blue moon. By the time you run them again, they’ll already be far out of date, so you end up having to run an install every time you want to use them anyway.
目录结构
create-react-app是以learn组织的monorepo。是一个多仓库的项目,主要代码都在packages下的各个包中
-
https://github.com/facebook/create-react-app
-
https://github.com/facebook/create-react-app/releases
-
目前react-react-app脚手架最新版本是
5.0.1
-
Create React App 5.0.1 is a maintenance release that improves compatibility with React 18. We’ve also updated our templates to use
createRoot
and relaxed our check for older versions of Create React App. -
packages下有11个目录
babel-plugin-named-asset-import babel-preset-react-app confusing-browser-globals cra-template cra-template-typescript create-react-app eslint-config-react-app react-app-polyfill react-dev-utils react-error-overlay react-scripts
-
babel-plugin-named-asset-import
- 允许将资源文件通过自定义的命名导入
-
babel-preset-react-app
- react相关babel preset预设,用于将 JavaScript(包括 JSX 语法和最新的 JavaScript 特性)转换为在大多数浏览器中兼容的 JavaScript
-
confusing-browser-globals
- 列出了在浏览器环境中容易混淆或产生误导的全局变量,eslint的一个插件,限制全局变量的使用
-
cra-template
- 默认模板,它提供了一个基础的 React 应用结构。该模板包含了一些基本的文件和配置
-
cra-template-typescript
- 它提供了一个基础的 TypeScript React 应用结构,并包含了必要的 TypeScript 配置
-
create-react-app
- 提供了create-react-app命令及选项
- 检测环境:如node版本、cra版本、yarn还是npm、是否断网、是否开启pnp,以及yarn是否支持pnp
- 获取模板
- 下载依赖(react、react-dom以及模板)
- 执行react-scripts下的init方法
-
eslint-config-react-app
- 含了一系列的 ESLint 规则和插件,用于确保代码质量和一致性。该配置是为 React 项目量身定制的
-
react-app-polyfill
- 包含了一些用于支持较旧浏览器(如 IE11)的 polyfills。这些 polyfills 可以确保 React 应用能够在不支持某些现代 JavaScript 特性的浏览器中正常运行
-
react-dev-utils
- 各种webpack插件、工具函数
-
react-error-overlay
- 显示友好的错误信息和调试提示
-
react-scripts
- 一组脚本和配置,包含了用于启动、构建和测试 React 应用的所有必要工具和配置
- 提供init方法:读取template包并生成项目;修改package.json;删除template依赖
- 提供项目内几个命令:start、build、test、eject
工作流程
- 运行create-react-app命令后的执行流程大致如下
-
这个过程中包含了许多细节
-
当node版本过低时将会降低react-scripts版本以尽可能成功
-
支持yarn和npm,可选pnp特性,并且检查了用户环境是否支持这些命令和特性
-
当用户离线时使用缓存安装
-
检查项目名是否和package包名重复
-
如果本地文件与生成的文件有冲突时进行了小心的处理
-
核心
create-react-app
index.js
-
判断当前运行node版本,小于V14,抛错,退出;否则继续向下执行,进入init
createReactApp.js
init
-
首先是解析用户输入的内容,代码如下:
-
const program = new commander.Command(packageJson.name)…option()…parse(process.argv)
-
注意create-react-app命令并不是commander提供的,commander起到的作用是解析用户输入的内容并给出反馈。
-
这一步通过用户输入获取几个变量
-
projectName
-
verbose:print additional logs
-
scriptsVersion:use a non-standard version of react-scripts
-
template:specify a template for the created project
-
usePnp
-
-
另外后续通过 (process.env.npm_config_user_agent || ‘’).indexOf(‘yarn’)获取到了另一个重要变量useYarn;
-
接下来就是检查cra的版本checkForLatestVersion,
checkForLatestVersion
cra最新版本号
-
获取cra最新版本号用到2个方法
-
// 正常获取时 https.get('https://registry.npmjs.org/-/package/create-react-app/dist-tags'
-
// 出错时 execSync('npm view create-react-app version').toString().trim();
-
接下来就进入createApp方法
createApp
创建react app
-
首先检测Node版本是否支持
-
然后做输入路径检查
-
const root = path.resolve(name); const appName = path.basename(root);
-
-
调用checkAppName方法
-
继续执行
fs.ensureDirSync
(fs为三方库fs-extra
)进行异步创建目录 -
判断
isSafeToCreateProjectIn(root,name)
冲突文件conflicts.length
为0,移除log
文件,返回true
-
写入package.json
-
process.chdir()方法是过程模块的内置应用程序编程接口,用于更改当前工作目录
-
通过
checkThatNpmCanReadCwd
检查Npm是否可以读取Cwd -
如果不使用
yarn
,使用的是npm
,需要检查npm
的版本,执行的npmInfo
-
执行
run
checkAppName
-
通过
validate-npm-package-name
进行检查,另外还检查了是否和'react', 'react-dom', 'react-scripts'
重名
isSafeToCreateProjectIn
判断
isSafeToCreateProjectIn(root,name)
冲突文件conflicts.length
为0,移除log
文件,返回true
checkThatNpmCanReadCwd
run
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp
) {
Promise.all([
getInstallPackage(version, originalDirectory),
getTemplateInstallPackage(template, originalDirectory),
]).then(([packageToInstall, templateToInstall]) => {
const allDependencies = ['react', 'react-dom', packageToInstall];
console.log('Installing packages. This might take a couple of minutes.');
Promise.all([
getPackageInfo(packageToInstall),
getPackageInfo(templateToInstall),
])
.then(([packageInfo, templateInfo]) =>
checkIfOnline(useYarn).then(isOnline => ({
isOnline,
packageInfo,
templateInfo,
}))
)
.then(({ isOnline, packageInfo, templateInfo }) => {
let packageVersion = semver.coerce(packageInfo.version);
const templatesVersionMinimum = '3.3.0';
// Assume compatibility if we can't test the version.
if (!semver.valid(packageVersion)) {
packageVersion = templatesVersionMinimum;
}
// Only support templates when used alongside new react-scripts versions.
const supportsTemplates = semver.gte(
packageVersion,
templatesVersionMinimum
);
if (supportsTemplates) {
allDependencies.push(templateToInstall);
} else if (template) {
console.log('');
console.log(
`The ${chalk.cyan(packageInfo.name)} version you're using ${
packageInfo.name === 'react-scripts' ? 'is not' : 'may not be'
} compatible with the ${chalk.cyan('--template')} option.`
);
console.log('');
}
console.log(
`Installing ${chalk.cyan('react')}, ${chalk.cyan(
'react-dom'
)}, and ${chalk.cyan(packageInfo.name)}${
supportsTemplates ? ` with ${chalk.cyan(templateInfo.name)}` : ''
}...`
);
console.log();
return install(
root,
useYarn,
usePnp,
allDependencies,
verbose,
isOnline
).then(() => ({
packageInfo,
supportsTemplates,
templateInfo,
}));
})
.then(async ({ packageInfo, supportsTemplates, templateInfo }) => {
const packageName = packageInfo.name;
const templateName = supportsTemplates ? templateInfo.name : undefined;
checkNodeVersion(packageName);
setCaretRangeForRuntimeDeps(packageName);
const pnpPath = path.resolve(process.cwd(), '.pnp.js');
const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];
await executeNodeScript(
{
cwd: process.cwd(),
args: nodeArgs,
},
[root, appName, verbose, originalDirectory, templateName],
`
const init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
if (version === 'react-scripts@0.9.x') {
console.log(
chalk.yellow(
`\nNote: the project was bootstrapped with an old unsupported version of tools.\n` +
`Please update to Node >=14 and npm >=6 to get supported tools in new projects.\n`
)
);
}
})
.catch(reason => {
console.log();
console.log('Aborting installation.');
if (reason.command) {
console.log(` ${chalk.cyan(reason.command)} has failed.`);
} else {
console.log(
chalk.red('Unexpected error. Please report it as a bug:')
);
console.log(reason);
}
console.log();
// On 'exit' we will delete these files from target directory.
const knownGeneratedFiles = ['package.json', 'node_modules'];
const currentFiles = fs.readdirSync(path.join(root));
currentFiles.forEach(file => {
knownGeneratedFiles.forEach(fileToMatch => {
// This removes all knownGeneratedFiles.
if (file === fileToMatch) {
console.log(`Deleting generated file... ${chalk.cyan(file)}`);
fs.removeSync(path.join(root, file));
}
});
});
const remainingFiles = fs.readdirSync(path.join(root));
if (!remainingFiles.length) {
// Delete target folder if empty
console.log(
`Deleting ${chalk.cyan(`${appName}/`)} from ${chalk.cyan(
path.resolve(root, '..')
)}`
);
process.chdir(path.resolve(root, '..'));
fs.removeSync(path.join(root));
}
console.log('Done.');
process.exit(1);
});
});
}
getPackageInfo
-
确定要安装的包
-
确定要安装的react-scripts的版本,也支持其他的
react-scripts
-
确定template的包及版本,可以是cra-template,也支持其他的
template
-
安装的包还包括react、react-dom
checkIfOnline
- 检查网络
install
- 确定安装命令和参数,分别有
yarnpkg add --exact --offline --enable-pnp --cwd --verbose ...
npm install --no-audit --save --save-exact --loglevel error --verbose
- 这些参数都是根据上面的流程确定,以
spawn(command, args, { stdio: 'inherit' });
执行
setCaretRangeForRuntimeDeps
- 为指定的运行时依赖设置caret范围
executeNodeScript
- 执行了
react-scripts
包中的init方法
cra-template
模板目录
├───template/
│ ├───public/
│ │ ├───favicon.ico
│ │ ├───index.html
│ │ ├───logo192.png
│ │ ├───logo512.png
│ │ ├───manifest.json
│ │ └───robots.txt
│ ├───src/
│ │ ├───App.css
│ │ ├───App.js
│ │ ├───App.test.js
│ │ ├───index.css
│ │ ├───index.js
│ │ ├───logo.svg
│ │ ├───reportWebVitals.js
│ │ └───setupTests.js
│ ├───gitignore
│ └───README.md
├───package.json
├───README.md
└───template.json
template目录下便是项目待copy的文件,而template.json则是这个template需要继续安装的依赖。 这些操作全都由react-scripts完成
react-scripts
scripts\init.js
-
加载
cra-template
中的package.json
,读取dependencies
scripts
-
更新package.json中的scripts
-
更新package.json中的eslintConfig和browserslist
-
写入
package.json
-
拷贝模版项目到新建项目目录下
- 判断是否存在
.gitignore
- 初始化
git repo
- 确定执行
yarn
ornpm
- 判断是否安装react
-
安装依赖,通过
const proc = spawn.sync(command, [remove, templateName], {stdio: 'inherit',});
开启子进程执行安装命令 -
删除
template
-
git commit
- 显示最优雅的 cd 方式
- 打印成功信息的提示
scripts\start.js
要初始化
webpack
配置,通过webpack-dev-server
本地启动一个node服务
const fs = require('fs');
const chalk = require('react-dev-utils/chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
// 判断nodejs是否在终端运行
const isInteractive = process.stdout.isTTY;
// 校验入口文件
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// 设置默认的端口和HOST
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
/**
* checkBrowsers内部使用browserslist,会从can-i-use数据库判断css、js支持的版本
* 会先校验package.json文件里面有没有browserslist字段
* 1.如果有直接返回promise
* 2.没有的话会在终端询问是否要添加browserslist
*/
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
/**
* detect-port-alt校验当前端口是否被占用
* 如果被占用,提示是否使用另外的端口
*/
return choosePort(HOST, DEFAULT_PORT);
})
.then(port => {
// 返回当前端口
if (port == null) {
// We have not found a port.
return;
}
/**
* configFactory有以下功能
* 初始化webpack配置
* 1.定义入口文件、输出文件
* 2.定义规则:处理图片、字体、css、jsx
* 3.使用插件
* - HtmlWebpackPlugin 为html自动插入输出的js
* - MiniCssExtractPlugin css压缩插件
* - WebpackManifestPlugin 生成manifest.json
* - ESLintPlugin 配置一些eslint规则
*/
const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
// 判断是否使用ts
const useTypeScript = fs.existsSync(paths.appTsConfig);
// 通过协议、域名、端口组合成完成的地址字符串
const urls = prepareUrls(
protocol,
HOST,
port,
paths.publicUrlOrPath.slice(0, -1)
);
// 内部通过调用webpack(config) 生成一个compiler实例
const compiler = createCompiler({
appName,
config,
urls,
useYarn,
useTypeScript,
webpack,
});
// 获取package.json文件中的proxy字段
const proxySetting = require(paths.appPackageJson).proxy;
// 配置一些代理相关的信息
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
// 配置WebpackDevServer参数
// https://github.com/webpack/webpack-dev-server
const serverConfig = {
...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
host: HOST,
port,
};
// 创建本地服务器
const devServer = new WebpackDevServer(serverConfig, compiler);
// 服务启动后的回调
devServer.startCallback(() => {
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);
});
})
config/webpack配置
react-scripts\config\webpack.config.js
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// @remove-on-eject-end
'use strict';
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const ESLintPlugin = require('eslint-webpack-plugin');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin =
process.env.TSC_COMPILE_ON_ERROR === 'true'
? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
: require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// @remove-on-eject-begin
const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier');
// @remove-on-eject-end
const createEnvironmentHash = require('./webpack/persistentCache/createEnvironmentHash');
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
const reactRefreshRuntimeEntry = require.resolve('react-refresh/runtime');
const reactRefreshWebpackPluginRuntimeEntry = require.resolve(
'@pmmmwh/react-refresh-webpack-plugin'
);
const babelRuntimeEntry = require.resolve('babel-preset-react-app');
const babelRuntimeEntryHelpers = require.resolve(
'@babel/runtime/helpers/esm/assertThisInitialized',
{ paths: [babelRuntimeEntry] }
);
const babelRuntimeRegenerator = require.resolve('@babel/runtime/regenerator', {
paths: [babelRuntimeEntry],
});
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
// Check if Tailwind config exists
const useTailwind = fs.existsSync(
path.join(paths.appPath, 'tailwind.config.js')
);
// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc;
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
const hasJsxRuntime = (() => {
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
return false;
}
try {
require.resolve('react/jsx-runtime');
return true;
} catch (e) {
return false;
}
})();
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function (webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
isEnvProduction && process.argv.includes('--profile');
// We will provide `paths.publicUrlOrPath` to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
// Get environment variables to inject into our app.
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const shouldUseReactRefresh = env.raw.FAST_REFRESH;
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
config: false,
plugins: !useTailwind
? [
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
},
],
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
'postcss-normalize',
]
: [
'tailwindcss',
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
},
],
],
},
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
root: paths.appSrc,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
},
}
);
}
return loaders;
};
return {
target: ['browserslist'],
// Webpack noise constrained to errors and warnings
stats: 'errors-warnings',
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry: paths.appIndexJs,
output: {
// The build folder.
path: paths.appBuild,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
assetModuleFilename: 'static/media/[name].[hash][ext]',
// webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: paths.publicUrlOrPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
},
cache: {
type: 'filesystem',
version: createEnvironmentHash(env.raw),
cacheDirectory: paths.appWebpackCache,
store: 'pack',
buildDependencies: {
defaultWebpack: ['webpack/lib/'],
config: [__filename],
tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
fs.existsSync(f)
),
},
},
infrastructureLogging: {
level: 'none',
},
optimization: {
minimize: isEnvProduction,
minimizer: [
// This is only used in production mode
new TerserPlugin({
terserOptions: {
parse: {
// We want terser to parse ecma 8 code. However, we don't want it
// to apply any minification steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending further investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2,
},
mangle: {
safari10: true,
},
// Added for profiling in devtools
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true,
},
},
}),
// This is only used in production mode
new CssMinimizerPlugin(),
],
},
resolve: {
// This allows you to set a fallback for where webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebook/create-react-app/issues/253
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
// These are the reasonable defaults supported by the Node ecosystem.
// We also include JSX as a common component filename extension to support
// some tools, although we do not recommend using it, see:
// https://github.com/facebook/create-react-app/issues/290
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
// Allows for better profiling with ReactDevTools
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
},
plugins: [
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [
paths.appPackageJson,
reactRefreshRuntimeEntry,
reactRefreshWebpackPluginRuntimeEntry,
babelRuntimeEntry,
babelRuntimeEntryHelpers,
babelRuntimeRegenerator,
]),
],
},
module: {
strictExportPresence: true,
rules: [
// Handle node_modules packages that contain sourcemaps
shouldUseSourceMap && {
enforce: 'pre',
exclude: /@babel(?:\/|\\{1,2})runtime/,
test: /\.(js|mjs|jsx|ts|tsx|css)$/,
loader: require.resolve('source-map-loader'),
},
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// TODO: Merge this config once `image/avif` is in the mime-db
// https://github.com/jshttp/mime-db
{
test: [/\.avif$/],
type: 'asset',
mimetype: 'image/avif',
parser: {
dataUrlCondition: {
maxSize: imageInlineSizeLimit,
},
},
},
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: imageInlineSizeLimit,
},
},
},
{
test: /\.svg$/,
use: [
{
loader: require.resolve('@svgr/webpack'),
options: {
prettier: false,
svgo: false,
svgoConfig: {
plugins: [{ removeViewBox: false }],
},
titleProp: true,
ref: true,
},
},
{
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash].[ext]',
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
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',
},
],
],
// @remove-on-eject-begin
babelrc: false,
configFile: false,
// Make sure we have a unique cache identifier, erring on the
// side of caution.
// We remove this when the user ejects because the default
// is sane and uses Babel options. Instead of options, we use
// the react-scripts and babel-preset-react-app versions.
cacheIdentifier: getCacheIdentifier(
isEnvProduction
? 'production'
: isEnvDevelopment && 'development',
[
'babel-plugin-named-asset-import',
'babel-preset-react-app',
'react-dev-utils',
'react-scripts',
]
),
// @remove-on-eject-end
plugins: [
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},
// Process any JS outside of the app with Babel.
// Unlike the application JS, we only compile the standard ES features.
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'),
{ helpers: true },
],
],
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
// @remove-on-eject-begin
cacheIdentifier: getCacheIdentifier(
isEnvProduction
? 'production'
: isEnvDevelopment && 'development',
[
'babel-plugin-named-asset-import',
'babel-preset-react-app',
'react-dev-utils',
'react-scripts',
]
),
// @remove-on-eject-end
// Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines.
sourceMaps: shouldUseSourceMap,
inputSourceMap: shouldUseSourceMap,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'icss',
},
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'icss',
},
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
type: 'asset/resource',
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
].filter(Boolean),
},
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
// https://github.com/facebook/create-react-app/issues/5358
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// It will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
new ModuleNotFoundPlugin(paths.appPath),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
// Experimental hot reloading for React .
// https://github.com/facebook/react/tree/main/packages/react-refresh
isEnvDevelopment &&
shouldUseReactRefresh &&
new ReactRefreshWebpackPlugin({
overlay: false,
}),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
isEnvDevelopment && new CaseSensitivePathsPlugin(),
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate an asset manifest file with the following content:
// - "files" key: Mapping of all asset filenames to their corresponding
// output file so that tools can pick it up without having to parse
// `index.html`
// - "entrypoints" key: Array of files which are included in `index.html`,
// can be used to reconstruct the HTML if necessary
new WebpackManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the webpack build.
isEnvProduction &&
fs.existsSync(swSrc) &&
new WorkboxWebpackPlugin.InjectManifest({
swSrc,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
// Bump up the default maximum size (2mb) that's precached,
// to make lazy-loading failure scenarios less likely.
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
// TypeScript type checking
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
async: isEnvDevelopment,
typescript: {
typescriptPath: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
configOverwrite: {
compilerOptions: {
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
skipLibCheck: true,
inlineSourceMap: false,
declarationMap: false,
noEmit: true,
incremental: true,
tsBuildInfoFile: paths.appTsBuildInfoFile,
},
},
context: paths.appPath,
diagnosticOptions: {
syntactic: true,
},
mode: 'write-references',
// profile: true,
},
issue: {
// This one is specifically to match during CI tests,
// as micromatch doesn't match
// '../cra-template-typescript/template/src/App.tsx'
// otherwise.
include: [
{ file: '../**/src/**/*.{ts,tsx}' },
{ file: '**/src/**/*.{ts,tsx}' },
],
exclude: [
{ file: '**/src/**/__tests__/**' },
{ file: '**/src/**/?(*.){spec|test}.*' },
{ file: '**/src/setupProxy.*' },
{ file: '**/src/setupTests.*' },
],
},
logger: {
infrastructure: 'silent',
},
}),
!disableESLintPlugin &&
new ESLintPlugin({
// Plugin options
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
context: paths.appSrc,
cache: true,
cacheLocation: path.resolve(
paths.appNodeModules,
'.cache/.eslintcache'
),
// ESLint class options
cwd: paths.appPath,
resolvePluginsRelativeTo: __dirname,
baseConfig: {
extends: [require.resolve('eslint-config-react-app/base')],
rules: {
...(!hasJsxRuntime && {
'react/react-in-jsx-scope': 'error',
}),
},
},
}),
].filter(Boolean),
// Turn off performance processing because we utilize
// our own hints via the FileSizeReporter
performance: false,
};
};
target
- 如果项目中有browserslist配置,webpack将会用它 - 确定可用于生成运行时代码的 ES 功能 - 推断环境,可以不再配置output.environment
mode
- mode设置为’production’,会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 TerserPlugi
bail
- 在生产环境编译遇到错误直接抛出并终止
devtool
- devtool: 生产环境使用shouldUseSourceMap控制是否需要source map。在实践中,生产环境打包可能需要生成source map方便监控或日志平台进行定位,但也可能不需要,因此这里设置了一个变量,由process.env.GENERATE_SOURCEMAP控制
entry
- 入口文件
output
-
输出配置
output: { // 输出目录,比如这里是'D:\\cra-demo1\\build'. path: paths.appBuild, // 开发环境输出代码里增加/* filename */注释 pathinfo: isEnvDevelopment, // 主bundle filename: isEnvProduction ? 'static/js/[name].[contenthash:8].js' : isEnvDevelopment && 'static/js/bundle.js', // 代码分隔后的chunk 文件 chunkFilename: isEnvProduction ? 'static/js/[name].[contenthash:8].chunk.js' : isEnvDevelopment && 'static/js/[name].chunk.js', assetModuleFilename: 'static/media/[name].[hash][ext]', // 这里是'/' publicPath: paths.publicUrlOrPath, // source map路径 devtoolModuleFilenameTemplate: isEnvProduction ? info => path .relative(paths.appSrc, info.absoluteResourcePath) .replace(/\\/g, '/') : isEnvDevelopment && (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')), },
cache
-
webpack5自带的缓存,加快构建速度
cache: { // 将缓存保存在文件中 type: 'filesystem', // 缓存版本,版本更新将使原缓存失效 version: createEnvironmentHash(env.raw), // 缓存的目录:node_modules/.cache cacheDirectory: paths.appWebpackCache, // 当编译器空闲时将数据存储在一个文件中,用于所有缓存项 store: 'pack', // 缓存的依赖,这里的变更将使缓存失效 buildDependencies: { defaultWebpack: ['webpack/lib/'], config: [__filename], tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f => fs.existsSync(f) ), }, },
infrastructureLogging
- infrastructureLogging:基础日志级别,这里不开启,cra有自己的日志
optimization
-
optimization: 代码优化,比如压缩、分割等
-
js压缩
-
new TerserPlugin({ terserOptions: { parse: { // 以es8语法解析 ecma: 8, }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, }, mangle: { // 解决Safari 10中的一个bug safari10: true, }, // 是否保留classnames keep_classnames: isEnvProductionProfile, keep_fnames: isEnvProductionProfile, output: { ecma: 5, comments: false, ascii_only: true, }, }, }),
-
-
css 压缩
-
new CssMinimizerPlugin(),
-
resolve
resolve:帮助找到模块的路径
-
modules:从哪里找模块
resolve: { // 这个配置主要考虑了monorepo的场景 modules: ['node_modules', paths.appNodeModules].concat( modules.additionalModulePaths || [] ), ... },
-
extensions: 可以省略的后缀名,包含了: [ ‘web.mjs’, ‘mjs’, ‘web.js’, ‘js’, ‘web.ts’, ‘ts’, ‘web.tsx’, ‘tsx’, ‘json’, ‘web.jsx’, ‘jsx’, ];
extensions: paths.moduleFileExtensions .map(ext => `.${ext}`) .filter(ext => useTypeScript || !ext.includes('ts')),
-
alias:modules.webpackAliases中只有
src
alias: { 'react-native': 'react-native-web', // Allows for better profiling with ReactDevTools ...(isEnvProductionProfile && { 'react-dom$': 'react-dom/profiling', 'scheduler/tracing': 'scheduler/tracing-profiling', }), // 这里只设置了src ...(modules.webpackAliases || {}), },
-
plugins
plugins: [ // 这个插件用来防止用户从src之外的地方导入文件 new ModuleScopePlugin(paths.appSrc, [ paths.appPackageJson, reactRefreshRuntimeEntry, reactRefreshWebpackPluginRuntimeEntry, babelRuntimeEntry, babelRuntimeEntryHelpers, babelRuntimeRegenerator, ]), ],
module
如何处理不同类型的模块
- strictExportPresence
module: {
// 将缺失的导出作为error,而不是warning
strictExportPresence: true,
...
},
- rules: 这里用了
oneOf
api,遇到第一个匹配的就会终止,如果没有匹配的,就会执行最下面的
rules: [
// 处理第三方库的source map
shouldUseSourceMap && {
enforce: 'pre',
exclude: /@babel(?:\/|\\{1,2})runtime/,
test: /\.(js|mjs|jsx|ts|tsx|css)$/,
loader: require.resolve('source-map-loader'),
},
{
// "oneOf" 遍历下面所有的loader,直到第一个符合的,如果没有找到,则使用最下面的'file loader'
// webpack5取消了file-loader,因此这里加了个引号
oneOf: [
{
test: [/\.avif$/],
type: 'asset',
mimetype: 'image/avif',
// 这个是webpack5的配置,取消raw-loader、url-loader、file-loader
parser: {
dataUrlCondition: {
maxSize: imageInlineSizeLimit,
},
},
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: imageInlineSizeLimit,
},
},
},
{
test: /\.svg$/,
use: [
{
//可以将svg以组件的形式导入 import Star from './star.svg'
loader: require.resolve('@svgr/webpack'),
options: {
prettier: false,
svgo: false,
svgoConfig: {
plugins: [{ removeViewBox: false }],
},
titleProp: true,
ref: true,
},
},
{
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash].[ext]',
},
},
],
// 在这些条件中生效
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
// babel-preset-react-app是cra自定义的preset,包括了 JSX, Flow, TypeScript, and some ESnext features
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
// 一下两个eject后会移除
babelrc: false,
configFile: false,
// 确保 cache identifier的唯一性,eject后会移除
cacheIdentifier: getCacheIdentifier(
isEnvProduction
? 'production'
: isEnvDevelopment && 'development',
[
'babel-plugin-named-asset-import',
'babel-preset-react-app',
'react-dev-utils',
'react-scripts',
]
),
plugins: [
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
// babel-loader能将缓存保存在./node_modules/.cache/babel-loader/
cacheDirectory: true,
cacheCompression: false,
compact: isEnvProduction,
},
},
// 处理其他js
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
... 同上
},
},
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'icss',
},
}),
sideEffects: true,
},
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'icss',
},
},
'sass-loader'
),
sideEffects: true,
},
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
// 兜底的'file loader'
{
exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
type: 'asset/resource',
},
],
},
].filter(Boolean),
plugins
各种插件
plugins: [
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
// 指定index.html中可以使用的变量,如<link rel="icon" href="%PUBLIC_URL%/favicon.ico">
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
new ModuleNotFoundPlugin(paths.appPath),
new webpack.DefinePlugin(env.stringified),
isEnvDevelopment &&
shouldUseReactRefresh &&
new ReactRefreshWebpackPlugin({
overlay: false,
}),
// 大小写敏感,这个插件挺有用的
isEnvDevelopment && new CaseSensitivePathsPlugin(),
isEnvProduction &&
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
new WebpackManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
// 不打包momentjs中的语言包
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
// service worker
isEnvProduction &&
fs.existsSync(swSrc) &&
new WorkboxWebpackPlugin.InjectManifest({
swSrc,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
// 改动ts文件触发类型检查
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
async: isEnvDevelopment,
typescript: {
typescriptPath: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
configOverwrite: {
compilerOptions: {
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
skipLibCheck: true,
inlineSourceMap: false,
declarationMap: false,
noEmit: true,
incremental: true,
tsBuildInfoFile: paths.appTsBuildInfoFile,
},
},
context: paths.appPath,
diagnosticOptions: {
syntactic: true,
},
mode: 'write-references',
},
issue: {
include: [
{ file: '../**/src/**/*.{ts,tsx}' },
{ file: '**/src/**/*.{ts,tsx}' },
],
exclude: [
{ file: '**/src/**/__tests__/**' },
{ file: '**/src/**/?(*.){spec|test}.*' },
{ file: '**/src/setupProxy.*' },
{ file: '**/src/setupTests.*' },
],
},
logger: {
infrastructure: 'silent',
},
}),
!disableESLintPlugin &&
new ESLintPlugin({
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
context: paths.appSrc,
cache: true,
cacheLocation: path.resolve(
paths.appNodeModules,
'.cache/.eslintcache'
),
// ESLint class options
cwd: paths.appPath,
resolvePluginsRelativeTo: __dirname,
baseConfig: {
extends: [require.resolve('eslint-config-react-app/base')],
rules: {
...(!hasJsxRuntime && {
'react/react-in-jsx-scope': 'error',
}),
},
},
}),
].filter(Boolean),
};
performance
performance:CRA自带了FileSizeReporter
- performance: false
实现
1.create-react-app
- Create React App是一个官方支持的创建 React 单页应用程序的方法。它提供了一个零配置的现代构建设置
- create-react-app
下载
git clone https://github.com/facebook/create-react-app.git --depth=1
cd create-react-app
yarn install
package.json
package.json
"scripts": {
+ "create": "node ./packages/create-react-app/index.js",
}
重要步骤
-
将命令行参数发送到npm脚本
npm run [command] [-- <args>]
yarn install #安装项止依赖和软链接 npm run create -- aaa #执行创建命令 Installing packages. This might take a couple of minutes. #安装依赖包 Installing react, react-dom, and react-scripts with cra-template... #安装依赖包 Installing template dependencies using yarnpkg... #安装模板依赖 Removing template package using yarnpkg... #移除模板模块 Removing module cra-template... #移除cra-template模块 Success! Created aaa at C:\aprepare\create-react-app\aaa #成功创建 Inside that directory, you can run several commands: #执行命令 cd aaa yarn start
.vscode\launch.json
.vscode\launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": [
"run-script",
"create"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node"
}
]
}
2.实现init方法
2.1 package.json
package.json
"scripts": {
+ "version": "node ./packages/create-react-app3/index.js --version",
+ "create": "node ./packages/create-react-app3/index.js aaa"
}
2.2 create-react-app3\package.json
packages\create-react-app3\package.json
{
+ "main": "./index.js"
}
2.3 create-react-app3\index.js
packages\create-react-app3\index.js
#!/usr/bin/env node
const { init } = require('./createReactApp');
init();
2.4 createReactApp.js
packages\create-react-app3\createReactApp.js
const {Command} = require('commander');
const chalk = require('chalk');
const packageJson = require('./package.json');
let appName;
async function init() {
new Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(projectDirectory => {
appName = projectDirectory;
})
.parse(process.argv);
console.log('appName=', appName);
}
module.exports = {
init
}
2.5 执行命令
npm run create
3.实现createApp方法
3.1 createReactApp.js
packages\create-react-app3\createReactApp.js
const {Command} = require('commander');
const chalk = require('chalk');
+const fs = require('fs-extra');
+const path = require('path');
const packageJson = require('./package.json');
let appName;
async function init() {
new Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(projectDirectory => {
appName = projectDirectory;
})
.parse(process.argv);
console.log('appName=', appName);
+ await createApp(appName);
}
+async function createApp(appName) {
+ const root = path.resolve(appName);
+ fs.ensureDirSync(appName);
+ console.log(`Creating a new React app in ${chalk.green(root)}.`);
+ const packageJson = {
+ name: appName,
+ version: '0.1.0',
+ private: true,
+ };
+ fs.writeFileSync(
+ path.join(root, 'package.json'),
+ JSON.stringify(packageJson, null, 2)
+ );
+ const originalDirectory = process.cwd();
+ process.chdir(root);
+ console.log('root',root);
+ console.log('appName',appName);
+ console.log('originalDirectory',originalDirectory);
+ }
module.exports = {
init
}
4.实现run方法
4.1 createReactApp.js
packages\create-react-app3\createReactApp.js
const {Command} = require('commander');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
+const spawn = require('cross-spawn');
const packageJson = require('./package.json');
let appName;
async function init() {
new Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(projectDirectory => {
appName = projectDirectory;
})
.parse(process.argv);
console.log('appName=', appName);
await createApp(appName);
}
async function createApp(appName) {
const root = path.resolve(appName);
fs.ensureDirSync(appName);
console.log(`Creating a new React app in ${chalk.green(root)}.`);
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
const originalDirectory = process.cwd();
process.chdir(root);
console.log('root',root);
console.log('appName',appName);
console.log('originalDirectory',originalDirectory);
+ await run(
+ root,
+ appName,
+ originalDirectory
+ );
}
+async function run(root,appName,originalDirectory) {
+ const scriptName = 'react-scripts';
+ const templateName = 'cra-template';
+ const allDependencies = ['react', 'react-dom', scriptName, templateName];
+ console.log('Installing packages. This might take a couple of minutes.');
+ console.log(
+ `Installing ${chalk.cyan('react')}, ${chalk.cyan(
+ 'react-dom'
+ )}, and ${chalk.cyan(scriptName)} with ${chalk.cyan(templateName)}`
+ );
+ await install(root, allDependencies);
+}
+function install(root, allDependencies) {
+ return new Promise((resolve) => {
+ const command = 'yarnpkg';
+ const args = ['add', '--exact', ...allDependencies, '--cwd', root];
+ console.log('command:',command,args);
+ const child = spawn(command, args, { stdio: 'inherit' });
+ child.on('close', resolve);
+ });
+}
module.exports = {
init
}
command: yarnpkg [
'add',
'--exact',
'react',
'react-dom',
'react-scripts',
'cra-template',
'--cwd',
'C:\\aprepare\\create-react-app3\\aaa'
]
yarnpkg add --exact react react-dom react-scripts cra-template --cwd C:\\aprepare\\create-react-app3\\aaa
5.执行init初始化命令
5.1 createReactApp.js
packages\create-react-app3\createReactApp.js
const {Command} = require('commander');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
const spawn = require('cross-spawn');
const packageJson = require('./package.json');
let appName;
async function init() {
new Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(projectDirectory => {
appName = projectDirectory;
})
.parse(process.argv);
console.log('appName=', appName);
await createApp(appName);
}
async function createApp(appName) {
const root = path.resolve(appName);
fs.ensureDirSync(appName);
console.log(`Creating a new React app in ${chalk.green(root)}.`);
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
const originalDirectory = process.cwd();
process.chdir(root);
console.log('root',root);
console.log('appName',appName);
console.log('originalDirectory',originalDirectory);
await run(
root,
appName,
originalDirectory
);
}
async function run(root,appName,originalDirectory) {
const scriptName = 'react-scripts';
const templateName = 'cra-template';
const allDependencies = ['react', 'react-dom', scriptName, templateName];
console.log('Installing packages. This might take a couple of minutes.');
console.log(
`Installing ${chalk.cyan('react')}, ${chalk.cyan(
'react-dom'
)}, and ${chalk.cyan(scriptName)} with ${chalk.cyan(templateName)}`
);
await install(root, allDependencies);
+ let data = [root, appName, true, originalDirectory, templateName];
+ let source = `
+ var init = require('react-scripts/scripts/init.js');
+ init.apply(null, JSON.parse(process.argv[1]));
+ `
+ await executeNodeScript({ cwd: process.cwd() }, data, source);
+ console.log('Done.');
+ process.exit(0);
}
+function executeNodeScript({ cwd }, data, source) {
+ return new Promise((resolve) => {
+ const child = spawn(
+ process.execPath,
+ ['-e', source, '--', JSON.stringify(data)],
+ { cwd, stdio: 'inherit' }
+ );
+ child.on('close', resolve);
+ });
+}
function install(root, allDependencies) {
return new Promise((resolve) => {
const command = 'yarnpkg';
const args = ['add', '--exact', ...allDependencies, '--cwd', root];
console.log('command:',command,args);
const child = spawn(command, args, { stdio: 'inherit' });
child.on('close', resolve);
});
}
module.exports = {
init
}