vue

vue2+ts组件库搭建

2021-07-18  本文已影响0人  hello_小丁同学

背景

公司通用的框架是vue2+js,组件库也是基于vue+js的。我们组内根据自己的业务特点,使用了vue2+ts框架,这样导致我们可以使用公司的组件库,但是基于这个框架写的一些组件却无法直接放到公司的组件库。
这个框架使用已有半年,期间沉淀了一些业务组件,都是零零散散分散在各个项目里面,便有了搭建一套基于vue2+ts的组件库把这些组件沉淀出来的想法。这个组件库参考lego、eden、dragon等已有组件库。

改造目标

组件库包含下面几个部分

  1. demo示例展示
    这一块需要使用vue2+ts重构,要提供无缝引用ts组件的功能
  2. 文档提取
    要制定一套适用于vue-property-decorator语法的文档注释提取规则
  3. 组件打包
    1、2两点都是解决如何生成文档网站可以预览组件,同时还需要打包成npm包供其他项目使用、当然也要支持按需引入

遇到的问题

demo展示

一 无法识别json模块

webpack已经添加了对应的loader,需要在tsconfig.json添加

{
"compilerOptions": {
    "resolveJsonModule": true,
}
}

二无法识别install属性

import BaseText from './base-text.vue'

BaseText.install = function (Vue: any) {
  Vue.component(BaseText.name, BaseText)
}

export { BaseText }

采用插件的方式引入component,提示

TS2339: Property 'install' does not exist on type 'typeof BaseText'.

原因是VueConstructor没有定义这个属性,解决办法是在shims-vue.d.ts中声明这个属性

declare module "vue/types/vue" {
  interface VueConstructor {
    install: any
  }
}

文档提取

以prop属性提取为例,讲述文档提取是如何处理的。

<script>
import './base-text.scss'

export default {
  name: 'ed-base-text',

  // __PROPS_START 到 __PROPS_END 之间的属性会自动提取出来显示在文档的最下面
  props: {
    // __PROPS_START
    /**
      * @title 内容
      * @type richtext
      * @description 容器自定义样式class
      */
    text: {
      type: String,
      default: '还没有内容哦',
    },
    // __PROPS_END
  },
}
</script>

eden的处理思路是,自定义一个webpack loader,通过正则表达式,截取// __PROPS_START和// __PROPS_END之间的字符串,然后通过@babel/parse转化为抽象语法树。
解析的核心代码:

const parser = require('@babel/parser')
code = `var props = {${code}}`
    const ast = parser.parse(code)

我们采用vue-property-decorator的代码如下所示

<script lang="ts">

import './tangguo-emoji.scss'
import { Component, Vue, Prop, Emit } from 'vue-property-decorator'
import emojiData from './const.json'

export interface IEmojiRenderData {
  type: string
  data: string
  emojiString?: string
}

@Component({
  name: 'tangguo-emoji',
})
export default class TangguoEmoji extends Vue {
  /**
 * @description 内容
 */
  @Prop({ type: String, default: '一条大河' }) readonly content!: string

  /**
   * @description 发送消息
   */
  @Emit()
  sendMessage() {
    console.log('发送消息')
  }

  /**
   * @description 公共方法再见
   */
  public sayHello(name: string): string {
    this.doSomething(name)
    return ''
  }

  private doSomething(name: string) {
    console.log(name)
  }
}
</script>

使用正则截取<script></script>之间的字符串。

  content.replace(/<script.*?>([\s\S]*?)<\/script>/g, (r, code) => {
    const res = parseScriptCode(code)

    Object.assign(events, escapeArrow(res.events))
    Object.assign(methods, escapeArrow(res.methods))
    addProps(res.props)
  })

对上面的字符串进行ast解析的时候,需要增加ts和decorator配置

const { parse } = require('@babel/parser')
    const ast = parse(code, {
      plugins: ['decorators-legacy','typescript'],
      sourceType: "module",
    })

对ast遍历的用到@babel/traverse,,在遍历之前,可以用先把整个ast提取出来,了解其大概的数据结构,也可以使用在线工具AST Explorer来分析

require('fs').writeFileSync('./ast.json', JSON.stringify(ast, null, 2))

同时对Prop,公共方法,事件进行提取

