-
Notifications
You must be signed in to change notification settings - Fork 51
create-react-app 原理及源码分析 #38
Comments
分析其实 create-react-app(以下简称 cra) 可以分为两个部分,react-scripts 的及剩下的部分,剩下的部分我称为项目初始化部分。顾名思义,项目初始化部分就是我们在命令行中输入 详细来说,项目初始化部分做了:
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"]
}
]
} 初始化流程下图是初始化工程的流程图(就是 启动开发模式react-scrits 中 react-dev-utils 源码分析react-dev-utils 中承载了 start、build、eject 中的主要逻辑,其文件目录及作用如下:
有些模块其实不难,但是涉及到很多琐碎的知识,在这里不对所有的模块进行分析,只分析一些比较复杂的模块。 WatchMissingNodeModulesPlugin代码并不长 '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 补充上。 webpackHotDevClientcra 使用了自己的 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
The text was updated successfully, but these errors were encountered: