webpack 全面入门

207 阅读18分钟

一直都在用使用 webpack 打包vue应用,但是从来没有详细了解过 webpack 是什么,所以用了4天的时间看完了一套 Webpack 视频教程,通过一步步的代码实现,我现在对 webpack 有了比较全面而清晰的了解,遂将自己学到的知识整理成这篇文章,希望能帮到也在学习 webpack 的你

概念

webpack 是什么

Webpack 是一个静态模块打包工具

在实际的项目开发过程中,会产生很多文件,比如 js 文件、css 文件、html文件,还有 ttf 文件、png 文件等等,这些文件都是项目要用到的文件而且它们之间存在依赖关系;比如一个 html 文件引入了一个 css 样式文件、一个外部的 js 文件,这些依赖关系往往很复杂,造成的结果就是,整个项目运行之前,要不断地去查找这些松散的文件,然后把它们整合到一起然后才能运行。

我们可以把这些松散的文件看成一个个静态模块,为了减少项目运行过程中耗费在聚合这些文件上的时间,我们要想个办法把**该合并的合并(减少 http 请求的次数),该压缩的压缩(去除代码中无用的字符,减少体积),该转换的转换(比如把 sass 转为 css)**等等,这一系列优化工作我们可以手动完成,无非就是一个一个文件的去处理,最后可以得到想要的项目,但是工作量将会非常大,而且非常低效

webpack 就提供了优化项目的能力,它可以基于项目中每个文件之间的关系,构建出一个依赖关系图,据此就能找到所有的静态资源;然后,通过调用 loader plugin 等自带的处理工具,就能执行上述所有的优化操作,最后将处理好的项目输出到一个文件中,最重要的是——这一切全是自动化的,具体细节我们不需要关心,我们只要给 webapck 指定一个入口(entry)(从哪里开始处理文件)和出口(output)(在哪里输出处理好的文件),再配置好 loaderplugin就可以了!

webpack 术语解释

module

在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块

每个模块具有比完整程序更小的接触面,使得校验、调试、测试轻而易举。 精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。

Node.js 从最一开始就支持模块化编程。然而,在web 中模块化的支持正缓慢到来。在 web 中存在多种支持 JavaScript 模块化的工具,这些工具各有优势和限制。webpack 基于从这些系统获得的经验教训,并将模块的概念应用于项目中的任何文件。

​ —— Webpack 官方文档

我个人的理解,在 webpack 中 module 的概念与 ES6 中的 module 、CommonJS 中的 module 都是相似的,它指的就是模块化

chunk

这是 webpack 特定的术语,它被用在 webpack 内部,来管理构建(building)的过程。bundle 由 chunk 组成,其中有几种类型(例如,入口 chunk(entry chunk) 和子 chunk(child chunk))。通常 chunk 会直接对应所输出的 bundle,但是有一些配置并不会产生一对一的关系

​ —— Webpack 官方文档

webpack会从你定义的入口开始,一个个的找到 module 然后构建依赖树,最终输出一个 chunk。它就像一个装着很多文件的文件袋,里面的文件就是各个模块,Webpack在外面加了一层包裹,从而形成了chunk。

bundle

由多个不同的模块生成,bundles 包含了已经过加载和编译的最终源文件版本

bundle 其实就是一个chunk打包后的输出产物,每个工程里都可以定义多个 entry ,那么就有多个 bundle。

安装

安装 webpack 有两种方式:全局安装和本地安装(这些都是 npm 的知识,可以参考这篇文章) ,我的结论是——本地安装

进入项目文件夹,然后运行下面的命令

npm i webpack webpack-cli -D

它安装了两个包,webpack 是核心包,webpack-cli 是通过命令行操作 webpack 执行的包

核心

Entry

​ entry的作用是告诉 webpack 从哪个文件开始处理,从而分析构建出依赖关系图

Output

​ output 的作用是告诉 webpack 构建并打包成功之后,往哪里输出打包好的资源文件

Loader

​ webpack 本身只能理解 Javascript,所以 loader 的作用是赋予 webpack 处理非 Javascript 文件的能力,也就是“翻译能力”,比如处理 less、sass、stylus、ts等等

Plugins

​ 执行一些 loader 无法完成的更复杂的任务,比如代码的压缩,静态资源的打包操作等等

Mode

​ 使 webpack 启动不同的的模式:

  • development——能让代码本地调试并运行的环境(不压缩)
  • production——能让代码优化上线运行的环境(压缩)

开发环境用法

打包命令

首先,新建一个目录作为你的项目的根目录,再创建src目录,然后在其中新建 index.js 文件

webpack ./src/index.js -o ./build/built.js --mode=development

