深入浅出Webpack

5,150 阅读11分钟

核心概念

  • Entry:入口,Webpack执行构建的第一步将从Entry开始,可抽象成输入。
  • Module:模块,在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始递归找出所有依赖的模块。
  • Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于将模块的原内容按照需求转换成新内容。
  • Plugin:拓展插件,在wp构建流程中的特定时机注入拓展逻辑。
  • Output:输出结果,在Webpack经过一系列处理并得出最终想要的代码后输出结果.

Entry

入口,Webpack执行构建的第一步将从Entry开始,可抽象成输入。

单页面单入口文件配置

module.exports = {
	entry: './path/to/my/entry/file.js'
}

多页面多入口文件配置

module.exports = {
	entry: {
		one: './path/one.js', 
		two: './path/two.js'
	},
	plugins:[
		new HtmlWebpackPlugin({
			title:'第一个页面',
			template: './pages/one.html', // 指定第一个页面的模板
			filename: './pages/one.html', // 指定第一个页面打包完成后的文件名
			chunks: ['one','two'] // 指定第一个页面要打包进入的js
		}),	
		new HtmlWebpackPlugin({
			title:'第二个页面',
			template: './pages/two.html', // 指定第二个页面的模板
			filename: './pages/two.html', // 指定第二个页面打包完成后的文件名
			chunks: ['one','two'] // 指定第二个页面要打包进入的js
		}),	
	]
}

Webpack构建单页面和多页面的实例

网上关于单页面和多页面的优点和缺点都有比较详细的描述,具体需要应用单页面还是多页面得根据项目的需求来选择。

一般单页面的配置都有相应的脚手架,比如vue-cli,集成了wp,减少了配置webpack的很多繁琐的工作

多页面应用现在没有脚手架,可以进行配置,具体的实例可参考这篇:

juejin.cn/post/684490…

多页面想向单页面实现组件共用和封装,可以参考这篇进行配置,需要引入ejs模板,参考这篇:

juejin.cn/post/684490…

Module

模块,在Webpack里一切皆模块,一个模块对应一个文件。Webpack会从配置的Entry开始递归找出所有依赖的模块。 ####配置Loader

  • test:匹配要进行转换的文件,使用正则表达式来匹配。

  • include: 只包含指定目录的文件进行转换,加快webpack的编译速度。

  • exclude: 排除某个文件的转换,加快编译和搜索速度。

  • use:对use后面加参数,比如进行缓存和压缩,也可以加快编译的速度。

      module:{
      	rules: [
      		{
      			//解析js文件
      			test: /\.js$/,
      			// 用babel-loader转换js文件
      			 // ?cacheDirectory表示传给babel-loader的参数,用于缓存babel的编译结果,加快重新编译的速度
      			use: ['babel-loader?cacheDirectory']
      			 // 只命中src目录里的Js文件,加快webpack的编译速度
      			 include: path.resolve(_dirname,'src')
      		},
      		{
      			//解析Scss文件
      			test: /\.scss$/,
      			// 使用一组loader去处理scss文件
      			// 处理顺序为从后到前,即先交给scss-loader处理,再将结果交给css-loader,最后交给style-loader
      			use: ['style-loader','css-loader','sass-loader'],
      			// 排除node_modules目录下的文件
      			exclude: path.resolve(__dirname,'node_modules')
      		},
      		{
      			// 对非文本文件采用file-loader加载
      			test: /\.(gif|png|jpe?g|eot|woff|ttf|svg|pdf)$/,
      			use: ['file-loader']
      		}
      	]
      }
    

在上面的例子中,test、include、exclude只传入了一个字符串或正则,其实他们也支持数组类型

{
	test:[
		/\.jsx?$/,
		/\.tsx?$/
	],
	include:[
		path.resolve(__dirname,'src'),
		path.resolve(__dirname,'tests')
	],
	exclude:[
		path.resolve(__dirname, 'node_modiles'),
		path.resolve(__dirname, 'bower_modules')
	]
}

noParse

noPaese配置项可以让Webpack忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能提高构建性能。 原因是一些库如jQuery,ChartJS庞大又没有采用模块化的标准,让Webpack去解析这些文件既耗时又没有意义。

noParse: /jquery|chartjs/
使用函数,从Webpack3.0.0开始支持
noParse: (content) => {
	//content代表一个模块的文件路径
	//返回true或false
	return /jquery|chartjs/.test(content)
}

parse

因为Webpack是以模块化的js文件为入口的,所以内置了对模块化js的解析功能,支持AMD,CommonJS,SystemJS,ES6 parse属性可以更细粒度地配置哪些模块语法被解析,哪些不被解析。

同noParse配置项的区别在于.parser可以精确到语法层面,而noParse只能控制哪些文件不被解析。

parse的使用方法如下:

