loader 背后的编译原理和JS的词法和语法知识

187 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情

一、AST(抽象语法树)及其编译流程

const recast = require("recast");

const checkUrlList = `[
    "https://d-dev.zhgcloud.com/image/2021/6/2/60b75680287c1.png",
    "https://d-dev.zhgcloud.com/image/2021/6/2/60b73e58b38d0.jpg",
    "https://d-dev.zhgcloud.com/image/2021/6/2/60b75758164c6.png",
    "https://d-dev.zhgcloud.com/image/2021/6/2/60b752cd01b1c.png",
    "https://webpack.docschina.org/site-logo.1fcab817090e78435061.svg"
]`

const ast = recast.parse(checkUrlList);

console.log(ast)

image.png

  • 编译的定义就是从一种编程语言转成另一种编程语言。主要指的是高级语言到低级语言(Compiler)。
  • 为了让计算机理解代码需要先对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串

babel 的编译流程为例

babel 是 source to source 的转换,整体编译流程分为三步:

  • parse:通过 parser 把源码转成抽象语法树(AST)
  • transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改
  • generate:把转换后的 AST 打印成目标代码,并生成 sourcemap

image.png

为什么需要抽象语法树(AST)?

二、Javascript 词法和语法

1. Javascript的边界与定义

1.Javascript边界在哪里?(什么可以表达?什么不可以表达?)

答:巴科斯范式(BNF: Backus-Naur Form 的缩写)是由 John Backus 和 Peter Naur 首先引入的用来描述计算机语言语法的符号集。现在,几乎每一位新编程语言书籍的作者都使用巴科斯范式来定义编程语言的语法规则。

image.png

2.怎么定义是对的,怎么定义是错的?

答:The standards for JavaScript are the ECMAScript Language Specification (ECMA-262) and the ECMAScript Internationalization API specification (ECMA-402) 参见MDN

语法规定 image.png

产生式规定 image.png

一个function表达式 image.png

2. 产生式的定义及写法

2.1 定义

  • 符号(Symbol):定义的语法结构名称
  • 终结符(Terminal Symbol):不是由其他符号定义的符号,不会出现在产生是左边
  • 非终结符(Non-Terminal Symbol):由其他符号经过“与”、“或”等逻辑组成的符号

语言定义:可由一个非终结符和它的产生式定义

  • 语法树:把一段具体的语言文本,根据产生式以树形结构表示出来

补充:抽象语法树(AST)babel

2.2 写法

BNF:巴克斯-诺尔范式

<中文>::=<句子>|<中文><句子> 
<句子>::=<主语><谓语><宾语>|<主语><谓语> 
<主语>::=<代词>|<名词>|<名词性短语> 
<代词>::="你"|“我”|“他” 
  • 1.非终结符用单括号(<>)包裹
  • 2.用竖线(|)表示或的关系
  • 3.用(::=)表示定义
  • 4.终结符用引号(“”)包裹 特点:定义严格,但产生了较多噪音,没有规定省略语法,导致用大量的或(|)来定义省略语法

EBNF:扩展巴科斯范式

中文::={句子} 句子::=主语 谓语 [宾语] 主语::=代词|名词|名词性短语 代词::=“你”|“我”|“他”

  • 1.非终结符不强制用单括号(<>)包裹
  • 2.用大括号({})表示重复多次(0-N)取代用竖线(|)表示或的关系
  • 3.用方括号([])表示0-1次(可以省略)
  • 4.用(::=)表示定义
  • 5.终结符用引号(“”)包裹

2.3 真实生产式 数学语言四则运算

范式:四则运算 10以内加减乘除

<四则运算表达式>::=<加法算式> 
<加法算式>::=(<加法算式>("+"|"-")<乘法算式>)|<乘法算式> 
<乘法算式>::=(<乘法算式> ("*"|"/") <数字>)|<数字> 
<数字>::={"0","1","2","3","4","5","6","7","8","9"} 

3. 产生式在语言中的应用

3.1 乔姆斯基谱系(形式法文法)

  • 3型正则文法(Regular):若存在递归只接受左递归
<A>::=<A>?  //正确写法 
<A>::=?<A>  //错误写法 
  • 2型:上下文无关文法
<A>::=? 1型:上下文相关文法 ?<A>?::=?<B>? 
  • 0型:无限制文法
?::=? 

3.2 词法和语法(C系语言)

词法(lexer):正则文法(3型)

空白

换行

注释

token 语法(syntax):

上下文无关文法(2型) 语法树

4. 用产生式定义Javascript词法和语法

Javascript词法和语法 简版

InputEelement ::= WhiteSpace | LineTerminater | comment | Token 
WhiteSpace ::= " " | " " LineTerminater ::= "\n" | "\r" | "\f" 
comment ::= SingleLineComment | MultiLineComment 
SingleLineComment ::= "/" "/" <any>* 
MultiLineComment ::= "/" "*" ([^*] | "*" [^/])* "*" "/" 
Token ::= Literal | keywords | Identifier | Punctuator 
Literal ::= NumberLiteral | BooleanLiteral | StringLiteral | NUllLiteral keywords ::= "if" | "else" | "for" | "function" ...... 
Punctuator ::= "+" | "-" | "*"| "/" | "{" | "}" | ...... 
零宽空格FEFF 
Literal 直接量/自变量(代码中直接表示) 
    keywords 关键字 
    Identifier 标识符 
    Punctuator(区别于Operator操作符) 
    ... 

三、一个例子(打包时删除 console )

Identifer 是标识符的意思,变量名、属性名、参数名等各种声明和引用的名字,都是Identifer。我们知道,JS 中的标识符只能包含字母或数字或下划线(“_”)或美元符号(“$”),且不能以数字开头。这是 Identifier 的词法特点。

// webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
    mode: "development", // 开发模式
    entry: {
        index: path.resolve(__dirname, "../src/index.js"),
    }, // 入口文件
    output: {
        filename: "[name].[chunkhash:8].js", // 打包后的文件名称
        path: path.resolve(__dirname, "../dist"), // 打包后的目录
    },

    module: {
        rules: [
            {
                test:/\.js$/,
                use:path.resolve(__dirname,'../lib/drop-console.js')
            },
        ],
    },

    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, "../public/index.html"),
            filename: "index.html",
            chunks: ["index"], // 与入口文件对应的模块名
        }),
    ],
};

// drop-console.js

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
module.exports=function(source){
  const ast = parser.parse(source,{ sourceType: 'module'})
  traverse(ast,{
    CallExpression(path){ 
       if(t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, {name: "console"})){
         path.remove()
       }
  })

  const output = generator(ast, {}, source);
  return output.code
}

image.png

参考:AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解