在开发环境下,使用这条命令来告诉 webpack :

  • 路径 ./src/index.js 被定义为入口文件,webpack 就从这个 index.js 文件开始,构建依赖关系图
  • 参数 -o 表示的就是output 出口的意思
  • 路径 ./build/built.js 被定义为出口文件,webpack 打包完成后,就会输出到这个 built.js 文件
  • 选项 --mode=development 定义当前环境为开发环境,也就是代码只会被打包,而不会被压缩和优化,另一个选项是 --mode=production 定义环境为生产环境,也就是代码会被打包、压缩、优化,准备发布到线上运行了

处理 js、json

webpack 本身只能理解 Javascript,所以,不需要任何额外插件或loader 就能处理 js 文件

// data.json
{
    "name":"jack",
    "age": 18
}
// index.js
import data from './data.json'

console.log(data)

function add(x, y) {
    return x+y
}

console.log(add(1,2))

对上面的文件执行打包后,浏览器控制台就会打印出 add函数的返回值和 json 中的数据,这说明 webpack 成功打包了 js、json 文件。在 出口文件 built.js 中也可以看到打包后的代码,所有的代码都被打包在了一个 eval 函数中,包括注释与缩进

webpack.config.js

我们每次都使用这样的上面的命令就会太繁琐了,因为还要带上路径参数,还要声明使用的 loader 参数,为了方便,我们在根目录创建一个配置文件来存储我们的配置,然后在终端使用webpack命令就能执行打包

// webpack.config.js

const { resolve } = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        // 输出文件名
        filename: 'built.js',

        /*
        * 输出路径
        * 为了保证不出错,需要使用绝对路径,就需要导入 node.js 的 path 模块
        * 使用其中的 resolve 来拼接路径
        */ 
        path: resolve(__dirname, 'build') //  __dirname 是 node.js 内置变量,代表当前文件的绝对路径
    },
    // loader 的配置,定义了使用哪些 loader 来处理的不同类型的文件
    module:[
        // 多个loader的写法
        {
            // 处理规则
            test: '',
            // 声明要使用的loader
            use:['loader1', 'loader2',.....]
        },
        //单个loader的写法
        {
            test:''
            loader:''
        }
    ],
    // 插件,用于处理更为复杂的打包操作,比如压缩、优化
    plugins:[]
    // 模式,此为必填项,可选值:production,代表生产环境
    mode: 'development'
}

处理 css 文件

css-loaderstyle-loader 用于处理 css 文件。首先要安装它们

npm i css-loader style-loader -D

然后在 module 的 rules中定义如何去使用它们:

// webpack.config.js

const { resolve } = require('path')

module.exports = {
    // loader 的配置,用于处理非 js 文件,比如 css、img
    module: {
        rules: [
            {
                // 使用正则表达式来指定要匹配文件的类型
                test: /\.css$/,
                // 使用哪些 loader
                use: [
                    // 执行顺序是 从下到上(从右到左)
                    'style-loader', //生成 style 标签,将样式插入其中
                    'css-loader' // 将 css 文件编译成 commonJS 模块加载到 js 中,里面的内容是 样式字符串
                ]
            }
        ]
    }
}

然后使用命令 webpack 打包运行,查看出口文件 built.js,其中的 eval 函数记录了 css 代码被打包成了字符串形式

  • css-loader 的作用是将 css代码转写成样式字符串
  • style-loader 的作用是创建 <style> 标签,将样式字符串插入到HTML页面 head 标签中

处理 图片

url-loader 用于处理 CSS 文件中引入的图片,html-loader 用于处理HTML中引入的图片

要先安装 file-loader,因为 url-loader 依赖于它

npm i url-loader file-loader html-loader -D

同样,在 module 的 rules中定义如何去使用它们:

// webpack.config.js

const { resolve } = require('path')

module.exports = {
    module: {
        rules: [
            // 处理 图片 的loader(url-loader 和 file-loader)
            {
                test: /\.(jpg|png|gif)$/,
                // url-loader 只能处理 CSS 中引入的图片,默认以 commonJS 模块处理
                loader: 'url-loader',
                options: {
                    /**图片小于 8kb 就会被转为 base64 格式
                     * 优点:减少http请求,减轻服务器压力
                     * 缺点:图片体积会变大
                     *  */ 
                    limit: 8 * 1024
                }
            },
            {
                test: /\.html$/,
                // 只能处理 HTML中的图片,处理成commonJS 模块,然后交给 url-loader 处理
                loader: 'html-loader'
            }
        ]
    }
}

打包之后的图片,会被重命名,新的命名是由 webpack 生成的唯一的32位hash值,例如:

830bf3c820562c180c8975c2a0d00557.jpg

为了方便,我们不需要给图片这么长的命名,所以我们修改下配置文件,在 options中的limit 后面增加一个 name 选项

