构建一个用于建立组件库的项目脚手架工具(类 Vue-cli3)

缘起

最近公司内部想搭建一个私有的 npm 仓库,用于将平时用到次数至关频繁的工具或者组件独立出来,方便单独管理,随着项目的规模变大,数量变多,单纯的复制粘粘无疑在优雅以及实用性上都没法知足咱们的需求,因此进一步模块化是必然的。vue

可是一个组件库的创建实际上是一个很是麻烦的过程,基础 webpack 的配置不用多说,接着你还要配合增长一些 es-lint 之类的工具来规范化团队成员的代码。在开发过程当中,你天然须要一个目录来承载使用示例,方便 dev 这个组件,随后呢,你还得创建一个打包规范,发布到私有 npm 仓库中。node

如此一来,必然大大下降咱们的积极性,因此不如建立一个用于创建模块包的脚手架工具,方便咱们项目的初始化。webpack

tips:最终成品在底部git

私有 NPM

这里简单说起一下 私有 npm 的搭建。github

npm i verdaccio -g
pm2 start verdaccio

推荐配合 nrm 使用 快速切换仓库地址web

verdaccio githubvue-cli

还整个意大利名,属实洋气。npm

工具

在进入正题以前,我先介绍一些要点和工具,有了这写关键点,写起来其实就至关简单了。element-ui

npm bin

你们有没有想过一些全局安装的工具,他是如何作到在命令行里面自由调用的呢?json

事实上这个东西是 npm 提供的连接功能

// package.json
{
  "name": "lucky-for-you",
  "bin": {
    "lucky": "bin/lucky"
  }
}

当这样的一个模块被发布以后,一旦有人使用 -g 参数全局安装

sudo npm i luck-for-you -g

/usr/local/bin/lucky -> /usr/local/lib/node_modules/luckytiger-package-cli/bin/lucky # npm 帮你进行连接

npm 事实上会帮你进行一次连接,连接到你操做系统的 Path 之中,从而但你敲出 Lucky 这个命令的时候,能从 path 中成功找到对应的程序

另一点就是用于连接执行的文件 通常在开头都要加上以下内容,让 bash 可以正确识别该文件应该如何执行

#!/usr/bin/env node
// 意味使用 node 运行该文件
// next script

Commander.js

tj 大神的做品,能够方便的书写命令行工具。可以自动生成帮助命令

const program = require('commander');

program.version('0.0.1').usage('<command> [options]');

program
  .command('create <app-name>')
  .description('建立一个全新的 npm 组件模块')
  .action((name, cmd) => {
    const options = cleanArgs(cmd);
    require('../lib/create')(name, options);
  });

// 用户未输入完整命令 输出帮助
if (!process.argv.slice(2).length) {
  program.outputHelp();
}

program.parse(process.argv);

Commander.js github

inquirer

事实上当我第一次使用 vue-cli3.0 的时候,里面的命令行表单真是很是惊艳,翻了 vue-cli3 的源码 找到了这款工具,用于命令行的表单。可以更加直观的配置选项。

image

inquirer
  .prompt([
    {
      type: 'list',
      name: 'template',
      message: 'template: 请选择项目起始模板',
      choices: [
        {
          key: '1',
          name: 'JavaScript Library - 适用于普通 JS 库',
          value: 'js-lib',
        },
        {
          key: '2',
          name: 'Vue-components - 适用于 Vue 组件库',
          value: 'vue-component',
        },
      ],
    },
    {
      type: 'input',
      name: 'author',
      message: 'author: 请输入你的名字',
      validate: function(value) {
        return !!value;
      },
    },
    {
      type: 'input',
      name: 'desc',
      message: 'desc: 请输入项目描述',
      validate: function(value) {
        return !!value;
      },
    },
    {
      type: 'confirm',
      name: 'confirm',
      message: 'confirm: 完成配置了?',
      default: false,
    },
  ])
  .then(answers => {
    console.log(answers.template);
    console.log(answers.author);
    console.log(answers.desc);
  });

还有不少的表单类型,我这里几个最简单的 list + input + confirm 就足够了。

inquire github

开始构建

如今开始分享个人构建流程。因为代码量比较大,挨个文件帖出来没有什么必要,因此我这里只作简单介绍,具体的能够查看个人 github项目。

我把个人 cli 工具大体分为两部分 template模板 + 建立器
z
建立器的主要功能是吸取用户的可选项,基于模板进行复制+渲染。Vue-cli3.0对于这部分操做会更加复杂,他把模板里面具体的功能都抽象成了一个 Plugin,能够按需组建模板,对于面向广泛大众固然是更好的。

可是我这个项目由于是公司内部用,因此不太须要太过泛化的设计,一个模板直接解决一个问题,简化模型就能够了。好比一个模板用于建立 Vue 的组件库,一个模板用于建立 React 的组件库,还有一个模板用于建立JavaScript 的工具函数类库。

如此一来咱们的 template模板 建立器在必定程度上能够作到解耦,也就是说往后须要更多类型的模板,不须要修改建立器部分的代码。

