Jeffrey

Jeffrey 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Jeffrey 回答了问题 · 2月23日

解决请教一个js数据格式化问题

要用递归遍历

关注 5 回答 4

Jeffrey 收藏了文章 · 2月23日

手写一个webpack,看看AST怎么用

本文开始我会围绕webpackbabel写一系列的工程化文章,这两个工具我虽然天天用,但是对他们的原理理解的其实不是很深入,写这些文章的过程其实也是我深入学习的过程。由于webpackbabel的体系太大,知识点众多,不可能一篇文章囊括所有知识点,目前我的计划是从简单入手,先实现一个最简单的可以运行的webpack,然后再看看plugin, loadertree shaking等功能。目前我计划会有这些文章:

  1. 手写最简webpack,也就是本文
  2. webpackplugin实现原理
  3. webpackloader实现原理
  4. webpacktree shaking实现原理
  5. webpackHMR实现原理
  6. babelast原理

所有文章都是原理或者源码解析,欢迎关注~

本文可运行代码已经上传GitHub,大家可以拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack

注意:本文主要讲webpack原理,在实现时并不严谨,而且只处理了importexportdefault情况,如果你想在生产环境使用,请自己添加其他情况的处理和边界判断

为什么要用webpack

笔者刚开始做前端时,其实不知道什么webpack,也不懂模块化,都是html里面直接写script,引入jquery直接干。所以如果一个页面的JS需要依赖jquerylodash,那html可能就长这样:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <script data-original="https://unpkg.com/jquery@3.5.1"></script>
    <script data-original="https://unpkg.com/lodash@4.17.20"></script>
    <script data-original="./src/index.js"></script>
  </head>
  <body>
  </body>
</html>

这样写会导致几个问题:

  1. 单独看index.js不能清晰的找到他到底依赖哪些外部库
  2. script的顺序必须写正确,如果错了就会导致找不到依赖,直接报错
  3. 模块间通信困难,基本都靠往window上注入变量来暴露给外部
  4. 浏览器严格按照script标签来下载代码,有些没用到的代码也会下载下来
  5. 当前端规模变大,JS脚本会显得很杂乱,项目管理混乱

webpack的一个最基本的功能就是来解决上述的情况,允许在JS里面通过import或者require等关键字来显式申明依赖,可以引用第三方库,自己的JS代码间也可以相互引用,这样在实质上就实现了前端代码的模块化。由于历史问题,老版的JS并没有自己模块管理方案,所以社区提出了很多模块管理方案,比如ES2015importCommonJSrequire,另外还有AMDCMD等等。就目前我见到的情况来说,import因为已经成为ES2015标准,所以在客户端广泛使用,而requireNode.js的自带模块管理机制,也有很广泛的用途,而AMDCMD的使用已经很少见了。

但是webpack作为一个开放的模块化工具,他是支持ES6CommonJSAMD等多种标准的,不同的模块化标准有不同的解析方法,本文只会讲ES6标准的import方案,这也是客户端JS使用最多的方案。

简单例子

按照业界惯例,我也用hello world作为一个简单的例子,但是我将这句话拆成了几部分,放到了不同的文件里面。

先来建一个hello.js,只导出一个简单的字符串:

const hello = 'hello';

export default hello;

然后再来一个helloWorld.js,将helloworld拼成一句话,并导出拼接的这个方法:

import hello from './hello';

const world = 'world';

const helloWorld = () => `${hello} ${world}`;

export default helloWorld;

最后再来个index.js,将拼好的hello world插入到页面上去:

import helloWorld from "./helloWorld";

const helloWorldStr = helloWorld();

function component() {
  const element = document.createElement("div");

  element.innerHTML = helloWorldStr;

  return element;
}

document.body.appendChild(component());

现在如果你直接在html里面引用index.js是不能运行成功的,因为大部分浏览器都不支持import这种模块导入。而webpack就是来解决这个问题的,它会将我们模块化的代码转换成浏览器认识的普通JS来执行。

引入webpack

