基於webpack4.x專案實戰3 - 手寫一個cli

鹹魚老弟發表於2019-04-28

基於webpack4.x專案實戰1-簡單使用

基於webpack4.x專案實戰2 - 配置一次,多個專案執行

基於webpack4.x專案實戰3 - 手寫一個cli


前言

之前寫過webpack-multi的配置,css前處理器用的是less,今天我們繼續這個系列的文章,來寫一個cli工具。所以本文其實不屬於webpack的範疇,而是教你寫一個屬於自己的cli工具,只是它是基於前面的兩篇文章來寫的,所以歸入這個系列裡。

我們參考一下vue-cli,來學習一下怎麼寫cli工具。

我們先來看看vue-cli的使用方法:

  1. npm install -g vue-cli
  2. vue init webpack test test為你的專案名 然後會出現一些配置的問答
Project name test: 專案名稱,如果不改,就直接原來的test名稱
Project description A Vue.js project: 專案描述,也可直接點選回車,使用預設名字
Author: 作者
Vue build standalone:
Install vue-router? 是否安裝vue-router
Use ESLint to lint your code? 是否使用ESLint管理程式碼
Pick an ESLint preset Standard: 選擇一個ESLint預設,編寫vue專案時的程式碼風格
Set up unit tests Yes: 是否安裝單元測試
Pick a test runner jest
Setup e2e tests with Nightwatch?  是否安裝e2e測試
Should we run npm install for you after the project has been created? (recommended) npm:是否幫你`npm install`
複製程式碼

參考vue-cli,我們的這個腳手架叫webpack-multi-cli,是基於基於webpack4.x專案實戰2 - 配置一次,多個專案執行這個demo來構建的。

完成後,我們的命令也和vue-cli類似

  1. 安裝npm install -g webpack-multi-cli
  2. 使用npm init project-name,然後會出現以下配置選擇
Project name: 專案名稱,如果不改,則為npm init時的專案名
Project description A webpack-multi project: 專案描述,也可直接點選回車,使用預設名字
Author: 作者
Pick a css preprocessor? 選擇一個css前處理器,可選擇是less或者是sass
Should we run npm install for you after the project has been created? (recommended) npm:是否幫你`npm install`,如果輸入npm命令,則幫你執行npm install
複製程式碼

開始構建

在開始構建之前,我們需要安裝一些npm包:

chalk :彩色輸出
figlet :生成字元圖案
inquirer :建立互動式的命令列介面,就是這些問答的介面
inquirer 命令列使用者介面
commander 自定義命令
fs-extra 檔案操作
ora 製作轉圈效果
promise-exec 執行子程式

編寫package.json檔案

看看我們最終的package.json檔案

{
  "name": "webpack-multi-cli",
  "version": "1.0.0",
  "description": "create webpack-multi cli",
  "main": "index.js",
  "bin": {
    "webpack-multi-cli": "bin/index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/xianyulaodi/webpack-multi-cli.git"
  },
  "author": "xianyulaodi",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/xianyulaodi/webpack-multi-cli/issues"
  },
  "dependencies": {
    "chalk": "^2.4.2",
    "commander": "^2.20.0",
    "figlet": "^1.2.1",
    "fs-extra": "^7.0.1",
    "inquirer": "^5.2.0",
    "ora": "^3.4.0",
    "promise-exec": "^0.1.0"
  },
  "homepage": "https://github.com/xianyulaodi/webpack-multi-cli#readme"
}
複製程式碼

我們的入口檔案為bin/index.js,類似於vue-cli init 專案名,我們一開始的命令為:webpack-multi-cli init 專案名,因此package.json的bin需要這樣寫

...
"bin": {
   "webpack-multi-cli": "bin/index.js"
}
..
複製程式碼

編寫init命令

接下來編寫自定義的init命令,依賴於commander這個庫 lib/cmd.js

#!/usr/bin/env node

const program = require("commander");
const path = require('path');
const currentPath = process.cwd();  // 當前目錄路徑
const fs = require('fs-extra');
let projectName = null;


// 定義指令
program
  .version(require('../package.json').version)
  .command('init <name>')
  .action(function (name) {
    projectName = name;
  });
  program.parse(process.argv);
  program.projectName = projectName;

if (!projectName) {
  console.error('no project name was given, eg: webpack-multi-cli init myProject');
  process.exit(1);
}

if (fs.pathExistsSync(path.resolve(currentPath, `${projectName}`))) {
  console.error(`您建立的專案名:${projectName}已存在,建立失敗,請修改專案名後重試`);
  process.exit(1);
}

module.exports = program;

複製程式碼

