前些天一直在學習入門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
,比如npm
和Node
版本、日誌的記錄和列印等等諸多方面,開發環境搭建的也十分完善,除了基本的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 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
的解釋我也還不是很清楚,歡迎大家補充指教!