自己实现一个webpack

为了了解webpack是怎么运行的,下面带领大家实现一个自己的webpack

初始化工程

使用yarn init -y 或者 npm init -y 快速初始化工程

安装的相关依赖包如下:

{
  "name": "cwtpack",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "bin": {
    "cwt-pack": "./bin/cwt-pack.js"
  },
  "dependencies": {
    "@babel/generator": "^7.8.8",
    "@babel/traverse": "^7.8.6",
    "@babel/types": "^7.8.7",
    "babylon": "^6.18.0",
    "ejs": "^3.0.1",
    "tapable": "^1.1.3"
  }
}

目录结构


image-20200418134109942.png

package.json中的bin

在bin中配置执行指令及执行哪一个文件,如我的配置指令名为cwt-pack运行bin文件夹下的cwt-pack.js

可以看这篇博客更具体的了解其作用

cwt-pack.js

#! /usr/bin/env node

// 1.需要找到当前执行名的路径 拿到webpack.config.js
let path=require('path');
//获取配置文件名参数,没有则默认使用名为webpack.config.js文件
let configFileName=process.argv[2] || 'webpack.config.js';
//config 配置文件
let config;
try {
    config=require(path.resolve(configFileName));
} catch (error) {
    console.log('配置文件不存在,请先创建配置文件');
    //结束进程
    process.exit();
}
let Compiler = require('../lib/Compiler.js');
let compiler = new Compiler(config);
compiler.hooks.entryOption.call();
//标识运行编译
compiler.run();

这个文件是我们打包器的入口。

#! /usr/bin/env node用于指明该脚本文件要使用node来执行,它必须放在第一行,否则不生效;其中#! 可以让系统动态的去查找node,以解决不同机器不同用户设置不一致问题。

config默认webpack.config.js,如需自定义配置文件名 可在执行指令后添加参数,如:cwt-pack aa.js,如果在打包的项目根目录中找不到这个配置文件,会在终端中打印配置文件不存在,请先创建配置文件并结束进程。

require 位于lib文件夹下的 Compiler.js,调用run方法编译文件。

Compiler.js

这就是我们打包器的核心代码了,分为六部分讲解:

  • constructor

    构造方法

    初始化变量,遍历配置文件中的plugins插件并调用插件apply方法,插件的样子:

    class MyPlugin{
        //传入compiler对象,每个插件都要有这个applay方法
        apply(compiler){
            console.log('start');
            //发布一个事件
            compiler.hooks.afterPulgins.tap('afterPulgins',function(){
                console.log('afterPulgins')
            });
        }
    }
    

插件需要在不同的生命周中执行各自的方法,这时我们就需要使用tapable帮助我们实现事件流传递,tapable它是一个基于发布订阅模式实现的事件流机制。在hooks中设置多个钩子,在插件中发布,然后在生命周期的不同位置订阅。例如this.hooks.afterPulgins.call();这就在消费afterPulgins钩子中的方法。

  • getSource(modulePath)

    获取源码方法

    通过modulePath读取文件内容,遍历配置文件中的rules取得loader后处理源码并返回。

  • parse(source,parentPath)

    转化源码方法

    参数source是getSource方法返回的源码,parentPath父路径用于构建当前模块名。

    通过babylon.parse(source)将源码转化为ast语法树,

    traverse遍历ast,使用CallExpression方法操作节点,将require方法名替换为__webpack_require__

    generator(ast)取得转化后的代码,

    返回转化后的代码和依赖关系

  • buildModule(modulePath,isEntry)

    建立模块名与模块代码关系方法

    参数modulePath模块路径,isEntry是否为入口

    通过parse返回的依赖关系递归,并将模块名与转化后的代码以键值对的形式存入this.modules变量中

  • emitFile()

    发射文件方法

    获取ejs模板,使用ejs.render方法取得渲染后的代码,这个就是最终打包后的代码了

    使用fs写文件到config.output.path

  • run

    入口方法,启动Compiler。