// hash:10 表示只取前10位值,ext 表示源文件的后缀名
name:'[hash:10].[ext]'

再次打包以后,新的图片的名字就会更短:

830bf3c820.jpg

处理 html 文件

html-webpack-plugin 是一个插件,专门用于处理 HTML 文件

npm i html-webpack-plugin -D
// webpack.config.js

const { resolve } = require('path')

// HtmlWebpackPlugin是一个构造函数
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    plugins: [
        /**
         * html-webpack-plugin
         * 功能:默认会创建一个空的HTML,自动引入打包输出的所有资源(js/css)
         * 需求:需要有一个已经写好的 html 作为模板
         */
        new HtmlWebpackPlugin({
            // 复制一个 './src/index.html' 文件,保持原有的结构,自动写入外部引用
            template: './src/index.html'
        })
    ],
    mode: 'development'
}

使用命令 webpack 执行打包后,会在出口 build 文件夹下面生成一个新的HTML文件,其中自动引入了 built.js

处理其他文件

其他文件就是指:除了 css、html、js 以外的文件,比如 font(字体)、SVG图片、icon(图标)等等

这些文件,统一用 file-loader 来处理

module.exports = {
    module: {
        rules: [{
                // 使用 exclude 排除不需要打包的资源
                exclude: /\.(css|html|js)$/,
                loader: 'file-loader',
                options: {
                    name: '[hash:10].[ext]'
                  }
            }]
    }
}

webpack-dev-server

用于在开发时,自动编译、自动打包项目、自动打开并显示结果到浏览器(省去了我们手动打包的麻烦),它会在内存中进行编译,所以速度非常快

// 安装 dev derver 功能
npm i webpack-dev-server -D

dev server 不属于 webpack 的五大核心之一,它要单独配置,代码如下:

module.exports = {
	entry: ...,
	output: ...,
	module: [],
	plugin: ...,
	mode: ...,

	devServer: {
		contentBase: resolve(__dirname, 'build'), // 项目构建后的输出路径
		compress: true, // 是否压缩代码
		port: 3000, // 服务器的端口号
         open: true // 是否自动打开页面到浏览器中
	}
}

使用指令来启动 devServer 服务

npx webpack-dev-server

最后,由于 devServer 只在内存中编译你的代码,所以它在本地不会有任何输出,你可以删除 /built 文件夹,然后再次运行 devServer,你会发现一切正常,只是本地没有任何文件会被创建

生产环境用法

打包CSS为单独文件

使用 style-loader 处理CSS文件,会将CSS转为字符串写入Javascript代码中,如果CSS的体积过大,那么页面就会产生闪屏现象,而且也会导致Javascript文件体积增大,也增加了浏览器解析js的时间

mini-css-extract-plugin 是一个Plugin,它可以将CSS打包成单独的文件

npm i mini-css-extract-plugin -D

使用 mini-css-extract-plugin 以及 MiniCssExtractPlugin.loader

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                // 不再需要style-loader将css字符串注入Javascript,取代 style-loader
                use: [MiniCssExtractPlugin.loader, 'css-loader']
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            // 给打包出的CSS文件重命名
            filename: 'css/built.css'
        })
    ]
}

运行打包命令后,查看 build 文件夹,就能看到输出的 css 文件

CSS兼容性处理

我们知道,有些浏览器是不完全支持所有 css 属性的,所以我们需要写上适配不同浏览器的 css 属性,比如加上前缀moz-,就表示适配 FireFox 浏览器;但是浏览器的种类很多,我们不可能每一个属性都手动去写,因为工作量将会巨大。

webpack 提供了 postcss-loaderpostcss-preset-env 来处理 css 的兼容性,它们能在输出的 css 中帮我们加上这些兼容的属性。首先安装它们:

npm i postcss-loader postcss-preset-env -D

现在,我们要去package.json 文件中定义一个属性 browserlist ,它规定了如何去兼容浏览器,下面是一段示例代码:

// package.json
"browserslist": {
    // 开发环境
    "development": [
      "last 1 chrome version", // 适配最新版本的 chrome 浏览器
      "last 1 firefox version", // 适配最新版本的 firefox 浏览器
      "last 1 safari version" // 适配最新版本的 safari 浏览器
    ],
    // 生产环境
    "production": [
      ">0.2%", // 适配市面上99.8%以上的浏览器
      "not dead", // 排除以及淘汰的浏览器,比如 IE5
      "not op_mini all"
    ]
 },

接下来我们还要设置一下 node.js 环境变量,因为默认情况下会应用的browserlist配置是生产环境配置,也就是上面的 development,我们先来看看开发环境下,postcss会如何处理 css 兼容性

// webpack.config.js

process.env.NODE_ENV = 'development'

