一款破產版腳手架的誕生

Russ_Zhong發表於2018-05-21

前些天一直在學習入門Webpack,後來嘗試了自己搭建一下一個簡單的React開發環境,後來就在想可不可以自己寫一個簡單的腳手架,以免每次搭建一個簡單的開發環境都需要自己一個個的配置,這樣很麻煩,使用create-react-app的話,配置一大堆可能不會用到的功能,比較冗餘,所以自己寫一個超級簡化的腳手架,只處理ES6程式碼、JSX語法和css模組,這樣就滿足了基本的使用。

後來在開發的過程中又遇到了新的麻煩,比如使用Node的child_process.spawn方法呼叫npm命令時,會出現錯誤,因為在Windows環境下,實際上要呼叫npm.cmd,而非npm,在這裡出現了問題,還有一些其他問題,後來正好看到了@Jsonz大神寫的兩篇文章:探索 create-react-app 原始碼create-react-app 原始碼解析之react-scripts,於是也照著學習了一下create-react-app腳手架的原始碼,基本解決了一些問題,最終寫出來了一個簡(can)單(fei)的React腳手架,當然還有許許多多的不足,但是這個學習的過程值得我記錄下來。

這篇文章記錄了以下知識:

  • 如何使用Node開發一個簡單的腳手架。
  • 如何釋出你的npm模組並定製命令。

一、開發React腳手架

create-react-app是一個很成功的、功能完善的腳手架,考慮到了許多方面,比如使用npm或者yarn,比如npmNode版本、日誌的記錄和列印等等諸多方面,開發環境搭建的也十分完善,除了基本的React開發之外,還考慮了圖片、postcss、sass、graphQL等等模組的處理。由於能力有限,本文開發的腳手架只涵蓋了基本模組的處理,不包含圖片、sass……等等。

腳手架的作用主要是建立一個React開發的標準目錄、並且配置好webpack打包工具,使得開發過程中可以直接在標準的目錄上修改,然後通過配置好的命令啟動本地伺服器或者打包app。所以腳手架中應該包括一個模板資料夾,裡面放入應該拷貝到使用者工程資料夾的所有檔案或目錄。在使用腳手架時,先把模板資料夾中的內容拷貝到使用者工程資料夾下,然後修改package.json配置檔案,最後安裝所有模組。這就是我開發的腳手架所完成的基本工作。

腳手架工程目錄結構如下:

ROOT
│  .gitignore
│  .npmignore
│  LICENSE
│  package-lock.json
│  package.json
│  README.md
│
├─dist
├─package
│      create-react.js
│
└─templates
    │  .babelrc
    │  .gitignore
    │  README.md
    │  webpack.base.conf.js
    │  webpack.dev.conf.js
    │  webpack.prod.conf.js
    │
    ├─dist
    └─src
        │  index.css
        │  index.html
        │  index.js
        │
        └─components
                App.js
複製程式碼

根據我的前一篇文章,搭建React開發環境,最小化的標準目錄結構應該如下:

ROOT
│  .babelrc
│  .gitignore
│  README.md
│  webpack.base.conf.js
│  webpack.dev.conf.js
│  webpack.prod.conf.js
│
├─dist
└─src
    │  index.css
    │  index.html
    │  index.js
    │
    └─components
            App.js
複製程式碼

所以在腳手架根目錄下的templates資料夾中應該包含以上檔案,檔案內的內容可以自由定製。

同樣根據上一篇文章,需要安裝的模組主要有:

'webpack', 
'webpack-cli', 
'html-webpack-plugin', 
'clean-webpack-plugin', 
'webpack-dev-server', 
'css-loader', 
'webpack-merge', 
'style-loader', 
'babel-preset-env', 
'babel-loader', 
'babel-polyfill', 
'babel-preset-react'
複製程式碼

'react',
'react-dom'
複製程式碼

第一部分只需要安裝在開發環境(npm i -D ...),第二部分生產環境也要安裝(npm i --save ...)。

那麼接下來可以通過Node實現腳手架的開發了。

