谈谈webpack

构建工具备不少,好比Npm Script任务执行者、Grunt也是任务执行者、Gulp基于流的自动化构建工具、Fis3百度构建工具、Webpack打包模块化JavaScript工具和Rollup模块打包工具。javascript

这里谈谈用得很频繁的webpack构建工具。css

基本概念

从最基本的概念开始了解:html

入口(entry)

entry是配置模块的入口,必填。前端

module.exports = {
	entry: './path/to/my/entry/file.js'
}
复制代码

出口(output)

output配置如何输出最终想要的代码。output是一个object,里面包含一系列的配置项。java

output.filename配置输出文件的名称,为string类型。node

output.path配置输出文件存放在本地的目录(路径),必须是string类型的绝对路径。react

path: path.resolve(__dirname, 'dist_[hash]')
复制代码

output.publicPath配置发布到线上资源的URL前缀,为string类型。默认为空字符串'',即便用相对路径。jquery

好比须要将构建的资源上传到CDN服务上,以便加快网页的打开速度。配置代码以下:webpack

filename: '[name]_[chunkhash:8].js'
publicPath: 'https://cdn.example.com/assets/'
复制代码

发布到线上时候,HTML中引入的JavaScript文件以下:git

<script src='https://cdn.example.com/assets/a_12345678.js'></script>
复制代码

线上出现404错误的时候,看下路径有没有错~

还有其余配置请看文档

模块(module)

module配置如何处理模块。

module.rules配置模块的读取和解析规则,一般用来配置Loader。其类型是一个数组,数组里每一项都描述了如何去处理部分文件。应用一项rules时大体经过如下方式:

  1. 条件匹配:经过testincludeexclude三个配置项来命中Loader要应用规则的文件。
  2. 应用规则:对选中后的文件经过use配置项来应用Loader,能够只应用一个Loader或者按照从后往前的顺序应用一组Loader,同时还能够给Loader传入参数。
  3. 重置顺序:一组Loader执行顺序默认是从右往左执行,经过enforce选项可让其中一个Loader的执行顺序放在前面或者最后。
module: {
	rules: [
		{
			// 命中scss文件
			test: /\.scss$/,
			// 处理顺序从右往左
			use: ['style-loader', 'css-loader', 'sass-loader'],
			// 排除node_modules目录下的文件
			exclude: path.resolve(__dirname, 'node_modules'),
		}
	]
}
复制代码

Loader须要传入多个参数的时候的例子:

use: [
	{
		loader:'babel-loader',
		options:{
			cacheDirectory:true,
		},
		// enforce:'post' 的含义是把该 Loader 的执行顺序放到最后
		// enforce 的值还能够是 pre,表明把 Loader 的执行顺序放到最前面
		enforce:'post'
	},
	// 省略其它 Loader
]
复制代码

module.noParse配置项可让webpack忽略对部分没采用模块化的文件的递归解析和处理,这样作有助于提升构建性能。好比:

module: {
	noParse: (content) => /jquery|lodash/.test(content)
}
复制代码

module.rules.parser属性能够更细粒度的配置哪些模块须要解析,哪些不须要,和noParse配置项的区别在于parser能够精确到语法层面,而noParse只能控制哪些文件不被解析。

module: {
	rules: [
		{
			test: /\.js$/,
			use: ['babel-loader'],
			parser: {
				amd: false, // 禁用 AMD
      			commonjs: false, // 禁用 CommonJS
      			...
			}
		}
	]
}
复制代码

解析(resolve)

Resolve配置webpack如何寻找模块所对应的文件。Webpack内置Javascript模块化语法解析功能,默认会采用模块化标准里面约定好的规则去寻找,你也能够按照需求修改默认规则。

resolve.alias配置项经过别名来把原导入的路径映射成一个新的导入路径。以下:

resolve: {
	alias: {
		components: './src/components/'
	}
}
复制代码

当你经过import Button from 'components/button'导入时,实际上被alias等价替换了import Button from './src/components/button'

resolve.modules配置webpack去哪些目录下找第三方模块,默认只会去node_modules目录下寻找。

resolve.enforceExtension若是配置为true全部导入语句都必须带有后缀,例如开启前import './foo能正常工做,开启后就必须写成import './foo.js'

