🔥首先明确这篇文章目标,就是前端工程化,而webpack是我们工程化之一。通过编写一个webpack的配置,理清楚我们为什么要写这些配置。也就是说:我们的配置是服务于我们的想法,写的配置也是服务于我们的项目,根据我们的项目情况跟想要的效果进行编写,所以在这里并不会太多在于webpack有哪些模块,模块里面又有哪些参数。
项目效果:
上面说了,要围绕想法跟项目写配置。这个项目是一个多页面(每个页面是一个spa)的ssr项目,也就是第一篇 core(这篇后续会重新整理写出来,现在有点乱)所讲到的。通过webpack配置注入我们的tpl文件中,服务端(node + koa)渲染返回页面。
一、工程化过程
💡:首先要先了解这个过程到底经历了什么?
❗️这里会感觉到奇怪,为啥要生成tpl,为啥不是html呢?
二、为什么还要用 .tpl?
- 语义区分
.tpl明确告诉团队:这是模板文件,需要渲染.html更容易让人误以为是最终静态页面
- 项目规范
- 多页面 SSR 或前后端分离项目,通常会统一模板后缀(tpl、ejs、hbs 等)
- 方便 Webpack 或后端渲染系统识别、批量处理
- 模板引擎约定
- 有些模板引擎默认只处理特定后缀的文件
- 比如 Nunjucks 可以配置
.tpl、.njk,默认可能不解析.html
实现
一、webpack 工程化搭建
前置行为-搭建结构
💡 在我们现实开发中会有开发环境跟生产环境,而在这两个环境的配置是有所不同的。
所以我们的第一步就是要创建,分好我们的文件目录结构,每个文件我们要写啥,哪些复用,我们要先有想法。而这里就分为两种 dev|prod。而在这之上就有一个base文件去写共用的,也就是
输入与输出
🔥 首先不管我们中间到底要配置多么完美的参数
❗️ 首先就得明确告知配置,你从哪进,从哪出。
🙋♂️一个入口还是两个?而我这里项目写的是多页面的spa项目,考虑到后期项目逐渐加功能也会增加入口,那不能每次加一个页面,我还得改配置吧?所以这里就用了动态引入。
然后挂载到 entry:pageEntries
🤔️ 输出啥呢❓
🫡 因为:产物输出路径,因为开发跟生产环境输出不一致,所以在各自的环境中运行。
webpack.dev.js
可能看到这里就有点奇怪了❓publicPath为啥有http,大家可以看到后面就知道了,涉及到热更新(现在基本都有内置了,不会这么写)。
webpack.prod.js
💥好了,到这里我们就根据自己项目跟想法设置了输入输出,各位同学可以看看自己的项目然后配置自己的输入输出。
然后我们会看文章最上方的流程图。有了输入输出,中间一个大的模块是解析模块,也就是说通过我们的解析配置解析代码,然后输出产物。
而我们平常要写的文件有啥?vue,css, less, png等等,那我们是不是主要就是针对他们去写配置。
所以❗️
module 来了
如果各位同学项目中还有其他的文件,那就是接着往上面写配置。
prod
plugin:在给你的项目增加额外的能力
在这里你可以理解为给你项目增强能力,这里是通用的。在dev / prod 里面会有所不同,毕竟不同环境下的操作。
dev在开发环境下,代码改动很频繁,每次改变都要编译一次?是不是很费事?为了减少重复性操作,我们就想到了热更新。
prod 而在生产环境我们考虑的因素会更多,打包结果的大小,加载文件时候的速度,打包的产物有些是不是重复打包了,怎么减小大小,加快打包速度等等,怎么结合好浏览器的机制优化。
而在我们前端最常提到的一个优化的手段就是分包
这个时候~~~~
optimization来了
🔥 这里去如何拆包并不是你随便搜一篇教程复制就可以用(虽然大多时候可以用,但是我们要在了解自己项目的情况下进行拆包,效果才会更好)。我们所要遵循的就是,让变化不大的在一起,变化大的在一起,配合浏览器缓存等达到优化项目的目的,是缓存与请求成本之间的平衡。
prod
而到这里我们再回过头看,输入与输出 那里提到的热更新,其实现阶段很多都已经内置了,webpack可以安装一个webpack-dev-server 去启动热更新,这里我们是自己开启一个热更新。
前面也提到我们更多是要理解,了解,而不是工具的使用。
热更新
首先我们了解热更新到底是咋样的?
现在热更新在写的项目中的位置逻辑
大家可以注意有一个地方是,注入文件!!还有就是前面的http,我指的文件路径问题,也就是说我的文件是存储在server里,当我更新的时候就是更新这里的文件。
这个的时候,帮我们解决了什么问题?一句话:
代码改了
↓
页面局部更新
↓
不刷新整个浏览器
没有这个东西是啥样的呢?
改代码
↓
重新打包
↓
浏览器刷新整个页面
↓
状态全部丢失
好的,那就好奇了,他是怎么知道的呢? 从server我们就知道了。
他是依赖于服务器上实现的一套机制
需要服务器?
因为: 浏览器:
不能直接监听你电脑里的源码文件
那现在我们就实现一个热更新!
那前面也说了,我们需要一个服务器,这里就用了express.
1⃣️ 第一步,启动一个服务器
按照官网缩写,我们需要为每一个入口注入一段代码。
加载热替换模块
根据你的 webpack 配置创建一个“webpack 编译引擎(compiler)”,这个引擎负责把源码解析、打包、生成模块依赖关系,并且提供 run/watch 等能力;后面的 devMiddleware 和 hotMiddleware 都不是直接做编译的,它们只是“依赖并使用 compiler”,一个负责把编译结果转成可访问的内存资源,一个负责监听 compiler 的编译完成并通知浏览器做热更新,所以 compiler 是整个 webpack 开发服务器和热更新机制的核心驱动对象。
课后小话题:
多页面构建
🔥 在开头输入,输出那里定义了多个入口,每个都是一个页面,打包后对应各自的bundle。
在浏览器中,页面用url去请求,后端返回渲染好的模板,也就是 服务端渲染(ssr)。我们也可以在服务端就对模板进行处理。
而在这里每个页面,又是一个spa页面。
通过获取我们多个页面入口,循环遍历配置webpack,后面我们只要增加页面文件即可
最主要是chunks,项目是多个入口,这里会去分析 决定:“这个页面到底注入哪个 JS” 。 而又会引出 MPA另一个问题,重复引用同一个js,而这时候webpack模块化就派上了用场。
这时候就引出我们的👇
分包策略
把一个大的js,拆成小的js,按需加载
从我们主入口开始,代码文件里面有import,require,而webpack就是扫描这些形成依赖图。
而在模块文件中就会有 重复引入,第三方库,组件,js.....
这时候就会产生区别,a文件是第三方库长期不会修改,common通用文件经常不修改,代码文件经常修改。
那我们就从这里入手。
在分包策略中,其实是“网络请求,浏览器缓存,cpu解析,代码复用,更新粒度”之间的平衡。
首先我们打包出来的文件,都带了hash。
而我们浏览器在发起请求时候也是请求url,那么这时候我们修改文件会影响到浏览器加载,重复下载。
所以这时候,我们把第三方库(基本不变化),common(改动频率小),pagejs(经常改动)打出三种包。
而且这样文件更小,加快加载性能。
http的话,打多少个包会影响到浏览器并行发起请求数,但是http2的更加是要考虑包大小了,请求数并不是问题了。
然后 Webpack内部:
会生成:ChunkGraph记录:哪个模块属于哪个 chunk;
💣:题外话,我们聊一聊早期的浏览器,那时候:
<script src="a.js"></script>
浏览器遇到👆会干嘛?加载!把js代码丢进去js引擎->eval(ajscode)
执行环境挂载,window.a = 111
js一多,出现全局污染。
--------------------------------------------------
# 浏览器根本没有:
- 模块
- import
- chunk
- 依赖图
- 私有作用域
于是,IIFE(立即执行函数)出现 (function(){})(),函数作用域带来了天然隔离
webpack核心来了:“自动给每个模块包一层函数”
---------------------------------------------------
# 那模块之间怎么互相引用?
例如:
## home.js
export const name = 'lin'
## user.js
import { name } from './home'
Webpack会变成:
function home(module,exports){
exports.name = 'lin'
}
然后:
function user(module,exports,__webpack_require__){
const home = __webpack_require__('./home')
}
-------------------------------------------------
webpack_require
# 模块仓库
var modules = {
'./home': function(module,exports){
exports.name = 'lin'
}
}
# require
function __webpack_require__(id){
const module = { exports:{} }
modules[id](module,module.exports)
return module.exports
}
于是:
const home = __webpack_require__('./home')
成功。
--------------------------------------------
这个时候,分包来了。
modules = {
'./home': fn,
'./user': fn,
'./editor': fn
}
都在一个modules里面,就一个bundle
就会出现非常大,有些首页根本用不到。
-------------------------------------------
# 于是“切割模块仓库”
原本:
一个 modules
变成:
# main chunk
# import 是同步
意思:
启动必须有
# import() 是异步
意思:
以后再加载也行
# Webpack 分包本质:
把 modules 仓库拆成多个 JS 文件
需要时动态下载
再把模块追加回模块系统
------------------------------------------
# 为什么需要 runtime
因为:
浏览器:根本不懂 chunk
~~空虚寂寞冷,所以要来个懂他的人~~
必须有一段代码负责:
- 下载 chunk
- script 注入
- Promise 管理
- 模块注册
- chunk 状态管理
这就是:
runtime.js:runtime 单独拆包
因为:
它是整个 Webpack 模块系统的“操作系统”
热更新
HMR 本质不是“刷新页面”
而是:替换 modules 仓库里的某个模块函数,衔接👆
然后重新执行这个模块。
本质:
把旧模块函数
替换成新模块函数
然后重新执行
本质是:本地 dev server + websocket + 内存编译 + 模块替换
上面 加载热替换模块 写了,用的是 express作为服务器。
我们再重新回看这张图,是不是会再清晰一点
用的是
然后我们通过编译打包形成 chunks
开头就提到的 http,存放到内存当中
然后当 http 路径改变,加载了不同的资源文件。
🎉文章结束,后续还会再继续更新补充🎉