手写简易版webpack

59 阅读1分钟

步骤

01 从入口文件开始查找所有的依赖模块

  • 读取代码内容
  • 读取模块文件相对路径
  • 读取模块中子依赖包,首先需要解析当前模块
  • 解析结果,是否存在子依赖包dependcies, 解析的源代码sourceCode
  • 代码解析:vue---> html css js es6 -> es5

02 模块解析

  • 使用ast语法树解析: astexplorer.net
  • 将代码中的require修改__webpack_require__
  • 将require("./jd") -> require('./jd.js')
  • 收集dependencies

03 打包输出

  • 使用模版生成bundle.ejs,传入的参数必须是动态的
  • 模版express ejs
  • 使用fs将生成的文件写入bundle.js

04 执行手写loader

  • less sass vue ...
  • 作用:转化,less->css vue->js、html、css
  • 自定义loader less-loader style-loader

05 执行手写plugin

  • 代码加工:压缩、合并、混淆
  • 通过tapable--发布订阅处理plugin事件流程
  • 在特定的生命钩子函数中执行相应功能
  • 需要一个固定的apply方法,会在编译器中调用

打包核心代码

配置文件:my-webapck.config.js

const path =require('path')
const myPlugin = require("./plugin/myplugin")
module.exports = {
    entry: '../jd-pack/src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, "dist")
    },
    module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    path.resolve(__dirname,'loader', 'style-loader'), // 自定义loader
                    path.resolve(__dirname, 'loader', 'less-loader')
                ]
            }
        ],
        plugins: [
            new myPlugin() // 自定义plugin
        ]
    }
}

bundle模版文件:

(function(modules) {
    var installedModules = {};

    function __webpack_require__(moduleId) {
      if(installedModules[moduleId]) {
         return installedModules[moduleId].exports;
        }
   
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
    module.l = true;
    
    return module.exports;
   }
    __webpack_require__.m = modules;
    __webpack_require__.c = installedModules;
    return __webpack_require__(__webpack_require__.s = "<%=entryId%>");
    }) 
    ({
        <%for(let key in modules) {%> 
            "<%-key%>": (function(module, exports, __webpack_require__) {eval(`<%-modules[key]%>`);}),
        <%}%>
    });

入口文件:

#! /usr/bin/env node
const path= require('path')
const config = require(path.resolve('my-webapck.config.js'))
const Compiler = require('../lib/Compiler')
const compiler = new Compiler(config)
// 编译
compiler.run()

打包:

const path = require('path')
const fs = require('fs')
const ejs = require('ejs')
const types = require('@babel/types')
const babylon = require('babylon')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const {SyncHook} = require('tapable') // 发布订阅模式
// babylon @babel/traverse @babel/types @babel/generator
// 编译器
 class Compiler {
    constructor(config) {
      this.config = config
      this.entry  = config.entry
      this.entryId;// 存储入口文件
      // 当前目录
      this.root = process.cwd()
      this.modules = {} // 保存所有模块
      // 添加钩子
      this.hooks = {
        entryOption: new SyncHook() , // 开始
        compile: new SyncHook(), // 编译
        afterCompile: new SyncHook(), // 编译后
        run: new SyncHook(), // 运行
        emit: new SyncHook(), // 发射
        done: new SyncHook() // 完成
      }
      // 获取配置文件中的plugins,并执行applay函数
      let plugins = this.config.module.plugins
      if(Array.isArray(plugins)) {
        plugins.forEach(p => {
            p.apply(this)
        })
      }
    }
    // 模块文件解析 source--文件内容 parentPath--文件目录
    parse(source, parentPath) {
      const ast = babylon.parse(source, {
        // 以严格模式解析并允许模块声明
        sourceType: "module",
      })
      let dependencies = [] // 子模块依赖
      traverse(ast, {
        CallExpression(p) {
            let node = p.node
            if(node.callee.name === 'require') {
               node.callee.name = "__webpack_require__" 
               let moduleName = node.arguments[0].value
               moduleName = moduleName + (path.extname(moduleName) ? "" : ".js")
               moduleName = "./" + path.join(parentPath,moduleName)
               dependencies.push(moduleName)
               // 将更新后的子模块依赖名写回去
               node.arguments = [types.stringLiteral(moduleName)]
            }
        }
      })
      let sourceCode = generator(ast).code
      console.log(sourceCode,dependencies)
      return {sourceCode, dependencies}
    }
    getSource(modulePath) {
      // for loader
      let rules = this.config.module.rules
      let content = fs.readFileSync(modulePath,'utf-8')
      for(let i = 0; i<rules.length; i++) {
        let rule = rules[i]
        let {test, use} = rule
        let len = use.length - 1 // loader的总长度
        // 获取配置文件中的loader,传入文件内容并执行loader函数
        if(test.test(modulePath)) {
          function normalLoader() {
            let loader = require(use[len--])
            content = loader(content)
            if(len >=0) {
                normalLoader()
            }
          }
          normalLoader()
        }
      }
      return content
    }
    // 从root节点找所有的依赖模块
    // modulePath模块文件路径
    // isEntry--是否是入口
    buildModule(modulePath, isEntry) {
        let source = this.getSource(modulePath)
        let moduleName = "./" + path.relative(this.root, modulePath)
        
        if(isEntry) {
            this.entryId = moduleName
        }
        let {sourceCode,dependencies } = this.parse(source,path.dirname(moduleName))
        this.modules[moduleName] = sourceCode
        // 递归
        dependencies.forEach(dep => {
            this.buildModule(path.join(this.root,dep), false)
        });
    }
    //生成打包文件
    emitFile() {
        let main = path.join(this.config.output.path, this.config.output.filename)
        let templateStr = this.getSource(path.join(__dirname,"bundle.ejs")) // 拿到模版内容
        let result = ejs.render(templateStr, {entryId:this.entryId, modules:this.modules})
        this.assets = {}
        this.assets[main] = result // 文件全名-文件内容
        fs.writeFileSync(main, this.assets[main])
    }
    run() {
      this.hooks.run.call()
      this.hooks.compile.call() // 编译
      this.buildModule(path.resolve(this.root,this.entry), true)
      this.hooks.afterCompile.call() // 编译完成
      this.hooks.emit.call()
      this.emitFile()
      this.hooks.done.call() // 完成

    }
}
module.exports = Compiler

手写loader

style-loader:

function loader(sourceCss) {
    let style =`
      let style = document.createElement('style')
      style.innerHTML = ${JSON.stringify(sourceCss)}
      document.head.appendChild(style)
    `
    return style
}
module.exports = loader

less-loader:

const less = require('less')

function loader(sourceLess) {
    let css = ""
    less.render(sourceLess, function(err, res) {
        css = res.css
    })
    css = css.replace(/\n/g, '\\n')
    return css 
}

module.exports = loader

手写plugin

class MyPlugin {
    apply(compiler) {
        console.log('start')
        // 注册订阅
        compiler.hooks.emit.tap('emit', function() {
          console.log('emit')
        })
        compiler.hooks.done.tap('done', () => {
            console.log('打包结束')
        })
    }

}
module.exports = MyPlugin