首先介紹一些有用的並且會用到的模組:

  • cross-spawn:解決跨平臺使用npm命令的問題的模組。
  • chalk:實現控制檯彩色文字輸出的模組。
  • fs-extra:實現了一些fs模組不包含的檔案操(比如遞迴複製、刪除等等)的模組。
  • commander: 實現命令列傳入引數預處理的模組。
  • validate-npm-package-name:對於使用者輸入的工程名的可用性進行驗證的模組。

首先,在程式碼中引入這些基本的模組:

const spawn = require('cross-spawn');
const chalk = require('chalk');
const os = require('os');
const fs = require('fs-extra');
const path = require('path');
const commander = require('commander');
const validateProjectName = require('validate-npm-package-name');
const packageJson = require('../package.json');
複製程式碼

然後定義我們的模板複製函式:

function copyTemplates() {
    try {
        if(!fs.existsSync(path.resolve(__dirname, '../templates'))) {
            console.log(chalk.red('Cannot find the template files !'));
            process.exit(1);
        }
        fs.copySync(path.resolve(__dirname, '../templates'), process.cwd());
        console.log(chalk.green('Template files copied successfully!'));
        return true;
    }
    catch(e) {
        console.log(chalk.red(`Error occured: ${e}`))
    }
}
複製程式碼

fs模組首先檢測模板檔案是否存在(防止被使用者刪除),如果存在則通過fs的同步拷貝方法(copySync)拷貝到腳手架的當前工作目錄(即process.cwd()),如果不存在則彈出錯誤資訊,隨後使用退出碼1退出程式。

隨後定義package.json的處理函式;

function generatePackageJson() {
    let packageJson = {
        name: projectName,
        version: '1.0.0',
        description: '',
        scripts: {
            start: 'webpack-dev-server --open --config webpack.dev.conf.js',
            build: 'webpack --config webpack.prod.conf.js'
        },
        author: '',
        license: ''
    };
    try {
        fs.writeFileSync(path.resolve(process.cwd(), 'package.json'), JSON.stringify(packageJson));
        console.log(chalk.green('Package.json generated successfully!'));
    }
    catch(e) {
        console.log(chalk.red(e))
    }
}
複製程式碼

可以看出先是定義了一個JavaScript Object,然後修改屬性之後通過fs模組將其JSON字串寫入到了package.json檔案中,實現了package.json的生成。

最後安裝所有的依賴,分為devDependencies和dependencies:

function installAll() {
    console.log(chalk.green('Start installing ...'));
    let devDependencies = ['webpack', 'webpack-cli', 'html-webpack-plugin', 'clean-webpack-plugin', 'webpack-dev-server', 'css-loader', 'webpack-merge', 'style-loader', 'babel-preset-env', 'babel-loader', 'babel-polyfill', 'babel-preset-react'];
    let dependencies = ['react', 'react-dom'];
    const child = spawn('cnpm', ['install', '-D'].concat(devDependencies), { 
        stdio: 'inherit' 
    });
    
    child.on('close', function(code) {
        if(code !== 0) {
            console.log(chalk.red('Error occured while installing dependencies!'));
            process.exit(1);
        }
        else {
            const child = spawn('cnpm', ['install', '--save'].concat(dependencies), {
                stdio: 'inherit'
            })
            child.on('close', function(code) {
                if(code !== 0) {
                    console.log(chalk.red('Error occured while installing dependencies!'));
                    process.exit(1);
                }
                else {
                    console.log(chalk.green('Installation completed successfully!'));
                    console.log();
                    console.log(chalk.green('Start the local server with : '))
                    console.log();
                    console.log(chalk.cyan('    npm run start'))
                    console.log();
                    console.log(chalk.green('or build your app via :'));
                    console.log();
                    console.log(chalk.cyan('    npm run build'));
                }
            })
        }
    });
}
複製程式碼

函式中,通過cross-spawn執行了cnpm的安裝命令,值得注意的是其配置項:

{ 
    stdio: 'inherit' 
}
複製程式碼

代表將子程式的輸出管道連線到父程式上,及父程式可以自動接受子程式的輸出結果,詳情見options.stdio

通過commander模組實現命令列引數的預處理;

