全栈日记之工程化设计(webpack)

32 阅读10分钟

🔥首先明确这篇文章目标,就是前端工程化,而webpack是我们工程化之一。通过编写一个webpack的配置,理清楚我们为什么要写这些配置。也就是说:我们的配置是服务于我们的想法,写的配置也是服务于我们的项目,根据我们的项目情况跟想要的效果进行编写,所以在这里并不会太多在于webpack有哪些模块,模块里面又有哪些参数。

项目效果:
上面说了,要围绕想法跟项目写配置。这个项目是一个多页面(每个页面是一个spa)的ssr项目,也就是第一篇 core(这篇后续会重新整理写出来,现在有点乱)所讲到的。通过webpack配置注入我们的tpl文件中,服务端(node + koa)渲染返回页面。

一、工程化过程

💡:首先要先了解这个过程到底经历了什么?

image.png
❗️这里会感觉到奇怪,为啥要生成tpl,为啥不是html呢?

二、为什么还要用 .tpl

  1. 语义区分
    • .tpl 明确告诉团队:这是模板文件,需要渲染
    • .html 更容易让人误以为是最终静态页面
  2. 项目规范
    • 多页面 SSR 或前后端分离项目,通常会统一模板后缀(tpl、ejs、hbs 等)
    • 方便 Webpack 或后端渲染系统识别、批量处理
  3. 模板引擎约定
    • 有些模板引擎默认只处理特定后缀的文件
    • 比如 Nunjucks 可以配置 .tpl.njk,默认可能不解析 .html

实现

一、webpack 工程化搭建

前置行为-搭建结构

💡 在我们现实开发中会有开发环境跟生产环境,而在这两个环境的配置是有所不同的。
所以我们的第一步就是要创建,分好我们的文件目录结构,每个文件我们要写啥,哪些复用,我们要先有想法。而这里就分为两种 dev|prod。而在这之上就有一个base文件去写共用的,也就是
image.png

输入与输出

🔥 首先不管我们中间到底要配置多么完美的参数
❗️ 首先就得明确告知配置,你从哪进,从哪出。
🙋‍♂️一个入口还是两个?而我这里项目写的是多页面的spa项目,考虑到后期项目逐渐加功能也会增加入口,那不能每次加一个页面,我还得改配置吧?所以这里就用了动态引入。
然后挂载到 entry:pageEntries

image.png

🤔️ 输出啥呢❓
🫡 因为:产物输出路径,因为开发跟生产环境输出不一致,所以在各自的环境中运行。

webpack.dev.js

image.png

可能看到这里就有点奇怪了❓publicPath为啥有http,大家可以看到后面就知道了,涉及到热更新(现在基本都有内置了,不会这么写)。

webpack.prod.js

image.png

💥好了,到这里我们就根据自己项目跟想法设置了输入输出,各位同学可以看看自己的项目然后配置自己的输入输出。

然后我们会看文章最上方的流程图。有了输入输出,中间一个大的模块是解析模块,也就是说通过我们的解析配置解析代码,然后输出产物。
image.png
而我们平常要写的文件有啥?vue,css, less, png等等,那我们是不是主要就是针对他们去写配置。

所以❗️

module 来了

image.png
如果各位同学项目中还有其他的文件,那就是接着往上面写配置。
prod
image.png

plugin:在给你的项目增加额外的能力

image.png

在这里你可以理解为给你项目增强能力,这里是通用的。在dev / prod 里面会有所不同,毕竟不同环境下的操作。
dev在开发环境下,代码改动很频繁,每次改变都要编译一次?是不是很费事?为了减少重复性操作,我们就想到了热更新

image.png

prod 而在生产环境我们考虑的因素会更多,打包结果的大小,加载文件时候的速度,打包的产物有些是不是重复打包了,怎么减小大小,加快打包速度等等,怎么结合好浏览器的机制优化。

image.png

而在我们前端最常提到的一个优化的手段就是分包

这个时候~~~~

optimization来了

image.png

🔥 这里去如何拆包并不是你随便搜一篇教程复制就可以用(虽然大多时候可以用,但是我们要在了解自己项目的情况下进行拆包,效果才会更好)。我们所要遵循的就是,让变化不大的在一起,变化大的在一起,配合浏览器缓存等达到优化项目的目的,是缓存与请求成本之间的平衡。

prod

image.png

而到这里我们再回过头看,输入与输出 那里提到的热更新,其实现阶段很多都已经内置了,webpack可以安装一个webpack-dev-server 去启动热更新,这里我们是自己开启一个热更新。
前面也提到我们更多是要理解,了解,而不是工具的使用。

热更新

首先我们了解热更新到底是咋样的?
现在热更新在写的项目中的位置逻辑
大家可以注意有一个地方是,注入文件!!还有就是前面的http,我指的文件路径问题,也就是说我的文件是存储在server里,当我更新的时候就是更新这里的文件。 image.png

image.png 这个的时候,帮我们解决了什么问题?一句话:

代码改了  
↓  
页面局部更新  
↓  
不刷新整个浏览器  

没有这个东西是啥样的呢?

改代码  
↓  
重新打包  
↓  
浏览器刷新整个页面  
↓  
状态全部丢失

好的,那就好奇了,他是怎么知道的呢? 从server我们就知道了。
他是依赖于服务器上实现的一套机制

需要服务器?

因为: 浏览器:

不能直接监听你电脑里的源码文件

那现在我们就实现一个热更新!

那前面也说了,我们需要一个服务器,这里就用了express.

1⃣️ 第一步,启动一个服务器
image.png 按照官网缩写,我们需要为每一个入口注入一段代码。

image.png

加载热替换模块

image.png

image.png 根据你的 webpack 配置创建一个“webpack 编译引擎(compiler)”,这个引擎负责把源码解析、打包、生成模块依赖关系,并且提供 run/watch 等能力;后面的 devMiddleware 和 hotMiddleware 都不是直接做编译的,它们只是“依赖并使用 compiler”,一个负责把编译结果转成可访问的内存资源,一个负责监听 compiler 的编译完成并通知浏览器做热更新,所以 compiler 是整个 webpack 开发服务器和热更新机制的核心驱动对象。

image.png

image.png

课后小话题:
多页面构建

🔥 在开头输入,输出那里定义了多个入口,每个都是一个页面,打包后对应各自的bundle。
在浏览器中,页面用url去请求,后端返回渲染好的模板,也就是 服务端渲染(ssr)。我们也可以在服务端就对模板进行处理。

image.png
image.png
而在这里每个页面,又是一个spa页面。
通过获取我们多个页面入口,循环遍历配置webpack,后面我们只要增加页面文件即可

image.png

最主要是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作为服务器。

image.png

我们再重新回看这张图,是不是会再清晰一点
image.png

用的是

image.png

image.png
然后我们通过编译打包形成 chunks

image.png
开头就提到的 http,存放到内存当中 然后当 http 路径改变,加载了不同的资源文件。

🎉文章结束,后续还会再继续更新补充🎉