This paper links: jsonz1993. Making. IO / 2018/05 / cre…
Series 2 Address
Work has stabilized recently, not so much overtime… So you start to have free time to learn some front-end stuff
One of the company’s bigwigs had written a scaffolding like create-React-App to create the company’s projects. Create-react-app (create-react-app, create-React-app)
We do not see the source code is afraid to see, now so good projects are open source, plus a variety of IDE support is very good, directly hit a breakpoint debugging, it is easy to see about. You can also use this approach to other open source projects
Emmmm writes for the first time to accept any ridicule
Quickly understand
For those who want a quick look, just browse this section
Create-react-app uses Node to run the package installation process and file template demo into the appropriate directory.
It can be simply divided into the following steps:
- Checking the Node Version
- Do some initialization for command line processing, such as typing
-help
The help content is displayed - Check whether the project name is entered. If yes, install the package according to the parameters. The default installation mode is YARN.
yarn add react react-dom react-scripts
- Modify the installed dependencies in package.json from the exact version
16.0.0
Change to ^ Upward compatible version^ 16.0.0
And addstart
.build
Etc startup script - copy
react-scripts
Under thetemplate
To the target file. It haspublic
.src
Etc folder, in fact, is a simple working demo - END~
Continue to look at the small partners can follow the step by step to understand the implementation logic, the precedent line explains the environment version:
Create-react-app v1.1.4 macOS 10.13.4 node v8.9.4 NPM 6.0.0 YARN 1.6.0 vsCode 1.22.2Copy the code
Project initialization
Go to Github and pull the project code, then switch to the specified tag
git clone https://github.com/facebook/create-react-app.git
Git checkout v1.1.4
yarn
// This step can be skipped if breakpoint debugging is not required
If the yarn version is too low, a series of errors will be reported. The previous version is 0.x, and the upgrade to 1.x will be ok
Let’s use root instead of the project root directory to make things easier to understand
First we open the project and see a bunch of configuration files and two folders: ESLint configuration files, Travis deployment configuration, YARN configuration, update log, open source declaration, etc… We don’t have to look at all of these, so where is the core source code we want to look at
Highlight: If the project doesn’t know where to start, where to start with a package.json file
root/package.json
{
"private": true."workspaces": [
"packages/*"]."scripts": {
"start": "cd packages/react-scripts && node scripts/start.js",},"devDependencies": {},"lint-staged": {}}Copy the code
Json: NPM script, development dependencies, and commit hooks. The rest of the workspaces we need to focus on are “Packages /*”, so we’ll focus on packages
Root /packages/create-react-app/package.json root/packages/create-react-app/package
packages/create-react-app/package.json
{
"name": "create-react-app"."version": "1.5.2"."license": "MIT"."engines": {},"bugs": {},"files": [
"index.js"."createReactApp.js"]."bin": {
"create-react-app": "./index.js"
},
"dependencies": {}}Copy the code
There is no workspaces item at this time, we can look at bin. The function of bin is to map commands to executable files, see package Document for details
When create-react-app is installed globally, Run create-react-app my-react-app to run packages/create-react-app/index.js my-react-app
Finally found the source code entry, for simple source code we can directly read, for more complex or want to see the execution of each line of code when the variables are what the value of the case, we will use IDE or other tools to debug the code breakpoint.
Configuring breakpoint debugging
Those familiar with vscode or node debugging can skip the start breakpoint and read the source code
vscode debug
For vscode users, debugging is very simple, click on the bug icon in the sidebar, click on Settings and change the value of “program” directly, then click the green arrow in the upper left corner to run, if you want to break at a certain point, For example, create-react-app/index.js line39 breakpoint, just click to the left of the line
Launch. The json configuration
{
"version": "0.2.0"."configurations": [{"type": "node"."request": "launch"."name": "Start program"."program": "${workspaceFolder}/packages/create-react-app/index.js",}}]Copy the code
The node debugging
If you don’t use vscode or are used to chrome-devtool, you can run the node command directly. Debug in Chrome. First make sure node is version 6 or older. Then run node –inspect-brk Packages /create-react-app/index.js in the project root directory and type in the Chrome address bar Chrome ://inspect/#devices Then you can see the script we are going to debug for Node Chrome-devtool
Start reading the source code at breakpoint
packages/create-react-app/index.js Github file portal
./createReactApp
packages/create-react-app/createReactApp.js Github file portal
Follow our breakpoint to Createreactapp.js. The file is 750 lines long and at first glance looks like a lot, with a dozen dependencies introduced in the header, but don’t be alarmed, most of these high-quality open source projects are full of comments and error-friendly messages.
Try copying the code to another JS file, and then don’t look at the dependencies in front of it, and then go to NPM to check what is used. Don’t get sidetracked by looking at one dependency after another without seeing the core code. Then I read a section of the code and delete that section of the code. For example, IF I read 200 lines, I delete the first 200 lines so that the remaining 500 lines don’t look so guilty. Of course, it is recommended to debug reading with breakpoints, logic will be more clear.
First of all, we will not pay attention to the dependencies in the header of the file, and we will check them later
const validateProjectName = require('validate-npm-package-name');
const chalk = require('chalk');
const commander = require('commander');
const fs = require('fs-extra');
const path = require('path');
const execSync = require('child_process').execSync;
const spawn = require('cross-spawn');
const semver = require('semver');
const dns = require('dns');
const tmp = require('tmp');
const unpack = require('tar-pack').unpack;
const url = require('url');
const hyperquest = require('hyperquest');
const envinfo = require('envinfo');
Copy the code
Commander Command line processor
Following our breakpoint, the first line of code to be executed is L56
const program = new commander.Command(packageJson.name)
.version(packageJson.version) ${packagejson.version} ${packagejson.version}
.arguments('<project-directory>') / / here in a < > project - project directory said - directory is required
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(name= > {
projectName = name;
}) // Get the first argument passed in by the user as projectName **
.option('--verbose'.'print additional logs') // option is used to configure 'create-react-app -[option]'. For example, if the user parameter contains --verbose, program.verbose = true is automatically set.
.option('--info'.'print environment debug info') // This parameter will be used later to print version information for environment debugging
.option(
'--scripts-version <alternative-package>'.'use a non-standard version of react-scripts'
)
.option('--use-npm')
.allowUnknownOption()
// on('option', cb) enter create-react-app --help to automatically perform the following operations and output help
.on('--help', () = > {console.log(` Only ${chalk.green('<project-directory>')} is required.`);
console.log();
console.log(
` A custom ${chalk.cyan('--scripts-version')} can be one of:`
);
console.log(` - a specific npm version: ${chalk.green('0.8.2')}`);
console.log(
` - a custom fork published on npm: ${chalk.green(
'my-react-scripts'
)}`
);
console.log(
` - a .tgz archive: ${chalk.green(
'https://mysite.com/my-react-scripts-0.8.2.tgz'
)}`
);
console.log(
` - a .tar.gz archive: ${chalk.green(
'https://mysite.com/my-react-scripts-0.8.2.tar.gz'
)}`
);
console.log(
` It is not needed unless you specifically want to use a fork.`
);
console.log();
console.log(
` If you have any problems, do not hesitate to file an issue:`
);
console.log(
` ${chalk.cyan(
'https://github.com/facebookincubator/create-react-app/issues/new'
)}`
);
console.log();
})
.parse(process.argv); // Parses passed arguments can be ignored
Copy the code
The commander dependency is used here, so we can go to NPM to find out what it does. The complete Solution for Node.js command-line interfaces Inspired by Ruby’s Commander.API Documentation translates to a complete solution for the Node.js command line interface. Github Portal.
Check whether projectName is passed
if (typeof projectName === 'undefined') {
if (program.info) { // If the command line has the --info argument, print the react,react-dom,react-scripts versions and exit
envinfo.print({
packages: ['react'.'react-dom'.'react-scripts'].noNativeIDE: true.duplicates: true}); process.exit(0); }... Some error messages are printed here... process.exit(1);
}
Copy the code
Action (name => {projectName = name; }. Determine if there is no input, simply do some information, and then terminate the program. If –info is passed in, envinfo.print is executed. Check envinfo for NPM. This is a tool that displays system information about the current environment, such as the system version, NPM, etc. React,react-dom,react-scripts, etc. The current version of the package is quite different from the create-React-app version, but it does not affect our use of the ~ envinfo NPM portal
With the vscode debug configuration I provided above, the program should be finished at this point because we didn’t pass the projectName to the script when we started the debug service. Json “args”: [“test-create-react-app”] forget how to set the projectName parameter
{
"version": "0.2.0"."configurations": [{"type": "node"."request": "launch"."name": "Start program"."program": "${workspaceFolder}/packages/create-react-app/index.js"."args": [
"test-create-react-app"]]}}Copy the code
The commander parameter is hidden
Proceed to Line140 after judging projectName
const hiddenProgram = new commander.Command()
.option(
'--internal-testing-template <path-to-template>'.'(internal usage only, DO NOT RELY ON THIS) ' +
'use a non-standard application template'
)
.parse(process.argv);
Copy the code
As you can see, this is a hidden debug option that gives you a parameter to pass in the template path to the developer for debugging. Don’t screw him over
createApp
createApp(
projectName,
program.verbose,
program.scriptsVersion,
program.useNpm,
hiddenProgram.internalTestingTemplate
);
Copy the code
CreateApp is then called, passing in parameters that mean: the project name, whether to output additional information, the version of the script passed in, whether to use NPM, and the template path to debug. Next step into the function body to see what createApp actually does.
function createApp(name, verbose, version, useNpm, template) {
const root = path.resolve(name);
const appName = path.basename(root);
checkAppName(appName); // Check the validity of the incoming project name
fs.ensureDirSync(name); // Fs extra is used to extend node fs
// Check whether the new folder is safe
if(! isSafeToCreateProjectIn(root, name)) { process.exit(1);
}
// Write the package.json file to the new folder
const packageJson = {
name: appName,
version: '0.1.0 from'.private: true}; fs.writeFileSync( path.join(root,'package.json'),
JSON.stringify(packageJson, null.2));const useYarn = useNpm ? false : shouldUseYarn();
const originalDirectory = process.cwd();
process.chdir(root);
// If NPM is used, check whether NPM is executed in the correct directory
if(! useYarn && ! checkThatNpmCanReadCwd()) { process.exit(1);
}
// Determine the Node environment, print some prompts, and use the old version of react-scripts
if(! semver.satisfies(process.version,'> = 6.0.0')) {
// Output some prompt update information
version = '[email protected]';
}
if(! useYarn) {// Check the NPM version. If the NPM version is lower than 3.x, use the old version of React-scripts
const npmInfo = checkNpmVersion();
if(! npmInfo.hasMinNpm) { version ='[email protected]'; }}// After judging, run the run method
// Pass in the project path, project name, reactScripts version, whether to enter additional information, the path to run, templates (for development and debugging), and whether to use YARN
run(root, appName, version, verbose, originalDirectory, template, useYarn);
}
Copy the code
Createreactapp.js createApp portal I’ve simplified a few things here, deleted some output, and added some comments. The main thing createApp does is make some security judgments like: Check that the project name is valid, that the new version is safe, that the NPM version is compatible with the react-script version. The execution logic is written in the comment. After a series of checks, call the run method, passing in the project path, the project name, and the reactScripts version. Whether to enter additional information, the path to run, the template (for development and debugging), whether to use YARN. Once you understand the general flow, look at it function by function.
CheckAppName () // Check the validity of the incoming project name isSafeToCreateProjectIn(root, ShouldUseYarn () // Check yarn checkThatNpmCanReadCwd() // Check NPM run() // After the check, call run to perform installation and other operations
checkAppNameCheck that projectName is valid
function checkAppName(appName) {
const validationResult = validateProjectName(appName);
if(! validationResult.validForNewPackages) {// Check whether NPM specifications are met. If not, output a message and end the task
}
const dependencies = ['react'.'react-dom'.'react-scripts'].sort();
if (dependencies.indexOf(appName) >= 0) {
// Check whether the name is the same. If the name is the same, output a prompt and end the task}}Copy the code
CheckAppName is used to check whether the current project name complies with the NPM specification. For example, it cannot be capitated. The NPM package name is validate-npm-package-name. This simplifies most of the error code, but does not affect taste.
shouldUseYarnCheck whether yarn is installedcheckThatNpmCanReadCwd
Used to determine NPM
function shouldUseYarn() {
try {
execSync('yarnpkg --version', { stdio: 'ignore' });
return true;
} catch (e) {
return false; }}Copy the code
run
This is where the real core installation logic comes in. __ starts installing dependencies, copying templates, etc.
function run(.) {
// Here is the package to install, default is' react-scripts'. It may also be based on the pass to get the corresponding package
const packageToInstall = getInstallPackage(version, originalDirectory);
// Install all dependencies like react, react-dom, react-script
const allDependencies = ['react'.'react-dom', packageToInstall]; . }Copy the code
Run gets a package to install based on the version passed in and the originalDirectory, originalDirectory. The default version is empty, the packageToInstall value is react-scripts, and then the packageToInstall value is concatenated to allDependencies, meaning allDependencies that need to be installed. React-scripts is a set of Webpack configurations and templates that are part of the other core of create-React-App. portal
function run(.) {.../ / get the package name, support taz | tar format, git repository, version number, the file path and so on
getPackageName(packageToInstall)
.then(packageName= >
// If yarn is used, determine whether it is in online mode (corresponding to offline mode), and return to the next then processing
checkIfOnline(useYarn).then(isOnline= > ({
isOnline: isOnline,
packageName: packageName,
}))
)
.then(info= > {
const isOnline = info.isOnline;
const packageName = info.packageName;
/** Start the installation part of the core by passing in 'install path', 'use YARN', 'all dependencies',' Output additional information ', 'online status' **/
/ * * this is the main operating according to the incoming parameters, began to run NPM | | yarn to install the react the react - dom rely on * * /
/** if the network is not good, it may hang **/
return install(root, useYarn, allDependencies, verbose, isOnline).then(
(a)= >packageName ); })... }Copy the code
If the yarn installation mode is used, check whether the yarn installation mode is offline. Add the packageToInstall and allDependencies dependencies to the install method.
Run method getInstallPackage (); // The default is react-scripts install(); // Install allDependencies init(); // call react-scripts/script/init to copy the template. Catch (); // Error handling
install
function install(root, useYarn, dependencies, verbose, isOnline) {
// Use node to run installation scripts such as' NPM install react react-dom --save 'or' yarn add react react-dom
return new Promise((resolve, reject) = > {
let command;
let args;
// Start assembling the YARN command line
if (useYarn) {
command = 'yarnpkg';
args = ['add'.'--exact']; // Use the exact version mode
// Check whether the status is offline and add a status
if(! isOnline) { args.push('--offline');
}
[].push.apply(args, dependencies);
// Set CWD to the directory we want to install
args.push('--cwd');
args.push(root);
// Output some information if it is offline
} else {
// NPM installation mode, the same as YARN
command = 'npm';
args = [
'install'.'--save'.'--save-exact'.'--loglevel'.'error',
].concat(dependencies);
}
// If verbose is passed, add this parameter to output additional information
if (verbose) {
args.push('--verbose');
}
// execute the command line cross-platform with cross-spawn
const child = spawn(command, args, { stdio: 'inherit' });
// Close the handler
child.on('close', code => {
if(code ! = =0) {
return reject({ command: `${command} ${args.join(' ')}`}); } resolve(); }); }); }Copy the code
As we follow the breakpoint from run to install, we can see that the code is split into two processing methods depending on whether yarn is used. If (useYarn) {yarn install logic} else {NPM install logic} {react,react-dom,react-script} Verbose and isOnline plus some command line arguments. Platform differences are handled with cross-spawn and will not be described here. See the above code for specific logic, remove unimportant information output, the code is relatively easy to understand.
Install Determine whether to use YARN or NPM based on the input parameters. Run the cross-spawn command to install dependencies required by NPM
After install returns a Promise, the breakpoint goes back to our run function to continue with the following logic.
function run() {... getPackageName() .then((a)= > {
return install(root, useYarn, allDependencies, verbose, isOnline).then(
(a)= >packageName ); })... }Copy the code
Now that we have installed the dependencies we need for development, we can determine if the node we are currently running matches the version of node we have installed for the act-scripts packages.json. If the node version currently running is react-scripts, the dependency is required.
Then change the dependencies (react, react-dom, react-scripts) we have installed from the exact version eg(16.0.0) to the higher or equal version eg(^16.0.0). After that, our directory looks like this, with nothing in it but installed dependencies and package.json. So the next step is to generate some webpack configuration and a simple bootable demo.
So how does he make these things so fast? Remember that there is a hidden command line parameter, internal-tet-template, for developers to debug, so create-react-app generates this by simply copying the template from a path to the appropriate location. Isn’t that simple? HHHHH
run(...) {... getPackageName(packageToInstall) .then(...) .then(info= >install(...) .then((a)= > packageName))
/** install Logic after installation **/
/** Copy template logic from here **/
.then(packageName= > {
// After installing react, react-dom, and react-scripts, check whether the node version running in the current environment meets the requirements
checkNodeVersion(packageName);
// React, react-dom version range in package.json, eg: 16.0.0 => ^16.0.0
setCaretRangeForRuntimeDeps(packageName);
// Load the script and execute the init method
const scriptsPath = path.resolve(
process.cwd(),
'node_modules',
packageName,
'scripts'.'init.js'
);
const init = require(scriptsPath);
The init method mainly performs the following operations
// Write some scripts to package.json. eg: script: {start: 'react-scripts start'}
/ / rewrite the README. MD
// Copy the preset template to the project
// Displays information about success and subsequent operations
init(root, appName, verbose, originalDirectory, template);
if (version === '[email protected]') {
// If it is an older version of react-scripts output prompt
}
})
.catch(reason= > {
// Delete all installed files and output some log messages
});
}
Copy the code
After the dependency is installed, run the checkNodeVersion command to check whether the Node version matches the dependency. Then concatenate the path to /node_modules/react-scripts/scripts/init.js and pass it to do some initialization. Then do something about the error
/node_modules/react-scripts/script/init.js
Target folder /node_modules/react-scripts/script/init.js
module.exports = function(appPath, appName, verbose, originalDirectory, template) {
const ownPackageName = require(path.join(__dirname, '.. '.'package.json'))
.name;
const ownPath = path.join(appPath, 'node_modules', ownPackageName);
const appPackage = require(path.join(appPath, 'package.json'));
const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
// 1. Write the startup script to the destination package.json
appPackage.scripts = {
start: 'react-scripts start'.build: 'react-scripts build'.test: 'react-scripts test --env=jsdom'.eject: 'react-scripts eject'}; fs.writeFileSync( path.join(appPath,'package.json'),
JSON.stringify(appPackage, null.2));// 2. Rewrite readme. MD to include some help information
const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
if (readmeExists) {
fs.renameSync(
path.join(appPath, 'README.md'),
path.join(appPath, 'README.old.md')); }Public, SRC /[app.css, app.js, index.js,....] , .gitignore
const templatePath = template
? path.resolve(originalDirectory, template)
: path.join(ownPath, 'template');
if (fs.existsSync(templatePath)) {
fs.copySync(templatePath, appPath);
} else {
return;
}
fs.move(
path.join(appPath, 'gitignore'),
path.join(appPath, '.gitignore'),
[],
err => { /* Error handling */});// Install react and react-dom again
let command;
let args;
if (useYarn) {
command = 'yarnpkg';
args = ['add'];
} else {
command = 'npm';
args = ['install'.'--save', verbose && '--verbose'].filter(e= > e);
}
args.push('react'.'react-dom');
const templateDependenciesPath = path.join(
appPath,
'.template.dependencies.json'
);
if (fs.existsSync(templateDependenciesPath)) {
const templateDependencies = require(templateDependenciesPath).dependencies;
args = args.concat(
Object.keys(templateDependencies).map(key= > {
return `${key}@${templateDependencies[key]}`; })); fs.unlinkSync(templateDependenciesPath); }if(! isReactInstalled(appPackage) || template) {const proc = spawn.sync(command, args, { stdio: 'inherit' });
if(proc.status ! = =0) {
console.error(` \ `${command} ${args.join(' ')}\` failed`);
return; }}// 5. Logs are generated successfully
};
Copy the code
Init file is also a big head, processing logic mainly has
- Modify package.json by writing some startup scripts such as
script: {start: 'react-scripts start'}
To start a development project - Rewrite readme. MD to include some help information
- Copy the preset template to the project
public
.src/[APP.css, APP.js, index.js,....]
..gitignore
- Do some compatibility with older versions of Node. When selecting React-scripts, you can select the older @0.9.x version based on node versions.
- If the output is complete. If the output fails, perform operations such as outputting logs.
There’s a lot of code here, so I cut out a little bit, but if you’re interested in the original code you can jump here to see the react-scripts/scripts/init.js portal
END~
Now that the create-React-app project is part of the process, let’s review it:
- If the node version is smaller than 4, exit; otherwise, execute
createReactApp.js
file createReactApp.js
Do some command line processing and response processing, and then determine if there’s any incomingprojectName
If not, prompt and exit- According to incoming
projectName
Create a directory and createpackage.json
. - Determine whether there is a special requirement to install a version
react-scripts
And then usecross-spawn
To handle cross-platform command line issues, useyarn
ornpm
The installationreact
.react-dom
.react-scripts
. - Run after installation
react-scripts/script/init.js
Modify thepackage.json
Run the script and copy the corresponding template to the directory. - After processing this, output a prompt to the user.
I wanted to cover create-react-app, but I realized that there was only one creation, so IF I want to continue with React-scripts, I will write another one. React-scripts is probably a webpack configuration, so breakpoints don’t help much.