Webpack学习笔记(三)

SourceMap

在之前的几章里面,我们介绍了webpack的打包编译功能。能通过loader、plugin机制帮助我们构建代码。虽然构建代码很大程度上便利了我们的生产环境,但是我们实际的代码和我们开发的代码会有很大的差异,当出现错误的时候,我们很难直接定位到源代码中对应的位置。

而Souce Map(源代码地图)就是一个映射转化后的代码与源代码之间的关系。我们可以看到很多第三方的库的dist文件中都会包含.map文件

我们点开来看一下

在浏览器中打开开发者工具后,会自动请求.map文件,然后根据这个文件的内容逆向解析出源代码来,便于调试。同时因为有了映射关系,所以代码中如果出现了错误也就能自动定位找到源代码中的位置了。

Webpack中配置 SourceMap

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');


module.exports = {
  mode: 'none',
  entry: './src/index.js',
+ devtool: 'source-map', // source map 设置
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  }, 
  plugins: [
    new HtmlWebpackPlugin({
      title: 'My App',
      template: './src/index.html'
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
}

查看结果,生成了.map文件

其实source-map的模式有很多种,devtool属性可选的属性非常多官网

下面我想简单的介绍几个sourceMap属性

eval

首先是 eval 模式,它就是将模块代码放到 eval 函数中执行,并且通过 sourceURL 标注所属文件路径,在这种模式下没有 Source Map 文件,所以只能定位是哪个文件出错

eval-source-map

这个模式也是使用 eval 函数执行模块代码,不过这里有所不同的是,eval-source-map 模式除了定位文件,还可以定位具体的行列信息。相比于 eval 模式,它能够生成 Source Map 文件

cheap-eval-source-map

根据这个模式的名字就能推断出一些信息,它就是在 eval-source-map 基础上添加了一个 cheap,也就是便宜的,或者叫廉价的。用计算机行业的常用说法,就是阉割版的 eval-source-map,因为它虽然也生成了 Source Map 文件,但是这种模式下的 Source Map 只能定位到行,而定位不到列

cheap-module-eval-source-map

接下来再看一个叫作 cheap-module-eval-source-map 的模式。慢慢地我们就发现 Webpack 中这些模式的名字不是随意的,好像都有某种规律。这里就是在 cheap-eval-source-map 的基础上多了一个 module

源代码 10-webpack-sourceMap

Tree Shaking

Tree shaking 翻译过来就是“摇树”,伴随着摇树的动作,树上的叶子就会掉下来。在我们的代码中,通过Tree shaking能够帮助我们“摇掉”没有使用到的代码,或者说未引用的代码。

Tree shaking 最早是Rollup提出的,Webpack从2.0过后开始支持这个特性。

我们使用 Webpack 生产模式打包的优化过程中,就使用自动开启这个功能,以此来检测我们代码中的未引用代码,然后自动移除它们。

我们先用webpack的production模式来体验一下。

└─ 02-configuation
   ├── src
   │   ├── component.js
   │   └── index.js
   └── index.html
// component.js
export function isString(value) {
  return typeof value === 'string';
}

export function debounce(action, delay, maxDelay) {
  var statTime = 0;
  var timer = null;
  return function () {
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key];
    }

    if (statTime === 0) {
      statTime = new Date().getTime();
    }

    window.clearTimeout(timer);

    if (maxDelay && new Date().getTime() - statTime >= maxDelay) {
      statTime = 0;
      action.apply(void 0, args);
    } else {
      timer = setTimeout(function () {
        statTime = 0;
        action.apply(void 0, args);
      }, delay);
    }
  };
}
// index.js
import { isString } from './component'
console.log('isString()', isString());
// webpack.config.js
+ mode:'production'

查看打包结果,发现debounce并没有在bundle.js中

开启Tree shaking

需要注意的是,Tree-shaking 并不是指 Webpack 中的某一个配置选项,而是一组功能搭配使用过后实现的效果,这组功能在生产模式下都会自动启用,所以使用生产模式打包就会有 Tree-shaking 的效果。

假设我们在mode为none的情况下,需要对webpack加上optimization属性:

optimization: {
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    concatenateModules : true,
    // 压缩输出结果
    minimize: true
}
  1. usedExports - 打包结果中只导出外部用到的成员;
  2. minimize - 压缩打包结果。
  3. concatenateModules - 尽可能合并每一个模块到一个函数中

如果把我们的代码看成一棵大树,那你可以这样理解:

  1. usedExports 的作用就是标记树上哪些是枯树枝、枯树叶;
  2. minimize 的作用就是负责把枯树枝、枯树叶摇下来。
  3. concatenateModulesd 的作用就是将所有的树种在一起减少维护成本

源代码 11-webpack-tree-shaking

sideEffects

Webpack 4 中新增了一个 sideEffects 特性,它允许我们通过配置标识我们的代码是否有副作用,从而提供更大的压缩空间。

副作用就是指模块代码执行的时候除了导出成员,是否还做了其他的事情

比如我们将代码中的button,head模块提取出来,然后在index.js中统一导出

└─ 12-webpack-sideEffects
   ├── src
   │   ├── components
   ├   ├   ├──head.js
   ├   ├   ├──button.js
   ├   ├   ├──index.js
   └── index.html
   └── index.js
// button.js
console.log('Button component~') // 副作用代码
export default () => {
  return document.createElement('button')
}

// head.js
console.log('Head component~') // 副作用代码
export default () => {
  return document.createElement('head')
}

// src/components/index.js
export { default as Button } from './button'
export { default as Heading } from './head'

// src/index.js
import { Button } from './components'

document.body.appendChild(Button())

当我们不做任何处理的时候,运行webpack打包,具体结果如下

所有的组件模块都被打包进了bundle.js

