手写webpack得打包流程

目录

  1. 搭建一个最基础的环境(用于测试)
  2. 本地新建一个文件夹(打包库)webpack-meself
  3. 分析webpack环境打包后的js
  4. my-pack.js文件书写
  5. 手写Compiler.js
  6. 解析包,对源码source的改造
  7. emitFile方法
  8. 加入loader解析
  9. 给my-pack 添加生命周期

1.搭建一个最基础的环境(用于测试)

新建一个文件夹 webpack-test
1.初始下项目 npm init -y
2.安装webpack webpack-cli npm install [email protected] [email protected] -D
3.简单配置下文件

基本的几个js文件 相互关系稍微复杂点,如果只用一个Index.js也行
src/index.js

let str = require("./base/b.js");
console.log('asda);
module.exports = "a.js";

src/a.js

let str = require("./base/b.js");
console.log(str);
module.exports = "a.js";

src/base/b.js

module.exports='a.js'

配置文件

let path = require("path");

module.exports = {
    mode: "development",
    entry: {
        home: "./src/index.js",
    },
    output: {
        filename: "index.js",
        path: path.resolve(__dirname, "dist"),
    },
};

2.本地新建一个文件夹(打包库)webpack-meself

初始化 npm init -y

package.josn中配置下加入bin属性:映射到本地本地文件的作用

{
  
  "bin":{
    "my-pack":"./bin/my-pack.js"
  }
}

根目录下新建 bin文件夹,下面新建my-pack.js 跟package.json中配置的映射地址文件名保持一致

my-pack.js 
#! /usr/bin/env node   

// 上面那段 意思是使用 node 进行脚本的解释程序
console.log("asda");

然后链接到全局方便使用 npm link,链接成功下方可以看到已经到nom\node_modules里了。
在这里插入图片描述
最后,在我们第一步搭好的环境中使用,需要link 引入
npm link将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试

npm link my-pack 映射到本地对应的文件
在这里插入图片描述

执行一下,就有结果了 在这里插入图片描述
3.分析webpack环境打包后的js

去掉各种注视后

 (function(modules) { // webpackBootstrap
 	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;
 	}
 	__webpack_require__.m = modules;
 	__webpack_require__.c = installedModules;
 	__webpack_require__.d = function(exports, name, getter) {
 		if(!__webpack_require__.o(exports, name)) {
 			Object.defineProperty(exports, name, {
 				configurable: false,
 				enumerable: true,
 				get: getter
 			});
 		}
 	};
 	__webpack_require__.r = function(exports) {
 		Object.defineProperty(exports, '__esModule', { value: true });
 	};
 	__webpack_require__.n = function(module) {
 		var getter = module && module.__esModule ?
 			function getDefault() { return module['default']; } :
 			function getModuleExports() { return module; };
 		__webpack_require__.d(getter, 'a', getter);
 		return getter;
 	};
 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
 	__webpack_require__.p = "";
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })
 ({

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

eval("let str = __webpack_require__(/*! ./base/b.js */ \"./src/base/b.js\");\nconsole.log(str);\nmodule.exports = \"a.js\";\n\n\n//# sourceURL=webpack:///./src/a.js?");

 }),

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

eval("module.exports='a.js'\n\n//# sourceURL=webpack:///./src/base/b.js?");

 }),

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

eval("let str = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\nconsole.log(\"index.js\");\n\n\n//# sourceURL=webpack:///./src/index.js?");

 })

 });

分析源码后发现 就是一个匿名函数不断的调用__webpack_require__方法
再去掉没用的代码

(function (modules) {
    // webpackBootstrap
    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/a.js": function (module, exports, __webpack_require__) {
        eval(
            'let str = __webpack_require__(/*! ./base/b.js */ "./src/base/b.js");\nconsole.log(str);\nmodule.exports = "a.js";\n\n\n//# sourceURL=webpack:///./src/a.js?'
        );
    },

    "./src/base/b.js": function (module, exports) {
        eval(
            "module.exports='a.js'\n\n//# sourceURL=webpack:///./src/base/b.js?"
        );
    },

    "./src/index.js": function (module, exports, __webpack_require__) {
        eval(
            'let str = __webpack_require__(/*! ./a.js */ "./src/a.js");\nconsole.log("index.js");\n\n\n//# sourceURL=webpack:///./src/index.js?'
        );
    },
});

