# 迷你版webpack的实现

# webpack的模块机制

//bundle.js文件
(function (modules) { // webpackBootstrap
                      // The module cache
    var installedModules = {};
    // __webpack_require来加载模
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.l = true;
        // Return the exports of the module
        return module.exports;
    }
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = "./index.js");
})
({
    "./index.js":
        (function (module, exports) {
            eval("console.log('test world')\n\n//# sourceURL=webpack:///./index.js?");
        })
});

分析webpack构建完成后生成的bundle文件,可以了解到,打包出来的是一个立即执行函数IIFE,其中modules 是一个数组,每一项是一个模块初始化函数,通过__webpack_require来加载模块并返回module.export,并 通过__webpack_require__(0)启动。

通过打包构建后的bundle文件分析,并结合之前webpack源码的了解,我们可以知道,webpack核心就是将ES6语法转换 为浏览器可识别的ES5语法,并通过分析入口文件的文件依赖和模块之间的依赖关系,最后根据依赖树来构建bundle文件。

所以,我们需要实现的功能有:

  • 将ES6语法转换为ES5语法 通过babylon生成AST
    通过babel-core将AST重新生成源码
  • 分析模块之间的依赖关系
    通过babel-traverse的ImportDeclaration方法获取依赖属性
  • 生成可在浏览器中运行的JS文件

# 迷你版webpack实现

# 模拟webpack配置文件,新建easypack文件设置入口出口配置项。

//easypack.js
const path = require('path')

module.exports = {
    entry: path.join(__dirname, './src/index.js'), // 入口文件src/index.js
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist') //bundle文件导出到dist文件夹下
    }
}

# 新建index.js,参考webpack作为入口文件,接受easypack.js配置并执行compiler.run项目构建。

// lib/index.js
const Compiler = require('./Compiler')
const options = require('../easypack.config')

new Compiler(options).run()

则Compiler.js需要有run入口,构建模块方法和文件输出方法。

module.exports = class Compiler {
    //初始化参数,接受easypack配置
    constructor(options){
         const { entry, output } = options
         this.entry = entry
         this.output = output
    }
    // 提供run,入口方法
    run() {
        
    }
    // 模块构建
    buildModules(){
        
    }
    //文件输出:easypack提供的output输出路径
    emitFiles(){
        
    }
}

# 新建好Compiler.js入口文件后,需要Parser.js,实现将ES6+转换为ES5和分析依赖功能。

  • 将ES6+转换为AST树
const babylon = require('babylon')
module.exports ={
    getAST: path => {
        // 提供path路径,通过fs读取内容
        const source = fs.readFileSync(path,'utf-8')
        // 通过babylon,将source转换为ast
        return babylon.parse(source,{
              sourceType: 'module'
       })
    },
}

将ES6+转换为AST树

  • 依赖分析
const traverse = require('babel-traverse').default
module.exports = {
    getDepeendencies:(ast) => {
        const dependencies = []
        traverse(ast, {
            // 分析import语句,获取依赖内容
            ImportDeclaration:({node}) => {
                dependencies.push(node.source.value)
            }
            })
         return dependencies
    },
}

依赖分析

  • 将AST转换为ES5源码
const { transformFromAst } = require('babel-core')
module.exports = {
    transform: (ast) => {
        const { code } = transformFromAst(ast, null, {
            presets: ['env']
        })
        return code
    }
}

将AST转换为ES5源码

最后,完整的Parser.js源码:

const fs = require('fs')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')

module.exports = {
    getAST: path => {
        // 提供path路径,通过fs读取内容
        const source = fs.readFileSync(path,'utf-8')
        // 通过babylon,将source转换为ast
        return babylon.parse(source,{
            sourceType: 'module'
        })
    },
    getDepeendencies:(ast) => {
        const dependencies = []
        traverse(ast, {
            // 分析import语句,获取依赖内容
            ImportDeclaration:({node}) => {
                dependencies.push(node.source.value)
            }
        })
        return dependencies
    },
    // 将ast转换为源码
    transform: (ast) => {
        const { code } = transformFromAst(ast, null, {
            presets: ['env']
        })
        return code
    }
}