modele:{
	rules:[
		test: /\.js$/,
		use: ['babel-loader'],
		parser: {
			amd: false, //禁用AMD
			commonjs: false, // 禁用CommonJS
			system: false, // 禁用 SystemJS
			harmony: false, // 禁用ES6 import/export
			requireInclude: false, // 禁用requireInclude
			requireEnsure: false, // 禁用requireEnsure
			requireContext: false, // 禁用requireContext
			browserify: false,  // 禁用browserify
			requireJs: false // 禁用requirejs
		}
	]
}

Loader

模块转换器,用于将模块的原内容按照需求转换成新内容。

  • Loader的执行顺序是由后到前的。

  • 每Loader都可以通过URL querystring的方式传入参数,例如 css-loader?minimize中的minimize告诉css-loader要开启css压缩。

  • 向loader中传入属性的方式除了可以通过querystring实现,还可以通过object实现。

      user:[
      	'style-loader',{
      		loader:'css-loader',
      		options:{
      			minimize:true
      		}
      	}
      ]
    

在Loader需要传入很多参数时,我们还可以通过一个Object来描述,例如在上面的babel-loader配置中有如下代码

use:[
	{
		loader:'babel-loader',
		options:{
			cacheDirectory:true
		},
		// enforce: 'post'的含义是将该loader的执行顺序放到最后
		// enforce: 'pre'的含义是将loader的执行顺序放到最前面
	}
]

Plugin

拓展插件,在Webpack构建流程中的特定时机注入拓展逻辑。

Plugin的配置很简单,plugins的配置项接受一个数组,数组里的每一项都是一个要使用的Plugin的实例,Plugin需要的参数通过构造函数传入。

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin')
module.exports = {
	plugins: [
		// 所有页面都会用到的公共代码被提取到common代码块中
		new CommonsChunkPlugin({
			name: 'common',
			chunks: ['a','b']
		})
	]
}

将css文件单独提取出来

	const ExtractTextPluginn = require('extract-text-webpack-plugin')

	module:{
		reules:[
			{
				textL /\.css$/,
				loaders: ExtractTextPlugin.extract({
					use:[
						'css-loader'
					]
				}),
			}
		]
	},
	plugins: [
		new ExtractTextPlugin({
			//从.js文件中提取出来的.css文件的名称
			filename: `[name]_[contenthash:8].css`
		})	
	]

Resolve

webpack在启动后会从配置的入口模块出发找出所有依赖的模块,resolve配置webpack如何查找模块所对应的文件

alias

resolve.alias配置项通过别名来将原导入路径映射成一个新的导入路径。例如使用一下配置:

resolve:{
	alias:{
		components: './src/components'
	}
}

当通过**import Button from 'components/button'**导入时

实际上被alias等价替换成了import Button from './src/components/button'

extensions

在导入语句没带文件后缀时,Webpack会自动带上后缀后去尝试访问文件是否存在。

resolve.extensions用于配置在尝试过程中用到的后缀列表,默认是:

extensionsL ['.js','.json']

也就是说,当遇到require('./data')这样的导入语句时,Webpack会先寻找./data.js文件,如果该文件不存在,就去找./data.json文件,如果还是找不到就报错。

escriptionFiles

如果resolve.enforceExtension被配置为true,则所有导入语句都必须带文件后缀,例如开启前import './foo'能正常工作,开启后就必须写成**import './foo.js' **

Devtool

devltool配置Webpack如何生成source map,默认值是false,即不生成source map,若想构建出的代码生成source map以方便调试,则可以这样配置:

module.export = {
	devtool: 'source-map'
}

开启source-map会方便我们开发中的调试,方便我们定位到具体的代码问题,当也会影响一下相关的构建性能问题,所有要做出多配置文件,开发环境配置和生产环境配置

source-map模式下会输出质量最高且最详细的Source Map,这会造成构建速度缓慢,特别是在开发过程中需要频繁修改时会增加等待时间

在Source-Map模式下会将Source Map暴露,若构建发布到线上的代码的source map暴力,就等同于源码被泄露

为了解决以上两个问题,可以这样做,如下所述

  • 在开发环境下devtool设置成cheap-module-eval-source-map,因为生成这种source map的速度最快,能加速构建。由于在开发环境下不会做代码压缩,所以在source map的即使没有列信息,也不会影响断电调试.

  • 在生产环境下将devtool设置成hidden-source-map,意思是生成最详细的source map,但不会将source map暴露出去。由于生产环境下会做代码压缩,一个js文件只有一行,所以需要列信息。

在生产环境下通常不会将Source Map上传到http服务器让用户获取,而是上传到JavaScript错误收集系统,在错误收集系统上根据Source Map和收集到的JavaScript运行错误队栈,计算出错误所在源码的位置。