开发环境下经过 css 兼容性处理的输出文件如下:

接着,我们再来看看生产环境下,css 兼容性处理的情况,我们将 package.json 中的process.env.NODE_ENV = 'development'注释掉,然后再次打包,就会输出如下的样式:

通过开发环境和生产环境对比,我们可以发现,使用生产环境的 browserlist 配置,处理 css 兼容性的时候会更加的全面和严格

压缩CSS

有时候 css 样式可以多达几百行,其中有不少空白字符,为了精简 css 文件的体积,我们需要将其压缩,删去空白字符。optimize-css-assets-webpack-plugin 插件就提供了压缩 css 的功能

npm i optimize-css-assets-webpack-plugin -D

直接调用这个插件就可以了:

 // webpack.config.js
 
 plugins: [
        new OptimizeCssAssetsWebpackPlugin()
 ]

这时,你可以打开输出的 CSS 文件,里面的代码非常紧凑,只有一行,没有任何多余的字符

JS语法检查

Javascript 语法检查的作用就是规范化我们的源代码,检查出其中的语法错误,统一变成风格,比如字符串,要么就全部使用双引号,要么就全部使用单引号,不要一会儿用这个一会儿用那个,导致代码看起来非常不美观、不协调。

webpack 中运行 JS 语法检查用主要靠 ESLint

npm i eslint-loader eslint eslint-config-airbnb-base eslint-plugin-import -D

为了获得统一的代码风格,我们就需要一个通用的 模板 规范化我们的代码,通常呢,我们会使用airbnb-base这个代码风格,具体细节,可以查看它的文档

安装完成后,我们还需要在 package.json 中配置 eslint :

"eslintConfig": {
    "extends": "airbnb-base",  // 继承于 airbnb-base
    "env": {
      "browser": true
    }
 }

接下来在 webpack.config.js 文件中,使用 eslint-loader

module: {
        rules: [
            {
                test: /\.js$/,  // 只检查 js 文件
                exclude: /node_modules/,  // 排除掉 node 包,因为它们都是第三方js库
                loader: 'eslint-loader'
            }
        ]
    }

现在我们来写一段Javascript代码,故意写成风格混乱,看起来像这样:

然后运行 webpack 打包,我们可以控制台会报错,然后输出如下信息:

上面展示了部分输出,它们都来自于 ESLint 语法检查器,提示你,哪里不该换行却换行了,哪里使用了多余的空格;但手动的一行行的去修复太麻烦了,所以我们可以让 ESlint 自动修复它,要在webpack.config.js 文件中加上 options 配置:

module: {
        rules: [
            {
                test: /\.js$/, 
                exclude: /node_modules/,  
                loader: 'eslint-loader',
                options: {
                	fix: true // 启用自动修复
                }
            }
        ]
    }

这时,打包以后,我们再打开 index.js 文件,就可以看到,里面的代码已经被自动的规范化了

JS兼容性处理

基本 js 兼容性处理

我们知道,ES6 语法目前只有主流的浏览器(Chrome、FireFox)才支持,而且支持的还不是特别全面。比如下面这段代码,在 Chrome 中可以正常运行,但是在 IE 11 中就会报错

// 使用 ES6 中的箭头函数语法
const message = () => {
    console.log('Hello World')
}

message()

上面这种情况就是 js 不兼容了,为了使我们写的 js 代码兼容大部分浏览器,我们还得借助于专门的第三方库——Babel,它可以将我们写的 ES6 代码,转译成同等意义的 ES5 代码,这样就能让那些老旧的浏览器也能正常运行我们的 js 代码。

在 webpack 中使用 babel 很方便,只需要安装 babel-loader 以及其附带的包

npm i babel-loader @babel/core @babel/preset-env -D

然后配置 babel

// webpack.config.js

module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env'] // 使用预定义的兼容性方案
                }
            }
        ]
    },

这次,将输出的 index.html 用 IE 运行,就不会再有报错了

全部 js 兼容性处理

上面的 babel 配置只能处理基本的 Javascript 语法,如果遇到更高级的语法(比如 Promsie ),是无法处理的。我们使用 ES6 中的 Promise 来写一段代码:

const promise = Promise.resolve("Hello World")

promise.then(function (p) {
    console.log(p)
})

在 IE 中运行就会报错,因为 IE 根本不支持 Promise

为了让 IE 也兼容 Promise ,所以我们要安装并使用**@babel/polyfill**

npm i @babel/polyfill -D

它的使用非常简单,直接在 js 文件的头部引入就可以了

// index.js
import '@/babel/polyfill'
 
const promise = Promise.resolve("Hello World")

promise.then(function (p) {
    console.log(p)
})

