最近把 vue-cli@2.x 和 create-react-app 的源碼都看了一遍。由於現在官方推薦使用 vue-cli@3.0 ,改動比較大,所以就不寫關於 vue-cli 的了(據說是因為 vue-cli@2.x 創建項目時操作有點太復雜了,於是猶雨溪大大就借鑒了 create-react-app 的思想,搞出了個零配置的 vue-cli@3.0 ,有興趣的小伙伴可以去自己看一下哈)。這篇隨筆只講解 create-react-app 的實現,但是,因為 create-react-app源碼 加上注釋總共800多行代碼,這里也不打算對它的源碼進行逐一解讀了,如果想要對全部源碼解讀的,可以先把這篇文章看完、再去看源碼,應該會容易明白很多。
前面說了這么多廢話,現在該進入正題了。那 create-react-app 到底是什么東東叱?這里還是引用官方 readme 文件的第一句話解釋:
Create React apps with no build configuration.
嗯,這解釋得很清楚了:creact-react-app
可以讓你零配置創建一個React
應用!為什么要強調零配置呢?因為我們都知道,React
是分模塊的組件化的框架,這需要配置webpack
打包吧?還有使用了JSX
和高大上的ES6新特性
,這需要配置bable
了啊?另外還需要對代碼做靜態檢查,需要配置eslint
吧?對於一個新手來說,能夠成功的配置一個能運行React
的環境,真的很有可能需要一兩天時間的。所以零配置的意義就在於讓小萌新在不懂配置的情況下,也能迅速的編寫自己的第一個 react-hello-world
,這是很有成就感的!
如果之前沒有使用過create-react-app
也沒有關系 ,這里是它最簡單的用法:
create-react-app my-app
等待數分鍾,就會在當前目錄下創建一個my-app
的項目,然后進入這個根目錄npm start
就可以啟動一個React
項目了。記得要先全局安裝好create-react-app
。
介紹了create-react-app
是什么,以及他的最簡單的用法。現在我們就一起動手實現一個create-react-app
山寨版吧。因為我們實現的是一個簡化版的,去除了環境檢查、版本檢測、離線包安裝等功能,代碼就剩下100行左右,暫且就叫做simple-create-react-app
。
在實現代碼之前我們先梳理一上思路:
- 通過
commander
獲取項目名稱; - 如果項目名稱為空(實際上還要對包名進行有效性檢查的,這里暫且忽略),則退出進程,並提示用戶項目名稱不能為空,否則進行步驟3;
- 在當前目錄下創建一個子目錄,目錄名稱就是用戶輸入的項目名,並在里面初始化一個
package.json
文件; - 進入項目的根目錄,安裝
react
,react-dom
和react-scripts
三個依賴; - 依賴安裝完成后,調用
react-scripts
的init
方法初始化項目; - 結束;
按照上面的思路,開始編碼吧!
先引入一些必要的依賴,對於這些依賴有什么作用這里就不展開了。 以及定義一個用來存放項目名稱的變量projectName
:
const commander = require('commander');
const chalk = require('chalk');
const spawn = require('cross-spawn');
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const packageJson = require('./package.json');
let projectName; // 項目名稱,通過命令行參數獲取
接下來,就創建一個Commander
的實例,獲取用戶輸入的項目名稱, 並判斷是否為空。如果是空,則提示用戶,並退出進程。
const program = new commander.Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action(name => {
projectName = name;
})
.parse(process.argv) // 格式化參數,必須要的
// 如果沒有輸入項目名稱,則給出提示,並退出進程
if(typeof projectName === 'undefined') {
console.error('please specify the project directory');
console.log();
console.log('For examaple: ')
console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`)
console.log();
process.exit(1);
}
如果項目名稱不為空,則開始創建一個空的項目,並且初始化一個packgae.json
文件:
// 開始創建項目
createApp(projectName);
function createApp(name) {
const root = path.resolve(name);
fs.ensureDirSync(root); // 創建項目空目錄
console.log(`Creating a new React app in ${chalk.green(root)}.`);
// 創建新項目的package.json
const packageJson = {
name: name,
version: '0.1.0',
private: true
};
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(packageJson, null, 2) + os.EOL);
// 將當前目錄的路徑存下來。因為下一步我們就要進入到新項目的目錄了
// 后面可能還會用到當前的路徑
const originalDirectory = process.cwd();
// 進入新創建的項目里面
process.chdir(root);
run(root, originalDirectory);
}
創建新項目之后,通過process.chdir(root);
讓進程的工作目錄進入到新項目里面。然后開始安裝依賴,等數分鍾之后,安裝依賴完成后,開始調用create-react-app
的init
方法初始化項目:
function run(root, originalDirectory) {
const allDependencies = ['react', 'react-dom', 'react-scripts'];
console.log('Installing packages. This migth take a couple of minutes...');
console.log(`Installing ${chalk.cyan('react')}, ${chalk.cyan('react-dom')}, and ${chalk.cyan('react-scripts')}...`);
console.log();
install(root, allDependencies)
.then(() => {
console.log();
console.log('Installing is success!');
console.log();
// 執行react-scripts模塊下的init方法進行初始化項目
const scriptsPath = path.resolve(
process.cwd(),
'node_modules',
'react-scripts',
'scripts',
'init.js'
)
const init = require(scriptsPath);
init(root, projectName, null, originalDirectory);
})
.catch(reason => {
console.log();
console.log('Aborting installation.');
if(reason.command) {
console.log(` ${chalk.cyan(reason.command)} has failed.`);
} else {
console.log(chalk.red('Unexpected error!'), reason);
}
})
}
// 在指定目錄下安裝npm依賴
function install(root, dependencies) {
return new Promise((resolve, reject) => {
let command = 'yarnpkg';
const args = ['add'];
[].push.apply(args, dependencies);
let child = spawn(command, args, {stido: 'inherit'});
child.on('close', code => {
if(code !== 0) {
reject({
command: `${command} ${args.join(' ')}`
});
return;
}
resolve();
})
});
}
數了一下,代碼總共100多行,就這么簡單就實現了create-react-app
的核心功能了。當然,實際上,還有環境檢測、版本檢測、離線安裝等,我們這里忽略了的,如果有興趣的,可以自己看一下官方的源碼。
關於create-reate-app
就寫這么多了,源碼可以到我的github進行下載,如果喜歡的歡迎star
一下哈~