这是第 122 篇纯粹的原创文章。 如果想获取更多原创好文章,请搜索公众号并关注我们~本文首发于正彩云后端博客:Webpack原理-如何实现代码打包
前言
作为后端“攻城狮”,Webpack 是再熟悉不过了。 Webpack 可以做的事情太多了。 它可以将所有资源(包括JS、TS、JSX、图像、字体和CSS等)封装在一个关系中,您可以根据需求引用依赖关系来使用资源。 Webpack 在翻译多个后端文件资源和分析复杂的模块依赖关系方面做得很好,但我们也可以自定义加载器并自由加载我们自己的资源。 Webpack是如何实现打包的? 我们昨天过来看看。
如果我们想要了解Webpack打包的原理,我们需要提前了解两个知识点
1. 需要什么?
说到require,首先想到的可能是import,import是es6的一句标准,
–require是运行时调用,因此理论上require可以在代码中的任何地方使用;
--import是在编译时调用的,所以必须放在文件的开头;
当我们使用Webpack编译时,我们会使用babel将import翻译为require。 在 CommonJS 中webpack 自定义模块,有一个全局方法 require(),用于加载模块。 AMD和CMD也是使用require方法来引用。
例如:
var add = require('./a.js');
add(1,2)
简单来说,虽然require是一个函数,但是引用的./a.js只是该函数的一个参数。
2.什么是出口?
这里我们可以认为exports是一个对象,可以看具体用法。
了解了require和exports之后,我们就可以开始打包了
下面我们看一下我们打包后的代码结构,可以发现打包后会出现require和exports。
并不是所有的浏览器都能执行requireexports,你必须自己实现require并exports才能保证代码的正常运行。 打包后的代码是一个自执行函数,参数有依赖信息,以及文件的代码,执行的函数体通过eval执行代码。
整体设计图如下:
第1步:编译我们的配置文件
配置文件配置了我们打包的入口条目和打包的导出输出,为之前生成的文件做准备。
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),//打包后输出的文件地址,需要绝对路径因此需要path
filename:"main.js"
},
mode:"development"
第二步:模块分析
总体思路:可以概括为使用fs文件读取入口文件,通过AST获取导入依赖的文件的路径。 如果依赖文件仍然存在依赖关系,那么它将继续递归,直到依赖关系被清楚地分析并维护在地图上。
详细拆解:有些人可能会有疑问为什么要使用AST。 由于 AST 本身就有这个功能,它的 ImportDeclaration 可以帮助我们快速过滤掉 import 句型。 其实正则匹配也是可以的。 虽然文件读取后只是一个字符串,但通过 Compiled 强大的正则表达式来获取文件依赖路径,还不够优雅。
Step1:新建index.js,a.js,b.js依赖如下
索引.js 文件
import { str } from "./a.js";
console.log(`${str} Webpack`)
一个.js文件
import { b} from "./b.js"
export const str = "hello"
b.js 文件
export const b="bbb"
步骤2:编译Webpack
模块分析:使用AST的@babel/parser将从文件中读取的字符串转换成AST树,@babel/traverse分析句型,使用ImportDeclaration过滤导入来查找文件依赖关系。
const content = fs.readFileSync(entryFile, "utf-8");
const ast = parser.parse(content, { sourceType: "module" });
const dirname = path.dirname(entryFile);
const dependents = {};
traverse(ast, {
ImportDeclaration({ node }) {
// 过滤出import
const newPathName = "./" + path.join(dirname, node.source.value);
dependents[node.source.value] = newPathName;
}
})
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
return {
entryFile,
dependents,
code
}
结果如下:
使用递归或循环来一一分析导入文件的依赖关系。 请注意,我们使用 for 循环来分析所有依赖关系。 循环之所以能够分析所有依赖关系webpack 自定义模块,是为了注意到当存在依赖关系时,模块的宽度会发生变化。 module.push新的依赖项,modules.length会改变。
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i];
const { dependents } = item;
if (dependents) {
for (let j in dependents) {
this.modules.push(this.parse(dependents[j]));
}
}
}
步骤3:编写WebpackBootstrap函数+生成输出文件
编译WebpackBootstrap函数:这里我们需要做的第一件事是WebpackBootstrap函数。 编译完成后,我们源码的导入会被解析为require。 由于浏览器不知道 require,所以我们先声明它。 虽然 require 是一门技巧,但是在函数的编译时,我们也需要注意作用域隔离,避免变量污染。 我们代码中的导出也需要我们声明它,以确保代码执行时导出已经存在。
生成输出文件:我们已经在配置文件中写入了生成文件的地址,然后使用 fs.writeFileSync 将其写入到输出文件夹中。
file(code) {
const filePath = path.join(this.output.path, this.output.filename)
const newCode = JSON.stringify(code);
// 生成bundle文件内容
const bundle = `(function(modules){
function require(module){
function pathRequire(relativePath){
return require(modules[module].dependents[relativePath])
}
const exports={};
(function(require,exports,code){
eval(code)
})(pathRequire,exports,modules[module].code);
return exports
}
require('${this.entry}')
})(${newCode})`;
// WebpackBoostrap
// 生成文件。放入dist 目录
fs.writeFileSync(filePath,bundle,'utf-8')
}
第四步:分析执行顺序
我们可以在浏览器的控制台中运行打包的结果。 如果正常工作,应该复制helloWebpack。
总结
通过上面的分析,我们应该对Webpack的大致流程有了基本的了解。 使用 AST 解析代码只是本次演示的一种方法,并不是 Webpack 的真正实现。 Webpack有自己的AST解析方法,这离不开接收模块依赖。 Webpack生态系统非常完整。 有兴趣的童鞋可以考虑以下三个问题:
一、webpack背景知识html
后端项目开发的完美工具。或者大口大口地喝。前端
主要功能有:提供前端平滑的前端分离开发环境,可以配置和解析不同的资源文件webpack 插件实现,统一打包和包交付,资源文件按需加载,网站优化等。 webpack
webpack 的主要组件有:入口/退出、编译包括加载器和插件、模型、规则等开发环境配置,webpack 本身提供了许多插件,如分析、压缩、html 等。
联系后端一段时间开发同学应该基本了解怎么写一个加载器,一般流程比较简单,需要一些配置才能完成自定义加载器webpack 插件实现,咕噜咕噜地吃点
自定义插件有点合乎逻辑,了解一些 webpack 内部实现的源代码,后端
实现的逻辑是相关的,比如引入形式、加载时间、新编译等。
2、webpack自定义插件开发链表
2.1 构建插件,5步应用
在函数上构建函数 扩展 Apply 方法以指定绑定到 WebPack 自己的风钩 处理 WebPack 内部实例的特定数据 函数完成后,调用 WebPack 提供的反弹
// 自定义事件的插件函数,也能够写成class的形式,可是内部apply方法不能用 箭头函数。
function myPlugin () { } // 对 插件函数扩展 apply 方法 myPlugin.prototype.apply = function(compiler) {
// webpack提供的编译函数模板,监听webpack的钩子事件,而后会触发 compilation,和插件执行完成的回调。 compiler.plugin('webpacksEventHook', function(compilation,callback) { console.log('this is customer plugins'); callback(); }); }
2.2 webpack 插件的钩子功能与前端和后端分离
Webpack 在打包过程中也有自己的生命周期功能,webpack bar 流程也是分类的,所以有很多不同类型的插件。
有两个重要对象:编译和编译器
编译器包含 webpack (webpack.config.js 的所有配置信息,并在启动时初始化为 webpack 的实例。编译包含当前模块,编译文件,
例如在开发环境中,当文件更改时,新的编译将得到改进。
以下内容是 webpack 的自定义插件。
function MyPlugin(options) { this.options = options; } MyPlugin.prototype.apply = function(compiler) { console.log('开始执行插件') compiler.plugin('compile', function () { console.log('webpack 编译器开始编译...-----') }) compiler.plugin('compilation', function (compilation) { console.log('编译器开始一个新的编译任务...-----') compilation.plugin('optimize', function () { console.log('编译器开始优化文件...') }) }) compiler.plugin('done', function () { console.log('打包完成......') }) }; module.exports = MyPlugin;
例如,编译的块文件中包含的路径是通过 emit 挂钩获取的。不支持使用 webpack 2 进行更高版本的 API 更改
compiler.plugin('emit', function (compilation, callback) { // compilation.chunks 存放全部代码块,是一个数组 compilation.chunks.forEach(function (chunk) { // chunk 代码块 // 代码块由模块组成,读取模块 chunk.forEachModule(function (module) { // module 模块 // module.fileDependencies 访问当前模块须要的依赖,便是文件路径。 module.fileDependencies.forEach(function (filepath) { console.log(filepath) }) }) }) // emit 是异步事件,AsyncSeriesHook,异步事件都须要调用callback。有点像英语里的及物动词和不及物动词 callback(); })
2.3 多页网页包插件