新建一个html 单独引入这个js 能跑ok,我们的目的就是输出这么一个js,这个可以做为一个模板

4.my-pack.js文件书写

#! /usr/bin/env node

// 上面那段 意思是使用 node 进行脚本的解释程序
console.log("asda");

// 1) 需要找到当前目录下的路径,找到webpack.config.js

let path = require("path");

// 找到config配置文件 webpack.config.js
let config = require(path.resolve("webpack.config.js"));

// 引入
let Compiler = require("../lib/Compiler.js");
// new一个实例
let Com = new Compiler(config);

// 调用 run方法运行编译
Com.run();

5.手写Compiler.js

config参数就是webpack.config.js

let path = require("path");
let fs = require("fs"); //node的文件模块,带有很多API,
class Compiler {
    constructor(config) {
        this.config = config;
        // 需要保存入口文件的路径
        this.entryId; //'./src/index.js' 保存主入口路径
        // 需要保存所有的模块依赖  对应上面模板Js中 匿名函数传入的对象
        this.mudeles = {};
        // 如果路径
        this.entry = config.entry.home;
        // 工作路径 运行 npx my-pack的文件路径
        this.root = process.cwd();
    }

    // 拿模块的内容
    getSource(moudlePath) {
        return fs.readFileSync(moudlePath, "utf8");
    }
    // 创建模块的依赖关系 构建模块
    buildModule(moudlePath, isEntry) {
        // 拿模块的内容 index.js 根据绝对路径
        let source = this.getSource(moudlePath);

        // 拿模块的相对路径 = moudlePath- this.root =src\index.js  需要前面加./
        let moduleName = "./" + path.relative(this.root, moudlePath);

        // 判断是否是主入口 存路径
        if (isEntry) {
            this.entryId = moduleName;
        }
        console.log(source, moduleName, moudlePath);
       
        // 解析包source源码改造,返回一个依赖列表 //path.dirname去掉文件名 这里返回的应该是./src
        let { sourceCode, dependencies } = this.parse(
            source,
            path.dirname(moduleName)
        );

        // 把相对路径和模块中的内容 对应起来
        this.mudeles[moduleName] = sourceCode;
    }

    emitFile() {}

    run() {
    	//两步操作
    	
        // 执行、并且创建模块的依赖关系  true 是主模块  path.resolve(this.root, this.entry)返回的是拼接好的主路径
        this.buildModule(path.resolve(this.root, this.entry), true);

        // 发射一个文件,打包后的文件
        this.emitFile();
    }
}

module.exports = Compiler;

6.解析包,对源码source的改造
上面获取到了source源码的改造

这里需要用的几个插件
// babylon 将源码转换成AST
//@babel/traverse 遍历,需要遍历到对应的节点
//@babel/types 节点替换
//@babel/generator 替换好的结果输出

// 解析源码
顶部引入
let babylon = require("babylon");
let traverse = require("@babel/traverse").default; //是个es6模块 需要.default
let t = require("@babel/types");
let generator = require("@babel/generator").default; //是个es6模块 需要.default

