Skip to main content

webpack4.0进阶(三)

手写简单的loader、Plugin、简单的webpack

手写简单的loader

目录

myLoader
├── loaders
│ └── myLoader.js
├── package.json
├── src
│ └── index.js
└── webpack.config.js

{% tabs 1 %}

console.log('hello webpack!');
const path =require('path');

module.exports = {
mode: 'development',
entry: './src/index.js',
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: path.resolve(__dirname, './loaders/myLoader.js'),
options: {
key: 'my option value',
},
},
],
}],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
};
module.exports = function (source) {
return source.replace('webpack', this.query.key);
}

{% endtabs %}

这个例子非常简单,就是通过自建的loader将项目中的webpack字符串替换成webpack中配置的字符串。myLoader.js中可以通过this.query接受webpack中配置的options参数。更多this上的属性参考(包括异步处理、回调...)

上面的例子通过打包后代码如下

console.log('hello my option value!')

webpack5中可以直接通过this.getOptions (schema)来获取options参数

webpack resolveLoader:

和之前提到的resolve的使用类似,就是用来偷懒的😂

使用resolveLoader改写后的wepack.config.js

const path =require('path');

module.exports = {
mode: 'development',
entry: './src/index.js',
resolveLoader: {
modules: ['node_modules', './loaders'],
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'myLoader',
options: {
key: 'my option',
},
},
],
}],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
};

上面这个例子可以直接使用myLoader名,webpack会在node_modules./loaders中寻找对应的Loader。

{% note warning , 自定义的loader中不要使用箭头函数,会产生this指向问题 %}

手写简单的Plugin

目录

myPlugin
├── package.json
├── plugins
│ └── date-webpack-plugin.js
├── src
│ └── index.js
└── webpack.config.js

complier提供了许多钩子,可以让我们在打包的不同时刻来进行不同的处理,这里使用了emit钩子

下面通过手写的plugin来实现在dist目录下生成一个author.txt文件

{% tabs 2 %}

class DateWebpackPlugin {
constructor(options) {
// options是new插件时传进来的参数
this.options = options;
}

apply(compiler) {
const _this = this;
compiler.hooks.emit.tapAsync('DateWebpackPlugin', (compilation, cb) => {
compilation.assets['author.txt'] = {
// 返回的资源
source: function () {
return `created by ${_this.options.author} ${new Date()}`;
},
// 最后生成的文件大小
size: function () {
return 19;
}
};
// 由于emit是异步操作,所以最后要执行回调函数
cb();
})
}
}

module.exports = DateWebpackPlugin;
const path = require('path');
const DateWebpackPlugin = require('./plugins/date-webpack-plugin');

module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
new DateWebpackPlugin({
author: 'Alan',
}),
],
};

{% endtabs %}

打包后会在dist目录下自动生成一个author.txt文件,内容如下

created by AlanSun Jun 07 2020 11:49:42 GMT+0800 (GMT+08:00)

react和vue脚手架webpack配置

  • create-react-app通过npm run eject暴露webpack配置
  • vue-cli通过vue.config.js配置webpack(可以通过configureWebpack自定义webpack配置)

手写一个简单的webpack打包工具

先提前安装以下需要的插件

npm i @babel/parser -D // 将js内容转化为抽象语法树
npm i @babel/traverse -D // 用来遍历抽象语法树
npm i @babel/core -D
npm i @babel/preset-env -D //es6->es5
npm i cli-highlight -D // 可选,命令行高亮插件

前置知识:

node:

项目目录:

bundler
├── bundler.js // 主要文件
└── src
├── course.js
├── index.js
└── learn.js

{% tabs 3 %}

import notify from './learn.js';

console.log(notify);
import { course } from './course.js';

const learnNotify = `time to learn ${course}`;

export default learnNotify;
export const course = 'webpack';

{% endtabs %}

我将整个项目拆分成2个部分来分析

处理入口文件找到所有import文件

思路:

  1. 通过fs.readFileSync读取index.js的内容
  2. 使用@babel/parser将读取的内容转化为AST抽象语法树
  3. 使用@babel/traverse遍历找到所有import语句
  4. 分析出引用的文件,保存其路径

代码:

const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
// 命令行高亮工具
const highlight = require('cli-highlight').highlight

const moduleAnalysis = (filename) => {
// 读取出index.js文件内容
const content = fs.readFileSync(filename, 'utf-8');
// 将文件内容转化为抽象语法树
const ast = parser.parse(content, {
sourceType: 'module'
});
// 遍历抽象语法树
traverse(ast, {
ImportDeclaration({ node }) {
console.log(node);
}
})
// console.log(highlight(ast));
console.log(ast.program.body);
}
moduleAnalysis('./src/index.js');

通过parser转化成的抽象语法树

通过上图可以清楚看到我们现在要做的事情就是找到所有为type为importDeclaration的node属性

使用traverse得到的ImportDeclaration

最后对js文件进行babel处理,转化成浏览器能够识别的代码

完整代码

const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
const babel = require('@babel/core');
// 命令行高亮工具
const highlight = require('cli-highlight').highlight