插件(plugin)

Plugin用于扩展Webpack功能,各类各样的Plugin几乎让Webpack能够作任何构建相关的事情。

举个例子:

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

module.exports = {
	plugins: [
		// 全部页面都会用到的公共代码提取到 common 代码块中
		new CommonsChunkPlugin({
			name: 'common',
			chunks: ['a', 'b']
		})
	]
}
复制代码

DevServer配置

在开发环境的时候使用。要配置DevServer,除了在配置文件里面经过devServer传入参数外,还能够经过命令行参数传入。

注意:只有在经过DevServer去启动Webpack时配置项文件里devServer才会生效。

devServer.hot配置是否启用使用DevServer中提到的模块热替换功能。

devServer.host配置项用于配置 DevServer 服务监听的地址。

devServer.port配置项用于配置 DevServer 服务监听的端口,默认使用8080端口。

devServer.https配置HTTPS协议服务。某些状况下你必须使用HTTPS,HTTP2 和 Service Worker 就必须运行在 HTTPS 之上。

devServer: {
	https: true
}
复制代码

webpack原理

Webpack的运行是一个串行的过程,从启动到结束会执行如下流程:

  1. 初始化参数:从配置文件和Shell语句中读取与合并参数,获得最终的参数
  2. 开始编译:用上一步获得的参数初始化Compiler对象,加载全部配置的插件,执行对象的run方法开始执行编译。
  3. 肯定入口:根据entry找出全部文件
  4. 编译模块:从入口文件出发,调用全部配置的Loader对模块进行编译,再找到模块依赖的模块,再递归本步骤,直到全部入口依赖的文件都通过了本步骤的处理;
  5. 完成编译:在第四步骤后,获得了每一个模块被编译的内容和它们直接的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每一个chunk转换成一个单独的文件加入到输出内容后,这一步是能够修改输出内容的最后机会。
  7. 输出完成:在肯定好输出内容后,根据配置肯定输出的路径和文件名,把文件内容写入系统。

webpack优化

缩小文件搜索范围

  • 优化loader配置

Loader对文件的转换操纵很耗时,须要让尽量少的文件被Loader处理。

在使用Loader时能够经过testincludeexclude三个配置项来命中Loader要应用规则的文件。

module.exports = {
	module: {
		rules: [
			{
				test: /\.js$/,
				use: ['babel-loader?cacheDirectory'],
				include: path.resolve(__dirname, 'src')
			}
		]
	}
}
复制代码
  • 优化resolve.modules配置

resolve.modules用于配置webpack去哪些目录下寻找第三方模块。

resolve.modules的默认值是['node_modules'],含义是先去当前的目录下./node_modules目录下去找想找的模块,以此类推,若是没有找到就去上一级目录../node_modules中找,再没有去上上一级,以此类推...

若是知道安装的模块在项目的根目录下的./node_modules时候,没有必要按照默认的方式一层层找:

module.exports = {
	resolve: {
		modules: [path.resolve(__dirname, 'node_modules')]
	}
}
复制代码
  • 优化resolve.alias配置

resolve.alias配置项经过别名来把原导入路径映射成一个新的导入路径。能够减小耗时的递归解析操做。

  • 优化module.noParse配置

module.noParse配置项可让Webpack忽略对部分没采用模块化的文件的递归解析处理,这样作的好处是能提升构建性能。

const path = require('path');
module.exports = {
	module: {
		// 独完整的 `react.min.js` 文件就没有采用模块化,忽略对 `react.min.js` 文件的递归解析处理
		noParse: [/react\.min\.js$/],
	}
}
复制代码

把任务分解为多个子进程去并发执行

HappyPack把任务分解成多个子进程并发执行,子进程处理完后再把结果发送给主进程。减小了总的构建时间。

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
	module: {
		rules: [
			{
				test: /\.js$/,
				loader: 'happypack/loader?id=happyBabel',
				exclude: /node_modules/
			}
		]
	},
	plugins: [
		new HappyPack({
			id: 'happyBabel',
			loaders: [{
				loader: 'babel-loader?cacheDirectory= true',
			}],
			// 共享进程池
			threadPool: happyThreadPool,
			// 容许happypack输出日志
			verbose: true,
		})
	]
}
复制代码

