背景
公司通用的框架是vue2+js,组件库也是基于vue+js的。我们组内根据自己的业务特点,使用了vue2+ts框架,这样导致我们可以使用公司的组件库,但是基于这个框架写的一些组件却无法直接放到公司的组件库。
这个框架使用已有半年,期间沉淀了一些业务组件,都是零零散散分散在各个项目里面,便有了搭建一套基于vue2+ts的组件库把这些组件沉淀出来的想法。这个组件库参考lego、eden、dragon等已有组件库。
改造目标
组件库包含下面几个部分
- demo示例展示
这一块需要使用vue2+ts重构,要提供无缝引用ts组件的功能 - 文档提取
要制定一套适用于vue-property-decorator语法的文档注释提取规则 - 组件打包
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属性提取为例,讲述文档提取是如何处理的。
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的代码如下所示
使用正则截取之间的字符串。
content.replace(/([\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提取出来,复杂数据类型采用了一个取巧的方式直接截取字符串。
最终效果:
组件打包
对于一个组件库来说包含两部分:
- demo示例项目
- 组件
demo示例项目使用了公司的isaac框架(对webpack封装)进行打包,组件打包则是使用了rollup。这里主要讲如何对rollup改造,让其支持ts打包。
支持vue + ts
- 对vue-component-builder工具进行功能扩展
vue-component-builder工具,里面封装了rollup打包脚本,可以在具体项目中配置一些参数,比如入口、出口。需要对其进行修改,增加自定义配置插件功能。
...(opts.plugins? opts.plugins : []),
更新:由于vue-component-builder做了很多对以前项目的兼容,为了降低其维护成本,也让saber组件库更具有可定制性,放弃对vue-component-builder的功能扩展,直接把这个脚本放在saber项目中进行改造。
- 使用rollup-plugin-typescript2插件
这个插件为了处理ts,以及解决自动生成声明文件的问题。
组件打包使用了vue-component-builder工具,里面封装了rollup打包和webpack打包,这个工具是针对vue+js打包的,需要对它进行扩展。
typescript({
tsconfig: './tsconfigComponent.json',
useTsconfigDeclarationDir: true,
}),
- tsconfigComponent.json增加文件声明配置
"declaration": true,
"declarationDir": "./@types",
package.json中入口文件配置也需要配置
"main": "cjs/index.js",
"module": "esm/index.js",
"types": "@types/index.d.ts",
- 只针对组件所在的目录使用ts编译
"include": [
"src/components/**/*.ts",
"src/components/**/*.vue",
]
我们的组件都在src/components/这一个目录里面,不需要将宿主项目中的一些文件页打包进来
参考:
https://juejin.cn/post/6899256692615413767