基于webpack开发自定义cli,并发布到npm

441 阅读4分钟

需求:将 webpack 做成 cli 并发布到 www.npmjs.com

何时需要自定义 CLI

了解自定义 CLI 的应用场景

虽然市面上存在很多如 vue-cli 等流行且好用的 CLI 工具,但这些工具面向的是全体开发者,所以必须做的很通用,也正因如此,它们也就不可能包含一些和个人/企业强关联的私有化个性化功能。

比如:

  • 通过命令行生成业务代码:如一个业务数据的整套增、删、改、查代码
  • 通过命令行生成企业中已经非常成熟、复用度很高的基础框架代码,在开发新项目时能节约很多时间

webpack的结构

了解 webpack 的宏观结构,以便理解如何去开发基于 webpack 的命令行工具

核心内容:

  1. webpack 本身的结构

webpack 这个工具其实划分成三部分:

  • 核心 API - 实现 webpack 具体功能的代码
  • 命令行接口 - 即 webpack 自己提供的命令行工具(如:webpack build、webpack watch 等)
  • Node接口 - 开放给其他外部 node.js 程序调用的接口,让它们集成和调用 webpack 来完成打包相关的功能

image-20211028171919864.png

开发最简单的 CLI

使用 Node.js 开发一个最简单的 CLI 程序。

它的功能是:在命令行打印接受到的参数

核心内容:

  1. CLI 的概念
  2. 使用 Node.js 编写 CLI 程序

CLI 就是 Command-Line Interface,即命令行界面,也称为 CUI(Character User Interface,字符用户界面)。它是在 GUI (图形用户界面)普及前被广泛使用的计算机与用户间的交互界面,俗称”黑窗口“。

image-20211028173320260.png

具体步骤:

  1. 创建一个新目录,比如:my-cli

  2. 创建 src/index.js,编写 CLI 程序真正的业务逻辑

function echo(message) {
    console.log(`提示信息:${message}`)
}

module.exports = { echo }
  1. 创建 bin/echo-cli.js,CLI 可执行程序的入口代码
#!/usr/bin/env node

const { echo } = require('../src/index')

// 获取命令行携带的第一个参数
// process.argv是一个数组:
// - process.argv[0] 是 node 程序的路径
// - process.argv[1] 是本代码的路径
// - process.argv[2] 从该位置开始的元素,是命令行上指定的一个或多个参数
const msg = process.argv[2]

// 调用 echo 函数
echo(msg)

所有用脚本语言编写的 CLI 程序代码的开头第一行,都要加上 #! 脚本解释器名称

比如 Node.js 编写的 CLI 程序就要加上: #! /usr/bin/env node

以上这个由井号和叹号构成的字符序列 ,在计算领域中称为 Shebang 或 Hashbang。

文件中存在 Shebang 的情况下,Unix/Linux 操作系统的程序加载器会分析 Shebang 后的内容,将这些内容作为解释器指令来调用。

  1. 创建 package.json,并添加 bin配置:
{
  // ...
  "bin": {
    "myecho": "./bin/echo-cli.js"
  }
}

bin 的作用是:当 CLI 程序发布到 npmjs.com 之后:

  • 如果通过 npm 局部安装该包,npm 会将 echo-cli.js 放到 node_modules/.bin/myecho
  • 如果通过 npm 全局安装该包,则会产生一个软连接放到系统的全局程序目录中(Unix/Linux 上为 /usr/local/bin/myecho,windows 上为 C:\windows\system32\myecho)

安装后就能局部或全局调用该命令了( myecho )。

  1. 开发阶段的测试方法

开发阶段,如果频繁发布到 npmjs.com 上进行测试,肯定非常不便。可以采用下列方式:

首先,为 echo-cli.js 添加可执行权限,比如 Unix/Linux 系统上为:

chmod +x ./bin/echo-cli.js

然后就可以直接在命令行执行该程序文件:

./bin/echo-cli.js hello,cli

执行效果:

image-20211028181928386.png

以上是开发 CLI 程序所要具备的最基础知识。但在实际开发中,如果用这种方式开发稍微复杂点的 CLI 程序的话,会需要自行处理很多细节,花费大量时间。

开发标准体验的 CLI 程序

让开发出来的命令行程序更符合 Unix/Linux 等的命令行程序的使用体验

核心内容:

  1. 标准 CLI 的用户体验惯例及 Node.js 命令行工具库介绍
  2. 使用

命令行程序的交互形式是纯文字的,但即使是纯文字交互,也能可以为用户提供尽可能好好的体验,比如:

  • 提供命令的使用帮助信息
  • 提供对重要信息的高亮显示
  • 提供选项列表、文字输入等功能
  • 提供处理进度信息

如系列图片所示:

image-20211029092622217.png

image-20211029094609012.png

screenshot-2.gif

开源社区已将这类交互都封装成了工具包,供所有 CLI 程序开发者使用。

