Skip to content

Leophen/webpack-write

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

webpack-write

Handwritten implementation of webpack

手写实现一个 webpack

一、初始化项目

手写实现 webpack 前,需要安装四个依赖包:

  • @babel/parser:用于分析通过 fs.readFileSync 读取的文件内容,并返回 AST (抽象语法树);
  • @babel/traverse:用于遍历 AST, 获取必要的数据;
  • @babel/core:babel 核心模块,提供 transformFromAst 方法,用于将 AST 转化为浏览器可运行的代码;
  • @babel/preset-env:将转换后代码转化成 ES5 代码;

初始化项目:

yarn init -y
yarn add @babel/parser @babel/traverse @babel/core @babel/preset-env

初始化文件:

  write_webpack
  |- node_modules
+ |- /src
+   |- consts.js
+   |- index.js
+   |- info.js
+ |- write_webpack.js
  |- package.json

接下里,需要实现以下三个核心方法:

  • createAssets:收集和处理文件的代码;
  • createGraph:根据入口文件,返回所有文件依赖图;
  • bundle:根据依赖图整个代码并输出。

二、实现 createAssets 函数

1、读取入口文件,并转为 AST

// src/index.js

import info from "./info.js";
console.log(info);

实现 createAssets 方法的文件读取AST 转换操作:

// write_webpack.js

const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
// 由于 traverse 采用的 ES Module 导出,通过 require 引入的话就加个 .default
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

// 为了便于区分当前操作的模块,这里声明了一个 moduleId 变量来表示;
// 在这将读取到的文件流 buffer 转为 AST 的同时,也是将 ES6 代码转换为 ES5 代码了。
let moduleId = 0

// 实现 createAssets 方法中的 文件读取 和 AST转换 操作
const createAssets = (filename) => {
  // 根据文件名,同步读取文件流
  const content = fs.readFileSync(filename, 'utf-8')

  // 将读取文件流 buffer 转换为 AST
  const ast = parser.parse(content, {
    sourceType: 'module' // 指定源码类型
  })

  console.log(ast)
}

createAssets('./src/index.js')

上面通过 fs.readFileSync() 方法,以同步方式读取指定路径下的文件流,并通过 parser 包的 parse() 方法,将读取到的文件流 buffer 转换为浏览器可以认识的代码(AST),运行 write_webpack.js 文件,AST 输出如下:

// 命令行运行:node write_webpack.js,结果如下:
Node {
  type: 'File',
  start: 0,
  end: 48,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 4, column: 0 },
    filename: undefined,
    identifierName: undefined
  },
  errors: [],
  program: Node {
    type: 'Program',
    start: 0,
    end: 48,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    sourceType: 'module',
    interpreter: null,
    body: [ [Node], [Node] ],
    directives: []
  },
  comments: []
}

2、收集每个模块的依赖

接下来声明 dependencies 变量来保存收集到的文件依赖路径:通过 traverse 方法遍历 AST,获取每个节点依赖的路径,并 push 进 dependencies 数组中。

// write_webpack.js

function createAssets(filename) {
  // ...
  const dependencies = [] // 用于收集文件依赖的路径

  // 通过 traverse 提供的操作 AST 的方法,获取每个节点的依赖路径
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value)
    }
  })
}

3、将 AST 转为浏览器可运行代码

在收集依赖的同时,可以将 AST 代码转为浏览器可运行代码,这就需要用到 babel 提供的 transformFromAstSync() 方法,同步的将 AST 转换为浏览器可运行代码:

// write_webpack.js

function createAssets(filename) {
  // ...
  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ['@babel/preset-env']
  })

  // 设置当前处理的模块 ID
  let id = moduleId++

  return {
    id,
    filename,
    code,
    dependencies
  }
}

console.log(createAssets('./src/index.js'))

到这一步,再执行 node write_webpack.js,输出以下内容,包含入口文件的路径 filename、浏览器可执行代码 code 和文件依赖的路径 dependencies 数组:

// 命令行运行:node write_webpack.js,结果如下:
{
  id: 0,
  filename: './src/index.js',
  code: '"use strict";\n' +
    '\n' +
    'var _info = _interopRequireDefault(require("./info.js"));\n' +
    '\n' +
    'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
    '\n' +
    'console.log(_info["default"]);',
  dependencies: [ './info.js' ]
}

4、代码小结

// write_webpack.js

const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
// 由于 traverse 采用的 ES Module 导出,通过 require 引入的话就加个 .default
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

let moduleId = 0

// 实现 createAssets 方法中的 文件读取 和 AST转换 操作
const createAssets = (filename) => {
  // 根据文件名,同步读取文件流
  const content = fs.readFileSync(filename, 'utf-8')

  // 将读取文件流 buffer 转换为 AST
  const ast = parser.parse(content, {
    sourceType: 'module' // 指定源码类型
  })

  const dependencies = [] // 用于收集文件依赖的路径

  // 通过 traverse 提供的操作 AST 的方法,获取每个节点的依赖路径
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value)
    }
  })

  // 通过 AST 将 ES6 代码转换成 ES5 代码
  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ['@babel/preset-env']
  })

  // 设置当前处理的模块 ID
  let id = moduleId++

  return {
    id,
    filename,
    code,
    dependencies
  }
}

console.log(createAssets('./src/index.js'))

三、实现 createGraph 函数

