├── .gitignore ├── read.js ├── demo.js ├── package.json ├── parse.js ├── yarn.lock ├── exportific.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /read.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const recast = require('recast') 3 | 4 | recast.run(function(ast, printSource) { 5 | recast.visit(ast, { 6 | visitExpressionStatement: function({node}) { 7 | console.log(node) 8 | return false 9 | } 10 | }); 11 | }); -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | function add(a, b) { 2 | return a + b 3 | } 4 | 5 | function sub(a, b) { 6 | return a - b 7 | } 8 | 9 | function commonDivision(a, b) { 10 | while (b !== 0) { 11 | if (a > b) { 12 | a = sub(a, b) 13 | } else { 14 | b = sub(b, a) 15 | } 16 | } 17 | return a 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exportific", 3 | "version": "0.0.1", 4 | "description": "改写源码中的函数为可exports.XXX形式", 5 | "main": "exportific.js", 6 | "bin": { 7 | "exportific": "./exportific.js" 8 | }, 9 | "keywords": [], 10 | "author": "wanthering", 11 | "license": "ISC", 12 | "dependencies": { 13 | "recast": "^0.15.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /parse.js: -------------------------------------------------------------------------------- 1 | // 给你一把"螺丝刀"——recast 2 | const recast = require("recast"); 3 | 4 | // 你的"机器"——一段代码 5 | // 我们使用了很奇怪格式的代码,想测试是否能维持代码结构 6 | const code = 7 | ` 8 | function add(a, b) { 9 | return a + 10 | // 有什么奇怪的东西混进来了 11 | b", 12 | } 13 | ` 14 | // 用螺丝刀解析机器 15 | const ast = recast.parse(code); 16 | 17 | // ast可以处理很巨大的代码文件 18 | // 但我们现在只需要代码块的第一个body,即add函数 19 | const add = ast.program.body[0] 20 | 21 | console.log(add) -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | assert@^1.4.1: 6 | version "1.4.1" 7 | resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" 8 | dependencies: 9 | util "0.10.3" 10 | 11 | ast-types@^0.11.5: 12 | version "0.11.5" 13 | resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.5.tgz#9890825d660c03c28339f315e9fa0a360e31ec28" 14 | 15 | inherits@2.0.1: 16 | version "2.0.1" 17 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" 18 | 19 | util@0.10.3: 20 | version "0.10.3" 21 | resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" 22 | dependencies: 23 | inherits "2.0.1" 24 | -------------------------------------------------------------------------------- /exportific.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const recast = require("recast"); 3 | const { 4 | identifier: id, 5 | expressionStatement, 6 | memberExpression, 7 | assignmentExpression, 8 | arrowFunctionExpression 9 | } = recast.types.builders 10 | 11 | const fs = require('fs') 12 | const path = require('path') 13 | // 截取参数 14 | const options = process.argv.slice(2) 15 | 16 | //如果没有参数,或提供了-h 或--help选项,则打印帮助 17 | if(options.length===0 || options.includes('-h') || options.includes('--help')){ 18 | console.log(` 19 | 采用commonjs规则,将.js文件内所有函数修改为导出形式。 20 | 21 | 选项: -r 或 --rewrite 可直接覆盖原有文件 22 | `) 23 | process.exit(0) 24 | } 25 | 26 | // 只要有-r 或--rewrite参数,则rewriteMode为true 27 | let rewriteMode = options.includes('-r') || options.includes('--rewrite') 28 | 29 | // 获取文件名 30 | const clearFileArg = options.filter((item)=>{ 31 | return !['-r','--rewrite','-h','--help'].includes(item) 32 | }) 33 | 34 | // 只处理一个文件 35 | let filename = clearFileArg[0] 36 | 37 | console.log(filename) 38 | 39 | const writeASTFile = function(ast, filename, rewriteMode){ 40 | const newCode = recast.print(ast).code 41 | if(!rewriteMode){ 42 | // 非覆盖模式下,将新文件写入*.export.js下 43 | filename = filename.split('.').slice(0,-1).concat(['export','js']).join('.') 44 | } 45 | // 将新代码写入文件 46 | fs.writeFileSync(path.join(process.cwd(),filename),newCode) 47 | console.log(rewriteMode?`exportific成功!新代码已覆盖在${filename}中`:`exportific成功!已创建新文件${filename}`) 48 | } 49 | 50 | 51 | recast.run(function (ast, printSource) { 52 | let funcIds = [] 53 | recast.types.visit(ast, { 54 | visitFunctionDeclaration(path) { 55 | //获取遍历到的函数名、参数、块级域 56 | const node = path.node 57 | const funcName = node.id 58 | const params = node.params 59 | const body = node.body 60 | 61 | funcIds.push(funcName.name) 62 | const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName), 63 | arrowFunctionExpression(params, body))) 64 | path.replace(rep) 65 | return false 66 | } 67 | }) 68 | 69 | 70 | recast.types.visit(ast, { 71 | visitCallExpression(path){ 72 | const node = path.node; 73 | if (funcIds.includes(node.callee.name)) { 74 | node.callee = memberExpression(id('exports'), node.callee) 75 | } 76 | return false 77 | } 78 | }) 79 | 80 | writeASTFile(ast,filename,rewriteMode) 81 | }) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Javascript就像一台精妙运作的机器,我们可以用它来完成一切天马行空的构思。 2 | 3 | 我们对javascript生态了如指掌,却常忽视javascript本身。这台机器,究竟是哪些零部件在支持着它运行? 4 | 5 | —— 这时你需要懂得抽象语法树(AST)。 6 | 7 | AST在日常业务中也许很难涉及到,但当你不止于想做一个工程师,而想做工程师的工程师,写出类似webpack、vue-cli前端自动化的工具,或者有批量修改源码的工程需求,那你必须懂得AST。 8 | 9 | 事实上,在javascript世界中,你可以认为抽象语法树(AST)是最底层。 再往下,就是关于转换和编译的“黑魔法”领域了。 10 | 11 | ## 人生第一次拆解Javascript 12 | 13 | 小时候,当我们拿到一个螺丝刀和一台机器,人生中最令人怀念的梦幻时刻便开始了: 14 | 15 | 我们把机器,拆成一个一个小零件,一个个齿轮与螺钉,用巧妙的机械原理衔接在一起... 16 | 17 | 当我们把它重新照不同的方式组装起来,这时,机器重新又跑动了起来——世界在你眼中如获新生。 18 | 19 | ![image](http://img.hb.aicdn.com/1a930d490f341bb87a9eeedc3235ec3b527af0893eee0-41DK2A_fw658) 20 | 21 | 通过抽象语法树解析,我们可以像童年时拆解玩具一样,透视Javascript这台机器的运转,并且重新按着你的意愿来组装。 22 | 23 | ** 现在,我们拆解一个简单的add函数** 24 | ``` 25 | function add(a, b) { 26 | return a + b 27 | } 28 | ``` 29 | 首先,我们拿到的这个语法块,是一个FunctionDeclaration。 30 | 31 | 用力拆开,它成了三块: 32 | - 一个id,就是它的名字,即add 33 | - 两个params,就是它的参数,即[a, b] 34 | - 一块body,也就是大括号内的一堆东西 35 | 36 | add没办法继续拆下去了,它是一个最基础Identifier(标志)对象 37 | ``` 38 | { 39 | name: 'add' 40 | type: 'identifier' 41 | ... 42 | } 43 | ``` 44 | 45 | params继续拆下去,其实是两个Identifier组成的数组。之后也没办法拆下去了 46 | ``` 47 | [ 48 | { 49 | name: 'a' 50 | type: 'identifier' 51 | ... 52 | }, 53 | { 54 | name: 'b' 55 | type: 'identifier' 56 | ... 57 | } 58 | ] 59 | ``` 60 | 61 | 接下来,我们继续拆开body 62 | 我们发现,body其实是一个BlockStatement对象,用来表示是`{return a + b}` 63 | 64 | 打开Blockstatement,里面藏着一个ReturnStatement对象,用来表示`return a + b` 65 | 66 | 继续打开ReturnStatement,里面是一个BinaryExpression,用来表示`a + b` 67 | 68 | 继续打开BinaryExpression,它成了三部分,`left`,`operator`,`right` 69 | 70 | - `operator` 即`+` 71 | - `left` 里面装的,是Identifier对象 `a` 72 | - `right` 里面装的,是Identifer对象 `b` 73 | 74 | 就这样,我们把一个简单的add函数拆解完毕,用图表示就是 75 | 76 | ![image](http://note.youdao.com/yws/res/8692/8B0AA4E0C64F4E6B9691B2DDAB097CCC) 77 | 78 | 看!抽象语法树(Abstract Syntax Tree),的确是一种标准的树结构。 79 | 80 | 那么,上面我们提到的Identifier、Blockstatement、ReturnStatement、BinaryExpression, 这一个个小部件的说明书去哪查? 81 | 82 | **请查看 [AST对象文档](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API#Node_objects)** 83 | 84 | 85 | ### 送给你的AST螺丝刀:Recast 86 | 87 | 输入命令: 88 | ``` 89 | npm i recast -S 90 | ``` 91 | 你即可获得一把操纵语法树的螺丝刀 92 | 93 | 接下来,你可以在任意js文件下操纵这把螺丝刀,我们新建一个demo.js示意: 94 | 95 | **parse.js** 96 | ``` 97 | // 给你一把"螺丝刀"——recast 98 | const recast = require("recast"); 99 | 100 | // 你的"机器"——一段代码 101 | // 我们使用了很奇怪格式的代码,想测试是否能维持代码结构 102 | const code = 103 | ` 104 | function add(a, b) { 105 | return a + 106 | // 有什么奇怪的东西混进来了 107 | b", 108 | } 109 | ` 110 | // 用螺丝刀解析机器 111 | const ast = recast.parse(code); 112 | 113 | // ast可以处理很巨大的代码文件 114 | // 但我们现在只需要代码块的第一个body,即add函数 115 | const add = ast.program.body[0] 116 | 117 | console.log(add) 118 | ``` 119 | 120 | 输入`node parse.js`你可以查看到add函数的结构,与之前所述一致,通过[AST对象文档](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API#Node_objects)可查到它的具体属性: 121 | ``` 122 | FunctionDeclaration{ 123 | type: 'FunctionDeclaration', 124 | id: ... 125 | params: ... 126 | body: ... 127 | } 128 | ``` 129 | 你也可以继续使用console.log透视它的更内层,如: 130 | ``` 131 | console.log(add.params[0]) 132 | ``` 133 | ``` 134 | console.log(add.body.body[0].argument.left) 135 | ``` 136 | 137 | ## recast.types.builders 制作模具 138 | 139 | 一个机器,你只会拆开重装,不算本事。 140 | 141 | 拆开了,还能改装,才算上得了台面。 142 | 143 | recast.types.builders里面提供了不少“模具”,让你可以轻松地拼接成新的机器。 144 | 145 | 最简单的例子,我们想把之前的`function add(a, b){...}`声明,改成匿名函数式声明`const add = function(a ,b){...}` 146 | 147 | 如何改装? 148 | 149 | 第一步,我们创建一个VariableDeclaration变量声明对象,声明头为const, 内容为一个即将创建的VariableDeclarator对象。 150 | 151 | 第二步,创建一个VariableDeclarator,放置add.id在左边, 右边是将创建的FunctionDeclaration对象 152 | 153 | 第三步,我们创建一个FunctionDeclaration,如前所述的三个组件,id params body中,因为是匿名函数id设为空,params使用add.params,body使用add.body。 154 | 155 | 这样,就创建好了`const add = function(){}`的AST对象。 156 | 157 | 在之前的parse.js代码之后,加入以下代码 158 | ``` 159 | // 引入变量声明,变量符号,函数声明三种“模具” 160 | const {variableDeclaration, variableDeclarator, functionExpression} = recast.types.builders 161 | 162 | // 将准备好的组件置入模具,并组装回原来的ast对象。 163 | ast.program.body[0] = variableDeclaration("const", [ 164 | variableDeclarator(add.id, functionExpression( 165 | null, // Anonymize the function expression. 166 | add.params, 167 | add.body 168 | )) 169 | ]); 170 | 171 | //将AST对象重新转回可以阅读的代码 172 | const output = recast.print(ast).code; 173 | 174 | console.log(output) 175 | ``` 176 | 可以看到,我们打印出了 177 | ``` 178 | const add = function(a, b) { 179 | return a + 180 | // 有什么奇怪的东西混进来了 181 | b 182 | }; 183 | 184 | ``` 185 | 186 | 最后一行 187 | ``` 188 | const output = recast.print(ast).code; 189 | ``` 190 | 其实是recast.parse的逆向过程,具体公式为 191 | ``` 192 | recast.print(recast.parse(source)).code === source 193 | ``` 194 | 打印出来还保留着“原装”的函数内容,连注释都没有变。 195 | 196 | 我们其实也可以打印出美化格式的代码段: 197 | ``` 198 | const output = recast.prettyPrint(ast, { tabWidth: 2 }).code 199 | ``` 200 | 201 | 输出为 202 | ``` 203 | const add = function(a, b) { 204 | return a + b; 205 | }; 206 | 207 | ``` 208 | 209 | > 现在,你是不是已经产生了“我可以通过AST树生成任何js代码”的幻觉? 210 | 211 | > 我郑重告诉你,这不是幻觉。 212 | 213 | ## 实战进阶:命令行修改js文件 214 | 除了parse/print/builder以外,Recast的三项主要功能: 215 | - run: 通过命令行读取js文件,并转化成ast以供处理。 216 | - tnt: 通过assert()和check(),可以验证ast对象的类型。 217 | - visit: 遍历ast树,获取有效的AST对象并进行更改。 218 | 219 | 我们通过一个系列小务来学习全部的recast工具库: 220 | 221 | 创建一个用来示例文件,假设是demo.js 222 | 223 | **demo.js** 224 | ``` 225 | function add(a, b) { 226 | return a + b 227 | } 228 | 229 | function sub(a, b) { 230 | return a - b 231 | } 232 | 233 | function commonDivision(a, b) { 234 | while (b !== 0) { 235 | if (a > b) { 236 | a = sub(a, b) 237 | } else { 238 | b = sub(b, a) 239 | } 240 | } 241 | return a 242 | } 243 | ``` 244 | 245 | ### recast.run —— 命令行文件读取 246 | 247 | 新建一个名为`read.js`的文件,写入 248 | **read.js** 249 | ``` 250 | recast.run( function(ast, printSource){ 251 | printSource(ast) 252 | }) 253 | ``` 254 | 255 | 命令行输入 256 | ``` 257 | node read demo.js 258 | ``` 259 | 我们查以看到js文件内容打印在了控制台上。 260 | 261 | 我们可以知道,`node read`可以读取`demo.js`文件,并将demo.js内容转化为ast对象。 262 | 263 | 同时它还提供了一个`printSource`函数,随时可以将ast的内容转换回源码,以方便调试。 264 | 265 | ### recast.visit —— AST节点遍历 266 | 267 | **read.js** 268 | ``` 269 | #!/usr/bin/env node 270 | const recast = require('recast') 271 | 272 | recast.run(function(ast, printSource) { 273 | recast.visit(ast, { 274 | visitExpressionStatement: function({node}) { 275 | console.log(node) 276 | return false 277 | } 278 | }); 279 | }); 280 | ``` 281 | 282 | 283 | recast.visit将AST对象内的节点进行逐个遍历。 284 | 285 | **注意** 286 | 287 | - 你想操作函数声明,就使用visitFunctionDelaration遍历,想操作赋值表达式,就使用visitExpressionStatement。 只要在 [AST对象文档](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API#Node_objects)中定义的对象,在前面加visit,即可遍历。 288 | - 通过node可以取到AST对象 289 | - 每个遍历函数后必须加上return false,或者选择以下写法,否则报错: 290 | 291 | ``` 292 | #!/usr/bin/env node 293 | const recast = require('recast') 294 | 295 | recast.run(function(ast, printSource) { 296 | recast.visit(ast, { 297 | visitExpressionStatement: function(path) { 298 | const node = path.node 299 | printSource(node) 300 | this.traverse(path) 301 | } 302 | }) 303 | }); 304 | ``` 305 | 306 | 调试时,如果你想输出AST对象,可以`console.log(node)` 307 | 308 | 如果你想输出AST对象对应的源码,可以`printSource(node)` 309 | 310 | 命令行输入` 311 | node read demo.js`进行测试。 312 | 313 | 314 | > #!/usr/bin/env node 在所有使用recast.run的文件顶部都需要加入这一行,它的意义我们最后再讨论。 315 | 316 | ### TNT —— 判断AST对象类型 317 | 318 | TNT,即recast.types.namedTypes,就像它的名字一样火爆,它用来判断AST对象是否为指定的类型。 319 | 320 | TNT.Node.assert(),就像在机器里埋好的炸药,当机器不能完好运转时(类型不匹配),就炸毁机器(报错退出) 321 | 322 | TNT.Node.check(),则可以判断类型是否一致,并输出False和True 323 | 324 | 上述Node可以替换成任意AST对象,例如TNT.ExpressionStatement.check(),TNT.FunctionDeclaration.assert() 325 | 326 | **read.js** 327 | ``` 328 | #!/usr/bin/env node 329 | const recast = require("recast"); 330 | const TNT = recast.types.namedTypes 331 | 332 | recast.run(function(ast, printSource) { 333 | recast.visit(ast, { 334 | visitExpressionStatement: function(path) { 335 | const node = path.value 336 | // 判断是否为ExpressionStatement,正确则输出一行字。 337 | if(TNT.ExpressionStatement.check(node)){ 338 | console.log('这是一个ExpressionStatement') 339 | } 340 | this.traverse(path); 341 | } 342 | }); 343 | }); 344 | ``` 345 | **read.js** 346 | ``` 347 | #!/usr/bin/env node 348 | const recast = require("recast"); 349 | const TNT = recast.types.namedTypes 350 | 351 | recast.run(function(ast, printSource) { 352 | recast.visit(ast, { 353 | visitExpressionStatement: function(path) { 354 | const node = path.node 355 | // 判断是否为ExpressionStatement,正确不输出,错误则全局报错 356 | TNT.ExpressionStatement.assert(node) 357 | this.traverse(path); 358 | } 359 | }); 360 | }); 361 | ``` 362 | 命令行输入` 363 | node read demo.js`进行测试。 364 | 365 | ### 实战:用AST修改源码,导出全部方法 366 | 367 | 368 | exportific.js 369 | 370 | 现在,我们希望将demo中的function全部 371 | 372 | 我们想让这个文件中的函数改写成能够全部导出的形式,例如 373 | ``` 374 | function add (a, b) { 375 | return a + b 376 | } 377 | ``` 378 | 想改变为 379 | ``` 380 | exports.add = (a, b) => { 381 | return a + b 382 | } 383 | ``` 384 | 385 | 除了使用fs.read读取文件、正则匹配替换文本、fs.write写入文件这种笨拙的方式外,我们可以==用AST优雅地解决问题==。 386 | 387 | 查询[AST对象文档](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API#Node_objects) 388 | 389 | #### 首先,我们先用builders凭空实现一个键头函数 390 | **exportific.js** 391 | ``` 392 | #!/usr/bin/env node 393 | const recast = require("recast"); 394 | const { 395 | identifier:id, 396 | expressionStatement, 397 | memberExpression, 398 | assignmentExpression, 399 | arrowFunctionExpression, 400 | blockStatement 401 | } = recast.types.builders 402 | 403 | recast.run(function(ast, printSource) { 404 | // 一个块级域 {} 405 | console.log('\n\nstep1:') 406 | printSource(blockStatement([])) 407 | 408 | // 一个键头函数 ()=>{} 409 | console.log('\n\nstep2:') 410 | printSource(arrowFunctionExpression([],blockStatement([]))) 411 | 412 | // add赋值为键头函数 add = ()=>{} 413 | console.log('\n\nstep3:') 414 | printSource(assignmentExpression('=',id('add'),arrowFunctionExpression([],blockStatement([])))) 415 | 416 | // exports.add赋值为键头函数 exports.add = ()=>{} 417 | console.log('\n\nstep4:') 418 | printSource(assignmentExpression('=',memberExpression(id('exports'),id('add')), 419 | arrowFunctionExpression([],blockStatement([])))) 420 | }); 421 | ``` 422 | 上面写了我们一步一步推断出`exports.add = ()=>{}`的过程,从而得到具体的AST结构体。 423 | 424 | 使用`node exportific demo.js`运行可查看结果。 425 | 426 | 接下来,只需要在获得的最终的表达式中,把id('add')替换成遍历得到的函数名,把参数替换成遍历得到的函数参数,把blockStatement([])替换为遍历得到的函数块级作用域,就成功地改写了所有函数! 427 | 428 | 另外,我们需要注意,在commonDivision函数内,引用了sub函数,应改写成exports.sub 429 | 430 | **exportific.js** 431 | ``` 432 | #!/usr/bin/env node 433 | const recast = require("recast"); 434 | const { 435 | identifier: id, 436 | expressionStatement, 437 | memberExpression, 438 | assignmentExpression, 439 | arrowFunctionExpression 440 | } = recast.types.builders 441 | 442 | recast.run(function (ast, printSource) { 443 | // 用来保存遍历到的全部函数名 444 | let funcIds = [] 445 | recast.types.visit(ast, { 446 | // 遍历所有的函数定义 447 | visitFunctionDeclaration(path) { 448 | //获取遍历到的函数名、参数、块级域 449 | const node = path.node 450 | const funcName = node.id 451 | const params = node.params 452 | const body = node.body 453 | 454 | // 保存函数名 455 | funcIds.push(funcName.name) 456 | // 这是上一步推导出来的ast结构体 457 | const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName), 458 | arrowFunctionExpression(params, body))) 459 | // 将原来函数的ast结构体,替换成推导ast结构体 460 | path.replace(rep) 461 | // 停止遍历 462 | return false 463 | } 464 | }) 465 | 466 | 467 | recast.types.visit(ast, { 468 | // 遍历所有的函数调用 469 | visitCallExpression(path){ 470 | const node = path.node; 471 | // 如果函数调用出现在函数定义中,则修改ast结构 472 | if (funcIds.includes(node.callee.name)) { 473 | node.callee = memberExpression(id('exports'), node.callee) 474 | } 475 | // 停止遍历 476 | return false 477 | } 478 | }) 479 | // 打印修改后的ast源码 480 | printSource(ast) 481 | }) 482 | ``` 483 | 484 | ### 一步到位,发一个最简单的exportific前端工具 485 | 486 | 上面讲了那么多,仍然只体现在理论阶段。 487 | 488 | 但通过简单的改写,就能通过recast制作成一个名为exportific的源码编辑工具。 489 | 490 | 以下代码添加作了两个小改动 491 | 1. 添加说明书--help,以及添加了--rewrite模式,可以直接覆盖文件或默认为导出*.export.js文件。 492 | 2. 将之前代码最后的 printSource(ast)替换成 writeASTFile(ast,filename,rewriteMode) 493 | 494 | **exportific.js** 495 | ``` 496 | #!/usr/bin/env node 497 | const recast = require("recast"); 498 | const { 499 | identifier: id, 500 | expressionStatement, 501 | memberExpression, 502 | assignmentExpression, 503 | arrowFunctionExpression 504 | } = recast.types.builders 505 | 506 | const fs = require('fs') 507 | const path = require('path') 508 | // 截取参数 509 | const options = process.argv.slice(2) 510 | 511 | //如果没有参数,或提供了-h 或--help选项,则打印帮助 512 | if(options.length===0 || options.includes('-h') || options.includes('--help')){ 513 | console.log(` 514 | 采用commonjs规则,将.js文件内所有函数修改为导出形式。 515 | 516 | 选项: -r 或 --rewrite 可直接覆盖原有文件 517 | `) 518 | process.exit(0) 519 | } 520 | 521 | // 只要有-r 或--rewrite参数,则rewriteMode为true 522 | let rewriteMode = options.includes('-r') || options.includes('--rewrite') 523 | 524 | // 获取文件名 525 | const clearFileArg = options.filter((item)=>{ 526 | return !['-r','--rewrite','-h','--help'].includes(item) 527 | }) 528 | 529 | // 只处理一个文件 530 | let filename = clearFileArg[0] 531 | 532 | const writeASTFile = function(ast, filename, rewriteMode){ 533 | const newCode = recast.print(ast).code 534 | if(!rewriteMode){ 535 | // 非覆盖模式下,将新文件写入*.export.js下 536 | filename = filename.split('.').slice(0,-1).concat(['export','js']).join('.') 537 | } 538 | // 将新代码写入文件 539 | fs.writeFileSync(path.join(process.cwd(),filename),newCode) 540 | } 541 | 542 | 543 | recast.run(function (ast, printSource) { 544 | let funcIds = [] 545 | recast.types.visit(ast, { 546 | visitFunctionDeclaration(path) { 547 | //获取遍历到的函数名、参数、块级域 548 | const node = path.node 549 | const funcName = node.id 550 | const params = node.params 551 | const body = node.body 552 | 553 | funcIds.push(funcName.name) 554 | const rep = expressionStatement(assignmentExpression('=', memberExpression(id('exports'), funcName), 555 | arrowFunctionExpression(params, body))) 556 | path.replace(rep) 557 | return false 558 | } 559 | }) 560 | 561 | 562 | recast.types.visit(ast, { 563 | visitCallExpression(path){ 564 | const node = path.node; 565 | if (funcIds.includes(node.callee.name)) { 566 | node.callee = memberExpression(id('exports'), node.callee) 567 | } 568 | return false 569 | } 570 | }) 571 | 572 | writeASTFile(ast,filename,rewriteMode) 573 | }) 574 | ``` 575 | 现在尝试一下 576 | ``` 577 | node exportific demo.js 578 | ``` 579 | 已经可以在当前目录下找到源码变更后的`demo.export.js`文件了。 580 | 581 | ### npm发包 582 | 编辑一下package.json文件 583 | ``` 584 | { 585 | "name": "exportific", 586 | "version": "0.0.1", 587 | "description": "改写源码中的函数为可exports.XXX形式", 588 | "main": "exportific.js", 589 | "bin": { 590 | "exportific": "./exportific.js" 591 | }, 592 | "keywords": [], 593 | "author": "wanthering", 594 | "license": "ISC", 595 | "dependencies": { 596 | "recast": "^0.15.3" 597 | } 598 | } 599 | ``` 600 | 注意bin选项,它的意思是将全局命令`exportific`指向当前目录下的`exportific.js` 601 | 602 | 之后,只要哪个js文件想导出来使用,就`exportific XXX.js`一下。 603 | 604 | 这是在本地的玩法,想和大家一起分享这个前端小工具,只需要发布npm包就行了。 605 | 606 | 同时,一定要注意exportific.js文件头有 607 | ``` 608 | #!/usr/bin/env node 609 | ``` 610 | 否则在使用时将报错。 611 | 612 | #### 接下来,正式发布npm包! 613 | 614 | 如果你已经有了npm 帐号,请使用`npm login`登录 615 | 616 | 如果你还没有npm帐号 https://www.npmjs.com/signup 非常简单就可以注册npm 617 | 618 | 然后,输入 619 | `npm publish` 620 | 621 | 没有任何繁琐步骤,丝毫审核都没有,你就发布了一个实用的前端小工具exportific 。任何人都可以通过 622 | ``` 623 | npm i exportific -g 624 | ``` 625 | 全局安装这一个插件。 626 | 627 | 提示:==在试验教程时,请不要和我的包重名,修改一下发包名称。== 628 | 629 | ### 结语 630 | 631 | 我们对javascript再熟悉不过,但透过AST的视角,最普通的js语句,却焕发出精心动魄的美感。你可以通过它批量构建任何javascript代码! 632 | 633 | 童年时,这个世界充满了新奇的玩具,再普通的东西在你眼中都如同至宝。如今,计算机语言就是你手中的大玩具,一段段AST对象的拆分组装,构建出我们所生活的网络世界。 634 | 635 | 所以不得不说软件工程师是一个幸福的工作,你心中住的仍然是那个午后的少年,永远有无数新奇等你发现,永远有无数梦想等你构建。 636 | 637 | ![image](http://img.hb.aicdn.com/0f68442335482d9440e083598738a8a547a9763542cbd-zUQyLm_fw658) --------------------------------------------------------------------------------