共计 10371 个字符,预计需要花费 26 分钟才能阅读完成。
从零开始基于 @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