目录结构

├── README.md
├── bin
│   └── lucky #主程序
├── lib
│   ├── copy.js #复制
│   └── create.js #主建立器
├── package-lock.json
├── package.json
├── templates
│   ├── config.js #模板配置 解耦
│   ├── js-lib #预设模板1
│   └── vue-component #预设模板2
├── utils # 工具目录
│   └── dir.js

package.json

{
  "name": "luckytiger-package-cli",
  "version": "1.1.14",
  "description": "package-cli",
  "bin": {
    "lucky": "bin/lucky"
  },
  "scripts": {
    "lucky": "node bin/lucky",
    "bootstarp": "cnpm i && cd ./templates/js-lib/ &&  cnpm i   && cd ../vue-component/ && cnpm i  ",
    "dev:js-lib": "cd templates/js-lib  && npm run dev",
    "dev:vue-component": "cd templates/vue-component && npm run dev",
    "dev:create": "rm -rf test-app && node bin/lucky create test-app",
    "clear": "sudo rm -rf node_modules && sudo rm -rf templates/js-lib/node_modules && sudo rm -rf templates/vue-component/node_modules"
  },
  "author": "zhangzhengyi",
  "license": "ISC",
  "dependencies": {
    "chalk": "^2.4.2",
    "commander": "^2.20.0",
    "ejs": "^2.6.2",
    "inquirer": "^6.4.1",
    "validate-npm-package-name": "^3.0.0"
  }
}

配置了一些脚本 方便快速 DEV 模板的效果。

这样运行

npm run dev:js-lib

就能查看和开发 js-lib 这个模板

主程序

bin/lucky
#!/usr/bin/env node

const program = require('commander')

program.version('0.0.1').usage('<command> [options]')

program
  .command('create <app-name>')
  .description('建立一个全新的 npm 组件模块')
  .action((name, cmd) => {
    const options = cleanArgs(cmd)
    require('../lib/create')(name, options)
  })

if (!process.argv.slice(2).length) {
  program.outputHelp()
}

program.parse(process.argv)

// commander passes the Command object itself as options,
// extract only actual options into a fresh object.
function cleanArgs(cmd) {
  const args = {}
  cmd.options.forEach(o => {
    const key = camelize(o.long.replace(/^--/, ''))
    // if an option is not present and Command has a method with the same name
    // it should not be copied
    if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') {
      args[key] = cmd[key]
    }
  })
  return args
}

这个文件主要是作一下基本的命令设置 利用了 commander这个库

若是用户调用了建立命令,就会转发给 lib/create.js 处理

主建立器

lib/cerate.js
const path = require('path')
const inquirer = require('inquirer')
const validateProjectName = require('validate-npm-package-name')
const chalk = require('chalk')
const copy = require('./copy')
const fs = require('fs')
const dir = require('../utils/dir')
const templates = require('../templates/config')

async function create(projectName, options) {
  const cwd = options.cwd || process.cwd()
  const inCurrent = projectName === '.'
  const name = inCurrent ? path.relative('../', cwd) : projectName
  const targetDir = path.resolve(cwd, projectName || '.')

  const result = validateProjectName(name)
  if (!result.validForNewPackages) {
    console.error(chalk.red(`无效的项目名: "${name}"`))
    result.errors &&
      result.errors.forEach(err => {
        console.error(chalk.red.dim('Error: ' + err))
      })
    result.warnings &&
      result.warnings.forEach(warn => {
        console.error(chalk.red.dim('Warning: ' + warn))
      })
    return
  }

  if (!dir.isDir(targetDir)) {
    fs.mkdirSync(targetDir)
  } else {
    console.error(chalk.red(`该目录下已经存在该文件夹 请删除或者修改项目名`))
    return
  }

  const answers = await inquirer.prompt([
    {
      type: 'list',
      name: 'template',
      message: 'template: 请选择项目模板',
      choices: templates.map((v, i) => ({
        key: i,
        name: v.name,
        value: v.dir
      }))
    },
    {
      type: 'input',
      name: 'author',
      message: 'author: 请输入你的名字',
      validate: function(value) {
        return !!value
      }
    },
    {
      type: 'input',
      name: 'desc',
      message: 'desc: 请输入项目描述',
      validate: function(value) {
        return !!value
      }
    },
    {
      type: 'confirm',
      name: 'confirm',
      message: 'confirm: 完成配置了?',
      default: false
    }
  ])

  // 启动复制流程
  const sourceDir = path.resolve(__dirname, '..', 'templates', answers.template)
  console.log(chalk.blue(`🚀    开始建立...`))

  try {
    await copy({
      from: sourceDir,
      to: targetDir,
      renderData: {
        desc: answers.desc,
        author: answers.author,
        name: projectName
      },
      ignore: ['node_modules', 'package.json']
    })
  } catch (e) {
    console.error(chalk.red(e))
    return
  }

  console.log(chalk.green('🎉    建立完毕!'))
  console.log()
  console.log(chalk.cyan(` $ cd ${projectName}`))
  console.log(chalk.cyan(` $ npm i && npm run dev`))
}

