Skip to content
Go back

Implement a simpleist webpack

Published:  at  05:47 PM

文章目录

一、Webpack核心原理

1.1 模块打包机制

Webpack的核心功能是将多个模块打包成一个或多个文件。它通过以下方式实现:

  1. 模块解析:将ES6模块、CommonJS、AMD等模块化规范的代码解析成统一的模块格式
  2. 依赖收集:分析模块之间的依赖关系
  3. 代码转换:将代码转换成浏览器可执行的代码
  4. 资源合并:将多个模块合并成一个文件

1.2 依赖解析

Webpack使用深度优先搜索(DFS)来解析模块依赖:

// 依赖解析的核心逻辑
function parseDependencies(modulePath, context) {
  const module = {
    path: modulePath,
    dependencies: [],
    code: null
  };

  // 读取模块内容
  const content = fs.readFileSync(modulePath, 'utf-8');

  // 解析import/export语句
  const dependencies = parseImports(content);

  // 递归解析依赖
  dependencies.forEach(dep => {
    const depPath = resolvePath(dep, context);
    module.dependencies.push(parseDependencies(depPath, depPath));
  });

  return module;
}

1.3 代码分割

代码分割是Webpack的重要特性,它允许将代码分割成多个chunk,实现按需加载。

1.4 模块热替换(HMR)

HMR允许在不刷新页面的情况下更新模块,提升开发体验。

二、Webpack架构设计

2.1 核心流程

Webpack的构建流程主要包括以下步骤:

  1. 初始化阶段:创建Compiler实例,加载配置
  2. 编译阶段:从入口文件开始编译,创建Module实例
  3. 处理阶段:调用Loader处理模块,解析依赖
  4. 优化阶段:优化代码,生成Hash
  5. 输出阶段:写入文件系统

2.2 事件流机制

Webpack基于Tapable实现事件流机制,允许插件在构建的不同阶段执行。

2.3 插件系统

插件系统是Webpack扩展性的核心,通过插件可以介入构建的各个阶段。

2.4 配置系统

Webpack支持多种配置方式,包括配置文件、命令行参数、Node.js API等。

三、Loader实现原理

3.1 Loader的基本概念

Loader是Webpack处理非JavaScript文件的核心机制。它本质上是一个函数,接收源文件内容,返回处理后的结果。

3.2 常用Loader的实现

3.2.1 babel-loader

// babel-loader实现
class BabelLoader {
  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap('BabelLoader', factory => {
      factory.hooks.beforeResolve.tap('BabelLoader', result => {
        if (result && /\.js$/.test(result.request)) {
          result.loaders.unshift({
            loader: path.resolve(__dirname, './babel-loader.js'),
            options: {}
          });
        }
        return result;
      });
    });
  }
}

3.2.2 css-loader

// css-loader实现
function cssLoader(source) {
  // 处理@import和url()语法
  const cssWithImports = source.replace(/@import\s+(['"])(.+?)\1/g, (match, quote, url) => {
    return `exports.i(require(${quote}${url}${quote}));`;
  });

  // 处理url()语法
  const processedCSS = cssWithImports.replace(/url\(\s*['"]?(.+?)['"]?\s*\)/g, (match, url) => {
    return `url(${JSON.stringify(url)})`;
  });

  // 返回处理后的CSS
  return `
    const css = ${JSON.stringify(processedCSS)};
    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);
    module.exports = css;
  `;
}

3.3 Loader链式调用

Loader可以链式调用,前一个Loader的输出作为下一个Loader的输入。

3.4 异步Loader

异步Loader使用callback或Promise来处理异步操作。

四、Plugin实现原理

4.1 Plugin的基本概念

Plugin是Webpack的扩展机制,通过插件可以介入构建的各个阶段。

4.2 常用Plugin的实现

4.2.1 HtmlWebpackPlugin

// HtmlWebpackPlugin实现
class HtmlWebpackPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('HtmlWebpackPlugin', compilation => {
      // 在编译时注入HTML模板
      compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tapAsync(
        'HtmlWebpackPlugin',
        (htmlPluginData, callback) => {
          // 生成HTML内容
          const html = generateHTML(htmlPluginData);
          htmlPluginData.html = html;
          callback(null, htmlPluginData);
        }
      );
    });
  }
}