使用自动刷新

例如:

module.export = {
	watch: true,
	watchOptions: {
		// 监听到变化发生后会等300ms再去执行动做,防止文件更新太快致使从新编译频率过高
	    // 默认为 300ms
	    aggregateTimeout: 300,
	    // 判断文件是否发生变化是经过不停的去询问系统指定文件有没有变化实现的
	    // 默认每隔1000毫秒询问一次
	    poll: 1000
	}
}
复制代码

因为保存文件的路径和最后编辑时间须要占用内存,定时检查周期检查须要占用CPU以及文件I/O,因此最好减小须要监听的文件数量和下降检查频率。

热替换

热替换就是当一个源码发生改变的时,只从新编译发生改变的模块,再用新输出的模块替换掉浏览器中对应的老模块。

开启热替换:

webpack-dev-server --hot
复制代码

区分环境

区分开发环境和生产环境,进行不一样的构建~

CDN加速

CDN又叫内容分发网络,经过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。

CDN 实际上是经过优化物理链路层传输过程当中的网速有限、丢包等问题来提高网速的。

结合publicPath来处理:

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const { WebPlugin } = require('web-webpack-plugin');

module.exports = {
	output: {
		filename: '[name]_[chunkhash:8].js',
		path: path.resolve(__dirname, './dist'),
		publicPath: '//js.cdn.com/id/'
	},
	module: {
		rules: [
			{
				test: /\.css$/,
				use: ExtractTextPlugin.extract({
	          	use: ['css-loader?minimize'],
	          	publicPath: '//img.cdn.com/id/'
	        }),
			}
		]
	},
	plugins: [
		new WebPlugin({
	      template: './template.html',
	      filename: 'index.html',
	      // 指定存放 CSS 文件的 CDN 目录 URL
	      stylePublicPath: '//css.cdn.com/id/',
	    }),
	    new ExtractTextPlugin({
	      // 给输出的 CSS 文件名称加上 Hash 值
	      filename: `[name]_[contenthash:8].css`,
	    }),
	]
}
复制代码

tree shaking优化

将多余的代码移除。

webpack --display-used-exports --optimize-minimize
复制代码

提取公共代码

公共代码的提取。

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

new CommonsChunkPlugin({
	// 从哪些 Chunk 中提取
	chunks: ['a', 'b'],
	// 提取出的公共部分造成一个新的 Chunk,这个新 Chunk 的名称
	name: 'common'
})
复制代码

按需加载

对于采用单页应用做为前端架构的网站来讲,会面临一个网页须要加载的代码量很大的问题,由于许多功能都作到了一个HTML里面,这会致使网页加载缓慢、交互卡顿、用户体验将很是糟糕。

致使这个问题的根本缘由在于一次性的加载全部功能对应的代码,但其实用户每一阶段只可能使用其中一部分功能。 因此解决以上问题的方法就是用户当前须要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载。

Webpack 内置了强大的分割代码的功能去实现按需加载。好比:

  • 网页首次加载时只加载main.js文件,网页会展现一个按钮main.js文件中只包含监听按钮事件和加载按需加载的代码。
  • 当按钮被点击时才去加载被分割出去的show.js文件,加载成功后再执行show.js里的函数。

main.js中:

window.document.getElementById('btn').addEventListener('click', function () {
	// 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
	import(/* webpackChunkName: "show" */ './show').then((show) => {
		show('Webpack');
	})
});
复制代码

show.js中:

module.exports = function (content) {
	window.alert('Hello ' + content);
};
复制代码

代码中最关键的一句是import(/* webpackChunkName: "show" */ './show'),Webpack 内置了对import(*)语句的支持,当 Webpack 遇到了相似的语句时会这样处理:

  • ./show.js为入口新生成一个Chunk
  • 当代码执行到import所在语句时才会去加载由Chunk对应生成的文件。
  • import返回一个Promise,当文件加载成功时能够在Promisethen方法中获取到show.js导出的内容。

在工做中具体使用到的时候再按须要进行更改配置项啦~

参考

文章首发在github上--谈谈webpack。更多的内容,请戳个人博客进行了解,能留个star就更好了💨

相关文章
相关标签/搜索