为了了解webpack是怎么运行的,下面带领大家实现一个自己的webpack
初始化工程
使用yarn init -y 或者 npm init -y 快速初始化工程
安装的相关依赖包如下:
{ "name": "cwtpack", "version": "1.0.0", "main": "index.js", "license": "MIT", "bin": { "cwt-pack": "./bin/cwt-pack.js" }, "dependencies": { "@babel/generator": "^7.8.8", "@babel/traverse": "^7.8.6", "@babel/types": "^7.8.7", "babylon": "^6.18.0", "ejs": "^3.0.1", "tapable": "^1.1.3" } }
目录结构
package.json中的bin
在bin中配置执行指令及执行哪一个文件,如我的配置指令名为
cwt-pack
运行bin文件夹下的cwt-pack.js
cwt-pack.js
#! /usr/bin/env node // 1.需要找到当前执行名的路径 拿到webpack.config.js let path=require('path'); //获取配置文件名参数,没有则默认使用名为webpack.config.js文件 let configFileName=process.argv[2] || 'webpack.config.js'; //config 配置文件 let config; try { config=require(path.resolve(configFileName)); } catch (error) { console.log('配置文件不存在,请先创建配置文件'); //结束进程 process.exit(); } let Compiler = require('../lib/Compiler.js'); let compiler = new Compiler(config); compiler.hooks.entryOption.call(); //标识运行编译 compiler.run();
这个文件是我们打包器的入口。
#! /usr/bin/env node
用于指明该脚本文件要使用node来执行,它必须放在第一行,否则不生效;其中#!
可以让系统动态的去查找node,以解决不同机器不同用户设置不一致问题。config默认webpack.config.js,如需自定义配置文件名 可在执行指令后添加参数,如:cwt-pack aa.js,如果在打包的项目根目录中找不到这个配置文件,会在终端中打印
配置文件不存在,请先创建配置文件
并结束进程。require 位于lib文件夹下的 Compiler.js,调用run方法编译文件。
Compiler.js
这就是我们打包器的核心代码了,分为六部分讲解:
constructor
构造方法
初始化变量,遍历配置文件中的plugins插件并调用插件apply方法,插件的样子:
class MyPlugin{ //传入compiler对象,每个插件都要有这个applay方法 apply(compiler){ console.log('start'); //发布一个事件 compiler.hooks.afterPulgins.tap('afterPulgins',function(){ console.log('afterPulgins') }); } }
插件需要在不同的生命周中执行各自的方法,这时我们就需要使用tapable帮助我们实现事件流传递,tapable它是一个基于发布订阅模式实现的事件流机制。在hooks中设置多个钩子,在插件中发布,然后在生命周期的不同位置订阅。例如
this.hooks.afterPulgins.call();
这就在消费afterPulgins钩子中的方法。
getSource(modulePath)
获取源码方法
通过modulePath读取文件内容,遍历配置文件中的rules取得loader后处理源码并返回。
parse(source,parentPath)
转化源码方法
参数source是getSource方法返回的源码,parentPath父路径用于构建当前模块名。
通过babylon.parse(source)将源码转化为ast语法树,
traverse遍历ast,使用CallExpression方法操作节点,将require方法名替换为
__webpack_require__
generator(ast)取得转化后的代码,
返回转化后的代码和依赖关系
buildModule(modulePath,isEntry)
建立模块名与模块代码关系方法
参数modulePath模块路径,isEntry是否为入口
通过parse返回的依赖关系递归,并将模块名与转化后的代码以键值对的形式存入this.modules变量中
emitFile()
发射文件方法
获取ejs模板,使用ejs.render方法取得渲染后的代码,这个就是最终打包后的代码了
使用fs写文件到config.output.path
run
入口方法,启动Compiler。
let path = require('path'); let fs = require('fs'); //Babylon 把源码转为AST let babylon = require('babylon'); //@babel/traverse let traverse = require('@babel/traverse').default; //@babel/types let types = require('@babel/types'); //@babel/generator let generator = require('@babel/generator').default; let ejs = require('ejs'); let {SyncHook}=require('tapable'); class Compiler{ constructor(config){ //entry output this.config = config; //需要保存入口文件路径 this.entryId; //保存需要的所有模块依赖 this.modules={}; this.entry = config.entry;//入口路径 this.root = process.cwd();//工作路径 this.hooks={//编译生命周期的钩子 entryOption:new SyncHook(), compile:new SyncHook(), afterCompile:new SyncHook(), afterPulgins:new SyncHook(), run:new SyncHook(), emit:new SyncHook(), done:new SyncHook() } //获取插件列表 let plugins= this.config.plugins; //判断有无插件 if(Array.isArray(plugins)){ plugins.forEach(plugin=>{ //执行插件中的apply方法,每个插件都会有这个apply方法,如果你自定义过webpack插件应该能明白 plugin.apply(this); }); this.hooks.afterPulgins.call(); } } //获取源码 getSource(modulePath){ let rules= this.config.module.rules; let content = fs.readFileSync(modulePath,'utf-8'); //拿到每个规则 来处理 for(let i = 0;i<rules.length;i++){ let rule= rules[i]; let {test,use}=rule; let len = use.length-1; if(test.test(modulePath)){//如果能匹配上 那就说明模块需要被loader转化 function normalLoader(){ //获取loader 函数 let loader = require(use[len--]); // len -- ; //递归调用loader 实现转化功能 content = loader (content); if(len >=0){ normalLoader(); } } normalLoader(); } } return content; } //解析文件 转换文件内容 parse(source,parentPath){ //使用AST 解析语法树 将源码解析 let ast = babylon.parse(source); let dependencies = [];//依赖数组 //遍历ast树 traverse(ast,{ //进入ast节点 CallExpression(p){// p 是源码中的方法 如a() let node = p.node; if(node.callee.name === 'require'){ //将require改成__webpack_require__ node.callee.name = '__webpack_require__'; let moduleName = node.arguments[0].value;//取到模块的引用名字 如require('./a) value=./a moduleName = moduleName+(path.extname(moduleName)?'':'.js');// ./a.js moduleName = './'+path.join(parentPath,moduleName) // path.join(parentPath,moduleName) 返回 src/a.js 要加上./ //将依赖模块名存入dependencies dependencies.push(moduleName); node.arguments = [types.stringLiteral(moduleName)]; } } }); //取得转化后代码 let sourceCode = generator(ast).code; return { sourceCode, dependencies } } //创建模块依赖关系 buildModule(modulePath,isEntry){ //拿到模块内容 let source= this.getSource(modulePath); //模块id moduleName = modulePath - this.root let moduleName ='./' + path.relative(this.root,modulePath);//path.relative 返回的是 src/index.js 所以要加上./ if(isEntry){ this.entryId = moduleName; } //转换文件内容 返回一个依赖列表 let {sourceCode,dependencies} = this.parse(source,path.dirname(moduleName));//path.dirname(moduleName) 返回 ./src //把相对路径和模块名对应起来 this.modules[moduleName] = sourceCode ; dependencies.forEach(dep=>{ // 递归 附模块加载 this.buildModule(path.join(this.root,dep),false); }) } //发射文件 emitFile(){ //用数据渲染模板 main.ejs //main 文件输出路径 let main = path.join(this.config.output.path,this.config.output.filename); //ejs 模板路径 let templateString = this.getSource(path.join(__dirname,'main.ejs')); //经过ejs渲染后的代码 let code = ejs.render(templateString,{entryId:this.entryId,modules:this.modules}); this.assets={}; //资源中 路径对应的代码 this.assets[main] = code; //判断输出文件夹是否存在 if(!fs.existsSync(this.config.output.path)){ //创建输出的文件夹 fs.mkdirSync(this.config.output.path); } fs.writeFileSync(main,this.assets[main]); } run(){ this.hooks.run.call(); //执行 创建模块依赖关系 this.hooks.compile.call(); this.buildModule(path.resolve(this.root,this.entry),true); this.hooks.afterCompile.call(); //发射一个文件 打包后的文件 this.emitFile(); this.hooks.emit.call(); this.hooks.done.call(); } } module.exports = Compiler;
main.ejs
这个main.ejs是打包后的js模板,这里就直接搬webpack的,关于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 key in modules){%> "<%-key%>": (function(module, exports, __webpack_require__) { eval(`<%-modules[key]%>`); }), <%}%> });
乍一看一脸懵逼,其实webpack打包后的就是一个自运行函数,简单模拟一下:
(function(module){ //缓存已递归的依赖 var installedModules = {}; function __webpack_require__(id){ //如果有在缓存中则直接返回 if(installedModules[moduleId]) { return installedModules[moduleId].exports } //将当前递归的依赖id存入installedModules var module = installedModules[moduleId] = { exports {} }; //通过modules[moduleId]调用方法实现层层递归 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); return module.exports } //第一次调用传入入口文件id return __webpack_require__("./src/index.js") })({ "./src/index.js":(function(module, exports,__webpack_require__){ eval(`let a = __webpack_require__("./src/a.js"); console.log(a)`); }), "./src/a.js":(function(module, exports,__webpack_require__){ eval(`module.exports= 'a';`); }), });
index.js:
import a from './a'; console.log(a)
a.js:
module.exports = 'a';
示例代码中我已将
__webpack_require__
方法最简化。自运行函数参数传入的是依赖文件列表:
键:依赖的id(其实就是文件的路径),
值:一个函数通过eval(
经过Compiler编译后的代码
)执行代码并递归依赖这部分挺难懂的,需要认真的研究一下代码,理清逻辑,看懂之后就能明白webpack打包出来的是什么东西了。
测试使用
通过 npm link 或 npm install . -g 打包到全局中去
进入nodejs全局仓库查看
然后起一个项目,写一个测试的配置文件
webpack.config.js
let path=require('path');
class P{
apply(compiler){
console.log('start');
compiler.hooks.emit.tap('emit',function(){
console.log('emit')
});
}
}
class P1{
apply(compiler){
console.log('start');
compiler.hooks.afterPulgins.tap('afterPulgins',function(){
console.log('afterPulgins')
});
}
}
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,'loader','style-loader'),
path.resolve(__dirname,'loader','less-loader')
]
}
]
},
plugins:[
new P(),
new P1()
]
}
在项目根目录创建loader文件夹里面创建两个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;
less-loader.js
let 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;
在src下创建index.js a.js index.less
index.js
let str =require('./a');
require('./index.less');
console.log(str);
a.js
module.exports='a';
index.less
body{
background: red;
}
测试项目的最终的文件目录结构
在终端进入这个项目的文件夹 输入cwt-pack 或者 cwt-pack 配置文件.js 执行成功后有以下输出
然后可以在项目的根目录中看到dist文件夹,里面有个bundle.js,到这一步自定的打包器就测试完成了