欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

使用 Vue cli 3.0 构建自定义组件库的方法

程序员文章站 2023-12-04 14:28:16
本文旨在给大家提供一种构建一个完整 ui 库脚手架的思路:包括如何快速并优雅地构建ui库的主页、如何托管主页、如何编写脚本提升自己的开发效率、如何生成 changelog...

本文旨在给大家提供一种构建一个完整 ui 库脚手架的思路:包括如何快速并优雅地构建ui库的主页、如何托管主页、如何编写脚本提升自己的开发效率、如何生成 changelog 等

前言

主流的开源 ui 库代码结构主要分为三大部分:

  • 组件库本身的代码:这部分代码会发布到 npm 上
  • 预览示例和查看文档的网站代码:类似 vant、elementui 这类网站。
  • 配置文件和脚本文件:用于打包和发布等等

编写此博文的灵感 ui 框架库( vue-cards ),ps:此 ui框架库相对于vant、elementui会比较简单点,可以作为一份自定义ui框架库的入坑demo,同时这篇博文也是解读这份 ui 框架库的构建到上线的一个过程

前置工作

以下工作全部基于 vue cli 3.x,所以首先要保证机子上有 @vue/cli

vue create vtp-component # vtp-component 作为教学的库名vue-router , dart-sass , babel , eslint 这些是该项目使用的依赖项,小主可以根据自己的需求进行相应的切换

start

开始造*了

工作目录

在根目录下新增四个文件夹,一个用来存放组件的代码(packages),一个用来存放 预览示例的网站 代码(examples)(这里直接把初始化模板的 src 目录更改为 examples 即可,有需要的话可以将该目录进行清空操作,这里就不做过多的说明),一个用来存放编译脚本代码(build)修改当前的工作目录为以下的格式吗,一个用来存放自定义生成组件和组件的说明文档等脚本(scripts)

|--- build     
|
|--- examples
|
|--- packages
|

|--- scripts

让 webpack 编译 examples

由于我们将 src 目录修改成了 examples,所以在 vue.config.js 中需要进行相应的修改

const path = require('path')
function resolve (dir) {
 return path.join(__dirname, dir)
}
module.exports = {
 productionsourcemap: true,
 // 修改 src 为 examples
 pages: {
 index: {
  entry: 'examples/main.js',
  template: 'public/index.html',
  filename: 'index.html'
 }
 },
 chainwebpack: config => {
 config.resolve.alias
  .set('@', resolve('examples'))
 }
}

添加编译脚本

package.json

其中的组件 name 推荐和创建的项目名一致

{
 "scripts": {
 "lib": "vue-cli-service build --target lib --name vtp-component --dest lib packages/index.js"
 }
}

修改 main 主入口文件

{
 "main": "lib/vtp-component.common.js"
}

一个组件例子

创建组件和组件文档生成脚本

在 scripts 中创建以下几个文件,其中 create-comp.js 是用来生成自定义组件目录和自定义组件说明文档脚本, delete-comp.js 是用来删除无用的组件目录和自定义组件说明文档脚本, template.js 是生成代码的模板文件

|--- create-comp.js
|
|--- delete-comp.js
|
|--- template.js

相关的代码如下,小主可以根据自己的需求进行相应的简单修改,下面的代码参考来源 vue-cli3 项目优化之通过 node 自动生成组件模板 generate view、component

create-comp.js

