编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

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

wxchong 2024-07-02 03:09:09 开源技术 13 ℃ 0 评论

前言:笔者把学习的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

    新建项目目录

    目录解释:

    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

    // index.js
    import { greeting } from './greeting';
    
    document.write(greeting('curry'));
    
    // greeting.js
    // 使用ES6语法
    export function greeting(name) {
      return `hello ${name}`;
    }

    在lib目录下创建文件

    目录解释:

  • 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,分析依赖

    // 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

    // lib/test.js
    
    const { getAST } = require('./parser');
    const path = require('path');
    
    console.log(getAST(path.join(__dirname, '../src/index.js')));

    执行node lib/test.js看下转换效果


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

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

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

    npm i babel-traverse -S

    接着回来写分析依赖的方法

    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,可以看到出现了依赖文件


    现在把ES6转成了AST树,接下来将AST树转换成源码,也就是ES5

    // 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方法

    
    // 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 , 可以看到打印出了源码

    到此就写完了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

    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中返回的。


    我们需要把依赖全部放到一个数组中,定义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


    接下来,拿到所有依赖后,就要输出文件,在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,如下:

    手动创建下dist目录,执行node lib/index.js。可以看到dist目录下就有了打包好的文件

    在dist创建index.html,并引入main.js,在浏览器中打开index.html查看效果

    至此完成了一个简易的webpack

    Tags:

    本文暂时没有评论,来添加一个吧(●'◡'●)

    欢迎 发表评论:

    最近发表
    标签列表