Fork me on GitHub

手写webpack

手写 webpack

初始化项目

1
yarn init -y

配置

package.json

1
"bin": "./bin/pack.js",

新建文件夹 bin,新建文件 pack.js

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env node
//表示使用node执行该js

//1)找到当前执行命令的路径,拿到webpack.config.js

const path = require('path');
const config = require(path.resolve('webpack.config.js'));//获取到配置文件
let Compiler = require('../lib/Compiler.js'); //编译类

let compiler = new Compiler(config);
compiler.hooks.entryOption.call()
//标识运行编译
compiler.run()

新建文件夹 lib,文件 Compiler.js ,编写编译文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
const path = require('path');
const fs = require('fs');
//把源码转换成ast
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');
const { SyncHook } = require('tapable');

class Compiler{
constructor(config){
//配置文件
this.config = config;
//入口文件的路径
this.entryId;
//所有模块依赖
this.modules = {},
this.entry = config.entry;//入口文件路径
this.root = process.cwd();//运行当前脚本的路径npx pack
this.hooks = {
entryOption: new SyncHook(),
compiler: new SyncHook(),
afterCompiler: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
emit:new SyncHook(),
done:new SyncHook(),
}
let plugins = this.config.plugins;
if(Array.isArray(plugins)){
plugins.forEach(plugin => {
plugin.apply(this)
})
}
this.hooks.afterPlugins.call()
}

//获取文件内容
getSource(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;
if(test.test(modulePath)){//后缀匹配到路径匹配
function normalLoader(){
let loader = require(use[len--])
content = loader(content);
if(len>=0){
normalLoader()
}
}
normalLoader()
}
}

return content
}

// 解析源码
parse(source, parentPath){//AST解析语法树
let ast = babylon.parse(source);
let dependencies = [];
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 += path.extname(moduleName)?'':'.js';
moduleName = './' + path.join(parentPath,moduleName);
dependencies.push(moduleName);
node.arguments = [t.stringLiteral(moduleName)]//修改源码中的路径
}
}
});
let sourceCode= generator(ast).code;//生成新的源码
return {sourceCode, dependencies}
}

buildModule(modulePath, isEntry){

let source = this.getSource(modulePath);
let moduleName = './'+ path.relative(this.root,modulePath);
if(isEntry){
this.entryId = moduleName;//入口文件名字
}
//解析源码 改造source源码,返回依赖列表
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))//父路径
//保存解析好的模块
this.modules[moduleName] = sourceCode;

//递归解析依赖
dependencies.forEach(dep =>{
this.buildModule(path.resolve(this.root,dep),false)
})


}

emitFile(){
//输出文件路径
let outputPath = path.join(this.config.output.path,this.config.output.filename);
let template = this.getSource(path.join(__dirname,'main.ejs'));
let bundleCode = ejs.render(template,{entryId:this.entryId,modules:this.modules})
this.assets={};
//多个资源存储
this.assets[outputPath]=bundleCode;
Object.keys(this.assets).forEach( key=>{
fs.writeFileSync(key,this.assets[key])
})
}
run(){//执行编译
this.hooks.run.call()
this.hooks.compiler.call()
//创建模块的依赖关系
this.buildModule(path.resolve(this.root,this.entry),true)
this.hooks.afterCompiler.call();
//生成打包后的文件
this.emitFile();
this.hooks.emit.call();
this.hooks.done.call()

}
}
module.exports= Compiler

npm link

  • 在本地编写的 npm 模块中执行 npm link,将模块链接到全局
    mac 中使用 sudo npm link

  • 在需要项目中使用的执行 npm link,将模块连接到项目中

  • 想去掉 link :npm unlink my-utils

webpack 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
let path = require('path');
class P{
apply(compiler){
compiler.hooks.emit.tap('emit',() => {
console.log('emit')
})
}
}
class P1{
apply(compiler){
compiler.hooks.afterPlugins.tap('afterPlugins',() => {
console.log('afterPlugins')
})
}
}
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()
]
}

手写 less-loader

1
2
3
4
5
6
7
8
9
10
let less =require('less');
function loader(source){
let css ='';
less.render(source, (err,c) => {
css=c.css
})
css=css.replace(/\n/g,'\\n')
return css
}
module.exports =loader

手写 style-loader

1
2
3
4
5
6
7
8
9
function loader(source){
let style=`
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style)
`
return style;
}
module.exports = loader;

loader 加载顺序

  • 从左到右加载
1
2
3
4
rules: [
test:/\.js$/,
use:['loader1','loader2','loader3']
]
  • 从下到上加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 rules:[
{
test: /\.js$/,
use: {
loader:'loader1',
},
}, {
test: /\.js$/,
use: {
loader:'loader2'
}
}, {
test: /\.js$/,
use: {
loader:'loader3'
}
}
]
  • enfore 设置执行顺序,pre-前置 post-后置 normal-正常
    loader1 loader2 loader3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 rules:[
{
test: /\.js$/,
use: {
loader:'loader1',
},
enforce: 'pre'
}, {
test: /\.js$/,
use: {
loader:'loader2'
}
}, {
test: /\.js$/,
use: {
loader:'loader3'
},
enforce: 'post'
}
]
  • inline 内置 loader
    加载顺序 pre-normal-inline-post
    新建 a.js,在 index.js 中添加
1
let str =require('inline-loader!./a.js')

执行顺序
inline-loader

! 没有 normal

1
let str =require('!inline-loader!./a.js')

执行顺序
inline-loader1

-! 不执行 inline-loader 前面的 loader pre-normal

1
let str =require('-!inline-loader!./a.js')

执行顺序
inline-loader2

!!其他 loader 都不执行