然后我们再次打包,运行到 IE,这次就不会再有报错了。@babel/polyfill 这个库所做的工作就是:接管了代码中所有的高级语法,然后转译为 ES5 及以下的实现,这样 IE 就能运用 Promise 了

值得注意的是:

  • 引入 @babel/polyfill 之前打包的文件大小

  • 引入 @babel/polyfill 之后打包的文件大小

前后对比发现,居然变大了许多,513KB;这是因为引入的 @babel/polyfill 包含了所有的高级语法兼容性处理的实现,所以体积才会这么大

部分 js 兼容性处理

上面的例子中,我们只是使用了一个 Promise ,就引入了体积500多K的库文件,这样未免有点浪费空间。如果能按需引入就好了,这样将不会引入多余的库文件,占用空间。

corejs 就可以提供这样的能力:

npm i core-js -D

在使用 core-js 之前,记得先要注释掉 index.js 中的 @babel/polyfill

// webpack.config.js
module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                options: {
                    presets: [
                        [
                            '@babel/preset-env',
                            {
                                useBuiltIns: 'usage', // 启用按需引入
                                corejs: {
                                    version: 3 // 指定 core-js 的版本
                                },
                                targets: {
                                    chrome: '60',
                                    firefox: '60',
                                    safari: '10',
                                    ie: '9',
                                    edge: '17'
                                }
                            }
                        ]
                    ]
                }
            }
        ]
    }

配置完 core-js 以后,我们再来打包运行,就会发现体积相比于使用 @/babel/polyfill 要少了很多

js和html压缩

要压缩 js 文件,只需要将 mode 的值设置为 production,这样 webpack 就会在打包的时候自动压缩 js 文件。html 文件的压缩则需要用到 html-webpack-plugin 这个插件,在其中配置之后,就能实现 html 的压缩

// webpack.config.js

mode: 'production',
plugins:[
	new HtmlWebpackPlugin({
		// 配置 html 压缩
		minify: {
			collapseWhitespace: true,  // 去掉多余的空格
			removeComments: true  // 移除注释
		}
	})
] 

html-webpack-plugin 的用法远不止文章提到的这几种,更具体详细的用法,请参考文档

webpack性能优化

HMR

通常一个项目中会有很多个CSS文件和JS文件,还有很多其他的文件,但唯独HTML文件只有一个(通常情况下),当我们启动了webpack-dev-server的服务以后,我们修改任何文件,它都会将所有存在于依赖关系图中的文件,自动编译并刷新到页面上,这非常的方便

但是,你是否想过一个问题——当你的项目中有10个JS文件的时候,你修改其中一个,那么这10个文件都会被重新编译;当你的项目中有10000个JS文件的时候,你只修改其中一个,那么这10000个文件还是都会被重新编译;这就造成了性能上的浪费,我只修改了一个,何必要重新编译所有的文件呢?

能不能只是重新编译我们修改的那个文件,而没修改的就原封不动不用管它?

HMR(Hot Module Replacement)模块热替换,是 webpack-dev-server 提供的功能,它就是用来实现上述需求的。它可以只编译我们修改过的文件,而其余文件则不会编译,这样就不必去做那些重复的工作!要使用它,只需要在 devServer中配置:

// package.json

devServer: {
        contentBase: resolve(__dirname, 'build'),
        compress: true,
        port: 3000,
        open: true,
        hot: true // 启用 HMR 功能
  }

比如,我们的项目中有一个 a.css 文件,在启用HMR之后,我们修改它,然后 webpack-dev-server 就会只编译它而不会编译没有改动的模块,在控制台中可以看到 HMR 的输出信息,它显示了热替换的是哪个模块:

注意:

  1. 样式文件默认支持 HMR,因为 style-loader 中实现了

  2. JS 文件默认情况下是不支持 HMR 的,当你开启了 HMR,就算你只修改了一个样式文件,所有的JS文件还是会被全部编译

  3. HTML则没有必要使用HMR,因为它只有一个。但是,开启HMR之后HTML页面将不能自动刷新,除非手动刷新页面。

下面,我们就来解决这些问题:

  • 为了使HTML页面在我们修改后自动刷新,我们需要在 entry 中加入 index.html

    entry: ['./src/index.js', './src/index.html']
    
  • 为了使 JS 文件也支持 HMR,我们需要在 index.js 中配置:

    // print.js
    export default function print() {
        console.log('print.js 文件运行了')
    }
    
    // index.js
    import print from './print'
    
    // 如果 module.hot 为 true,说明启用了 HMR
    if (module.hot) {
        // 让 JS 也支持 HMR
        module.hot.accept('./print.js', function () {
            print() // 执行一个函数,来体现当前的 JS 文件被重新编译了,而其他的文件则没有
        })
    }
    

