从编码到发包实现一个简单的loader

我心飞翔 分类:javascript

loader是什么

loader就是一个函数,接受源文件内容处理后返回给下一个loader处理
 

loader规则

1. 单一职责,一个loader只做一件事,职责越单一,组合性越强
2. 从右向左,链式调用
3. 模块化,一个loader就是一个模块,单独存在不依赖其他模块。无状态(丢进来的是头猪,丢出去的只能是烤猪不能是烤牛烤羊)
 

需求:实现一个loader,在$ajax请求接口函数里包裹上async await标识

实现思路,参考babel的解析转换原理
 

babel的解析原理

1. 解析:将代码字符串转换成AST抽象语法树
2. 转译:对抽象语法树进行变换操作
3. 生成:根据变换后的抽象语法树生成新的代码字符串返回
 

核心:AST(抽象语法树)

AST是源代码抽象语法结构的树状表示,每个节点都表示源代码中的一种结构,并不会表示出真实语法出现的每一个细节,不依赖于源语言的语法,语法分析阶段采用上下文无文文法(目前所有语言都是上下文无关文法)
 

下面两段代码的AST是一致的

python
python.png

javascript
javascript.png

代码转AST测试地址:https://astexplorer.net/#/Z1exs6BWMq
 

下面看一个简单的函数的解析过程(我们只需要参与到第二步)

    function add(a, b) {
        return a + b;
    }
 
  1. 词法分析(scanner),生成tokens列表,遇到空格、操作符或者特殊字符的时候,会认为一个话已经完成了读取代码;调用符号表管理器和出错处理器,滤掉源程序中的无用成分,如注释、空格和回车等,处理相关非法字符或拼写错的关键字、标识符等

     上面这段代码的词法分析转换结果:
    
     [{value: 'function', type: {label: 'function', ...}}, {value: 'add', type: {label: 'name', ...}}, ...]
     
  2. 语法分析:将词法分析出来的数组转换成树形的形式,检查输入中的语法错误,并调用出错处理器进行适当处理(如少分号、括号不匹配等)

     拿到上面的tokens列表,首先拿到这个语法块,是一个FunctionDeclaration(函数定义)对象,拆成三块
    
     一个id,是它的名字,add --->Identifier(标志)对象
     {
         name: 'add'
         type: 'identifier'
         ...
     }
    
     两个params,作为它的参数,[a, b] --->两个Identifier组成的数组
     [
         {
             name: 'a'
             type: 'identifier'
             ...
         },
         {
             name: 'b'
             type: 'identifier'
             ...
         }
     ]
    
     一个body,大括号里面的内容
     BlockStatement: { //一个BlockStatement(块状域)对象,用来表示是{ return a + b }
         ReturnStatement: { //一个ReturnStatement(Return域)对象,用来表示return a + b
             BinaryExpression: { //一个BinaryExpression(二项式)对象,用来表示a + b
                 left: a,
                 operator: +,
                 right: b
             }
         }
     }
     
  3. 语义分析:根据语义规则对语法树中的语法单元进行静态语义检查,如类型检查和转换等,保证语法正确的结构在语义分析上也是合法的。语法如果有错则抛出语法错误(类型不一致、参数不匹配等,eslint)

  4. 中间代码生成,中间代码优化,目标代码生成

开始动手实现

先看下加上了async await和没加的函数的AST的区别

没加的:

function load () {
    this.$ajax({
        url: 'https://www.fastmock.site/mock/27fb729bc15bd866b0b1d34fbf3b5610/apps/test'
    });
}
 

对应的AST

ajax.jpg

加了的:

async function load () {
    await this.$ajax({
        url: 'https://www.fastmock.site/mock/27fb729bc15bd866b0b1d34fbf3b5610/apps/test'
    });
}
 

对应的AST

async_ajax.jpg

我们发现两者的差别就是在FunctionDeclaration中async字段的标识有变,另外就是在CallExpression外包裹了一层AwaitExpression

开始编码loader

我们借助几个库来帮我们实现转换过程中的一些处理(感兴趣的可以自己阅读实现源码)

@babel/parser:将传入的内容转换为AST
@babel/traverse:传入一个AST和钩子函数,内部会深度遍历这棵AST,遍历的节点匹配上钩子函数后,会执行钩子函数的回调
@babel/types:提供了操作AST节点的一些工具函数
@babel/core:将AST重新转为代码字符串返回
 

loader具体实现:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const types = require("@babel/types");
const core = require("@babel/core");
//const loaderUtils = require('loader-utils'); //获取参数
//const validate = require('schema-utils');//校验参数

const isAjax = function (path) {
    let node = path.node;
    if (!node) {
        return false;
    } else if (node.callee.name === '$ajax') {
        return true;
    } else if (types.isMemberExpression(node.callee) && node.callee.property.name ==='$ajax') {
        return true;
    } else {
        return false;
    }
}

const json = {//传入的options必须是个对象 里面content的值必须是一个数组
    "type": "object",
    "properties": {
        "content": {
            "type": "array",
        }
    }
}

module.exports = function (source) {
    // let options = loaderUtils.getOptions(this); //解析产传进来的参数

    //this.cacheable(); //开启缓存,依赖的文件没有发生变化时不会执行解析

    //validate(json, options, 'async_await_loader');// 第一个参数是校验的json 第二个参数是传入的options 第三个参数是loader名称

    // var callback = this.async(); //异步执行,不会阻塞构建,在callback中回调返回结果
    // doSmoething(source, function(err, result, sourceMaps, ast) {
    //     callback(err, result, sourceMaps, ast);
    // });

    let ast = parser.parse(source);
    traverse(ast, {
        CallExpression(path) {
            if (isAjax(path) && !types.isAwaitExpression(path.parent)) {
                let awaitAst = types.AwaitExpression(
                    path.node
                );
                path.replaceWithMultiple([awaitAst]);
                while (path && path.node) {
                    let parentPath = path.parentPath;
                    if (types.isFunctionDeclaration(path.node)) {
                        path.node.async = true;
                        return ;

                    } else {
                        path = parentPath;
                    }
                }
            } else {
                return ; 
            }
        }
    });
    return core.transformFromAstSync(ast).code;
};
 

调试loader

在package.json的script中加上"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js"就可以用npm run debug调试了
 

写完测试完之后,我们就可以上传到npm仓库

1. 先到官网注册一个npm账号,注册完记得验证邮箱,不然后面发包会包403
2. npm adduser将账号添加到本地的npm
3. npm publish发布
 

使用方式

1. yarn add async_await_loader

2. 配置
{
    test: /\.js$/,
    use: [
      {
        loader: 'babel-loader'
      },
      {
        loader: 'async_await_loader'
      }
    ]
}
 

GitHub地址:async—await-loader

回复

我来回复
  • 暂无回复内容