前些天一直在學習入門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
create-react-app
是一個很成功的、功能完善的腳手架,考慮到了許多方面,好比使用npm
或者yarn
,好比npm
和Node
版本、日誌的記錄和打印等等諸多方面,開發環境搭建的也十分完善,除了基本的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,便於使用。
實現自定義命令併發布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
的解釋我也還不是很清楚,歡迎你們補充指教!