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


Posted in Javascript onApril 30, 2019

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

Javascript 相关文章推荐
简单的jquery拖拽排序效果实现代码
Sep 20 Javascript
js仿百度有啊通栏展示效果实现代码
May 28 Javascript
jQuery实现的进度条效果
Jul 15 Javascript
一波JavaScript日期判断脚本分享
Mar 06 Javascript
有关jQuery中parent()和siblings()的小问题
Jun 01 Javascript
jQuery新窗口打开外链接
Jul 21 Javascript
基于Bootstrap的Metronic框架实现条码和二维码的生成及打印处理操作
Aug 29 Javascript
jQuery Ajax传值到Servlet出现乱码问题的解决方法
Oct 09 Javascript
让微信小程序支持ES6中Promise特性的方法详解
Jun 13 Javascript
使用Vue.js中的过滤器实现幂方求值的方法
Aug 27 Javascript
node.JS的crypto加密模块使用方法详解(MD5,AES,Hmac,Diffie-Hellman加密)
Feb 06 Javascript
vue实现购物车结算功能
Jun 18 Javascript
vue自动路由-单页面项目(非build时构建)
Apr 30 #Javascript
vue-router 前端路由之路由传值的方式详解
Apr 30 #Javascript
微信小程序页面间传值与页面取值操作实例分析
Apr 30 #Javascript
vue2.0基于vue-cli+element-ui制作树形treeTable
Apr 30 #Javascript
微信小程序常用赋值方法小结
Apr 30 #Javascript
微信小程序实现同一页面取值的方法分析
Apr 30 #Javascript
一百行JS代码实现一个校验工具
Apr 30 #Javascript
You might like
动态生成gif格式的图像要注意?
2006/10/09 PHP
PHP5中的时间相差8小时的解决办法
2008/03/28 PHP
php编程实现获取excel文档内容的代码实例
2011/06/28 PHP
CodeIgniter控制器之业务逻辑实例分析
2016/01/20 PHP
PHP编程计算两个时间段是否有交集的实现方法(不算边界重叠)
2017/05/30 PHP
php7安装yar扩展的方法详解
2017/08/03 PHP
JavaScript入门教程(7) History历史对象
2009/01/31 Javascript
javascript学习笔记(七) js函数介绍
2012/06/19 Javascript
JS实现图片的不间断连续滚动的简单实例
2016/06/03 Javascript
JavaScript_object基础入门(必看篇)
2016/06/13 Javascript
Bootstrap源码解读标签、徽章、缩略图和警示框(8)
2016/12/26 Javascript
nodejs动态创建二维码的方法
2017/08/12 NodeJs
使用JS动态显示文本
2017/09/09 Javascript
electron-vue利用webpack打包实现多页面的入口文件问题
2019/05/12 Javascript
Node.js学习教程之Module模块
2019/09/03 Javascript
js实现盒子拖拽动画效果
2020/08/09 Javascript
[02:17]快乐加倍!DOTA2食人魔魔法师至宝+迎霜节活动上线
2019/12/22 DOTA
跟老齐学Python之关于类的初步认识
2014/10/11 Python
python使用matplotlib画饼状图
2018/09/25 Python
python实现名片管理系统项目
2019/04/26 Python
Python基于pip实现离线打包过程详解
2020/05/15 Python
Python Matplotlib绘图基础知识代码解析
2020/08/31 Python
英国女性时尚品牌:Apricot
2018/12/04 全球购物
adidas泰国官网:adidas TH
2020/07/11 全球购物
如何实现一个自定义类的序列化
2012/05/22 面试题
电大物流学生的自我评价
2013/10/25 职场文书
户外婚礼策划方案
2014/02/08 职场文书
会计专业自荐信
2014/06/03 职场文书
党的群众路线教育实践活动总结报告
2014/07/03 职场文书
2014年平安夜寄语
2014/12/08 职场文书
2015年世界粮食日演讲稿
2015/03/20 职场文书
保送生自荐信范文
2015/03/26 职场文书
2015社区六五普法工作总结
2015/04/21 职场文书
2015年家长学校工作总结
2015/04/22 职场文书
Java集成swagger文档组件
2021/06/28 Java/Android
Python中递归以及递归遍历目录详解
2021/10/24 Python