适合人群
本文适合2~4年的前端开发人员的进阶,且对vue或者react搭配webpack有一定的经验。如果webpack的基本使用都未了解,建议实践后再看本文。
本文是原创,均为本人手写。部分思维, 借鉴了“宫小白”的webpack文章。文章结尾标注了“感谢”。
再谈谈博客源码的进度
此前有人关心博客源码的进度问题。笔者是打算利用额外的时间,把脑袋中的源码都撸一份。即源码篇,具体可参考如下:
| 序号 | 博客主题 | 相关链接 | |-----|------|------------|- | 1 | 手写vue_mini源码解析 | juejin.cn/post/684790… | | 2 | 手写react_mini源码解析 | juejin.cn/post/685457… | | 3 | 手写webpack_mini源码解析(即本文) | juejin.cn/post/685457… | | 4 | 手写jquery_mini源码解析| juejin.cn/post/685457… | | 5 | 手写vuex_mini源码解析 | juejin.cn/post/685705… | | 6 | 手写vue_router源码解析 | 预计8月 | | 7 | 手写diff算法源码解析 | 预计8月 | | 8 | 手写promis源码解析 | 预计8月 | | 9 | 手写原生js源码解析(手动实现常见api) | 预计8月 | | 10 | 手写react_redux,fiberd源码解析等 | 待定,本计划先出该文,整理有些难度 | | 11 | 手写koa2_mini | 预计9月,前端优先 | 期间除了除了源码篇,可能会写一两篇优化篇,基础篇等。有兴趣的欢迎持续关注。
讲讲废话
当前主流的前端,无论angular,vue还是react,都离不开的构建工具的编译与协助。比较有名气是有Webpack、Gulp 和 Grunt。
Grunt笔者没有实践经验,且相对另外两者比较偏冷门些,本文不做比较。
来个简单的高频面试题:
gulp与webpack有什么区别?
讲述一下个人理解(仅供参考):
gulp官方原话:gulp 将开发流程中让人痛苦或耗时的任务自动化,从而减少你所浪费的时间、创造更大价值。他更像是一个流水线上的过滤器,定义多task(即使多个过滤条件),讲我们的文件进行转译。比如我们将sass统一转译成css,或者文件的合并压缩等,专注于task的执行。笔者开发一个门户网站的时候,就将起引入,他精简的 API 集,只需一个文件代码,即帮我们完成了一个项目针对某些文件的改造,且使用于多页面的构建。
但单页面的构建,不得不用webpack。webpack有人称模块打包机,更侧重于模块打包。本质是为了打包js设计的,但是后期因为要针对整个前端项目,即支持loader对其他文件进行编译。
写源码之前的必备知识点
手写loader
写webpack的源码,你首先得懂得loader跟plugins是如何去加载的。本文写一个简单的栗子,让大家理解loader是个什么东西。
那我们就先看看大神写的loader是个什么东西。我们以less-loader为栗子:
npm install less-loader安装后,我们直接进入node_moudels/less-loader/package.json
查看main变量 我们可以看到: "main": "dist/cjs.js",
那么我们再看dist/cjs.js又指向index。他的源码在index.js文件中,我们来看index.js的代码:
图解中,我们可以分析到,他的首个参数,即是接受到文本内容。loaderContext即是将less转换为css的过程。但是整个loader,其实可以很清晰到看到。接受到source,然后通过转换,再暴露出去。按着这个思路,我们手写一个loader应该不难。
我们定义一个简单的需求:
-
正式环境中,去除项目中所有的console.log()
-
最后再打印出一个,关注博主博客
const join = require('path').join; const fs = require('fs');
function wzJsLoader(source) {
/*过滤输出*/ const mode = process.env.NODE_ENV === 'production' if( mode ){//正式环境 source = source.replace(/console.log\(.*?\);/ig, value => "" ); } /* 打上个人标记 */ const tip = "关注博主博客:https://juejin.cn/user/4195392104696519 "; source = source + `console.log("${tip}");` return source;
};
module.exports = wzJsLoader;
然后再我们的配置文件中,配置上上述的loader。
{
test: /\.js$/, //js文件加载器
//loader: 'happypack/loader?id=happyBabel',
exclude: /node_modules/,
use: [
{
loader: 'babel-loader?cacheDirectory=ture',
options: {
presets: ['@babel/preset-env']
}
},
{
loader: require.resolve("./myWebPackConfig/wz-js-loader"),
options: { name: __dirname },
},
]
},
那么,我们就完成我们的第一个手写loader。
其实plugins也一样的原理,这里不做重复讲解,本文demo中会提到plugins。
tapable
tapable一些前端朋友(没了解过的webpack)可能很陌生,包括笔者之前也是,直到了解到webpack时才去理解这个概念。我们首先要了解他是个什么东西,再去写webpack可能更好了解一下。这里分为简版说明,跟代码解释。如知识想了解webpack的执行机制,可能看简版说明即可。
简版说明
同步:
名称 | 解释 |
---|---|
SyncHook | 同步执行,无需返回值 |
SyncBailHook | 同步执行,无需返回值,返回undefined终止 |
SyncWaterfallHook | 同步执行,上一个处理函数的返回值是下一个的输入,返回undefined终止 |
SyncLoopHook | 同步执行, 订阅的处理函数有一个的返回值不是undefined就一直循环它 |
异步:
名称 | 解释 |
---|---|
AsyncSeriesHook | 异步执行,无需返回值 |
AsyncParallelHook | |
AsyncSeriesBailHook | 异步执行,无需返回值,返回undefined终止 |
AsyncSeriesWaterfallHook | 异步执行,上一个处理函数的返回值是下一个的输入,返回undefined终止 |
看了tapable具体的几个方法的作用,大概了解,那么为什么webpack要扯上他呢? 因为webpack的构建流程,很多的跟着tapable的执行顺序方案有关。
SyncHook就最好理解,同步执行,我们不同的plugins之间,只需要同步执行下去即可。这时候我们可以利用SyncHook按顺序同步执行SyncHook执行我们的plugins即可。那么另外的场景是什么?
HappyPack知道是什么麽?例如开启多个线程,同步打包。是不是符合AsyncSeriesHook的场景。
再举个最简单的栗子: 我们生成文件时候,是不是有一个hash值呢?假设这个hash值,有多个插件用到,我们是不是应该在第一个方法生成一个hash,然后后续用到的方法,读取到这个值。那么,这个场景,SyncWaterfallHook是否派上用场?此时的hash一直传递下去,就完成了不同plugins之间的hash值传递。
还是不明白?案例实现会根据这个栗子实现。 如果简版本的介绍,还是一脸懵逼,建议看一下下列源码。
代码解释
代码的解释,是为了让你更了解tapable。如果你只想知道webpack,可以直接忽略。
tapable这里,他本身是暴露了8个方法(上述)。但是我们简版没有那么复杂,我们讲解一下下述用到SyncHook跟AsyncSeriesWaterfallHook。
SyncHook
SyncHook我们大致理解,他有一个tap将任务添加的内部的执行队列中。此时,如果调用call方法,将执行同步执行所有tap过的方法。具体可以通过下述代码理解:
class SyncHook {
constructor(args){//args => ['name']
this.tasks = [];
}
call(...args){
this.tasks.forEach((task) => task(...args))
}
tap(name,task){
this.tasks.push(task);
}
}
let hook = new SyncHook(['name']);
hook.tap('plugins_0',function(name){
console.log('plugins_0',name)
})
hook.tap('plugins_1',function(name){
console.log('plugins_1',name)
})
hook.call('hello word');
AsyncSeriesWaterfallHook
AsyncSeriesWaterfallHook我们大致理解,他有一个tap将任务添加的内部的执行队列中。此时,如果调用call方法,将执行同步执行所有tap过的方法,且需上一个返回值,给下一个函数队列。具体可以通过下述代码理解:
class SyncWaterfallHook {
//一般是可以接收一个数组参数的,但是下面没有用到
constructor(args) {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
let [first, ...others] = this.tasks
let ret = first.cb(...arg)
others.reduce((pre, next) => {
return next.cb(pre)
}, ret)
}
}
AST语法树
该章节,建议移步文章:www.jianshu.com/p/019d449a9…
手写webpack过程
1)基本架子的搭建
手动新建
新建一个package.json,入口文件我们制定了bin/index.js
{
"name": "zwzpack",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "node ./bin/index.js"
},
"devDependencies": {},
"dependencies": {
}
}
再新建bin/index.js表示我们的入口文件,参考我们webpack常规写法,引入自写的webpack的lib包,再引入我们的配置文件webpack.config.js。启动他
const path = require('path')
const config = require(path.resolve('webpack.config.js'))
const WebpackCompiler = require('../lib/WebpackCompiler.js')
const webpackCompiler = new WebpackCompiler(config)
webpackCompiler.run();
根目录定义webpack.config.js,我们首先定义一个webpack的基本配置文件。
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.join(__dirname, './dist')
},
module: {
rules: []
},
plugins: [
]
}
public/index.html,作为我们测试webpack是否打包成功测试Html,引入打包后的js:
<html>
<head>
<meta charset="UTF-8">
<title>个人webpack</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
</head>
<script type="text/javascript" src="./main.js" ></script>
<body>
<div class="my_page" >
<h1 class="">欢迎小伙子小姑凉!</h1>
<div class="">欢迎关注掘金:<a target="_blank" href="https://juejin.cn/user/4195392104696519">点击</a></div>
<div class="">手写mini_webpack教程:<a target="_blank" href="https://juejin.im/editor/drafts/5f1793716fb9a07e8b215a62">点击</a></div>
</div>
</body>
<style>
</style>
</html>
WebpackCompiler是我们的核心编译类,下方重点讲解。
class WebpackCompiler {
constructor(config) {
this.config = config;
}
run() {
//编译开始
}
}
module.exports = WebpackCompiler;
就这样,完成我们基本的架子,此部分暂时未涉及wepback的编译过程。
快速拷贝
2) 新建编译模板
webpack的最终目的,是为了生成js文件。(暂不考虑其他文件因素) 我们要自己手写一个webpack生成的js文件,那么首先,我们需要了解官方webpack生成的js文件到底是个啥?
我们新建一个官方webpack项目(百度一下很简单)
新建test.js,跟testChildren.js进行编译:
test.js:
const obj = require("./testChildren.js");
module.exports = { name: "weizhan", obj }
testChildren.js
module.exports = { age: 18}
我们来观察编译结果:
图1:
图2:
截图大家还看不懂生成了什么的话,我再简化一下官方最后生成的文件,其中有变化的部分为:
((function(modules) {
...
//固定代码
return __webpack_require__(__webpack_require__.s = "<%-文件路径 %>");
})
"./src/js/test.js": (function (module, exports,__webpack_require__) {
eval(` const obj = __webpack_require__("./testChildren.js"); test1.js 剩余文件代码`);
}),
"./src/js/testChildren.js": (function (module, exports,__webpack_require__) {
eval(`testChildren.js 文件代码`);
}),
我们可以观察到,生成的js文件中,将生成原来的js文件,套入一个模板中,而我们自己js代码给嵌入到结尾部分。 观察我简化后的文件,我们可以看到,webpack官方生成的js文件,他的生成规律:
- 1.先快速拷贝一个编译模板,中间嵌入我们自己写的代码
- 2.需要暴露我们的文件路径
- 3.会将我们的js文件中的require转化为__webpack_require__,然后将所有require的js(包含自己)都用嵌入到function中的eval中。
了解完官方的生成规矩后,我们来模拟这个过程:
- 根据规则1,首先有一个固定的编译模板,我们可以将模板内容先写在一个js中,但是为方便渲染嵌入,我们借用ejs的快速渲染模板
- 根据规则2,我们需要知道我们的文件路径,ok,我们定义一个变量entryPath来标识文件路径
- 根据规则3,需要把每个模块的代码到嵌入文件中,同样的我们要以路径为Key。那我们用modules变量来作为数组。(且原文件还有require转化为__webpack_require__)
根据规则,我们新建lib/main.ejs文件:
(function(modules) {
...
//固定代码
return __webpack_require__(__webpack_require__.s = "<%- entryPath %>");
})
({
<% for(let key in modules){ %>
"<%- key %>": (function (module, exports,__webpack_require__) {
eval(`<%-modules[key] %>`);
}),
<% } %>
});
3) 获取模板参数
生成JS的模板有了,那么我们剩余的问题就是,怎么拿到modules跟entryPath的问题。
entryPath好处理,用过webpack的都知道,入口文件即是webpack.config.js的entry变量。 那么modules呢?首先我们需要读取原来的文件内容。上文提到,两个重点:
- 1.将require替换成__webpack_require__
- 2.再所有的页面路径,跟页面内容封装一个数组。
需求即是如此,我首先想到的时候,写一个正则表达式,将require替换成__webpack_require__,且用node的fs,将文件内容读取。但是后续发现,兼容性实在是太烂了,空白符,注释等,都会成为头疼的地方。
后续发现大神的解析方法非常赞,用的是babylon 转换 AST转换的方法,我们来看看代码:
// babylon主要把源码转成ast。Babylon 是 Babel 中使用的 JavaScript 解析器。
// @babel/traverse 对ast解析遍历语法树 负责替换,删除和添加节点
// @babel/types 用于AST节点的Lodash-esque实用程序库
// @babel/generator 结果生成
const babylon = require('babylon')
const traverse = require('@babel/traverse').default;
const type = require('@babel/types');
const generator = require('@babel/generator').default
// 根据路径解析源码
parse(source, parentPath) {
let ast = babylon.parse(source)//
// 用于存取依赖
let dependencies = []
traverse(ast, {//对ast解析遍历语法树 负责替换,删除和添加节点
CallExpression(p) {
let node = p.node
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__';//将require替换成__webpack_require__
const moduledName = './' + path.join(parentPath, node.arguments[0].value )
dependencies.push(moduledName);//记录包含的requeir的名称,后边需要遍历替换成源码
node.arguments = [type.stringLiteral(moduledName)] // 源码替换
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies };
}
AST的内容如果不清楚,上述已经给了链接,先继续步骤继续下去,后续可以根据链接再补充一下自己。 js转换的方法已经抒写完毕,我们来写转换的入口。
// 编译生成完成的main文件,完成递归
buildMoudle(modulePath, isEntry) {
const source = this.getSourceByPath(modulePath);//根据路径拿到源码
const moduleName = './' + path.relative(this.root, modulePath);//转换一下路径名称
const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))//根据路径拿到源码,以及源码中已经require的文件名称数组
this.modules[moduleName] = sourceCode;// 每个模块的代码都通过路径为Key,存入到modules对象中
dependencies.forEach(item => { // 递归需要转换的文件名称
this.buildMoudle(path.resolve(this.root, item));////再对应的文件名称,替换成对应的源码
})
}
getSourceByPath(modulePath){
let content = fs.readFileSync(modulePath, 'utf8')
return content
}
此时,如果程序执行完毕,我们就可以拿到我们的modules参数。
4) 将模板生成js文件输出
这个章节很好理解,根据文件路径,拿到模板内容,模板变量的值,输出到指定位置即可。
//输出文件
outputFile() {
let templateStr = this.getSourceByPath(path.join(__dirname, 'main.ejs')); // 拿到步骤1写好的模板
let code = ejs.render(templateStr, {
entryPath: this.entryPath,
modules: this.modules,
})// 填充模板数据
let outPath = path.join(this.config.output.path, this.config.output.filename)// 拿到输出地址
fs.writeFileSync(outPath, code )// 写入
}
我们改一下我们的编译类入口,在run方法执行他们:
class WebpackCompiler {
constructor(config) {
this.config = config;
this.modules = {}
this.root = process.cwd() //当前项目地址
this.entryPath = './' + path.relative(this.root, this.config.entry);
}
run() {
this.buildMoudle( this.entryPath )
this.outputFile();
}
}
此时,执行我们我们的基本架子,已经可以输出webpack的编译后的js文件。
5)嵌入loader
普通的js文件,已经抒写完毕。那我们来看看怎么嵌入我们的loader呢?看完了"写源码之前的必备知识点",我们应该明白,loader即是将我们的对应的文件后缀,通过loader的转移成我们的js文件支持的语法。
我们做一下前期工作。
-
新建一个新的loader文件:
const less = require('less') function loader(source) { let css = '' less.render(source, function(err, output) { css = output.css })
css = css.replace(/\n/g, '\\n') let style = ` let style = document.createElement('style') style.innerHTML = \n${JSON.stringify(css)} document.head.appendChild(style) ` return style
} module.exports = loader;
-
具体less的转换规则比较复杂我就不折腾了。这里借用一下官方的less,赋值到js中。修改一下入口:webpack.config.js的module:
module: { rules: [{ test: /\.less$/, use: [path.join(__dirname, './lib/loader/less-loader.js')] }] },
-
再在我们的src文件中新建test.less: body{ text-align: center; .my_page{ margin-top: 50px; line-height: 50px; } }
-
在测试文件(src/index.js)引入他:
require("./test.less"); const index2Obj = require("./index2.js"); alert("index文件告诉你:小伙子很帅" ); alert("index2文件告诉你:" + index2Obj.value ); module.exports = {}
做完前期工作,我们来实现编译过程:
我们上述获取源文件代码用了:
getSourceByPath(modulePath){
let content = fs.readFileSync(modulePath, 'utf8')
return content
}
loader的实现,就是在拿到源文件后,通过rules的规则匹配后缀,多做一层转换:
getSourceByPath(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8')
// 事先拿module中的匹配规则与路径进行匹配
const rules = this.config.module.rules
for (let i = 0; i < rules.length; i++) {
let { test, use } = rules[i]
let len = use.length
if (test.test(modulePath)) {
function changeLoader() {
// 先拿最后一个
let loader = require(use[--len])//倒叙执行
content = loader(content)
if (len > 0) {
changeLoader()
}
}
changeLoader()
}
}
return content
}
用了闭包,是因为一个后缀可能对应多个loader,所以这里写了循环执行。 再执行一次npm run start,此时你会发现,js中已经包含了less文件的内容。此时我们完成了loader的嵌入。
6)嵌入webpack的生命周期
理论上我们应该讲plugins的手写,但是plugins有一个问题,plugins可以设置在不同的编译阶段。例如在编译前,做什么?编译中,需做什么?这涉及到webpack的生命周期。我们来处理一下生命周期。
我们上述描述了tapable,SyncHook我们大致理解,他有一个tap将任务添加的内部的执行队列中,然后最后通过执行call方法,一次执行他们。
我们的plugins的执行也是如何,我们可以将我们的每个plugin通过tap任务放置在SyncHook中,等时机到了,调用call方法即可。
我们给我们的webpack定义五个生命周期,并在run方法适当的时机嵌入他们:
class WebpackCompiler {
constructor(config) {
this.config = config;
this.modules = {}
this.root = process.cwd() //当前项目地址
this.entryPath = './' + path.relative(this.root, this.config.entry);
this.hooks = {
entryInit: new tapable.SyncHook(),
beforeCompile: new tapable.SyncHook(),
afterCompile: new tapable.SyncHook(),
afterPlugins: new tapable.SyncHook(),
afteremit: new tapable.SyncWaterfallHook(['hash']),
}
}
run() {
this.hooks.entryInit.call(); //启动项目
this.hooks.beforeCompile.call(); //编译前运行
this.buildMoudle( this.entryPath )
this.hooks.afterCompile.call( ); //编译后运行
this.outputFile();
this.hooks.afterPlugins.call( );//执行完plugins后运行
this.hooks.afteremit.call( );//结束后运行
}
}
该webpack生命周期定义结束,下边我们手写plugins嵌入到生命周期中。
7)嵌入plugins
定义一个需求: 1)启动时,添加打印:前端小伙子,编译开始咯。 2)编译前:清空原来的dist文件夹内容 3)输出文件后:将我们的生成文件main.js,改名main.${hash}.js,并在index.html正确引入该文件。
- 我们手写一个InitPlugin,该plugin是在启动时执行,即tap到entryInit周期中:
class InitPlugin {
run(compiler) {
// 将的在执行期放到刚开始解析入口前
compiler.hooks.entryInit.tap('Init', function(res) {
console.log(前端小伙子,编译开始咯。
);
})
}
}
- 清空dist,在编译前:
class CleanDistPlugins { run(compiler) { // 将自身方法订阅到hook以备使用 //假设它的运行期在编译完成之后 compiler.hooks.beforeCompile.tap('CleanDistPlugins', function(res) { delFileFolderByName('./dist/'); }) } }
- 编译后:
重命名文件:
class JsCopyPlugins {
run(compiler) {
compiler.hooks.afterPlugins.tap('JsCopyPlugins', function(res) {
const ranNum = parseInt( Math.random() * 100000000 );
fs.copyFile('./dist/main.js',`./dist/main.${ranNum}.js`,function(err){
if(err) console.log('获取文件失败');
delFileByName('./dist/main.js');
})
console.log("重新生成js成功" );
return ranNum;
})
}
}
修改html的js引入:
class HtmlReloadPlugins {
run(compiler) {
compiler.hooks.afterPlugins.tap('HtmlReloadPlugins', function(res) {
let content = fs.readFileSync('./public/index.html', 'utf8')
content = content.replace('main.js', `main.${res}.js`);
fs.writeFileSync( './dist/index.html', content)
})
}
}
由于“重命名文件”“修改html的js引入”需要用到同一个hash值,上述SyncWaterfallHook可以传递值,SyncHook则无传递值,这就是我为什么将最后一步改为SyncWaterfallHook的原因
所有的pulgins都写好了,我们来写webpack如何执行pulgins的过程:
其实生命周期帮我们实现了SyncHook的start函数。我们只需要将plugins注册到编译类中接口。plugins中已经将方法 tap到SyncHook中,我们只需要执行plugins即可。方法很简单:
class WebpackCompiler {
constructor(config) {
this.config = config
//...省略
const plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(item => {
// 每个均是实例,调用实例上的一个方法即可,传入当前Compiler实例
item.run(this)
})
}
}
}
至此,我们已经完成了webpack_mini的手写。
简版webpack源码
给于核心类WebpackCompiler的完整代码,如还不清晰,建议导下项目查看,链接:github.com/zhuangweizh…
WebpackCompiler源码:
const path = require('path')
const fs = require('fs')
const { assert } = require('console')
// babylon 将源码转成ast Babylon 是 Babel 中使用的 JavaScript 解析器。
// @babel/traverse 对ast解析遍历语法树
// @babel/types 用于AST节点的Lodash-esque实用程序库
// @babel/generator 结果生成
const babylon = require('babylon')
const traverse = require('@babel/traverse').default;
const type = require('@babel/types');
const generator = require('@babel/generator').default
const ejs = require('ejs')
const tapable = require('tapable')
class WebpackCompiler {
constructor(config) {
this.config = config
this.modules = {}
this.root = process.cwd() //当前项目地址
this.entryPath = './' + path.relative(this.root, this.config.entry);
this.hooks = {
entryInit: new tapable.SyncHook(),
beforeCompile: new tapable.SyncHook(),
afterCompile: new tapable.SyncHook(),
afterPlugins: new tapable.SyncHook(),
afteremit: new tapable.SyncWaterfallHook(['hash']),
}
const plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(item => {
// 每个均是实例,调用实例上的一个方法即可,传入当前Compiler实例
item.run(this)
})
}
}
// 获取源码
getSourceByPath(modulePath) {
// 事先拿module中的匹配规则与路径进行匹配
const rules = this.config.module.rules
let content = fs.readFileSync(modulePath, 'utf8')
for (let i = 0; i < rules.length; i++) {
let { test, use } = rules[i]
let len = use.length
// 匹配到了开始走loader,特点从后往前
if (test.test(modulePath)) {
function changeLoader() {
// 先拿最后一个
let loader = require(use[--len])
content = loader(content)
if (len > 0) {
changeLoader()
}
}
changeLoader()
}
}
return content
}
// 根据路径解析源码
parse(source, parentPath) {
let ast = babylon.parse(source)//
// 用于存取依赖
let dependencies = []
traverse(ast, {//对ast解析遍历语法树 负责替换,删除和添加节点
CallExpression(p) {
let node = p.node
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__';//将require替换成__webpack_require__
const moduledName = './' + path.join(parentPath, node.arguments[0].value )
dependencies.push(moduledName);//记录包含的requeir的名称,后边需要遍历替换成源码
node.arguments = [type.stringLiteral(moduledName)] // 源码替换
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies };
}
// 构建模块
buildMoudle(modulePath) {
const source = this.getSourceByPath(modulePath);//根据路径拿到源码
const moduleName = './' + path.relative(this.root, modulePath);//转换一下路径名称
const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))//根据路径拿到源码,以及源码中已经require的文件名称数组
this.modules[moduleName] = sourceCode;// 每个模块的代码都通过路径为Key,存入到modules对象中
dependencies.forEach(item => { // 递归需要转换的文件名称
this.buildMoudle(path.resolve(this.root, item));////再对应的文件名称,替换成对应的源码
})
}
//输出文件
outputFile() {
let templateStr = this.getSourceByPath(path.join(__dirname, 'main.ejs')); // 拿到步骤1写好的模板
let code = ejs.render(templateStr, {
entryPath: this.entryPath,
modules: this.modules,
})// 填充模板数据
let outPath = path.join(this.config.output.path, this.config.output.filename)// 拿到输出地址
fs.writeFileSync(outPath, code )// 写入
}
run() {
this.hooks.entryInit.call(); //启动项目
this.hooks.beforeCompile.call(); //编译前运行
this.buildMoudle( this.entryPath )
this.hooks.afterCompile.call( ); //编译后运行
this.outputFile();
this.hooks.afterPlugins.call( );//执行完plugins后运行
this.hooks.afteremit.call( );//结束后运行
}
}
module.exports = WebpackCompiler;
文章结尾
感谢
笔者原本即将完成mini_webpack源码,后续发现有人的源码核心原理的实现,写的比我原本的要好一些,所以后期我做了改动。本文部分原理跟部分源码借鉴了“宫小白”,他的原文链接是:juejin.cn/post/684700…
此外, AST语法树不清晰建议文章:www.jianshu.com/p/019d449a9…
计划
本计划将webpack如何优化,写在本文中。发现文章已经差不多6K+字,太长小伙伴们也看不完,后续会单独一篇文章基于本文描述。
笔者也会继续写自己的mini框架源码,有兴趣继续关注:
| 序号 | 博客主题 | 相关链接 | |-----|------|------------|- | 1 | 手写vue_mini源码解析 | juejin.cn/post/684790… | | 2 | 手写react_mini源码解析 | juejin.cn/post/685457… | | 3 | 手写webpack_mini源码解析(即本文) | juejin.cn/post/685457… | | 4 | 手写jquery_mini源码解析 | juejin.cn/post/685457… | | 5 | 手写vuex_mini源码解析 | 预计下周 | | 6 | 手写vue_router源码解析 | 预计8月 | | 7 | 手写diff算法源码解析 | 预计8月 | | 8 | 手写promis源码解析 | 预计8月 | | 9 | 手写原生js源码解析(手动实现常见api) | 预计8月 | | 10 | 手写react_redux,fiberd源码解析等 | 待定,本计划先出该文,整理有些难度 | | 11 | 手写koa2_mini | 预计9月,前端优先 |