4.2.2 MiniCssExtractPlugin

// MiniCssExtractPlugin实现
class MiniCssExtractPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('MiniCssExtractPlugin', compilation => {
      // 在编译时提取CSS
      compilation.hooks.chunkAsset.tap('MiniCssExtractPlugin', (chunk, filename) => {
        if (chunk.name && /\.css$/.test(filename)) {
          // 提取CSS内容
          const cssContent = extractCSS(chunk);
          compilation.emitAsset(filename, new RawSource(cssContent));
        }
      });
    });
  }
}

4.3 Plugin生命周期

Plugin可以监听Webpack的各种生命周期钩子。

4.4 异步Plugin

异步Plugin使用tapAsync或tapPromise来处理异步操作。

五、完整代码实现

5.1 基础结构

// 基础Webpack实现
class MyWebpack {
  constructor(options) {
    this.entry = options.entry;
    this.output = options.output;
    this.modules = {};
  }

  run() {
    // 构建流程
    const entryModule = this.buildModule(this.entry);
    this.emitFiles();
  }

  buildModule(modulePath) {
    // 构建模块
    const module = {
      path: modulePath,
      dependencies: [],
      code: null
    };

    // 读取模块内容
    const content = fs.readFileSync(modulePath, 'utf-8');

    // 处理模块内容
    const processedContent = this.processModule(content, modulePath);

    // 解析依赖
    const dependencies = this.parseDependencies(processedContent);

    // 递归构建依赖
    dependencies.forEach(dep => {
      const depModule = this.buildModule(dep);
      module.dependencies.push(depModule);
    });

    return module;
  }

  processModule(content, modulePath) {
    // 处理模块内容
    return content;
  }

  parseDependencies(content) {
    // 解析依赖
    return [];
  }

  emitFiles() {
    // 输出文件
  }
}
### 5.2 模块处理

模块处理是Webpack的核心功能,它负责将源代码转换成可执行的代码。主要包括:

1. **AST解析**:使用acorn或@babel/parser等工具将代码解析成AST
2. **依赖提取**:从AST中提取import/export语句
3. **代码转换**:使用babel等工具转换代码
4. **模块包装**:将代码包装成webpack的模块格式