不要在生产环境下使用inline模式的Source Map,因为这会使JavaSctipt文件变的很大,而且会泄露源码。

Externals

External用来告诉Webpack要构建的代码中使用了哪些不用被打包的模块,也就是说这些模板是外部环境提供的,Webpack在打包时可以忽略它们

通过externals可以告诉Webpack在js运行环境中已经内置了哪些全局变量,不用将这些全局变量打包到代码中而是直接使用它们。

moudle.export = {
	externals: {
		//将导入语句里的jquery替换成运行环境里的全局变量jQuery
		jquery: 'jQuery'
	}
}

Webpack优化

  • 优化开发体验

      优化开发体验的目的是提升开发效率,减少每次构建的耗时
      1. 优化构建速度
      2. 优化使用体验,通过自动化手段完成一些重复的工资哦,让我们专注于解决问题本身。
    
  • 优化输出质量

      呈现用户体验更好的网页,减少首屏加载时间,提升性能流畅度。
    
      1. 缩小文件的搜索范围
      2. 优化Loader的配置,通过include去命中 只有哪些文件去处理,通过exclude去去除哪些文件不需要处理,比如node_module
      3. 优化resolve.modules配置
      resolve.modules用于配置Webpack去哪些目录下寻找第三方模块。
      
      resolve.modelus的默认值是['node_modules']含义是先去当前目录的./node_modules目录下去找我们想找的模块,如果没找到,就去上一级目录../node_modules中找,再没有就去../../node_modules中找,以此类推
      
      当安装的第三方模块都放在项目根目录的./node_modules目录下时,就没有必要按照默认的方式去一层层的寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:
      
      	module.exports = {
      		resolve: {
      			// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
      			// 其中,__dirname表示当前工作目录,也就是项目根目录
      			modules: [path.resolve(__dirname,'node_modules')]
      		}
      	}
      4.优化resolve.alias配置,跳过递归解析操作
      
      5.优化resolve.extensions配置减少后缀,后缀要尽可能少,提升速度
      
      6.优化noParse配置
    

使用DllPlugin

包含大量复用模块的动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会被重新编译,而是直接使用动态链接库中的代码。由于动态链接库中大多数包含的是常用的第三方模块,例如react,react-dom,所以只要不升级这些模块的版本,动态链接库就不用重新编译。

Webpack已经内置了对动态链接库的支持,需要通过以下两个内置的额插件接入。

  • DllPlugin插件:用于打包粗一个个单独的鼎泰链接库文件.
  • DllReferecePlugin插件:用于在主要的配置文件中引入DllPlugin插件打包好的动态链接库文件

HappyPack

运行在Node.js之上的Webpack是单线程模型,Happy Pack将任务分解给多个子进程去并发执行,子进程处理完后再讲结果发送给主进程,由于js是单线程模型,所以想要发挥多核cpu的功能,就只能通过多进程实现,而无法通过多线程实现。

整个Webpack构建流程中,最耗时的流程可能就是loader对文件的转换操作了,因为要转换的文件数据量巨大,而且这些转换操作都只能一个一个地处理。HappyPack的核心原理就是将这部分任务分解到多个进程中去并行处理,从而减少总的构事件。

ParallelUglifyPlugin

原本会使用Uglifyjs去一个一个压缩再输出

Paralleuglifyplugin会开启多个子线程,将对多个文件的压缩工作分配给多个子进程完成,每个子进程其实还是通过uglify去压缩代码,但是变成了并行执行,所以Paralleuglifyplugin能更快地完成对多个文件的压缩工作

文件监听

文件监听是发现源码发生变化时,自动重新构建出新的输出文件.

让Webpack开启监听模式,有如下两种方式。

  • 在配置文件webpack.config.js中设置watch:true
  • 在执行启动webpack的命令时带上--watch参数,完成的命令是webpack--watch

优化文件的性能

忽略node_modules

module.export = {
	watchOptions:{
		ignored: /node_modules/
	}
}

自动刷新浏览器

开启模块热替换

##区分环境 区分开发环境和生产环境,指定对用的不同调试模式Source Map,是否开启压缩,是否提取公共代码等

提取公共代码

相同的资源被重复加载,浪费用户的流量和服务器成本

页面需要加载的资源太大,会导致网页首屏加载缓慢,影响用户体验。

webpackchunkplugin

代码分割,按需加载。

如何按需加载

  1. 在为单页应用做按需加载优化时,一般采用以下原则

  2. 将整个网站划分成一个个小功能,再按照每个功能的相关程度将它们分成几类

  3. 将每一类合并为一个chunk,按需加载对应的chunk.

  4. 不要按需加载用户首次打开网站是需要看到的画面所对应的功能,将其放到执行入口所在的Chunk中,以减少用户能感知的网页加载时间。