traverse(ast, {
      ClassProperty(path) {
        try {
          const classPropertyNode = path.node
          const decorators = classPropertyNode.decorators
          if (decorators && decorators.length === 1) {
            const currentDecorator = decorators[0].expression
            // 找到注解为Prop节点,提取出来作为文档信息
            if (currentDecorator.callee.name === 'Prop') {
              let prop = {}
              prop.key = classPropertyNode.key.name
              // 提取修饰器中的内容
              if (currentDecorator.arguments && currentDecorator.arguments.length === 1) {
                const properties = currentDecorator.arguments[0].properties
                // parsePropValue(properties.value, prop, code)
                properties && properties.forEach(item =>{
                  if (t.isIdentifier(item.value)){
                    prop[item.key.name] = item.value.name
                  } else if (t.isBooleanLiteral(item.value) || t.isNumericLiteral(item.value) || t.isStringLiteral(item.value)){
                    prop[item.key.name] = item.value.value
                  } else {
                    prop[item.key.name] = code.substring(item.value.start, item.value.end)
                  }
                })
              }
              // 提取文档注释中的内容
              parseCommentBlock(classPropertyNode.leadingComments, prop)
              res.props.push(prop)
            }
          }
        } catch (e) {
          warn(`解析代码失败:${e.stack || e.message || e}`)
        }
      },
      ClassMethod(path) {
        try {
          const classMethodNode = path.node
          const decorators = classMethodNode.decorators
          // 解析emit事件
          if (decorators && decorators.length === 1) {
            const event = {}
            const currentDecorator = decorators[0].expression
            // emit事件
            if (currentDecorator.callee.name === 'Emit'){
              event.name = classMethodNode.key.name
               if (currentDecorator.arguments && currentDecorator.arguments.length === 1) {
                 event.name = currentDecorator.arguments[0].value
               }
              parseCommentBlock(classMethodNode.leadingComments, event)
              // 形参若是存在则取形参作用emit事件payload
              if (classMethodNode.params && classMethodNode.params.length > 0) {
                const paramsNode = classMethodNode.params[0]
                event.payload = code.substring(paramsNode.start, paramsNode.end)
              }
               // 若有返回值,则取返回值作为payload
               if (classMethodNode.returnType) {
                 event.payload = code.substring(classMethodNode.returnType.start+1, classMethodNode.returnType.end)
               }
               res.events.push(event)
            }
          } else if (!decorators) {
            // 解析普通方法,规则是不含注解,且显示声明为public
            if (classMethodNode.accessibility && classMethodNode.accessibility === 'public') {
              const method = {}
              method.name = classMethodNode.key.name
              parseCommentBlock(classMethodNode.leadingComments, method)
              method.params = classMethodNode.params && classMethodNode.params.map(param => code.substring(param.start, param.end)).join('\n')
              method.returns = classMethodNode.returnType && code.substring(classMethodNode.returnType.start+1, classMethodNode.returnType.end)
              res.methods.push(method)
            }
          }

        } catch (e) {
          warn(`解析代码失败:${e.stack || e.message || e}`)
        }
      }
    });

这里面有个注意点,复杂数据类型比如function的提取与基本数据类型不同,基本数据类型可以直接使用node.value提取出来,复杂数据类型采用了一个取巧的方式直接截取字符串。
最终效果:

image.png

组件打包

对于一个组件库来说包含两部分:

支持vue + ts

  1. 对vue-component-builder工具进行功能扩展
    vue-component-builder工具,里面封装了rollup打包脚本,可以在具体项目中配置一些参数,比如入口、出口。需要对其进行修改,增加自定义配置插件功能。
...(opts.plugins? opts.plugins : []),

更新:由于vue-component-builder做了很多对以前项目的兼容,为了降低其维护成本,也让saber组件库更具有可定制性,放弃对vue-component-builder的功能扩展,直接把这个脚本放在saber项目中进行改造。

  1. 使用rollup-plugin-typescript2插件
    这个插件为了处理ts,以及解决自动生成声明文件的问题。
    组件打包使用了vue-component-builder工具,里面封装了rollup打包和webpack打包,这个工具是针对vue+js打包的,需要对它进行扩展。
     typescript({
      tsconfig: './tsconfigComponent.json',
      useTsconfigDeclarationDir: true,
    }),
  1. tsconfigComponent.json增加文件声明配置
        "declaration": true,
        "declarationDir": "./@types",

package.json中入口文件配置也需要配置

  "main": "cjs/index.js",
  "module": "esm/index.js",
  "types": "@types/index.d.ts",
  1. 只针对组件所在的目录使用ts编译
    "include": [
        "src/components/**/*.ts",
        "src/components/**/*.vue",
    ]

我们的组件都在src/components/这一个目录里面,不需要将宿主项目中的一些文件页打包进来

参考:
https://juejin.cn/post/6899256692615413767

上一篇下一篇

猜你喜欢

热点阅读