// 解析源码
    parse(source, parentPath) {
        // AST解析语法树
        let ast = babylon.parse(source);
        let dependencies = []; //依赖数组
        // https://astexplorer.net/   看下ast下 require是用什么方法去判断转译的
        traverse(ast, {
            CallExpression(p) {
                //转译require()
                // 拿到节点
                let node = p.node;
                if (node.callee.name === "require") {
                    //__webpack_require__  是输出的模板js中的主要方法
                    node.callee.name = "__webpack_require__";
                    // 路径也更改
                    let moduleName = node.arguments[0].value; //这里输出的是没有文件的扩展名的
                    moduleName =
                        moduleName + (path.extname(moduleName) ? "" : ".js"); // index.js
                    moduleName = "./" + path.join(parentPath, moduleName); // ./src/index.js

                    // 放入数组
                    dependencies.push(moduleName);

                    // 节点替换
                    node.arguments = [t.stringLiteral(moduleName)];
                }
            },
        });

        let sourceCode = generator(ast).code;
        return { sourceCode, dependencies };
    }

手写webpack得打包流程_第1张图片
在buildModule方法中递归调用 加载副模块

// 创建模块的依赖关系 构建模块
    buildModule(moudlePath, isEntry) {
        // 拿模块的内容 index.js 根据绝对路径
        let source = this.getSource(moudlePath);

        // 拿模块的相对路径 = moudlePath- this.root =src/index.js  需要前面加./
        let moduleName = "./" + path.relative(this.root, moudlePath);

        // 判断是否是主入口 存路径
        if (isEntry) {
            this.entryId = moduleName;
        }

        // console.log(source, moduleName, moudlePath);
        // 解析需要包source源码改造,返回一个依赖列表 //path.dirname去掉文件名 这里返回的应该是./src
        let { sourceCode, dependencies } = this.parse(
            source,
            path.dirname(moduleName)
        );

        // console.log(sourceCode, dependencies);

        // 把相对路径和模块中的内容 对应起来
        this.modules[moduleName] = sourceCode;

        // 递归调用 加载副模块
        dependencies.forEach((item) => {
            this.buildModule(path.join(this.root, item), false);
        });
    }

最后跑一下输出(这里去掉所有的console)打印 console.log(this.modules, this.entryId);跟我的模板js中传入的参数一样了
手写webpack得打包流程_第2张图片
**7.emitFile **
输出文件js,这边将上面的模板转成ejs ,然后动态设置获取值
手写webpack得打包流程_第3张图片

