vue2+ts组件库搭建

背景

公司通用的框架是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

组件打包

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

  • demo示例项目
  • 组件
    demo示例项目使用了公司的isaac框架(对webpack封装)进行打包,组件打包则是使用了rollup。这里主要讲如何对rollup改造,让其支持ts打包。

支持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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容