mini-webpack实现

最近在通过对webapck的学习,自己手写实现一个mini-webpack,特地在此进行记录。

准备工作

我们首先需要准备一份待打包的文件进行测试。
文件夹目录如下

loader
	| less-loader.js
	| style-loader.js
plugins
	| p1.js
src
	| base
		| b.js
	| a.js
	| index.js
	| index.less
webpack.config.js

入口文件index.js

require('./index.less')
let a = require('./a.js')  // module.exports = 'a'
let b = require('./base/b.js') // module.exports = 'ab'

console.log(a+b)  // abb

loader文件

less-loader.js:
const less = require('less')
function loader (content){
	let css = ''
	less.render(content, function(e,code){
		css = code.css
	})
	//这里由于我们在写css时会换行,需要对换行进行转义。
	css = css.replace(/\n/g,'\\n')
	return css
}
module.exports = loader
-----------------------------------
style-loader.js:
function loader(content){
	let style = `
		let style = document.createElement('style')
		style.innerHTML = ${JSON.stringify(content)}
		document.head.appendChild(style)
	`
	return style
}
module.export = loader

webpack.config.js:

const path = require('path')
const P = require('./plugins/P1')
module.exports = {
	entry:'./src/index.js',
	output:{
		path: path.resolve(__dirname,'dist'),
		filename: 'main.js'
	},
	mode:'development',
	module:{
		rules:[
			{
				test:'/\.less$/',
				use:[
					path.resolve(__dirname,'loader','style-loader.js'),
					path.resolve(__dirname,'loader','less-loader.js')
					]
			}
		]
	}
}

mini-webpack手写

新建文件夹hd-pack写我们自己的mini-webpack工具。

1、npm init -y
2、bin
	| hd-pack.js
   lib
   	| Compiler.js
   	| main.ejs
3、连接:当前目录下npm link
4、在待打包的文件目录下,npm link hd-pack

这个时候,待后面hd-pack文件中的代码写完整,直接可以利用npx hd-pack打包文件了。
hd-pack.js

#! /usr/bin/env node
// 使用node环境

const fs = require('fs')
const path = require('path')
const Compiler = require('../lib/Compiler.js')

let config = require(path.resolve('webpack.config.js'))
// webpack的配置文件

// webpack会先创建一个compiler实例并调用run方法。
let compiler = new Compiler(config)
compiler.run()

webpack打包实现的原理都在Compiler.js中。

const fs = require('fs')
const path = require('path')
// 文件解析成AST
const babylon = require('babylon')
const t = require('@babel/types')
// AST中收集依赖
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const ejs = require('ejs')

//首先Compiler导出的是一个类
class Compiler {
	constructor(config){
		// 配置信息
		this.config = config
		// 相对路径
		this.entryId = ''
		// 入口文件
		this.entry = config.entry
		// 依赖收集
		this.modules = {}
		// 当前的工作路径
		this.root = process.cwd()
	}

	run(){
		// 收集依赖
		this.buildModules(path.resolve(this.root,this.entry),true)
		// 生成打包文件
		this.emitFile()
	}
	// 在读取文件时,由于webpack只能处理js文件,所以此时需要用loader对非js文件进行处理。
	getCode(filePath){
		let content = fs.readFileSync(filePath,'utf8')
		// 获取配置中的loaders
		let rules = this.config.modules.rules
		
		rules.forEach((e,i)=>{
			let {test,use} = e
			if(test.test(filePath)){
			//  如果正则检验成功,则进行loader处理
			// 注意一点:loader的执行顺序是从上到下,所以需要倒序处理
				use = use.reverse()
				use.forEach((e,i)=>{
					let loader = require(e)
					content = loader(content )
				})
			}
		})
		return content
	}
	// 参数:绝对路径、是否是入口文件
	buildModules(pathAbs,isEntry){
		// 读取文件内容
		let content =  this.getCode(pathAbs)
		// 得到一个相对路径--可以用正常的webpack打包看看,里面都是用相对路径作为key。
		let moduleName = './' + this.relative(this.root,pathAbs)
		if(isEntry){
			this.entryId = isEntry
		}
		let res = this.parse(content,path.dirname(moduleName))
		this.modules[moduleName] = res.code
		
		// 递归解析所有依赖
		res.deps.forEach((e,i)=>{
			this.buildModules(path.resolve(this.root, e),false)
		})
	}
	parse(code,parentPath){
		const AST = babylon.parser(code)
		// 存储被引用的相对路径
		let dep = []
		traverse(AST,{
			CallExpression({node}){
				// callee:对应于函数的名字。caller:函数的参数
				if(node.callee.name==='require'){
					node.callee.name='__webpack_require__'
					// 得到模块的路径--即为key值
					let moduleName = node.arguments[0].value
					// 判断是否有扩展名
                    moduleName += path.extname(moduleName) ? '' : '.js'
                    // 改为相对路径
                    moduleName = './' + path.join(parentPath, moduleName)
                    deps.push(moduleName)
                    node.arguments = [t.stringLiteral(moduleName)]
				}
			}
		})
		let code = generator(AST).code
		return {code,deps}
	}
	emitFile(){
		// 目标文件
		let main = path.join(this.config.output.path, this.config.output.filename)
		// 模板文件:由webpack打包出来并作一定修改。
        let temp = fs.readFileSync(path.join(__dirname, 'main.ejs'), 'utf8')
        // 模块渲染,传参
        let tempCode = ejs.render(temp, { entryId: this.entryId, modules: this.modules })
        this.assets = {}
        this.assets[main] = tempCode
        // console.log(tempCode);
        fs.writeFileSync(main, this.assets[main])
	}
}
module.exports = Compiler

注意事项:

1、在mini-webpack的package.json中需要加上"bin": {"hd-pack": "./bin/hd-pack.js"}字段;
2、webpack中loader的职责应遵守单一原则,即一个loader只完成一个职责,这里less-loader实际上还对css进行了处理;
3、plugin问题:本质上一个类,其就是利用tapable发布订阅原理,在打包的各个阶段进行发布事件。

ps:
简单实现了一个mini-webpack,但是还有很多webpack知识点未曾涉及到,如果有疑问,欢迎留言讨论。

你可能感兴趣的:(html5,reactjs,javascript,webpack,es6)