webpack-04:拓展 手写一个小webpack打包过程

187 阅读4分钟

1、新建一个项目(搭建架子)

npm init - y

需要以下几个文件

1、webpack.config.js 作为 options

const path = require('path')

module.exports = {
  mode: 'development',
  entry: './src/index.js', // 需要打包的文件
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js',
  },
}

2、lib/webpack.js // 作为webpack的架子框架来扩展

const fs = require('fs')

module.exports = class webpack {
  constructor(options) {
    const { entry, output } = options
    this.entry = entry
    this.output = output
  }
  run() {
    this.parse(this.entry) // 输出入口
  }
  
  parse(entryFile) {
    const content = fs.readFileSync(entryFile, 'utf-8')
    console.log(content) // 输出原本内容
  }
}

3、bundle.js 通过此文件运行打包

const options = require('./webpack.config.js')
const Webpack = require('./lib/webpack')

new Webpack(options).run()

4、准备需要的打包文件

src/index.js

import { fuck } from './a.js'
console.log(fuck)
console.log('webpack -4.0xxxx!!!')

src/a.js

export const fuck = "fuck you"

通过上述代码我们就可以搭出一个webpack的架子

一个webpack的类,一份webpack的配置,需要打包的文件,还有执行打包的代码

我们将流程分为3部分

第一部分: parse过程 主要读取打包文件内容,通过babel库我们解析每个文件的导入语句,我们获取每个文件的加载映射

比如 main.js 文件 中 import a.js 还有 b.js 文件,那么我们要生成 一个依赖映射

用一个对象保存

 {
     'main.js': {
        yilai: {
            './a.js' :  '这里存相对入口的路径,
            './b.js' :  '这里存相对入口的路径'
        }
     }
 }

第二部分:traverse 转义代码, 生成浏览器可执行的文件内容。把 import 转化为 require 的语法

第三部分: 生成打包后的文件, 主要处理 require 函数加载其他文件的函数





2、首先目标是扩展webpack类 的 parse 函数

class webpack {
  // ...
  parse(entryFile) {
    const content = fs.readFileSync(entryFile, 'utf-8')
    console.log(content) // 输出原本内容
  }
}

对代码解析第一件事:

1、转义成ast 树

我们引用 @babel/parse ,我们可以通过它来转义成 ast

const parser = require('@babel/parser')

// content 读取到的文件内容
const ast = parser.parse(content, {
    sourceType: 'module', // es6
})

ast 的内容是一个node类型的对象

{
  type: 'File',
  start: 0,
  end: 86,
  loc:
   SourceLocation {
     start: Position { line: 1, column: 0 },
     end: Position { line: 4, column: 0 },
     filename: undefined,
     identifierName: undefined },
  // ...
  program:   // 第一层 关注这里
  {
     type: 'Program',
     start: 0,
     end: 86,
     //....
     body: [ [Node], [Node], [Node] ], // 第二层 关注这里
  }
}

ast.program.body 数组内的对象是行对代码的解析
有type: 'ImportDeclaration', 导入语句 表达式等等

对代码解析成第二件事:

2、traverse 对代码转换过程

traverse(ast, null, options) 可以找到导入语句

    console.log('traverse----------:')
    const traverse = require('@babel/traverse').default
    const yilai = {}
    const prePath = path.dirname(entryFile)
    traverse(ast, {
      ImportDeclaration({ node }) {
        console.log(node) // 过滤出来 type 为 ImportDeclaration 节点
        // 要的 node.source.value
        // 对路径做拼接
        const newPath = './' + path.join(prePath, node.source.value)
        yilai[node.source.value] = newPath.replace('\\', '/') // 收集路径
      },
    })
    
    console.log('yilai:' + yilai) // {'./a.js': './src/a.js'} 得到相对于入口文件的路径

对代码解析成第三件事:

2、把ast 解析成想要得代码

const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env'],
    })
    console.log('code:-----------')
    console.log(code)
    
    return {
      entryFile,
      yilai,
      code,
    }

此时转义后的代码会成这样

var _a = require("./a.js"); // 这块 语法浏览器 还是解析不了,毕竟是node的

console.log(_a.fuck);
console.log('webpack -4.0xxxx!!!');

汇总后的代码:

class webpack {
  // ...
  parse(entryFile) {
    const content = fs.readFileSync(entryFile, 'utf-8')
    const ast = parser.parse(content, {
      sourceType: 'module', // es6
    })

    // 实际要的是 ast.program.body    ===> [ [Node], [Node], [Node] ] 行代码

    const yilai = {}
    const prePath = path.dirname(entryFile)
    traverse(ast, {
      ImportDeclaration({ node }) {
        console.log(node) // 过滤出来 type 为 ImportDeclaration 节点
        // 要的 node.source.value
        // 对路径做拼接
        const newPath = './' + path.join(prePath, node.source.value)
        yilai[node.source.value] = newPath.replace('\\', '/')
      },
    })
    
    // 得到编译后的代码
    
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env'],
    })

    // 返回
    return {
      entryFile,
      yilai,
      code,
    }
  }
 
 }





3、扩展 generateCode 函数