我们印象中webpack的配置很多,很麻烦,但那是因为我们需要开启的功能很多,如果只是解析转换import,配置起来非常简单。

  1. 先把依赖装上吧,这没什么好说的:

    // package.json
    {
      "devDependencies": {
        "webpack": "^5.4.0",
        "webpack-cli": "^4.2.0"
      },
    }
  2. 为了使用方便,再加个build脚本吧:

    // package.json
    {
      "scripts": {
        "build": "webpack"
      },
    }
  3. 最后再简单写下webpack的配置文件就好了:

    // webpack.config.js
    
    const path = require("path");
    
    module.exports = {
      mode: "development",
      devtool: 'source-map',
      entry: "./src/index.js",
      output: {
        filename: "main.js",
        path: path.resolve(__dirname, "dist"),
      },
    };

    这个配置文件里面其实只要指定了入口文件entry和编译后的输出文件目录output就可以正常工作了,这里这个配置的意思是让webpack./src/index.js开始编译,编译后的文件输出到dist/main.js这个文件里面。

    这个配置文件上还有两个配置modedevtool只是我用来方便调试编译后的代码的,mode指定用哪种模式编译,默认是production,会对代码进行压缩和混淆,不好读,所以我设置为development;而devtool是用来控制生成哪种粒度的source map,简单来说,想要更好调试,就要更好的,更清晰的source map,但是编译速度变慢;反之,想要编译速度快,就要选择粒度更粗,更不好读的source mapwebpack提供了很多可供选择的source map具体的可以看他的文档

  4. 然后就可以在dist下面建个index.html来引用编译后的代码了:

    // index.html
    
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
      </head>
      <body>
        <script data-original="main.js"></script>
      </body>
    </html>
  5. 运行下yarn build就会编译我们的代码,然后打开index.html就可以看到效果了。

    image-20210203154111168

深入原理

前面讲的这个例子很简单,一般也满足不了我们实际工程中的需求,但是对于我们理解原理却是一个很好的突破口,毕竟webpack这么庞大的一个体系,我们也不能一口吃个胖子,得一点一点来。

webpack把代码编译成了啥?

为了弄懂他的原理,我们可以直接从编译后的代码入手,先看看他长啥样子,有的朋友可能一提到去看源码,心理就没底,其实我以前也是这样的。但是完全没有必要惧怕,他编译后的代码浏览器能够执行,那肯定就是普通的JS代码,不会藏着这么黑科技。

下面是编译完的代码截图:

image-20210203155553091

虽然我们只有三个简单的JS文件,但是加上webpack自己的逻辑,编译后的文件还是有一百多行代码,所以即使我把具体逻辑折叠起来了,这个截图还是有点长,为了能够看清楚他的结构,我将它分成了4个部分,标记在了截图上,下面我们分别来看看这几个部分吧。

  1. 第一部分其实就是一个对象__webpack_modules__,这个对象里面有三个属性,属性名字是我们三个模块的文件路径,属性的值是一个函数,我们随便展开一个./src/helloWorld.js看下:

    image-20210203161613636

    我们发现这个代码内容跟我们自己写的helloWorld.js非常像:

    image-20210203161902647

    他只是在我们的代码前先调用了__webpack_require__.r__webpack_require__.d,这两个辅助函数我们在后面会看到。

    然后对我们的代码进行了一点修改,将我们的import关键字改成了__webpack_require__函数,并用一个变量_hello__WEBPACK_IMPORTED_MODULE_0__来接收了import进来的内容,后面引用的地方也改成了这个,其他跟这个无关的代码,比如const world = 'world';还是保持原样的。

    这个__webpack_modules__对象存了所有的模块代码,其实对于模块代码的保存,在不同版本的webpack里面实现的方式并不一样,我这个版本是5.4.0,在4.x的版本里面好像是作为数组存下来,然后在最外层的立即执行函数里面以参数的形式传进来的。但是不管是哪种方式,都只是转换然后保存一下模块代码而已。

  2. 第二块代码的核心是__webpack_require__,这个代码展开,瞬间给了我一种熟悉感:

    image-20210203162542359

    来看一下这个流程吧:

    1. 先定义一个变量__webpack_module_cache__作为加载了的模块的缓存
    2. __webpack_require__其实就是用来加载模块的
    3. 加载模块时,先检查缓存中有没有,如果有,就直接返回缓存
    4. 如果缓存没有,就从__webpack_modules__将对应的模块取出来执行
    5. __webpack_modules__就是上面第一块代码里的那个对象,取出的模块其实就是我们自己写的代码,取出执行的也是我们每个模块的代码
    6. 每个模块执行除了执行我们的逻辑外,还会将export的内容添加到module.exports上,这就是前面说的__webpack_require__.d辅助方法的作用。添加到module.exports上其实就是添加到了__webpack_module_cache__缓存上,后面再引用这个模块就直接从缓存拿了。

    这个流程我太熟悉了,因为他简直跟Node.jsCommonJS实现思路一模一样,具体的可以看我之前写的这篇文章:深入Node.js的模块加载机制,手写require函数

  3. 第三块代码其实就是我们前面看到过的几个辅助函数的定义,具体干啥的,其实他的注释已经写了:

    1. __webpack_require__.d:核心其实是Object.defineProperty,主要是用来将我们模块导出的内容添加到全局的__webpack_module_cache__缓存上。

      image-20210203164427116

    2. __webpack_require__.o:其实就是Object.prototype.hasOwnProperty的一个简写而已。

      image-20210203164450385

    3. __webpack_require__.r:这个方法就是给每个模块添加一个属性__esModule,来表明他是一个ES6的模块。

      image-20210203164658054

    4. 第四块就一行代码,调用__webpack_require__加载入口模块,启动执行。

