使用 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实现pager控件示例
Apr 09 Javascript
jquery控制表单输入框显示默认值的方法
May 22 Javascript
jQuery实现的网格线绘制方法
Jun 20 Javascript
JavaScript中函数声明与函数表达式的区别详解
Aug 18 Javascript
bootstrap table操作技巧分享
Feb 15 Javascript
Javascript实现base64的加密解密方法示例
Jun 27 Javascript
JavaScript设计模式之原型模式分析【ES5与ES6】
Jul 26 Javascript
vue 循环加载数据并获取第一条记录的方法
Sep 26 Javascript
angular6的响应式表单的实现
Oct 10 Javascript
利用JS如何获取form表单数据
Dec 19 Javascript
js实现单元格拖拽效果
Feb 10 Javascript
JavaScript中继承原理与用法实例入门
May 09 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
php数组相加 array(“a”)+array(“b”)结果还是array(“a”)
2012/09/19 PHP
PHP 解决session死锁的方法
2013/06/20 PHP
PHP实现获取图片颜色值的方法
2014/07/11 PHP
PHP的new static和new self的区别与使用
2019/11/27 PHP
jquery选择器的选择使用及性能介绍
2013/01/16 Javascript
jQuery+css实现图片滚动效果(附源码)
2013/03/18 Javascript
解决js数据包含加号+通过ajax传到后台时出现连接错误
2013/08/01 Javascript
node.js实现多图片上传实例
2014/06/03 Javascript
jquery ajax请求方式与提示用户正在处理请稍等
2014/09/01 Javascript
JavaScript 异常处理 详解
2015/02/06 Javascript
JQuery页面地址处理插件jqURL详解
2015/05/03 Javascript
Node.js事件驱动
2015/06/18 Javascript
js立即执行函数: (function ( ){})( ) 与 (function ( ){}( )) 有什么区别?
2015/11/18 Javascript
js随机生成26个大小写字母
2016/02/12 Javascript
浅析Bootstrap缩略图组件与警示框组件
2016/04/29 Javascript
阻止表单提交按钮多次提交的完美解决方法
2016/05/16 Javascript
angular中的http拦截器Interceptors的实现
2017/02/21 Javascript
JavaScript中的工厂函数(推荐)
2017/03/08 Javascript
JavaScript闭包原理与用法学习笔记
2020/05/29 Javascript
为什么推荐使用JSX开发Vue3
2020/12/28 Vue.js
[02:51]2014DOTA2 TI小组赛总结中国军团全部进军钥匙球馆
2014/07/15 DOTA
Python的函数嵌套的使用方法
2014/01/24 Python
python通过socket实现多个连接并实现ssh功能详解
2017/11/08 Python
Python实现通讯录功能
2018/02/22 Python
Python将一个CSV文件里的数据追加到另一个CSV文件的方法
2018/07/04 Python
python实现飞机大战微信小游戏
2020/03/21 Python
浅析python3字符串格式化format()函数的简单用法
2018/12/07 Python
python,Django实现的淘宝客登录功能示例
2019/06/12 Python
Python爬取365好书中小说代码实例
2020/02/28 Python
Python GUI之tkinter窗口视窗教程大集合(推荐)
2020/10/20 Python
CSS3 实用技巧:实现黑白图像效果示例代码
2013/07/11 HTML / CSS
size?爱尔兰官方网站:英国伦敦的球鞋精品店
2019/03/31 全球购物
优秀德育工作者事迹材料
2014/05/07 职场文书
2014年政风行风评议工作总结
2014/10/21 职场文书
12.4全国法制宣传日活动总结
2014/11/01 职场文书
文明班级申报材料
2014/12/24 职场文书