更新于2020-07-04文章和代码存放在Github。打包上传到npm@forzoom/shuttleTypescript正在广泛成为前端工程师开发项目的首选。手头有一些用js写的vue项目,最近准备用ts重写。光是项目的页面数就超过100,更不用说组件的数量了。如果一个一个改写那么多vue文件,工程会很大,很枯燥。其实我之前也手动转换过几个项目,发现转换的过程大部分都是重复性的工作,通过程序实现自动转换是可以的。当然js转ts的时候难免会出现类型问题,所以只需要完成重复性的工作即可。当真正需要类型信息时,还是需要手工处理。在使用ts写项目的时候,可以使用两种不同的编码风格:使用Vue.extend方法来实现。使用类语法配合vue-property-decorator来实现。应该选择哪个选项是一个见仁见智的问题。我用的是方法二,为什么要选它,如果用方法一重写不是方便吗?之所以选择方法2,是因为this关键字在Vue中被广泛使用,使用类的形式更直观——所有内容都在类实例上(其实可能只是我比较喜欢折腾(′???`).实现思路思路就像把大象放进冰箱一样简单:将旧代码转换成AST(AbstractSyntaxTree,抽象语法树).将AST修改为类形式.(当然类型信息不能完全填写,可以先用any或者选择不填写)将AST转换成新的代码。关于什么是抽象语法树,大家可以上网找相关资料了解一下(我觉得很有必要对抽象语法树有一定的了解。简单的说,js代码可以用树结构来表示,也就是抽象语法树。比如functionfoo(){returna+b;}对应的AST可能如下图,一个简单的树结构(当然,这里做其实就多了更复杂)。如果要把代码中的b改成c,只需要修改树中的节点,例如:然后使用修改后的AST生成代码就可以了。CodeandASTconversionrecast是一个可以方便的实现代码和AST转换的库,可以帮我们开关冰箱门。这里还要提到两个概念,estree和ast-types。estree是一个将js代码解析成AST的社区标准,即最终生成的AST节点中的值基本上应该参照estree中的说明来实现。对这个标准有所了解,或者对编译原理有一定了解,可以提高后期修改代码的效率。关于ast-typesast-types是recast中使用的一个库,提供了语法树节点定义和遍历等功能,在项目中被大量使用。ast-types中定义的类型是兼容etree的,但是在实际使用中,有时会遗漏一些,比如某些情况下,会出现decorators字段不存在的情况,可以通过d.ts文件对ast-types类型定义进行了扩展。下面是一个代码示例,使用ast-types构建AST//操作AST中的一些节点import{buildersasb,}from'ast-types';constclazz=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',})classMyComponentextendsVue{}上面的操作AST代码看起来很吓人,它只对应了3行ts代码。另外,大部分人的编译原理课可能都还给老师了,更不用说js的语法了.但是不要害怕,ast-types已经为我们准备了一张备忘单。这样的代码可以在ast-types/def/core.ts文件中看到。//ast-types/def/core.tsvarBinaryOperator=or("==","!=","===","!==","<","<=",">",">=","<<",">>",">>>","+","-","*","/","%","**","&","|","^","in","instanceof");def("BinaryExpression").bases("Expression").build("operator","left","right")//需要的参数。field("operator",BinaryOperator).field("left",def("Expression"))//需要一个表达式.field("right",def("Expression"));def("Identifier").bases("Expression","Pattern")//因为Identifier是在Expression的基础上重新定义的,所以可以作为BinaryExpression的参数.build("name").field("name",String).field("optional",布尔值,默认值["false"]);上面其实就是对js语法的ast-types的定义。def的定义会在builder中生成相应的函数。比如根据上面的定义,builders对象中应该有binaryExpression和identifier两个函数,从上面的代码可以看出这两个函数的参数。当然,如果你用IDE,它也会提示函数的参数,会更方便。import{buildersasb}from'ast-types';//a+b对应的代码b.binaryExpression('+',b.identifier('a'),b.identifier('b'));如果对编译原理不是很清楚,也可以通过recast.parse一些代码了解一下怎么写,然后照葫芦画瓢写代码。constast=recast.parse(`constfoo='bar'`);console.log(ast.program.body);//可以参考输出结果“逆向”编写ast操作代码,选择解析器解析recast中的代码。parse默认会使用esprima进行语法解析,esprima(目前是4.0.1版本)已经有了更多的支持新的js语法,但是对于目前的项目来说,还是有一些语法无法解析。为了解决这个问题,recast还可以自定义使用的解析器。我还找到了另外两个语法解析库,分别是@typescript-eslint/typescript-estree和@babel/parser,其中@typescript-eslint/typescript-estree不适合vue-property-decorator目前使用的装饰器语法支持,所以最后选择@babel/parser。//使用自定义语法解析库constast=recast.parse(jsScript,{parser:{parse(source:string,options:any){returnparser.parse(source,Object.assign(options,{plugins:['estree',//支持estree格式'decorators-legacy',//支持装饰器语法//'typescript',支持解析typescript],tokens:true,//必要参数。默认为false,解析结果中缺少Tokens内容,当令牌丢失时,recast将重新使用esprima进行解析操作}))},},tabWidth:4,});对生成的代码进行详细的调整。目前,在使用@vue/cli生成项目的过程中,都会提示使用tslint或eslint来帮助保持代码的整洁。如果你没有使用@vue/cli来构建项目,仍然建议在项目中添加tslint或eslint。这些库提供了一些代码规范规则,比如:“所有引号都应该使用单引号”,但是使用recast.print生成的代码默认使用双引号。最终选择取决于项目的实际情况。为此,recast还提供了一些配置选项,使其生成代码更加灵活。//使用recast将AST转为js代码constcode=recast.print(ast,{tabWidth:4,//使用的空格数quote:'single',//使用单引号或双引号trailingComma:true,//使用尾随逗号})。代码;遍历文件在Node中使用fs完成文件的遍历importfsfrom'fs';constdir='/Volumes/Repo2/repo/vue/project/src';constdist='/Volumes/Repo2/repo/vue/project_ast/src';constpageDir=dir+'/pages';constqueue=[pageDir];while(queue.length>0){constfilePath=queue.shift();if(filePath){conststats=fs.statSync(filePath);constisDirectory=stats.isDirectory();if(isDirectory){//如果是文件夹,将所有子路径加入队列constchildren=fs.readdirSync(filePath);queue.unshift(...children.map(child=>filePath+'/'+child));}else{//如果是文件,判断是否是.vue文件if(filePath.indexOf('.vue')>=0){constoutput=dist+filePath.substr(dir.length);fs.mkdirSync(路径。目录名(输出),{递归:真,模式:0o755,});handleVue(文件路径,输出);//processvuefiles}}}}用于开发环境目前在自己的项目上测试,虽然很多工作量已经自动化了,但是还是很多(哇!我创建了一个大列表的npm组件库来减少项目的大小,使用class样式,因为引入了vue-property-decorator,用ts写的,所以最后用rollup打包,没有uglify的情况下,大小为27K,从class样式转换过来代码转成VueOptions风格,然后用rollup打包,同样没有uglify也只有4K大小,不仅让我可以用类的形式写代码,也让最终发布的代码足够地小。我用来帮助项目从js转ts的一个项目是在vue-cli@2版本创建的代码,在重构为ts的过程中,库基本用完了,总共有近300个文件被修改了。主子(看我的眼睛,你是不是想先给我点个赞哟你去吗修改较少在项目迁移过程中,除了修改js内容外,还有对样式文件的修改。目前项目中使用的是less,虽然less的语法比较简单,甚至可以直接使用多个正则替换来完成修改,但谁让我更喜欢折腾呢。(′???`)虽然css代码和js代码差别很大,但是这次还是通过操作AST来完成修改。依赖postcss将css转为AST(我觉得理解postcss也很重要),但是与recast不同的是,postcss不直接返回AST,需要借助postcss插件(plguin)来完成这个修改。下面的例子写了一个简单的插件:importpostcssfrom'postcss';constcode=`.rule{width:20px;}`;constmyPlugin=postcss.plugin('postcss-my-plugin',(root,result)=>{root.walkRule((rule)=>{rule.walkDecl((decl)=>{console.log(decl.prop,decl.value);//将输出宽度和20pxdecl.value='40px';//将20px更改为40px非常简单});});});postcss([myPlugin]).process(code).then((result)=>{/***输出已修改Thecode*.rule{*width:40px;*}*/console.log(result.css);})我不会多写使用postcss修改less的方法。需要的童鞋可以自己研究下。