手寫webpack腳手架命令行工具

平常吐槽

原本想搭建一個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

  1. 複製或下載腳手架模板。(爲了更靈活,上傳到GitHub,或發佈npm中)。github

  2. 根據不一樣需求,在腳手架模板基礎上從新配置webpack、package文件。web

  3. 安裝依賴。shell

如下代碼可見GitHub

CLI 中的預備工做

首先了解一下 #!。文件開頭要加上#! /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 的所有內容了。

  1. 拷貝腳手架。2. 從新生成配置文件。3安裝依賴。
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

相關文章
相關標籤/搜索