不聊webpack配置,来说说它的原理

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 最近在前端论坛闲逛,看到了一些讲parcel、webpack的文章,就突然很好奇,每天都在用的打包工具,他们打包的原理究竟是什么。只有知道了这一点,才可以在众多的打包工具里,找到最适合的那个它。在了解打包原理之前,先花一些篇章说明了一下为什么要使用打包工具。
最近在前端论坛闲逛,看到了一些讲parcel、webpack的文章,就突然很好奇,每天都在用的打包工具,他们打包的原理究竟是什么。只有知道了这一点,才可以在众多的打包工具里,找到最适合的那个它。在了解打包原理之前,先花一些篇章说明了一下为什么要使用打包工具。


0.模块系统

前端产品的交付是基于浏览器,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统。这个理想中的模块化系统是前端工程师多年来一直探索的难题。

模块系统主要解决模块的定义、依赖和导出。 原始的<script>标签加载方式有一些常见的弊端:例如全局作用域下容易造成变量冲突;文件只能按照<script>的书写顺序进行加载;开发人员必须主观解决模块和代码库的依赖关系等。

因此衍生出很多模块化方案:

1.CommonJs:优点:服务器端模块便于重用。缺点:同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的。

2.AMD:依赖前置。优点:适合在浏览器环境异步加载;缺点:阅读和书写比较困难。

3.CMD:依赖就近,延迟执行。优点:很容易在node中运行;缺点:依赖spm打包,模块的加载逻辑偏重。

4.ES6模块::尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。优点:容易进行静态分析;缺点:原生浏览器未实现该标准。


说到模块的加载和传输,若是每个文件都单独请求,会导致请求次数过多,导致启动速度过慢。若是全部打包在一块只请求一次,会导致流量浪费,初始化过程慢。 因此最佳方案是分块传输,按需进行懒加载,在实际用到某些模块的时候再增量更新。要实现模块的按需加载,就需要一个对整个代码库中的模块进行 静态分析、编译打包的过程。Webpack 就是在这样的需求中应运而生。

注:要注意一个概念,一切皆模块。样式、图片、字体、HTML 模板等等众多的资源,都可以视作模块。

1.模块打包器:webpack

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为例,说明以上问题。

2.打包工具核心原理——以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地址

3.总结

webpack解决了包与包之间潜在的循环依赖难题,同时,按需合并静态文件,以避免浏览器在网络取数阶段的并发瓶颈。除了打包,还可以进一步实现压缩(减少网络传输)和编译(ES6、JSX等语法向下兼容)的功能。

基于对webpack.config.js文件的配置,执行打包时的工作原理,可总结为:把页面逻辑当作一个整体,通过一个给定的入口文件,webpack从这个文件开始,找到所有的依赖文件,进行打包、编译、压缩,最后输出一个浏览器可识别的JS文件。

一个模块打包工具,第一步会从入口文件开始,对其进行依赖分析,第二步对其所有依赖再次递归进行依赖分析,第三步构建出模块的依赖图集,最后一步根据依赖图集使用CommonJS规范构建出最终的代码。


4.参考网址

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


原文发布时间为:2018年07月01日
本文作者:whuzxq
本文来源:掘金  如需转载请联系原作者
相关文章
|
1月前
|
前端开发
在Webpack配置文件中,如何配置loader以处理其他类型的文件,如CSS或图片
在Webpack配置文件中,通过设置`module.rules`来配置loader处理不同类型的文件。例如,使用`css-loader`和`style-loader`处理CSS文件,使用`file-loader`或`url-loader`处理图片等资源文件。配置示例:在`rules`数组中添加对应规则,指定`test`匹配文件类型,`use`指定使用的loader。
|
1月前
|
缓存 前端开发 JavaScript
Webpack与Babel的进阶配置与优化
通过以上的进阶配置和优化策略,可以更好地发挥`Webpack`与`Babel`的功能,提高项目的性能和开发效率。
|
1月前
|
JavaScript 前端开发 UED
如何配置 Webpack 进行代码分离?
通过以上方法,可以有效地配置Webpack进行代码分离,根据项目的具体需求和场景选择合适的方式,能够显著提高应用的性能和用户体验。在实际应用中,还可以结合其他优化技术,进一步优化Webpack的打包结果和应用的加载速度。
52 5
|
1月前
|
前端开发 JavaScript
webpack相关配置
以上只是Webpack配置的一些常见部分,实际应用中还可以根据具体的项目需求和场景进行更复杂和细致的配置,以满足不同的构建和优化要求。
45 2
|
1月前
|
缓存 JavaScript 前端开发
配置 Webpack 进行代码分离的常见方法有哪些?
通过以上常见方法,可以根据项目的具体需求和场景,灵活地配置Webpack进行代码分离,从而提高应用的性能和加载速度,优化用户体验。
35 3
|
1月前
|
缓存 前端开发 JavaScript
前端性能优化:Webpack与Babel的进阶配置与优化策略
【10月更文挑战第28天】在现代Web开发中,Webpack和Babel是不可或缺的工具,分别负责模块打包和ES6+代码转换。本文探讨了它们的进阶配置与优化策略,包括Webpack的代码压缩、缓存优化和代码分割,以及Babel的按需引入polyfill和目标浏览器设置。通过这些优化,可以显著提升应用的加载速度和运行效率,从而改善用户体验。
61 6
|
1月前
|
缓存 监控 前端开发
前端工程化:Webpack与Gulp的构建工具选择与配置优化
【10月更文挑战第26天】前端工程化是现代Web开发的重要趋势,通过将前端代码视为工程来管理,提高了开发效率和质量。本文详细对比了Webpack和Gulp两大主流构建工具的选择与配置优化,并提供了具体示例代码。Webpack擅长模块化打包和资源管理,而Gulp则在任务编写和自动化构建方面更具灵活性。两者各有优势,需根据项目需求进行选择和优化。
76 7
|
2月前
|
JavaScript
webpack学习三:webpack初始化整合配置vue,一步一步的抽离代码块整合vue。
这篇文章是关于如何在webpack环境中配置Vue.js,包括安装Vue.js、解决报错、理解el与template的区别、使用SPA模式、抽离模板为对象、封装为单独的js文件、安装vue-loader时遇到的问题及解决方案,以及整个过程的总结。
99 2
webpack学习三:webpack初始化整合配置vue,一步一步的抽离代码块整合vue。
|
1月前
|
前端开发
配置 Webpack 实现热更新
【10月更文挑战第23天】还可以进一步深入探讨热更新的具体实现细节、不同场景下的应用案例,以及如何针对特定需求进行优化等方面的内容。通过全面、系统地了解 Webpack 热更新的配置方法,能够更好地利用这一功能,提升项目的开发效率和性能表现。同时,要不断关注 Webpack 及相关技术的发展动态,以便及时掌握最新的热更新技术和最佳实践。
|
1月前
|
缓存 前端开发 JavaScript
前端工程化:Webpack与Gulp的构建工具选择与配置优化
【10月更文挑战第27天】在现代前端开发中,构建工具的选择对项目的效率和可维护性至关重要。本文比较了Webpack和Gulp两个流行的构建工具,介绍了它们的特点和适用场景,并提供了配置优化的最佳实践。Webpack适合大型模块化项目,Gulp则适用于快速自动化构建流程。通过合理的配置优化,可以显著提升构建效率和性能。
56 2