手写tiny webpack,理解webpack原理

一. webpack介绍

webpack是前端模块构建工具,支持模块化规范解析处理。

代码示例如下

javascript 复制代码
// utils/helloworld.js 
export function helloworld() {
  console.log('are you ok?')
}

// index.js
import { helloworld } from './utils/helloworld.js'

helloworld()

代码产物如下

javascript 复制代码
;(function () {
  // 依赖模块
  const modules = {
    './src/index.js': function (require, module, exports) {
      'use strict'

      var _helloworld = require('./src/utils/helloworld.js')
      ;(0, _helloworld.helloworld)()
    },
    './src/utils/helloworld.js': function (require, module, exports) {
      'use strict'

      Object.defineProperty(exports, '__esModule', {
        value: true,
      })
      exports.helloworld = helloworld
      function helloworld() {
        console.log('are you ok?')
      }
    },
  }

  // 模块缓存
  const cached = {}

  // 模块执行方法
  function require(moduleId) {
    if (cached[moduleId]) return cached[moduleId]
    const module = {
      exports: {},
    }
    cached[moduleId] = module
    const fn = modules[moduleId]
    // 执行模块代码逻辑
    fn(require, module, module.exports)
    return module.exports
  }

  require('./src/index.js')
})()

二. 实现webpack

2.1 tinywebpack.config.js

该文件负责提供webpack构建需要的配置,如入口依赖模块文件路径。

javascript 复制代码
const path = require('path')

const resolvePath = (...paths) => path.join(__dirname, ...paths)

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

2.2 bundle

该方法负责组织整个构建流程,具体逻辑如下:

  • 获取配置文件
  • 解析入口依赖模块,获取依赖模块,转换依赖模块代码
  • 递归解析依赖模块,构建依赖图谱
  • 生成代码,输出到指定文件
javascript 复制代码
const config = require(`${process.cwd()}/tinywebpack.config.js`)

// 项目路径
const rootPath = process.cwd()

// 获取文件相对路径
function relativePath(filePath) {
  return `./${path.relative(rootPath, filePath)}`
}

function bundle() {
  // 依赖模块图谱
  const moduleGraph = {}
  const { entry } = config
  // 解析入口依赖模块
  parse(moduleGraph, entry)
  // 生成代码
  generate(entry, moduleGraph)
}

2.2.1 parse

转换依赖模块代码主要通过babel实现,读者可自行查阅官网了解,本文不再赘述。

该方法负责讲解依赖模块,如转换依赖模块代码,获取子依赖模块。

javascript 复制代码
const babel = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAstSync } = require('@babel/core')

function parse(moduleGraph, filePath) {
  // 如果依赖图谱中已存在说明已经解析过,不需要重复解析直接跳过即可
  if (moduleGraph[filePath]) return
  // 读取依赖模块文件内容
  const code = fs.readFileSync(filePath, { encoding: 'utf-8' })
  // 将代码转换成ast树
  const ast = bable.parse(code, { sourceType: 'module' })
  // 依赖模块
  const dependencies = []
  // 当前模块目录路径
  const dirname = path.dirname(filePath)
  // 遍历ast树,收集依赖模块
  traverse(ast, {
    ImportDeclaration(nodePath) {
      const { node } = nodePath
      const value = path.resolve(dirname, node.source.value)
      node.source.value = relativePath(value)
      dependencies.push(value)
    },
  })
  // 转换代码
  const result = transformFromAstSync(ast, code, {
    presets: ['@babel/preset-env'],
  })
  const dependency = {
    path: relativePath(filePath),
    code: result.code,
  }
  moduleGraph[filePath] = dependency
  // 递归依赖模块
  for (let i = 0; i < dependencies.length; i++) {
    parse(moduleGraph, dependencies[i])
  }
}

2.2.2 generate

该方法负责生成依赖模块对应代码,具体逻辑如下:

  • 提供template模版,注入require方法
  • 将依赖模块图谱转换成modules对象,key是依赖模块文件路径,value是函数,函数体即依赖模块代码
  • 输出到指定文件
javascript 复制代码
function generate(entry, moduleGraph) {
  const template = `
    ;(function () {
      // 依赖模块
      const modules = {
        ${Object.keys(moduleGraph)
          .map(
            filePath => `'${moduleGraph[filePath].path}': function (require, module, exports) {
          ${moduleGraph[filePath].code}
        }`,
          )
          .join(',\n')}
      }

      // 模块缓存
      const cached = {}

      // 模块执行方法
      function require(moduleId) {
        if (cached[moduleId]) return cached[moduleId]
        const module = {
          exports: {},
        }
        cached[moduleId] = module
        const fn = modules[moduleId]
        // 执行模块代码逻辑
        fn(require, module, module.exports)
        return module.exports
      }
      
      require('${moduleGraph[entry].path}')
    })()
  `
  const { output } = config
  const exist = fs.existsSync(output.path)
  if (!exist) fs.mkdirSync(output.path)
  fs.writeFile(`${output.path}/${output.filename}`, template, err => {
    if (err) {
      console.log(err)
      return
    }
    console.log('success')
  })
}

三. 总结

webpack整个构建流程核心两步,一是构建依赖图谱,二是输出代码产物,值得注意的是模块化规范的解析处理流程。代码仓库

创作不易,如果文章对你有帮助,那点个小小的赞吧,你的支持是我持续更新的动力!

相关推荐
Senar14 分钟前
Web端选择本地文件的几种方式
前端·javascript·html
烛阴31 分钟前
UV Coordinates & Uniforms -- OpenGL UV坐标和Uniform变量
前端·webgl
姑苏洛言36 分钟前
扫码小程序实现仓库进销存管理中遇到的问题 setStorageSync 存储大小限制错误解决方案
前端·后端
烛阴1 小时前
JavaScript 的 8 大“阴间陷阱”,你绝对踩过!99% 程序员崩溃瞬间
前端·javascript·面试
lh_12541 小时前
ECharts 地图开发入门
前端·javascript·echarts
jjw_zyfx1 小时前
成熟的前端vue vite websocket,Django后端实现方案包含主动断开websocket连接的实现
前端·vue.js·websocket
Mikey_n2 小时前
前台调用接口的方式及速率对比
前端
周之鸥2 小时前
使用 Electron 打包可执行文件和资源:完整实战教程
前端·javascript·electron
我爱吃朱肉2 小时前
HTMLCSS模板实现水滴动画效果
前端·css·css3
机器视觉知识推荐、就业指导2 小时前
开源QML控件:进度条滑动控件(含源码下载链接)
前端·qt·开源·qml