关于javascript:从零开始基于vuecli45手把手搭建组件库

5次阅读

共计 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.vuesrc/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
正文完
 0