webpack | 进阶

SourceMap 配置选择

之前我们通过webpack, 将我们的源码打包成了 bundle.js 。 试想:实际上客户端(浏览器)读取的是打包后的 bundle.js ,那么当浏览器执行代码报错的时候,报错的信息自然也是bundle的内容。

我们如何将报错信息(bundle错误的语句及其所在行列)映射到源码上?

devtool 配置

const config = {
  entry: './src/index.js', // 打包入口地址
  output: {
    filename: 'bundle.js', // 输出文件名
    path: path.join(__dirname, 'dist'), // 输出文件目录
  },
  devtool: 'source-map'//开发环境中默认为eval,生产环境中默认不开启
  module: { 
     // ...
  }
  // ...
复制代码


执行打包后,dist 目录下会生成以 .map 结尾的 SourceMap 文件

dist                   
├─ avatard4d42d52.png  
├─ bundle.js           
├─ bundle.js.map     
└─ index.html          
复制代码

除了 source-map 这种类型之外,还有很多种类型可以用,例如:

dist                                     
├─ js                                    
│  ├─ cheap-module-source-map.js #............ 有对应的 .map 文件        
│  ├─ cheap-module-source-map.js.map     
│  ├─ cheap-source-map.js #................... 有              
│  ├─ cheap-source-map.js.map            
│  ├─ eval-cheap-module-source-map.js #....... 无  
│  ├─ eval-cheap-source-map.js #.............. 无          
│  ├─ eval-source-map.js #.................... 无               
│  ├─ eval.js #............................... 无                           
│  ├─ hidden-source-map.js #.................. 有              
│  ├─ hidden-source-map.js.map           
│  ├─ inline-cheap-module-source-map.js #..... 无  
│  ├─ inline-cheap-source-map.js #............ 无         
│  ├─ inline-source-map.js #.................. 无               
│  ├─ nosources-source-map.js #............... 有           
│  ├─ nosources-source-map.js.map        
│  ├─ source-map.js #......................... 有                     
│  └─ source-map.js.map                  
├─ cheap-module-source-map.html          
├─ cheap-source-map.html                 
├─ eval-cheap-module-source-map.html     
├─ eval-cheap-source-map.html            
├─ eval-source-map.html                  
├─ eval.html                             
├─ hidden-source-map.html                
├─ inline-cheap-module-source-map.html   
├─ inline-cheap-source-map.html          
├─ inline-source-map.html                
├─ nosources-source-map.html             
└─ source-map.html                       
复制代码

从目录结构我们可以很容易看出来,evalinline 模式的都没有对应的.map 文件

eval 模式:

  1. 生成代码通过 eval 执行 👇🏻

image.png

  1. 源代码位置通过 @sourceURL 注明 👇🏻

image.png

  1. 无法定位到错误位置,只能定位到某个文件

  2. 不用生成 SourceMap 文件,打包速度快

source-map 模式:

  1. 生成了对应的 SourceMap 文件,打包速度慢
  2. 源代码中定位到错误所在行列信息 👇🏻

image.png

eval-source-map 模式:

  1. 生成代码通过 eval 执行 👇🏻

image.png 2. 包含 dataUrl 形式的 SourceMap 文件

image.png

  1. 可以在编译后的代码中定位到错误所在行列信息

image.png

  1. 生成 dataUrl 形式的 SourceMap,打包速度慢

eval-cheap-source-map 模式:

  1. 生成代码通过 eval 执行
  2. 包含 dataUrl 形式的 SourceMap 文件
  3. 可以在编译后的代码中定位到错误所在信息
  4. 不需要定位列信息,打包速度较快

eval-cheap-module-source-map 模式:

  1. 生成代码通过 eval 执行
  2. 包含 dataUrl 形式的 SourceMap 文件
  3. 可以在编译后的代码中定位到错误所在信息
  4. 不需要定位列信息,打包速度较快
  5. 源代码中定位到错误所在信息 👇🏻

image.png

inline-source-map 模式:

  1. 通过 dataUrl 的形式引入 SourceMap 文件 👇🏻

image.png

devtool build rebuild 显示代码 SourceMap 文件 描述
(none) 很快 很快 无法定位错误
eval 很快(cache) 编译后 定位到文件
source-map 很慢 很慢 源代码 定位到行列
eval-source-map 很慢 一般(cache) 编译后 有(dataUrl) 定位到行列
eval-cheap-source-map 一般 快(cache) 编译后 有(dataUrl) 定位到行
eval-cheap-module-source-map 快(cache) 源代码 有(dataUrl) 定位到行
inline-source-map 很慢 很慢 源代码 有(dataUrl) 定位到行列
hidden-source-map 很慢 很慢 源代码 无法定位错误
nosource-source-map 很慢 很慢 源代码 定位到文件

对照一下校验规则 ^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$ 分析一下关键字

关键字 描述
inline 代码内通过 dataUrl 形式引入 SourceMap
hidden 生成 SourceMap 文件,但不使用
eval eval(...) 形式执行代码,通过 dataUrl 形式引入 SourceMap
nosources 不生成 SourceMap
cheap 只需要定位到行信息,不需要列信息
module 展示源代码中的错误位置
  1. 本地开发:

推荐:eval-cheap-module-source-map

理由:

  • 本地开发首次打包慢点没关系,因为 eval 缓存的原因,rebuild 会很快
  • 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap
  • 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module
  1. 生产环境:

推荐:(none)

理由:

  1. 通过bundle和sourcemap文件,可以反编译出源码————也就是说,线上产

物有soucemap文件的话,就意味着有暴漏源码的风险。

  1. 我们可以观察到,sourcemap文件的体积相对比较巨大,这跟我们生产环境的追

求不同(生产环境追求更小更轻量的bundle)。

devServer

1.安装 webpack-dev-server

npm intall webpack-dev-server -D


2.配置本地服务

// webpack.config.js
const config = {
  // ...
  devServer: {
    static: path.resolve(__dirname, 'dist'), // 输出静态文件目录,默认是输出到public
    compress: true, //是否启动压缩 gzip
    port: 8080, // 端口号
    // open:true  // 是否自动打开浏览器
  },
 // ...
}
module.exports = (env, argv) => {
  console.log('argv.mode=',argv.mode) // 打印 mode(模式) 值
  // 这里可以通过不同的模式修改 config 配置
  return config;
}

为了方便,我们配置一下工程的脚本命令,在package.json的scripts里

{ //... "scripts": { //... "dev": "webpack serve --mode development" } }

3.添加响应头

    headers: {      'X-Access-Token': 'abc123'    },

在浏览器中右键检查(或者使用f12快捷键),在Network一栏查看任意资源访 问,我们发现响应头里成功打入了一个'X-Access-Token': 'abc123' 。

4.开启代理

我们打包出的 js bundle 里有时会含有一些对特定接口的网络请求(ajax/fetch). 要注意,此时客户端地址是在 http://localhost:3000/ 下,假设我们的接口来自 http://localhost:4001/ ,那么毫无疑问,此时控制台里会报错并提示你跨域。

module.exports = { 
//... devServer: {
 proxy: { '/api': 'http://localhost:4001', }, 
  },
 }

对 /api/users 的请求会将请求代理到 http://localhost:4001/api/users 。 如 果不希望传递/api,则需要重写路径:

proxy: {
 '/api': {
 target: 'http://localhost:4001',
 pathRewrite: { '^/api': '' },

 },
 }

5.historyApiFallback

此时打开network,刷新并查看,就会发现问题所在———浏览器把这个路由当作了 静态资源地址去请求,然而我们并没有打包出/some这样的资源,所以这个访问无疑 是404的。 如何解决它? 这种时候,我们可以通过配置来提供页面代替任何404的静态资源响应

historyApiFallback:true

在多数业务场景下,我们 需要根据不同的访问路径定制替代的页面,这种情况下,我们可以使用rewrites

module.exports = { 
//... devServer: { 
historyApiFallback: { 
rewrites: [ { from: /^\/$/, to: '/views/landing.html' },
 { from: /^\/subpage/, to: '/views/subpage.html' },
 { from: /./, to: '/views/404.html' },
 ], 
}, 
}, 
};