首先我们得明白run做了什么

  run() {
    const info = this.parse(this.entry)
    this.modules.push(info)
    for (let i = 0; i < this.modules.length; i++) {
      const item = this.modules[i]
      const { yilai } = item
      if (yilai) {
        for (let j in yilai) {
          this.modules.push(this.parse(yilai[j]))
        }
      }
    }

    const obj = {}
    this.modules.forEach((item) => {
      obj[item.entryFile] = item
    })
    console.log(obj) // 此时获得了 图谱
    
    
    // 通过parse 形成 映射
    
    // 对象中的key 是 相对入口的路径
    // value: {
    //   yilai: 文件加载 url到 相对入口文件的路径的映射
    //     code:'真实代码的字符串'
    // }
    
    this.gennerateCode(obj) // 生成代码和创建bundle文件
  }

gennerateCode 实现

    const filePath = path.join(this.output.path, this.output.filename) // 输出文件目录和名
    const content = `(function (modules) {

      // 加载函数
      function require(module) {
        
        function newRequire(relativePath) {
          // .a.js => ./src/a.js
          return require(modules[module].yilai[relativePath])
        }

        const exports = {};  // 这个分号缺了就报错了,会和下面自执行函数连起来报错
        
        // require 重载,这样加载的url就是对的了
        
        (function(require, code) {
          eval(code)
        })(newRequire, modules[module].code)
        
        return exports
      }

      return require('${this.entry}')  /* ./src/index.js  */
    })(${JSON.stringify(obj)})`
   
    fs.writeFileSync(filePath, content, 'utf-8')





完整代码清单

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const path = require('path')
const fs = require('fs')
const { transformFromAst } = require('@babel/core')

module.exports = class webpack {
  constructor(options) {
    const { entry, output } = options
    this.entry = entry
    this.output = output
    this.dir = path.dirname(this.entry)
    this.modules = []
  }
  run() {
    const info = this.parse(this.entry)
    this.modules.push(info)
    for (let i = 0; i < this.modules.length; i++) {
      const item = this.modules[i]
      const { yilai } = item
      if (yilai) {
        for (let j in yilai) {
          this.modules.push(this.parse(yilai[j]))
        }
      }
    }

    const obj = {}
    this.modules.forEach((item) => {
      obj[item.entryFile] = item
    })
    console.log(obj) // 此时获得了 图谱

    this.gennerateCode(obj) // 生成代码和创建bundle文件
  }

  parse(entryFile) {
    const content = fs.readFileSync(entryFile, 'utf-8')
    const ast = parser.parse(content, {
      sourceType: 'module', // es6
    })

    // 实际要的是 ast.program.body    ===> [ [Node], [Node], [Node] ] 行代码

    const yilai = {}
    const prePath = path.dirname(entryFile)
    const currentPath = traverse(ast, {
      ImportDeclaration({ node }) {
        // console.log(node) // 过滤出来 type 为 ImportDeclaration 节点
        // 要的 node.source.value
        // 对路径做拼接
        const newPath = './' + path.join(prePath, node.source.value)
        yilai[node.source.value] = newPath.replace(/\\/g, '/')
      },
    })
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env'],
    })

    return {
      entryFile,
      yilai,
      code,
    }
  }

  gennerateCode(obj) {
    const filePath = path.join(this.output.path, this.output.filename) // 输出文件目录和名
    const content = `(function (modules) {

      // 加载函数
      function require(module) {
        
        function newRequire(relativePath) {
          // .a.js => ./src/a.js
          return require(modules[module].yilai[relativePath])
        }

        const exports = {};

        (function(require, code) {
          eval(code)
        })(newRequire, modules[module].code)
        
        return exports
      }

      return require('${this.entry}')  /* ./src/index.js  */
    })(${JSON.stringify(obj)})`
   
    fs.writeFileSync(filePath, content, 'utf-8')
  }
}

最后代码验证:

node bundle.js

结果生成文件 main.js

;(function (modules) {
  // 加载函数
  function require(module) {
    function newRequire(relativePath) {
      // .a.js => ./src/a.js
      return require(modules[module].yilai[relativePath])
    }

    const exports = {};
    
    (function (require, code) {
      eval(code)
    })(newRequire, modules[module].code)

    return exports
  }

  return require('./src/index.js') /* ./src/index.js  */
})({
  './src/index.js': {
    entryFile: './src/index.js',
    yilai: { './a.js': './src/a.js' },
    code:
      '"use strict";\n\nvar _a = require("./a.js");\n\nconsole.log(_a.fuck);\nconsole.log(\'webpack -4.0xxxx!!!\');',
  },
  './src/a.js': {
    entryFile: './src/a.js',
    yilai: { './file/b.js': './src/file/b.js' },
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.fuck = void 0;\n\nvar _b = require("./file/b.js");\n\nconsole.log(_b.hello);\nvar fuck = \'fuck you\';\nexports.fuck = fuck;',
  },
  './src/file/b.js': {
    entryFile: './src/file/b.js',
    yilai: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.hello = void 0;\nvar hello = \'hello world\';\nexports.hello = hello;',
  },
})