webpack原理解析

(function(modules) { // webpackBootstrap
    
    var installedModules = {}; // 先定义一个缓存池,如果有重复模块,不需要再次加载,直接从缓存中拿
  // 实现一个require方法
    function __webpack_require__(moduleId) { // moduleId是入口模块,./src/index.js

        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;
    }


    return __webpack_require__(__webpack_require__.s = "./src/index.js"); // 入口模块
})({

  "./src/content.js": // key -> 模块的路径
  (function(module, exports) { // 函数

      eval("function Content() {\n\tvar dom = document.getElementById('root');\n\tvar content = document.createElement('div');\n\tcontent.innerText = 'content';\n\tdom.append(content);\n}\n\nmodule.exports = Content;\n\n//# sourceURL=webpack:///./src/content.js?");

  }),

  "./src/header.js":
  (function(module, exports) {

    eval("function Header() {\n\tvar dom = document.getElementById('root');\n\tvar header = document.createElement('div');\n\theader.innerText = 'header';\n\tdom.append(header);\n}\n\nmodule.exports = Header;\n\n//# sourceURL=webpack:///./src/header.js?");

  }),

  "./src/index.js":
  (function(module, exports, __webpack_require__) {

    eval("var Header = __webpack_require__(/*! ./header.js */ \"./src/header.js\");\nvar Sidebar = __webpack_require__(/*! ./sidebar.js */ \"./src/sidebar.js\");\nvar Content = __webpack_require__(/*! ./content.js */ \"./src/content.js\");\n\nnew Header();\nnew Sidebar();\nnew Content();\n\n//# sourceURL=webpack:///./src/index.js?");

  }),

  "./src/sidebar.js":
  (function(module, exports) {

    eval("function Sidebar() {\n\tvar dom = document.getElementById('root');\n\tvar sidebar = document.createElement('div');\n\tsidebar.innerText = 'sidebar';\n\tdom.append(sidebar);\n}\n\nmodule.exports = Sidebar;\n\n//# sourceURL=webpack:///./src/sidebar.js?");

  })
});

Tapable


image.png

image.png

低配版webpack实现

  1. 配置webpack.config.js
let path = require('path')
class P{ // plugin
    apply(compiler) {
        compiler.hooks.emit.tap('emit', function() {
            console.log('emit')
        })
    }
}

class P1{
    apply(compiler) {
        compiler.hooks.afterPlugins.tap('emit', function() {
            console.log('afterPlugins')
        })
    }
}

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    path.resolve(__dirname, 'loader', 'style-loader.js'),
                    path.resolve(__dirname, 'loader', 'less-loader.js')
                ]
            },{
                test: /\.js$/,
                use: path.resolve(__dirname, 'loader', 'loader1.js')
            }
        ]
    },
    plugins: [
        new P(),
        new P1()
    ]
}
  1. 新建bin目录,在bin目录中新建webpack.js文件
let path = require('path');

// config配置文件
let config = require(path.resolve('webpack.config.js'))

let Compiler = require('../lib/Compiler.js')
let compiler = new Compiler(config)
compiler.hooks.entryOption.call()

// 标识运行编译
compiler.run()

我们新建lib目录,来实现Compiler.js

let fs = require('fs');
let path = require('path')
let babylon = require('babylon') // Babylon是Babel中使用的JavaScript解析器,把源码转换为ast
let t = require('@babel/types') // 在创建AST的过程中判断各种语法的类型,替换节点
let ejs = require('ejs')
let { SyncHook } = require('tapable')
let traverse = require('@babel/traverse').default // es6模块需要default一下 babel-traverse来进行筛选遍历节点
let generator = require('@babel/generator').default //v生成结果
class Compiler {
    constructor(config) {
        // entry output
        this.config = config
        // 保存入口文件路径
        this.entryId // './src/index.js'
        // 保存所有的模块依赖
        this.modules = {}
        this.entry = config.entry // 入口路径
        // 工作路径
        this.root = process.cwd()
        // 生命周期
        this.hooks = {
            entryOption: new SyncHook(),
            compile: new SyncHook(),
            afterCompile: new SyncHook(),
            afterPlugins: new SyncHook(),
            run: new SyncHook(),
            emit: new SyncHook(),
            done: new SyncHook()
        }
        // 如果传递了plugins参数
        let plugins = this.config.plugins
        if(Array.isArray(plugins)) {
            plugins.forEach( plugin => {
                plugin.apply(this)
            })
        }
        this.hooks.afterPlugins.call()
    }
    getSource(modulePath) {
        let content = fs.readFileSync(modulePath, 'utf8')
        if(this.config.module.rules){
            let rules = this.config.module.rules
            // // 拿到每个规则来处理
            for(let i = 0; i < rules.length; i++) {
                let rule = rules[i]
                let {test, use} = rule
                let len = use.length - 1
                if(test.test(modulePath)) { // 这个模块需要loader来转化
                    // 获取对应的loader函数
                    function normalLoader() {
                        let loader = require(use[len--])
                        content = loader(content)
                        // 递归调用loader,实现转化功能
                        if(len >= 0) {
                            normalLoader()
                        }
                    }
                    normalLoader()
                }
            }
        }
        return content
    }
    // 解析源码 AST解析语法树 打开https://astexplorer.net/对照语法树查看
    parse(source, parentPath) { // source源码, parentPath路径
        let ast = babylon.parse(source) // 把源码转换为ast
        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 = [t.stringLiteral(moduleName)]
                }
            }
        })
        let sourceCode = generator(ast).code
        return { sourceCode, dependencies }
    }
    // 构建模块
    buildModule(modulePath, isEntry) {
        // 拿到模块内容
        let source = this.getSource(modulePath)
        // 模块id modulePath - this.root
        let moduleName = './' + path.relative(this.root, modulePath)
        if(isEntry) { // 如果是主入口
            this.entryId = moduleName // 保存入口名
        }
        // 解析需要把source源码进行改造,并返回一个依赖列表
        let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)) // path.dirname(moduleName) 获取父路径 ./src
        // 把相对路径和模块中的内容对应起来
        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, 'main.ejs'))
        let code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules})
        this.assets = {}
        // 资源中路径对应的代码
        this.assets[main] = code;
        // 写到main文件中
        fs.writeFileSync(main, this.assets[main])
    }
    run() {
        this.hooks.run.call()
        this.hooks.compile.call()
        // 执行, 并且创建模块依赖关系
        this.buildModule(path.resolve(this.root, this.entry), true) // true代表是否是主模块
        this.hooks.afterCompile.call()
        // 发射一个文件 打包后的文件
        this.emitFile()
        this.hooks.emit.call()
        this.hooks.done.call()
    }
}

module.exports = Compiler

下面是ejs模版

(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;
    }
    return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})({
    <%for(let key in modules) {%>
        "<%-key%>":
        (function(module, exports, __webpack_require__) {
            eval(`<%-modules[key]%>`);
        }),
    <%}%>
});

新建loader文件夹
下面我们来实现一个style-loader

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

module.exports = loader

再来实现一个less-loader

let less = require('less')
function loader(source){
    let css = ''
    less.render(source, function(err, c){
        css = c.css
    })
    css = css.replace(/\n/g,'\\n')
    return css
}
module.exports = loader

来新建src目录,测试我们的webpack是否可以运行

// base/b.js
module.exports = 'bbb'

// a.js
let b = require('./base/b.js')

module.exports = 'aaa' + b

// index.js
require('./a.js')
require('./index.less')

// index.less
body{
    background: red;
}

你可能感兴趣的:(webpack原理解析)