手写webpack——简易版(打包js模块)

306 阅读4分钟

一句话:从入口文件开始,对引用的依赖进行收集处理,最后输出到指定目录中。

  • webpack是一个帮助开发者自动构建模块的工具。
  • 给webpack一个入口模块路径,还你一个最终文件。其中入口模块可以依赖其他模块,其他模块又可以依赖另外的模块。
  • webpack会自动处理模块之间的依赖关系

image.png

从执行一个命令说起:npm run build,当执行该命令时,会找到package.json中的脚本命令,可以看到会使用node命令执行./script/build.js中的代码。

// package.json
"scripts": {
    "build": "node ./script/build.js"
},

将配置文件和打包器(webpack,其本质是个函数,接收配置对象为参数)整合

// 自己写的webpack
const myWebpack = require('../lib/myWebpack')
// webpack配置文件
const config = require('../config/webpack.config')

// 根据配置文件(配置对象),获取编译器 (函数)
let compiler = myWebpack(config)

// 调用编译器对象的执行接口,开始打包编译
compiler.run()

webpack的配置文件

// webpack.config.js
const {resolve} = require('path')

module.exports = {
  // 打包入口配置,指定入口文件的路径
  entry: './src/index.js',
  // 打包输出配置
  output: {
    // 指定输出文件的目录
    path: resolve(__dirname, '../dist'),
    // 指定输出文件的名称
    filename: 'main.js'
  }
}

自己写的打包器(webpack)的主文件

// webpack的入口文件
// 引入编译器
const Compiler = require('./Compiler')

// 导出打包工具(webpack),就是个函数
module.exports = function myWebpack(config){
  // 返回一个编译器对象,接收配置对象
  return new Compiler(config)
}

编译器

const path = require('path');
const fs = require('fs');

// 自定义模块
const {getAst, getDeps, getCode} = require('./parser')

// 定义并导出编译器
module.exports = class Compiler{
  // 构造函数
  constructor(options = {}){
    // 接收配置对象
    this.options = options
    // 所有依赖的容器
    /*
      [
        {
          code: '',
          deps: {},
          entryFilePath: ''
        }
      ]
    */ 
    this.modules = [];
  }

  // 入口方法
  run(){
    // 获取入口文件路径
    const entryFilePath = this.options.entry;
    // 第一次构建,得到入口文件的信息
    const fileInfo = this.build(entryFilePath);
    // 将入口文件的信息放到依赖容器中,此时依赖容器中存放着一个入口文件的信息
    this.modules.push(fileInfo);
    // 递归收集依赖
    this.getDepsMethod(fileInfo.deps);

    // 将 this.modules 转换成 depsGraph
    const depsGraph = this.modules.reduce((graph, module) => {
      graph[module['entryFilePath']] = {
        code: module.code,
        deps: module.deps,
      } 
      return graph
    }, {})

    this.generate(depsGraph)
  }

  // 递归收集依赖
  getDepsMethod(deps) {
    for (const key in deps) {
      // 是否存在该依赖
      let item = this.modules.find((item) => item.entryFilePath == deps[key])
      // 存在则跳过本次遍历,进行下一次遍历
      if(item) continue;
      // 获取该依赖的信息
      const fileInfo = this.build(deps[key])
      // 将依赖存到容器中
      this.modules.push(fileInfo)
      // 该依赖如果还有依赖则继续依赖收集
      if(JSON.stringify(fileInfo.deps) != '{}'){
        this.getDepsMethod(fileInfo.deps)
      }
    }
  }

  // 生成输出资源
  generate(depsGraph){
    const bundle = `
      (function (depsGraph) {
        // require目的:为了加载入口文件
        function require(module) {
          // 定义模块内部的require函数
          function localRequire(relativePath) {
            // 为了找到要引入模块的绝对路径,通过require加载
            return require(depsGraph[module].deps[relativePath]);
          }
          // 定义暴露对象(将来我们模块要暴露的内容)
          var exports = {};
          (function (require, exports, code) {
            eval(code);
          })(localRequire, exports, depsGraph[module].code);
          
          // 作为require函数的返回值返回出去
          // 后面的require函数能得到暴露的内容
          return exports;
        }
        // 加载入口文件
        require('${this.options.entry}');

      })(${JSON.stringify(depsGraph)})
    `
    // 生成输出文件的绝对路径
    const outputFilePath = path.resolve(this.options.output.path, this.options.output.filename)
    // 写入文件
    fs.writeFileSync(outputFilePath, bundle, 'utf-8');
  }

  // 构建方法
  build(entryFilePath){

    // 1. 将文件解析成ast
    const ast = getAst(entryFilePath);
    // 2. 获取ast中所有的依赖
    const deps = getDeps(entryFilePath, ast);
    // 3. 将ast解析成code
    const code = getCode(ast);

    return {
      // 文件路径
      entryFilePath,
      // 当前文件的所有依赖
      deps,
      // 当前文件解析后的代码
      code
    }
  }
}

解析工具

  • 根据读取的文件模块内容生成语法树;
  • 根据语法树获取该模块的依赖;
  • 根据语法树获取需要执行的代码;
// 内置模块
const fs = require('fs');
const path = require('path');

// 第三方模块
const babelParser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');

module.exports = {
  // 获取语法树
  getAst(entryFilePath){
    // 根据文件路径,同步读取文件内容
    const fileContent = fs.readFileSync(entryFilePath, 'utf-8');
    // 使用@babel/parser,将文件内容转换成AST语法树
    const ast = babelParser.parse(fileContent, {
      sourceType: 'module' // 解析文件的模块化方案是 ES Module
    })

    return ast
  },
  // 获取依赖
  getDeps(entryFilePath, ast){
    // 获取路径中最后文件或目录的目录,'./src/index.js' ——> './src'
    const dirname = path.dirname(entryFilePath);
    // 定义存储依赖的容器
    const deps = {}
    // 使用traverse,收集依赖
    traverse(ast, {
      // 内部会遍历ast中program.body,判断里面语句类型
      // 如果 type:ImportDeclaration 就会触发当前函数
      ImportDeclaration({node}) {
        // 文件相对路径:'./add.js'
        const relativePath = node.source.value;
        // 生成基于入口文件的绝对路径,将导入模块的路径转换成绝对路径
        const absolutePath = path.resolve(dirname, relativePath);
        // 添加依赖
        /*
          {
            './modules/add.js': "D:\myWebpack\src\modules\add.js",
            './modules/sub.js': "D:\myWebpack\src\modules\sub.js",
          }
        */
        deps[relativePath] = absolutePath;
      }
    })

    return deps
  },
  // 获取代码
  getCode(ast){
    // 根据生成的语法树,通过transformFromAst工具和@babel/preset-env,将该模块解析生成低版本的代码(兼容性较好的代码,把let、const、箭头函数解析成var和function)
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })

    return code
  },
}