深入理解webpack打包机制(四)

王军强 2019-07-01

有了webpack的模版 并且有了各个模块之间的依赖关系,接下来我们就可以实现打包。
接下来就开始实现Compiler.js中的最终打包(即emit)方法:

写emit()方法之前,首先要安装一下ejs模块,我们需要用ejs模版引擎来解析刚才手写的webpck模版。
进入到my-pick目录, 运行命令:npm i ejs -D

Compiler.js:

let path = require('path');
let fs = require('fs');
let babylon = require('babylon');
let traverse = require('@babel/traverse').default;
let t = require('@babel/types');
let generator = require('@babel/generator').default;
let ejs = require('ejs');

class Compiler{
    constructor(config){
        this.config = config;
        this.entry = config.entry;
        this.entryId = '';
        this.modules = {};
        this.rootPath = process.cwd();
    }
    run(){
        this.buildModule(path.resolve(this.rootPath,this.entry),true);
        this.emit();
    }
    buildModule(modulePath, isEntry){
        let source = this.getSource(modulePath);
        let moduleName = './'+path.relative(this.rootPath,modulePath);
        if(isEntry){ this.entryId = moduleName };
        let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));
        this.modules[moduleName] = sourceCode;
        dependencies.forEach((depend)=>{
            this.buildModule(path.join(this.rootPath,depend),false);
        });
    }
    parse(source, parentPath){
        let dependencies = [];
        let ast = babylon.parse(source);
        traverse(ast, {
            CallExpression(p){
                if(p.node.callee.name === 'require'){
                    p.node.callee.name = '__webpack_require__';
                    let moduleName = p.node.arguments[0].value;
                    moduleName = moduleName + (path.extname(moduleName)?'':'.js');
                    moduleName = './'+path.join(parentPath,moduleName);
                    dependencies.push(moduleName);
                    p.node.arguments = [t.stringLiteral(moduleName)];
                }
            }
        });
        let sourceCode = generator(ast).code;
        return { sourceCode, dependencies };
    }
    getSource(sourcePath){
        return fs.readFileSync(sourcePath,'utf8');
    }
    emit(){
       let main = path.join(this.config.output.path,this.config.output.filename);
       let templateStr = this.getSource(path.resolve(__dirname, 'template.ejs'));
       let code = ejs.render(templateStr,{
           entryId:this.entryId,modules:this.modules
       });
       this.assets = {};
       this.assets[main] = code;
       fs.writeFileSync(main,this.assets[main]);
       
    }
}


module.exports = Compiler;

emit()方法就是最终我们实现webpack的打包方法。最后的bundle.js就是由该方法生成。
首先,通过path.join()方法和config中的output 获取到最终的打包文件的路径。
第二行又获取到之前写好的模版文件:template.ejs。最终通过ejs模块解析 并且传入主入口entryId和依赖关系对象modules生成最终的打包文件。
生成了最终的打包文件后就很简单了,首先把最终的文件放到this.assets对象中,最后又通过fs.readFileSync()写入bundle.js文件到输出路径。
最后,回到webpack的目录:运行自己的pick命令:npx my-pack. 即可看到dist目录中多了一个bundle.js的文件
bundle.js:

(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 = "./src/index.js");
    })
    /* 自执行函数 传入参数 */
    ({  
        
            "./src/index.js":  
            (function(module, exports, __webpack_require__) {
                eval(`console.log('index.js');

__webpack_require__("./src/a.js");`);
            }),
        
            "./src/a.js":  
            (function(module, exports, __webpack_require__) {
                eval(`let b = __webpack_require__("./src/b.js");

console.log('a.js');
console.log(b);`);
            }),
        
            "./src/b.js":  
            (function(module, exports, __webpack_require__) {
                eval(`module.exports = 'b.js';`);
            }),
        
       });

可以看到 ,所以的依赖关系都被传递到了webpack的自执行函数的参数中。通过右键run,或者浏览器中打开。发现打印了我们写的代码:

index.js
a.js
b.js

到此为止,我们已经手动实现了webpack的打包功能。当然这只是webpack中的冰山一角,我们只是简单实现了解析模块的依赖关系,打包了js文件。像webpack的钩子,loader,plugins,css文件..等等都没有进行处理。
可能会在以后的文章中会较为深入的解析webpack的loader和plugins是如何实现的。谢谢观看~喜欢点个

相关推荐