在说parse之前,我们要先来了解一下ast是什么东东。

在编译型语言的源代码执行之前,通常会经历三个步骤。分别是分词,解析,生成。很多都说js是动态语言,可实际上,js其实也是一门编译型语言,只不过实在运行前非常快速的编译,并且编译过程要复杂的多。

我们先来按照编译器的思维看看一个代码是如何编译的。

一、分词过程

  1. function sum(a,b) {
  2. const c = 1;
  3. return a+b+c+2;
  4. }

如果编译器遇到上述代码,会先将可拆解的部分和节点,拆成最小词法单元。首先这段代码块,会被识别成一个FunctionDeclaration(函数声明)对象。这个函数声明又有哪几部分组成呢?

function关键字指明声明函数,sum是一个Identifier标识符,会被编译器提取成为一个id节点(名字),标识这个函数的局部唯一性。然后我们会看到这个函数又有两个参数,分别又会生成两个param节点,他们都是属于标识符类型(并非声明)。

进入function内部,也就是函数体,会被编译器解析成可执行代码块,即BlcokStament,BlockStament下又有两个节点,一个是VariableDeclaration(变量声明),一个是ReturnStatement(返回代码块)。
_
编译器还会接着往下拆,首先拆这个变量声明const c = 1;首先我们知道了,这个变量声明的类型是const,我们用一个kind属性,来记住他的类型,其他类型还有var和let。再拆,他有一个标识符,名字叫做c。然后右边是一个初始化节点,他的value是1。到此为止,这个变量声明已经拆不下去了。

下面再来拆返回代码块,这个代码会被分析成为一个二元表达式,加减乘除取余与或非这样的操作符处理变量,都会被认为是二元表达式。

到这里,我们可以得出一个结论,代码的关键字,标识符,代码块,操作符等等,所有语言表象上提供给我们的工具,都是可以被拆解成代码单元的。并能被ast描述出来。函数声明,变量声明,参数命名,赋值操作,还有函数调用等等,都是可以通过节点来描述出来的。

二、生成ast。

现在我们来看看这段代码的解析树。

其他的都好理解,返回代码块有些不好理解。

为什么会是上面这种树形结果?因为表达式解析是按照栈结构来解析的,后进先出,所以从右边开发起算,然后每碰到一个操作符,即入栈,直到无操作符为止。

三、代码生成。

既然代码能被抽象成语法树,我们平时做开发,通常都是数据驱动功能,那么编译器也可以根据语法树去生成平台可执行代码的。这个过程,是和语言以及平台息息相关的。每个语言和平台,都会有不同的处理。

和一些强类型语言不同的是,js的编译是在执行前的几毫秒内。所以才会让人忽略掉js本质也是编译语言的事实。

现在既然知道了这个过程,那我们就可以试着去解析出这样的代码,并且转换,babel就是这样做的。其核心思路就是利用各种有效的数据结构(例如栈),逐个字符解析代码,并分析,最后转换。所幸,这部分最复杂最细碎的功能,现在已经有了现成的工具库,babel基于此,webpakc也基于此,uglify也基于此。如果真的要去实现一个分词器+ast生成器,有了思路并不是一件特别难的事,可大量的时间大量的测试是避免不了的,所以我们还是以了解实现思路为主,暂时先站在巨人的肩膀上玩玩。

babel所使用底层的库目前是@babel/parse,@babel/traverse,@babel/generaor,@babel/types等,分别负责分词解析ast,遍历ast,构建代码,创建规范节点。

我们通过传入sourceMap的源码,就可以分析出上述的代码,经过遍历转换,最后生成代码,即可实现module的转换与依赖的分析。下面进入代码演示环节

  1. const parser = require('@babel/parser');
  2. const { default: traverse } = require('@babel/traverse');
  3. const { default: generator } = require('@babel/generator');
  4. const { stringLiteral } = require('@babel/types');
  5. class Compiler {
  6. constructor(config) {
  7. // 记录入口文件的id
  8. this.entryId = '';
  9. // module
  10. this.modules = {};
  11. // 储存config文件
  12. this.config = config;
  13. // 找到entryFile
  14. this.entryFile = path.resolve(this.config.entry);
  15. // 工作目录
  16. this.workspace = path.resolve();
  17. }
  18. async buildMode(modulePath , entry) {
  19. // 读取文件内容
  20. const source = await this.getSource(modulePath);
  21. // 获取到模块基于工作空间中的相对路径差值
  22. // 这部分差值,即是moduleId
  23. // 即 /workspace/src - /workspace = ./src
  24. const moduleName = `./${ path.relative(this.workspace , modulePath) }`;
  25. // 获取解析模块依赖,且转换后的源码和依赖。
  26. const { sourceCode , dependencies } = this.parse(source , path.dirname(moduleName));
  27. this.modules[moduleName] = sourceCode;
  28. if (entry) {
  29. this.entryId = moduleName;
  30. }
  31. // 将依赖项,进一步的分析构建,进行递归
  32. await Promise.all(dependencies.map(async dep => {
  33. await this.buildMode(dep , false);
  34. }));
  35. }
  36. parse(source , parentPath) {
  37. // 分词并解析出ast树
  38. const ast = parser.parse(source , {
  39. sourceType: 'module'
  40. });
  41. // 依赖list
  42. const dependencies = [];
  43. traverse(ast , {
  44. // 如果是调用表达式
  45. CallExpression(nodePath) {
  46. const { node } = nodePath;
  47. // 如果声明的名字是require
  48. // 函数调用表达式的callee是个标识符对象
  49. if (node.callee.name === 'require') {
  50. // require函数唯一一个参数即是依赖模块的路径
  51. const moduleName = node.arguments[0].value;
  52. // 计算出绝对路径
  53. const modulePath = path.join(parentPath , moduleName);
  54. // 获取后缀
  55. const extname = path.extname(moduleName);
  56. // 计算相对路径 即sourceModule的key
  57. const fullModuleName = './' + (extname ? modulePath : modulePath + extname);
  58. // 更改命名为__webpack__require
  59. node.callee.name = '__webpack__require__';
  60. // 将参数 改为转换后的字面量
  61. node.arguments = [ stringLiteral(fullModuleName) ];
  62. dependencies.push(fullModuleName);
  63. }
  64. } ,
  65. VariableDeclaration({ node }) {
  66. node.kind = 'var';
  67. }
  68. });
  69. const sourceCode = generator(ast).code;
  70. console.log(sourceCode);
  71. return {
  72. sourceCode ,
  73. dependencies ,
  74. };
  75. }
  76. async run() {
  77. // 构建
  78. await this.buildMode(this.entryFile , true);
  79. // 发射文件
  80. await this.emitFile();
  81. }
  82. }

