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 模块链接到对应的运行项目中去,方便地对模块进行调试和测试
执行一下,就有结果了
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 };
}
// 创建模块的依赖关系 构建模块
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中传入的参数一样了
**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中得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效果
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生命周期的时候执行