Vue从js迁移到ts

项目整理中,完成后补上Github链接

Typescript正广泛成为前端工程师开发项目的首选,我手头上有一些使用js编写的Vue项目,最近准备使用ts重写。项目中单单是页面的数量就超过100个,更不用提组件的数量,如果对这么多Vue文件进行一一重写的话,工程量浩大,并且十分枯燥。其实在此之前也手动转换过几个项目,发现转换过程大多都是重复劳动,是有可能通过程序实现自动转换。当然从js转换成ts,不可避免地会出现类型问题,所以只要求完成重复性地工作,当真的需要类型信息时,还是需要手动处理。

使用ts来编写项目时,可以使用两种不同的代码风格:

  1. 使用Vue.extend方法实现。
  2. 使用class语法配合vue-property-decorator实现。

具体应该选择哪种方案,见仁见智。我所采用的是方法2。为什么选择它,如果使用方法1的话重写起来岂不是很方便?选择方法2是因为在Vue中大量使用this关键字,使用class形式更加符合直觉——所有的内容都是在class实例上(其实可能只是我比较喜欢折腾( ´・ᴗ・` )。

实现思路

实现思路就和把大象装进冰箱一样简单:

  1. 将旧代码转换成AST(Abstract Syntax Tree, 抽象语法树)。
  2. 将AST修改成class形式。(类型信息自然没法全部填上,可以先用any或者选择不填写)
  3. 将AST转换成新代码。

关于什么是抽象语法树,可以在网上查找相关资料详细了解(我觉得对于抽象语法树有一定的了解是很有必要的)。简单来说,js代码可以用一个树形结构表示,这个树形结构就是抽象语法树。例如:

function foo() {
    return a + b;
}

对应的AST可能是下图这样的,一个简单的树形结构(当然这里做了很大程度的简化,实际上要复杂地多)。

image

如果希望将代码中的b修改为c,那么只需要修改树中的节点就可以,例如这样:

image

之后再用修改后的AST生成代码就可以了。

代码和AST的转换

recast是一个可以方便对代码和AST进行转换的库,可以帮我们打开冰箱门和关上冰箱门。

这里必须再提到两个概念,分别是estreeast-types

estree是将js代码解析成AST的一个社区标准,也就是,最终生成的AST节点中有哪些值,目前基本上都应该参照estree中的说明进行实现。对这个标准有一些的了解,或者说对于编译原理有一定的了解,可以提高之后修改代码的效率。

ast-types是recast中所使用的库,提供了语法树节点定义、遍历等功能。ast-types中所定义的类型兼容estree,但实际使用中,感觉有时会有一些缺失,例如在某些情况下,会存在decorators字段不存在的情况,可以通过d.ts文件对ast-types中的类型定义进行扩展。

如果对于编译原理了解的不是那么清楚的话,那么也可以通过recast.parse一些代码,来了解应该如何写,之后依葫芦画瓢编写代码就可以。

以下是一段代码示例:

// 操作AST中的一些节点
import {
    builders as b,
} from 'ast-types';

const clazz = b.classDeclaration(
    b.identifier(camelCaseWithFirstLetter('MyComponent')),
    b.classBody([]),
    b.identifier('Vue')
);
clazz.decorators = [
    b.decorator(
        b.callExpression(
            b.identifier('Component'),
            [
                b.objectExpression([
                    b.property('init', b.identifier('name'), b.literal('MyComponent')),
                ],
            ],
        )
    )
];
// 上个代码片段所对应的代码
@Component({
    name: 'MyComponent',
})
class MyComponent extends Vue {}

选择parser

在recast.parse解析代码时,会默认使用esprima来进行语法解析,esprima(目前为4.0.1版本)对js新语法已经有了较多的支持,但是对于目前的项目中说,还是有部分语法无法解析。为了解决这个问题,recast也可以自定义所使用的语法解析器。

我还找到另外两个语法解析库,分别是@typescript-eslint/typescript-estree@babel/parser,其中@typescript-eslint/typescript-estree对于目前vue-property-decorator中使用的修饰器语法并不支持,所以最终选择@babel/parser。

// 使用自定义的语法解析库
const ast = recast.parse(jsScript, {
    parser: {
        parse(source: string, options: any) {
            return parser.parse(source, Object.assign(options, {
                plugins: [
                    'estree', // 支持estree格式
                    'decorators-legacy', // 支持修饰器语法
                    // 'typescript', 支持解析typescript
                ],
                tokens: true, // 必要的参数。默认为false,解析结果中缺少tokens内容,当缺少tokens时,recast将会重新使用esprima进行解析操作
            }))
        },
    },
    tabWidth: 4,
});

对生成的代码进行细节调整

目前使用@vue/cli生成项目过程中,都会提示使用tslint或eslint来帮助保持代码的整洁,如果你不是使用@vue/cli来搭建项目的话,依旧推荐在项目中加上tslint或eslint。

这些库提供了一些代码规范规则,例如:“所以的引号都应该使用单引号”这样的规范,然而使用recast.print生成的代码中默认使用双引号。最终选择还是依据项目的实际情况而定,为此recast也提供一些配置选项,使其能够更灵活地生成代码。

// 使用recast将AST转换成js代码
const code = recast.print(ast, {
    tabWidth: 4, // 使用的空格数量
    quote: 'single', // 使用单引号或者双引号
    trailingComma: true, // 使用启用trailingComma
}).code;

遍历文件

在Node中使用fs来完成对于文件的遍历

import fs from 'fs';
const dir = '/Volumes/Repo2/repo/vue/project/src';
const dist = '/Volumes/Repo2/repo/vue/project_ast/src';
const pageDir = dir + '/pages';
const queue = [ pageDir ];

while (queue.length > 0) {
    const filePath = queue.shift();
    if (filePath) {
        const stats = fs.statSync(filePath);
        const isDirectory = stats.isDirectory();
        if (isDirectory) {
            // 如果是文件夹,将所有的子路径加入queue
            const children = fs.readdirSync(filePath);
            queue.unshift(...children.map(child => filePath + '/' + child));
        } else {
            // 如果是文件,判断是否是.vue文件
            if (filePath.indexOf('.vue') >= 0) {
                const output = dist + filePath.substr(dir.length);
                fs.mkdirSync(path.dirname(output), {
                    recursive: true,
                    mode: 0o755,
                });
                handleVue(filePath, output); // 对于vue文件进行处理
            }
        }
    }
}

后续内容

目前在自己的项目上测试,虽然已经把好多工作量自动化了,但还是好多啊(摔!

还有一个我创建的npm组件库large-list,之前使用class的形式来写,应该是因为引入了vue-property-decorator逻辑,所以最终使用rollup打包不进行uglify情况下有27K大小。使用这个库将class形式代码转换成VueOptions形式,之后再使用rollup打包同样不进行uglify只有4K大小,既能让我使用class形式来编写代码,也让最终发布用的代码足够地小。

另外既然可以完成迁移到ts语法的过程,在Vue@3正式发布之后,可能会考虑是否能将旧代码,转换成composition-api的格式。

image

主子(看我的眼神,不点个赞再走吗~


修改less

在项目迁移过程中,除了对于js内容进行修改之外,也有对样式文件的修改。目前项目中使用的是less,虽然less语法比较简单,甚至可以直接使用多次正则替换来完成修改,但是谁让我比较喜欢折腾呢。( ´・ᴗ・` )

虽然css代码和js代码差别很大,但这次仍旧通过操作AST的方式来完成修改。

依靠postcss将css转换成AST(我觉得了解postcss也很重要呢),不过和recast不同,postcss并不会直接返回AST,需要使用postcss的插件(plguin)来完成这次修改。

下面例子中编写了一个简单的插件:

import postcss from 'postcss';
const code = `
    .rule {
        width: 20px;
    }
`;
const myPlugin = postcss.plugin('postcss-my-plugin', (root, result) => {
    root.walkRule((rule) => {
        rule.walkDecl((decl) => {
            console.log(decl.prop, decl.value); // 将输出 width 和 20px
            decl.value = '40px'; // 很简单滴就将20px修改成了40px
        });
    });
});
postcss([ myPlugin ]).process(code).then((result) => {
    /**
     * 输出修改过的代码
     * .rule {
     *     width: 40px;
     * }
     */
    console.log(result.css);
})

更多关于使用postcss来修改less的方法就不多写了,有需要的童鞋可以自己研究一下。

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

推荐阅读更多精彩内容