webpack系列学习-详细的实现简易webpack
字节爱好者 357 5

前言:笔者把学习的webpack知识从基础到原理写个系列,以便回顾。希望能帮助到更多正在学习webpack的小伙伴。

webpack系列学习-初体验

webpack系列学习-基本用法一

webpack系列学习-各种loader使用

webpack系列学习-热更新和压缩

webpack系列学习-使用eslint和发布npm包

webpack系列学习-构建webpack配置

前言:实现一个如下功能的简易webpack

1.将ES6语法转换成ES5的语法

  • 通过 Babylon 生成AST
  • 通过 babel-core 将AST 重新生成源码

    2.分析模块之间的依赖关系

  • 通过 babel-traverse 的 ImportDeclaration方法获取依赖属性

    3.生成的js文件可以在浏览器运行

现在开始:

初始化项目

mkdir simple_webpack
cd simple_webpack
npm init -y

新建项目目录

webpack系列学习-详细的实现简易webpack

  • 目录解释:

    • lib:simple_webpack的源码
    • src:业务代码的入口
    • simplepack.config.js:相当于webpack.config.js
  • 首先在simplepack.config.js中配置输入和输出

const path = require('path');

module.exports = { entry: path.join(dirname, './src/index.js'), output: { path: path.join(dirname, './dist'), filename: 'main.js', }, };

+ 在src目录下创建index.js和greeting.js
```js

// index.js
import { greeting } from './greeting';

document.write(greeting('curry'));

// greeting.js
// 使用ES6语法
export function greeting(name) {
  return `hello ${name}`;
}

在lib目录下创建文件

webpack系列学习-详细的实现简易webpack

  • 目录解释:
    • index.js:入口文件
    • parser.js:解析AST语法树,转换成源码,将ES6转换成ES5,分析依赖
    • compiler.js:执行最后文件的输出

开始源码编写

  • 首先在compiler.js输出一个Compiler类,包括下面的属性和方法

    // compiler.js
    module.exports = class Compiler {
    constructor(options) {
      // 这里的options就是simplepack导出的配置项
      const { entry, output } = options;
      this.entry = entry;
      this.output = output;
    }
    
    run() {}
    
    // 模块构建
    buildModule() {}
    
    // 输出文件
    emitFiles() {}
    };
  • 在index.js中实例化Compiler类

    const Compiler = require('./compiler');
    const options = require('../simplepack.config');
    

new Compiler(options);

+ 然后编写parser.js,这里做的是转换成AST树,将ES6转换成ES5,分析依赖
```js
// parser.js

module.exports = {
  // 生成AST树,根据文件路径生成
  getAST: path => {

  }
}
  • 生成AST树,需要借助babylon,先安装下。
    npm i babylon -S
  • 继续编写getAST方法

const babylon = require('babylon'); // 引入babylon const fs = require('fs'); // 引入node中fs模块