其余配置,如http2、https、主机号

module.exports = {
  mode: 'development',

  entry: './app.js',

  output: {
    publicPath: '/'  //打包好的文件的公共路径
  },

  devServer: {
    static: path.resolve(__dirname, './dist'),//静态文件路径
    compress: false,//gzip
    port: 3000,//端口
    host: '0.0.0.0',//主机号,设置0.0.0.0后同一个局域网的可以访问

    headers: {
      'X-Access-Token': 'abc123'//请求头
    },

    proxy: {
      '/api': 'http://localhost:9000'//代理,可rewrites
    },

    // https: true 
    http2: true,

    historyApiFallback: true
  },

  plugins: [
    new HtmlWebpackPlugin()
  ]
}

模块热替换与热加载

模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中, 替换、添加或删除 模块,而无需重新加载整个页面

热加载(文件更新时,自动刷新我们的服务和页面,不用手动去F5刷新), 新版的webpack-dev-server 默认已经开启了热加载的功能。 它对应的参数是devServer.liveReload,默认为 true。

devServer: {
 hot:false,
 liveReload: false, //默认为true,即开启热更新功能。 
},

注意,如果想要关掉它,要将liveReload设置为false的同时,也要关掉 hot

eslint

npm install eslint --save-dev
npm install eslint-webpack-plugin --save-dev

