在之前 实现create-react-app packages 中的 create-react-app 这篇学习记录里头,七七八八实现了 create-react-app
主要流程,再往下就到了 script
命令执行,我们常用的两个命令就是 start
和 build
了,这两个命令也有点相似,我们就来看看 “非死不可” 是怎么实现的吧
react-scripts/package.json 中注册了一个命令。当我们在终端执行 react-scripts
,就会运行这个文件
"bin": {
"react-scripts": "./bin/react-scripts.js"
},
bin/react-scripts.js
的主要实现是这样的,其实也很简单,他相当于一个入口,根据终端提供的命令,直接去找对应的文件执行
#!/usr/bin/env node
// 捕获未处理的异常
process.on('unhandledRejection', err => {
throw err;
});
const spawn = require('react-dev-utils/crossSpawn');
const args = process.argv.slice(2);
// build,eject,start,test 只有这 4 个指令
const scriptIndex = args.findIndex(
x => x === 'build' || x === 'eject' || x === 'start' || x === 'test'
);
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];
if (['build', 'eject', 'start', 'test'].includes(script)) {
// 开个子进程执行 scripts 文件下对应名称的文件
// 如: 执行 scripts/build.js
const result = spawn.sync(
process.execPath,
nodeArgs
.concat(require.resolve('../scripts/' + script))
.concat(args.slice(scriptIndex + 1)),
{ stdio: 'inherit' }
);
// ...省略 SIGKILL 和 SIGTERM 提示,和主要流程无关,暂且不用管他
} else {
// 未知命令的提示,不用管他
console.log('Unknown script "' + script + '".');
console.log('Perhaps you need to update react-scripts?');
console.log(
'See: https://facebook.github.io/create-react-app/docs/updating-to-new-releases'
);
}
流程梳理:注册命令 + 根据命令找相对应的文件执行(一个命令的主要逻辑都封装在一个文件里头)
我们只需要分析对应文件的逻辑就可以了。
start
命令代码量也不多,主要逻辑就几十行代码,主要得流程就是
configFactory('development')
const compiler = webpack(config)
const serverConfig = createDevServerConfig()
const devServer = new WebpackDevServer(serverConfig, compiler);
openBrowser(urls.localUrlForBrowser);
如果不是很熟悉 webpack
配置的小伙伴,也可以去 react-scripts/config/webpack.config.js 看一下他的 webpack
配置,这里用的是工厂模式,module.exports = function (webpackEnv) {}
根据 webpackEnv
的不用,返回不同的配置信息。稍微整理了一下代码:
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
// 捕获未处理的异常
process.on('unhandledRejection', err => {
throw err;
});
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');
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;
// 检查是否有入口文件
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// 有默认端口,如果端口被占用,就用用户提供的端口
return choosePort(HOST, DEFAULT_PORT);
})
.then(port => {
if (port == null) {
// We have not found a port.
return;
}
// 有点工厂模式的感觉,development 和 production 生成两个对应的配置
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 urls = prepareUrls(
protocol,
HOST,
port,
paths.publicUrlOrPath.slice(0, -1)
);
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler({
appName,
config,
urls,
useYarn,
useTypeScript,
webpack,
});
// 配置代理服务器
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
// 整合配置
const serverConfig = {
...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
host: HOST,
port,
};
// 用 serverConfig 配置 WebpackDevServer 起服务
const devServer = new WebpackDevServer(serverConfig, compiler);
devServer.startCallback(() => {
if (isInteractive) {
clearConsole();
}
// ...版本更新提示
openBrowser(urls.localUrlForBrowser);
});
// ...省略 SIGINT,SIGTERM 处理 和 环境变量处理
})
//...省略catch 处理,就是打印错误信息,退出程序
build
命令build
命令和 start
命令有一些相似的地方,我们梳理一下主要的流程:
webpack
的配置文件build
目录public
下面的静态文件到 build 目录webpack compiler
直接打包就行了const path = require('path');
const fs = require('fs-extra');
const webpack = require('webpack');
const configFactory = require('../config/webpack.config');
const paths = require('../config/paths');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const measureFileSizesBeforeBuild =
FileSizeReporter.measureFileSizesBeforeBuild;
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
const useYarn = fs.existsSync(paths.yarnLockFile);
// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
const isInteractive = process.stdout.isTTY;
// 同样检查入口
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// 获取 webpack 配置
const config = configFactory('production');
// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
return measureFileSizesBeforeBuild(paths.appBuild);
})
.then(previousFileSizes => {
// 清空 build 文件夹
fs.emptyDirSync(paths.appBuild);
// 复制 public 文件夹
copyPublicFolder();
// 开始打包
return build(previousFileSizes);
})
.then(
({ stats, previousFileSizes, warnings }) => {
// ...省略各种打印信息
// 打印打包后的信息
printFileSizesAfterBuild(
stats,
previousFileSizes,
paths.appBuild,
WARN_AFTER_BUNDLE_GZIP_SIZE,
WARN_AFTER_CHUNK_GZIP_SIZE
);
const appPackage = require(paths.appPackageJson);
const publicUrl = paths.publicUrlOrPath;
const publicPath = config.output.publicPath;
const buildFolder = path.relative(process.cwd(), paths.appBuild);
printHostingInstructions(
appPackage,
publicUrl,
publicPath,
buildFolder,
useYarn
);
},
// ...省略错误信息处理
)
// Create the production build and print the deployment instructions.
function build(previousFileSizes) {
console.log('Creating an optimized production build...');
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
//...各种 warning 打印,和错误处理
});
});
}
function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
// appHtml 有插件 htmlWebpackPlugin 处理
filter: file => file !== paths.appHtml,
});
}
只要把 webpack
的配置信息配置好了,其他的就交给 webpack
的 complier
打包就行了,更多的可能就是要做交互的信息提示,以及异常处理而已。感觉还是比较简单的,我对后面开发自己的脚手架少了许多迷茫,多了许多信心。