目录

  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 webpack@4.0.0 webpack-cli@3.3.12 -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) { // webpackBootstrapvar 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) {// webpackBootstrapvar 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.jslet 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去掉文件名 这里返回的应该是./srclet { 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.jsmoduleName = "./" + path.join(parentPath, moduleName); // ./src/index.js// 放入数组dependencies.push(moduleName);// 节点替换node.arguments = [t.stringLiteral(moduleName)];}},});let sourceCode = generator(ast).code;return { sourceCode, dependencies };}


在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去掉文件名 这里返回的应该是./srclet { 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中传入的参数一样了

**7.emitFile **
输出文件js,这边将上面的模板转成ejs ,然后动态设置获取值

(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文件夹 不然报错

将这个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中得ruleslet 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效果

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

运行周期,编译周期,编译之后周期,发送文件周期,完成周期
conpiler.js

插件周期

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()],


成功!这里说明了ApplyChaJian 这个插件中apply方法中注册的方法emit 是在 emitFile生命周期的时候执行

手写webpack得打包流程相关推荐

  1. babel原理_手写webpack核心原理,再也不怕面试官问我webpack原理

    手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6转成ES5(AST) 七.递归获取 ...

  2. 手写webpack系列一:了解认识loader-utils

    Created By JishuBao on 2019-03-29 12:38:22 Recently revised in 2019-04-01 12:38:22   欢迎大家来到技术宝的掘金世界, ...

  3. 手写数字识别整体流程

    手写数字识别整体流程 pytorch中数据加载 batch:数据打乱顺序,组成一波-波的数据,批处理 epoch:拿所有的数据训练一-次 Dataset基类,数据集类 torch.utils.data ...

  4. 使用Caffe进行手写数字识别执行流程解析

    之前在 http://blog.csdn.net/fengbingchun/article/details/50987185 中仿照Caffe中的examples实现对手写数字进行识别,这里详细介绍下 ...

  5. react打包后图片丢失_手写Webpack从0编译Vue/React项目

    当前前端开发,90%的项目都是Vue和React,然而70%的同学都基于脚手架创建项目,因为脚手架会包含项目基本框架.webpack配置.scss/sass/less解析.babel配置.DevSer ...

  6. react 图片放在src里面还是public_手写Webpack从0编译Vue/React项目

    当前前端开发,90%的项目都是Vue和React,然而70%的同学都基于脚手架创建项目,因为脚手架会包含项目基本框架.webpack配置.scss/sass/less解析.babel配置.DevSer ...

  7. 手写webpack插件,小白入门 —— md文档转成html

    Compiler 模块是 webpack 的主要引擎,它通过 CLI 传递的所有选项, 或者 Node API,创建出一个 compilation 实例. 它扩展(extend)自 Tapable 类 ...

  8. java流程引擎实现_手写实现一套流程编排

    转载自:https://blog.csdn.net/weixin_39631301/article/details/112082970 流程编排 随着业务的不断发展,业务流程迭代慢慢变得复杂了起来,全 ...

  9. webpack的打包流程是什么?

    1.初始化一个管理包 yarn init 2 .安装使用webpack所需要的依赖包 yarn add webpack webpack-cli -D 3. 在package.json文件中配置scri ...

最新文章

  1. python,day13-堡垒机
  2. Microsoft Office SharePoint Server 2007 Starter Page
  3. 网络工程师必懂的专业术语!
  4. opengl 高级技巧
  5. 阿里云新手必踩坑系列 - 安全组
  6. 为何 Emoji 能给产品设计(营销)带来如此大的数据增长?
  7. Docker与容器化-04-Docker私有仓库与镜像迁移备份
  8. 计算机科学技术专业词汇,计算机专业一些单词
  9. 微信小程序教程、开发资源下载汇总(2.8日更新,持续更新中……)
  10. docker中启动Springboot时异常之Failed to instantiate [com.zaxxer.hikari.HikariDataSource]
  11. 利用向量求点到线的最短距离
  12. python和origin数据分析_【Origin】【Python】大学物理实验数据处理
  13. 学校计算机实训室座次安排,班级座位调整流程设计
  14. 解决Linux下adb devices找不到设备
  15. win10如何设置自己喜欢图片为桌面背景
  16. 程序员必备的5个自媒体工具
  17. Google C++编程风格
  18. 结构型设计模式(七种)
  19. 【三角】【棱形】【等等】
  20. 群联USB3.0 PS2251-02\PS2251-03主控量产工具

热门文章

  1. html将页面分成四部分,将HTML页面拆分为定义的宽度和高度部分
  2. C盘空间不足?扩充C盘
  3. 关于阿里云视频点播出现的问题
  4. 智源AI日报(2022-09-02):我是如何从头开始写一篇顶级论文的
  5. Vue中使用mavonEditor插件实现markdown在线编辑
  6. SWR:最具潜力的 React Hooks 数据请求库
  7. ps之一寸照片的制作详解(1)
  8. c# chart缩放,局部放大
  9. 利用轻量级js插件Beer Slider实现新老图片的实时对比
  10. Springboot汽车配件销售管理系统毕业设计源码131650