前些天一直在學習入門Webpack,後來嘗試了本身搭建一下一個簡單的React開發環境,後來就在想可不能夠本身寫一個簡單的腳手架,以避免每次搭建一個簡單的開發環境都須要本身一個個的配置,這樣很麻煩,使用create-react-app
的話,配置一大堆可能不會用到的功能,比較冗餘,因此本身寫一個超級簡化的腳手架,只處理ES6代碼、JSX語法和css模塊,這樣就知足了基本的使用。javascript
後來在開發的過程當中又遇到了新的麻煩,好比使用Node的child_process.spawn
方法調用npm命令時,會出現錯誤,由於在Windows環境下,實際上要調用npm.cmd
,而非npm
,在這裏出現了問題,還有一些其餘問題,後來正好看到了@Jsonz大神寫的兩篇文章:探索 create-react-app 源碼和create-react-app 源碼解析之react-scripts,因而也照着學習了一下create-react-app
腳手架的源碼,基本解決了一些問題,最終寫出來了一個簡(can)單(fei)的React腳手架,固然還有許許多多的不足,可是這個學習的過程值得我記錄下來。css
這篇文章記錄瞭如下知識:html
create-react-app
是一個很成功的、功能完善的腳手架,考慮到了許多方面,好比使用npm
或者yarn
,好比npm
和Node
版本、日誌的記錄和打印等等諸多方面,開發環境搭建的也十分完善,除了基本的React開發以外,還考慮了圖片、postcss、sass、graphQL等等模塊的處理。因爲能力有限,本文開發的腳手架只涵蓋了基本模塊的處理,不包含圖片、sass……等等。java
腳手架的做用主要是創建一個React開發的標準目錄、而且配置好webpack打包工具,使得開發過程當中能夠直接在標準的目錄上修改,而後經過配置好的命令啓動本地服務器或者打包app。因此腳手架中應該包括一個模板文件夾,裏面放入應該拷貝到用戶工程文件夾的全部文件或目錄。在使用腳手架時,先把模板文件夾中的內容拷貝到用戶工程文件夾下,而後修改package.json
配置文件,最後安裝全部模塊。這就是我開發的腳手架所完成的基本工做。node
腳手架工程目錄結構以下:react
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開發環境,最小化的標準目錄結構應該以下:webpack
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
文件夾中應該包含以上文件,文件內的內容能夠自由定製。git
一樣根據上一篇文章,須要安裝的模塊主要有:github
'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'
和web
'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模塊只須要如下幾步:
修改入口文件,頭部添加如下兩句:
#!/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下載你的模塊啦!
#!/usr/bin/env node
這是Unix系操做系統中的一種寫法,名字叫作Shebang
或者Hashbang
等等。在Wikipedia的解釋中,把這一行代碼寫在腳本中,使得操做系統把腳本當作可執行文件執行時,會找到對應的程序執行(好比此文中的node),而這段代碼自己會被解釋器所忽略。
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的值可爲:
具體參考:prefix configuration和npm 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
的解釋我也還不是很清楚,歡迎你們補充指教!