原本想搭建一個webpack腳手架的,因而在搭建的過程當中不斷地蒐集相關資料。可最終的結果是,webpack腳手架沒有搭建成,卻寫出個 CLI 小工具。其實,這也並非沒有緣由的。如今流行的框架都推出了本身的腳手架工具,好比,Vue CLI,Create React App 等。腳手架和CLI每每如影隨形,這也致使了二者在概念上的混淆。標題爲何這麼拗口,實際上是爲了區分這兩個概念。css
既然被帶跑偏了,就只能在跑偏的路上越跑越遠吧。前端
命令行界面(英語:Command-Line Interface,縮寫:CLI)是在圖形用戶界面獲得普及以前使用最爲普遍的用戶界面,它一般不支持鼠標,用戶經過鍵盤輸入指令,計算機接收到指令後,予以執行。也有人稱之爲字符用戶界面(character user interface, CUI) ———— 維基百科vue
使用過 Vue CLI 的同窗應該都知道,咱們只須要在終端敲幾個的命令就能夠搭建一個 Vue 的腳手架。若是不使用 CLI 的話,每次建立項目時,都須要配置文件(好比webpack配置文件)、設計結構、技術棧選型等。若是每次從零開始去搭建項目就會很麻煩,因此咱們能夠把相同的東西抽離成腳手架。之後建立項目時,就能夠直接把腳手架複製過來,並以此爲基礎搭建項目。node
回過頭來再看看咱們手動搭建項目的過程,從每次從零開始搭建項目到腳手架的複用,這中間有了很大的進步。可即便是複製黏貼,咱們依然以爲很麻煩,若是用命令行的方式來取代圖形操做,咱們就能夠更懶一些了。react
回到主題,我原本打算寫的webpack腳手架是基於這樣的一個想法。➡️ 如今大部分的前端工程,webpack做爲打包工具已經成了標配了。而 webpack 的配置是大同小異的,徹底能夠剝離出一個通用的webpack配置,而後針對個別配置進行修改。本次但願最終實現一個基於webpack適用於不一樣前端模板(React、Vue、ES+)的腳手架。webpack
如今腳手架有了,如何自動化去搭建一個項目呢?git
複製或下載腳手架模板。(爲了更靈活,上傳到GitHub,或發佈npm中)。github
根據不一樣需求,在腳手架模板基礎上從新配置webpack、package文件。web
安裝依賴。shell
如下代碼可見GitHub。
首先了解一下 #!
。文件開頭要加上#! /usr/bin/env node
。
在計算領域中,Shebang(也稱爲 Hashbang )是一個由井號和歎號構成的字符序列 #! ,其出如今文本文件的第一行的前兩個字符。 在文件中存在 Shebang 的狀況下,類 Unix 操做系統的程序加載器會分析 Shebang 後的內容,將這些內容做爲解釋器指令,並調用該指令,並將載有 Shebang 的文件路徑做爲該解釋器的參數。 ———— 維基百科
使用 #!/usr/bin/env 腳本解釋器名稱 是一種常見的在不一樣平臺上都能正確找到解釋器的辦法。 ———— 維基百科
而後看看都用到了哪些東西(部分)。
npm install commander chalk fs-extra shelljs inquirer ora ejs --save
複製代碼
#! /usr/bin/env node
// multi-spa.js
const program = require('commander'); // 解析命令;
const chalk = require('chalk'); // 命令行界面輸出美顏
const fs = require('fs-extra'); // fs的拓展;
const shell = require('shelljs'); // 從新包裝了 child_process;
const inquirer = require('inquirer'); // 交互式問答;
const ora = require('ora'); // 輸出樣式美化;
const ejs = require('ejs'); // 模版引擎;
const path = require('path');
const currentPath = process.cwd();
let answersConfig = null;
複製代碼
相似與 Vue 的 vue init
,咱們也但願本身的 CLI 也能擁有相似的功能。
// package.json
"bin": {
"multi-spa-webpack": "./bin/multi-spa.js"
},
複製代碼
這樣,咱們就有了multi-spa-webpack
的命令。若是咱們想要全局使用,還須要執行下面命令。
npm link
複製代碼
接下來就要初始化multi-spa-webpack
相關的命令了。
// multi-spa.js
program
.command('init <項目路徑> [選項]')
.description('指令說明:初始化項目')
.action(async (appName) => {
try {
answersConfig = await getAnswers(appName);
let targetDir = path.resolve(currentPath, appName || '.');
if (fs.pathExistsSync(targetDir)) {
if (program.force) {
GenarateProject(appName); // 建立項目;
}
ora(chalk.red(`!當前目錄下,${appName}已存在,請修更名稱後重試`)).fail();
process.exit(1);
};
GenarateProject(appName); // 建立項目;
} catch (error) {
ora(chalk.red(`項目建立失敗:${error}`)).fail();
process.exit(1);
}
});
program
.arguments('<command>')
.action((cmd) => {
console.log();
console.log(chalk.red(`!命令未能解析 <${chalk.green(cmd)}>`));
console.log();
program.outputHelp();
console.log();
});
program.parse(process.argv);
if (program.args.length === 0) {
console.log();
console.log(chalk.red('!輸入的命令有誤'));
console.log();
chalk.cyan(program.help());
}
複製代碼
在執行multi-spa-webpack init spa-project
後,就須要拷貝一份腳手架到本地了。至於腳手架從哪裏來,能夠放在 github 上(相似 Vue CLI)或 放在 CLI 對應的目錄下(相似create-react-app)。
本文是採用的是從 github 獲取腳手架模板的。可是常規的方式,只能下載整個項目,而對於不須要的文件夾或文件,也會同時下載,下載後,只能在本地中刪除無關文件了。我這裏是從源頭上剔除無關文件的下載,這個方法可能會有一些侷限性吧(sparse-checkout)。不過二者最終的目的是同樣的。
// multi-spa.js
function DownTemplate(projectDir) {
const remote = 'https://github.com/yexiaochen/multi-spa-webpack-cli.git';
const { template } = answersConfig;
let downTemplateSpinner = ora(chalk.cyan('模板下載中...')).start();
return new Promise((resolve, reject) => {
shell.exec(` mkdir ${projectDir} cd ${projectDir} git init git remote add -f origin ${remote} git config core.sparsecheckout true echo "template/common" >> .git/info/sparse-checkout echo "template/config" >> .git/info/sparse-checkout echo "template/services" >> .git/info/sparse-checkout echo "template/${template}" >> .git/info/sparse-checkout echo ".gitignore" >> .git/info/sparse-checkout echo "package.json" >> .git/info/sparse-checkout git pull origin master rm -rf .git mv template/* ./ rm -rf template `, (error) => {
if (error) {
downTemplateSpinner.stop()
ora(chalk.red(`模板下載失敗:${error}`)).fail()
reject()
}
downTemplateSpinner.stop();
ora(chalk.cyan('模板下載成功')).succeed();
resolve();
})
})
}
複製代碼
像 webpack、package 等配置文件,也都是包含在腳手架裏的,不過這些配置還不能直接拿來用。咱們還須要經過交互式問答,來針對性得在現有的基礎上從新生成配置文件。
// multi-spa.js
function getAnswers(appName) {
const options = [
{
type: 'input',
name: 'name',
message: '項目名稱',
default: appName,
},
{
type: 'input',
name: 'description',
message: '項目描述',
default: '單頁面應用',
},
{
type: 'confirm',
name: 'eslint',
message: '是否啓用 eslint+pretty',
default: true
},
{
name: 'cssPreprocessor',
type: 'list',
message: 'CSS 預處理器',
choices: [
"less",
"sass",
"none",
]
},
{
name: 'template',
type: 'list',
message: '選取模板',
choices: [
"react",
"vue",
"es"
]
},
];
return inquirer.prompt(options);
}
複製代碼
在得到特定的需求後,還要把這些數據注入到配置文件中。就是經過模板引擎把數據塞到模板裏。這裏使用的是 ejs 模版引擎。
<!--webpack.common.ejs-->
<%= answers.cssPreprocessor == 'none' ? /\.css$/ : (answers.cssPreprocessor == 'less' ? /\.less$/ : /\.scss$/) %>
<%= answers.cssPreprocessor == 'none' ? '' : (answers.cssPreprocessor == 'less' ? 'less-loader' : 'sass-loader') %>
複製代碼
// multi-spa.js
async function GenarateWebpackConfig(targetDir) {
try {
const webpackConfigPath = path.resolve(`${currentPath}/${targetDir}/config`, 'webpack.common.ejs');
const webpackConfigTargetPath = path.resolve(`${currentPath}/${targetDir}/config`, 'webpack.common.js');
const webpackConfigSpinner = ora(chalk.cyan(`配置 webpack 文件...`)).start();
let webpackConfig = await fs.readFile(webpackConfigPath, 'utf8');
let generatedWebpackConfig = ejs.render(webpackConfig, { answers: answersConfig });
await Promise.all([
fs.writeFile(webpackConfigTargetPath, generatedWebpackConfig),
fs.remove(webpackConfigPath)
])
webpackConfigSpinner.stop();
ora(chalk.cyan(`配置 webpack 完成`)).succeed();
} catch (error) {
ora(chalk.red(`配置文件失敗:${error}`)).fail();
process.exit(1);
}
}
async function GenaratePackageJson(projectDir) {
try {
const { name, description, cssPreprocessor } = answersConfig;
const packageJsonPath = path.resolve(`${currentPath}/${projectDir}`, 'package.json');
const packageJsonSpinner = ora(chalk.cyan('配置 package.json 文件...')).start();
let package = await fs.readJson(packageJsonPath);
package.name = name;
package.description = description;
if (cssPreprocessor == 'less') {
package.devDependencies = {
...package.devDependencies,
"less-loader": "^5.0.0"
}
}
if (cssPreprocessor == 'sass') {
package.devDependencies = {
...package.devDependencies,
"node-sass": "^4.12.0",
"sass-loader": "^7.1.0"
}
}
await fs.writeJson(packageJsonPath, package, { spaces: '\t' });
packageJsonSpinner.stop();
ora(chalk.cyan('package.json 配置完成')).succeed();
} catch (error) {
if (error) {
ora(chalk.red(`配置文件失敗:${error}`)).fail();
process.exit(1);
};
}
}
複製代碼
其實配置文件生成後,CLI 就快接近尾聲了。剩下就是安裝依賴。
// multi-spa.js
function InstallDependencies(targetDir) {
const installDependenciesSpinner = ora(chalk.cyan(`安裝依賴中...`)).start();
return new Promise((resolve, reject) => {
shell.exec(` cd ${targetDir} npm i `, (error) => {
if (error) {
installDependenciesSpinner.stop()
ora(chalk.red(`依賴安裝失敗:${error}`)).fail()
reject()
}
installDependenciesSpinner.stop();
ora(chalk.cyan('依賴安裝完成')).succeed();
resolve();
})
})
}
複製代碼
一個粗糙的CLI,就這麼完成了。把以上幾個方法包裝一下,就是本次 CLI 的所有內容了。
async function GenarateProject(targetDir) {
await DownTemplate(targetDir);
await Promise.all([GenaratePackageJson(targetDir).then(() => {
return InstallDependencies(targetDir);
}),
GenarateWebpackConfig(targetDir)
]);
ora(chalk.cyan('項目建立成功!')).succeed();
}
複製代碼
若是想要發佈,須要登錄npm ,npm publish
。