1
let str =require('!!inline-loader!./a.js')

执行顺序
inline-loader2

手写 babel-loader

  • 安装模块@babel/core loader-utils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const babel = require('@babel/core');//核心模块
const loaderUtils = require('loader-utils');
function loader(source){
let options = loaderUtils.getOptions(this);//获取loader的options
let cb = this.async();//执行完的回调
babel.transform(source, {
...options,
sourceMap: true,
filename: this.resourcePath.split('/').pop() //source-map文件名
}, (err, result) => {
cb(null, result.code, result.map )/异步
})


}
module.exports = loader

手写 banner-loader 在打包的 js 前面加上相同前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const loaderUtils = require('loader-utils');
const validateOptions = require('schema-utils');//验证参数的类型是否正确
const fs = require('fs')
function loader(source){
let options = loaderUtils.getOptions(this);
let cb = this.async();
let schema ={
type: "object",
properties:{
text:{
type: "string"
},
filename:{
type: "string"
}
}
}
validateOptions(schema, options, 'banner-loader');
if(options.filename){
fs.readFile(options.filename, (err, data) => {
this.addDependency(options.filename);//将文件加入webpack依赖文件中,修改后webpack自动打包
cb(null, `/**${data}**/${source}`)
})
}else{
cb(null, `/**${options.text}**/${source}`)
}
}
module.exports = loader

手写 file-loader

1
2
3
4
5
6
7
8
9
10
const loaderUtils = require('loader-utils');

function loader(source){
//根据传入的文件的二进制,然后转换成MD5文件,文件名是hash+后缀,返回文件名
let filename=loaderUtils.interpolateName(this,'[hash].[ext]',{content: source});
this.emitFile(filename, source); //发射文件放到dist里面
return `module.exports= "${filename}"`//返回MD5文件路径
}
loader.raw = true //二进制
module.exports = loader;

手写 url-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const loaderUtils = require('loader-utils');
const fileLoader= require('./file-loader');
let mime =require('mime');

function loader(source){
let {limit } = loaderUtils.getOptions(this);
if(limit&&source.length>limit){
return fileLoader.call(this, source)
}else{
//返回base64格式 mime获取文件类型
return `module.exports = "data:${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"`
}
}
loader.raw = true
module.exports = loader;

css-loader 解析 background: url(‘./img.jpg’)将路径变成 require(‘./img.jpg’)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function loader(source){
let reg = /url\((.+?)\)/g;
let pos = 0;
let current;
let arr = ['let list = []'];//将css各部分拼接
while(current = reg.exec(source)){
let [matchUrl, g] = current;//匹配到的整体内容和括号里面的内容
let last = reg.lastIndex-matchUrl.length;
arr.push(`list.push(${JSON.stringify(source.slice(pos,last))})`);
pos =reg.lastIndex;
//把g替换成require的写法,=》url(require('xxx))
arr.push(`list.push('url('+require(${g})+')')`);
}
arr.push(`list.push(${JSON.stringify(source.slice(pos))})`)
arr.push(`module.exports= list.join('')`);
return arr.join('\r\n');
}
module.exports = loader

loader.pitch

1
2
3
4
5
6
7
8
9
10
11
12
// 在style-loader上 写了pitch
// style-loader less-loader!css-loader!./index.less
loader.pitch = function (remainingRequest) { // 剩余的请求
// 让style-loader 去处理less-loader!css-loader/./index.less
// require路径 返回的就是css-loader处理好的结果 require('!!css-loader!less-loader!index.less')
let str = `
let style = document.createElement('style');
style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)});
document.head.appendChild(style);
`
return str;
}

手写文件列表 FileListPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class FileListPlugin{
constructor({filename}){
this.filename=filename
}
apply(complier){//编译后的文件
complier.hooks.emit.tap('FileListPlugin', (compilation) => {
let { assets } = compilation;//编译后的静态资源
let content ='## 文件名 文件大小\r\n'
Object.entries(assets).forEach(([filename, statObj]) => {
content += `- ${filename} ${statObj.size()}\r\n`
})
//将生成的列表文件加入打包的资源中,根据该文件生成打包后的dist
assets[this.filename]={
source(){
return content
},
size(){
return content.length
}
}
})
}
}
module.exports = FileListPlugin

使用

1
2
3
new FileListPlugin({
filename:'list.md'
})

将 css 和 js 内联到 html 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

//把外联标签变成内联
const HtmlWebpackPlugin = require('html-webpack-plugin');

class InlineSourcePlugin{
constructor({match}) {
this.reg=match;
}
processTag(tag, compilation){
let newTag,url;
if(tag.tagName === 'link' && this.reg.test(tag.attributes.href)){

newTag = {
tagName: 'style'
};
url = tag.attributes.href;
}
if(tag.tagName === 'script' && this.reg.test(tag.attributes.src)){
newTag = {
tagName: 'script'
};
url = tag.attributes.src;
}
if(url){
newTag.innerHTML = compilation.assets[url].source();
delete compilation.assets[url];
return newTag
}

return tag

}
processTags(data, compilation){
let headTags = [];
let bodyTags = [];
data.headTags.forEach(headTag => {
headTags.push(this.processTag( headTag, compilation))
})
data.bodyTags.forEach(bodyTag => {
bodyTags.push(this.processTag( bodyTag, compilation))
})
return { ...data, headTags, bodyTags}
}
apply(compiler){
compiler.hooks.compilation.tap('InlineSourcePlugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync('MyPlugin', (data, cb) => {
data = this.processTags(data, compilation)
cb(null ,data)
})

})
}
}
module.exports = InlineSourcePlugin
-------------本文结束感谢阅读-------------