从零开始基于@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-demonpm 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'@Componentexport 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 = falseVue.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