后把插件添加到你的 webpack 配置。例如:

const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // ...
  plugins: [new ESLintPlugin(options)],
  // ...
};

Webpack 模块

1、webpack 模块

何为 webpack 模块 能在webpack工程化环境里成功导入的模块,都可以视作webpack模块。 与 Node.js 模块相比,webpack 模块 能以各种方式表达它们的依赖关系。下面是 一些示例:

  • ES2015 import 语句
  • CommonJS require()
  • 语句 AMD define 和 require
  • 语句 css/sass/less 文件中的 @import 语句
  • stylesheet url(...) 或者 HTML <img src=...> 文件中的图片链接

支持的模块类型 Webpack 天生支持如下模块类型:

  • ECMAScript 模块
  • CommonJS 模块
  • AMD 模块
  • Assets WebAssembly 模块

而我们早就发现——通过 loader 可以使 webpack 支持多种语言格式和预处理器语法编写的模块。loader 向 webpack 描述了如何处理非原生模块,并将相关依赖引入到你 的 bundles中。包括且不限于:

TypeScript(.ts) Sass(.sass) Less(.less) JSON(.json)

2、compiler与Resolvers

在我们运行webpack的时候(就是我们执行webpack命令进行打包时),其实就是相当 于执行了下面的代码:

const webpack = require('webpack'); 
const compiler = webpack({
 // ...这是我们配置的webpackconfig对象 
})

webpack的Resolvers解析器的主体功能就是模块解析,它是基于 enhanced-resolve 这个包实现的。换句话讲,在webpack中,无论你使用怎样的 模块引入语句,本质其实都是在调用这个包的api进行模块路径解析。

模块解析(resolve

webpack通过Resolvers实现了模块之间的依赖和引用。举个例子:

import _ from 'lodash'; // 或者 const add = require('./utils/add');

所引用的模块可以是来自应用程序的代码,也可以是第三方库。 resolver 帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。 当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。

1、webpack中的模块路径解析规则

通过内置的enhanced-resolve,webpack 能解析三种文件路径:

绝对路径

import '/home/me/file'; import 'C:\\Users\\me\\file'; 注意:/指向根目录

相对路径

import '../utils/reqFetch'; import './styles.css';

模块路径

import 'module'; import 'module/lib/file';

node_modules里的模块已经 被默认配置了)。 你可以通过配置别名的方式来替换初始模块路径, 具体请参照下面 resolve.alias 配置选项

resolve解析优化

alias:自定义配置模块路径

extentions:定义 文件/目录 的路径 解析规则。

const path = require('path')
module.exports = {
  mode: 'development',
  entry: './src/app.js',

  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      'components': path.resolve(__dirname,'src/components'),
      "@utils": path.resolve(__dirname, 'src/utils/')
    },

       // extensions: ['.js', '.json', '.wasm']   webpack 默认配置
    extensions: ['.json', '.js', '.vue']
  }
}