这样我们将代码分成了4块,每块的作用都搞清楚,其实webpack干的事情就清晰了:

  1. import这种浏览器不认识的关键字替换成了__webpack_require__函数调用。
  2. __webpack_require__在实现时采用了类似CommonJS的模块思想。
  3. 一个文件就是一个模块,对应模块缓存上的一个对象。
  4. 当模块代码执行时,会将export的内容添加到这个模块对象上。
  5. 当再次引用一个以前引用过的模块时,会直接从缓存上读取模块。

自己实现一个webpack

现在webpack到底干了什么事情我们已经清楚了,接下来我们就可以自己动手实现一个了。根据前面最终生成的代码结果,我们要实现的代码其实主要分两块:

  1. 遍历所有模块,将每个模块代码读取出来,替换掉importexport关键字,放到__webpack_modules__对象上。
  2. 整个代码里面除了__webpack_modules__和最后启动的入口是变化的,其他代码,像__webpack_require____webpack_require__.r这些方法其实都是固定的,整个代码结构也是固定的,所以完全可以先定义好一个模板。

使用AST解析代码

由于我们需要将import这种代码转换成浏览器能识别的普通JS代码,所以我们首先要能够将代码解析出来。在解析代码的时候,可以将它读出来当成字符串替换,也可以使用更专业的AST来解析。AST全称叫Abstract Syntax Trees,也就是抽象语法树,是一个将代码用树来表示的数据结构,一个代码可以转换成ASTAST又可以转换成代码,而我们熟知的babel其实就可以做这个工作。要生成AST很复杂,涉及到编译原理,但是如果仅仅拿来用就比较简单了,本文就先不涉及复杂的编译原理,而是直接将babel生成好的AST拿来使用。

注意:webpack源码解析AST并不是使用的babel,而是使用的acornwebpack自己实现了一个JavascriptParser类,这个类里面用到了acorn。本文写作时采用了babel,这也是一个大家更熟悉的工具

比如我先将入口文件读出来,然后用babel转换成AST可以直接这样写:

const fs = require("fs");
const parser = require("@babel/parser");

const config = require("../webpack.config"); // 引入配置文件

// 读取入口文件
const fileContent = fs.readFileSync(config.entry, "utf-8");

// 使用babel parser解析AST
const ast = parser.parse(fileContent, { sourceType: "module" });

console.log(ast);   // 把ast打印出来看看

上面代码可以将生成好的ast打印在控制台:

image-20210207153459699

这虽然是一个完整的AST,但是看起来并不清晰,关键数据其实是body字段,这里的body也只是展示了类型名字。所以照着这个写代码其实不好写,这里推荐一个在线工具https://astexplorer.net/,可以很清楚的看到每个节点的内容:

image-20210207154116026

从这个解析出来的AST我们可以看到,body主要有4块代码:

  1. ImportDeclaration:就是第一行的import定义
  2. VariableDeclaration:第三行的一个变量申明
  3. FunctionDeclaration:第五行的一个函数定义
  4. ExpressionStatement:第十三行的一个普通语句