此时如果我们开启Tree Shaking(只设置useExports),这里没有用的导出成员最终会被移除,查看结果

但是!!!当我们开启minimize时,发现副作用代码并没有被删除

所以说,Tree-shaking 只能移除没有用到的代码成员,而想要完整移除没有用到的模块,那就需要开启 sideEffects 特性了。

开启sideEffects

  1. 首先打开webpack的配置文件,在optimization中设置sideEffects为true,开启sideEffects特性
  2. 在webpack打包某个模块之前会先去检查模块所属的package.json中的sideEffects标识,以此来判断这个模块是否有副作用,如果没有副作用的话,这些没有用到的模块就不会再被打包。所以我们需要在12-webpack-sideEffects这个项目的根目录下的package.json文件中添加一个sideEffects字段,并把它设置为false
{
  "name": "12-webpack-sideEffects",
  "version": "1.0.0",
  "main": "index.js",
  "devDependencies": {
    "clean-webpack-plugin": "^3.0.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.2"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --open"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "sideEffects": false
}

再次运行webpack打包,查看bundle.js

现在,我们代码中的模块如果没有被引用,就算有副作用代码,也不会被打包进来了。

源代码 12-webpack-sideEffects

Code Splitting

ALL in One的弊端

通过Webpack实现前端项目整体模块化的优势固然明显,但是它也会存在一些弊端:它最终会将我们所有的代码打包到一起。这个时候,如果我们的应用非常复杂,模块非常多,那么这种All in One的方式就会导致打包的结果过大。

虽然我们之前讲到Webpack的作用就是把项目中散落的代码打包到一起,从而提高加载效率,那么为什么又分离?其实并不矛盾,Web应用中的资源收环境限制,往往都是需要我们着重考虑的问题。

如果我们不将这些资源模块打包,直接按照开发过程中划分的模块颗粒度进行加载,那么运行一个小小的功能,就需要加载非常多的资源模块。

再者,目前主流的 HTTP 1.1 本身就存在一些缺陷,例如:

  1. 同一个域名下的并行请求是有限制的;
  2. 每次请求本身都会有一定的延迟;
  3. 每次请求除了传输内容,还有额外的请求头,大量请求的情况下,这些请求头加在一起也会浪费流量和带宽。

为了解决打包结果过大导致的问题,Webpack 设计了一种分包功能:Code Splitting(代码分割)

动态导入

Code Splitting 更常见的实现方式是结合 ES Modules 的动态导入特性,从而实现按需加载。

按需加载是开发浏览器应用中一个非常常见的需求。一般我们常说的按需加载指的是加载数据或者加载图片,但是我们这里所说的按需加载,指的是在应用运行过程中,需要某个资源模块时,才去加载这个模块。这种方式极大地降低了应用启动时需要加载的资源体积,提高了应用的响应速度,同时也节省了带宽和流量。

Webpack 中支持使用动态导入的方式实现模块的按需加载,而且所有动态导入的模块都会被自动提取到单独的 bundle 中,从而实现分包。

相比于多入口的方式,动态导入更为灵活,因为我们可以通过代码中的逻辑去控制需不需要加载某个模块,或者什么时候加载某个模块。而且我们分包的目的中,很重要的一点就是让模块实现按需加载,从而提高应用的响应速度。下面我们来看一个例子

└─ 13-webpack-code-splitting
   ├── src
   │   ├── button
   │   │    ├── index.js
   │   ├── head
   │   │    ├── index.js
   └── index.js
   └── index.html
   └── package.json
   └── webpack.config.js
// button/index.js
const button = () => {
  const btn = document.createElement('button');
  btn.style.width = '100px';
  btn.style.height = '50px';
  btn.style.background = 'red';
  document.body.appendChild(btn)  
};

export default button;

// head/index.js
const head = () => {
  const head = document.createElement('input');
  head.style.width = '100px';
  head.style.width = '100px'
  head.style.background = 'green';
  document.body.appendChild(head);
};

export default head;
// index.js
const update = () => {
  const hash = window.location.hash || '#button'
  if (hash === '#button') {
    import('./button').then(({default: button}) => {
      button();
      console.log('加载button成功');
    })

  } else if (hash === '#head') {
    import('./head').then(({default: head}) => {
      head()
      console.log('加载head成功');
    })
  }
}
window.addEventListener('hashchange', update)
update();




可以看到,我们的bundle1和bundle2并没有一次性加载,而是实现了按需加载

源代码 13-webpack-code-splitting

魔法注释

默认通过动态导入产生的 bundle 文件,它的 name 就是一个序号,这并没有什么不好,因为大多数时候,在生产环境中我们根本不用关心资源文件的名称。

但是如果你还是需要给这些 bundle 命名的话,就可以使用 Webpack 所特有的魔法注释去实现。具体方式如下:

// 魔法注释

import(/* webpackChunkName: 'posts' */'./posts/posts')
  .then(({ default: posts }) => {
    mainElement.appendChild(posts())
  })

所谓魔法注释,就是在 import 函数的形式参数位置,添加一个行内注释,这个注释有一个特定的格式:webpackChunkName: ‘’,这样就可以给分包的 chunk 起名字了。

完成过后,我们再次打开命令行终端,运行 Webpack 打包,那此时我们生成 bundle 的 name 就会使用刚刚注释中提供的名称了,具体结果如下:

除此之外,魔法注释还有个特殊用途:如果你的 chunkName 相同的话,那相同的 chunkName 最终就会被打包到一起,例如我们这里可以把这两个 chunkName 都设置为 components,然后再次运行打包,那此时这两个模块都会被打包到一个文件中。

结束语

好了,对于webpack的分享就告一段落,之后准备写一篇关于对之前项目优化的文章(如果我能做到的话,先立个flag 哈哈!)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值