# 完善Compiler.js

  • 模块构建并遍历每个模块的依赖,然后获取所有模块的依赖
const {getAST, getDepeendencies, transform } = require('./Parser')

module.exports = class Compiler {
    run(){
        const entryModule = this.buildModules(this.entry, true)
        this.modules.push(entryModule)
        //遍历获取所有依赖
        this.modules.map((module)=>{
            module.dependencies.map((dependency)=>{
                this.modules.push(this.buildModules(dependency))
            })
        })
    }
     // 模块构建
     buildModules(filename,isEntry){
        let ast
        if(isEntry){
            ast = getAST(filename)
        } else {
            //由于不是入口文件,则返回的为相对路径,转换为绝对路径
            const absolutePath = path.join(process.cwd(),'./src',filename)
            ast = getAST(absolutePath)
        }
        return {
            filename,
            dependencies: getDepeendencies(ast),
            source: transform(ast)
        }
     }
}

依赖

  • 参考bundle.js的输出结构,输出bundle文件,自定义require方法实现模块化方法。
module.exports = class Compiler {
    //文件输出:easypack提供的output输出路径
    emitFiles(){
        const outputPath = path.join(this.output.path,this.output.filename)
        let modules = ''
        this.modules.map((module)=>{
            modules += `'${module.filename}': function (require, module, exports){${module.source}},`
        })
        const bundle = `(function(modules){
            function require(filename){
                var fn = modules[filename];
                var module = { exports: {} };
                    
                fn(require, module, module.exports);
                return module.exports
            }
            require('${this.entry}')
        })({${modules}})`
        //console.log(bundle)
        fs.writeFileSync(outputPath, bundle, 'utf-8')
    }
}

完整的Compiler.js源码:

const path = require('path')
const fs = require('fs')
const {getAST, getDepeendencies, transform } = require('./Parser')

module.exports = class Compiler {
    //初始化参数
    constructor(options){
        const { entry, output } = options
        this.entry = entry
        this.output = output
        // 最终构建的模块依赖列表
        this.modules = []
    }

    // 提供run,入口方法
    run(){
        const entryModule = this.buildModules(this.entry, true)
        //console.log(entryModule)
        this.modules.push(entryModule)
        //遍历获取所有依赖
        this.modules.map((module)=>{
            module.dependencies.map((dependency)=>{
                this.modules.push(this.buildModules(dependency))
            })
        })
        //console.log(this.modules)
        this.emitFiles()
    }
    // 模块构建
    buildModules(filename,isEntry){
        let ast
        if(isEntry){
            ast = getAST(filename)
        } else {
            //由于不是入口文件,则返回的为相对路径,转换为绝对路径
            const absolutePath = path.join(process.cwd(),'./src',filename)
            ast = getAST(absolutePath)
        }
        return {
            filename,
            dependencies: getDepeendencies(ast),
            source: transform(ast)
        }
    }
    //文件输出:easypack提供的output输出路径
    emitFiles(){
        const outputPath = path.join(this.output.path,this.output.filename)
        let modules = ''
        this.modules.map((module)=>{
            modules += `'${module.filename}': function (require, module, exports){${module.source}},`
        })
        const bundle = `(function(modules){
            function require(filename){
                var fn = modules[filename];
                var module = { exports: {} };
                
                fn(require, module, module.exports);
                return module.exports
            }
            require('${this.entry}')
        })({${modules}})`
        //console.log(bundle)
        fs.writeFileSync(outputPath, bundle, 'utf-8')
    }
}

Compiler.js

完整代码可查看:https://github.com/PCAaron/blogCode/tree/master/webpack/easypack