Node.js 中常用的库:

  • commander - 完整的 node.js 命令行解决方案,能方便的创建自定义命令行
  • inquirer - 实现命令行输入提示的交互界面工具库
  • chalk - 实现对命令行文字进行着色的工具库
  • shelljs - 实现了一套常用 Unix Shell 命令功能的工具库
  • ora - 实现在命令行中显示 loading 信息的工具库

具体步骤:

  1. 创建新项目目录,并安装 commander
npm i commander --save
  1. 创建 bin/hm.js
#! /usr/bin/env node

const { program } = require('commander')

// 设定我们的命令行程序的版本
program.version('0.0.1')

// 定义一个命令:init
program
    // 命令名称
    .command('init')
    // 命令别名
    .alias('new')
    // 命令描述
    .description('create a new project')
    // 命令具体做的事情
    .action(() => {
        console.log('初始化新项目...')
    })

// 定义另一个命令:serve
program
    // 命令名称
    .command('serve')
    // 命令描述
    .description('run the code with the dev server')
    // 命令选项
    .option('-p, --port <port>', 'server port', '8080')
    // 命令具体做的事情
    .action((args) => {
        console.log('运行开发服务器...', args)
    })

// 通过 commander 来解析命令行传递的参数信息【非常重要】
program.parse(process.argv)
  1. 配置 package.json 中的 bin 选项
{
  "name": "hm-cli",
  "bin": {
    "hm": "./bin/hm.js"
  },
  // ...
}
  1. 通过 npm link 功能,让本地开发中的 CLI 程序变成系统全局命令,方便开发测试
npm link

然后执行以下命令, 查看帮助信息:

hm --help

更进一步,执行 initserve 两个命令:

hm init
hm serve -p 8888

image-20211130191829296.png

实现简易版 Vue-CLI

开发一个类似 vue-cli 的 CLI 程序,可以生成项目骨架,并调用 webpack 实现 dev server 的启动和 build 打包

核心内容:

  1. 按上一章节介绍的标准体验 CLI 编写方式,编写一个 CLI 程序结构
  2. 实现三个 CLI 功能:创建项目骨架、使用开发服务器运行项目、打包项目

本简易版 vue-cli 将实现以下命令:

  • hm init
  • hm serve
  • hm build

推荐目录结构:

/my-webpack-cli
├─┬ bin
│ └── hm.js                          # CLI 主入口
├─┬ src
│ ├── init.js												 # hm init 功能代码
│ ├── serve.js                       # hm serve 功能代码
│ └── build.js                       # hm build 功能代码
├─┬ template
│ ├── framework                      # 骨架代码模板
│ └── webpack                        # webpack 配置文件
└── package.json

具体步骤:

一、开发初始化项目命令(hm init)

  1. 创建目录 my-webpack-cli,并按推荐结构创建各目录和空代码文件;并且下载模板文件template 目录
  1. 安装 commander,并编写 CLI 入口代码 bin/hm.js
npm i commander --save

bin/hm.js 中实现三个命令的最基本代码:

#! /usr/bin/env node

const { program } = require('commander')
const init = require('../src/init')
const serve = require('../src/serve')
const build = require('../src/build')

// 设定我们的命令行程序的版本
program.version('0.0.1')

// 定义一个命令:init
program
    .command('init')
    .description('create a new project')
    .action(() => {
        init()
    })

program
    .command('serve')
    .option('-p, --port <port>', 'server port', '3000')
    .description('run the project with dev server')
    .action(args => {
        serve(args)
    })

program
    .command('build')
    .description('build for production')
    .action(() => {
        build()
    })

// 通过 commander 来解析命令行传递的参数信息【非常重要】
program.parse(process.argv)

  1. package.json 中添加 bin 属性
{
  "name": "hm-cli",
  "bin": {
    "hm": "./bin/hm.js"
  },
  // ...
}
  1. 实现 src/init.js :生成项目骨架的功能

需要安装的依赖包:inquirershelljs

npm i inquirer shelljs --save

代码:

const inquirer = require('inquirer')
const shell = require('shelljs')
const path = require('path')

module.exports = async () => {
  // 1. 显示输入项目名称的提示
  const input = await inquirer.prompt([
    {
      type: 'input',
      name: 'name',
      message: '新项目的名称',
      default: 'myapp'
    },
    {
      type: 'list',
      name: 'packager',
      message: '使用哪个包管理工具安装依赖?',
      choices: ['yarn', 'npm'],
    }
  ])

  const projectName = input.name

  // 2. 创建项目目录
  shell.mkdir(projectName)

  // 3. 复制模板代码(从本地目录复制,理想情况下可以从网络下载)
  shell.cp('-R', path.join(__dirname, '../template/framework/*'), projectName)

  // 4. 进入项目目录,并使用 yarn/npm 安装依赖
  shell.cd(projectName)

  if (input.packager === 'yarn') {
    shell.exec('yarn')
  } else {
    shell.exec('npm i')
  }
}
  1. 通过 npm link 功能,让本地开发中的 CLI 程序变成系统全局命令,方便开发测试
npm link

在 CLI 项目下执行以上命令后,就能直接在命令行界面中执行 package.json中由 bin 指定的 hm 命令:

hm init

