使用 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('https://www.jb51.net/')) { const inputArr = inputName.split('https://www.jb51.net/') 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> ${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}) } } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/931e5d7e1ef8f6091cdd3aafc446fbd7.html