// 创建自定义组件脚本
const chalk = require('chalk')
const path = require('path')
const fs = require('fs-extra')
const uppercamelize = require('uppercamelcase')
const resolve = (...file) => path.resolve(__dirname, ...file)
const log = message => console.log(chalk.green(`${message}`))
const successlog = message => console.log(chalk.blue(`${message}`))
const errorlog = error => console.log(chalk.red(`${error}`))
const {
 vuetemplate,
 entrytemplate,
 mddocs
} = require('./template')
const generatefile = (path, data) => {
 if (fs.existssync(path)) {
 errorlog(`${path}文件已存在`)
 return
 }
 return new promise((resolve, reject) => {
 fs.writefile(path, data, 'utf8', err => {
  if (err) {
  errorlog(err.message)
  reject(err)
  } else {
  resolve(true)
  }
 })
 })
}
// 这里生成自定义组件
log('请输入要生成的组件名称,形如 demo 或者 demo-test')
let componentname = ''
process.stdin.on('data', async chunk => {
 let inputname = string(chunk).trim().tostring()
 inputname = uppercamelize(inputname)
 const componentdirectory = resolve('../packages', inputname)
 const componentvuename = resolve(componentdirectory, `${inputname}.vue`)
 const entrycomponentname = resolve(componentdirectory, 'index.js')
 const hascomponentdirectory = fs.existssync(componentdirectory)
 if (inputname) {
 // 这里生成组件
 if (hascomponentdirectory) {
  errorlog(`${inputname}组件目录已存在,请重新输入`)
  return
 } else {
  log(`生成 component 目录 ${componentdirectory}`)
  await dotexistdirectorycreate(componentdirectory)
 }
 try {
  if (inputname.includes('/')) {
  const inputarr = inputname.split('/')
  componentname = inputarr[inputarr.length - 1]
  } else {
  componentname = inputname
  }
  log(`生成 vue 文件 ${componentvuename}`)
  await generatefile(componentvuename, vuetemplate(componentname))
  log(`生成 entry 文件 ${entrycomponentname}`)
  await generatefile(entrycomponentname, entrytemplate(componentname))
  successlog('生成 component 成功')
 } catch (e) {
  errorlog(e.message)
 }
 } else {
 errorlog(`请重新输入组件名称:`)
 return
 }
 // 这里生成自定义组件说明文档
 const docsdirectory = resolve('../examples/docs')
 const docsmdname = resolve(docsdirectory, `${inputname}.md`)
 try {
 log(`生成 component 文档 ${docsmdname}`)
 await generatefile(docsmdname, mddocs(`${inputname} 组件`))
 successlog('生成 component 文档成功')
 } catch (e) {
 errorlog(e.message)
 }
 process.stdin.emit('end')
})
process.stdin.on('end', () => {
 log('exit')
 process.exit()
})
function dotexistdirectorycreate (directory) {
 return new promise((resolve) => {
 mkdirs(directory, function () {
  resolve(true)
 })
 })
}
// 递归创建目录
function mkdirs (directory, callback) {
 var exists = fs.existssync(directory)
 if (exists) {
 callback()
 } else {
 mkdirs(path.dirname(directory), function () {
  fs.mkdirsync(directory)
  callback()
 })
 }
}delete-comp.js 
// 删除自定义组件脚本
const chalk = require('chalk')
const path = require('path')
const fs = require('fs-extra')
const uppercamelize = require('uppercamelcase')
const resolve = (...file) => path.resolve(__dirname, ...file)
const log = message => console.log(chalk.green(`${message}`))
const successlog = message => console.log(chalk.blue(`${message}`))
const errorlog = error => console.log(chalk.red(`${error}`))
log('请输入要删除的组件名称,形如 demo 或者 demo-test')
process.stdin.on('data', async chunk => {
 let inputname = string(chunk).trim().tostring()
 inputname = uppercamelize(inputname)
 const componentdirectory = resolve('../packages', inputname)
 const hascomponentdirectory = fs.existssync(componentdirectory)
 const docsdirectory = resolve('../examples/docs')
 const docsmdname = resolve(docsdirectory, `${inputname}.md`)
 if (inputname) {
 if (hascomponentdirectory) {
  log(`删除 component 目录 ${componentdirectory}`)
  await removepromise(componentdirectory)
  successlog(`已删除 ${inputname} 组件目录`)
  log(`删除 component 文档 ${docsmdname}`)
  fs.unlink(docsmdname)
  successlog(`已删除 ${inputname} 组件说明文档`)
 } else {
  errorlog(`${inputname}组件目录不存在`)
  return
 }
 } else {
 errorlog(`请重新输入组件名称:`)
 return
 }
 process.stdin.emit('end')
})
process.stdin.on('end', () => {
 log('exit')
 process.exit()
})
function removepromise (dir) {
 return new promise(function (resolve, reject) {
 // 先读文件夹
 fs.stat(dir, function (_err, stat) {
  if (stat.isdirectory()) {
  fs.readdir(dir, function (_err, files) {
   files = files.map(file => path.join(dir, file)) // a/b a/m
   files = files.map(file => removepromise(file)) // 这时候变成了promise
   promise.all(files).then(function () {
   fs.rmdir(dir, resolve)
   })
  })
  } else {
  fs.unlink(dir, resolve)
  }
 })
 })
}template.js 
module.exports = {
 vuetemplate: compoenntname => {
 compoenntname = compoenntname.charat(0).tolowercase() + compoenntname.slice(1)
 return `<template>
 <div class="vtp-${compoenntname}">
 ${compoenntname}
 </div>
</template>
<script>
export default {
 name: 'vtp-${compoenntname}',
 data () {
 return {
 }
 },
 props: {
 },
 methods: {}
}
</script>
<style lang="scss" scope>
.vtp-${compoenntname}{}
</style>
`
 },
 entrytemplate: compoenntname => {
 return `import ${compoenntname} from './${compoenntname}'
${compoenntname}.install = function (vue) {
 vue.component(${compoenntname}.name, ${compoenntname})
}
export default ${compoenntname}
if (typeof window !== 'undefined' && window.vue) {
 window.vue.component(${compoenntname}.name, ${compoenntname})
}
`
 },
 mddocs: (title) => {
 return `# ${title}
<!-- {.md} -->
---
<!-- {.md} -->
## 如何使用
<!-- {.md} -->
## attributes
<!-- {.md} -->
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|-----|-----|-----|-----|-----|
| - | - | - | - | - |
 `
 }
}
`
 },
 entrytemplate: compoenntname => {
 return `import ${compoenntname} from './${compoenntname}'
