本文连接:jsonz1993.github.io/2018/05/cre…javascript
上一篇咱们已经讲了 create-react-app
里面建立package.json
安装依赖而且拷贝可运行的demo等步骤。传送门html
这一篇咱们来说一下 create-react-app
里面的启动服务等部分,就是平时咱们安装完依赖以后,启动开发服务:npm start
。这一块涉及到太多关于webpack与配置的东西,加上第一篇以为描述的太过冗余~因此这篇不会讲得很细,只是大概把他运转的逻辑思路写出来,具体源码会提供传送门。前端
推荐你们看第一篇的 项目初始化 和 断点调试 部分,这里就不在赘述。传送门 项目初始化断点调试部分java
这里咱们讨论的create-react-app版本依旧是v1.1.4
node
既然这篇咱们主要讲的是 create-react-app
里面的webpack服务,那咱们确定要先新建一个项目。react
npm install create-react-app -g
全局安装create-react-appcreate-react-app my-react-project
用create-react-app新建一个项目cd my-react-project
yarn start
复制代码
新建完以后,终端提示了咱们直接进入项目,跑 yarn(npm) start 就能够开发了。咱们打开 package.json
就能够看到 yarn start 跑的命令是 "react-scripts start"
webpack
那么这个 react-scripts 命令究竟是哪个呢?git
通常写在 package.json=> scripts
的命令,都会先去 project_path(项目目录)/node_modules/.bin
查找,找不到再找全局安装的包。github
那么 node_modules/.bin 里面的文件又是怎么来的呢? 咱们若是在包的 package.json 加上 bin
字段,npm就会自动帮咱们映射到 node_modules/.bin 里面 npm bin文档传送门web
咱们直接打开 node_modules/react-scripts/package.json
能看到这么一行"react-scripts": "./bin/react-scripts.js"
,直接把命令指向node_modules/react-scripts/.bin/react-scripts.js
,也验证了咱们的观点。
还记得上一篇,咱们在 create-react-app/packages
里面发现了有一个 react-scripts
。实际上是同一个东西来的,那么接下来的步骤就很明确了,直接用老办法,改下配置,而后用vscode跑断点调试阅读project_path/node_modules/react-scripts/.bin/react-scripts.js
的源码 一探究竟。
这里咱们传入 start 做为参数,模拟在项目里跑 yarn start
的效果。
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"program": "${workspaceFolder}/node_modules/react-scripts/bin/react-scripts.js", //调试的文件路径
"args": [
"start" // 传入 start 作为参数
]
}
]
}
复制代码
ps:下面的react-scripts
没有特殊说明,都表明project_path/node_modules/react-scripts
目录方便阅读
文件传送门 这里咱们仍是老办法,先不看依赖 看主流程理解先,咱们能看到这个文件也是一个入口文件,很是简短。
const args = process.argv.slice(2);
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) : [];
复制代码
首先处理传进来的参数,用script
变量来获取咱们跑的命令是哪个,有['build', 'eject', 'start', 'test']这么几种,分别对应 构建、暴露配置、开发、测试命令。 而后再获取一块儿传入的其余的参数,好比npm test
命令就会带一个额外的参数--env=jsdom
。
switch (script) {
case 'build':
case 'eject':
case 'start':
case 'test': {
// 用 cross-spawn 去跑一个同步的命令
// 根据传入的命令来拼接对应的路径 用node去跑
const result = spawn.sync(
'node',
nodeArgs
.concat(require.resolve('../scripts/' + script))
.concat(args.slice(scriptIndex + 1)),
{ stdio: 'inherit' }
);
if (result.signal) {
if (result.signal === 'SIGKILL') {
// 输出错误提醒日志
} else if (result.signal === 'SIGTERM') {
// 输出错误提醒日志
}
process.exit(1); // 退出进程, 传1表明有错误
}
process.exit(result.status);
break;
}
default:
// 这里输出匹配不到对应的命令
break;
}
复制代码
而后根据获取到的命令,对应到react-scripts/scripts
下面的文件去跑,好比 react-scripts start
就会去跑 react-scripts/scripts/start.js
。
这里插几句讲一下一个项目上比较常见的类库解耦方式,咱们能够看到这里的 spawn
引用的是react-dev-utils/crossSpawn
。而在react-dev-utils/corssSpawn
里面也只是简简单单的几句,引入cross-spawn
再把cross-spawn
暴露出去。 可是这么写就能够起到类库解耦的做用,好比之后这个库被爆出有重大的bug或者中止维护了,直接修改这个文件引入其余的类库,其余引用该文件的代码就不须要改动。
// react-dev-utils/corssSpawn
'use strict';
var crossSpawn = require('cross-spawn');
module.exports = crossSpawn;
复制代码
看过第一篇的人对这个文件夹应该不陌生,create-react-app
在安装完 react
等依赖以后,就会跑这个文件夹下面的init.js
来拷贝模版文件,修改package.json
等操做。
既然咱们已经知道他要执行 start.js, 接下来咱们把vscode调试文件修改成 start.js 文件"program": "${workspaceFolder}/node_modules/react-scripts/scripts/start.js",
之因此要修改是由于他这里不是引用js文件来运行,而是用终端来跑,因此不属于咱们的项目调试范围~
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
复制代码
文件的最开头设置了两个环境变量,由于 start 是用来跑开发的,因此这里的环境变量都是 development
,而后再给 process
绑定一个错误监听函数,这个错误监听实质上是用来监听 一些没有被.catch的Promise。 具体能够看node的文档, 关于 Promise能够看一下以前写过的一篇介绍Promise的文章从用法和实现原理都有所涉及
接着引进一个 ../config/env
, 看文件名猜应该是作一些关于环境配置的事情,找到文件断点进来
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
复制代码
env.js
文件在引入 ./paths.js
以后,当即把他从cache中删除掉,这样下次若是有其余的模块引入paths.js
,就不会从缓存里面去获取,保证了paths.js
里面执行逻辑都会用到最新的环境变量。
var dotenvFiles = [
// 举个例子:第一个元素在个人电脑路径是这样的 Users/jsonz/Documents/my-react-project/.env.development.local.js
`${paths.dotenv}.${NODE_ENV}.local`,
`${paths.dotenv}.${NODE_ENV}`,
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
paths.dotenv,
].filter(Boolean);
复制代码
而后再根据paths给出的地址去拿其余的环境变量,这里paths.js
会根据不一样的状况给出不一样的路径,咱们讨论的是正常的建立项目状况。 其余的几种状况有:
npm(yarn) eject
,这时候 react-scripts
会把配置都暴露到 project_path/config
方便咱们去根据项目修改配置,这个操做是不可逆的。project/node_modules/react-scripts
。create-react/packages/react-scripts/config
。拼装完路径以后,用dotenv-expand和dotenv来把文件里面的环境变量加载进来,这一块通常场景用不上。
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',
}
);
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { raw, stringified };
}
复制代码
而后返回一个 getClientEnvironment
函数,这个函数执行后会返回客户端的环境变量。
const fs = require('fs');
const chalk = require('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 config = require('../config/webpack.config.dev');
const createDevServerConfig = require('../config/webpackDevServer.config');
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;
复制代码
加载完各类环境变量以后,咱们回到react-scripts/scripts/start.js
,老规矩,一系列的依赖先跳过不看,后面用到再来看。 还记得咱们在env.js
里面delet掉node.catch吗,这里conts paths = require('../config/paths)
就不会从缓存里面去拿而是从新去加载。
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
复制代码
先判断一下咱们两个入口文件有没有存在,分别是project_path/public/index.html
和project_path/src/index.js
,若是不存在给出提示结束程序。
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
复制代码
而后设置默认的端口和host,若是有特殊的需求,能够从环境变量传进去改变,没有就会用默认的3000
端口。
choosePort(HOST, DEFAULT_PORT).then(...) // @return Promise
复制代码
设置完默认的端口与host以后,开始判断这个端口有没有被其余的进程占用,有的话会提供下一个可用的端口,咱们顺着choosePort
去文件头找依赖,找到该方法位于依赖react-dev-utils/WebpackDevServerUtils
。
function choosePort(host, defaultPort) {
return detect(defaultPort, host).then(
port =>
new Promise(resolve => {
if (port === defaultPort) {
return resolve(port);
}
const message =
process.platform !== 'win32' && defaultPort < 1024 && !isRoot()
? `Admin permissions are required to run a server on a port below 1024.`
: `Something is already running on port ${defaultPort}.`;
if (isInteractive) {
clearConsole();
const existingProcess = getProcessForPort(defaultPort);
const question = {
type: 'confirm',
name: 'shouldChangePort',
message:
chalk.yellow(
message +
`${existingProcess ? ` Probably:\n ${existingProcess}` : ''}`
) + '\n\nWould you like to run the app on another port instead?',
default: true,
};
inquirer.prompt(question).then(answer => {
if (answer.shouldChangePort) {
resolve(port);
} else {
resolve(null);
}
});
} else {
console.log(chalk.red(message));
resolve(null);
}
}),
err => {
// 输出错误日志
}
);
}
复制代码
choosePort
里面用到detect-port-alt去检测端口占用,若是被占用了返回一个最接近的递增方向可用的端口,好比3000端口被占用,3001没被占用就返回回来。 若是发现返回的可用端口不是默认的端口,给出一个交互式的命令询问用户是否要换一个端口去访问,交互式命令用的是inquirer这个包。 这里若是用vsCode来调试,process.stdout.isTTY
返回的值是undefined
。因此若是要测试这一块交互式命令,只能切回系统的终端去调试~
文件传送门 检测完可用端口以后,回到start.js
。
前端处理一堆环境变量,还有加载一堆配置,全都用在这一块。这里主要作的就是把环境变量和配置组装起来,开个webpack本地调试服务。主要作的事情有:
https
,默认是http
。Browser
的url与Terminal
的url。createCompiler
传入webpack,webpack配置,appName,第三步获取的url,还有是否使用Yarn等参数,生成一个 webpackCompiler。createCompiler负责的东西有: 4.1 根据环境变量判断是否有冒烟测试的需求,若是有加一个 handleCompile
,一有错误就中断程序。 4.2 用传进来的配置和handleCompile生成一个webpackCompiler 4.2 增长invalid
钩子,一检测到更改文件,并且是交互式终端的话,先清空控制台,再输出日志 4.3 增长done
钩子,对webpack的输出日志整理统一输出webpackDevServer.config.js
WebpackDevServer
,生成一个 webpack 本地开发服务相关的代码执行写到注释里面去了,没办法每一个方法配置都拎出来说...否则篇幅会很长,这里面不少点一讲均可以是一个知识点。
choosePort(HOST, DEFAULT_PORT)
.then(port => {
// 没有找到可用端口,直接return
if (port == null) {
return;
}
// 根据环境变量判断是否要用https
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
// 获取当前的 host, port, protocol 生成一系列url
const urls = prepareUrls(protocol, HOST, port);
// 建立一个webpack compiler
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
// 加载代理的配置,在 project_path/package.json 里面加载配置
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
// 生成 webpack dev server 的配置
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
const devServer = new WebpackDevServer(compiler, serverConfig);
// 监听 devServer
devServer.listen(port, HOST, err => {
// 一些日志输出
// 自动用默认浏览器打开调试连接
openBrowser(urls.localUrlForBrowser);
});
})
.catch(err => {
// 错误处理
});
复制代码
react-dev-utils/WebpackDevServerUtils.js
function createCompiler(webpack, config, appName, urls, useYarn) {
let compiler;
try {
compiler = webpack(config, handleCompile); // handleCompile为冒烟测试的对应处理
} catch (err) {
// 错误提示
}
compiler.plugin('invalid', () => {
// invalid 钩子,若是当前处于TTY终端,那么先清除控制台再输出 Compiling...
if (isInteractive) {
clearConsole();
}
console.log('Compiling...');
});
let isFirstCompile = true;
compiler.plugin('done', stats => {
// 监听了 done 事件,对输出的日志作了格式化输出
// 正常状况下会直接输出 `Compiled successfully!`
// 若是有错误则输出错误信息,这里对错误信息作一些处理,让其输出比较友好
});
return compiler;
}
复制代码
以前就一直好奇,这些脚手架是怎么清空咱们的终端屏幕的。在看create-react-app
的时候,瞄到有这么一个文件react-dev-utils/clearConsole.js。这个文件十分剪短,核心代码就那么一句:
process.stdout.write(process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H');
复制代码
而后好奇心特别重,不知道后面两串是什么意思,一直搜没有找到想要的答案。
问了身边的同事,说是十六进制,而在我狭隘的认知里面一直觉得十六进制只能转成数字....可是定睛一看,这有个J
明显不是十六进制。
一个女装大佬和我说这是ASCII码,百度了一下ASCII码,看了 \x1B
ASCII对应到 ESC
。 可是后面的 [2J
[3J
[H
是什么意思仍是不清楚... 后面大佬又和我说找到多是 Linux ANSI 控制码 找来找去折腾了挺久的后面才揭开神秘面纱~
这几个命令大概的意思是: [2J
清除控制台 [H
将光标移至最顶部 [3J
仍是没有找到,应该是更高级的系统层级的清除控制台
给出几个 Linux ANSI 控制码资料网站有兴趣能够自行了解一下做为知识储备
Ubuntu Manpage: 控制终端代码 - Linux 控制终端转义和控制序列
控制终端代码 - Linux 控制终端转义和控制序列(转) - 木瓜脑壳 - 博客园
最后前端的小伙伴不少和我同样不是科班出身的,真的得加把劲补补一些计算机比较原理性或比较接近系统层级的知识~