# 前言
在上篇文章-教你如何手写 webpack 源码,其实并没那难中主要完成了JS模块的打包,那么想要处理非JS的语言就要用到了loader和plugins,实现的大概思路我们一起探讨下。
# loader 的实现
其实一个loader就是一个函数,我们先在项目代码里创建一个loader文件夹,再分别创建less-loader.js 和 style-loader.js,代码如下:
less-loader.js 文件
const 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;
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;
项目代码里,配置webpack.config.js引入loader,如下:
const path = require('path');
module.exports = {
mode: "development",
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rolues: [{
test: /\.less$/,
// 引入我们手写的 loader
use: [
path.resolve(__dirname, 'loader', 'style-loader.js'),
path.resolve(__dirname, 'loader', 'less-loader.js')
]
}]
}
}
那么接下来我们就开始写打包的代码,需要在拿源码的函数getSoure里做处理:
Compiler.js 文件 ==> 只更新了 getSoure 方法,其他代码相同
// 读取路径下的模块内容
getSoure(modulePath) {
let rules = this.config.module.rules;
let content = fs.readFileSync(modulePath, 'utf8');
// 循环遍历每一个规则
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
let { test, use } = rule;
let len = use.length - 1;
// 判断模块是不是需要 loader 来转换
if (test.test(modulePath)) {
function normalLoader() {
let loader = require(use[len--]);
// 递归调用 laoder 实现转化功能
content = loader(content)
if (len >= 0) {
normalLoader();
}
}
normalLoader();
}
}
return content;
};
这时运行npx mypack打包后,运行代码查看,样式已经生效了,noce ~~
# plugins 的实现
我们每次再用plugin的时候都是new一下,由此可见,每个plugin都是一个构造函数,我们在实现plugin的时候,需要借助我们之前说的 tapable 模块,需要先安装下:
npm install tapable
安装完成后,需要进一步完善我们的Compoler.js如下:
Compiler.js 文件
const { resolve, relative, dirname, extname, join } = require('path');
const fs = require('fs');
const { SyncHook } = require('tapable');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;
const ejs = require('ejs');
class Compiler {
constructor(config) {
// 配置文件
this.config = config;
// 入口文件路径
this.extryId;
// 所有模块依赖
this.modules = {};
// 入口路径
this.entry = config.entry;
// 当前工作路径
this.root = process.cwd();
this.hooks = {
entryOption: new SyncHook(), // 入口时调用
compile: new SyncHook(), // 编译时调用
afterCompile: new SyncHook(), // 编译之后
afterPlugins: new SyncHook(), // 调用插件之后
run: new SyncHook(), // 启动时
emit: new SyncHook(), // 发射文件时
done: new SyncHook() // 结束之后
}
// 取出所有的 plugin
let plugins = this.config.plugins;
if (Array.isArray(plugins)) {
// 遍历 逐个执行
plugins.forEach(plugin => {
plugin.apply(this);
})
}
this.hooks.afterPlugins.call();
};
// 读取路径下的模块内容
getSoure(modulePath) {
let rules = this.config.module.rules;
let content = fs.readFileSync(modulePath, 'utf8');
// 循环遍历每一个规则
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
let { test, use } = rule;
let len = use.length - 1;
// 判断模块是不是需要 loader 来转换
if (test.test(modulePath)) {
function normalLoader() {
let loader = require(use[len--]);
// 递归调用 laoder 实现转化功能
content = loader(content)
if (len >= 0) {
normalLoader();
}
}
normalLoader();
}
}
return content;
};
/**
* 源码解析
* @param {*} source:源码
* @param {*} parentPath:路径
*/
parseCode(source, parentPath) {
// 把源码解析成ast
let ast = babylon.parse(source);
let dependencies = []; // 依赖数组
// 遍历ast
traverse(ast, {
CallExpression(p) {
let node = p.node;
if (node.callee.name == 'require') {
// 重命名
node.callee.name = '__webpack_require__';
// 拿到模块的引用名字
let moduleName = node.arguments[0].value;
// 拿到模块的依赖
moduleName = moduleName + (extname(moduleName) ? '' : '.js');
moduleName = './' + join(parentPath, moduleName);
// 把所有依赖放在数组里
dependencies.push(moduleName);
// 替换依赖内容
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
// 把转换后的生成源码
let sourceCode = generator(ast).code;
return { sourceCode, dependencies }
};
/**
* 创建模块的依赖关系
* @param {*} modulePath:入口的绝对路径
* @param {*} isEntry:是否是入口文件
* @memberof Compiler
*/
buildModule(modulePath, isEntry) {
// 拿到模块内容
const source = this.getSoure(modulePath);
// 拿到模块的相对路径 = modulePath - this.root
// relative(this.root, modulePath):获得相对路径
const moduleName = './' + relative(this.root, modulePath);
// 保存入口的名字
if (isEntry) {
this.entryId = moduleName;
}
// 把 source 源码进行改造,并返回一个依赖列表
// dirname(moduleName):获得 moduleName 的父路径
const { sourceCode, dependencies } = this.parseCode(source, dirname(moduleName));
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode;
// 遍历所有依赖项
dependencies.forEach(dep => {
// 递归加载附属模块
this.buildModule(join(this.root, dep), false);
})
};
// 发射一个打包后的文件
emitFile() {
// 输出路径
let main = join(this.config.output.path, this.config.output.filename);
// 模板路径
let templateStr = this.getSoure(join(__dirname, 'main.ejs'));
// 根据模板和代码输出编译后的代码块
let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules });
this.assets = {};
// 资源中路径对应的代码
this.assets[main] = code;
// 文件写入到哪
fs.writeFileSync(main, this.assets[main]);
};
// 执行打包
run() {
this.hooks.run.call();
this.hooks.compile.call();
// 1. 创建模块的依赖关系
this.buildModule(resolve(this.root, this.entry), true);
this.hooks.afterCompile.call();
// 2. 发射一个打包后的文件
this.emitFile();
this.hooks.emit.call()
this.hooks.done.call()
}
}
module.exports = Compiler;
这时还需要在我们的主文件bin/mypack.js文件里,调用一下入口的钩子,如下
mypack.js 文件
#! /usr/bin/env node
const { resolve } = require('path');
const Compiler = require('../lib/Compiler');
// 找到当前路径下的 webpack.config.js 文件
const config = require(resolve('webpack.config.js'));
// 通过配置文件处理打包逻辑
const compiler = new Compiler(config);
// 调用入口钩子
compiler.hooks.entryOption.call();
// 通过 run 方法启动打包
compiler.run();
现在我的打包文件准备好了,需要在项目文件里创建两个模拟的插件,创建一个plugins文件夹,并创建Test1.js和Test2.js两个插件文件,内如如下:
Test1.js 文件
class Test1 {
apply(compile) {
compile.hooks.emit.tap('emit', function() {
console.log('我是 Test1');
})
}
}
module.exports = Test1;
Test2.js 文件
class Test2 {
apply(compile) {
compile.hooks.emit.tap('emit', function() {
console.log('我是 Test2');
})
}
}
module.exports = Test2;
在webpack.config.js文件里引入两个插件,如下:
webpack.config.js 文件
const path = require('path');
const Test1 = require('./src/plugins/Test1')
const Test2 = require('./src/plugins/Test2')
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, 'src/loaders', 'style-loader.js'),
path.resolve(__dirname, 'src/loaders', 'less-loader.js')
]
}]
},
plugins: [
new Test1(),
new Test2()
]
}
这时运行打包命令npx mypack会输出如下结果:
到这里我们手写webpack的源码主要实现了js模块的打包,loader与plugin的实现原理,都是大致模拟真实webpack的实现原理,希望对你理解webpack有所帮助,nice ~~