你如果把每个节点展开,会发现他们下面又嵌套了很多其他节点,比如第三行的VariableDeclaration展开后,其实还有个函数调用helloWorld()

image-20210207154741847

使用traverse遍历AST

对于这样一个生成好的AST,我们可以使用@babel/traverse来对他进行遍历和操作,比如我想拿到ImportDeclaration进行操作,就直接这样写:

// 使用babel traverse来遍历ast上的节点
traverse(ast, {
  ImportDeclaration(path) {
    console.log(path.node);
  },
});

上面代码可以拿到所有的import语句:

image-20210207162114290

import转换为函数调用

前面我们说了,我们的目标是将ES6的import

import helloWorld from "./helloWorld";

转换成普通浏览器能识别的函数调用:

var _helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");

为了实现这个功能,我们还需要引入@babel/types,这个库可以帮我们创建新的AST节点,所以这个转换代码写出来就是这样:

const t = require("@babel/types");

// 使用babel traverse来遍历ast上的节点
traverse(ast, {
  ImportDeclaration(p) {
    // 获取被import的文件
    const importFile = p.node.source.value;

    // 获取文件路径
    let importFilePath = path.join(path.dirname(config.entry), importFile);
    importFilePath = `./${importFilePath}.js`;

    // 构建一个变量定义的AST节点
    const variableDeclaration = t.variableDeclaration("var", [
      t.variableDeclarator(
        t.identifier(
          `__${path.basename(importFile)}__WEBPACK_IMPORTED_MODULE_0__`
        ),
        t.callExpression(t.identifier("__webpack_require__"), [
          t.stringLiteral(importFilePath),
        ])
      ),
    ]);

    // 将当前节点替换为变量定义节点
    p.replaceWith(variableDeclaration);
  },
});

上面这段代码我们用了很多@babel/types下面的API,比如t.variableDeclarationt.variableDeclarator,这些都是用来创建对应的节点的,具体的API可以看这里。注意这个代码里面我有很多写死的地方,比如importFilePath生成逻辑,还应该处理多种后缀名的,还有最终生成的变量名_${path.basename(importFile)}__WEBPACK_IMPORTED_MODULE_0__,最后的数字我也是直接写了0,按理来说应该是根据不同的import顺序来生成的,但是本文主要讲webpack的原理,这些细节上我就没花过多时间了。

上面的代码其实是修改了我们的AST,修改后的AST可以用@babel/generator又转换为代码:

const generate  = require('@babel/generator').default;

const newCode = generate(ast).code;
console.log(newCode);

这个打印结果是:

image-20210207172310114

可以看到这个结果里面import helloWorld from "./helloWorld";已经被转换为var __helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");

替换import进来的变量

前面我们将import语句替换成了一个变量定义,变量名字也改为了__helloWorld__WEBPACK_IMPORTED_MODULE_0__,自然要将调用的地方也改了。为了更好的管理,我们将AST遍历,操作以及最后的生成新代码都封装成一个函数吧。

function parseFile(file) {
  // 读取入口文件
  const fileContent = fs.readFileSync(file, "utf-8");

  // 使用babel parser解析AST
  const ast = parser.parse(fileContent, { sourceType: "module" });

  let importFilePath = "";

  // 使用babel traverse来遍历ast上的节点
  traverse(ast, {
    ImportDeclaration(p) {
      // 跟之前一样的
    },
  });

  const newCode = generate(ast).code;

  // 返回一个包含必要信息的新对象
  return {
    file,
    dependcies: [importFilePath],
    code: newCode,
  };
}

然后启动执行的时候就可以调这个函数了

parseFile(config.entry);

拿到的结果跟之前的差不多:

image-20210207173744463

好了,现在需要将使用import的地方也替换了,因为我们已经知道了这个地方是将它作为函数调用的,也就是要将

const helloWorldStr = helloWorld();

转为这个样子:

const helloWorldStr = (0,_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default)();

这行代码的效果其实跟_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default()是一样的,为啥在前面包个(0, ),我也不知道,有知道的大佬告诉下我呗。

