从零开始基于@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.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

以上是关于从零开始基于@vue/cli4.5手把手搭建组件库的主要内容,如果未能解决你的问题,请参考以下文章

从零开始搭建自己的VueJS2.0+ElementUI单页面网站(环境搭建)

手把手教你从零开始搭建个人博客,20 分钟上手

手把手教你,从零开始实战搭建SpringCloud Alibaba!这份笔记太牛了!

手把手教你,从零开始实战搭建SpringCloud Alibaba!这份笔记太牛了!

手把手教你,从零开始实战搭建SpringCloud Alibaba!这份笔记太牛了!

手把手教你,从零开始搭建Spring Cloud Alibaba!这份笔记太牛了