本文旨在給大家提供一種構建一個完整 UI 庫腳手架的思路:包括如何快速并優雅地構建UI庫的主頁、如何托管主頁、如何編寫腳本提升自己的開發效率、如何生成 CHANGELOG 等
前言
主流的開源 UI 庫代碼結構主要分為三大部分:
編寫此博文的靈感 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 構建自定義組件庫的方法,希望對大家有所幫助,如果大家有任何疑問歡迎給我留言,小編會及時回復大家的!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。