```javascript
// 模块处理的实现
class NormalModule {
  constructor({ name, context, rawRequest, resource, parser, generator }) {
    this.name = name;
    this.context = context;
    this.rawRequest = rawRequest;
    this.resource = resource;
    this.parser = parser;
    this.generator = generator;
    this._source = null;
    this._ast = null;
  }

  build(options, compilation, resolver, fs, callback) {
    return this.doBuild(options, compilation, resolver, fs, (err) => {
      if (err) return callback(err);

      try {
        // 解析模块
        const result = this.parser.parse(this._source, {
          current: this,
          module: this
        });

        // 处理依赖
        this.dependencies = result.dependencies;
        this.blocks = result.blocks;

        callback();
      } catch (e) {
        callback(e);
      }
    });
  }

  doBuild(options, compilation, resolver, fs, callback) {
    // 读取文件内容
    fs.readFile(this.resource, (err, buffer) => {
      if (err) return callback(err);

      const source = buffer.toString();
      this._source = source;

      // 处理源码
      this._source = this.processSource(source);

      callback();
    });
  }

  processSource(source) {
    // 处理源码,如babel转换等
    return source;
  }
}

5.3 依赖管理

依赖管理负责处理模块之间的依赖关系,包括:

  1. 依赖解析:将相对路径解析成绝对路径
  2. 循环依赖处理:检测并处理循环依赖
  3. 异步依赖处理:处理动态import等异步依赖
  4. 外部依赖处理:处理node_modules中的依赖
// 依赖管理的实现
class Dependency {
  constructor() {
    this.module = null;
    this.request = '';
    this.userRequest = '';
    this.range = null;
    this.loc = null;
  }

  getResourceIdentifier() {
    return this.request;
  }
}

class ImportDependency extends Dependency {
  constructor(range, source) {
    super();
    this.range = range;
    this.source = source;
  }

  getResourceIdentifier() {
    return this.source;
  }
}

// 依赖解析
class Parser {
  constructor() {
    this.hooks = {
      import: new SyncBailHook(['expr', 'source']),
      call: new SyncBailHook(['expr'])
    };
  }

  parse(source, initialState) {
    const ast = acorn.parse(source, { ecmaVersion: 2020 });
    const dependencies = [];

    // 遍历AST,提取依赖
    walk.simple(ast, {
      ImportDeclaration(node) {
        const source = node.source.value;
        dependencies.push(new ImportDependency(node.range, source));
      },
      CallExpression(node) {
        if (node.callee.type === 'Import' && node.arguments.length > 0) {
          const source = node.arguments[0].value;
          dependencies.push(new ImportDependency(node.range, source));
        }
      }
    });

    return { dependencies };
  }
}

5.4 代码生成

代码生成负责将处理后的模块生成最终的代码,包括:

  1. 模块包装:将代码包装成webpack的模块格式
  2. 依赖注入:注入模块的依赖
  3. 代码优化:进行代码压缩、混淆等优化
  4. Hash生成:生成文件的Hash用于缓存
// 代码生成的实现
class MainTemplate {
  render(hash, chunk, moduleTemplate, dependencyTemplates) {
    const source = new ConcatSource();

    // 添加webpack引导代码
    source.add('/******/ (function(modules) { // webpackBootstrap\n');
    source.add('/******/ 	// The module cache\n');
    source.add('/******/ 	var installedModules = {};\n\n');

    // 添加模块执行函数
    source.add('/******/ 	// The require function\n');
    source.add('/******/ 	function __webpack_require__(moduleId) {\n');
    source.add('/******/ 		// Check if module is in cache\n');
    source.add('/******/ 		if(installedModules[moduleId]) {\n');
    source.add('/******/ 			return installedModules[moduleId].exports;\n');
    source.add('/******/ 		}\n');
    source.add('/******/ 		// Create a new module (and put it into the cache)\n');
    source.add('/******/ 		var module = installedModules[moduleId] = {\n');
    source.add('/******/ 			i: moduleId,\n');
    source.add('/******/ 			l: false,\n');
    source.add('/******/ 			exports: {}\n');
    source.add('/******/ 		};\n\n');

    // 添加模块执行逻辑
    source.add('/******/ 		// Execute the module function\n');
    source.add('/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n');
    source.add('/******/ 		// Flag the module as loaded\n');
    source.add('/******/ 		module.l = true;\n');
    source.add('/******/ 		// Return the exports of the module\n');
    source.add('/******/ 		return module.exports;\n');
    source.add('/******/ 	}\n\n');

    // 添加模块定义
    source.add('/******/ 	// define __esModule on exports\n');
    source.add('/******/ 	__webpack_require__.r = function(exports) {\n');
    source.add('/******/ 		if(typeof Symbol !== \'undefined\' && Symbol.toStringTag) {\n');
    source.add('/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: \'Module\' });\n');
    source.add('/******/ 		}\n');
    source.add('/******/ 		Object.defineProperty(exports, \'__esModule\', { value: true });\n');
    source.add('/******/ 	};\n\n');

    // 添加模块代码
    source.add('/******/ 	// Load entry module and return exports\n');
    source.add('/******/ 	return __webpack_require__(__webpack_require__.s = '+chunk.entryModule.id+');\n');
    source.add('/******/ })
    /************************************************************************/\n');
    source.add('/******/ ({'+hash+',');

    // 添加模块代码
    chunk.modules.forEach(module => {
      source.add(`/${'/'} ${module.identifier()}\n`);
      source.add(`${module.id}: `);
      source.add(moduleTemplate.render(module, dependencyTemplates, hash));
      source.add(',\n');
    });

    source.add('/******/ })');
    return source;
  }
}

六、疑难技术点解析

6.1 Tree Shaking原理

Tree Shaking是Webpack优化代码的重要手段,它通过静态分析消除未使用的代码。

6.1.1 基本原理

Tree Shaking基于ES6模块的静态结构特性,通过分析AST来确定哪些代码是可删除的。

6.1.2 实现机制

// Tree Shaking的实现
class TreeShakingPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('TreeShakingPlugin', (compilation) => {
      // 在模块处理时进行Tree Shaking
      compilation.hooks.optimizeChunks.tap('TreeShakingPlugin', (chunks) => {
        chunks.forEach((chunk) => {
          // 分析模块的导出使用情况
          const usedExports = new Set();
          const providedExports = new Set();

          chunk.modules.forEach((module) => {
            // 收集模块的导出
            if (module.exports) {
              module.exports.forEach((exportInfo) => {
                providedExports.add(exportInfo.name);
              });
            }

            // 收集模块的使用情况
            if (module.usages) {
              module.usages.forEach((usage) => {
                usedExports.add(usage.name);
              });
            }
          });

          // 删除未使用的导出
          providedExports.forEach((exportName) => {
            if (!usedExports.has(exportName)) {
              // 删除未使用的导出
              deleteUnusedExport(exportName);
            }
          });
        });
      });
    });
  }
}

6.1.3 注意事项

  • Tree Shaking只对ES6模块有效
  • 需要配合sideEffects配置
  • 第三方库需要提供ES6模块版本

6.2 长缓存优化

长缓存优化可以提升构建性能和用户体验。

6.2.1 基本原理

长缓存通过为每个模块生成唯一的标识符,当模块内容没有变化时,标识符保持不变。

6.2.2 实现机制

// 长缓存优化的实现
class LongTermCachingPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('LongTermCachingPlugin', (compilation) => {
      // 在模块处理时生成稳定的标识符
      compilation.hooks.chunkHash.tap('LongTermCachingPlugin', (chunk, chunkHash) => {
        // 使用内容的Hash作为标识符
        const contentHash = createHash(chunk.modules);
        chunkHash.update(contentHash);
      });

      // 在文件名中使用Hash
      compilation.mainTemplate.hooks.assetPath.tap('LongTermCachingPlugin', (path, data) => {
        const chunk = data.chunk;
        if (chunk.isOnlyInitial()) {
          return path.replace(/[contenthash]/ig, chunk.contentHash);
        }
        return path;
      });
    });
  }
}

6.2.3 最佳实践

  • 使用[contenthash]而不是[hash]
  • 将第三方库和业务代码分开打包
  • 避免在模块中使用类似Date.now()这样的动态内容

6.3 代码分割策略

代码分割策略可以优化加载性能。

6.3.1 基本原理

代码分割将代码分成多个chunk,实现按需加载。

6.3.2 实现机制

// 代码分割的实现
class CodeSplittingPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('CodeSplittingPlugin', (compilation) => {
      // 处理动态import
      compilation.hooks.buildModule.tap('CodeSplittingPlugin', (module) => {
        if (module.type === 'javascript/dynamic') {
          // 创建新的chunk
          const chunk = new Chunk();
          chunk.name = `chunk-${chunk.id}`;
          chunk.entryModule = module;

          // 添加模块到chunk
          chunk.addModule(module);

          // 设置模块的chunk
          module.chunkReason = 'split chunk';
          module.chunkName = chunk.name;
        }
      });
    });
  }
}

6.3.3 常用策略

  • 按路由分割
  • 按业务功能分割
  • 按第三方库分割
  • 按使用频率分割

6.4 构建性能优化

构建性能优化可以提升开发体验。

七、实战案例

7.1 多入口配置

7.2 开发环境配置

7.3 生产环境配置

7.4 自定义Plugin和Loader

八、总结

Webpack的实现原理涉及模块打包、依赖解析、代码分割、插件系统等多个核心概念。通过理解这些原理,我们可以更好地使用Webpack进行项目构建,并且在需要时实现自定义的Plugin和Loader。

完整的Webpack实现需要考虑很多边界情况和规范细节,上述代码是一个简化的版本,实际的Webpack要复杂得多。建议在学习过程中结合Webpack源码和官方文档进行深入理解。



Previous Post
Blind75 leetcode
Next Post
Typescript类型体操