在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
$ npm init -y
$ yarn add webpack webpack-cli html-webpack-plugin
webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {},
plugins: []
}
let a1=require('./a1');
console.log(a1);
let a2=require('./base/a2');
module.exports='a1'+a2;
module.exports='a2';
(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 = "./src/index.js");
})
({
"./src/a1.js":
(function (module, exports, __webpack_require__) {
eval("let a2 = __webpack_require__( \"./src/base/a2.js\");\r\nmodule.exports = 'a1' + a2;");
}),
"./src/base/a2.js":
(function (module, exports) {
eval("module.exports = 'a2';");
}),
"./src/index.js":
(function (module, exports, __webpack_require__) {
eval("let a1 = __webpack_require__(\"./src/a1.js\");\r\nconsole.log(a1);");
})
});
$ yarn add babel-types babel-generator babel-traverse
package.json
{
"name": "webpackhand",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": {
"webpackhand": "./bin/webpackhand.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}
bin\webpackhand.js
#! /usr/bin/env node
const path = require('path');
const fs = require('fs');
const Compiler = require('../lib/Compiler');
//命令的当前工作目录
const root = process.cwd();
//匹配配置文件对象
let options = require(path.resolve('webpack.config.js'));
let compiler = new Compiler(options);
compiler.run();
const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const t = require('babel-types');
const generate = require('babel-generator').default;
const traverse = require('babel-traverse').default;
const ejs = require('ejs');
class Compiler {
constructor(options) {
this.options = options;
}
run() {
let that = this;
this.root = process.cwd();//获取当前的工作目录
let { entry } = this.options;//获取入口文件路径
this.entryId = null;//记录入口文件的ID
this.modules = {};//记录模块ID和内容的对应关系,所有的ID都是相对于根目录的
this.buildModule(path.resolve(this.root, entry), true);//从入口文件开始编译
console.log(this.modules);
this.emitFile();
}
emitFile() {
let mainTemplate = fs.readFileSync(path.join(__dirname, 'main.ejs'), 'utf8');
let { modules, entryId } = this;
let main = ejs.compile(mainTemplate)({ entryId, modules });
let { output: { path: dist, filename } } = this.options;
fs.writeFileSync(path.join(dist, filename), main);
}
getSource(modulePath) {
return fs.readFileSync(modulePath, 'utf8');
}
//解析模块和依赖的模块,路径是一个绝对路径
buildModule(modulePath, isEntry) {
let source = this.getSource(modulePath);//获取源代码
let moduleId = './' + path.relative(this.root, modulePath);//生成相对于工作根目录的模块ID
if (isEntry) {//如果是入口的话把ID赋给入口
this.entryId = moduleId;
}
//获取AST的编译结果 依赖的模块 转换后的源代码
let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId));
this.modules[moduleId] = sourcecode;
//递归解析依赖的模块
dependencies.forEach(dependency => this.buildModule(path.join(this.root, dependency)));
}
//解析源代码 传入父路径
parse(source, parentPath) {
let that = this;
const ast = babylon.parse(source);
let dependencies = [];
traverse(ast, {
CallExpression(p) {
if (p.node.callee.name == 'require') {
let node = p.node;
node.callee.name = '__webpack_require__';
let modName = node.arguments[0].value;
modName += (modName.lastIndexOf('.') > 0 ? '' : '.js');
let moduleId = './' + path.join(parentPath, modName);
dependencies.push(moduleId);
node.arguments = [t.stringLiteral(moduleId)];
}
}
});
let sourcecode = generate(ast).code;
return { sourcecode, dependencies };
}
}
module.exports = Compiler;
main.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 id in modules){
let source = modules[id];%>
"<%-id%>":
(function (module, exports,__webpack_require__) {
eval(`<%-source%>`);
}),
<%}
%>
});
(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 = "./src\index.js");
})
({
"./src\index.js":
(function (module, exports,__webpack_require__) {
eval(`let a1 = __webpack_require__("./src\\a1.js");
console.log(a1);`);
}),
"./src\a1.js":
(function (module, exports,__webpack_require__) {
eval(`let a2 = __webpack_require__("./src\\base\\a2.js");
module.exports = 'a1' + a2;`);
}),
"./src\base\a2.js":
(function (module, exports,__webpack_require__) {
eval(`module.exports = 'a2';`);
}),
});
getSource(modulePath) {
let that = this;
let source = fs.readFileSync(modulePath, 'utf8');
let { module: { rules } } = this.options;
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
if (rule.test.test(modulePath)) {
let loaders = rule.use;
let loaderIndex = loaders.length - 1;
function iterateLoaders() {
let loaderName = loaders[loaderIndex--];
let loader = require(path.resolve(that.root, 'node_modules', loaderName));
source = loader(source);
if (loaderIndex >= 0) {
iterateLoaders();
}
}
iterateLoaders();
break;
}
}
return source;
}
var less = require('less');
module.exports = function (source) {
let css;
less.render(source, (err, output) => {
css = output.css;
});
return css.replace(/\n/g, '\\n', 'g');
}
module.exports = function (source) {
let str = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
`;
return str;
}
require('./index.less');
const path = require('path');
class EntryOptionWebpackPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap('Plugin', (option) => {
console.log('EntryOptionWebpackPlugin');
});
}
}
class AfterPlugins {
apply(compiler) {
compiler.hooks.afterPlugins.tap('Plugin', (option) => {
console.log('AfterPlugins');
});
}
}
class RunPlugin {
apply(compiler) {
compiler.hooks.run.tap('Plugin', (option) => {
console.log('RunPlugin');
});
}
}
class CompileWebpackPlugin {
apply(compiler) {
compiler.hooks.compile.tap('Plugin', (option) => {
console.log('CompileWebpackPlugin');
});
}
}
class AfterCompileWebpackPlugin {
apply(compiler) {
compiler.hooks.afterCompile.tap('Plugin', (option) => {
console.log('AfterCompileWebpackPlugin');
});
}
}
class EmitWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tap('Plugin', () => {
console.log('EmitWebpackPlugin');
});
}
}
class DoneWebpackPlugin {
apply(compiler) {
compiler.hooks.done.tap('Plugin', (option) => {
console.log('DoneWebpackPlugin');
});
}
}
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'less-loader']
}
]
},
plugins: [
new EntryOptionWebpackPlugin(),
new AfterPlugins(),
new RunPlugin(),
new CompileWebpackPlugin(),
new AfterCompileWebpackPlugin(),
new EmitWebpackPlugin(),
new DoneWebpackPlugin()
]
}
const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const t = require('babel-types');
const generate = require('babel-generator').default;
const traverse = require('babel-traverse').default;
const ejs = require('ejs');
const { SyncHook } = require('tapable');
class Compiler {
constructor(options) {
this.options = options;
this.hooks = {
entryOption: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
emit: new SyncHook(["compiler"]),
afterEmit: new SyncHook(),
done: new SyncHook()
}
let plugins = options.plugins;
if (plugins && plugins.length > 0) {
plugins.forEach(plugin => plugin.apply(this));
}
this.hooks.afterPlugins.call();
}
run() {
this.hooks.run.call(this);
let that = this;
this.root = process.cwd();//获取当前的工作目录
let { entry } = this.options;//获取入口文件路径
this.entryId = null;//记录入口文件的ID
this.modules = {};//记录模块ID和内容的对应关系,所有的ID都是相对于根目录的
this.hooks.compile.call();
this.buildModule(path.resolve(this.root, entry), true);//从入口文件开始编译
this.hooks.afterCompile.call();
this.emitFile();
}
emitFile() {
this.hooks.emit.call(this, this);
let mainTemplate = fs.readFileSync(path.join(__dirname, 'main.ejs'), 'utf8');
let { modules, entryId } = this;
let main = ejs.compile(mainTemplate)({ entryId, modules });
let { output: { path: dist, filename } } = this.options;
fs.writeFileSync(path.join(dist, filename), main);
this.hooks.afterEmit.call();
this.hooks.done.call();
}
}
module.exports = Compiler;
emitFile() {
this.hooks.emit.call(this, this);
let mainTemplate = fs.readFileSync(path.join(__dirname, 'main.ejs'), 'utf8');
let { modules, entryId } = this;
let main = ejs.compile(mainTemplate)({ entryId, modules });
let { output: { path: dist, filename } } = this.options;
fs.writeFileSync(path.join(dist, filename), main);
Object.entries(this.chunks).forEach(([chunkIndex, chunk]) => {
let chunkTemplate = fs.readFileSync(path.join(__dirname, 'chunk.ejs'), 'utf8');
let chunkData = ejs.compile(chunkTemplate)({ chunkIndex, chunk });
let { output: { path: dist, filename } } = this.options;
fs.writeFileSync(path.join(dist, `${chunkIndex}.bundle.js`), chunkData);
});
this.hooks.afterEmit.call();
this.hooks.done.call();
}
//解析模块和依赖的模块,路径是一个绝对路径
buildModule(modulePath, isEntry, chunkIndex) {
let source = this.getSource(modulePath);//获取源代码
let moduleId = './' + path.relative(this.root, modulePath);//生成相对于工作根目录的模块ID
if (isEntry) {//如果是入口的话把ID赋给入口
this.entryId = moduleId;
}
//获取AST的编译结果 依赖的模块 转换后的源代码
let { dependencies, sourcecode } = this.parse(source, path.dirname(moduleId));
if (typeof chunkIndex != 'undefined') {
let currentChunk = typeof this.chunks[chunkIndex] == 'undefined' ? {} : this.chunks[chunkIndex];
currentChunk[moduleId] = sourcecode;
this.chunks[chunkIndex] = currentChunk;
} else {
this.modules[moduleId] = sourcecode;
}
//递归解析依赖的模块
dependencies.forEach(dependency => this.buildModule(path.join(this.root, dependency, chunkIndex)));
}
//解析源代码 传入父路径
parse(source, parentPath) {
let that = this;
const ast = babylon.parse(source, {
plugins: ['dynamicImport']
});
let dependencies = [];
traverse(ast, {
CallExpression(p) {
if (p.node.callee.name == 'require') {
let node = p.node;
node.callee.name = '__webpack_require__';
let modName = node.arguments[0].value;
modName += (modName.lastIndexOf('.') > 0 ? '' : '.js');
let moduleId = './' + path.join(parentPath, modName);
dependencies.push(moduleId);
node.arguments = [t.stringLiteral(moduleId)];
} else if (t.isImport(p.node.callee)) {
let node = p.node;
let modName = node.arguments[0].value;//取得模块名
modName += (modName.lastIndexOf('.') > 0 ? '' : '.js');
let moduleId = './' + path.join(parentPath, modName);
p.replaceWithSourceString(`__webpack_require__.e(${that.chunkIndex}).then(__webpack_require__.t.bind(null, "${moduleId}", 7))`);
that.buildModule(path.join(that.root, moduleId), false, that.chunkIndex++);
}
}
});
let sourcecode = generate(ast).code;
return { sourcecode, dependencies };
}
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[<%=chunkIndex%>],{
<%
for(let id in chunk){
let source = chunk[id];%>
/***/ "<%-id%>":
/***/ function(module, exports,__webpack_require__) {
eval(`<%-source%>`);
/***/ },
<%}%>
}]);
let loadButton = document.querySelector('#loadButton');
loadButton.addEventListener('click', () => {
import('./video').then(video => {
console.log(video.default);
});
});
module.exports = 'video';