${compoenntname}.install = function (vue) {
 vue.component(${compoenntname}.name, ${compoenntname})
}
if (typeof window !== 'undefined' && window.vue) {
 window.vue.component(${compoenntname}.name, ${compoenntname})
}
 }
}

在 build 中创建以下几个文件,其中 build-entry.js 脚本是用来生成自定义组件导出 packages/index.js , get-components.js 脚本是用来获取 packages 目录下的所有组件

|--- build-entry.js
|
|--- get-components.js

相关的代码如下,小主可以根据自己的需求进行相应的简单修改,下面的代码参考来源 vue-cards

build-entry.js

const fs = require('fs-extra')
const path = require('path')
const chalk = require('chalk')
const uppercamelize = require('uppercamelcase')
const components = require('./get-components')()
const packagejson = require('../package.json')
const log = message => console.log(chalk.green(`${message}`))
const version = process.env.version || packagejson.version
function buildpackagesentry () {
 const uninstallcomponents = []
 const importlist = components.map(
 name => `import ${uppercamelize(name)} from './${name}'`
 )
 const exportlist = components.map(name => `${uppercamelize(name)}`)
 const intalllist = exportlist.filter(
 name => !~uninstallcomponents.indexof(uppercamelize(name))
 )
 const content = `import 'normalize.css'
${importlist.join('\n')}
const version = '${version}'
const components = [
 ${intalllist.join(',\n ')}
]
const install = vue => {
 if (install.installed) return
 components.map(component => vue.component(component.name, component))
}
if (typeof window !== 'undefined' && window.vue) {
 install(window.vue)
}
export {
 install,
 version,
 ${exportlist.join(',\n ')}
}
export default {
 install,
 version,
 ...components
}
`
 fs.writefilesync(path.join(__dirname, '../packages/index.js'), content)
 log('packages/index.js 文件已更新依赖')
 log('exit')
}
buildpackagesentry()get-components.js 
const fs = require('fs')
const path = require('path')
const excludes = [
 'index.js',
 'theme-chalk',
 'mixins',
 'utils',
 '.ds_store'
]
module.exports = function () {
 const dirs = fs.readdirsync(path.resolve(__dirname, '../packages'))
 return dirs.filter(dirname => excludes.indexof(dirname) === -1)
}

让 vue 解析 markdown

文档中心的 ui 是如何编码的这里不做阐述,小主可以自行参照 vue-cards 中的实现方式进行改造

需要安装以下的依赖,让 vue 解析 markdown

npm i markdown-it-container -d
npm i markdown-it-decorate -d
npm i markdown-it-task-checkbox -d
npm i vue-markdown-loader -d

关于 vue.config.js 的配置在 vue-cards 该项目中也有了,不做阐述

这里将补充高亮 highlight.js 以及点击复制代码 clipboard 的实现方式

安装依赖

npm i clipboard highlight.js改造 app.vue ,以下只是列出部分代码,小主可以根据自己的需求进行添加

<script>
import hljs from 'highlight.js'
import clipboard from 'clipboard'
const highlightcode = () => {
 const preel = document.queryselectorall('pre')
 preel.foreach((el, index) => {
 hljs.highlightblock(el)
 const lang = el.children[0].classname.split(' ')[1].split('-')[1]
 const pre = el
 const span = document.createelement('span')
 span.setattribute('class', 'code-copy')
 span.setattribute('data-clipboard-snippet', '')
 span.innerhtml = `${lang.touppercase()} | copy`
 pre.appendchild(span)
 })
}
export default {
 name: 'app',
 mounted () {
 if ('onhashchange' in window) {
  window.onhashchange = function (ev) {
  let name = window.location.hash.substring(2)
  router.push({ name })
  }
 }
 highlightcode()
 let clipboard = new clipboard('.code-copy', {
  text: (trigger) => {
  return trigger.previoussibling.innertext
  }
 })
 // 复制成功执行的回调
 clipboard.on('success', (e) => {
  e.trigger.innerhtml = `已复制`
 })
 },
 updated () {
 highlightcode()
 }
}
</script>

生成命令

package.json 中添加以下内容,使用命令 yarn new:comp 创建组件目录及其文档或者使用命令 yarn del:comp 即可删除组件目录及其文档

{
 "scripts": {
 "new:comp": "node scripts/create-comp.js && node build/build-entry.js",
 "del:comp": "node scripts/delete-comp.js && node build/build-entry.js"
 }
}

changelog

在 package.json 中修改 script 字段,接下来你懂的,另一篇博客有介绍哦,小主可以执行搜索

{
 "scripts": {
 "init": "npm install commitizen -g && commitizen init cz-conventional-changelog --save-dev --save-exact && npm run bootstrap",
 "bootstrap": "npm install",
 "changelog": "conventional-changelog -p angular -i changelog.md -s -r 0"
 }
}

总结

以上所述是小编给大家介绍的使用 vue cli 3.0 构建自定义组件库的方法,希望对大家有所帮助