(function (modules) {
// The module cache
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]%>`);
}),
<%}%>
});

emitFile方法

emitFile() {
        // 拿到输出到哪个目录下
        let main = path.join(
            this.config.output.path,
            this.config.output.filename
        );

        // 获取模板
        let templeteStr = this.getSource(path.resolve(__dirname, "main.ejs"));

        // ejs传值进去
        let code = ejs.render(templeteStr, {
            entryId: this.entryId,
            modules: this.modules,
        });

        // 存放,可以使用多个入口
        this.assets = {};

        // 路径对应的代码
        this.assets[main] = code;

        // 文件中写入对应的代码
        fs.writeFileSync(main, this.assets[main]);
    }

test项目中跑npx my-pack 这里必须得有个dist文件夹 不然报错
手写webpack得打包流程_第4张图片
将这个js,引入html 后是可以输出打印的,

8.加入loader解析
简单得js 能打包输出实现以后,这里我们尝试在测试包下加入一个在src下创建index.less文件
index.less npm i less -D

body{
    background: red;
    div {
        background: yellow;
    }
}

测试包 根目录下 创建loader文件夹,里面新建
在这里插入图片描述
less-loader.js

let less = require("less");
// less 转css 功能
function loader(source) {
    let css = "";
    // 可以在Node中调用编译器  '.class { width: (1 + 1) }'  输出 .class { width: 2 }
    less.render(source, function (err, c) {
        css = c.css;
    });
    css = css.replace(/\n/g, "\\n");  \n 换行符需要转译下
    return css;
}
module.exports = loader;

style-loader.js

// css插入页面
function loader(source) {
    // innerHTML 无法识别换行(这里不是直接拿到得n/) 需要转成一行
    let style = `
        let style=document.createElement('style');
        style.innerHTML=${JSON.stringify(source)}
        document.head.append(style)
    `;
    return style;
}
module.exports = loader;

webpack.config.js中配置规则,然后在index.js中引入
require("./index.less");

module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    path.resolve(__dirname, "loader", "style-loader"),
                    path.resolve(__dirname, "loader", "less-loader"),
                ],
            },
        ],
    },

接下来可以开始写我们my-pack的包了
主要是在getSource方法中写,获取模块内容,上面已经实现了可以递归获取,所以这边content能拿到index.less的内容。现在要匹配配置文件中的规则去解析

// 拿模块的内容
    getSource(moudlePath) {
        let content = fs.readFileSync(moudlePath, "utf8");
        // 拿module中得rules
        let rules = this.config.module.rules;
        // 匹配需要用什么规则解析
        rules.forEach((item) => {
            // 找到对应得规则解析
            if (item.test.test(moudlePath)) {
                // Loader顺序下往上执行
                for (let i = item.use.length - 1; i >= 0; i--) {
                    console.log(i);
                    let loader = require(item.use[i]);
                    content = loader(content);
                }
            }
        });
        return content;
    }

测试包npx my-pack效果
手写webpack得打包流程_第5张图片
9.给my-pack 添加生命周期

Compiler 控制器里加入生命周期,这里用到了tapabel中的SyncHook同步钩子,专注于自定义事件的触发和处理,webpack就是通过tapabel里面的同步异步钩子来串联插件的,SyncHook原理可以看下我上一篇文章。

// 专注于自定义事件的触发和处理 webpack 就是通过他来串联各种插件的, 里面有 同步钩子 和 异步钩子
my-pack包中:
let tapabel = require("tapable");
class Compiler {
	constructor(config) {
			// 这里可以设置一些生命周期
        this.hooks = {
            // 入口
            entryOption: new tapabel.SyncHook(),
            // 编译周期
            compile: new tapabel.SyncHook(),
            // 编译之后
            afterCompile: new tapabel.SyncHook(),
            // 编译插件以后
            afterPlugins: new tapabel.SyncHook(),
            // 运行的时候
            run: new tapabel.SyncHook(),
            // 执行完成
            done: new tapabel.SyncHook(),
            // 发送文件
            emit: new tapabel.SyncHook(),
        };
	}
}

我们可以在各个步骤中加入这些生命周期

入口周期
my-pack.js
手写webpack得打包流程_第6张图片
运行周期,编译周期,编译之后周期,发送文件周期,完成周期
conpiler.js
手写webpack得打包流程_第7张图片
插件周期

constructor(config) {
        // 这里可以设置一些生命周期
        this.hooks = {
            // 入口
            entryOption: new tapabel.SyncHook(),
            // 编译周期
            compile: new tapabel.SyncHook(),
            // 编译之后
            afterCompile: new tapabel.SyncHook(),
            // 编译插件以后
            afterPlugins: new tapabel.SyncHook(),
            // 运行的时候
            run: new tapabel.SyncHook(),
            // 执行完成
            done: new tapabel.SyncHook(),
            // 发送文件
            emit: new tapabel.SyncHook(),
        };

        // 获取插件
        let plugins = this.config.plugins;
        console.log(plugins);
        if (Array.isArray(plugins)) {
            plugins.forEach((item) => {
                console.log(item);
                item.apply(this);
            });
        }
        this.hooks.afterPlugins.call();
    }

测试生命周期
test包中
创建一个插件 ApplyChaJian.js

class ApplyChaJian {
    apply(compiler) {
        // SyncHook中注册tap事件 call的时候执行
        compiler.hooks.emit.tap("emit", function () {
            console.log("emit:发射周期");
        });
        console.log("A插件");
    }
}
module.exports = ApplyChaJian;

webpack.config.js中配置

顶部
let ApplyChaJian = require("./src/ApplyChaJian");
webpack.config.js加入
plugins: [new ApplyChaJian()],

手写webpack得打包流程_第8张图片
成功!这里说明了ApplyChaJian 这个插件中apply方法中注册的方法emit 是在 emitFile生命周期的时候执行

你可能感兴趣的:(webpack)