所以我们在traverse里面加一个CallExpression

  traverse(ast, {
    ImportDeclaration(p) {
      // 跟前面的差不多,省略了
    },
    CallExpression(p) {
      // 如果调用的是import进来的函数
      if (p.node.callee.name === importVarName) {
        // 就将它替换为转换后的函数名字
        p.node.callee.name = `${importCovertVarName}.default`;
      }
    },
  });

这样转换后,我们再重新生成一下代码,已经像那么个样子了:

image-20210207175649607

递归解析多个文件

现在我们有了一个parseFile方法来解析处理入口文件,但是我们的文件其实不止一个,我们应该依据模块的依赖关系,递归的将所有的模块都解析了。要实现递归解析也不复杂,因为前面的parseFile的依赖dependcies已经返回了:

  1. 我们创建一个数组存放文件的解析结果,初始状态下他只有入口文件的解析结果
  2. 根据入口文件的解析结果,可以拿到入口文件的依赖
  3. 解析所有的依赖,将结果继续加到解析结果数组里面
  4. 一直循环这个解析结果数组,将里面的依赖文件解析完
  5. 最后将解析结果数组返回就行

写成代码就是这样:

function parseFiles(entryFile) {
  const entryRes = parseFile(entryFile); // 解析入口文件
  const results = [entryRes]; // 将解析结果放入一个数组

  // 循环结果数组,将它的依赖全部拿出来解析
  for (const res of results) {
    const dependencies = res.dependencies;
    dependencies.map((dependency) => {
      if (dependency) {
        const ast = parseFile(dependency);
        results.push(ast);
      }
    });
  }

  return results;
}

然后就可以调用这个方法解析所有文件了:

const allAst = parseFiles(config.entry);
console.log(allAst);

看看解析结果吧:

image-20210208152330212

这个结果其实跟我们最终需要生成的__webpack_modules__已经很像了,但是还有两块没有处理:

  1. 一个是import进来的内容作为变量使用,比如

    import hello from './hello';
    
    const world = 'world';
    
    const helloWorld = () => `${hello} ${world}`;
  2. 另一个就是export语句还没处理

替换import进来的变量(作为变量调用)

前面我们已经用CallExpression处理过作为函数使用的import变量了,现在要处理作为变量使用的其实用Identifier处理下就行了,处理逻辑跟之前的CallExpression差不多:

  traverse(ast, {
    ImportDeclaration(p) {
      // 跟以前一样的
    },
    CallExpression(p) {
            // 跟以前一样的
    },
    Identifier(p) {
      // 如果调用的是import进来的变量
      if (p.node.name === importVarName) {
        // 就将它替换为转换后的变量名字
        p.node.name = `${importCovertVarName}.default`;
      }
    },
  });

现在再运行下,import进来的变量名字已经变掉了:

image-20210208153942630

替换export语句

从我们需要生成的结果来看,export需要进行两个处理:

  1. 如果一个文件有export default,需要添加一个__webpack_require__.d的辅助方法调用,内容都是固定的,加上就行。
  2. export语句转换为普通的变量定义。

对应生成结果上的这两个:

image-20210208154959592

要处理export语句,在遍历ast的时候添加ExportDefaultDeclaration就行了:

  traverse(ast, {
    ImportDeclaration(p) {
      // 跟以前一样的
    },
    CallExpression(p) {
            // 跟以前一样的
    },
    Identifier(p) {
      // 跟以前一样的
    },
    ExportDefaultDeclaration(p) {
      hasExport = true; // 先标记是否有export

      // 跟前面import类似的,创建一个变量定义节点
      const variableDeclaration = t.variableDeclaration("const", [
        t.variableDeclarator(
          t.identifier("__WEBPACK_DEFAULT_EXPORT__"),
          t.identifier(p.node.declaration.name)
        ),
      ]);

      // 将当前节点替换为变量定义节点
      p.replaceWith(variableDeclaration);
    },
  });

然后再运行下就可以看到export语句被替换了:

image-20210208160244276

然后就是根据hasExport变量判断在AST转换为代码的时候要不要加__webpack_require__.d辅助函数:

const EXPORT_DEFAULT_FUN = `
__webpack_require__.d(__webpack_exports__, {
   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
});\n
`;

