谈谈webpack
构建工具有很多,比如Npm Script任务执行者、Grunt也是任务执行者、Gulp基于流的自动化构建工具、Fis3百度构建工具、Webpack打包模块化JavaScript
工具和Rollup模块打包工具。
这里谈谈用得很频繁的webpack
构建工具。
基本概念
从最基本的概念开始了解:
入口(entry)
entry
是配置模块的入口,必填。
module.exports = {
entry: './path/to/my/entry/file.js'
}
出口(output)
output
配置如何输出最终想要的代码。output
是一个object
,里面包含一系列的配置项。
output.filename配置输出文件的名称,为string类型。
output.path配置输出文件存放在本地的目录(路径),必须是string类型的绝对路径。
path: path.resolve(__dirname, 'dist_[hash]')
output.publicPath配置发布到线上资源的URL前缀,为string类型。默认为空字符串''
,即使用相对路径。
比如需要将构建的资源上传到CDN服务上,以便加快网页的打开速度。配置代码如下:
filename: '[name]_[chunkhash:8].js'
publicPath: 'https://cdn.example.com/assets/'
发布到线上时候,HTML中引入的JavaScript
文件如下:
<script src='https://cdn.example.com/assets/a_12345678.js'></script>
线上出现404错误的时候,看下路径有没有错~
还有其他配置请看文档
模块(module)
module
配置如何处理模块。
module.rules配置模块的读取和解析规则,通常用来配置Loader
。其类型是一个数组,数组里每一项都描述了如何去处理部分文件。应用一项rules
时大致通过以下方式:
- 条件匹配:通过
test
、include
、exclude
三个配置项来命中Loader
要应用规则的文件。 - 应用规则:对选中后的文件通过
use
配置项来应用Loader
,可以只应用一个Loader
或者按照从后往前的顺序应用一组Loader
,同时还可以给Loader
传入参数。 - 重置顺序:一组
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的运行是一个串行的过程,从启动到结束会执行以下流程:
- 初始化参数:从配置文件和Shell语句中读取与合并参数,得到最终的参数
- 开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译。
- 确定入口:根据entry找出所有文件
- 编译模块:从入口文件出发,调用所有配置的Loader对模块进行编译,再找到模块依赖的模块,再递归本步骤,直到所有入口依赖的文件都经过了本步骤的处理;
- 完成编译:在第四步骤后,得到了每个模块被编译的内容和它们直接的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个chunk转换成一个单独的文件加入到输出内容后,这一步是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入系统。
webpack优化
缩小文件搜索范围
- 优化loader配置
Loader
对文件的转换操纵很耗时,需要让尽可能少的文件被Loader
处理。
在使用Loader时可以通过test
、include
、exclude
三个配置项来命中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
,当文件加载成功时可以在Promise
的then
方法中获取到show.js
导出的内容。
在工作中具体使用到的时候再按需要进行更改配置项啦~