学以致用之webpack(v4)
背景
前端代码组织由命名空间(特别代表jquery, $)变为模块化(commonjs, amd, es6等),各类前端框架层出不穷(三驾马车等),为了解决开发大型项目所暴露出的语言缺陷及提高开发效率,各类新语言应运而生,(ts,flow,less,scss等)。但他们的源码都无法直接在浏览器中直接运行,而构建做的事情就是将源码转换成可执行的js,css,html代码。构建是工程化,自动化思想在前端开发中的体现,由于作为前端熟悉js,而nodejs又可以胜任所有构建需求,故大多数构建工具都是由nodejs开发的。
前端构建工具
npm script
npm
是安装node
附带的包管理器,npm script
是npm
内置功能,允许在package.json文件里使用scripts字段定义任务,实现原理为通过调用shell去运行脚本。
Grunt
有大量现成插件封装了常见的任务,也能管理任务之间的依赖关系,自动化地执行依赖的任务,灵活。相当于进化版的npm script,诞生是为了弥补npm script的不足。
1 | module.exports = function(grunt) { |
Gulp
一个基于流的自动化构建工具。除了可以管理和执行任务,还支持监听文件、读写文件。好用且不失灵活,可以看做是Grunt的加强版。相对于Grunt,增加了监听文件,读写文件、流式处理。
Fis3
来自百度的优秀国产构建工具,fis3集成了web开发中的常用构建功能。
- 读写文件
- 资源定位
- 文件指纹(通过useHash配置输出文件时为文件url+md5戳,来优化浏览器缓存)
- 文件编译(如es6=>es5)
- 压缩文件
- 图片合并(雪碧图,通过spriter配置合并css里导入的图片到一个文件中,来减少http请求数)
fi3很强大,内置了许多功能,是一种专注于web开发的完整解决方案,如果将Grunt、Gulp比作汽车发动机,那么fis3就是一辆完整汽车
Webpack
webpack是一个现代 JavaScript 应用程序的静态模块打包器(module bundler),在webpack中一切文件如js,css,scss,图片,模板等皆模块,这样的好处是能清晰的描述各个模块之前的依赖关系,以方便webpack对模块进行组合和打包,最终输入浏览器能使用的静态资源。
rollup
rollup是一个跟webpack类似但是专注于es6的模块打包工具,它的亮点在于Tree Shaking,以除去已定义但未被使用代码并进行Scope Hoisting(作用域提升),以减小输出文件的大小及提升运行性能。然后这些亮点随后就被webpack模仿并实现。使用起来与webpack及其相似。
对比
npm script | Grunt | Gulp | Fis3 | Webpack | Rollup | |
---|---|---|---|---|---|---|
优势 | 内置 | 灵活,只负责执行我们定义的任务。 大量可复用的插件封装好了常见的构建任务 |
监听文件,读写文件、流式处理。 | 集成强大,配置简单,开箱即用 | 1.开箱即用,一步到位 2.可通过plugin扩展,完整好用且不失灵活 3.社区活跃庞大,良好的开发体验 |
打包js库更有优势(没有webpack打包后的那段模块加载,执行,缓存代码) |
缺点 | 功能较简单,无法方便管理多个任务之间的依赖 | 集成度不高,要写很多配置后才可以用,无法做到开箱即用 | 集成度不高,要写很多配置后才可以用,无法做到开箱即用 | 目前官方已不再维护,且不支持最新版本Node | 只能用于采用模块化开发的项目 | 生态链不完善,体验不如webpack,功能不如webpack完善 |
why webpack
- 提供一站式的解决方案
- 良好的生态链和维护团队,提供良好的开发体验并保证质量
- 被广泛使用和验证,可以很轻松找到各个场景下的经验分享
start webpack
核心概念
- Entry: 入口
- Module: 模块
- Chunk: 代码块,一个Chunk由多个模块组合而成,用于代码合并与分割
- Loader: 模块转换器
- Plugin: 扩展插件
- Output: 输出结果
- Resolve: webpack如何寻找模块所对应文件
关于webpack的配置项有很多,这里大家通过文档去了解。
通常我们可以用如下经验判断如何配置webpack
- 若想让源文件加入构建流程被webpack控制,则配置entry
- 若想自定义输出文件的位置和名称,则配置output
- 若想自定义寻找依赖模块时的策略,则配置resolve
- 若想自定义解析和转换文件的策略,则配置module,通常是配置module.rules里的loaders
- 若其他大部分需求可能通过plugin去实现,则配置plugin
实战
使用es6
部分浏览器支持es6或者更高版本不全,故需要做转换为支持良好的es5代码包含如下两件事情:
- es6及更新版本语法用es5实现
- 为新的Api注入polyfill
- es6语法
babel就是用来满足上述需求的。它是一个javascript编译器,让我们使用最新语言特性而不用担心兼容问题。
在babel 7.0以上版本
官方推荐配置文件由之前.babelrc
命名转为babel.config.js
。
- 以编程方式创建配置文件,编译node_module目录下的模块?
babel.config.js
1 | module.exports = function (api) { |
plugins: 配置插件,配置的插件可以控制如何转换代码。默认前缀babel-plugin (bable-plugin-myPlugin 等于 myPlugin)
注:插件在presets前运行,插件顺序是从前往后执行,而presets顺序是颠倒的(从后往前)
presets: 告诉babel要转换的源码使用了哪些新的语法特性,一个presets对一组新语法的特性提供了支持,多个presets可以叠加,其实就是一组Plugins的集合。
通常为分为三大类
- 已经被写入EMCAScript标准里的特性:ES2015, ES2016, ES2017,Env(包括当前所有EMCAScript标准里的最新特性)
- 被社区提出来但还未写入标准里的特性:stage0(不确定是否会纳入标准),stage1(值得被纳入),stage2(已被起草,将会被纳入),stage3(已定稿,各大浏览器厂商和node.js社区已开始着手实现),stage4(在接下来的一年将会加入标准)。
- 用于支持一些特定场景下的语法的特性,和ECMAScript标准无关,如babel-preset-react用于支持React开发里的JSX语法
webpack中如何接入babel?
通过loader去接入babel
1 | // npm install -D babel-loader @babel/core @babel/preset-env webpack |
使用postcss
postcss是一个css处理工具,包括向css自动加前缀,使用下一代css语法等。postcss和css的关系就像babel和javascript的关系。
webpack中如何接入postcss?
1 | // webpack.config.js |
使用react
核心:jsx解析,引入babel-preset-react
使用vue
核心:vue-loader,提取.vue文件中script、style、template,然后将他们交给对应的loader处理,vue-template-compiler
:将template编译成对应可执行的js代码,预先编译好的html模板相对于在浏览器中编译html模板性能更好。
webpack之优化
前言
优化主要可以分为两个层面
- 优化开发体验
- 优化构建速度
- 优化使用体验,通过自动化手段完成一些重复工作
- 优化输出质量
- 减少加载时间
- 提升代码性能
积少成多,水滴石穿!
开发体验篇
优化构建速度
缩小文件的搜索范围
webpack启动后从entry出发,解析出文件中的导入语句,再递归解析。
遇到导入语句做两件事:
- 根据导入语句找文件,例如
require('vue') => ./node_modules/vue/dist/vue/runtime/common.js(一般为package.json中定义的main)
,require('./util') => ./util.js
- 根据找到文件的后缀,使用配置中Loader去处理文件。
虽然这两件事情对于处理一个文件来讲是非常快的,但是当项目日益庞大后文件量会变得非常大,此时构建速度慢的问题就会暴露出来。
优化loader配置
由于loader对文件的转换操作很耗时,故让尽可能少的文件被loader处理,也是一个优化构建速度的方法。
由于loader是通过test, include, exclude来命中,故我们应该缩小匹配范围。
1 | modules:{ |
tip: rule.inlcude|rule.test|rule.exclude
是rule.resource.xxx
的缩写
优化resolve.modules配置
此配置用于配置webpack寻找第三方模块的目录,默认值为node_modules
,是相对路径,故默认行为是先去当前目录找,然后依次往上。但是在实际项目中,第三方模块都放在根目录下,故应使用绝对路径,使用绝对路径时,只会搜索给定目录。
1 | module.exports={ |
优化resolve.mainFields配置
此配置用于配置第三模块使用的入口文件,通常在第三方模块的package.json中有main, broswer, module
字段来描述入口文件。而该配置可以约束这一范围,通常配合target使用。
1 | module.exports={ |
webpack采用策略,是从数组中依次查找,没有就查找下一个,为了减少搜索步骤,在明确第三方模块的入口文件描述字段时,我们可以将它设置的尽量少,如大部分第三方模块都会申明main
字段,故可配置mainFileds:['main']
。
tip: 此方法有风险,只要有一个第三方模块无main
字段导致出错,也会造成构建的代码无法正常运行
优化resolve.extensions配置
在导入语句没带文件后缀时,webpack会尝试添加后缀询问文件是否存在,此配置用于配置webpack在尝试过程中用到的后缀列表,默认值为['.wasm', '.mjs', '.js', '.json']
。
故在配置时应遵守如下几点
- 后缀列表要尽可能小,不存在的后缀就无需添加上
- 频率高的文件放前面
- 在写入导入语句时,尽可能的带上后缀,避免寻找过程。
配置如下:
1 | resolve: { |
tip: 按照数组顺序依次询问,询问存在则停止
优化module.noParse配置
此配置可以让webpack忽略对部分没有采用模块化的文件的递归解析处理,从而提高构建性能。如jquery lodash
就是如此。
1 | module: { |
tip: 被忽略的文件不应包含import, require, define等模块化语句
,不然还导致构建出的代码中包括无法在浏览器环境下执行的模块化语句。
使用DllPlugin
dll
: 最初由microsoft引入的动态链接库,一个动态链接库包含为其他模块调用的函数和数据。
核心思想为:
- 基础模块抽离,打包为一个个单独的动态链接库,在一个动态链接库中可包含多个模块
- 当需要导入的模块存在于某个动态链接库时,这个模块不再被打包,而是从动态链接库中获取
- 页面依赖的所有动态链接库都需要被加载
其实,提升构建效率的核心就在于大量复用的模块只需编译一次。
在webpack中使用主要通过如下两个内置的插件接入。
- DllPlugin插件,用于打包出一个个单独的动态链接库,生成mainfest.json供DllReferencePlugin映射依赖项。
- DllReferencePlugin插件,用于在主要的配置文件中引入DllPlugin插件打包好的动态链接库文件
1 | // webpack.dll.config.js |
tip: DllPlugin中的name必须与output.library中保持一致,name会影响输出mainfest.json中的name字段的值,而DllReferencePlugin会去mainfest.json文件中读取name的值,将值的内容作为在从全局变量中获取动态链接库的内容时的全局变量名
使用HappyPack?thread-loader
HappyPack可以将任务分解给多个子进程去并发执行,子进程处理完后再将结果发给主进程。
tip: 由于js是单线程模型,想发挥多cpu的功能,只能通过多进程来实现,而无法通过多线程。
但是请注意,在webpack4中,HappyPack不一定适用,下面节选了该开源项目的README。
维护模式通知
Webpack’s native performance is improving and (I hope) it will soon make this plugin unnecessary.
// webpack4的性能正在不断提高,我希望这个插件将变得没有必要。
FAQ
Is it necessary for Webpack 4?
Short answer: maybe not.
Look at thread-loader and if it works for you - that’s great, otherwise you can try HappyPack and see which fares better for you.
// 如果thread-loader能满足你的需求,那最好,否则你可以试试HappyPack看看哪一种更合适。
主要原因是webpack4自身的优化提升和官方发布了一个和它做相同事情的loaderthread-loader.
虽然如此,我们还是简单了解下HappyPack的使用
1 | // webpack.config.js |
接下来我们来了解下thread-loader的使用
1 | module.exports = { |
使用UglifyjsWebpackPlugin?TerserWebpackPlugin
UglifyjsWebpackPlugin通过uglifyjs来压缩js。
1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); |
但是只支持es5,而且由于之前uglifyjs的分支uplifyjs-es不再维护,但该分支的性能测试中优于master三倍,故从此分支fork出了个新项目terser,terser最大程度兼容了uglifyjs-es和uglifyjs。同时支持es6.
TerserWebpackPlugin通过terser来压缩js,而且被应用至webpack.optimization.minimize中。
如果想要覆盖默认配置的话
1 | const TerserPlugin = require('terser-webpack-plugin'); |
优化使用体验
文件监听优化
文件监听是由webpack提供,其原理为webpack定时获取文件最后编辑时间并保存,若当前保存与获取不一致则视为文件发生变化。
可以直接配置watch
或设置devServer
(推荐)。
1 | devServer: { |
tip: poll 的 aggregateTimeout的优化会导致监听的灵敏度变低
热更新
原理:向开发的网页中注入一个代理客户端来连接devServer和网页(network中的websoket)
1 | devServer: { |
输出质量篇
优化加载时间
区分环境
webpack4推出的mode极大的促进了开箱即用性,每个mode下的默认配置都是很便利,在大多数情况下无需更多配置,所以一定要把握住。根据实际场景使用development、production
。
Option | Description |
---|---|
development |
Sets process.env.NODE_ENV on DefinePlugin to value development . Enables NamedChunksPlugin and NamedModulesPlugin . |
production |
Sets process.env.NODE_ENV on DefinePlugin to value production . Enables FlagDependencyUsagePlugin , FlagIncludedChunksPlugin ,ModuleConcatenationPlugin , NoEmitOnErrorsPlugin ,OccurrenceOrderPlugin , SideEffectsFlagPlugin andTerserPlugin . |
none |
Opts out of any default optimization options |
接入CDN
CDN一般会为资源开启很长时间的缓存,如何避免?
业界做法:其实就是只将静态资源js、css、图片等文件,开启cdn和缓存,同时为每个文件名带上由文件内容算出的hash值。
核心在于 output.publicPath设置为对应cdn域名,我们目前做法也是类似,只是多了ng映射。
使用Tree Shaking
production mode 默认会采用,但是为什么会无效呢?
tree shaking依赖于静态的es6模块化语法,故让tree shaking正常工作的前提是,提交给webpack的js代码必须要采用了es6的模块化语法。
而在我们的项目中常常会使用到babel,故需要babel保留es6模块化语句。
1 | "presets": [ |
tip:
提取公共代码
主要目的是为了避免相同资源重复加载,利用浏览器缓存去减少网络传输流量和降低服务器成本。
CommonsChunkPlugin(v3) => SplitChunksPlugin(v4)
chunk是一系列文件的集合,在一个chunk中会包含这个chunk的入口文件及其所依赖的文件。
使用为optimization.splitChunks
,webpack的默认配置为
1 | module.exports = { |
我们的核心在于cacheGroups,缓存组会继承splitChunks的配置,但是test、priorty和reuseExistingChunk只能用于配置缓存组。默认的缓存组权重为负数,自定义默认权重为0,故很容易覆盖。当然你也可以直接关闭默认配置。
实际使用
1 | { |
Prefetching/Preloading modules
tip: webpack4.6+支持
预拉取/加载模块:
- prefetch: 将来某些导航可能需要的资源
- preload: 当前导航可能需要的资源
1 | import(/* webpackPrefetch: true */ 'LoginModal'); |
对比:
- preload的模块是与父模块并行加载,而prefetch是等父模块加载完成再加载。
- preload具有中等优先级并且是即时加载,prefetch是浏览器空闲加载。
- 浏览器支持程度不一样
tip: 使用preload加载跨域资源如字体等需要加上crossorign,否则会加载两次,主要是跟浏览器加载不同资源的优先级规则相关
提升代码性能
作用域提升
Scope Hoisting :分析模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,但是前提是不能造成代码冗余,因此只有那些被引用了一次的模块才能被合并。
通过ModuleConcatenationPlugin去实现,现在product mode也会默认开启。
tip: 也是去分析模块之间的依赖关系,故仅适用于es6模块,注意babel中modules:false
打包分析工具
1 | webpack --mode production --profile --json > stats.json |
- webpack-chart: 交互式饼图
- webpack-visualizer: 可视化和分析.
- webpack-bundle-analyzer: 一个插件和CLI实用程序,(推荐,现在我们项目也是使用该插件)
- webpack bundle optimize helper: 此工具将分析您的打包,并为您提供有关改进措施的可操作建议,以减少打包大小。
- bundle-stats: 生成打包报告(包大小,资产,模块)并比较不同构建之间的结果。
展望webpack5
核心变更点:
- 使用持久化缓存提高构建性能;
- 使用更好的算法和默认值改进长期缓存(long-term caching);
- 清理内部结构而不引入任何破坏性的变化;
- 引入一些breaking changes,以便尽可能长的使用v5版本。
v3 => v4的重点在于webpack添加了默认值。(改革开放)
v4 => v5更像是优化内部问题,加强算法及缓存。(反腐维稳)