学习资料:拉勾课程《大前端高薪训练营》
阅读建议:搭配文章的侧边栏目录进行食用,体验会更佳哦
内容说明:本文不做知识点的搬运工,技术详情请查看官方文档
接上一篇:webpack概念和理论探讨
前言:
在webpack这部分知识上停留了快三周了,学习进度也落后了两个模块。究其原因,除了懒这个主要的个人因素之外,还是webpack系列的知识太过繁杂了。唉!(一字包含千言万语…)
这篇文章写于2021年1月1号,新年伊始,多少得发表一些想法,但也不知道说啥,就用以下几个字表达吧。鼠实不易,牛转乾坤。漫漫征途,唯有奋斗!2021,大家一起加油!
好的,进入正题!
上一篇文章中主要从个人理解角度叙述了对webpack概念和理论的探讨,本篇文章所要探讨的内容如题,侧重于webpack构建实践的认识和总结,接下来的行文中会主要从以下两个部分来展开叙述:
- webpack实现基本打包构建任务
- webpack实现开发环境打包构建任务
对于webpack实现生产环境打包构建任务,它并不是简单学几个webpack配置项或者几个理论就可以hold住的,其打包构建需要考虑很多因素,有时甚至要为项目量身定制,这非常依赖于个人经验的积累。博主经验尚浅(未满一年),尚不能驾驭这个话题,待后续羽翼丰满,会再单独总结成一篇文章。
本文相关实践代码都已经放入了github中:https://github.com/iamjwe/webpack-demo
好的,缕清了行文主旨,接下来我们循序渐进不装杯,先探讨一下如何使用webpack实现基本打包构建任务吧。
一:webpack实现基本打包构建任务
在本部分中,我们去 TMD 开发环境和生产环境,也去TMD 各种群魔乱舞的优化。我们的目的很简单,就是通过webpack打包构建后,让我们的示例项目能够跑起来(模块打包成功、工程构建成功)。
下面简单介绍一下作为我们构建目标的示例项目。
1.构建目标项目
下面会一一列出在使用webpack打包构建时,所需要关注的目录结构和文件内容,它们分别是:
- 项目的目录结构
- 项目的package.json
- 项目的模板html文件(spa页)
- 项目的入口js文件
(1): 目录结构
(2): package.json
{
"name": "webpack-demo",
"version": "0.1.0",
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11"
}
}
(3): src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
(4): src/main.js
import Vue from 'vue'
import App from './App.vue'
import './style.less'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
2.引入webpack
webpack官方中文文档:https://webpack.docschina.org/
安装:
yarn add webpack webpack-cli --dev
配置:
项目根路径下新建webpack.config.js,写入打包构建的入口与输出位置。
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
3.引入loader
loader用于将模块的原内容按照需求转换成新内容。我们先观察一下示例项目中的资源文件,发现包含以下几种类型:
- 基本资源:.html、.js
- 静态资源:.ico
- 图片资源:.png
- 样式资源:.less
- 组件资源:.vue
webpack会把入口模块 src/main.js 中直接或间接引入的资源文件视为一个模块。所以示例项目中包含的资源模块类型,以及需要的loader对应于下表:
资源模块 | loader | 目的 |
---|---|---|
.js | babel-loader | 转换es6为es5 |
.png | file-loader / url-loader | 加载模块 |
.less | less-loader、css-loader、style-loader | 加载、转换、嵌入模块 |
.vue | vue-loader | 加载模块 |
经过以上分析之后,根据webpack loader list中对应的loader文档以及搜索npm中的对应loader包(版本更新)的安装配置方式,相关loader包安装命令如下:
# babel-loader
yarn add babel-loader @babel/core @babel/preset-env --dev
# file-loader、url-loader
yarn add file-loader url-loader --dev
# less-loader、css-loader、style-loader
yarn add less less-loader css-loader style-loader --dev
# vue-loader
yarn add vue-loader vue-template-compiler --dev
webpack loader配置如下:
const VueLoaderPlugin = require('vue-loader/lib/plugin')
// ...
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: "defaults"
}]
]
}
}
}, {
test: /\.(png|jpg|gif)$/i,
use: [{
loader: 'url-loader',
options: {
limit: 10 * 1024, // 10kb以下的模块文件以DataUrl的形式嵌入在·bundle.js中
esModule: false
},
}, {
test: /\.less$/,
use: [
'style-loader',
'css-loader',
"less-loader"
]
}, {
test: /.vue$/,
loader: 'vue-loader'
}, ],
}]
},
plugins: [
new VueLoaderPlugin(),
]
4.引入plugin
plugin用于增强webpack自动化能力。我们先分析列举一下我们需要的自动化能力:
- 自动化清除构建后的文件,防止干扰
- 自动化渲染并生成html模板
- 自动化拷贝静态资源文件
这些自动化需求对应于一些解决这些需求时常用包的表格如下:
自动化需求 | plugin |
---|---|
自动化清除构建后的文件 | clean-webpack-plugin |
自动化渲染并生成html模板 | html-webpack-plugin + DefinePlugin(可选) |
自动化拷贝静态资源文件 | copy-webpack-plugin |
经过以上分析,根据webpack plugin list中对应的plugin文档以及搜索npm中的对应plugin包(版本更新)的安装配置方式,相关plugin包安装命令如下:
yarn add clean-webpack-plugin html-webpack-plugin copy-webpack-plugin --dev
webpack plugin配置如下:
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// ...
plugins: [
new VueLoaderPlugin(),
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Tutorials',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html',
// templateParameters: { // 指定模板参数方式1
// BASE_URL: './public/'
// }
}),
new CopyWebpackPlugin({
patterns: [{
from: "public",
to: "public"
}, ]
}),
new webpack.DefinePlugin({ // 指定模板参数方式2
// 值要求的是一个代码片段
BASE_URL: JSON.stringify('./public/')
})
]
5.开始打包
执行命令:
yarn webpack -c webpack.config.js
或者在package.json中的scripts选项中配置一条script,通过yarn basePack执行:
"scripts": {
"basePack": "webpack -c webpack.config.js"
},
6.打包结果
7.运行测试
8.bundle.js分析
(1): src/assets/logo.png去哪了
虽然打包后的dist文件夹中没有看到logo.png这个资源,但是如上图运行结果,图片资源logo.png是加载正常的。这是因为该图片资源是的大小为6.68kb,小于我们为url-loader设置的阈值大小10kb。
所以该图片在被base64转码后以dataUrl的方式直接写进了bundle.js中以减少资源加载次数。在bundle.js中搜索logo.png,可以看到以下结果:
二:webpack实现开发环境打包构建任务
开发环境和生产环境的认识我在前端工程化之自动化构建这篇文章中探讨过一次,简单来说,开发环境就是开发者开发、构建、运行、调试代码的环境,使用webpack实现开发环境打包构建任务的需求也可以围绕这几点展开。
能细化的需求很多,接下来本文会主要关注于使用webpack来实现开发环境的几个主要需求,这些需求与其对应方案的映射表格如下:
需求 | 方案 |
---|---|
http服务访问 | devServer |
易于调试 | sourceMap |
加快构建速度 | HMR、… |
1.devServer
webpack提供配置项devServer来配置管理开发服务器。而开发服务器也在前端工程化之自动化构建这篇文章中探讨过一次。简单来说,通过开发服务器我们可以实现在开发环境下的http服务访问,就如在使用一个nginx或者Tomcat一样。
通过在webpack中配置开发服务器devServer后以http服务的方式访问项目,我们可以达到以下几个目的:
- 内存打包,减免IO输出步骤,加快构建速度
- 监听源码变化,自动刷新
- 反向代理,解决跨域问题
- 添加请求切面,可以方便更改host等
- 自动打开浏览器等
参考官方文档:webpack dev-server,对示例项目做出以下实践以支持devServer:
安装:
yarn add webpack-dev-server --dev
配置:
devServer: {
contentBase: './public', // web root: ram data and public dir data
port: 9000,
proxy: {
'/api': {// http://localhost:8080/api/users -> https://api.github.com/users
target: 'https://api.github.com',
pathRewrite: {
'^/api': ''
},
changeOrigin: true // 不能使用 localhost:8080 作为请求 GitHub 的主机名
}
}
},
为package.json的scripts加上start:dev与build:dev方便运行:
"scripts": {
"basePack": "webpack -c webpack.common.js",
"start:dev": "webpack serve -c webpack.dev.js",
"build:dev": "webpack -c webpack.dev.js"
},
2.sourceMap
sourceMap可以解决源代码与运行代码不一致所产生的调试困难问题。在webpack中配置sourceMap之后,一旦运行构建后的代码出现错误,我们就能够快速定位到对应的源码中出现错误的位置,极大提升解决bug的效率。
在学习sourceMap在webpack中的如何配置使用之前,我们先思考一波解决调试问题需要考虑的内在逻辑:
- 源代码构建时,不但要生成构建后的代码,还要生成一份源代码 – 构建后代码的映射关系文件(如map文件)
- 构建代码运行时,运行环境(如浏览器)下载好构建后的代码、map文件和源代码资源(后两者也可能是懒加载)
- 一旦运行出现错误,运行环境(如浏览器)构建后代码的错误位置加上之前生成的 源代码 – 构建后代码的映射关系 计算得到在源码的错误位置
- 运行环境(如浏览器)反馈给开发者
对于sourceMap更加细节的内容以及原理在本文中不做研究,接下来探讨sourceMap在webpack中的配置使用。
在webpack中控制是否生成以及如何生成sourceMap文件的配置项为devtool,从(webpack devtools配置文档)中我们可以看到该配置项有一张表格记录了26种配置值,并且分别从构建速度、重新构建速度、生产环境、品质(quality)四个维度来进行比较,下表列举出官方推荐的开发环境适用的配置值。
devtool | 构建速度 | 重新构建速度 | 生产环境 | 品质(quality) |
---|---|---|---|---|
eval | 非常快速 | 非常快速 | no | 生成后的代码(语法转换后,模块合并后) |
eval-source-map | 慢 | 比较快 | no | 原始源代码(语法转换前、模块合并前) |
eval-cheap-source-map | 比较快 | 快速 | no | 转换过的代码(仅限行,语法转换后,模块合并前) |
eval-cheap-module-source-map | 中等 | 快速 | no | 原始源代码(仅限行,语法转换前,模块合并前) |
以下简单记录一下个人对以上几种devtool配置方式的理解:
- eval:使用 eval()运行每个模块(可以定位错误所在的模块)
- source-map:构建后代码以及源代码精确完整的映射关系。
- cheap-source-map:追求cheap的映射关系,不追求列映射,不追求语法转换映射。
- cheap-module-source-map:在追求cheap的情况下,追求语法转换映射。
根据以上表格提供的信息,在理解了几个标记的含义之后,我们就可以选择sourceMap的具体策略了。
从开发环境调试追求映射到源码的程度以及构建速度这两点,以及考虑到开发者开发的代码行通常不会太长(有错误时定位到行就够了),对于常见的代码逻辑调试,我们选择eval-cheap-module-source-map,牺牲一点构建速度,追求更加完整的映射到源码的程度。
而对于其它非业务逻辑调试的调试目的,我们有时可以选择eval甚至可以用上表没有出现的none策略。
而对于生产环境下的sourceMap配置,下面一并探讨完吧。
(2): 生产环境sourceMap
下表是官方推荐的生产环境适用的配置值:
devtool | 构建速度 | 重新构建速度 | 生产环境 | 品质(quality) |
---|---|---|---|---|
(none)(省略 devtool 选项) | 非常快速 | 非常快速 | yes | 打包后的代码 |
source-map | 慢 | 慢 | yes | 原始源代码 |
hidden-source-map | 慢 | 慢 | yes | 原始源代码 |
nosources-source-map | 慢 | 慢 | yes | 无源代码内容 |
通常来说,生产环境下是不会有调试需求的,所以我们通常选择none即可。但是也不能那么死板,就个人理解来说,如果开发测试有多轮提测的话,个人认为至少第一轮测试时是可以考虑考虑选择使用source-map的,毕竟这第一轮的测试肯定会且断断续续的发现并反馈很多的代码逻辑错误。这样做的优点是我们可以具备现成的调试环境,超快速的定位一些小问题。
(3): 实践配置
经过上面对sourceMap的探讨之后,以下是实践配置:
devtool: 'eval-cheap-module-source-map', // dev
devtool: false, // prod
3.HMR(Hot Module Replace)
模块热替换(HMR – hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:
- 保留在完全重新加载页面期间丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。
上述文字参考于官方文档,https://webpack.docschina.org/concepts/hot-module-replacement/
那么如何实现模块的HMR功能呢,对不同类型的模块,个人做了以下分类:
- 引用模块的HMR:重新加载实现,如bundle.js中的外部资源(如大图片)发生了变化
- 静态模块的HMR:直接替换实现,如bundle.js中的样式资源发生了变化
- 动态模块的HMR:逻辑控制实现,如bundle.js中的js代码逻辑发生了变化
对于引用模块类型以及通常由对应loader内置实现了的HMR替换逻辑的静态模块类型下面不做探讨,接下来我们简单举一个动态模块的HMR示例。
(1): 打开配置
const webpack = require('webpack')
module.exports = {
//...
devServer: {
hot: true,
// hotOnly: true, // 构建失败时不会回退,不会自动刷新丢掉错误日志,方便调试
},
plugins: {
new webpack.HotModuleReplacementPlugin()
}
};
(2): 最简示例
// 本模块热替换时的回调
if(module.hot) {
console.log('-------HMR----------') // 自动刷新导致看不到
}
总结来说,实现动态模块的HMR会比较繁琐且需要在业务代码中加上一些额外的逻辑。看情况使用吧,这里就不多做探讨了。值得一提的是,我们使用的集成开发框架大部分是内置了比较好的HMR支持的,实现上我猜测它是把HMR功能写在了业务组件调用执行的某一个切面上,从而实现不侵入我们写的业务代码并支持HMR功能的。
本文结束,谢谢观看。
如若认可,点赞收藏。