如果沒有傳入專案名,或者傳入的專案名稱已經存在,則丟擲異常,並結束程式,順便把專案名稱存起來,當做一個預設名稱。還記得我們的命令不,

Project name test: // 專案名稱,如果不改,就直接原來的test名稱
複製程式碼

這個預設的專案名稱就是這裡來的

編寫互動問題

這裡依賴於inquirer這個庫

lib/inquirer.ja

const inquirer = require('inquirer');
const cmd = require('./cmd');
const projectName = cmd.projectName;

module.exports = {

    getQuestions: () => {

        const questions = [
            {
                name: 'projectName',
                type: 'input',
                message: 'Project name',
                default: projectName
            },
            {
                name: 'description',
                type: 'input',
                message: `Project description`,
                default: 'A webpack-multi project'
            },
            {
                name: 'author',
                type: 'input',
                message: `Author`,
            },
            {
                name: 'cssPreprocessor',
                type: 'list',
                message: 'Pick an css preprocessor',
                choices: [
                    "less",
                    "sass"
                ]
            },
            {
                name: 'isNpmInstall',
                type: 'input',
                message: 'Should we run `npm install` for you after the project has been create?<recommended>',
            }
        ];
        return inquirer.prompt(questions);
    },
}
複製程式碼

主要就是我們的一些互動問題,具體這個庫的用法,可以google之

主檔案

bin/index.js

我們這邊的思路是,新建一個template目錄,用來存在我們的webpack配置模板,將你輸入的那些問題,比如專案名,作者、描述、選擇less還是sass等,寫入這些配置檔案中,然後再下載到你執行命令的根目錄下

還有一種思路是獲取你回到的互動問題,然後從github中獲取檔案,再下載到你執行命令的根目錄下

知道這個思路後,就比較簡單了

先來獲取你輸入命令的當前路徑

const currentPath = process.cwd();  // 當前目錄路徑
複製程式碼

獲取輸入的互動問題

const config = await inquirer.getQuestions();
複製程式碼

接下來,就將獲取的互動問題答案,寫入我們的模板中即可

  • 寫入package.json
function handlePackageJson(config) {
    const spinner = ora('正在寫入package.json...').start();
    const promise = new Promise((resolve, reject) => {
        const packageJsonPath = path.resolve(`${currentPath}/${config.projectName}`, 'package.json');
        fs.readJson(packageJsonPath, (err, json) => {
            if (err) {
                console.error(err);
            }
            json.name = config.projectName;
            json.description = config.description;
            json.author = config.author;
            if(config.cssPreprocessor == 'less') {
                json.devDependencies = Object.assign(json.devDependencies, { 
                    "less": "^3.9.0",
                    "less-loader": "^4.1.0"
                });
            } else {
                json.devDependencies = Object.assign(json.devDependencies, { 
                    "sass-loader": "^7.1.0",
                    "node-sass": "^4.11.0"
                });
            }
            fs.writeJSON(path.resolve(`${currentPath}/${config.projectName}/package.json`), json, err => {
                if (err) {
                    return console.error(err)
                }
                spinner.stop();
                ora(chalk.green('package.json 寫入成功')).succeed();
                resolve();
            });
        });
    });
    return promise;
}
複製程式碼
  • 寫入webpack配置

webpack的互動問題比較簡單,就是選擇less還是sass,預設為less

function handleWebpackBase(config) {
    const spinner = ora('正在寫入webpack...').start();
    const promise = new Promise((resolve, reject) => {
        const webpackBasePath = path.resolve(`${currentPath}/${config.projectName}`, '_webpack/webpack.base.conf.js');
        fs.readFile(webpackBasePath, 'utf8', function(err, data) {
            if (err) {
                return console.error(err)
            }
            if(config.cssPreprocessor == 'scss') {
                data = data.replace("less-loader", "sass-loader");
            }
            fs.writeFile(path.resolve(`${currentPath}/${config.projectName}/_webpack/webpack.base.conf.js`), data, (err,result) => {
                if (err) {
                    return console.error(err)
                }
                spinner.stop();
                ora(chalk.green('webpack 寫入成功')).succeed();
                resolve();
            })
        })
    });
    return promise;
}
複製程式碼

完整的主檔案bin/index.js

#!/usr/bin/env node

const inquirer = require('../lib/inquirer');
const path = require('path');
const fs = require('fs-extra');
const ora = require('ora'); // 終端顯示的轉輪loading
const chalk = require('chalk');
const figlet = require('figlet');
const exec = require('promise-exec');
const currentPath = process.cwd();  // 當前目錄路徑
const templatePath = path.resolve(__dirname, '../template\/');