function parseFile(file) {
  // 省略其他代码
  // ......
  
  let newCode = generate(ast).code;

  if (hasExport) {
    newCode = `${EXPORT_DEFAULT_FUN} ${newCode}`;
  }
}

最后生成的代码里面export也就处理好了:

image-20210208161030554

__webpack_require__.r的调用添上吧

前面说了,最终生成的代码,每个模块前面都有个__webpack_require__.r的调用

image-20210208161321401

这个只是拿来给模块添加一个__esModule标记的,我们也给他加上吧,直接在前面export辅助方法后面加点代码就行了:

const ESMODULE_TAG_FUN = `
__webpack_require__.r(__webpack_exports__);\n
`;

function parseFile(file) {
  // 省略其他代码
  // ......
  
  let newCode = generate(ast).code;

  if (hasExport) {
    newCode = `${EXPORT_DEFAULT_FUN} ${newCode}`;
  }
  
  // 下面添加模块标记代码
  newCode = `${ESMODULE_TAG_FUN} ${newCode}`;
}

再运行下看看,这个代码也加上了:

image-20210208161721369

创建代码模板

到现在,最难的一块,模块代码的解析和转换我们其实已经完成了。下面要做的工作就比较简单了,因为最终生成的代码里面,各种辅助方法都是固定的,动态的部分就是前面解析的模块和入口文件。所以我们可以创建一个这样的模板,将动态的部分标记出来就行,其他不变的部分写死。这个模板文件的处理,你可以将它读进来作为字符串处理,也可以用模板引擎,我这里采用ejs模板引擎:

// 模板文件,直接从webpack生成结果抄过来,改改就行
/******/ (() => { // webpackBootstrap
/******/     "use strict";
// 需要替换的__TO_REPLACE_WEBPACK_MODULES__
/******/     var __webpack_modules__ = ({
                <% __TO_REPLACE_WEBPACK_MODULES__.map(item => { %>
                    '<%- item.file %>' : 
                    ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
                        <%- item.code %>
                    }),
                <% }) %>
            });
// 省略中间的辅助方法
    /************************************************************************/
    /******/     // startup
    /******/     // Load entry module
// 需要替换的__TO_REPLACE_WEBPACK_ENTRY
    /******/     __webpack_require__('<%- __TO_REPLACE_WEBPACK_ENTRY__ %>');
    /******/     // This entry module used 'exports' so it can't be inlined
    /******/ })()
    ;
    //# sourceMappingURL=main.js.map

生成最终的代码

生成最终代码的思路就是:

  1. 模板里面用__TO_REPLACE_WEBPACK_MODULES__来生成最终的__webpack_modules__
  2. 模板里面用__TO_REPLACE_WEBPACK_ENTRY__来替代动态的入口文件
  3. webpack代码里面使用前面生成好的AST数组来替换模板的__TO_REPLACE_WEBPACK_MODULES__
  4. webpack代码里面使用前面拿到的入口文件来替代模板的__TO_REPLACE_WEBPACK_ENTRY__
  5. 使用ejs来生成最终的代码

所以代码就是:

// 使用ejs将上面解析好的ast传递给模板
// 返回最终生成的代码
function generateCode(allAst, entry) {
  const temlateFile = fs.readFileSync(
    path.join(__dirname, "./template.js"),
    "utf-8"
  );

  const codes = ejs.render(temlateFile, {
    __TO_REPLACE_WEBPACK_MODULES__: allAst,
    __TO_REPLACE_WEBPACK_ENTRY__: entry,
  });

  return codes;
}

大功告成

最后将ejs生成好的代码写入配置的输出路径就行了:

const codes = generateCode(allAst, config.entry);

fs.writeFileSync(path.join(config.output.path, config.output.filename), codes);

然后就可以使用我们自己的webpack来编译代码,最后就可以像之前那样打开我们的html看看效果了:

image-20210218160539306

总结

本文使用简单质朴的方式讲述了webpack的基本原理,并自己手写实现了一个基本的支持importexportdefaultwebpack

本文可运行代码已经上传GitHub,大家可以拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack

下面再就本文的要点进行下总结:

  1. webpack最基本的功能其实是将JS的高级模块化语句,importrequire之类的转换为浏览器能认识的普通函数调用语句。
  2. 要进行语言代码的转换,我们需要对代码进行解析。
  3. 常用的解析手段是AST,也就是将代码转换为抽象语法树
  4. AST是一个描述代码结构的树形数据结构,代码可以转换为ASTAST也可以转换为代码。
  5. babel可以将代码转换为AST,但是webpack官方并没有使用babel,而是基于acorn自己实现了一个JavascriptParser
  6. 本文从webpack构建的结果入手,也使用AST自己生成了一个类似的代码。
  7. webpack最终生成的代码其实分为动态和固定的两部分,我们将固定的部分写入一个模板,动态的部分在模板里面使用ejs占位。
  8. 生成代码动态部分需要借助babel来生成AST,并对其进行修改,最后再使用babel将其生成新的代码。
  9. 在生成AST时,我们从配置的入口文件开始,递归的解析所有文件。即解析入口文件的时候,将它的依赖记录下来,入口文件解析完后就去解析他的依赖文件,在解析他的依赖文件时,将依赖的依赖也记录下来,后面继续解析。重复这种步骤,直到所有依赖解析完。
  10. 动态代码生成好后,使用ejs将其写入模板,以生成最终的代码。
  11. 如果要支持require或者AMD,其实思路是类似的,最终生成的代码也是差不多的,主要的差别在AST解析那一块。

参考资料

  1. babel操作AST文档
  2. webpack源码
  3. webpack官方文档

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

欢迎关注我的公众号进击的大前端第一时间获取高质量原创~

“前端进阶知识”系列文章源码地址: https://github.com/dennis-jiang/Front-End-Knowledges

1270_300二维码_2.png

查看原文

Jeffrey 关注了用户 · 2月23日

蒋鹏飞 @jiangpengfei_5ecce944a3d8a

前端工程师,底层技术人。
思否2020年度“Top Writer”!
掘金“优秀作者”!
开源中国2020年度“优秀源创作者”!
分享各种大前端进阶知识!
关注公众号【进击的大前端】第一时间获取高质量原创。
更多文章和示例源码请看:https://github.com/dennis-jia...

关注 1950

Jeffrey 关注了标签 · 2020-05-13

前端

Web前端开发是从网页制作演变而来的,名称上有很明显的时代特征。在互联网的演化进程中,网页制作是Web 1.0时代的产物,那时网站的主要内容都是静态的,用户使用网站的行为也以浏览为主。2005年以后,互联网进入Web 2.0时代,各种类似桌面软件的Web应用大量涌现,网站的前端由此发生了翻天覆地的变化。网页不再只是承载单一的文字和图片,各种富媒体让网页的内容更加生动,网页上软件化的交互形式为用户提供了更好的使用体验,这些都是基于前端技术实现的。

Web前端优化
  1. 尽量减少HTTP请求 (Make Fewer HTTP Requests)
  2. 减少 DNS 查找 (Reduce DNS Lookups)
  3. 避免重定向 (Avoid Redirects)
  4. 使得 Ajax 可缓存 (Make Ajax Cacheable)
  5. 延迟载入组件 (Post-load Components)
  6. 预载入组件 (Preload Components)
  7. 减少 DOM 元素数量 (Reduce the Number of DOM Elements)
  8. 切分组件到多个域 (Split Components Across Domains)
  9. 最小化 iframe 的数量 (Minimize the Number of iframes)
  10. 杜绝 http 404 错误 (No 404s)

关注 189605

Jeffrey 关注了标签 · 2020-05-13

vue.js

Reactive Components for Modern Web Interfaces.

Vue.js 是一个用于创建 web 交互界面的。其特点是

  • 简洁 HTML 模板 + JSON 数据,再创建一个 Vue 实例,就这么简单。
  • 数据驱动 自动追踪依赖的模板表达式和计算属性。
  • 组件化 用解耦、可复用的组件来构造界面。
  • 轻量 ~24kb min+gzip,无依赖。
  • 快速 精确有效的异步批量 DOM 更新。
  • 模块友好 通过 NPM 或 Bower 安装,无缝融入你的工作流。

官网:https://vuejs.org
GitHub:https://github.com/vuejs/vue

关注 132812

Jeffrey 关注了标签 · 2020-05-13

react.js

React (sometimes styled React.js or ReactJS) is an open-source JavaScript library for creating user interfaces that aims to address challenges encountered in developing single-page applications. It is maintained by Facebook, Instagram and a community of individual developers and corporations.