let path = require('path');
let fs = require('fs');
//Babylon 把源码转为AST
let babylon = require('babylon');
//@babel/traverse
let traverse = require('@babel/traverse').default;
//@babel/types
let types = require('@babel/types');
//@babel/generator
let generator = require('@babel/generator').default;
let ejs = require('ejs');
let {SyncHook}=require('tapable');
class Compiler{
    constructor(config){
        //entry output
        this.config = config;
        //需要保存入口文件路径
        this.entryId;
        //保存需要的所有模块依赖
        this.modules={};
        this.entry = config.entry;//入口路径
        this.root = process.cwd();//工作路径
        this.hooks={//编译生命周期的钩子
            entryOption:new SyncHook(),
            compile:new SyncHook(),
            afterCompile:new SyncHook(),
            afterPulgins:new SyncHook(),
            run:new SyncHook(),
            emit:new SyncHook(),
            done:new SyncHook()
        }
        //获取插件列表
        let plugins= this.config.plugins;
        //判断有无插件
        if(Array.isArray(plugins)){
            plugins.forEach(plugin=>{
                //执行插件中的apply方法,每个插件都会有这个apply方法,如果你自定义过webpack插件应该能明白
                plugin.apply(this);
            });
            this.hooks.afterPulgins.call();
        }
    }
    //获取源码
    getSource(modulePath){
        let rules= this.config.module.rules;
        let content = fs.readFileSync(modulePath,'utf-8');
        //拿到每个规则 来处理
        for(let i = 0;i=0){
                        normalLoader();
                    }
                    
                }
                normalLoader();
            }
        }
        
        return content;
    }
    //解析文件 转换文件内容
    parse(source,parentPath){
        //使用AST 解析语法树  将源码解析
        let ast = babylon.parse(source);
        let dependencies = [];//依赖数组
        //遍历ast树
        traverse(ast,{
            //进入ast节点
            CallExpression(p){// p 是源码中的方法 如a()
                let node = p.node;
                if(node.callee.name === 'require'){
                    //将require改成__webpack_require__
                    node.callee.name = '__webpack_require__';
                    let moduleName = node.arguments[0].value;//取到模块的引用名字 如require('./a) value=./a
                    moduleName = moduleName+(path.extname(moduleName)?'':'.js');// ./a.js
                    moduleName = './'+path.join(parentPath,moduleName) // path.join(parentPath,moduleName) 返回 src/a.js 要加上./
                    //将依赖模块名存入dependencies
                    dependencies.push(moduleName);
                    node.arguments = [types.stringLiteral(moduleName)];
                }
            }
        });
        //取得转化后代码
        let sourceCode = generator(ast).code;
        return {
            sourceCode,
            dependencies
        }
    }
    //创建模块依赖关系
    buildModule(modulePath,isEntry){
        //拿到模块内容
        let source= this.getSource(modulePath);
        //模块id moduleName = modulePath - this.root
        let moduleName ='./' + path.relative(this.root,modulePath);//path.relative 返回的是 src/index.js 所以要加上./
        if(isEntry){
            this.entryId = moduleName;
        }
        //转换文件内容  返回一个依赖列表
        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(){
        
        //用数据渲染模板 main.ejs
        //main 文件输出路径
        let main = path.join(this.config.output.path,this.config.output.filename);
        //ejs 模板路径
        let templateString = this.getSource(path.join(__dirname,'main.ejs'));
        //经过ejs渲染后的代码
        let code = ejs.render(templateString,{entryId:this.entryId,modules:this.modules});
        this.assets={};
        //资源中 路径对应的代码
        this.assets[main] = code;

        //判断输出文件夹是否存在
        if(!fs.existsSync(this.config.output.path)){
            //创建输出的文件夹
            fs.mkdirSync(this.config.output.path);
        }
        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.emitFile();
        this.hooks.emit.call();
        this.hooks.done.call();
    }
}

module.exports = Compiler;

main.ejs

这个main.ejs是打包后的js模板,这里就直接搬webpack的,关于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]%>`);
    }),
<%}%>    


});

乍一看一脸懵逼,其实webpack打包后的就是一个自运行函数,简单模拟一下:

(function(module){
    //缓存已递归的依赖
    var installedModules = {};
    function __webpack_require__(id){
        //如果有在缓存中则直接返回
        if(installedModules[moduleId]) {
            return installedModules[moduleId].exports
        }
        //将当前递归的依赖id存入installedModules
        var module = installedModules[moduleId] = {
            
            exports {}
        };
        //通过modules[moduleId]调用方法实现层层递归
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        return module.exports
    }
  //第一次调用传入入口文件id
    return __webpack_require__("./src/index.js")
})({
    "./src/index.js":(function(module, exports,__webpack_require__){
        eval(`let a = __webpack_require__("./src/a.js");
          console.log(a)`);
    }),
    "./src/a.js":(function(module, exports,__webpack_require__){
        eval(`module.exports= 'a';`);
    }),
});

index.js:

import a from './a';
console.log(a)

a.js:

module.exports = 'a';

示例代码中我已将__webpack_require__方法最简化。

自运行函数参数传入的是依赖文件列表:

  • 键:依赖的id(其实就是文件的路径),

  • 值:一个函数通过eval(经过Compiler编译后的代码)执行代码并递归依赖

这部分挺难懂的,需要认真的研究一下代码,理清逻辑,看懂之后就能明白webpack打包出来的是什么东西了。

测试使用

通过 npm link 或 npm install . -g 打包到全局中去

image-20200418130326889.png

进入nodejs全局仓库查看

image-20200418130630968.png
image-20200418130706136.png

然后起一个项目,写一个测试的配置文件

webpack.config.js

let path=require('path');
class P{
    apply(compiler){
        console.log('start');
        compiler.hooks.emit.tap('emit',function(){
            console.log('emit')
        });
    }
}
class P1{
    apply(compiler){
        console.log('start');
        compiler.hooks.afterPulgins.tap('afterPulgins',function(){
            console.log('afterPulgins')
        });
    }
}
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'),
                    path.resolve(__dirname,'loader','less-loader')
                ]
            }
        ]
    },
    plugins:[
        new P(),
        new P1()
    ]
}

在项目根目录创建loader文件夹里面创建两个loader

style-loader.js

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.js

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下创建index.js a.js index.less

index.js

let str =require('./a');
require('./index.less');
console.log(str);

a.js

module.exports='a';

index.less

body{
    background: red;
}

测试项目的最终的文件目录结构

image-20200418131735801.png

在终端进入这个项目的文件夹 输入cwt-pack 或者 cwt-pack 配置文件.js 执行成功后有以下输出

image-20200418131055360.png

然后可以在项目的根目录中看到dist文件夹,里面有个bundle.js,到这一步自定的打包器就测试完成了

你可能感兴趣的:(自己实现一个webpack)