原文地址Nealyang/personalBlogcss
对于前端工程构建,不少公司、BU 都有本身的一套构建体系,好比咱们正在使用的 def,或者 vue-cli 或者 create-react-app,因为笔者最近一直想搭建一个我的网站,秉持着呼吸不停,折腾不止的原则,编码的过程当中,仍是不想太过于枯燥。在 coding 以前,搭建本身的项目架构的时候,忽然想,为何以前搭建过不少的项目架构不能直接拿来用,却仍是要从 0 到 1 的去写 webpack 去下载相关配置呢?遂!学习下 create-react-app 源码,而后本身搞一套吧~前端
代码的入口在 packages/create-react-app/index.js
下,核心代码在createReactApp.js
中,虽然有大概 900+行代码,可是删除注释和一些友好提示啥的大概核心代码也就六百多行吧,咱们直接来看vue
index.js 的代码很是的简单,其实就是对 node 的版本作了一下校验,若是版本号低于 8,就退出应用程序,不然直接进入到核心文件中,createReactApp.js
中node
createReactApp 的功能也很是简单其实,大概流程:react
create-react-app --info
的输出等react-script
下的模板文件{
"type": "node",
"request": "launch",
"name": "CreateReactApp",
"program": "${workspaceFolder}/packages/create-react-app/index.js",
"args": [
"study-create-react-app-source"
]
},
{
"type": "node",
"request": "launch",
"name": "CreateReactAppNoArgs",
"program": "${workspaceFolder}/packages/create-react-app/index.js"
},
{
"type": "node",
"request": "launch",
"name": "CreateReactAppTs",
"program": "${workspaceFolder}/packages/create-react-app/index.js",
"args": [
"study-create-react-app-source-ts --typescript"
]
}
复制代码
这里咱们添加三种环境,其实就是 create-react-app 的不一样种使用方式webpack
create-react-app study-create-react-app-source
create-react-app
create-react-app study-create-react-app-source-ts --typescript
let projectName;
const program = new commander.Command(packageJson.name)
.version(packageJson.version)//create-react-app -v 时候输出的值 packageJson 来自上面 const packageJson = require('./package.json');
.arguments('<project-directory>') //定义 project-directory ,必填项
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(name => {
projectName = name;//获取用户的输入,存为 projectName
})
.option('--verbose', 'print additional logs')
.option('--info', 'print environment debug info')
.option(
'--scripts-version <alternative-package>',
'use a non-standard version of react-scripts'
)
.option('--use-npm')
.option('--use-pnp')
.option('--typescript')
.allowUnknownOption()
.on('--help', () => {// on('option', cb) 语法,输入 create-react-app --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 specific npm tag: ${chalk.green('@next')}`);
console.log(
` - a custom fork published on npm: ${chalk.green(
'my-react-scripts'
)}`
);
console.log(
` - a local path relative to the current working directory: ${chalk.green(
'file:../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/facebook/create-react-app/issues/new'
)}`
);
console.log();
})
.parse(process.argv);
复制代码
关于 commander 的使用,这里就不介绍了,对于 create-react-app 的流程咱们须要知道的是,它,初始化了一些 create-react-app 的命令行环境,这一波操做后,咱们能够看到 program 张这个样纸:github
接着往下走web
当咱们 debug 启动 noArgs
环境的时候,走到这里就结束了,判断 projectName 是否为 undefined,而后输出相关提示信息,退出~vue-cli
在查看 createApp function 以前,咱们再回头看下命令行的一些参数定义,方便咱们理解 createApp 的一些参数
咱们使用
{
"type": "node",
"request": "launch",
"name": "CreateReactAppTs",
"program": "${workspaceFolder}/packages/create-react-app/index.js",
"args": [
"study-create-react-app-source-ts",
"--typescript",
"--use-npm"
]
}
复制代码
debugger 咱们项目的时候,就能够看到,program.typescript
为 true
,useNpm
为 true
,固然,这些也都是咱们在commander
中定义的 options,因此源码里面 createApp 中,咱们传入的参数分别为:
function createApp(
name,
verbose,
version,
useNpm,
usePnp,
useTypescript,
template
) {
const root = path.resolve(name);//path 拼接路径
const appName = path.basename(root);//获取文件名
checkAppName(appName);//检查传入的文件名合法性
fs.ensureDirSync(name);//确保目录存在,若是不存在则建立一个
if (!isSafeToCreateProjectIn(root, name)) { //判断新建这个文件夹是否安全,不然直接退出
process.exit(1);
}
console.log(`Creating a new React app in ${chalk.green(root)}.`);
console.log();
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
);//写入 package.json 文件
const useYarn = useNpm ? false : shouldUseYarn();//判断是使用 yarn 呢仍是 npm
const originalDirectory = process.cwd();
process.chdir(root);
if (!useYarn && !checkThatNpmCanReadCwd()) {//若是是使用npm,检测npm是否在正确目录下执行
process.exit(1);
}
if (!semver.satisfies(process.version, '>=8.10.0')) {//判断node环境,输出一些提示信息, 并采用旧版本的 react-scripts
console.log(
chalk.yellow(
`You are using Node ${
process.version
} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
`Please update to Node 8.10 or higher for a better, fully supported experience.\n`
)
);
// Fall back to latest supported react-scripts on Node 4
version = 'react-scripts@0.9.x';
}
if (!useYarn) {//关于 npm、pnp、yarn 的使用判断,版本校验等
const npmInfo = checkNpmVersion();
if (!npmInfo.hasMinNpm) {
if (npmInfo.npmVersion) {
console.log(
chalk.yellow(
`You are using npm ${
npmInfo.npmVersion
} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
`Please update to npm 5 or higher for a better, fully supported experience.\n`
)
);
}
// Fall back to latest supported react-scripts for npm 3
version = 'react-scripts@0.9.x';
}
} else if (usePnp) {
const yarnInfo = checkYarnVersion();
if (!yarnInfo.hasMinYarnPnp) {
if (yarnInfo.yarnVersion) {
console.log(
chalk.yellow(
`You are using Yarn ${
yarnInfo.yarnVersion
} together with the --use-pnp flag, but Plug'n'Play is only supported starting from the 1.12 release.\n\n` +
`Please update to Yarn 1.12 or higher for a better, fully supported experience.\n`
)
);
}
// 1.11 had an issue with webpack-dev-middleware, so better not use PnP with it (never reached stable, but still)
usePnp = false;
}
}
if (useYarn) {
let yarnUsesDefaultRegistry = true;
try {
yarnUsesDefaultRegistry =
execSync('yarnpkg config get registry')
.toString()
.trim() === 'https://registry.yarnpkg.com';
} catch (e) {
// ignore
}
if (yarnUsesDefaultRegistry) {
fs.copySync(
require.resolve('./yarn.lock.cached'),
path.join(root, 'yarn.lock')
);
}
}
run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp,
useTypescript
);
}
复制代码
代码很是简单,部分注释已经加载代码中,简单的说就是对一个本地环境的一些校验,版本检查啊、目录建立啊啥的,若是建立失败,则退出,若是版本较低,则使用对应低版本的create-react-app
,最后调用 run 方法
这些工具方法,其实在写咱们本身的构建工具的时候,也能够直接 copy 的哈,因此这里咱们也是简单看下里面的实现,
checkAPPName 方法主要的核心代码是validate-npm-package-name
package,从名字便可看出,检查是否为合法的 npm 包名
var done = function (warnings, errors) {
var result = {
validForNewPackages: errors.length === 0 && warnings.length === 0,
validForOldPackages: errors.length === 0,
warnings: warnings,
errors: errors
}
if (!result.warnings.length) delete result.warnings
if (!result.errors.length) delete result.errors
return result
}
...
...
var validate = module.exports = function (name) {
var warnings = []
var errors = []
if (name === null) {
errors.push('name 不能使 null')
return done(warnings, errors)
}
if (name === undefined) {
errors.push('name 不能是 undefined')
return done(warnings, errors)
}
if (typeof name !== 'string') {
errors.push('name 必须是 string 类型')
return done(warnings, errors)
}
if (!name.length) {
errors.push('name 的长度必须大于 0')
}
if (name.match(/^\./)) {
errors.push('name 不能以点开头')
}
if (name.match(/^_/)) {
errors.push('name 不能如下划线开头')
}
if (name.trim() !== name) {
errors.push('name 不能包含前空格和尾空格')
}
// No funny business
// var blacklist = [
// 'node_modules',
// 'favicon.ico'
// ]
blacklist.forEach(function (blacklistedName) {
if (name.toLowerCase() === blacklistedName) { //不能是“黑名单”内的
errors.push(blacklistedName + ' is a blacklisted name')
}
})
// Generate warnings for stuff that used to be allowed
// 为之前容许的内容生成警告
// 后面的就再也不赘述了
return done(warnings, errors)
}
复制代码
最终,checkAPPName返回的东西如截图所示,后面写代码能够直接拿来借鉴!借鉴~
所谓安全性校验,其实就是检查当前目录下是否存在已有文件。
后面的代码也都比较简单,这里就不展开说了,版本比较实用的是一个semver package.
代码跑到这里,该检查的都检查了,鸡也不叫了、狗也不咬了,该干点正事了~
run 主要作的事情就是安装依赖、拷贝模板。
getInstallPackage
作的事情很是简单,根据传入的 version 和原始路径 originalDirectory 去获取要安装的 package 列表,默认状况下version 为 undefined,获取到的 packageToInstall 为react-scripts
,也就是咱们如上图的 resolve 回调。
最终,咱们拿到须要安装的 info 为
{
isOnline:true,
packageName:"react-scripts"
}
复制代码
当咱们梳理好须要安装的 package 后,就交给 npm 或者 yarn 去安装咱们的依赖便可
在spawn
执行完命令后会有一个回调,判断code是否为 0,而后 resolve Promise,
.then(async packageName => {
// 安装完 react, react-dom, react-scripts 以后检查当前环境运行的node版本是否符合要求
checkNodeVersion(packageName);
// 检查 package.json 中的版本号
setCaretRangeForRuntimeDeps(packageName);
const pnpPath = path.resolve(process.cwd(), '.pnp.js');
const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];
await executeNodeScript(
{
cwd: process.cwd(),
args: nodeArgs,
},
[root, appName, verbose, originalDirectory, template],
`
var init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
复制代码
在 create-react-app
以前的版本中,这里是经过调用react-script
下的 init
方法来执行后续动做的。这里经过调用executeNodeScript
方法
function executeNodeScript({ cwd, args }, data, source) {
// cwd:"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source"
// data:
// 0:"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source"
// 1:"study-create-react-app-source"
// 2:undefined
// 3:"/Users/nealyang/Desktop/create-react-app"
// 4:undefined
// source
// " var init = require('react-scripts/scripts/init.js'); // init.apply(null, JSON.parse(process.argv[1])); // "
return new Promise((resolve, reject) => {
const child = spawn(
process.execPath,
[...args, '-e', source, '--', JSON.stringify(data)],
{ cwd, stdio: 'inherit' }
);
child.on('close', code => {
if (code !== 0) {
reject({
command: `node ${args.join(' ')}`,
});
return;
}
resolve();
});
});
}
复制代码
executeNodeScript
方法主要是经过 spawn 来经过 node命令执行react-script
下的 init 方法。因此截止当前,create-react-app
完成了他的工做:npm i
,
修改 vscode 的 debugger 配置,而后咱们来 debugger react-script 下的 init 方法
function init(appPath, appName, verbose, originalDirectory, template) {
// 获取当前包中包含 package.json 所在的文件夹路径
const ownPath = path.dirname(
//"/Users/nealyang/Desktop/create-react-app/packages/react-scripts"
require.resolve(path.join(__dirname, '..', 'package.json'))
);
const appPackage = require(path.join(appPath, 'package.json')); //项目目录下的 package.json
const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock')); //经过判断目录下是否有 yarn.lock 来判断是否使用 yarn
// Copy over some of the devDependencies
appPackage.dependencies = appPackage.dependencies || {};
// react:"16.8.6"
// react-dom:"16.8.6"
// react-scripts:"3.0.1"
const useTypeScript = appPackage.dependencies['typescript'] != null;
// Setup the script rules 设置 script 命令
appPackage.scripts = {
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test',
eject: 'react-scripts eject',
};
// Setup the eslint config 这是 eslint 的配置
appPackage.eslintConfig = {
extends: 'react-app',
};
// Setup the browsers list 组件autoprefixer、bable-preset-env、eslint-plugin-compat、postcss-normalize共享使用的配置项 (感谢网友指正)
appPackage.browserslist = defaultBrowsers;
// 写入咱们须要建立的目录下的 package.json 中
fs.writeFileSync(
path.join(appPath, 'package.json'),
JSON.stringify(appPackage, null, 2) + os.EOL
);
const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
if (readmeExists) {
fs.renameSync(
path.join(appPath, 'README.md'),
path.join(appPath, 'README.old.md')
);
}
// Copy the files for the user 获取模板的路径
const templatePath = template //"/Users/nealyang/Desktop/create-react-app/packages/react-scripts/template"
? path.resolve(originalDirectory, template)
: path.join(ownPath, useTypeScript ? 'template-typescript' : 'template');
if (fs.existsSync(templatePath)) {
// 这一步就过度了, 直接 copy! appPath:"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source"
fs.copySync(templatePath, appPath);
} else {
console.error(
`Could not locate supplied template: ${chalk.green(templatePath)}`
);
return;
}
// Rename gitignore after the fact to prevent npm from renaming it to .npmignore 重命名gitignore以防止npm将其重命名为.npmignore
// See: https://github.com/npm/npm/issues/1862
try {
fs.moveSync(
path.join(appPath, 'gitignore'),
path.join(appPath, '.gitignore'),
[]
);
} catch (err) {
// Append if there's already a `.gitignore` file there if (err.code === 'EEXIST') { const data = fs.readFileSync(path.join(appPath, 'gitignore')); fs.appendFileSync(path.join(appPath, '.gitignore'), data); fs.unlinkSync(path.join(appPath, 'gitignore')); } else { throw err; } } 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'); // args Array // 0:"install" // 1:"--save" // 2:"react" // 3:"react-dom" // 安装其余模板依赖项(若是存在) const templateDependenciesPath = path.join(//"/Users/nealyang/Desktop/create-react-app/study-create-react-app-source/.template.dependencies.json" 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); } // 安装react和react-dom以便与旧CRA cli向后兼容 // 没有安装react和react-dom以及react-scripts // 或模板是presetend(经过--internal-testing-template) if (!isReactInstalled(appPackage) || template) { console.log(`Installing react and react-dom using ${command}...`); console.log(); const proc = spawn.sync(command, args, { stdio: 'inherit' }); if (proc.status !== 0) { console.error(`\`${command} ${args.join(' ')}\` failed`); return; } } if (useTypeScript) { verifyTypeScriptSetup(); } if (tryGitInit(appPath)) { console.log(); console.log('Initialized a git repository.'); } // 显示最优雅的cd方式。 // 这须要处理未定义的originalDirectory // 向后兼容旧的global-cli。 let cdpath; if (originalDirectory && path.join(originalDirectory, appName) === appPath) { cdpath = appName; } else { cdpath = appPath; } // Change displayed command to yarn instead of yarnpkg const displayedCommand = useYarn ? 'yarn' : 'npm'; console.log('xxxx....xxxxx'); } 复制代码
初始化方法主要作的事情就是修改目标路径下的 package.json,添加一些配置命令,而后 copy!react-script 下的模板到目标路径下。
走到这一步,咱们的项目基本已经初始化完成了。
因此咱们 copy 了这么多 scripts
start: 'react-scripts start',
build: 'react-scripts build',
test: 'react-scripts test',
eject: 'react-scripts eject',
复制代码
到底是如何工做的呢,其实也不难,就是一些开发、测试、生产的环境配置。鉴于篇幅,咱就下一篇来分享下大佬们的前端构建的代码写法吧~~
原本想用一张流程图解释下,可是。。。create-react-app 着实没有作啥!咱仍是等下一篇分析完,本身写构建脚本的时候再画一下总体流程图(架构图)吧~
ok~ 简单概述下:
cross-spawn
来用命令行执行全部的安装通篇看完 package 的职能后,发现,哇,这有点简答啊~~其实,咱们学习源码的其实就是为了学习大佬们的一些边界状况处理,在后面本身开发的时候再去 copy~ 借鉴一些判断方法的编写。后面会再简单分析下react-scripts,而后写一个本身的一些项目架构脚本~