配置完成之后,我们在项目中就可以

// 使用 src 别名 ~ 
import '~/fonts/iconfont.css'

// 使用 src 别名 @ 
import '@/fonts/iconfont.css'

// 使用 components 别名
import footer from "components/footer";

如果用户引入模块时不带扩展名,例如

import file from '../path/to/file';
复制代码

那么 webpack 就会按照 extensions 配置的数组从左到右的顺序去尝试解析模块

externals

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。减小bundle的体积,从而把一些不变的第三方库用cdn的形式引入进 来,比如jQuery:

例如,从 CDN 引入 jQuery(本地node_modules中不含这个包),而不是把它打包:

  1. 引入链接
<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"
></script>
复制代码
  1. 配置 externals
const config = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};
复制代码
  1. 使用 jQuery
import $ from 'jquery';

$('.my-element').animate(/* ... */);
复制代码

构建结果分析

借助插件 webpack-bundle-analyzer 我们可以直观的看到打包结果中,文件的体积大小、各模块依赖关系、文件是够重复等问题,极大的方便我们在进行项目优化的时候,进行问题诊断。

  1. 安装
$ npm i -D webpack-bundle-analyzer
复制代码
  1. 配置插件
// 引入插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin


const config = {
  // ...
  plugins:[ 
    // ...
    // 配置插件 
    new BundleAnalyzerPlugin({
      // analyzerMode: 'disabled',  // 不启动展示打包报告的http服务器
      // generateStatsFile: true, // 是否生成stats.json文件
    })
  ],
};
复制代码
  1. 修改启动命令
 "scripts": {
    // ...
    "analyzer": "cross-env NODE_ENV=prod webpack --progress --mode production"
  },
复制代码
  1. 执行编译命令 npm run analyzer

打包结束后,会自行启动地址为 http://127.0.0.1:8888 的 web 服务,访问地址就可以看到

image.png

Tree-shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码 (dead-code)。

Webpack 没看到你使用的代码。Webpack 跟踪整个应用程序的 import/export 语句,因此,如果它看到导入的东西最终没有被使用,它会认为那 是未引用代码(或叫做“死代码”—— dead-code ),并会对其进行 tree-shaking 。

webpack 的生产环境默认支持Tree-shaking

缩小范围

在配置 loader 的时候,我们需要更精确的去指定 loader 的作用目录或者需要排除的目录,通过使用 includeexclude 两个配置项,可以实现这个功能,常见的例如:

  • include:符合条件的模块进行解析
  • exclude:排除符合条件的模块,不解析

例如在配置 babel 的时候

const path = require('path');

// 路径处理方法
function resolve(dir){
  return path.join(__dirname, dir);
}

const config = {
  //...
  module: { 
    noParse: /jquery|lodash/,
    rules: [
      {
        test: /\.js$/i,
        include: resolve('src'),
        exclude: /node_modules/,
        use: [
          'babel-loader',
        ]
      },
      // ...
    ]
  }
};

多进程配置

thread-loader

配置在 thread-loader 之后的 loader 都会在一个单独的 worker 池(worker pool)中运行

  1. 安装
$ npm i -D  thread-loader
复制代码
  1. 配置
const path = require('path');

// 路径处理方法
function resolve(dir){
  return path.join(__dirname, dir);
}

const config = {
  //...
  module: { 
    noParse: /jquery|lodash/,
    rules: [
      {
        test: /\.js$/i,
        include: resolve('src'),
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader', // 开启多进程打包
            options: {
              worker: 3,
            }
          },
          'babel-loader',
        ]
      },
      // ...
    ]
  }
};

压缩 JS

在生成环境下打包默认会开启 js 压缩,但是当我们手动配置 optimization 选项之后,就不再默认对 js 进行压缩,需要我们手动去配置。

因为 webpack5 内置了terser-webpack-plugin 插件,所以我们不需重复安装,直接引用就可以了,具体配置如下

const TerserPlugin = require('terser-webpack-plugin');

const config = {
  // ...
  optimization: {
    minimize: true, // 开启最小化
    minimizer: [
      // ...
      new TerserPlugin({})
    ]
  },
  // ...
}

阅读剩余
THE END