Webpack 简版手写

本文将以​  Webpack 5为例,通过简单实现,整体介绍一下Wwebpack 构建过程与实现细节。

1. 项目结构

 simple-webpack
   ├── dist
   ├── src
   │   ├── index.js
   │   ├── module1.js
   │   └── module2.js
   ├── loaders
   │   ├── babelLoader.js
   │   └── exampleLoader.js
   ├── plugins
   │   └── examplePlugin.js
   ├── webpack.config.js
   └── index.js

2. 设置项目和安装依赖

在项目根目录下运行以下命令初始化项目并安装依赖:

npm init -y

npm install @babel/core @babel/preset-env @babel/traverse babylon

3. 实现核心模块

3.1. 创建createAsset函数

在 index.js 文件中实现 createAsset 函数,用于读取和解析模块,并根据 loader 进行处理:

const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;

let ID = 0;

function applyLoaders(source, filename, loaders) {
  for (let i = loaders.length - 1; i >= 0; i--) {
    const loader = loaders[i];
    if (loader.test.test(filename)) {
      const loaderFn = require(loader.use);
      source = loaderFn(source);
    }
  }
  return source;
}

function createAsset(filename, loaders) {
  let content = fs.readFileSync(filename, 'utf-8');
  content = applyLoaders(content, filename, loaders);
  const ast = babylon.parse(content, { sourceType: 'module' });
  const dependencies = [];
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    },
  });
  const id = ID++;
  return {
    id,
    filename,
    dependencies,
    code: content,
  };
}
module.exports = { createAsset };

3.2. 创建createGraph函数

在 index.js 文件中实现 createGraph 函数,用于构建依赖图:

const { createAsset } = require('./index');
const path = require('path');

function createGraph(entry, loaders) {
  const mainAsset = createAsset(entry, loaders);
  const queue = [mainAsset];

  for (const asset of queue) {
    const dirname = path.dirname(asset.filename);
    asset.mapping = {};

    asset.dependencies.forEach((relativePath) => {
      const absolutePath = path.join(dirname, relativePath);
      const child = createAsset(absolutePath, loaders);
      asset.mapping[relativePath] = child.id;
      queue.push(child);
    });
  }

  return queue;
}
module.exports = { createGraph };

3.3. 创建bundle函数

在index.js文件中实现bundle函数,用于生成打包文件:

const { createGraph } = require('./index');

function bundle(graph) {
  let modules = '';

  graph.forEach((mod) => {
    modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });

  return `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];

        function localRequire(name) {
          return require(mapping[name]);
        }

        const module = { exports: {} };
        fn(localRequire, module, module.exports);

        return module.exports;
      }

      require(0);
    })({${modules}})
  `;
}

module.exports = { bundle };

4. 编写配置文件

在webpack.config.js中定义基本的配置:

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: './loaders/babelLoader.js'
      },
      {
        test: /.js$/,
        use: './loaders/exampleLoader.js'
      }
    ]
  },
  plugins: [
    new (require('./plugins/examplePlugin.js'))()
  ]
};

5. 实现Loader和Plugin

5.1. Babel Loader

在loaders/babelLoader.js中实现Babel Loader:

const babel = require('@babel/core');

module.exports = function (source) {
  const result = babel.transform(source, {
    presets: ['@babel/preset-env']
  });
  return result.code;
};

5.2. Example Loader

在 loaders/exampleLoader.js中实现一个简单的 Loader:

module.exports = function (source) {
  // Example loader logic
  return source.replace(/console.log(/g, 'console.warn(');
};

5.3. Plugin

在 plugins/examplePlugin.js 中实现一个简单的 Plugin:

class ExamplePlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('ExamplePlugin', (compilation, callback) => {
      console.log('ExamplePlugin is working!');
      callback();
    });
  }
}
module.exports = ExamplePlugin;

6. 编写启动脚本

在 index.js 文件中整合所有模块,执行打包过程:

const fs = require('fs');
const path = require('path');
const { createGraph, bundle } = require('./index');
const config = require('./webpack.config');

function run() {
  const graph = createGraph(config.entry, config.module.rules);
  const result = bundle(graph);

  if (!fs.existsSync(config.output.path)) {
    fs.mkdirSync(config.output.path);
  }

  fs.writeFileSync(path.join(config.output.path, config.output.filename), result);
  console.log('Bundle created successfully!');
}

run();

7. 示例源码

在 src 目录中编写示例 JavaScript 文件:

7.1. src/index.js

import module1 from './module1.js';
import module2 from './module2.js';

console.warn('This is the entry file.');
module1();
module2();

7.2. src/module1.js

export default function module1() {
  console.warn('This is module 1.');
}

7.3. src/module2.js

export default function module2() {
  console.warn('This is module 2.');
}

8. 运行示例

在项目根目录下运行以下命令打包项目:

node index.js

运行成功后,dist 目录下会生成 bundle.js 文件,其中包含打包后的代码。

9. 补充资料

Webpack 官方文档:webpack

Webpack 核心概念:Concepts | webpack

Webpack 相关库:Awesome webpack | webpack

官方 loader:Loaders | webpack

编写 loader 指南:Writing a Loader | webpack

编写 plugin 指南:Writing a Plugin | webpack

loader 执行原理: https://github.com/webpack/webpack/blob/40479852054817bb765631217c27739968768a0/lib/NormalModule.js#L903

官方 plugin:Plugins | webpack

插件机制实现:https://github.com/webpack/tapable

钩子列表: https://github.com/webpack/webpack/blob/40479852054817bb765631217c27739968768a0/lib/Compiler.js#L140

插件模式补充:Plugin Patterns | webpack

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容