接着,按提示信息生成一个新的骨架项目:

image-20211101104212153.png

二、开发 dev server 命令(hm serve)

  1. template/webpack 目录下创建webpack.config.default.js` :
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {

  // 生成 Source Map 文件
  devtool: 'eval-source-map',

  // 入口文件
  entry: './src/main.js',

  // 打包输出
  output: {
    // 注意需要使用 process.cwd() 获取到当前的工作目录
    path: path.join(process.cwd(), 'dist'),
    filename: 'app.[contenthash].js'
  },

  module: {
    rules: [
      // 处理纯 css 样式文件
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      // 处理 sass|scss 样式文件
      {
        test: /\.(scss|sass)$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      },
      // 处理图片和字体文件
      {
        test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
        type: "asset"
      },
      // 处理 ES6+ 语法
      {
        test: /\.m?js$/i,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-transform-runtime']
          }
        }
      },
      {
        test: /\.vue$/i,
        use: 'vue-loader'
      }
    ]
  },

  plugins: [
    // 生成 index.html
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),

    // 抽取样式
    new MiniCssExtractPlugin(),

    // 打包清理
    new CleanWebpackPlugin(),

    // 处理 Vue 组件
    new VueLoaderPlugin()
  ],

  resolve: {
    alias: {
      // 为 src 目录设置别名 @
      '@': path.resolve(process.cwd(), 'src')
    }
  }
}

并安装需要的依赖包:

npm i clean-webpack-plugin html-webpack-plugin mini-css-extract-plugin vue-loader vue-template-compiler --save

【注意】

以上使用 process.cwd() 的目的是获取新生成的骨架项目的目录!(用户工作目录)

  1. 实现 vue-cli 中的 vue.config.js 的功能(用户配置)

先安装 webpackwebpack-mergewebpack-dev-server

npm i webpack webpack-merge webpack-dev-server --save

编写src/serve.js

const path = require('path')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const WebpackDevServer = require('webpack-dev-server')
const defaultConfig = require('../template/webpack/webpack.config.default')

module.exports = async (args) => {
  // 读取用户配置文件
  const userConfig = require(path.join(process.cwd(), 'hm.config')) || {}

  // 组装 webpack 配置对象(将默认配置和命令相关联配置合并)
  const config = merge(defaultConfig, {
    mode: 'development'
  })

  // 创建 webpack 编译器实例
  const compiler = webpack(config)

  // 创建 dev server 实例
  const server = new WebpackDevServer({
    static: {
      directory: path.join(process.cwd(), 'dist')
    },
    
    // 从命令行参数获取服务器端口
    port: args.port,
    
    // 从配置文件 hm.config.js 中获取其他配置信息
    ...userConfig.devServer
  }, compiler)

  // 启动 dev server
  await server.start()
}
  1. 在之前生成的骨架项目中该 serve 命令,启动开发服务器

三、开发打包命令(hm build)

  1. 编写 src/build.js
const path = require('path')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const defaultConfig = require('../template/webpack/webpack.config.default')

module.exports = () => {
  // 读取用户配置文件
  const userConfig = require(path.join(process.cwd(), 'hm.config')) || {}

  // 组装 webpack 配置对象(将默认配置和命令相关联配置合并)
  const config = merge(defaultConfig, {
    mode: 'production',
    // 从用户配置中获取 devtool 的值
    devtool: userConfig.devtool || 'source-map'
  })

  // 创建 webpack 编译器实例
  const compiler = webpack(config)

  // 执行打包
  compiler.run((err, stats) => {
    if (err) {
      console.log('build error:', err)
    }
    // 销毁编译器实例
    compiler.close((err, result) => {
      if (err) {
        console.log('close error:', err)
      }
    })
  })

}

随后即可使用 build 命令在生成的骨架项目下进行打包。

值得一提的知识

  1. 哪些包放到 cli 的 dependencies 中?哪些包放到模板的 devDependencies 中?
  • cli 程序中直接引用到的包,放到 cli 的 dependencies 中
  • 其他统统放到模板项目代码的 devDependencies 中(比如各种 webpack loader)
  1. 由于在生成的骨架项目的 package.json 中配有快捷命令:
"scripts": {
  "serve": "hm serve",
  "build": "hm build"
},

因此也可以用 npm run servenpm run build 来启动服务器和打包。

发布到 npmjs.com

将开发完成的简易版 vue-cli 发布到 npmjs.com

核心内容:

  1. npmjs.com 介绍

  2. 发布要用到的 npm 命令:npm login 和 npm publish

具体步骤:

  1. 注册一个 npmjs.com 的账号

  2. 在命令行登录 npmjs.com

npm login
  1. 上传发布我们的代码包
npm publish

发布时可能会遇到各种问题,最常遇到的就是由 package.json 中字段设置错误引起的。 以下这些字段一定要检查是否已正确设置:

  • name 包名,且不能和 npm 上已有包发生重名!
  • version 版本号
  1. 发布成功后,即可通过 npm 从 npmjs.com 下载安装我们的 CLI 程序
npm i -g 最终发布时的包名