当buildMode方法整个递归结果执行完成后,我们已经生成了初始的sourceModule。但是初始的sourceModule,和我们要实现的代码还有些差。

接下来,就应该把这个文件转成最终的打包文件。执行emitFile方法。

const parser = require('@babel/parser');
const { default: traverse } = require('@babel/traverse');
const { default: generator } = require('@babel/generator');
const { stringLiteral } = require('@babel/types');


class Compiler {

  constructor(config) {
        // 记录入口文件的id
        this.entryId = '';
        // module
        this.modules = {};
        // 储存config文件
        this.config = config;
        // 找到entryFile
        this.entryFile = path.resolve(this.config.entry);
        // 工作目录
        this.workspace = path.resolve();
  }

  async buildMode(modulePath , entry) {
        // 读取文件内容
        const source = await this.getSource(modulePath);
        // 获取到模块基于工作空间中的相对路径差值
        // 这部分差值,即是moduleId
        // 即 /workspace/src - /workspace = ./src
        const moduleName = `./${ path.relative(this.workspace , modulePath) }`;
        // 获取解析模块依赖,且转换后的源码和依赖。
        const { sourceCode , dependencies } = this.parse(source , path.dirname(moduleName));

        this.modules[moduleName] = sourceCode;

        if (entry) {
            this.entryId = moduleName;
        }

        // 将依赖项,进一步的分析构建,进行递归
        await Promise.all(dependencies.map(async dep => {
            await this.buildMode(dep , false);
        }));

  }

  parse(source , parentPath) {
            // 分词并解析出ast树
        const ast = parser.parse(source , {
            sourceType: 'module'
        });
            // 依赖list
        const dependencies = [];
        traverse(ast , {
            // 如果是调用表达式
            CallExpression(nodePath) {
                const { node } = nodePath;
                // 如果声明的名字是require
                  // 函数调用表达式的callee是个标识符对象
                if (node.callee.name === 'require') {
                    // require函数唯一一个参数即是依赖模块的路径
                    const moduleName = node.arguments[0].value;
                    // 计算出绝对路径
                    const modulePath = path.join(parentPath , moduleName);
                    // 获取后缀
                    const extname = path.extname(moduleName);
                    // 计算相对路径 即sourceModule的key
                    const fullModuleName = './' + (extname ? modulePath : modulePath + extname);
                    // 更改命名为__webpack__require
                    node.callee.name = '__webpack__require__';
                    // 将参数 改为转换后的字面量
                    node.arguments = [ stringLiteral(fullModuleName) ];
                    dependencies.push(fullModuleName);
                }
            } ,
            VariableDeclaration({ node }) {
                node.kind = 'var';
            }

        });
        const sourceCode = generator(ast).code;
        console.log(sourceCode);
        return {
            sourceCode ,
            dependencies ,
        };
    }

      replaceModules() {
        return Object.keys(this.modules).map((key) => {
            return `'${ key }': function(module,exports,__webpack__require__) {
                                              ${ this.modules[key] }
                                          } ,`;
        });

    };


    async emitFile() {
                // 拼接dist文件路径
        const main = path.join(this.config.output.path , this.config.output.filename);
        // 替换全部module为webpack的模块化作用域代码
        const newModules = this.replaceModules();
                // 模板变量替换
        const newTemplate = emitFileTemplate.replace("<%=modules%>" , `{${ newModules.join('\n') }}`);
        // 替换入口执行方法
        const code = newTemplate.replace("<%=entryId%>" , JSON.stringify(this.entryId));


        this.assets = {};

        this.assets[main] = code;
          //  写入文件
        return fs.writeFile(main , code);
    }

   async run() {
        // 构建
        await this.buildMode(this.entryFile , true);
        // 发射文件
        await this.emitFile();
    }

}
     现在试运行下代码,跑起来了。放在浏览器可用,这个模块化打包机,算是完成了,但接下来,我们还有好多的功能没有实现。

附加

表达式和字面量的区别:

首先,表达式和字面量相同的点是,他们都会产生值。

表达式是要通过rhs结合操作符(二元表达式,三元表达式等)而间接产生的值。

字面量则是直接值,也可以成为直接量。

image.png