现在控制台就会显示出 HMR 的输出信息,当我们只修改 print.js 文件,那么就只有 print.js 文件会被重新编译

source-map

我们使用 webpack 构建并打包项目之后,运行的都是构建后的代码;要知道,构建后代码将非常不适合程序员阅读(为了适合机器去阅读),所以代码错误调试就是个大问题。假如构建后代码运行出错了,我们将很难定位到出错位置

souce-map 提供了构建后代码到源代码的映射,这样就能准确的显示代码错误以及提示信息,极大的方便我们调式错误,它也是在 webpack.config.js 中配置的,它有着很多种用法,下面一一列举:

   devtool: 'source-map'
// devtool: 'inline-source-map'
// devtool: 'hidden-source-map'
// devtool: 'eval-source-map'
// devtool: 'nosources-source-map'
// devtool: 'cheap-source-map'
// devtool: 'cheap-module-source-map'

运行 webpack 打包之后,就可以看到在输出目录中,多了一个built.js.map 文件,其中记录源代码于构建后代码的映射关系

source-map 有很多种用法:

  • source-map & inline-source-map

    source-map会将生成的映射代码集中在一起,输出built.js.map 中;inline-source-map 则是内联到 built.js中,优点是构建速度快。它们都能够准确的指示出源代码中错误发生的位置

  • hidden-source-map & nosources-source-map

    能够提示错误信息,但是无法追踪到源代码;它们两个的作用就是隐藏源代码

  • eval-source-map

    能够提示错误,以及定位到精准的源代码位置。不同点在于:它分别为每个文件生成对应的内联 source-map ,都包含在 eval 函数

  • cheap-source-map & cheap-module-source-map

    都能提示错误信息,但是只能定位到错误发生的代码行,而不能精确到错误语句(以上几种都能精确到语句)

之所以 source-map 提供了这么多的用法,就是为了适应不同的环境;有的方式速度快,有的方式对调试友好...我们在开发的时候可以按需使用,这样才能最方便我们的开发。在开发环境下,我们需要的可能是对调试友好,在生产环境下我们需要的则可能是速度快、隐藏源代码

从构建速度来看,source-map 的速度从快到慢依次是:eval-source-map > inline-source-map > cheap-source-map;从提示错误信息的精度(是否对调试友好)来看,从高到低依次是 source -map> cheap-module-source-map > cheap-source-map

所以,兼顾到速度快与易于调试两个方面,建议使用 eval-source-map

oneOf

webpack中loader的处理机制是:将当前要处理的文件匹配列表中所有的 loader,比如有一个js文件,rules中定义了10个loader,第一个是处理js文件的loader,当第一个loader处理完成后,webpack不会自动跳出,而是会继续拿着这个js文件去尝试匹配剩下的9个loader,相当于 switch 语句没有break。

而 oneOf 的作用就相当于break,它在匹配到第一个正确的 loader 以后,就不再继续去匹配 rules 中其余的 loader ,这样就能节省一部分开销。示例如下:

rules: [
    {
        oneOf: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.(jpg|png|gif)$/,
                loader: 'url-loader',
                options: {
                    limit: 8 * 1024,
                    name: '[hash:10].[ext]',
                    esModule: false
                }
            },
            {
                test: /\.html$/,
                loader: 'html-loader',
            },
            {
                exclude: /\.(html|css|js|png|gif)$/,
                loader: 'file-loader',
                options: {
                    name: '[hash:10].[ext]',
                }
            }
        ]
    }
]

**注意:**在 oneOf 中,不能有两个 loader 都用来处理同一类型的文件,比如 eslint-loaderbabel-loader 都处理 js 文件,但是 oneOf 中,只会匹配一个(只有一个能生效);如果要使两个 loader 都生效,就要拿出一个 loader 放到 oneOf 外面,就像下面这样:

rules: [
    // eslint-loader 在 oneOf 外面,会被第一个匹配到
    {
        test: /\.js$/,
        exclude: /node_modules/,
        enforce: 'pre',  // enforce 选项表示,优先匹配
        loader: 'eslint-loader',
        options: {
            fix: true
        }
    },
    {
        oneOf: [
            // babel-loader 同样也能匹配到,因为 oneOf 中只有它这一个 loader 用于处理js
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                options: {.....}
            },
            {
                exclude: /\.(html|css|js|png
                loader: 'file-loader',
                options: {
                    name: '[hash:10].[ext]',
                }
            }
        ]
    }
]

缓存

babel缓存

babel-loader 在处理项目中 js 文件时存在着一个问题:无论文件有没有被修改,都会被 babel 编译;也就是说,100个 js 文件中,只修改了1个,但是其余99个 js 文件还是会被 babel 处理;这与 webpack-dev-server 中产生的问题非常相似。

