使用 Vue cli 3.0 构建自定义组件库的方法
本文旨在给大家提供一种构建一个完整 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 构建自定义组件库的方法,希望对大家有所帮助
推荐阅读
-
使用 Vue cli 3.0 构建自定义组件库的方法
-
vue cli3.0结合echarts3.0与地图的使用方法示例
-
vue在自定义组件中使用v-model进行数据绑定的方法
-
Angular使用cli生成自定义文件、组件的方法
-
vue在自定义组件中使用v-model进行数据绑定的方法
-
使用vue-cli导入Element UI组件的方法
-
在vue-cli的组件模板里使用font-awesome的两种方法
-
vue-cli3.0+element-ui上传组件el-upload的使用
-
Angular使用cli生成自定义文件、组件的方法
-
在 vue-cli v3.0 中使用 SCSS/SASS的方法