记录:搭建属于自己的简单cli

186 阅读8分钟

前言:一直很好奇为什么在命令行输入一些指令(例如vue create)可以创建项目,以及执行一些特殊的操作,这两天浅学了一下如何实现自己的cli项目,在这里记录一下。

目前想实现如下功能:

  • 拉取我发布在githubvue模板(避免每次都需要重复配置,安装依赖,好麻烦)
  • 通过命令行命令创建pinia store的模块
  • 通过命令行命令创建components(待完成)
  • 通过命令行命令创建pages,并且生成对应的route(待完成)

实现完成之后发布到npm,供自己方便使用~(如果功能大家觉得不错的话也可以下载来试试)

第一步:准备模板

我们准备的是vue3的模板,用vite init初始化一个项目

image.png

集成Element-Plus

安装Element-Plus,这里我们用的是npm。

在命令行执行npm install element-plus --save

安装完成后,安装另外两个插件实现按需引入,如下。

npm install -D unplugin-vue-components unplugin-auto-import

image.png

在项目的vite.config.ts文件内添加如下配置

image.png

配置完成后,运行项目,会自动生成下面两个文件

image.png

在使用的时候也不需要写导入语句,也能正常解析,如下所示。

image.png

安装自动导入css插件解决命令方式使用ElementPlus组件没有样式的问题npm i unplugin-element-plus -D

image.png

集成eslint和prettier

安装vue官方生态的插件:vue add @vue/cli-plugin-eslint,会出现下面两个步骤

image.png

image.png

完成后会自动生成两个文件

image.png

下面内容覆盖到.editorconfig文件中

# http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

安装prettier npm install prettier -D,创建.prettierrc和.prettierignore文件,如下所示

image.png

image.png

安装插件解决prettier和eslint冲突:npm i eslint-plugin-prettier eslint-config-prettier -D

.eslintrc.js中对安装的解决冲突的插件进行配置

image.png

配置自动导入插件

上面为了实现按需引入,我们集成了unplugin-vue-component以及unplugin-auto-import插件,但是他的功能更加强大,可以让我们连import {ref} from 'vue'这种的导入文件也不需要写。我们需要进行如下配置。

先将enabled设置为true,运行项目,生成完文件之后改为false即可(也可以一直是true,但是会不停的生成)

image.png

.eslintrc.js中配置

image.png

auto-imports.d.ts加入到tsconfig.json中,就不会报错了。

image.png

配置完成之后我们可以直接使用内置的api而不需要进行导入,如下所示。

image.png

配置别名

vite.config.ts中添加如下配置

image.png

tsconfig.node.json中添加如下配置(允许使用esm的方式导入path模块)

image.png

tsconfig.json中添加如下配置(防止在使用@作为别名的时候报错)

image.png

集成vue-router路由管理库

执行命令 npm install vue-router@next安装vue-router

创建pages文件夹,创建index.vue页面,为默认路由做准备

image.png

创建router文件夹,创建router对象并导出

image.png

main.ts中安装插件

image.png

集成pinia状态管理库

执行命令安装pinia: npm install pinia

main.ts中安装插件

image.png

创建stores文件夹,保存所有的store对象定义。

image.png

定义一个main store,下面这个例子将说明如何使用pinia(比vuex好用太多了有没有!)

image.png

在使用的时候可以通过storeToRefs来解构成对应的ref,如下所示。

image.png

集成并封装axios

npm i axios

新建service文件夹,结构如下

image.png

request文件夹用于封装axios,代码如下

  • /service/request/index.ts
import axios from 'axios'
import { AxiosInstance, AxiosRequestConfig } from 'axios'
import { wjjRequestConfig, interceptor } from '@/service/request/type'
export class wjjRequest {
  //保存axios实例
  instance: AxiosInstance
  //保存属于每个axios实例特有的拦截器函数
  interceptor?: interceptor

  constructor(config: wjjRequestConfig) {
    this.instance = axios.create(config)
    this.interceptor = config.interceptor

    //从config中取出的,实例特有的拦截器
    this.instance.interceptors.request.use(
      this.interceptor?.requestOnFulfilled,
      this.interceptor?.requestOnRejected
    )

    this.instance.interceptors.response.use(
      this.interceptor?.responseOnFulfilled,
      this.interceptor?.responseOnRejected
    )
  }

  //传入泛型,用户可以指定返回的Promise的值的类型
  request<T = any>(config: AxiosRequestConfig): Promise<T> {
    return new Promise((resolve, reject) => {
      //request指定第二个泛型为传入的泛型,可以指定request返回的Promise的值的类型
      this.instance.request<any, T>(config).then(resolve, reject)
    })
  }

  get<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  post<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  delete<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }
  put<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'PUT' })
  }
}

  • /service/request/types.ts
import { AxiosRequestConfig, AxiosResponse } from 'axios'

export interface interceptor {
  requestOnFulfilled?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestOnRejected?: (err: any) => any
  responseOnFulfilled?: (config: AxiosResponse) => AxiosResponse
  responseOnRejected?: (err: any) => any
}

//实例也可以传入自定义的interceptor拦截器,这个对象的类型是自定义的interceptor类型
export interface wjjRequestConfig extends AxiosRequestConfig {
  interceptor?: interceptor
}

  • /service/index.ts 创建axios实例
