1、Webpack为何而生
作为前端,日常接触到的新工具形形色色,但是大部分都随着时间销声匿迹,剩下的基本就是热门好评的小部分,并融入大家的开发生涯当中。其中,网络热门的webpack自然成为了开发者争相研究的目标,并因此引申出一系列的问题,最主要的便是Webpack究竟因何而生?
事实上,Webpack的出现是顺其自然的,因为webpack出现之前,前端开发其实已经暴露出了各种各样的问题,根据时间线出现的顺序,可以大概如下:大的脚本文件维护及其困难,小的脚本文件导致网络瓶颈 =》后面问世的CommonJS引入了require 机制,但是浏览器并不支持 =》 Webpack出现;
因而就以上问题得出结论:前端开发中存在的无法用当时技术去解决问题的时候,新的解决方案(工具)就会应运而生。所以,webpack并不神秘,概念上完全可以认为是一整套解决前端开发过程中问题的解决方案集合,其目前主要方案点如下:
(1)模块化开发,强大的构建优化能力(import,require)
(2)预处理,方便使用多种格式资源(Less,Sass,ES6,TypeScrip等)
(3)主流框架脚手架支持,大大加快开发效率(Vue,React,Angular)
2、Webpack的基本概念
webpack的实质就是js模块打包工具,在 webpack 的眼里只认识模块,而一个文件相当于一个模块(module)。它能够根据配置的入口路径(entry),通过模块间的依赖关系(import/export),递归地构建出一张依赖关系图(dependency graph)。而由于webpack只理解 JavaScript 和 JSON 文件,过程中需要通过配置指定的加载器(loader)对相应文件进行转换,也可以通过配置指定的插件(plugins)对上下文进行优化输出(output)为代码块(chunk)。如下图所示:
3、webpack核心术语概念
3.1 模式mode
webpack存在两种打包模式,一种为配置文件,另一种为命令行输入。
通过配置文件:
module.exports= { mode: 'production', // development}
通过命令行传参:
webpack--mode=production
然后代码中:
if(process.env.NODE_ENV==='development'){ //开发环境处理逻辑}else{ //生产环境处理逻辑}
3.2 入口entry
webpack通过配置的入口路径开始递归遍历各个模块。
单入口:
module.exports= { entry: './src/main.js', }
多入口:
module.exports= { entry: { foo: './src/main.js', bar: './src/foo.js', com: './src/bar.js' } }
3.3 import/export
某个模块通过import导入其他模块的变量及对象,通过export导出自身的变量及对象,webpack内部通过import可以构建出模块关系图。
3.4 出口output
webpack根据配置的输出路径,决定在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值是 ./dist:
单出口:
constpath=require('path') module.exports= { output: { filename: 'main.js', path: path.resolve(__dirname, 'dist') } }
多出口:
constpath=require('path') module.exports= { output: { filename: '[name].js', path: path.resolve(__dirname, 'dist') } }
3.5 模块module
在webpack中,一切皆是模块,而一个文件就是一个模块。
3.6 加载器loader
事实上,webpack本身只能理解JavaScript语法,因此只能对js文件进行直接的文件合并、压缩处理。然而实际开发中会用到各种其他类型的文件,如 css、less、jpg、jsx、vue 等等类型的文件,webpack本身是无法处理这些类型文件的,此时就需要借助各种文件的loader(加载器)。loader 的作用就是将各种类型的文件转换成 webpack 能够处理的模块,一个实际开发中经常接触到的例子:项目中使用了 less 语法,就需要使用 less-loader 去将其转译为 css,然后通过 css-loader 去加载 css 文件,处理后交给 style-loader,最后把资源路径写入到 html 中的 style 标签内生效。
module: { rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader', 'less-loader'] }] },
需要注意的是由于 loader 是从右往左执行的,一个 loader 处理完的结果会交给下一个 loader 继续处理,就像一条工厂流水线一样,所以加载器数组存在一定的次序,配置的时候就需要注意书写的loader次序。 此外有一个在实际中接触得非常多且非常重要的 loader,那就是 babel-loader,其可以将es6语法转换成能普遍被浏览器所执行的es5,几乎用到es6语法的项目都会有所接触。
3.7 插件plugin
插件可以用来处理各种特定的任务,比如代码的压缩,静态文件打包优化等等。如下面所示的自动生成 html 文件的插件:
constHtmlWebpackPlugin=require('html-webpack-plugin'); module.exports= { plugins: [ newHtmlWebpackPlugin({ template: './src/index.html' }) ] };
3.8 代码块chunk
代码块,一个chunk由多个模块组合而成,用于代码合并和分割。
3.9 源代码映射source-map
source map 是一种映射关系,当程序出问题时,我们想要从打包后的代码中去排查问题时比较困难的。这个时候我们就要借助于源代码去排查。其中包含有好几种配置方式:
module.exports= { devtool: 'source-map',//inline-source-map, inline-cheap-source-map, inline-cheap-module-source-map, eval}
source-map:打包后会生成 map 格式的文件,里面包含映射关系的代码
inline-source-map:打包后不会生成 map 格式的文件,包含映射关系的代码会放在打包后的代码中
inline-cheap-source-map:cheap有两种作用:一是将错误只定位到行,不定位到列。二是映射业务代码,不映射loader和第三方库等。会提升打包构建的速度。
inline-cheap-module-source-map:module会映射loader和第三方库
eval:用 eval 的方式生成映射关系代码,效率和性能最佳。但是当代码复杂时,提示信息可能不精确
因此实际开发中往往推荐以下两种配置方式
开发环境:
devtool: 'cheap-module-eval-source-map',
生产环境
devtool: 'cheap-module-source-map',
4、打包流程基本原理浅析
以下的原理解析是基于webpack4的自我理解,根据简单的demo目标打包生成物为切入口进行分析,进而去理解其内部执行流程,其大致工作流程如下图所示:
![list.jpg](https://ucc.alicdn.com/pic/developer-ecology/50345c2dbf6d4ac0a337ca1e12b9c942.jpg)
4.1 打包后产物(简略部分)
(function (modules) { //内部执行流程})({ //外部模块id对象'./main.js': (function (module, exports, __webpack_require__) {}), './foo.js': (function (module, exports, __webpack_require__) {}), })
webpack 打包完输出的 bundle.js 大致就是上面这个架子。可以看出它本质就是一个自执行的函数,参数是 一个Object 对象,而对象的key 就是我们项目中的各个模块的js 文件名称。value 是一个 function,模块中的代码都会被解析后放在这个 function 内。这些 function 会接收 module 和 exports 做为参数,这也是为什么我们在模块中使用 CommonJs 语法(也就是 modules.exports = {} 或者 直接 exports = {}),浏览器能够识别的原因。我们在模块的最后其实使用 module.exports = {},其实就是把模块的内容放进了 exports 对象里而已。
4.2 传参数组(模块数组)的形成
那上面这个传递给匿名函数的数组又是怎么形成的呢?过程大致如下:
webpack 中有这么一个 Complier 类,它首先会读取 webpack.config.js 中的内容
然后通过 @babel/parse,从入口文件 entry 来构建一颗抽象语法树 AST
然后通过 @babel/traverse,根据抽象语法树得出入口文件的依赖模块
然后通过 @babel/core 和 @babel/preset-env,根据抽象语法树解析出入口文件的代码。这一步已经把 ES6 或更高版本的 js 转换了,然后循环递归入口文件的依赖,构建出一张依赖关系图这就是我们上面用到的数组了。
4.3 匿名函数内部
varinstalledModules= {}; function__webpack_require__(moduleId) { if(installedModules[moduleId]) { returninstalledModules[moduleId].exports; } varmodule=installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports,module,module.exports,__webpack_require__); module.l=true; returnmodule.exports; } return__webpack_require__(__webpack_require__.s="./src/main.js");
可以看到自执行函数里面定义了一个 __webpack_require__ 函数,是用来装载模块的。它的入参是模块的唯一标识,从4.1中我们可以看到这个唯一标识就是模块的文件名称。其中装载后的变量会放在变量 installedModules 里面,防止同一模块重复装载。
而什么是装载?装载就是执行传进来的 modules 中的一个个函数,也就是会执行我们所写的业务模块里面的代码,把 exports 传进去,然后我们会这样 module.exports = {...},把内容给放进去。简而言之,装载就是把模块代码装进 exports 对象。__webpack_require__ 函数最终会返回这个 exports。
我们模块中的依赖也会调用这个 __webpack_require__,比如 main.js 引入了 foo.js,那么 main 函数内部会有这么一句 const foo = __webpack_require__(/*! ./foo */ "./foo.js"); 这样我们就可以在 main 中获取并使用到 foo 所提供的功能了(其实就是在用 foo.js 中最后赋值的那个 exports)
4.4 流程细分图(引用的网图)如下:
4.5 更详细通俗易懂的demo可以查阅此文章:
https://blog.csdn.net/haodawang/article/details/77126686
5、常见优化构建
5.1 优化loader配置
上面章节我们可以知道loader作用是对相应文件进行转换,所以我们可以考虑通过减少loader作用范围,大大缩短构建时间。例如Babel,由于Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。因此我们直接指定哪些文件不通过loader处理,或者指定哪些文件通过loader处理:
constpath=require('path') module.exports= { module: { rules: [ { // js 文件才使用 babeltest: /\.js$/, use: ['babel-loader'], // 只处理src文件夹下面的文件include: path.resolve('src'), // 不处理node_modules下面的文件,node_modules 中使用的代码都是编译过的,所以我们也完全没有必要再去处理一遍。exclude: /node_modules/ } ] } }
另外,对于babel-loader,我们还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间。
module.exports= { loader: 'babel-loader?cacheDirectory=true'}
5.2 使用多线程处理打包
由于node是单线程运行的,所以 webpack 在打包的过程中也是单线程的,特别是在执行 loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。那么我们可以使用一些方法将 loader 的同步执行转换为并行,这样就能充分利用系统资源来提高打包速度了。
例如happypack,如其包名,快乐的打包。就是能够让Webpack把打包任务分解给多个子线程去并发的执行,子线程处理完后再把结果发送给主线程:
constHappyPack=require('happypack'); module.exports= { module: { rules: [ { test: /\.js$/, //把 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例use: ['happypack/loader?id=babel'], exclude: path.resolve(__dirname, 'node_modules'), }, { test: /\.css$/, // 把 .css 文件的处理转交给 id 为 css 的 HappyPack 实例use: ['happypack/loader?id=css'] } ] }, plugins: [ newHappyPack({ id: 'js', //ID是标识符的意思,ID用来代理当前的happypack是用来处理一类特定的文件的threads: 2, //你要开启多少个子进程去处理这一类型的文件loaders: [ 'babel-loader' ] }), newHappyPack({ id: 'css', threads: 2, loaders: [ 'style-loader', 'css-loader' ] }) ], }
5.3 module.noParse 属性
module.noParse 属性,可以用于配置那些模块文件的内容不需要进行解析(即无依赖) 的第三方大型类库(例如jquery,lodash)等,使用该属性让 Webpack不扫描该文件,以提高整体的构建速度。
module.exports= { module: { noParse: /jquery|lodash/, // 正则noParse: function(content) { return/jquery|lodash/.test(content) } } }
5.4 打包文件分析工具
webpack-bundle-analyzer插件的功能是可以生成代码分析报告,帮助提升代码质量和网站性能。
constBundleAnalyzerPlugin=require('webpack-bundle-analyzer').BundleAnalyzerPluginmodule.exports={ plugins: [ newBundleAnalyzerPlugin({ generateStatsFile: true, // 是否生成stats.json文件 }) ] }
5.5 对图片进行压缩和优化
image-webpack-loader这个loder可以帮助我们对打包后的图片进行压缩和优化,例如降低图片分辨率,压缩图片体积等。
module.exports={ module: { rules: [ { test: /\.(png|gif|jpe?g|svg)$/i, exclude:[path.resolve(process.cwd(), 'src/assets/css')], use: [ { loader: 'url-loader', options: { limit: 1024,// 图片限制name: '[hash:8].[ext]', useRelativePath: false, outputPath: function(fileName){ return'assets/images/'+fileName } } }, { loader:'image-webpack-loader' } ] } };
5.6 删除无用的CSS样式
久远且疏于维护的项目可能会存在一些CSS样式被迭代废弃,可以将其去除掉,此时就可以使用purgecss-webpack-plugin插件,该插件可以去除未使用的CSS,一般与 glob、glob-all 一起配合使用。
注意:此插件必须和CSS代码抽离插件mini-css-extract-plugin配合使用。
constglob=require('glob'); constPurgecssPlugin=require('purgecss-webpack-plugin'); module.exports={ plugins: [ // 需要配合mini-css-extract-plugin插件newPurgecssPlugin({ paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, {nodir: true}), // 不匹配目录,只匹配文件 }) }), ] };
**参考:**