create-react-app原始碼解讀

zhanyuzhang發表於2018-07-24

最近把vue-cli@2.xcreate-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新特性,這需要配置babel吧?另外還需要對程式碼風格進行檢查,需要配置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

在實現程式碼之前我們先梳理一上思路:

  1. 通過commander獲取專案名稱;
  2. 如果專案名稱為空(實際上還要對包名進行有效性檢查的,這裡暫且忽略),則退出程式,並提示使用者專案名稱不能為空,否則進行步驟3;
  3. 在當前目錄下建立一個子目錄,目錄名稱就是使用者輸入的專案名,並在裡面初始化一個package.json檔案;
  4. 進入專案的根目錄,安裝react, react-domreact-scripts三個依賴;
  5. 依賴安裝完成後,呼叫react-scriptsinit方法初始化專案(主要是複製模板);
  6. 結束;

按照上面的思路,開始編碼吧!

先引入一些必要的依賴,對於這些依賴有什麼作用這裡就不展開了。 以及定義一個用來存放專案名稱的變數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);讓程式的工作目錄進入到新專案裡面。然後開始安裝依賴,等數分鐘之後,安裝依賴完成後,開始呼叫react-scripts (這是create-react-app的一個子模組,它包含了為你的專案載入其它外掛、解析最終的 webpack 配置這裡也不展開了, 有興趣的可以點選這裡 )的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一下哈~

相關文章