module.exports = create

这里主要作了几件事

  1. 保证项目名合法。
  2. 确认项目在当前目录不存在
  3. 收集用户的填写信息
  4. 启动复制流程

这里面 chalk 这个库可以输出带颜色的命令行,美观一点。

我把模板的一些配置信息都放到了 templates/config.js 中,目的是为了解耦

//templates/config.js
module.exports = [
  {
    name: 'JavaScript Library - 适用于普通 JS 库',
    dir: 'js-lib'
  },
  {
    name: 'Vue-components - 适用于 Vue 组件库',
    dir: 'vue-component'
  }
]

接下来让咱们看看复制流程

复制

lib/copy
const fs = require('fs')
const path = require('path')
const dir = require('../utils/dir')
const ejs = require('ejs')

async function copy({ from, to, renderData, ignore = [] }) {
  let files = fs.readdirSync(from)
  // 区分 文件 和 目录
  let rFiles = []
  let dirs = []
  for (const fileName of files) {
    if (dir.isDir(path.resolve(from, fileName))) {
      dirs.push(fileName)
    } else {
      rFiles.push(fileName)
    }
  }

  // 复制并编译文件
  rFiles.forEach(fileName => {
    // 须要忽略
    if (ignore.some(v => v === fileName)) {
      return
    }
    let content = fs.readFileSync(path.resolve(from, fileName), 'utf-8')
    // 该文件须要调用 ejs 模板引擎进行编译
    if (/ejs$/.test(fileName)) {
      content = ejs.render(content, renderData)
      fileName = fileName.replace('.ejs', '')
    }
    fs.writeFileSync(path.resolve(to, fileName), content)
  })

  // 递归复制 目录
  dirs.forEach(dirName => {
    // 须要忽略
    if (ignore.some(v => v === dirName)) {
      return
    }
    const fromDir = path.resolve(from, dirName)
    const toDir = path.resolve(to, dirName)
    if (!dir.isDir(toDir)) {
      fs.mkdirSync(toDir)
    }
    copy({ from: fromDir, to: toDir, renderData, ignore })
  })
}

module.exports = copy

copy 是一个递归复制文件和目录的结构,深度优先。

其中他拥有四个参数源文件夹,目标文件夹,渲染数据,忽略列表。

咱们的模板实际上是须要一些按需渲染内容的能力的,好比生成的 package.json 应该拥有用户建立时填写的项目名,建立者,描述等等信息。因此我这里采用了 EJS 模板引擎进行渲染,全部以.ejs 结尾的文件,都将通过引擎+渲染数据的渲染,接着再输出,好比 package.json.ejs

另外作了一些忽略的设计,缘由是某些文件在开发模板的过程当中须要,实际生成的时候须要进行过滤。

所有采用同步 API,由于咱们的文件都是比较小的,而且不是服务器上用,阻塞一下也没有问题。

模板的构建

个人这里设计了两个预设模板,分别是 Vue-component 组件库模板 另一个是 JS 库的模板(示例一样基于 Vue)。若是大家有相似的 需求能够去看看。这两个模板都是先用 vue-cli3.0生成以后进行改装。

改装的目的就是为了更加契合组件库这一需求,跟普通的项目不太同样,组件库须要在 DEV 模式下对组件进行测试和开发,而后必须拥有单独打包这个组件的能力,接着进行发布。

具体能够直接看代码

构建的过程当中有些坑须要注意

模板内部应该拥有两个 package.json 文件

package.json 用于模板的 DEV 模式

package.json.ejs 用于建立时的最终导出

而且不要在 package.json 里面使用 files 字段作文件 publish 白名单,这会致使你的 cli 工具没法正常发布整个模板(这个应该是模板内部的 package.json 与整个 cli 工具的 package.json 产生了覆盖关系)。

模板内部的.gitignore文件加个.ejs

一样是 cli publish 的时候没法正常 上传模板里面的.gitignore 文件,因此加个 ejs 可让他假装成普通文件。

因此我以为 npm包 的嵌套是否是太容易产生干扰了一点。

types 推荐

这里推荐你们写组件库的时候,能够手写一下 TS 的类型声明 types,在 VSCode 下能得到很是好的代码提示效果。

首先你须要在组件库的 package.json 里面添加一个属性

{
  "typings": "types/index.d.ts",
}

我这里写一个简单的函数

// 最终导出
export default {
  say (name) {
    return `your name: ${name}`
  }
}
// index.d.ts
function say(name: String): String

export default {
  say
}

这样 VSCode 就能在你使用这个模块的时候,给你更加健全的提示。

image

这里额外提醒下,通过个人研究,element-ui 这样的组件库,能有 props 的提示是由于人家 vetur 组件专门给开的后门,写 types 只能拥有 JS 层面的提示,写 Vue-template 的时候依旧没有,期待后续可以支持。

参考

vue-cli

Vue cli3 库模式搭建组件库并发布到 npm的流程

element-ui

个人我的博客

相关文章
相关标签/搜索