module.exports = { // 生成AST树 getAST: path => { // 同步读取文件 const source = fs.readFileSync(path, 'utf-8'); // 使用babylon的parse方法进行生成AST return babylon.parse(source, { sourceType: 'module', }); }, };

+ 现在getAST方法写好了,我们来测试下。在lib目录下创建test.js
```js
// lib/test.js

const { getAST } = require('./parser');
const path = require('path');

console.log(getAST(path.join(__dirname, '../src/index.js')));
  • 执行node lib/test.js看下转换效果 webpack系列学习-详细的实现简易webpack

  • 下面接着写 分析依赖的方法:

// lib/parser.js

module.exports = { // ... getDependencies: () => {}, }

+ 进行依赖分析,需要借助babel-traverse,这里安装下。

npm i babel-traverse -S

+ 接着回来写分析依赖的方法
```js
const traverse = require('babel-traverse').default;

module.exports = {
  // ...
  getDependencies: ast => {
    const dependencies = [];
    traverse(ast, {
      // ImportDeclaration:分析import语句
      ImportDeclaration: ({ node }) => {
        // 将依赖push到dependencies中
        dependencies.push(node.source.value);
      },
    });
    // 将依赖返回
    return dependencies;
  },
};
  • 接下来,测试下这个方法
    // lib/test.js
    const { getAST, getDependencies } = require('./parser');
    const path = require('path');
    

const ast = getAST(path.join(__dirname, '../src/index.js')); console.log(getDependencies(ast));

+ 执行 node lib/test.js,可以看到出现了依赖文件
![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/6213b7605b5c3f52eb9d2b7f189a2d7b.png)
+ 现在把ES6转成了AST树,接下来将AST树转换成源码,也就是ES5
```js

// lib/parser.js

module.exports = {
  // 将AST树转换成ES5
  transform: (ast) => {},
}
  • 将AST树转换成ES5,需要借助babel-core,先安装下
    npm i babel-core -S
  • 回来写transform方法
    const { transformFromAst } = require('babel-core');
    module.exports = {
    transform: ast => {
      const { code } = transformFromAst(ast, null, {
        presets: ['env'],
      });
      return code;
    },
    }
  • 此时安装下env
    npm i @babel/preset-env babel-preset-env -S
  • 在根目录下创建.babelrc

{ "presets": ["@babel/preset-env"] }

+ 测试下transform方法
```js

// lib/test.js
const { getAST, getDependencies, transform } = require('./parser');
const path = require('path');

const ast = getAST(path.join(__dirname, '../src/index.js'));
const dep = getDependencies(ast);
const source = transform(ast);
console.log(source);
  • 执行 node lib/test.js , 可以看到打印出了源码 webpack系列学习-详细的实现简易webpack
  • 到此就写完了parser.js中的方法。

接下来开始写compiler.js中的方法

  • 首先需要在index.js中执行run方法

// lib/index.js const Compiler = require('./compiler'); const options = require('../simplepack.config');

new Compiler(options).run();

+ 开始写compiler.js中的buildModule
```js
module.exports = class Compiler {
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
  }
  run() {
    const entryModule = this.buildModule(this.entry, true);
  }

  // 模块构建
  buildModule(filename, isEntry) {
    let ast;
    if (isEntry) {
      ast = getAST(filename);
    } else {
      // 这里需要找到绝对路径,通过path转换下
      const absolutePath = path.join(process.cwd(), './src', filename);
      ast = getAST(absolutePath);
    }
    return {
      filename,
      dependencies: getDependencies(ast),
      source: transform(ast),
    };
  }

  // 输出文件
  emitFiles() {}
};
  • 接着写run方法,此时我们先可以打印下entryModule,查看结果,是在buildModule中返回的。 webpack系列学习-详细的实现简易webpack

  • 我们需要把依赖全部放到一个数组中,定义this.modules来填充依赖。

    // lib/compiler.js
    const { getAST, getDependencies, transform } = require('./parser');
    const path = require('path');
    module.exports = class Compiler {
    constructor(options) {
      const { entry, output } = options;
      this.entry = entry;
      this.output = output;
      this.modules = [];
    }
    run() {
      const entryModule = this.buildModule(this.entry, true);
      // 把依赖全部push到modules中
      this.modules.push(entryModule);
      // 遍历递归
      this.modules.map(_module => {
        _module.dependencies.map(dependency => {
          this.modules.push(this.buildModule(dependency));
        });
      });
      console.log(this.modules)
    }
    
    // 模块构建
    buildModule(filename, isEntry) {
      let ast;
      if (isEntry) {
        ast = getAST(filename);
      } else {
        const absolutePath = path.join(process.cwd(), './src', filename);
        ast = getAST(absolutePath);
      }
      return {
        filename,
        dependencies: getDependencies(ast),
        source: transform(ast),
      };
    }
    
    // 输出文件
    emitFiles() {}
    };
  • 打印下modules webpack系列学习-详细的实现简易webpack

  • 接下来,拿到所有依赖后,就要输出文件,在run方法中执行this.emitFiles方法

modules.exports = { run(){ // ... this.emitFiles() }, emitFiles() { const outputPath = path.join(this.output.path, this.output.filename); // let modules = ''; this.modules.map(_module => { modules += '${_module.filename}': function(require,module,exports){${_module.source}},; }); // 自执行 const bundle = (function(modules){ function require(filename){ var fn = modules[filename]; var module = { exports: {}}; fn(require, module, module.exports) return module.exports; } require('${this.entry}') })({${modules}});

console.log('bundle', bundle);
fs.writeFileSync(outputPath, bundle, 'utf-8');

} }

```

  • 打印下最后的bundle,如下: webpack系列学习-详细的实现简易webpack

  • 手动创建下dist目录,执行node lib/index.js。可以看到dist目录下就有了打包好的文件 webpack系列学习-详细的实现简易webpack

  • 在dist创建index.html,并引入main.js,在浏览器中打开index.html查看效果 webpack系列学习-详细的实现简易webpack

  • 至此完成了一个简易的webpack

以上代码放到了github

可以下载调试。

评论区

索引目录