关注 65440

Jeffrey 关注了标签 · 2020-05-13

程序员

一种近几十年来出现的新物种,是工业革命的产物。英文(Programmer Monkey)是一种非常特殊的、可以从事程序开发、维护的动物。一般分为程序设计猿和程序编码猿,但两者的界限并不非常清楚,都可以进行开发、维护工作,特别是在中国,而且最重要的一点,二者都是一种非常悲剧的存在。

国外的程序员节

国外的程序员节,(英语:Programmer Day,俄语:День программи́ста)是一个俄罗斯官方节日,日期是每年的第 256(0x100) 天,也就是平年的 9 月 13 日和闰年的 9 月 12 日,选择 256 是因为它是 2 的 8 次方,比 365 少的 2 的最大幂。

1024程序员节,中国程序员节

1024是2的十次方,二进制计数的基本计量单位之一。程序员(英文Programmer)是从事程序开发、维护的专业人员。程序员就像是一个个1024,以最低调、踏实、核心的功能模块搭建起这个科技世界。1GB=1024M,而1GB与1级谐音,也有一级棒的意思。

从2012年,SegmentFault 创办开始我们就从网络上引导社区的开发者,发展成中国程序员的节日 :) 计划以后每年10月24日定义为程序员节。以一个节日的形式,向通过Coding 改变世界,也以实际行动在浮躁的世界里,固执地坚持自己对于知识、技术和创新追求的程序员们表示致敬。并于之后的最为临近的周末为程序员们举行了一个盛大的狂欢派对。

2015的10月24日,我们SegmentFault 也在5个城市同时举办黑客马拉松这个特殊的形式,聚集开发者开一个编程大爬梯。

特别推荐:

【SF 黑客马拉松】:http://segmentfault.com/hacka...
【1024程序员闯关秀】小游戏,欢迎来挑战 http://segmentfault.com/game/

  • SF 开发者交流群:206236214
  • 黑客马拉松交流群:280915731
  • 开源硬件交流群:372308136
  • Android 开发者交流群:207895295
  • iOS 开发者交流群:372279630
  • 前端开发者群:174851511

欢迎开发者加入~

交流群信息


程序员相关问题集锦:

  1. 《程序员如何选择自己的第二语言》
  2. 《如何成为一名专业的程序员?》
  3. 《如何用各种编程语言书写hello world》
  4. 《程序员们最常说的谎话是什么?》
  5. 《怎么加入一个开源项目?》
  6. 《是要精于单挑,还是要善于合作?》
  7. 《来秀一下你屎一般的代码...》
  8. 《如何区分 IT 青年的“普通/文艺/二逼”属性?》
  9. 程序员必读书籍有哪些?
  10. 你经常访问的技术社区或者技术博客(IT类)有哪些?
  11. 如何一行代码弄崩你的程序?我先来一发
  12. 编程基础指的是什么?
  13. 后端零起步:学哪一种比较好?
  14. 大家都用什么键盘写代码的?

爱因斯坦

程序猿崛起

关注 149691

Jeffrey 关注了标签 · 2020-05-13

javascript

JavaScript 是一门弱类型的动态脚本语言,支持多种编程范式,包括面向对象和函数式编程,被广泛用于 Web 开发。

一般来说,完整的JavaScript包括以下几个部分:

  • ECMAScript,描述了该语言的语法和基本对象
  • 文档对象模型(DOM),描述处理网页内容的方法和接口
  • 浏览器对象模型(BOM),描述与浏览器进行交互的方法和接口

它的基本特点如下:

  • 是一种解释性脚本语言(代码不进行预编译)。
  • 主要用来向HTML页面添加交互行为。
  • 可以直接嵌入HTML页面,但写成单独的js文件有利于结构和行为的分离。

JavaScript常用来完成以下任务:

  • 嵌入动态文本于HTML页面
  • 对浏览器事件作出响应
  • 读写HTML元素
  • 在数据被提交到服务器之前验证数据
  • 检测访客的浏览器信息

《 Javascript 优点在整个语言中占多大比例?

关注 171562

认证与成就

  • 获得 0 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-05-13
个人主页被 45 人浏览