自己实现一个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<rules.length;i++){
            let rule= rules[i];
            let {test,use}=rule;
            let len = use.length-1;
            if(test.test(modulePath)){//如果能匹配上 那就说明模块需要被loader转化
                function normalLoader(){
                    //获取loader 函数
                    let loader = require(use[len--]);
                    // len -- ;
                    //递归调用loader 实现转化功能
                    content = loader (content);
                    if(len >=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,到这一步自定的打包器就测试完成了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容

  • 简单介绍webpack-dev-middleware,作用就是,生成一个与webpack的compiler绑定的中...
    前端大飞阅读 12,491评论 2 12
  • 一、一个入口,一个文件 webpack.config.js main.js bundle.js 整体分析整个的bu...
    婷楼沐熙阅读 4,824评论 0 7
  • 转载自: http://echizen.github.io/tech/2019/03-17-webpack-bun...
    李留白阅读 1,415评论 0 0
  • publicPath指定了一个在浏览器中被引用的URL地址。 对于使用 和 加载器,当文件路径不同于他们的本地磁盘...
    飞呀飞哥阅读 1,688评论 0 0
  • 目录第1章 webpack简介 11.1 webpack是什么? 11.2 官网地址 21.3 为什么使用 web...
    lemonzoey阅读 1,731评论 0 1