//统一出口
import { wjjRequest } from '@/service/request'
const isLoading = ref(false)
//创建axios实例
let timer: any = null
const request_util = new wjjRequest({
  baseURL: process.env.VUE_APP_BASE_URL,
  interceptor: {
    // 请求成功拦截
    requestOnFulfilled(config) {
      isLoading.value = true
      return config
    },
    // 响应成功拦截
    responseOnFulfilled(res) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        isLoading.value = false
      }, 500)
      return res.data
    }
  },
  timeout: 5000
})

export { request_util, isLoading }

注意,上面的baseURL是需要在根目录创建.env文件并配置的,如下,会根据生产环境或者是开发环境分别取出对应的值(#号是注释)

image.png

取消css默认样式

npm install normalize.css

main.ts中导入normalize.css

image.png

在assets中管理静态资源

image.png

第二步:创建项目,搭建自己的cli工具

初始化

创建一个目录,在命令行输入npm init --yes生成package.json管理文件,并创建index.js入口文件

image.png

安装commander

commander是一个命令行插件,可以帮助我们定义一些命令行的指令,执行对应的函数。

npm i commander

修改index.js的代码,如下所示。(第一行是固定写法,目的是配置执行环境)

image.png

修改完成之后,在package.json中添加配置

image.png

执行npm link连接环境

执行wjj -V即可查看版本号

image.png

还可以通过wjj --help查看其他信息

image.png

定义自己的options信息

通过上面的例子可以看出,我们可以在指令后面添加--xxx 类似的options信息,但是commander插件默认的只有-V和-h两个options信息。当然,我们可以自己定义。

我们可以使用commander导出的program对象的options方法来定义自己的options信息,如下所示。

第一个参数是指定的options,-d 和--dest是指定的两种使用options的方式,<dest>表示需要在-d 后面跟上参数。第二个参数是对该options的描述。

image.png

封装完注册options的函数之后,在index.js导入并执行该函数,之后执行wjj -h就可以看到其他的options选项了

image.png

我们可以通过program.opts()方法获取到指令中options传入的参数(尖括号),如下所示

image.png

image.png

了解如何自定义指令

我们可以用program.command()方法创建命令行指令,例如我们现在希望可以在命令行执行wjj create demo指令,可以像下面这样注册指令。

image.png

  • command()内传入的是指令名,<>内是需要传入的参数,[]内是可选的其他参数
  • description()内传入的是该指令的描述
  • action()内传入的是执行该指令时,要运行的回调函数,会分别传入指令中传入的参数。

另外,我们是会创建很多指令的,为了拓展性考虑,我们将注册指令的逻辑封装到一个模块中,如下

image.png

并且将有些复杂的action回调也定义在一个模块中,如下所示

image.png

自定义create项目指令

image.png

具体的逻辑就在这个createProjectAction回调函数中,在这个回调函数内部,我们需要执行下面几个步骤:

  • clone项目
  • 执行npm install 安装依赖
  • 运行项目 npm run dev

clone项目拉取模板

首先是clone项目,在nodeclone项目的话,需要安装一个库:download-git-repo

npm i download-git-repo

这个库导出一个download函数,用法如下

  • 第一个参数是仓库地址
  • 第二个参数是保存的目录
  • 第三个参数是回调函数,用于获取内容

image.png

我们使用内置的util库将这个库导出的方法转成promise形式,如下所示。

image.png

在另一个模块中定义与仓库地址有关的配置信息,如下所示。

image.png

action回调默认传入的参数(project,地址)传入download,这样我们就可以下载远程模板到对应的地址了。

image.png

执行npm install 和npm run dev

平时拉取完模板之后,我们都是要手动执行npm install安装依赖的,但是如果想自动安装要怎么实现呢?

我们需要用到node内置的child_process模块内的spawn函数开启一个新的进程,在新的进程中执行命令。如下所示。

image.png

封装完函数之后,我们就可以通过这个函数来执行一些命令行指令。

image.png

测试

在命令行执行 wjj create tttt命令,结果如下,可见create指令功能已经完成~

image.png

自定义addstore指令

老样子,先注册指令,具体的逻辑在回调函数中写。如下所示

image.png

我们在addStoreAction函数内部需要执行下面几步操作:

  • 预先定义好要写入的ejs模板
  • 编译ejs模板
  • 写入文件

定义ejs模板

首先是定义ejs模板,如下所示,其中<%=%>是特殊语法,类似于占位符,可以将传入的内容替换到这个位置。

image.png

解析ejs模板

安装ejs库,封装compile函数编译ejs模板,如下所示

npm i ejs

image.png

写入文件到对应目录

封装写入文件的函数writeToFile

image.png

使用mkdirp这个库创建目录

npm i mkdirp

addStoreAction函数具体实现如下

image.png

测试

可以看到,执行wjj addstore demo指令,会在对应的stores文件夹中创建对应的pinia store

image.png

自定义addcpn指令(待实现)

addcpn指令的效果应该是,在components文件夹下创建对应的.vue文件,应该挺简单的,之后再实现具体逻辑。

自定义addpage指令(待实现)

addpage指令的效果应该是

  • 在pages文件夹下,根据指令传入的dest创建目录(如果是嵌套层级的话)和对应的.vue文件
  • 读取router文件夹下的routes内容,添加对应的路由规则进行拼接。

第三步:发布到npm方便使用

配置package.json

image.png

登陆npm:npm login

发布:npm publish