从零开始基于@vue/cli4.5手把手搭建组件库
Posted 。。
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始基于@vue/cli4.5手把手搭建组件库相关的知识,希望对你有一定的参考价值。
从零开始基于@vue/cli4.5手把手搭建组件库
1. 预期功能:
- 支持按需加载/全量加载
- 按目录结构自动注册组件
- 快速打包发布
- 同时支持使用JS/TS写组件
- 支持预览测试
2. 搭建@vue/cli4.5
安装:
npm install -g @vue/cli // 全局安装,使用最新的即可,目前最新是4.5.1
新建:
vue create components-library-demo
配置:
? Please pick a preset: Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) > Manually select features // 手动选择 ? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection) // 根据自身需要 >(*) Choose Vue version (*) Babel >(*) TypeScript ( ) Progressive Web App (PWA) Support ( ) Router ( ) Vuex (*) CSS Pre-processors (*) Linter / Formatter (*) Unit Testing ( ) E2E Testing ? Choose a version of Vue.js that you want to start the project with > 2.x 3.x (Preview) ? Use class-style component syntax? Yes ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass) > Sass/SCSS (with node-sass) Less Stylus ? Pick a linter / formatter config: ESLint with error prevention only ESLint + Airbnb config > ESLint + Standard config ESLint + Prettier TSLint (deprecated) ? Pick additional lint features: Lint on save ? Pick a unit testing solution: Mocha + Chai > Jest ? Where do you prefer placing config for Babel, ESLint, etc.? > In dedicated config files In package.json ? Save this as a preset for future projects? No
运行
cd components-library-demo npm install vue-property-decorator // 支持TS装饰器语法 npm run serve
3. 组件自动注册:
在
src
目录下新建index.js
, 内容写入:(实现自动读取src/components下的所有文件夹下的index.vue根据文件名自动进行组件的注册)const requireComponent = require.context( \'./components\', true, /\\w+\\.vue$/ ) const list = requireComponent.keys().filter(item => { return item.endsWith(\'index.vue\') }) const componentsObj = {} const componentsList = list.map((file) => { requireComponent(file).default.__file = file const fileList = file.split(\'/\') const defaultComponent = requireComponent(file).default componentsObj[fileList[fileList.length - 2]] = defaultComponent return defaultComponent }) const install = (Vue) => { componentsList.forEach((item) => { const fileList = item.__file.split(\'/\') const name = fileList[fileList.length - 2] Vue.component(name, item) }) } if (typeof window !== \'undefined\' && window.Vue) { window.Vue.use(install) } const exportObj = { install, ...componentsObj } export default exportObj
4. 增加测试组件:
在
src/components
下新增两个组件(分别使用js和ts写两个组件):// src/components/CldTest/index.vue <template> <div class="red">{{ test }}</div> </template> <script> export default { data () { return { test: \'JS测试组件\' } } } </script> <style lang="scss" scoped> .red { color: red; } </style>
// src/components/CldTsTest/index.vue
<template>
<div class="green">{{ test }}</div>
</template>
<script lang="ts">
import { Vue, Component } from \'vue-property-decorator\'
@Component
export default class CldTsTest extends Vue {
public test = \'TS测试组件\'
}
</script>
<style lang="scss" scoped>
.green {
color: green;
}
</style>
5. 目录调整:
- 移除
src/app.vue
和src/components/HelloWorld.vue
新建一个
examples
文件夹,用于组件库组件预览展示。新建一个
examples/mian.js
, 内容:import Vue from \'vue\' import Index from \'./index.vue\' import components from \'../src\' Vue.config.productionTip = false Vue.use(components) new Vue({ render: h => h(Index) }).$mount(\'#app\')
新建一个
examples/index.vue
, 内容:<template> <div> <example-show v-for="(item, index) in exampleList" :key="index" :name="item"></example-show> </div> </template> <script> import ExampleShow from \'./example/show\' export default { data () { return { exampleList: [\'CldTest\', \'CldTsTest\'] } }, components: { ExampleShow } } </script> <style lang="scss"> body, html { margin: 0px; padding: 0; } </style>
新增
examples/index.html
, 内容:<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript> <strong>We\'re sorry but <%= htmlWebpackPlugin.options.title %> doesn\'t work properly without javascript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
新增一个
examples/example
文件夹用于存放组件使用示例文件:新增一个
examples/example/index.js
, 用于自动注册使用示例文件:const requireComponent = require.context( \'../example\', true, /\\w+\\.vue$/ ) const examples = {} requireComponent.keys().forEach((file) => { const name = `Example${file.replace(\'.vue\', \'\').replace(\'./\', \'\')}` examples[name] = requireComponent(file).default }) export default examples
- 新增一个`examples/example/show.js`, 用于使用示例的渲染:
```
import ExampleComponents from \'./index\'
export default {
components: {
...ExampleComponents
},
name: \'ExampleShow\',
props: [\'name\'],
render (h) {
return h(
`example-${this.name}`
)
}
}
```
- 新增`examples/example/CldTest.vue`文件,调用CldTest组件
```
<template>
<cld-test/>
<template>
```
- 新增`examples/example/CldTsTest.vue`文件,调用CldTsTest组件
```
<template>
<cld-ts-test/>
</template>
```
根目录新增
vue.config.js
,修改webpack配置, 将入口改为examples:module.exports = { pages: { index: { entry: \'examples/main.js\', template: \'examples/index.html\', filename: \'index.html\' } }, productionSourceMap: false }
完成这些配置之后启动项目即可看到当前两个组件的使用示例效果
6. 打包调整:
package.json
修改:修改"private" 为false, "license" 为 "UNLICENSED", "main" 为 "lib/index.js", "style" 为 "lib/theme/index.css",将build打包语句修改为"cross-env rimraf ./lib && node ./build/build.js"(先删除lib文件夹,再执行build文件夹下的build.js文件)
{ "name": "components-library-demo", "version": "0.1.0", "description": "一个组件库搭建示例", "private": false, "license": "UNLICENSED", "main": "lib/index.js", "style": "lib/theme/index.css", "scripts": { "serve": "vue-cli-service serve", "build": "cross-env rimraf ./lib && node ./build/build.js", "test:unit": "vue-cli-service test:unit", "lint": "vue-cli-service lint" }, ..... }
新建
build
文件夹,用于存放打包相关文件:先执行:
npm install cross-env runjs
新建一个
build/build.js
,该文件为npm run build
执行的入口文件,内容:/* eslint-disable @typescript-eslint/no-var-requires */ const fs = require(\'fs\') const path = require(\'path\') const { run } = require(\'runjs\') const rimraf = require(\'rimraf\') const componentsUtils = require(\'./utils/componentsUtils\') componentsUtils() const componentsJson = require(\'../components.json\') const { getAssetsPath, chalkConsole, resolve, fsExistsSync, move, fileDisplay } = require(\'./utils\') const styleOutputPath = \'theme\' const whiteList = [\'index\', \'base\'] const cssFiles = [] function build ({ input, output } = {}, index, arr) { chalkConsole.building(index + 1, arr.length) run( `vue-cli-service build --target lib --no-clean --name ${output} --dest ${getAssetsPath()} ${input}` ) cssFiles.push(`${output}.css`) // 删除组件index.js文件 !whiteList.includes(output) && fs.unlinkSync(input) } const pkg = [] Object.keys(componentsJson).forEach((moduleName) => { const component = componentsJson[moduleName] const input = whiteList.includes(moduleName) ? component : `${component.slice(2)}/index.js` const basename = path.basename(component) const output = basename === \'src\' ? \'index\' : moduleName pkg.push({ input, output }) }) pkg.forEach(build) // 删除多余文件 rimraf(getAssetsPath(\'./demo.html\'), () => {}) // 创建样式文件夹 fs.mkdirSync(getAssetsPath(styleOutputPath)) // 拷贝css文件到单独目录 fs.writeFileSync(`${getAssetsPath(styleOutputPath)}/base.css`, \'\') cssFiles.forEach((cssFile) => { const fileUrl = getAssetsPath(styleOutputPath + \'/\' + cssFile) if (fsExistsSync(getAssetsPath(cssFile))) { move(getAssetsPath(cssFile), fileUrl) } else { fs.writeFileSync(fileUrl, \'\') // 不存在css时补css } }) rimraf(getAssetsPath(\'./base.js\'), () => {}) rimraf(getAssetsPath(\'./base.umd.js\'), () => {}) rimraf(getAssetsPath(\'./base.umd.min.js\'), () => {}) // 重命名common文件 fileDisplay(getAssetsPath(), (file) => { const reg = /.common/ if (reg.test(file)) { file = `../${file}` move(resolve(file), resolve(file.replace(reg, \'\'))) } }) chalkConsole.success()
新增一个
build/utils/componentsUtils.js
, 该文件在将实现src/components下的每一个每一个组件文件夹新增一个index.js, 注入单组件注册内容,为组件库的按需加载做准备, 并在根文件生成一个components.json,用于打包入口的记录:const fn = () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const fs = require(\'fs\') const __dir = \'./src/components\' const dir = fs.readdirSync(__dir) // eslint-disable-next-line @typescript-eslint/no-var-requires const path = require(\'path\') const changePath = (file) => { // 对路径进行转化处理 let re = file if (file.indexOf(\'..\') === 0) { re = file.replace(\'..\', \'.\') } re = re.replace(\'\\\\\', \'/\').replace(\'\\\\\', \'/\').replace(\'\\\\\', \'/\') return `./${re}` } const components = {} components.index = \'./src/index.js\' const fileNameToLowerCase = (fileName) => { const re = fileName.replace(/([A-Z])/g, \'-$1\').toLowerCase() return re[0] === \'-\' ? re.slice(1) : re } // const commonImport = fs.readFileSync(\'./src/common.js\') dir.forEach((fileName) => { const filePath = path.join(__dir, `/${fileName}`) const indexPath = path.join(filePath, \'/index.vue\') const hasIndex = fs.existsSync(indexPath) if (!hasIndex) { console.error(`error: ${filePath}文件夹不存在index.vue文件, 无法打包`) return } components[fileNameToLowerCase(fileName)] = changePath(filePath) // 生成一个多入口对象 const indexContent = ` import Component from \'./index.vue\' Component.install = (Vue) => { Vue.component(\'${fileName}\', Component) } export default Component ` fs.writeFileSync(path.join(filePath, \'/index.js\'), indexContent) // 为src/components下的每一个文件夹注入一个index.js文件并写入以上内容 }) delete components.app fs.writeFileSync(\'./components.json\', JSON.stringify(components, null, 2)) } module.exports = fn
新增一个
`build/utils.js
存放打包时需要的各种工具:/* eslint-disable @typescript-eslint/no-var-requires */ const path = require(\'path\') const fs = require(\'fs\') const outputPath = \'lib\' const chalk = require(\'chalk\') module.exports = { getAssetsPath (_path = \'.\') { // 获取资源路径 return path.posix.join(outputPath, _path) }, resolve (_path) { // 进入路径 return _path ? path.resolve(__dirname, _path) : path.resolve(__dirname, \'..\', outputPath) }, isProduct: [\'production\', \'prod\'].includes(process.env.NODE_ENV), env: process.env.NODE_ENV, chalkConsole: { // 打印内容 success: () => { console.log(chalk.green(\'=========================================\')) console.log(chalk.green(\'========打包成功(build success)!=========\')) console.log(chalk.green(\'=========================================\')) }, building: (index, total) => { console.log(chalk.blue(`正在打包第${index}/${total}个文件...`)) } }, fsExistsSync: (_path) => { try { fs.accessSync(_path, fs.F_OK) } catch (e) { return false } return true }, move: (origin, target) => { const resolve = (dir) => path.resolve(__dirname, \'..\', dir) fs.rename(resolve(origin), resolve(target), function (err) { if (err) { throw err } }) }, fileDisplay: function fileDisplay (filePath, callback) { // 递归文件夹 // 根据文件路径读取文件,返回文件列表 fs.readdir(filePath, (err, files) => { if (!err) { // 遍历读取到的文件列表 files.forEach((filename) => { // 获取当前文件的绝对路径 const fileDir = path.join(filePath, filename) // 根据文件路径获取文件信息,返回一个fs.Stats对象 fs.stat(fileDir, (error, stats) => { if (!error) { const isFile = stats.isFile() // 是文件 const isDir = stats.isDirectory() // 是文件夹 isFile ? callback(fileDir) : fileDisplay(fileDir, callback) // 递归,如果是文件夹,就继续遍历该文件夹下面的文件 } }) }) } }) } }
7. 打包:
执行:
npm run build
在打包成功打印打包成功(build success)!后
lib文件夹下生成
├── theme 样式文件存放
├── base.css 通用样式,该文件缺少在babel-plugin-component按需加载中会报错
├── cld-test.css 组件cld-test的样式
├── cld-ts-test.css 组件cld--ts-test的样式
├── index.css 组件库所有的样式(全量引入时将使用该文件)
├── cld-test.js 组件cld-test的代码
├── cld-test.umd.js
├── cld-test.umd.min.js
├── cld-ts-test.js 组件cld-ts-test的代码
├── cld-ts-test.umd.js
├── cld-ts-test.umd.min.js
├── index.js 组件库所有组件的代码(全量引入时将使用该文件)
├── index.umd.js
├── index.umd.min.js
以上是关于从零开始基于@vue/cli4.5手把手搭建组件库的主要内容,如果未能解决你的问题,请参考以下文章
从零开始搭建自己的VueJS2.0+ElementUI单页面网站(环境搭建)
手把手教你,从零开始实战搭建SpringCloud Alibaba!这份笔记太牛了!
手把手教你,从零开始实战搭建SpringCloud Alibaba!这份笔记太牛了!