摘要:這里通過(guò)調(diào)用方法方法主要是通過(guò)來(lái)通過(guò)命令執(zhí)行下的方法。
原文地址Nealyang/personalBlog前言
對(duì)于前端工程構(gòu)建,很多公司、BU 都有自己的一套構(gòu)建體系,比如我們正在使用的 def,或者 vue-cli 或者 create-react-app,由于筆者最近一直想搭建一個(gè)個(gè)人網(wǎng)站,秉持著呼吸不停,折騰不止的原則,編碼的過(guò)程中,還是不想太過(guò)于枯燥。在 coding 之前,搭建自己的項(xiàng)目架構(gòu)的時(shí)候,突然想,為什么之前搭建過(guò)很多的項(xiàng)目架構(gòu)不能直接拿來(lái)用,卻還是要從 0 到 1 的去寫(xiě) webpack 去下載相關(guān)配置呢?遂!學(xué)習(xí)下 create-react-app 源碼,然后自己搞一套吧~
create-react-app 源碼代碼的入口在 packages/create-react-app/index.js下,核心代碼在createReactApp.js中,雖然有大概 900+行代碼,但是刪除注釋和一些友好提示啥的大概核心代碼也就六百多行吧,我們直接來(lái)看
index.jsindex.js 的代碼非常的簡(jiǎn)單,其實(shí)就是對(duì) node 的版本做了一下校驗(yàn),如果版本號(hào)低于 8,就退出應(yīng)用程序,否則直接進(jìn)入到核心文件中,createReactApp.js中
createReactApp.jscreateReactApp 的功能也非常簡(jiǎn)單其實(shí),大概流程:
命令初始化,比如自定義create-react-app --info 的輸出等
判斷是否輸入項(xiàng)目名稱,如果有,則根據(jù)參數(shù)去跑安裝,如果沒(méi)有,給提示,然后退出程序
修改 package.json
拷貝 react-script 下的模板文件
準(zhǔn)備工作:配置 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" ] }
這里我們添加三種環(huán)境,其實(shí)就是 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 命令行處理程序commander 文檔傳送門
let projectName; const program = new commander.Command(packageJson.name) .version(packageJson.version)//create-react-app -v 時(shí)候輸出的值 packageJson 來(lái)自上面 const packageJson = require("./package.json"); .arguments("") //定義 project-directory ,必填項(xiàng) .usage(`${chalk.green(" ")} [options]`) .action(name => { projectName = name;//獲取用戶的輸入,存為 projectName }) .option("--verbose", "print additional logs") .option("--info", "print environment debug info") .option( "--scripts-version ", "use a non-standard version of react-scripts" ) .option("--use-npm") .option("--use-pnp") .option("--typescript") .allowUnknownOption() .on("--help", () => {// on("option", cb) 語(yǔ)法,輸入 create-react-app --help 自動(dòng)執(zhí)行后面的操作輸出幫助 console.log(` Only ${chalk.green(" ")} 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);
關(guān)于 commander 的使用,這里就不介紹了,對(duì)于 create-react-app 的流程我們需要知道的是,它,初始化了一些 create-react-app 的命令行環(huán)境,這一波操作后,我們可以看到 program 張這個(gè)樣紙:
接著往下走
當(dāng)我們 debug 啟動(dòng) noArgs 環(huán)境的時(shí)候,走到這里就結(jié)束了,判斷 projectName 是否為 undefined,然后輸出相關(guān)提示信息,退出~
createApp在查看 createApp function 之前,我們?cè)倩仡^看下命令行的一些參數(shù)定義,方便我們理解 createApp 的一些參數(shù)
我們使用
{ "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 我們項(xiàng)目的時(shí)候,就可以看到,program.typescript 為 true,useNpm 為 true,當(dāng)然,這些也都是我們?cè)?b>commander中定義的 options,所以源碼里面 createApp 中,我們傳入的參數(shù)分別為:
projectName : 項(xiàng)目名稱
program.verbose 是否輸出額外信息
program.scriptsVersion 傳入的腳本版本
program.useNpm 是否使用 npm
program.usePnp 是否使用 Pnp
program.typescript 是否使用 ts
hiddenProgram.internalTestingTemplate 給開(kāi)發(fā)者用的調(diào)試模板路徑
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);//確保目錄存在,如果不存在則創(chuàng)建一個(gè) if (!isSafeToCreateProjectIn(root, name)) { //判斷新建這個(gè)文件夾是否安全,否則直接退出 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 );//寫(xiě)入 package.json 文件 const useYarn = useNpm ? false : shouldUseYarn();//判斷是使用 yarn 呢還是 npm const originalDirectory = process.cwd(); process.chdir(root); if (!useYarn && !checkThatNpmCanReadCwd()) {//如果是使用npm,檢測(cè)npm是否在正確目錄下執(zhí)行 process.exit(1); } if (!semver.satisfies(process.version, ">=8.10.0")) {//判斷node環(huán)境,輸出一些提示信息, 并采用舊版本的 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. ` + `Please update to Node 8.10 or higher for a better, fully supported experience. ` ) ); // Fall back to latest supported react-scripts on Node 4 version = "[email protected]"; } if (!useYarn) {//關(guān)于 npm、pnp、yarn 的使用判斷,版本校驗(yàn)等 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. ` + `Please update to npm 5 or higher for a better, fully supported experience. ` ) ); } // Fall back to latest supported react-scripts for npm 3 version = "[email protected]"; } } 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. ` + `Please update to Yarn 1.12 or higher for a better, fully supported experience. ` ) ); } // 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 ); }
代碼非常簡(jiǎn)單,部分注釋已經(jīng)加載代碼中,簡(jiǎn)單的說(shuō)就是對(duì)一個(gè)本地環(huán)境的一些校驗(yàn),版本檢查啊、目錄創(chuàng)建啊啥的,如果創(chuàng)建失敗,則退出,如果版本較低,則使用對(duì)應(yīng)低版本的create-react-app,最后調(diào)用 run 方法
這些工具方法,其實(shí)在寫(xiě)我們自己的構(gòu)建工具的時(shí)候,也可以直接 copy 的哈,所以這里我們也是簡(jiǎn)單看下里面的實(shí)現(xiàn),
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 的長(zhǎng)度必須大于 0") } if (name.match(/^./)) { errors.push("name 不能以點(diǎn)開(kāi)頭") } if (name.match(/^_/)) { errors.push("name 不能以下劃線開(kāi)頭") } if (name.trim() !== name) { errors.push("name 不能包含前空格和尾空格") } // No funny business // var blacklist = [ // "node_modules", // "favicon.ico" // ] blacklist.forEach(function (blacklistedName) { if (name.toLowerCase() === blacklistedName) { //不能是“黑名單”內(nèi)的 errors.push(blacklistedName + " is a blacklisted name") } }) // Generate warnings for stuff that used to be allowed // 為以前允許的內(nèi)容生成警告 // 后面的就不再贅述了 return done(warnings, errors) }
最終,checkAPPName返回的東西如截圖所示,后面寫(xiě)代碼可以直接拿來(lái)借鑒!借鑒~
isSafeToCreateProjectIn所謂安全性校驗(yàn),其實(shí)就是檢查當(dāng)前目錄下是否存在已有文件。
checkNpmVersion后面的代碼也都比較簡(jiǎn)單,這里就不展開(kāi)說(shuō)了,版本比較實(shí)用的是一個(gè)semver package.
run代碼跑到這里,該檢查的都檢查了,雞也不叫了、狗也不咬了,該干點(diǎn)正事了~
run 主要做的事情就是安裝依賴、拷貝模板。
getInstallPackage做的事情非常簡(jiǎn)單,根據(jù)傳入的 version 和原始路徑 originalDirectory 去獲取要安裝的 package 列表,默認(rèn)情況下version 為 undefined,獲取到的 packageToInstall 為react-scripts,也就是我們?nèi)缟蠄D的 resolve 回調(diào)。
最終,我們拿到需要安裝的 info 為
{ isOnline:true, packageName:"react-scripts" }
當(dāng)我們梳理好需要安裝的 package 后,就交給 npm 或者 yarn 去安裝我們的依賴即可
在spawn執(zhí)行完命令后會(huì)有一個(gè)回調(diào),判斷code是否為 0,然后 resolve Promise,
.then(async packageName => { // 安裝完 react, react-dom, react-scripts 之后檢查當(dāng)前環(huán)境運(yùn)行的node版本是否符合要求 checkNodeVersion(packageName); // 檢查 package.json 中的版本號(hào) 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之前的版本中,這里是通過(guò)調(diào)用react-script下的 init方法來(lái)執(zhí)行后續(xù)動(dòng)作的。這里通過(guò)調(diào)用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 方法主要是通過(guò) spawn 來(lái)通過(guò) node命令執(zhí)行react-script下的 init 方法。所以截止當(dāng)前,create-react-app完成了他的工作: npm i ,
react-script/init.js修改 vscode 的 debugger 配置,然后我們來(lái) debugger react-script 下的 init 方法
function init(appPath, appName, verbose, originalDirectory, template) { // 獲取當(dāng)前包中包含 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")); //項(xiàng)目目錄下的 package.json const useYarn = fs.existsSync(path.join(appPath, "yarn.lock")); //通過(guò)判斷目錄下是否有 yarn.lock 來(lái)判斷是否使用 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 設(shè)置 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共享使用的配置項(xiàng) (感謝網(wǎng)友指正) appPackage.browserslist = defaultBrowsers; // 寫(xiě)入我們需要?jiǎng)?chuàng)建的目錄下的 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)) { // 這一步就過(guò)分了, 直接 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" // 安裝其他模板依賴項(xiàng)(如果存在) 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向后兼容 // 沒(méi)有安裝react和react-dom以及react-scripts // 或模板是presetend(通過(guò)--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."); } // 顯示最優(yōu)雅的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"); }
初始化方法主要做的事情就是修改目標(biāo)路徑下的 package.json,添加一些配置命令,然后 copy!react-script 下的模板到目標(biāo)路徑下。
走到這一步,我們的項(xiàng)目基本已經(jīng)初始化完成了。
所以我們 copy 了這么多 scripts
start: "react-scripts start", build: "react-scripts build", test: "react-scripts test", eject: "react-scripts eject",
究竟是如何工作的呢,其實(shí)也不難,就是一些開(kāi)發(fā)、測(cè)試、生產(chǎn)的環(huán)境配置。鑒于篇幅,咱就下一篇來(lái)分享下大佬們的前端構(gòu)建的代碼寫(xiě)法吧~~
總結(jié)本來(lái)想用一張流程圖解釋下,但是。。。create-react-app 著實(shí)沒(méi)有做啥!咱還是等下一篇分析完,自己寫(xiě)構(gòu)建腳本的時(shí)候再畫(huà)一下整體流程圖(架構(gòu)圖)吧~
ok~ 簡(jiǎn)單概述下:
判斷 node 版本,如果大版本小于 8 ,則直接退出(截止目前是 8)
createReactApp.js 初始化一些命令參數(shù),然后再去判斷是否傳入了 packageName,否則直接退出
各種版本的判斷,然后通過(guò)cross-spawn來(lái)用命令行執(zhí)行所有的安裝
當(dāng)所有的依賴安裝完后,依舊通過(guò)命令行,初始化 node 環(huán)境,來(lái)執(zhí)行 react-script 下的初始化方法:修改 package.json 中的一些配置、以及 copy 模板文件
處理完成,給出用戶友好提示
通篇看完 package 的職能后,發(fā)現(xiàn),哇,這有點(diǎn)簡(jiǎn)答啊~~其實(shí),我們學(xué)習(xí)源碼的其實(shí)就是為了學(xué)習(xí)大佬們的一些邊界情況處理,在后面自己開(kāi)發(fā)的時(shí)候再去 copy~ 借鑒一些判斷方法的編寫(xiě)。后面會(huì)再簡(jiǎn)單分析下react-scripts,然后寫(xiě)一個(gè)自己的一些項(xiàng)目架構(gòu)腳本~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/104825.html
摘要:這個(gè)選項(xiàng)看意思就知道了,默認(rèn)使用來(lái)安裝,運(yùn)行,如果你沒(méi)有使用,你可能就需要這個(gè)配置了,指定使用。 2018-06-13 更新。昨天突然好奇在Google上搜了一波關(guān)于create-react-app 源碼的關(guān)鍵詞,發(fā)現(xiàn)掘金出現(xiàn)好幾篇仿文,就連我開(kāi)頭前沿瞎幾把啰嗦的話都抄,我還能說(shuō)什么是吧?以后博客還是首發(fā)在Github上,地址戳這里戳這里??!轉(zhuǎn)載求你們注明出處、改編求你們貼一下參考鏈...
摘要:所以再看文件這里不貼圖占地方有興趣自己看。常見(jiàn)的用法就是和中的搭配發(fā)布自己的包通讀下來(lái)就是一個(gè)幫助搭建項(xiàng)目文件夾,寫(xiě)一些語(yǔ)句的作用。 版本說(shuō)明:2.0.4 首先看文檔中怎樣使用:showImg(https://segmentfault.com/img/bVbiI27?w=1422&h=616); 這里的兩條命令是等價(jià)的,以npm init ...為例來(lái)看命令是怎么用的 showImg(...
摘要:使用快速構(gòu)建開(kāi)發(fā)環(huán)境第一步安裝全局包是來(lái)自于,通過(guò)該命令我們無(wú)需配置就能快速構(gòu)建開(kāi)發(fā)環(huán)境。執(zhí)行以下命令創(chuàng)建項(xiàng)目項(xiàng)目目錄在瀏覽器中打開(kāi),即可顯示上一篇開(kāi)發(fā)教程初識(shí)下一篇開(kāi)發(fā)教程三組件的構(gòu)建 react安裝 React可以直接下載使用,下載包中也提供了很多學(xué)習(xí)的實(shí)例。本教程使用了 React 的版本為 15.4.2,你可以在官網(wǎng) http://facebook.github.io/reac...
摘要:使用官方的的另外一種版本和一起使用自動(dòng)配置了一個(gè)項(xiàng)目支持。需要的依賴都在文件中。帶靜態(tài)類型檢驗(yàn),現(xiàn)在的第三方包基本上源碼都是,方便查看調(diào)試。大型項(xiàng)目首選和結(jié)合,代碼調(diào)試維護(hù)起來(lái)極其方便。 showImg(https://segmentfault.com/img/bVbrTKz?w=1400&h=930); 阿特伍德定律,指的是any application that can be wr...
閱讀 3344·2021-11-22 14:44
閱讀 2554·2019-08-30 14:10
閱讀 2615·2019-08-30 13:12
閱讀 1227·2019-08-29 18:36
閱讀 1356·2019-08-29 16:16
閱讀 3342·2019-08-26 10:33
閱讀 1776·2019-08-23 18:16
閱讀 392·2019-08-23 18:12