createGraph() 函数中,将递归所有依赖模块,循环分析每个依赖模块的依赖,生成一份依赖图谱。

为了方便测试,先补充下 consts.jsinfo.js 文件的代码,增加一些依赖关系:

// src/consts.js
export const company = 'Riot'

// src/info.js
import { company } from './consts.js'
export default `Hello, ${company}`

接下来开始实现 createGraph() 函数,它需要接收一个入口文件的路径 entry 作为参数:

// write_webpack.js

function createGraph(entry) {
  // 获取入口文件下的内容
  const mainAsset = createAssets(entry)

  // 入口文件的结果作为第一项
  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 = createAssets(absolutePath)

      // 保存模块ID
      asset.mapping[relativePath] = child.id

      // 递归去遍历所有子节点的文件
      queue.push(child)
    })
  }
  return queue
}

上面代码中,先通过 createAssets() 函数读取入口文件下的内容,并作为依赖关系的队列 queue 数组的第一项,接着遍历队列 queue 每一项,再遍历将每一项中的依赖 dependencies 依赖数组,也是调用 createAssets() 函数,递归去遍历所有子节点的文件,并将结果都保存在队列 queue 中。

需要注意的是,mapping 是用来保存文件的相对路径和模块 ID 的对应关系,在 mapping 中,使用依赖文件的相对路径作为 key,id 作为 value 保存模块 ID。

接下里在 write_webpack.js 中修改启动函数:

- console.log(const result = createAssets('./src/index.js'))
+ const graph = createGraph("./src/index.js")
+ console.log(graph)

同理,运行 write_webpack.js,可以获取到一份包含所有文件依赖关系的依赖图谱:

// 命令行运行:node write_webpack.js,结果如下:
[
  {
    id: 0,
    filename: './src/index.js',
    code: '"use strict";\n' +
      '\n' +
      'var _info = _interopRequireDefault(require("./info.js"));\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      '\n' +
      'console.log(_info["default"]);',
    dependencies: [ './info.js' ],
    mapping: { './info.js': 1 }
  },
  {
    id: 1,
    filename: 'src/info.js',
    code: '"use strict";',
    dependencies: [],
    mapping: {}
  }
]

到这一步,已经获取了所有文件的依赖以及依赖的代码内容。下一步只要实现 bundle() 函数,将结果输出即可。

四、实现 bundle 函数

bundle() 函数中,接收一个依赖图谱 graph 作为参数,最后输出编译后的结果。

createGraph() 函数中可以知道,它会返回一个队列 queue,包含每个依赖的相关信息*(id / filename / code / dependencies)*

1、读取所有模块信息

首先声明一个变量 modules,值为字符串类型,用来将前面 graph 每一项处理成字符串。对参数 graph 进行遍历,将每一项中的 id 取出,作为 key,值为一个数组,包括执行方法和每一项的 mapping

// write_webpack.js

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

上面代码在 modules 中每一项的值中,下标为 0 的元素是个函数,接收三个参数 require / module / exports,为什么会需要这三个参数呢?

构建工具无法判断是否支持 require / module / exports 这三种模块方法,所以需要自己实现,然后方法内的 code 才能正常执行。

2、返回最终结果

接下来实现最终 bundle 函数返回值的处理:

// write_webpack.js

function bundle(graph) {
  //...
  return `
    (function(modules){
      function require(id){
        const [fn, mapping] = modules[id];
        function localRequire(relativePath){
          return require(mapping[relativePath]);
        }

        const module = {
          exports: {}
        }

        fn(localRequire, module, module.exports);

        return module.exports;
      }
      require(0);
    })({${modules}})
  `
}

上面代码最终 bundle 函数返回值是一个字符串,内容是一个自执行函数 IIFE,其中函数参数是一个对象,key 为 modules,value 为前面拼接好的 modules 字符串,即 {modules: modules字符串}

在这个自执行函数中,实现了 require 方法,接收一个 id 作为参数,在方法内部,分别实现了 localRequire / module / modules.exports 三个方法,并作为参数,传到 modules[id] 中的 fn 方法中。

3、代码小结

// write_webpack.js

function bundle(graph) {
  let modules = ''

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

  return `
    (function(modules){
      function require(id){
        const [fn, mapping] = modules[id];
        function localRequire(relativePath){
          return require(mapping[relativePath]);
        }
        const module = {
          exports: {}
        }
        fn(localRequire, module, module.exports);
        return module.exports;
      }
      require(0);
    })({${modules}})
  `
}

五、执行代码

write_webpack.js 中修改启动函数:

- console.log(graph)
+ const result = bundle(graph)
+ console.log(result)

然后运行代码:

// 命令行运行:node write_webpack.js,结果如下:
(function(modules){
  function require(id){
    const [fn, mapping] = modules[id];
    function localRequire(relativePath){
      return require(mapping[relativePath]);
    }
    const module = {
      exports: {}
    }
    fn(localRequire, module, module.exports);
    return module.exports;
  }
  require(0);
})({
  0: [
    function (require, module, exports){
      "use strict";

var _info = _interopRequireDefault(require("./info.js"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

console.log(_info["default"]);
    },
    {"./info.js":1}
  ],

  1: [
    function (require, module, exports){
      "use strict";
    },
    {}
  ],
})

到这里,一个简单的 webpack 构建工具就实现了。

About

Handwritten implementation of webpack

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published