# webpack4(四) webpack底层原理及脚手架工具分析
# 如何编写一个loader
初始化一个项目
npm init
安装webpack webpack-cli
npm install webpack webpack-cli --save-dev
创建webpack.config.js文件
var path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
2
3
4
5
6
7
8
9
10
11
12
在src文件夹下创建index.js文件
console.log('hello webpack');
此时测试已经可以正常打包了
在根目录创建loaders文件夹,创建replaceLoader.js文件
// 将源里的所有 'webpack' 替换为 'loader'
module.exports = function (source) {
return source.replace('webpack', 'loader');
}
2
3
4
然后在webpack.config.js中添加loader依赖
var path = require('path');
module.exports = {
//....
module: {
rules: [
{
test: /\.js/,
use: [
path.resolve(__dirname, './loaders/replaceLoader.js')
]
}
]
},
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
打包后发现已经成功使用replaceLoader将 'webpack' 都替换成了 'loader'
module的参数还可以再修改,我们再传递一个option试试看
var path = require('path');
module.exports = {
//....
module: {
rules: [
{
test: /\.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
options: {
name: 'loaders'
}
}
]
}
]
},
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
this.query
在loader中,使用this能接收很多东西,options就是通过this.query来接收的
module.exports = function (source) {
return source.replace('webpack', this.query.name);
}
2
3
this.callback
this.callback(
err: Error | null, //
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
// 1. 第一个参数必须是 Error 或者 null
// 2. 第二个参数是一个 string 或者 Buffer。
// 3. 可选的:第三个参数必须是一个可以被这个模块解析的 source map。
// 4. 可选的:第四个选项,会被 webpack 忽略,可以是任何东西(例如一些元数据)。
2
3
4
5
6
7
8
9
10
11
this.async
如果我们想在loader里运行异步代码怎么办,直接写是不行的,this里面提供了一个 this.async 方法
this.async 返回一个 this.callback
module.exports = function (source) {
const options = this.query;
const callback = this.async();
setTimeout(() => {
const result = source.replace('webpack', options.name);
callback(null, result);
}, 1000);
}
2
3
4
5
6
7
8
现在引用loader的时候用resovle一串很麻烦,我们想要想引入外部loader一样直接使用loader名
在webpack.config.js中
module.exports = {
//....
resolveLoader: {
modules: ['node_modules', './loaders'] // 引入loader时会先去node_modules找,如果找不到再去loaders文件夹下去找
},
module: {
rules: [
{
test: /\.js/,
use: [
{
loader: 'replaceLoader', // 这样就可以直接写名字了
options: {
name: 'loaders'
}
}
]
}
]
},
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 如何编写一个plugin
生成一个项目
在根目录下创建plugins,创建copyright-webpack-plugin.js 文件
class CopyrightWebpackPlugin {
constructor(options){
// options会拿到外面传的所有参数
console.log('插件被使用了')
}
apply(compiler) {
}
}
module.exports = CopyrightWebpackPlugin;
2
3
4
5
6
7
8
9
10
11
12
在webpack.config.js中添加插件
const path = require('path');
const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins: [
new CopyRightWebpackPlugin({
// 这里可以传参数
})
],
output: {
path: path.resolve(__dirname, 'dist')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
此时打包就会打印“插件被使用了 ”
继续完善插件
插件都基于webpack的生命周期钩子来进行
compiler参数是webpack的一个实例,里面存储了webpack打包相关的全部东西,compiler.hooks就是钩子。
class CopyrightWebpackPlugin {
constructor(options){
console.log('插件被使用了')
}
apply(compiler) {
// 同步钩子 用 tap就可以
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
console.log('compilation')
})
// 打断点,方便调试
debugger;
// webapck生命周期钩子,emit是当把文件放入dist目录前,这是一个异步钩子,所以需要用 tapAsync 和 cb
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
// 将'copyright.txt' 文件在打包文件放入dist之前放在dist中
compilation.assets['copyright.txt'] = {
// 文件内容
source: function () {
return 'copyright by zxy'
},
// 文件大小
size: function () {
return 16
}
}
// 异步钩子最后要执行一下回调
cb();
})
}
}
module.exports = CopyrightWebpackPlugin;
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
为了方便我们快速调试查看里面的内容,我们可以使用node的一个调试工具
在package中我们先定义一个命令 debug --inspect 参数意思是要开启node调试工具 --inspect-brk 意思是要在第一行添加一个断点
{
//...
"scripts": {
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
"build": "webpack"
},
//...
}
2
3
4
5
6
7
8
打开chrome,在控制台打开node标志的图标进行调试,我们在插件中可以写入 debugger 来打断点,这样在调试的时候就会在断点停止,这时我们就可以看到一些想看到的内容
# Bundler 源码编写
# 模块分析
首先在src下创建几个文件
word.js
export const word = 'hello';
2
message.js
import {word} from './word.js';
const message = `say ${word}`;
export default message;
2
3
4
5
index.js
import message from './message.js'
console.log(message);
2
然后再创建一个bundler.js文件
const fs = require('fs');
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
console.log(content);
}
moduleAnalyser('./src/index.js');
2
3
4
5
6
7
8
用node运行 bundler.js 可以拿到index文件里的内容了
接下来就要拿index.js里的引入的依赖 我们安装一个@babel/parser
npm install @babel/parser --save-dev
引用parser 会输出一个 ast 抽象语法树,可以用来分析 再安装 @babel/traverse 来把 import 进来的依赖分出来 最后输出出来的数组便是依赖路径
const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
const path = require('path');
// 分析模块
const moduleAnalyser = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
const ast = parser.parse(content, {
sourceType: 'module' // 使用module方式解析
});
const dependencies = {};// 用来存放所有依赖路径
traverse(ast, {
ImportDeclaration({node}) {
const dirname = path.dirname(filename);
const newFile = './' + path.join(dirname, node.source.value);
// 将绝对路径和相对路径都存储,方便后面使用
dependencies[node.source.value] = newFile;
}
})
//用babel进行编译,将es6编译成es5
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return {
filename,
dependencies,
code
}
}
const moduleInfo = moduleAnalyser('./src/index.js');
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
# Dependencies Graph 依赖图谱
通过递归循环,将一层一层的依赖及内容形成依赖图谱,并保存
const makeDependenciesGraph = (entry) => {
const entryModule = moduleAnalyser(entry);
const graphArray = [entryModule];
for(let i = 0; i < graphArray.length; i++) {
const item = graphArray[i];
const {dependencies} = item;
if (dependencies) {
for(let j in dependencies) {
graphArray.push(
moduleAnalyser(dependencies[j])
);
}
}
}
// 将数组转存为对象,方便使用
const graph = {};
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graph;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 生成代码
const generateCode = (entry) => {
const graph = JSON.stringify(makeDependenciesGraph(entry));
return `
(function(graph){
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function(require, code) {
eval(code)
})(localRequire, graph[module].code);
return exports;
};
require('${entry}')
})(${graph})`
}
const code = generateCode('./src/index.js');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19