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

作者:Coding and Fixing 时间:2024-05-05 09:07:50 

本文旨在给大家提供一种构建一个完整 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 构建自定义组件库的方法,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!

来源:https://vxhly.github.io/archives/67d67641.html

标签:vue,cli,组件库
0
投稿

猜你喜欢

  • python 使用elasticsearch 实现翻页的三种方式

    2021-03-09 17:39:57
  • 虚拟环境及venv和virtualenv的区别说明

    2021-07-07 09:19:51
  • 详解Bagging算法的原理及Python实现

    2021-06-10 00:20:41
  • SQL窗口函数之取值窗口函数的使用

    2024-01-27 06:53:37
  • python基础面试题整理

    2023-11-03 02:09:45
  • SQLSERVER的非聚集索引结构深度理解

    2024-01-20 01:54:00
  • 用python实现五子棋实例

    2022-08-23 21:42:38
  • python dlib人脸识别代码实例

    2021-04-05 12:57:33
  • python encode和decode的妙用

    2021-01-08 01:34:20
  • Python利用prettytable实现格式化输出内容

    2023-10-17 11:02:32
  • 使用Python实现分别输出每个数组

    2021-10-30 00:28:49
  • Navicat For MySQL的简单使用教程

    2024-01-17 12:22:02
  • python如何编写win程序

    2022-12-09 11:48:38
  • 浅谈Python 钉钉报警必备知识系统讲解

    2023-11-13 15:14:38
  • Pycharm远程调试和MySQL数据库授权问题

    2024-01-14 10:56:00
  • Python 格式化输出字符串的方法(输出字符串+数字的几种方法)

    2021-05-03 21:25:00
  • Python 微信公众号文章爬取的示例代码

    2021-11-06 22:39:20
  • GoFrame错误处理常用方法及错误码使用示例

    2024-04-25 15:30:35
  • Python数据可视化之简单折线图的绘制

    2021-05-25 11:59:30
  • 值得收藏的10道python 面试题

    2022-04-22 12:10:33
  • asp之家 网络编程 m.aspxhome.com