function handlePackageJson(config) {
    const spinner = ora('正在寫入package.json...').start();
    const promise = new Promise((resolve, reject) => {
        const packageJsonPath = path.resolve(`${currentPath}/${config.projectName}`, 'package.json');
        fs.readJson(packageJsonPath, (err, json) => {
            if (err) {
                console.error(err);
            }
            json.name = config.projectName;
            json.description = config.description;
            json.author = config.author;
            if(config.cssPreprocessor == 'less') {
                json.devDependencies = Object.assign(json.devDependencies, { 
                    "less": "^3.9.0",
                    "less-loader": "^4.1.0"
                });
            } else {
                json.devDependencies = Object.assign(json.devDependencies, { 
                    "sass-loader": "^7.1.0",
                    "node-sass": "^4.11.0"
                });
            }
            fs.writeJSON(path.resolve(`${currentPath}/${config.projectName}/package.json`), json, err => {
                if (err) {
                    return console.error(err)
                }
                spinner.stop();
                ora(chalk.green('package.json 寫入成功')).succeed();
                resolve();
            });
        });
    });
    return promise;
}

function handleWebpackBase(config) {
    const spinner = ora('正在寫入webpack...').start();
    const promise = new Promise((resolve, reject) => {
        const webpackBasePath = path.resolve(`${currentPath}/${config.projectName}`, '_webpack/webpack.base.conf.js');
        fs.readFile(webpackBasePath, 'utf8', function(err, data) {
            if (err) {
                return console.error(err)
            }
            if(config.cssPreprocessor == 'scss') {
                data = data.replace("less-loader", "sass-loader");
            }
            fs.writeFile(path.resolve(`${currentPath}/${config.projectName}/_webpack/webpack.base.conf.js`), data, (err,result) => {
                if (err) {
                    return console.error(err)
                }
                spinner.stop();
                ora(chalk.green('webpack 寫入成功')).succeed();
                resolve();
            })
        })
    });
    return promise;
}

function successConsole(config) {
    console.log('');
    const projectName = config.projectName;
    console.log(`${chalk.gray('專案路徑:')} ${path.resolve(`${currentPath}/${projectName}`)}`);
    console.log(chalk.gray('接下來,執行:'));
    console.log('');
    console.log('      ' + chalk.green('cd ') + projectName);
    if(config.isNpmInstall != 'npm') {
        console.log('      ' + chalk.green('npm install'));
    }
    console.log('      ' + chalk.green('npm run dev --dirname=demo'));
    console.log('');
    console.log(chalk.green('enjoy coding ...'));
    console.log(
        chalk.green(figlet.textSync("webpack multi cli"))
    );
}


function createTemplate(config) {
    const projectName = config.projectName;
    const spinner = ora('正在生成...').start();
    fs.copy(path.resolve(templatePath), path.resolve(`${currentPath}/${projectName}`))
    .then(() => {
        spinner.stop();
        ora(chalk.green('目錄生成成功!')).succeed();
        return handlePackageJson(config);
    })
    .then(() => {
        return handleWebpackBase(config);
    })
    .then(() => {
        if(config.isNpmInstall == 'npm') {
            const spinnerInstall = ora('安裝依賴中...').start();
            if(config.cssPreprocessor == 'sass') {
                console.log('如果node-sass安裝失敗,請檢視:https://github.com/sass/node-sass');
            }
            exec('npm install', {
                cwd: `${currentPath}/${projectName}`
            }).then(function(){
                console.log('')
                spinnerInstall.stop();
                ora(chalk.green('相賴安裝成功!')).succeed();
                successConsole(config);
            }).catch(function(err) {
                console.error(err);
            });
        } else {
            successConsole(config);
        }
    })
    .catch(err => console.error(err))
}

const launch = async () => {
    const config = await inquirer.getQuestions();
    createTemplate(config);
}
launch();
複製程式碼

最後,將我們的腳手架釋出到npm即可,關於npm包的釋出即可,很早之前寫過一篇檔案,可以點選檢視如何寫一個npm包

我們的包已經發布成功 www.npmjs.com/package/web…

接下來全域性安裝一下 npm install webpack-multi-cli -g

使用webpack init myTest,如下圖所示:

基於webpack4.x專案實戰3 - 手寫一個cli

安裝依賴後,執行 npm run dev --dirname=demo,就可以看到我們的效果了

基於webpack4.x專案實戰3 - 手寫一個cli

至此,我們的webpack-multi-cli腳手架就完成了,比較簡單,當然跟vue-cli是沒法比的,不過基本思路就在這裡,如果想搞複雜一點的腳手架,其實也就是一個擴充套件而已。

通過這篇文章,希望你可以學習到如何寫一個屬於自己的腳手架,瞭解到類似於vue-cli的基本思路,希望你能有些許的收穫

程式碼原始碼點選這裡

相關文章