babel-laoder 没有 HMR 功能,但是它有缓存的功能:将上一次处理好的文件全部缓存起来,下一次调用时只处理那些修改过的文件,而没有修改过的文件直接调用缓存中的结果

在 babel-loader 中使用缓存功能,只要一个配置——cacheDirectory

test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
    presets: [......],
    cacheDirectory: true  // 开启缓存功能
}

文件资源缓存

使用 webpack 来打包项目, webpack 会把打包后的内容放置在 /dist 目录中,只要把 /dist 目录中的内容部署到服务器 server 上,浏览器就能够访问它。而在载入项目的过程中,获取资源是比较耗费时间的,所以浏览器使用了 缓存 。浏览器可以通过使用缓存,以降低网络流量,使网站加载速度更快

打开浏览器控制台的 Network 选项,就可以看到当前网站资源的缓存情况

通常,浏览器会将当前版本的资源文件缓存起来,以供下次使用(缓存起来就不用再去请求),然而,如果在部署新版本的项目时不更改资源的文件名,浏览器会认为它没有被更新,就会直接使用它的缓存版本,我们看到的结果就是:虽然更新了代码,但是页面上并没有发生变化。

为了确保 webpack 编译生成的文件能被浏览器缓存,而且在文件内容变化后,还能够请求到最新的文件从而在页面上显示,我们就要通过一些手段来实现文件资源缓存的更新,比如每次 webpack 打包后都给文件重命名,这样只要浏览器发现文件名被修改了,就不会再去使用缓存,而是请求最新的文件。

我们可以使用 hash 值来给文件重命名

hash

在输出的 built.js 文件名中植入一个 hash 值,达到修改资源文件名的目的

output: {
    filename: 'built.[hash:10].js',
    path: resolve(__dirname, 'build')
}

然后再次打包,你就会看到built.js的文件名发生了变化,比如built.2a310a49ad.js。其实这个 hash 值就是每次 webpack 打包成功后生成的那个 hash 值,也就是说:任何一个文件发生了修改,那么所有的文件命名中都会植入一个新的 hash 值

chunkhash

根据 chunk 来生成 hash 值

output: {
    filename: 'built.[chunkhash:10].js',
    path: resolve(__dirname, 'build')
}

chunkhash 与上面的不同点在于:hash 是以整个项目文件资源为范围,而 chunkhash 则是以同一个 chunk 为范围来命名文件的。例如一个 a.js 引入了 a.css ,那么它们在被打包时,就属于同一个 chunk。chunkhash 只会在属于同一个 chunk 的文件上生效,也就是说:在同一个 chunk 下的许多个文件,只要有一个修改了,那么所有的文件名都会被植入一个新的 hash 值

contenthash

根据文件内容来生成 hash 值

output: {
    filename: 'built.[contenthash:10].js',
    path: resolve(__dirname, 'build')
}

不同的文件,生成的 hash 值一定不同,也就是说:只要你修改了一个文件,那么它的命名就会包含新的 hash 值,其余的任何文件,只要没修改则命名不变

结合上面三种 hash 命名的方法来看,我们当然要选用 contenthash 啦,因为它的开销最小,粒度最小,所以是最精确、性能最好的!

tree shaking

假如,你从 a.js 文件中引入了两个函数 a 和 b,然后你只是使用了 a 函数,并没有使用 b 函数;这时,使用 webpack 打包,b 函数的代码也会被打包构建,即使它没有被使用——去除掉项目中无用的代码,对提高性能具有很大的帮助,不仅可以减小体积,还能节省编译这些代码的开销

想象一下秋天里,你站在一棵小树下,然后你摇晃这棵树会怎样?树上的枯枝败叶会掉落下来...

tree shaking 就是这个形象的比喻中的“摇树”的行为,我们的项目就好比一颗大树,那些无用的代码就是树上的枯枝败叶,通过“摇晃”项目——这颗大树,就能把那些无用的代码都去除掉——这就是 tree shaking 的作用

webpack 中如何启用 tree shaking 呢?很简单,只需要设置 mode = "production"

启用了 tree shaking 以后,webpack 打包的代码中,就不会再有那些无用(或引入了但是没有使用)的代码;同时,也有着一个问题,因为 webpack 并没有独立思考的能力,它默认情况下会把引入的 CSS 文件也“从树上摇下来”,所以,我们要通过配置,来告诉 webpack 那些文件是有用的,这样 tree shaking 的时候就不会把它们误删

package.json 中使用一个 sideEffects属性来定义

"sideEffect": false

// 或者

"sideEffect": ['*.css']