const program = commander
.version(packageJson.version)
.usage(' [options]')
.arguments('<project-name>')
.action(name => {
    projectName = name;
})
.allowUnknownOption()
.parse(process.argv);
複製程式碼

其中,version方法定義了create-react-application -V的輸出結果,usage定義了命令列裡的用法,arguments定義了程式所接受的預設引數,然後在action函式回撥中處理了這個預設引數,allowUnknownOption表示接受多餘引數,parse表示把多餘未解析的引數解析到process.argv中去。

最後是呼叫三個方法實現React開發環境的搭建:

if(projectName == undefined) {
    console.log(chalk.red('Please pass the project name while using create-react!'));
    console.log(chalk.green('for example:'))
    console.log();
    console.log('   create-react-application ' + chalk.yellow('<react-app>'));
}
else {
    const validateResult = validateProjectName(projectName);
    if(validateResult.validForNewPackages) {
        copyTemplates();
        generatePackageJson();
        installAll();
        //console.log(chalk.green(`Congratulations! React app has been created successfully in ${process.cwd()}`));
    }
    else {
        console.log(chalk.red('The project name given is invalid!'));
        process.exit(1);
    }
}
複製程式碼

如果接受的工程名為空,那麼彈出警告。如果不為空,就驗證工程名的可用性,如果不可用,就彈出警告並且退出程式,否則呼叫之前定義的三個主要函式,完成環境的搭建。

截止到此,使用該程式的方式仍然是node xxx.js --parameters的方式,我們需要自定義一個命令,並且最好將程式上傳到npm,便於使用。

二、定義你的命令並且釋出npm包

實現自定義命令併發布npm模組只需要以下幾步:

  • 修改入口檔案,頭部新增以下兩句:

      #!/usr/bin/env node
      'use strict'
    複製程式碼

    第二行也一定不能少!

  • 修改package.json,新增bin屬性:

      // package.json
      {
          "bin": {
              "create-react-application": "package/create-react.js"
          }
      }
    複製程式碼
  • 執行以下命令:

      npm link
    複製程式碼
  • 註冊npm賬戶(如已經註冊則可以忽略)。

  • 執行以下命令:

      npm adduser
    複製程式碼

    並輸入賬戶密碼。

  • 執行以下命令:

      npm publish
    複製程式碼

    接下來就可以收到釋出成功的郵件啦!

如果要更新你的npm模組,執行以下步驟:

  • 使用一下命令更新你的版本號:

      npm version x.x.x
    複製程式碼
  • 再使用以下命令釋出;

      npm publish
    複製程式碼

執行完以上步驟之後,就可以在npm下載你的模組啦!

三、FQA

(1)關於#!/usr/bin/env node

這是Unix系作業系統中的一種寫法,名字叫做Shebang或者Hashbang等等。在Wikipedia的解釋中,把這一行程式碼寫在指令碼中,使得作業系統把指令碼當做可執行檔案執行時,會找到對應的程式執行(比如此文中的node),而這段程式碼本身會被直譯器所忽略。

(2)關於npm link

在npm官方文件的解釋中,npm link的執行,是一個兩步的過程。當你在你的包中使用npm link時,會將全域性資料夾:{prefix}/lib/node_modules/<package>連結到執行npm link的資料夾,同樣也會將執行npm link命令的包中的所有可執行檔案連結到全域性資料夾{prefix}/bin/{name}中。

此外,npm link project-name會將全域性安裝的project-name模組連結到執行npm link命令的當前資料夾的node_modules中。

根據npm官方文件,prefix的值可為:

  • /usr/local (大部分系統中)
  • %AppData%\npm (Windows中)

具體參考:prefix configurationnpm link

四、簡單嘗試

本文所開發的腳手架已經上傳到了npm,可以通過以下步驟檢視實際效果:

  • 安裝create-react-application

      npm i -D create-react-application
    複製程式碼

    或者

      npm i -g create-react-application
    複製程式碼
  • 使用create-react-application

      create-react-application <project-name>
    複製程式碼

原始碼已經上傳到了GitHub,歡迎大家一起哈啤(#手動滑稽)。

此外文中還有許多不足,比如關於npm link的解釋我也還不是很清楚,歡迎大家補充指教!

五、參考文章

相關文章