Open fi3ework opened 6 years ago
其实 create-react-app(以下简称 cra) 可以分为两个部分,react-scripts 的及剩下的部分,剩下的部分我称为项目初始化部分。顾名思义,项目初始化部分就是我们在命令行中输入 crate-react-app project-name
到结束所做的所有事情,而 react-scripts 负责了启动 npm start
, npm eject
, npm test
, npm build
这些命令的 Webpack 配置及报错位置提示等等。
详细来说,项目初始化部分做了:
react-scripts 中做了:
其实 cra 最精髓的部分是 react-scripts,它赋予了我们在开发时的极佳体验,也占了整个包大部分的代码;至于项目构建部分其实难度不大,就是一步步像流水线一样对命令行及文件进行操作,但是由于 node,npm,yarn,网络,文件夹情况等各种环境的干扰,整个流畅要各种防御式编程并设计的非常鲁棒。
使用 VSCode 进行调试,create-react-app 的入口为 index.js,附加一个要创建的项目的名字即可,配置如下
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "init",
"program": "${workspaceFolder}/packages/create-react-app/index.js",
"args": ["test-app"]
},
{
"type": "node",
"request": "launch",
"name": "start",
"program":
"${workspaceFolder}/test-app/node_modules/react-scripts/bin/react-scripts.js",
"args": ["start"]
}
]
}
下图是初始化工程的流程图(就是 create-react-app myapp
之后执行的事),整个流程为了健壮期间进行了很多的判断,这里是列出了比较主要的,整个 pipeline 很清晰明确,大量的代码都集中在了各种判断上来保证 cra 是一个 "battle tested" cli,具体源码的细节可以参见我写了注释的版本:???
react-scrits 中 start.js
的代码量不多,因为主要都是 Webpack 的配置,这些都集中在了 react-dev-utils 这个独立的包中,后面会着重分析,start.js
仅仅是调用并串起整个流程:
react-dev-utils 中承载了 start、build、eject 中的主要逻辑,其文件目录及作用如下:
checkRequiredFiles.js
:同步的检验传入的文件是否都可以存在,返回一个 bool 值。clearConsole.js
:跨平台的清空控制台。crossSpawn.js
:导出 cross-spawn,跨平台的创建进程。errorOverlayMiddleware.js
:可以直接通过报错打开本地的编辑器并跳转到对应的位置,在 webpackDevServer 的 before 钩子上。eslintFormatter.js
:给 Webpack 配置 cra 默认的 eslint 配置,在对警告/错误的信息做了美化。FileSizeReporter.js
:统计 build 前后文件的大小。formatWebpackMessages.js
:通过 Webpack 的 stats 来输出一个更好的警告和错误提示。getProcessForPort.js
:获得当前项目运行的线程的端口,并返回包含 localhost 和 ip 的字符串。ignoredFiles.js
:返回一个忽略 node_modules 的正则表达式。inquirer.js
:导出 inquirer,命令行交互的库。InterpolateHtmlPlugin.js
:webpack 插件,可以向 html 中插入全局变量,需要与 HtmlWebpackPlugin 配合使用。launchEditor.js
:启动本地的编辑器。launchEditorEndpoint.js
:配合 launchEditor.js
使用的一个参数。ModuleScopePlugin.js
:Webpack 插件,报应引入的模块不会包含 src 之外的文件。noopServiceWorkerMiddleware.js
:避免在开发环境中使用生产版本到的 /service-worker.js
,返回一个使用重置的 service worker 配置。openBrowser.js
:在浏览器中打开指定的 url。openChrome.applescript
:在 openBrowser.js
调用,在 macOS 中使用 applescript 脚本打开浏览器。 printBuildError.js
:输出更好的 build 时的报错提示。printHostingInstructions.js
:Prints hosting instructions after the project is built.WatchMissingNodeModulesPlugin.js
:如果你先引入一个还没有安装的包再 npm install
,webpack 无法检测到这个包,需要重启。这个插件可以让 webpack 来检测到这个包。WebpackDevServerUtils.js
:webpackDevServer 的配置入口。webpackHotDevClient.js
:cra 自己实现的一个 WebpackDevServer 的 client 端。有些模块其实不难,但是涉及到很多琐碎的知识,在这里不对所有的模块进行分析,只分析一些比较复杂的模块。
代码并不长
'use strict';
class WatchMissingNodeModulesPlugin {
constructor(nodeModulesPath) {
this.nodeModulesPath = nodeModulesPath;
}
apply(compiler) {
compiler.plugin('emit', (compilation, callback) => {
var missingDeps = compilation.missingDependencies;
var nodeModulesPath = this.nodeModulesPath;
// If any missing files are expected to appear in node_modules...
if (missingDeps.some(file => file.indexOf(nodeModulesPath) !== -1)) {
// ...tell webpack to watch node_modules recursively until they appear.
compilation.contextDependencies.push(nodeModulesPath);
}
callback();
});
}
}
module.exports = WatchMissingNodeModulesPlugin;
一个标准的 webpack 插件写法,通过实现 apply 方法调用 emit 这个钩子函数,emit 的时间点是“在生成资源并输出到目录之前“,???为什么是 emit,插入到 contextDependencies ,contextDependencies 是一个保存依赖的绝对路径的数组,也就是在 emit 时检测如果有丢失的依赖那么给 compilation 补充上。
cra 使用了自己的 webpackDevClient,提供了包括:
webpackDevServer 的原理简单来说就是 webpack 作为 server 为每一次代码更改带来的编译会向 client 通信,webpackHotDevClient.js 这个文件的代码是跑在浏览器中,通过 socket 和 server 通信。
var connection = new SockJS(
url.format({
protocol: window.location.protocol,
hostname: window.location.hostname,
port: window.location.port,
// Hardcoded in WebpackDevServer
pathname: "/sockjs-node"
})
);
然后根据 server 传过来的 message 决定更新策略
// 接收 server 发过来的信号
connection.onmessage = function(e) {
var message = JSON.parse(e.data);
switch (message.type) {
case "hash": // 更新 hash
handleAvailableHash(message.data);
break;
case "still-ok": // 所有更新的代码是否已经被编译到本地
case "ok":
handleSuccess();
break;
case "content-changed": // contentBase 更新时直接 reload 浏览器,与 HMR 无关
// Triggered when a file from `contentBase` changed.
window.location.reload();
break;
case "warnings": // 编译遇到 warning
handleWarnings(message.data);
break;
case "errors": // 编译遇到 error
handleErrors(message.data);
break;
default:
// Do nothing.
}
};
策略分为 hash, still-ok, ok, content-changed, warnings, errors,这些信号是由 webpack 的 server 发送的,配合 server 的 源码 还有 官方文档,在除了 errors 的每种情况下,都会标记 isFirstCompilation 为 true,以便在后续更新中启用热更新。
以下是 server 的源码:
Server.prototype._sendStats = function (sockets, stats, force) {
if (
!force &&
stats &&
(!stats.errors || stats.errors.length === 0) &&
stats.assets &&
stats.assets.every(asset => !asset.emitted)
) {
return this.sockWrite(sockets, 'still-ok');
}
this.sockWrite(sockets, 'hash', stats.hash);
if (stats.errors.length > 0) {
this.sockWrite(sockets, 'errors', stats.errors);
} else if (stats.warnings.length > 0) {
this.sockWrite(sockets, 'warnings', stats.warnings);
} else {
this.sockWrite(sockets, 'ok');
}
};
一个一个来看:
重点说一下 ok 和 still-ok,如果是这两种情况,会执行 tryApplyUpdates
// Attempt to update code on the fly, fall back to a hard reload.
function tryApplyUpdates(onHotUpdateSuccess) {
if (!module.hot) {
// HotModuleReplacementPlugin is not in Webpack configuration.
window.location.reload(); // 没开启热更新,直接刷新
return;
}
// 只更新当前最新版本的 compilation || webpack 热更新模块状态为 idle
if (!isUpdateAvailable() || !canApplyUpdates()) {
return;
}
// check 的回调入口
function handleApplyUpdates(err, updatedModules) {
// 如果编译报错则直接刷新页面
if (err || !updatedModules || hadRuntimeError) {
window.location.reload();
return;
}
// 主要目的就是引入 onHotUpdateSuccess
if (typeof onHotUpdateSuccess === "function") {
// Maybe we want to do something.
onHotUpdateSuccess();
}
// 在更新期间又来了更新,则再执行一次
if (isUpdateAvailable()) {
// While we were updating, there was a new update! Do it again.
tryApplyUpdates();
}
}
// https://webpack.github.io/docs/hot-module-replacement.html#check
// A check makes an HTTP request to the update manifest. If this request fails, there is no update available.
// If it succeeds, the list of updated chunks is compared to the list of currently loaded chunks.For each loaded chunk,
// the corresponding update chunk is downloaded.All module updates are stored in the runtime.
// When all update chunks have been downloaded and are ready to be applied, the runtime switches into the ready state.
// 热更新完成时触发回调
var result = module.hot.check(/* autoApply */ true, handleApplyUpdates);
// // Webpack 2 returns a Promise instead of invoking a callback
if (result && result.then) {
result.then(
function(updatedModules) {
handleApplyUpdates(null, updatedModules);
},
function(err) {
handleApplyUpdates(err, null);
}
);
}
}
tryApplyUpdates 主要目的就是为热更新完成时引入回调函数,在成功时,清除之前的编译报错信息
if (isHotUpdate) {
tryApplyUpdates(function onHotUpdateSuccess() {
// 只要不是第一次编译就尝试热更新
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
ErrorOverlay.dismissBuildError();
});
}
有警告时,显示警告并清除之前的报错信息
if (isHotUpdate) {
tryApplyUpdates(function onSuccessfulHotUpdate() {
// Only print warnings if we aren't refreshing the page.
// Otherwise they'll disappear right away anyway.
printWarnings();
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
ErrorOverlay.dismissBuildError();
});
} else {
// Print initial warnings immediately.
printWarnings();
}
干的漂亮
wip