最近在前端论坛闲逛,看到了一些讲parcel、webpack的文章,就忽然很好奇,天天都在用的打包工具,他们打包的原理到底是什么。只有知道了这一点,才能够在众多的打包工具里,找到最适合的那个它。在了解打包原理以前,先花一些篇章说明了一下为何要使用打包工具。html
前端产品的交付是基于浏览器,这些资源是经过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,而且保证他们在浏览器端快速、优雅的加载和更新,就须要一个模块化系统。这个理想中的模块化系统是前端工程师多年来一直探索的难题。前端
模块系统主要解决模块的定义、依赖和导出。 原始的<script>
标签加载方式有一些常见的弊端:例如全局做用域下容易形成变量冲突;文件只能按照<script>
的书写顺序进行加载;开发人员必须主观解决模块和代码库的依赖关系等。node
所以衍生出不少模块化方案:webpack
1.CommonJs:优势:服务器端模块便于重用。缺点:同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的。git
2.AMD:依赖前置。优势:适合在浏览器环境异步加载;缺点:阅读和书写比较困难。github
3.CMD:依赖就近,延迟执行。优势:很容易在node中运行;缺点:依赖spm打包,模块的加载逻辑偏重。web
4.ES6模块::尽可能的静态化,使得编译时就能肯定模块的依赖关系,以及输入输出的变量。CommonJS 和 AMD 模块,都只能在运行时肯定这些东西。优势:容易进行静态分析;缺点:原生浏览器未实现该标准。数组
说到模块的加载和传输,如果每一个文件都单独请求,会致使请求次数过多,致使启动速度过慢。如果所有打包在一块只请求一次,会致使流量浪费,初始化过程慢。所以最佳方案是分块传输,按需进行懒加载,在实际用到某些模块的时候再增量更新。要实现模块的按需加载,就须要一个对整个代码库中的模块进行静态分析、编译打包的过程。Webpack 就是在这样的需求中应运而生。浏览器
注:要注意一个概念,一切皆模块。样式、图片、字体、HTML 模板等等众多的资源,均可以视做模块。缓存
Webpack 是一个模块打包器。它将根据模块的依赖关系进行静态分析,而后将这些模块按照指定的规则生成对应的静态资源。 那么问题来了,webpack真的能作到上述提到的静态分析、编译打包么?咱们首先来看一下webpack能作什么:
1.代码拆分 Webpack 有两种组织模块依赖的方式,同步和异步。异步依赖做为分割点,造成一个新的块。在优化了依赖树后,每个异步区块都做为一个文件被打包。
2.Loader Webpack 自己只能处理原生的 JavaScript 模块,可是 loader 转换器能够将各类类型的资源转换成 JavaScript 模块。这样,任何资源均可以成为 Webpack 能够处理的模块。
3.智能解析 Webpack 有一个智能解析器,几乎能够处理任何第三方库,不管它们的模块形式是 CommonJS、 AMD 仍是普通的 JS 文件。
4.插件系统 Webpack 还有一个功能丰富的插件系统。大多数内容功能都是基于这个插件系统运行的,还能够开发和使用开源的 Webpack 插件,来知足各式各样的需求。
5.快速运行 Webpack 使用异步 I/O 和多级缓存提升运行效率,这使得 Webpack 可以以使人难以置信的速度快速增量编译。
以上是webpack五个主要特色,可是看完仍是以为有些雾里看山,webpack究竟是如何把一些分散的小模块,整合成大模块?又是如何处理好各模块的依赖关系?下面就以parcel核心开发者@ronami的开源项目minipack为例,说明以上问题。
打包工具就是负责把一些分散的小模块,按照必定的规则整合成一个大模块的工具。与此同时,打包工具也会处理好模块之间的依赖关系,将项目运行在平台上。minipack项目最想说明的问题,也是打包工具最核心的部分,就是如何处理好模块间的依赖关系。
首先,打包工具会从一个入口文件开始,分析里面的依赖,并进一步地分析依赖中的依赖。 咱们新建三个文件,并创建依赖:
/* name.js */
export const name = 'World'
/* message.js */
import { name } from './name.js'
export default `Hello ${name}!`
/* entry.js */
import message from './message.js'
console.log(message)
复制代码
首先引入必要的工具
/* minipack.js */
const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')
复制代码
接着咱们将建立一个函数,参数是文件的路径,做用是读取文件内容并提取它的依赖关系。
function createAsset(filename) {
// 以字符串形式读取文件的内容.
const content = fs.readFileSync(filename, 'utf-8');
// 如今咱们试图找出这个文件依赖于哪一个文件。虽然咱们能够经过查看其内容来获取import字符串. 然而,这是一个很是笨重的方法,咱们将使用JavaScript解析器来代替。
// JavaScript解析器是能够读取和理解JavaScript代码的工具,它们生成一个更抽象的模型,称为`ast (抽象语法树)(https://astexplorer.net)`。
const ast = babylon.parse(content, {
sourceType: 'module',
});
// 定义数组,这个数组将保存这个模块依赖的模块的相对路径.
const dependencies = [];
// 咱们遍历`ast`来试着理解这个模块依赖哪些模块,要作到这一点,咱们须要检查`ast`中的每一个 `import` 声明。
// `Ecmascript`模块至关简单,由于它们是静态的. 这意味着你不能`import`一个变量,或者有条件地`import`另外一个模块。每次咱们看到`import`声明时,咱们均可以将其数值视为`依赖性`。
traverse(ast, {
ImportDeclaration: ({node}) =>
// 咱们将依赖关系存入数组
dependencies.push(node.source.value);
},
});
// 咱们还经过递增简单计数器为此模块分配惟一标识符.
const id = ID++;
// 咱们使用`Ecmascript`模块和其余JavaScript,可能不支持全部浏览器。
// 为了确保咱们的程序在全部浏览器中运行,
// 咱们将使用[babel](https://babeljs.io)来进行转换。
// 咱们能够用`babel-preset-env``将咱们的代码转换为浏览器能够运行的东西.
const {code} = transformFromAst(ast, null, {
presets: ['env'],
});
// 返回有关此模块的全部信息.
return {
id,
filename,
dependencies,
code,
};
}
复制代码
如今咱们能够提取单个模块的依赖关系,那么,咱们将提取它的每个依赖关系的依赖关系,并循环下去,直到咱们了解应用程序中的每一个模块以及他们是如何相互依赖的。
function createGraph(entry) {
// 首先解析整个文件.
const mainAsset = createAsset(entry);
// 咱们将使用queue来解析每一个asset的依赖关系.
// 咱们正在定义一个只有entry asset的数组.
const queue = [mainAsset];
// 咱们使用一个`for ... of`循环遍历 队列.
// 最初 这个队列 只有一个asset,可是当咱们迭代它时,咱们会将额外的assert推入到queue中.
// 这个循环将在queue为空时终止.
for (const asset of queue) {
// 咱们的每个asset都有它所依赖模块的相对路径列表.
// 咱们将重复它们,用咱们的`createAsset() `函数解析它们,并跟踪此模块在此对象中的依赖关系.
asset.mapping = {};
// 这是这个模块所在的目录.
const dirname = path.dirname(asset.filename);
// 咱们遍历其相关路径的列表
asset.dependencies.forEach(relativePath => {
// 咱们能够经过将相对路径与父资源目录的路径链接,将相对路径转变为绝对路径.
const absolutePath = path.join(dirname, relativePath);
// 解析asset,读取其内容并提取其依赖关系.
const child = createAsset(absolutePath);
// 了解`asset`依赖取决于`child`这一点对咱们来讲很重要.
// 经过给`asset.mapping`对象增长一个新的属性(值为child.id)来表达这种一一对应的关系.
asset.mapping[relativePath] = child.id;
// 最后,咱们将`child`这个资产推入队列,这样它的依赖关系也将被迭代和解析.
queue.push(child);
});
}
return queue;
}
复制代码
接下来咱们定义一个函数,传入上一步的graph,返回一个能够在浏览器上运行的包。
function bundle(graph) {
let modules = '';
// 在咱们到达该函数的主体以前,咱们将构建一个做为该函数的参数的对象.
// 请注意,咱们构建的这个字符串被两个花括号 ({}) 包裹,所以对于每一个模块,
// 咱们添加一个这种格式的字符串: `key: value,`.
graph.forEach(mod => {
// 图表中的每一个模块在这个对象中都有一个entry. 咱们用模块的id`做为`key`,用数组做为`value`
// 第一个参数是用函数包装的每一个模块的代码. 这是由于模块应该被限定范围: 在一个模块中定义变量不会影响其余模块或全局范围.
// 对于第二个参数,咱们用`stringify`解析模块及其依赖之间的关系(也就是上文的asset.mapping). 解析后的对象看起来像这样: `{'./relative/path': 1}`.
// 这是由于咱们模块的被转换后会经过相对路径来调用`require()`. 当调用这个函数时,咱们应该可以知道依赖图中的哪一个模块对应于该模块的相对路径.
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`;
/ 最后,使用`commonjs`,当模块须要被导出时,它能够经过改变exports对象来暴露模块的值.
// require函数最后会返回exports对象.
const result = `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`;
return result;
});
复制代码
运行!
const graph = createGraph('./example/entry.js');
const result = bundle(graph);
//获得结果,开心!
console.log(result);
复制代码
更多信息可访问项目github地址
webpack解决了包与包之间潜在的循环依赖难题,同时,按需合并静态文件,以免浏览器在网络取数阶段的并发瓶颈。除了打包,还能够进一步实现压缩(减小网络传输)和编译(ES六、JSX等语法向下兼容)的功能。
基于对webpack.config.js文件的配置,执行打包时的工做原理,可总结为:把页面逻辑看成一个总体,经过一个给定的入口文件,webpack从这个文件开始,找到全部的依赖文件,进行打包、编译、压缩,最后输出一个浏览器可识别的JS文件。
一个模块打包工具,第一步会从入口文件开始,对其进行依赖分析,第二步对其全部依赖再次递归进行依赖分析,第三步构建出模块的依赖图集,最后一步根据依赖图集使用CommonJS规范构建出最终的代码。
https://mp.weixin.qq.com/s/w-oXmHNSyu0Y_IlfmDwJKQ
https://github.com/chinanf-boy/minipack-explain/blob/master/src/minipack.js
https://zhaoda.net/webpack-handbook/configuration.html