使用 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前端技巧收集
Dec 24 Javascript
js实现Select列表各项上移和下移的方法
Aug 14 Javascript
jQuery ajax方法传递中文时出现中文乱码的解决方法
Jul 25 Javascript
原生js实现键盘控制div移动且解决停顿问题
Dec 05 Javascript
jQuery实现获取隐藏div高度的方法示例
Feb 09 Javascript
原生js实现移动端触摸轮播的示例代码
Dec 22 Javascript
玩转vue的slot内容分发
Sep 22 Javascript
微信小程序调用天气接口并且渲染在页面过程详解
Jun 24 Javascript
layui按条件隐藏表格列的实例
Sep 19 Javascript
解决layer.confirm快速点击会重复触发事件的问题
Sep 23 Javascript
Vue快速实现通用表单验证的方法
Feb 24 Javascript
JavaScript获取时区实现过程解析
Sep 24 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
PHP5.3.1 不再支持ISAPI
2010/01/08 PHP
php中使用explode查找某个字符是否存在的方法
2011/07/12 PHP
微信 getAccessToken方法详解及实例
2016/11/23 PHP
PHP开发的微信现金红包功能示例
2017/06/29 PHP
ECMAScript 创建自己的js类库
2012/11/22 Javascript
js隐藏与显示回到顶部按钮及window.onscroll事件应用
2013/01/25 Javascript
js判断undefined类型,undefined,null, 的区别详细解析
2013/12/16 Javascript
使用jQuery实现input数值增量和减量的方法
2015/01/24 Javascript
JavaScript获取数组最小值和最大值的方法
2015/06/09 Javascript
javascript实现手机震动API代码
2015/08/05 Javascript
全面解析jQuery $(document).ready()和JavaScript onload事件
2016/06/08 Javascript
jQuery插入节点和移动节点用法示例(insertAfter、insertBefore方法)
2016/09/08 Javascript
正则表达式替换html元素属性的方法
2016/11/26 Javascript
jquery删除数组中重复元素
2016/12/05 Javascript
vue.js 1.x与2.0中js实时监听input值的变化
2017/03/15 Javascript
Webpack实现按需打包Lodash的几种方法详解
2017/05/08 Javascript
JavaScript中使用参数个数实现重载功能
2017/09/01 Javascript
用 Vue.js 递归组件实现可折叠的树形菜单(demo)
2017/12/25 Javascript
JS求Number类型数组中最大元素方法
2018/04/08 Javascript
Js面试算法详解
2018/04/08 Javascript
JavaScript函数式编程(Functional Programming)箭头函数(Arrow functions)用法分析
2019/05/22 Javascript
antd vue table跨行合并单元格,并且自定义内容实例
2020/10/28 Javascript
使用Python3 编写简单信用卡管理程序
2016/12/21 Python
解决在Python编辑器pycharm中程序run正常debug错误的问题
2019/01/17 Python
python通过robert、sobel、Laplace算子实现图像边缘提取详解
2019/08/21 Python
Python使用matplotlib绘制三维参数曲线操作示例
2019/09/10 Python
Django自定义用户表+自定义admin后台中的字段实例
2019/11/18 Python
Tensorflow累加的实现案例
2020/02/05 Python
Pycharm新手使用教程(图文详解)
2020/09/17 Python
python实现PolynomialFeatures多项式的方法
2021/01/06 Python
什么是托管函数?托管函数有什么用?
2014/06/15 面试题
自我鉴定怎么写
2013/12/05 职场文书
毕业寄语大全
2014/04/09 职场文书
ktv周年庆活动方案
2014/08/18 职场文书
《平移和旋转》教学反思
2016/02/19 职场文书
解决jupyter notebook图片显示模糊和保存清晰图片的操作
2021/04/24 Python