上面配置的含义是:

  • sideEffects 为 false 时,webpack 会认为所有文件都可以进行 tree hshaking ,这样那些显式使用的代码会保留,但显式引入,隐式调用的代码却会被清除;比如,外部引入的 @babel/polyfill 可能会被清除
  • sideEffects 为一个数组,其中写入要匹配的文件后缀名,比如上面代码中的*.css就表示所有的 CSS 文件都是有用代码(不需要 tree shaking处理),所以就不会被清除

最后,很重要的一个前提:tree shaking 只识别 ES6 Module,其他类型的模块它不能识别。更详细的用法在这里

代码分离

多入口

前面所使用的 webpack 打包后输出的 bundle 中,只有一个 built.js 文件,毫无疑问,它就是我们项目中所有 js 代码的集成,所以,它会很臃肿,因为所有的 js (可能还有 css 等文件)都在其中;我们需要将这个臃肿的文件分割开来,这样就能减小 bundle 的体积,便于加载,可以通过多入口来实现 bundle 的分离

entry: {
	// key是指定输出的JS文件的名称,value是入口文件
    main: './src/js/index.js',
    test: './src/js/test.js'
},

设置多入口打包之后,查看 webpack 的输出信息:分别输出了 main.jstest.js

不过,多入口虽然可以将最终打包输出的文件分解为多个小文件,但是它不能解决重复引用的问题,假如 a.js b.js中都引入了 jQuery,那么最后打包输出的两个文件中,每个都会包含 jQuery——这就是重复引用

SplitChunksPlugin

为了解决重复引用的问题,我们通过 SplitChunksPlugin来将文件之间公共引用的库文件,打包成单独的 bundle

  • index.js

    import $ from 'jQuery'
    
    console.log($)
    
  • test.js

    import $ from 'jQuery'
    
    console.log($)
    

通过 optimization属性来配置 SplitChunksPlugin:

optimization: {
    splitChunks: {
        // all 表示所有的 chunk 都要被SplitChunksPlugin处理
        chunks: "all"
    }
}

执行打包:

我们可以看到,main.js 和 test.js 都引入了 jQuery ,但是 jQuery 被单独打包成一个 bundle 文件,从文件大小也可以看出来。SplitChunksPlugin 更高级的用法在这里

import()

还有一种方法可以让 webpack 在打包的时候将一个 js 打包成单独的 bundle ——使用 ES6 的 import() 方法来导入一个外部 js 文件

  • index.js

    // 因为使用了 import() 方法,所以就不用再使用下面的语句
    // import { msg } from './test'
    
    import('./test').then((result) => {
        console.log(result.msg)
    }).catch((error) => {
        console.log(error)
    })test.js
    
  • test.js

    import $ from 'jQuery'
    
    console.log($)
    
    export const msg = 'Hello World'
    

现在执行打包:

可以看到,结果如我们所愿,每个 js 文件都被打包成了单独的 bundle;只是文件名发生了变化——都是以 chunk 的 id 来命名的,这样命名的坏处就是 id 仅仅是一个数字,不便于我们分辨

现在我们来让 webpack 给这些文件一个规范的命名,通过添加注释的方式

import(/*webpackChunkName: 'my_test'*/'./test').then((result) => {
    console.log(result.msg)
}).catch((error) => {
    console.log(error)
})test.js

再次打包:

个人觉得这种在代码中执行注释的方式不太“正常”,很奇怪

懒加载

所谓懒加载,就是用到的时候才被加载,没用到就不加载;就像妈妈喊你拖地,你才去拖地;妈妈没喊你拖地,你就懒着,不主动的去帮忙拖地。

下面用两个文件来演示,没使用懒加载的情况:

  • index.js

    console.log('index.js 被加载了')
    
    import { msg } from './test'
    
  • test.js

    console.log('test 被加载了')
    
    export const msg = 'Hello World'
    

    执行打包

我们来分析一下:index.js 虽然引入了 test.js ,但是并没有调用 test.js 中的变量,然而 test.js 中的 console 语句还是执行了!这说明——test.js 无论有没有被调用,它都被加载并执行了

显然,一下子把所有文件都加载(无论那个文件是否会被用到)不是个好的方式,至少对性能来说不是件好事情,所以这里,我们要想办法优化——通过 懒加载

还是用 ES6 的 import() 方法,它的返回值是一个 promise 对象,通过 then() 就能得到 import 导入的文件,我们改写 index.js 如下:

console.log('index.js 被加载了')

document.getElementById('btn').onclick = function () {
    import('./test').then((result) => {
        console.log(result.msg)
    }).catch((error) => {
        console.log(error)
    })
}

再次执行打包

可以看到,这次 test.js 没有被加载,它只有在点击了按钮以后才会被加载并运行,这就是懒加载

上面仅仅是一个简单的示例,懒加载是一个很好的性能优化手段,可以深入了解其文档

参考链接:www.bilibili.com/video/BV1e7…