const moduleAnalysis = (filename) => {
// 读取出index.js文件内容
const content = fs.readFileSync(filename, 'utf-8');
// 将文件内容转化为抽象语法树
const ast = parser.parse(content, {
sourceType: 'module'
});
const dependencies = {};
// 遍历抽象语法树
traverse(ast, {
ImportDeclaration({ node }) {
// 文件对应目录./src
const dirPath = path.dirname(filename);
// 绝对路径./src/learn.js(window操作系统)
let filePath = ('./' + path.join(dirPath, node.source.value)).replace('\\', '/');
dependencies[node.source.value] = filePath;
// { './learn.js': './src/learn.js' }
console.log(dependencies);
}
});
// 转化成浏览器可以执行的代码
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
console.log(highlight(code));
return {
filename,
dependencies,
code
}
}
moduleAnalysis('./src/index.js');

通过入口文件分析出所有文件依赖

上面已经分析出了入口文件的一些依赖,接下来可以通过递归遍历来分析出所有的文件依赖并保存在变量中,先分析一些经过上面函数处理后的数据

{
filename: './src/index.js',
dependencies: { './learn.js': './src/learn.js' },
code: '"use strict";\n' +
'\n' +
'var _learn = _interopRequireDefault(require("./learn.js"));\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'console.log(_learn["default"]);'
}

我们只需要遍历对象中的dependencies属性,把里面的路径名传入到上面的moduleAnalysis函数中,最终获取所有的依赖信息。

具体代码如下

const analysisDependenciesGraph = (entry) => {
const entryModule = moduleAnalysis(entry);
const graphList = [entryModule];
for (let i = 0; i < graphList.length; i++) {
const item = graphList[i];
const { dependencies } = item;
if (dependencies) {
for (let i in dependencies) {
graphList.push(moduleAnalysis(dependencies[i]))
}
}
}
const graph = {};
graphList.forEach(({ filename, dependencies, code }) => {
graph[filename] = {
dependencies,
code
}
});
return graph;
}

分析出的所有依赖对象

{
'./src/index.js': {
dependencies: { './learn.js': './src/learn.js' },
code: '"use strict";\n' +
'\n' +
'var _learn = _interopRequireDefault(require("./learn.js"));\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'console.log(_learn["default"]);'
},
'./src/learn.js': {
dependencies: { './course.js': './src/course.js' },
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = void 0;\n' +
'\n' +
'var _course = require("./course.js");\n' +
'\n' +
'var learnNotify = "time to learn ".concat(_course.course);\n' +
'var _default = learnNotify;\n' +
'exports["default"] = _default;'
},
'./src/course.js': {
dependencies: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports.course = void 0;\n' +
"var course = 'webpack';\n" +
'exports.course = course;'
}
}

生成代码

const generateCode = (entry) => {
const graph = JSON.stringify(analysisDependenciesGraph(entry));
return `
(function(graph){
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code){
eval(code)
})(localRequire, exports, graph[module].code);
return exports;
};
require('${entry}')
})(${graph});
`;
}

生成后的代码就可以直接在浏览器运行了

image-20200613160248899

完整代码

const fs = require('fs');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const path = require('path');
const babel = require('@babel/core');
// 命令行高亮工具
const highlight = require('cli-highlight').highlight

const moduleAnalysis = (filename) => {
// 读取出index.js文件内容
const content = fs.readFileSync(filename, 'utf-8');
// 将文件内容转化为抽象语法树
const ast = parser.parse(content, {
sourceType: 'module'
});
const dependencies = {};
// 遍历抽象语法树
traverse(ast, {
ImportDeclaration({ node }) {
// 文件对应目录./src
const dirPath = path.dirname(filename);
// 绝对路径./src/learn.js(window操作系统)
let filePath = ('./' + path.join(dirPath, node.source.value)).replace('\\', '/');
dependencies[node.source.value] = filePath;
// { './learn.js': './src/learn.js' }
console.log(dependencies);
}
});
// 转化成浏览器可以执行的代码
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
return {
filename,
dependencies,
code
}
}

const analysisDependenciesGraph = (entry) => {
const entryModule = moduleAnalysis(entry);
const graphList = [entryModule];
for (let i = 0; i < graphList.length; i++) {
const item = graphList[i];
const { dependencies } = item;
if (dependencies) {
for (let j in dependencies) {
graphList.push(moduleAnalysis(dependencies[j]))
}
}
}
const graph = {};
graphList.forEach(({ filename, dependencies, code }) => {
graph[filename] = {
dependencies,
code
}
});
return graph;
}

const generateCode = (entry) => {
const graph = JSON.stringify(analysisDependenciesGraph(entry));
return `
(function(graph){
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code){
eval(code)
})(localRequire, exports, graph[module].code);
return exports;
};
require('${entry}')
})(${graph});
`;
}

const code = generateCode('./src/index.js');

console.log(highlight(code));

总结

到这里总算是对webpack有了大体的了解了。奈何当我学完webpack后看到了vite这个东西😒。。。

image-20200615160241265