内容简介:原文地址对于前端工程构建,很多公司、BU 都有自己的一套构建体系,比如我们正在使用的 def,或者 vue-cli 或者 create-react-app,由于笔者最近一直想搭建一个个人网站,秉持着呼吸不停,折腾不止的原则,编码的过程中,还是不想太过于枯燥。在 coding 之前,搭建自己的项目架构的时候,突然想,为什么之前搭建过很多的项目架构不能直接拿来用,却还是要从 0 到 1 的去写 webpack 去下载相关配置呢?遂!学习下 create-react-app 源码,然后自己搞一套吧~代码的入口在
前言
对于前端工程构建,很多公司、BU 都有自己的一套构建体系,比如我们正在使用的 def,或者 vue-cli 或者 create-react-app,由于笔者最近一直想搭建一个个人网站,秉持着呼吸不停,折腾不止的原则,编码的过程中,还是不想太过于枯燥。在 coding 之前,搭建自己的项目架构的时候,突然想,为什么之前搭建过很多的项目架构不能直接拿来用,却还是要从 0 到 1 的去写 webpack 去下载相关配置呢?遂!学习下 create-react-app 源码,然后自己搞一套吧~
create-react-app 源码
代码的入口在 packages/create-react-app/index.js
下,核心代码在 createReactApp.js
中,虽然有大概 900+行代码,但是删除注释和一些友好提示啥的大概核心代码也就六百多行吧,我们直接来看
index.js
index.js 的代码非常的简单,其实就是对 node 的版本做了一下校验,如果版本号低于 8,就退出应用程序,否则直接进入到核心文件中, createReactApp.js
中
createReactApp.js
createReactApp 的功能也非常简单其实,大概流程:
- 命令初始化,比如自定义
create-react-app --info
的输出等 - 判断是否输入项目名称,如果有,则根据参数去跑安装,如果没有,给提示,然后退出程序
- 修改 package.json
- 拷贝
react-script
下的模板文件
准备工作:配置 vscode 的 debug 文件
{ "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 的不同种使用方式
create-react-app study-create-react-app-source create-react-app create-react-app study-create-react-app-source-ts --typescript
commander 命令行处理程序
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 张这个样纸:
接着往下走
当我们 debug 启动 noArgs
环境的时候,走到这里就结束了,判断 projectName 是否为 undefined,然后输出相关提示信息,退出~
createApp
在查看 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 中,我们传入的参数分别为:
- projectName : 项目名称
- program.verbose 是否输出额外信息
- program.scriptsVersion 传入的脚本版本
- program.useNpm 是否使用 npm
- program.usePnp 是否使用 Pnp
- program.typescript 是否使用 ts
- hiddenProgram.internalTestingTemplate 给开发者用的调试模板路径
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 方法
checkAppName
这些 工具 方法,其实在写我们自己的构建工具的时候,也可以直接 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返回的东西如截图所示,后面写代码可以直接拿来借鉴!借鉴~
isSafeToCreateProjectIn
所谓安全性校验,其实就是检查当前目录下是否存在已有文件。
checkNpmVersion
后面的代码也都比较简单,这里就不展开说了,版本比较实用的是一个 semver package.
run
代码跑到这里,该检查的都检查了,鸡也不叫了、狗也不咬了,该干点正事了~
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
,
react-script/init.js
修改 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 这是浏览器 openBrowser 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 ,然后写一个自己的一些项目架构脚本~
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Adobe Dreamweaver CS5中文版经典教程
Adobe公司 / 陈宗斌 / 人民邮电 / 2011-1 / 45.00元
《Adobe Dreamweaver CS5中文版经典教程》由Adobe公司的专家编写,是AdobeDreamweavelCS5软件的官方指定培训教材。全书共分为17课,每一课先介绍重要的知识点,然后借助具体的示例进行讲解,步骤详细、重点明确,手把手教你如何进行实际操作。全书是一个有机的整体,它涵盖了Dreamweavercs5的基础知识、HTML基础、CSS基础、创建页面布局、使用层叠样式表、使......一起来看看 《Adobe Dreamweaver CS5中文版经典教程》 这本书的介绍吧!