一款破產版腳手架的誕生

前些天一直在學習入門Webpack,後來嘗試了本身搭建一下一個簡單的React開發環境,後來就在想可不能夠本身寫一個簡單的腳手架,以避免每次搭建一個簡單的開發環境都須要本身一個個的配置,這樣很麻煩,使用create-react-app的話,配置一大堆可能不會用到的功能,比較冗餘,因此本身寫一個超級簡化的腳手架,只處理ES6代碼、JSX語法和css模塊,這樣就知足了基本的使用。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腳手架,固然還有許許多多的不足,可是這個學習的過程值得我記錄下來。html

這篇文章記錄瞭如下知識:node

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

1、開發React腳手架

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

腳手架的做用主要是創建一個React開發的標準目錄、而且配置好webpack打包工具,使得開發過程當中能夠直接在標準的目錄上修改,而後經過配置好的命令啓動本地服務器或者打包app。因此腳手架中應該包括一個模板文件夾,裏面放入應該拷貝到用戶工程文件夾的全部文件或目錄。在使用腳手架時,先把模板文件夾中的內容拷貝到用戶工程文件夾下,而後修改package.json配置文件,最後安裝全部模塊。這就是我開發的腳手架所完成的基本工做。webpack

腳手架工程目錄結構以下:git

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開發環境,最小化的標準目錄結構應該以下:github

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文件夾中應該包含以上文件,文件內的內容能夠自由定製。web

一樣根據上一篇文章,須要安裝的模塊主要有:npm

'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'
複製代碼

json

'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,便於使用。

2、定義你的命令而且發佈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下載你的模塊啦!

3、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

4、簡單嘗試

本文所開發的腳手架已經上傳到了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的解釋我也還不是很清楚,歡迎你們補充指教!

5、參考文章

相關文章
相關標籤/搜索