webpack理解

217 阅读8分钟

webpack理解

1、webpack是模块化打包工具,最早js文件通过script标签引入存在全局污染,模块依赖不清等问题,即使通过命令空间方式,每个模块只暴露一个全局对象,还是有依赖不清的问题

2、到后来前端框架不论vue,react,angular写得模块化语法,less,scss预编译器,浏览器兼容性,typescript语法,文件压缩合并,都不是浏览器能直接识别的,webpack就是提供了打包框架,能帮助将上述复杂的问题打包成浏览器能运行的代码

3、webpack默认是一个入口一个bundle输出,中间依赖会打包到一起,根据需要文件压缩,css编译,typescript转es5,vue,react框架转es5,js高版本转es5用babel等依靠插件和loader提供出的接口,完成输出

4、提供优化像tree-shaking ,split chunk ,多进程打包,缓存,css文件拆分独立静态文件,js和css文件预请求等

webpack构建流程

1、根据命令行和配置文件和内置默认配置参数合并成构建的参数

2、初始化Compiler对象,根据tapable注册钩子函数,执行插件注册插件的方法到生命周期的钩子函数上

3、Compiler执行run方法后,会触发compile,构建compilation对象,改对象是编译阶段的执行者,模块创建,依赖收集,分块,打包

4、当完成compilation对象后,就开始从entry入口读取文件,文件内容通过acorn做AST语法树,nodejs只识别js,json,node文件,其他文件格式从module.rules中匹配不同loader处理文件,根据做AST语法树返回内容递归解析文件内容,并做缓存,形成依赖树

5、然后生成chunks,chunks是有入口,异步加载,split chunk 三种方式,最后根据chunks输出生成文件

常见loader

1、style-loader css-loader MiniCssExtract.loader sass-loader less-loader postcss-loader

2、babel-loader ts-loader vue-loader

3、file-loader url-loader ,但是webapck5 这些都内置了,type配置 asset asset/resource

像这里面 style-loader 是做将css-loader的样式从字符串根据配置,用内联或者独立文件,内联会创建style标签,独立文件会动态载入,主要是用到pitch熔断,会将后续的sass-loader,css-loader的编译结构后,执行script标签,所以实际执行其实还是style-loader, sass-loader, css-loader

同样的方式在vue-loader里面也是类似的地方,我们vue文件编译中,会先进入vue-loader,把三块编译成不同的import语句后面带type,lang,id,scoped这些参数

loader可以同步可以异步,this指向webpack对象,不能用箭头函数,异步返回是this.callback

常见的plugin

mini-css-extract-plugin,clean-webpack-plugin, copy-webpack-pluin,terser-webpack-plugin,VueWebpackPlugin,DefinePlugin,html-webpack-plugin

plugin是类,提供apply方法,参数是compiler对象,通过tapable注册监听时间,也可以监听compilation对象上的事件,常用的有run,make,compile,compilation,seal,emit,done

调用有tab,tabSync,tabPromise

热更新原理

1、起http服务器,websocket链接,在入口entry中加入client.js和hotreplace.js客户端更新代码,webpack监听模式启动后监听到文件变化,生成update.json和hot.update.js文件,文件名中有新的hash值,客户端收到websocket消息,请求update.json文件,返回新的hash值,用老的hash值请求,前端返回的js代码更新依赖树,因为之前编译每个文件的依赖关系都收集好了,每个js文件有hot.module.accept中指定更新依赖关系,空就是文件自己,根据新的依赖树和更新的文件重新执行更新过的模块完成热更新

2、配置hot参数,按照webpack-dev-server包,如果hot.module.accept报错会刷新整个页面,所以hotOnly参数就报错不刷新

3、vite使用chokidar监听文件变化,webpack使用watchpack

借助webpack优化前端性能

1、script和css文件增加preload和prefetch属性,给js文件增加async和defer属性,给css文件增加media属性,这些都是浏览器渲染过程的优化,防止阻塞解析dom树和阻塞渲染树

*** preload 或 prefetch ***
preload 告诉浏览器立即加载资源,具有Hightest最高优先级;
prefetch 告诉浏览器在空闲时才开始加载资源,具有Lowest最低优先级;
preload、prefetch 仅仅是加载资源,并不会“执行”;
preload、prefetch 均能设置、命中缓存;
正确使用 preload、prefetch 不会导致重复请求;
在预加载启用CORS的资源(例如fetch、XMLHttpRequest 或字体)时,需要特别注意在你的link元素上设置crossorigin属性
<link rel="preload" href="fonts/cicle_fina-webfont.woff2" as="font" type="font/woff2" crossorigin />

***没有 defer 或 async 时***
浏览器会立即加载并执行指定的脚本,
“立即”指的是在渲染该 script 标签之下的文档元素之前,
也就是说不等待后续载入的文档元素,读到就加载并执行。

***有 async 时***
script.js会被异步加载,即加载和渲染后续文档元素的过程将和 script.js 的加载并行进行(异步)。
当 script.js加载完整立即执行script.js。执行script.js时,html解析暂停。  
从加载完成立即执行来看,async模式 执行顺序与写的顺序无关,不保证执行顺序。

***有 defer 时***
script.js会被异步加载,即加载和渲染后续文档元素的过程将和 script.js 的加载并行进行(异步)。
这一点与`async`模式一致。不同的是当 script.js加载完成并不会立即执行,而是在所有元素解析完成之后,
`DOMContentLoaded` 事件触发之前完成。因此它会按照写的顺序执行。

*** 总结 ***
综合来看,如果加上js文件写成preload和defer结合,即高优先级加载a.js,并在加载后DOMContentLoaded才执行
<link rel="preload" href='a.js' as="script"/>
<script src='a.js' defer/>

2、多入口,webpack默认是一个入口一个输出文件,多入口可以实现js文件拆分利用多个http请求

3、split chunk,手动拆包,将大文件拆成多个小文件,也是利用多个http请求

4、同理还有异步路由加载文件,也是会生成不同bundle利用http多个请求

5、代码压缩无论js,css,html都压缩降低文件大小

6、普片转base64减少文件请求数量

7、大型应用一个项目打出多套代码,根据入口和参数不同

8、hash值防止前端错误缓存

9、sourcemap查找错误代码

10、tree shaking 去掉没有使用的代码

11、内联chunk到html中,不用请求

12、element,antd,lodash 包的按需加载

13、不使用moment.js改为dayjs

14、pwd缓存,提前请求资源

15、gzip压缩

16、外联CDN公共三方库

如何提高webpack构建速度

1、多线程,在babel-loader编译,terser压缩js文件都可以设置多线程

2、缓存,同样babel-loader设置缓存

3、开发环境不压缩代码

4、合理使用sourcemap,不同环境配置不同,如果是有异常监控系统的像私有化部署的sentry还是有hidden-source-map就行,公开的用nosources-source-map,开发环境可以用cheap-module-source-map

5、优化loader配置,设置exclude去掉node_module打包,增加 oneOf 防止多次打包

微前端 webpack module Federation

module-federation.io/zh/

创建一个生产者

1. 初始化项目

使用 Rsbuild 来创建一个生产者

npm create rsbuild@latest

根据提示完成项目创建

? Input target folder -> federation_provider
? Select framework -> React
? Select language -> TypeScript

2. 安装 Module Federation 构建插件

根据上面初始化项目的步骤,我们创建了名为 federation_provider 的 React 项目,依次执行以下命令

cd federation_provider
npm add @module-federation/enhanced
npm add @module-federation/rsbuild-plugin --save-dev

3. 生产者导出模块

将入口改为异步

修改 index.tsx 异步加载 bootstrap.tsx

// src/index.tsx
import('./bootstrap');

新建 bootstrap.tsx

// src/bootstrap.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

增加 Button 组件

// src/button.tsx
export default function Button() {
  return <div>Provider button</div>;
}

通过 Module Federation 导出 Button 组件

rsbuild.config.ts

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';

export default defineConfig({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: 'federation_provider',
      exposes: {
        './button': './src/button.tsx',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
  server: {
    port: 3000,
  },
});

4. 启动生产者

npm run dev
➜  federation_provider npm run dev

> federation_provider@1.0.0 dev
> rsbuild dev --open

  Rsbuild v0.5.1

  > Local:    http://localhost:3000/
  > Network:  http://10.94.55.204:3000/
  > Network:  http://10.4.255.21:3000/

start   Compiling...
[ Module Federation Manifest Plugin ] Manifest Link: http://localhost:3000/mf-manifest.json

项目启动后出现 Manifest Link: http://localhost:3000/mf-manifest.json 信息,这是 Module federation 的 manifest 信息链接

创建一个消费者

1. 初始化项目

使用 Rsbuild 来创建一个消费者,调用以下命令:

npm create rsbuild@latest

根据提示完成项目创建

? Input target folder -> federation_consumer
? Select framework -> React
? Select language -> TypeScript

2. 安装 Module Federation 构建插件

根据上面初始化项目的步骤,我们创建了名为 federation_consumer 的 React 项目,依次执行以下命令

cd federation_consumer
npm add @module-federation/enhanced
npm add @module-federation/rsbuild-plugin --save-dev

3. 消费生产者

增加 Module Federation 插件消费远程模块

rsbuild.config.ts

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';

export default defineConfig({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: 'federation_consumer',
      remotes: {
        federation_provider:
          'federation_provider@http://localhost:3000/mf-manifest.json',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
  server: {
    port: 2000,
  },
});

将入口改为异步

修改 src/index.tsx

import('./bootstrap');

新建 src/bootstrap.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

在 tsconfig.json 中声明 Module Federation 类型路径

这个运行项目之后会自动在根目录创建这个目录

{
  "compilerOptions": {
    "paths": {
      "*": ["./@mf-types/*"]
    }
  }
}

引用远程模块

第一次vscode会提示没有federation_provider/button定义 ,运行之后创建@mf-types就好了

import './App.css';
// federation_provider 提供的远程组件
import ProviderButton from 'federation_provider/button';

const App = () => {
  return (
    <div className="content">
      <h1>Rsbuild with React</h1>
      <p>Start building amazing things with Rsbuild.</p>
      <div>
        <ProviderButton />
      </div>
    </div>
  );
};

export default App;

启动消费者

npm run dev
> federation_consumer@1.0.0 dev
> rsbuild dev --open

  Rsbuild v1.1.10Network:  http://192.168.56.1:2000/
  ➜ Network:  http://10.102.1.132:2000/
  ➜ Local:    http://localhost:2000/
  ➜ press h + enter to show shortcuts

start   Building...
[ Module Federation Dev Server ] Success Federated types extraction completed
[ Module Federation Manifest Plugin ] Info Manifest Link: /mf-manifest.json 
ready   Built in 1.25 s (web)

问题解决

样式隔离

要子应用自己做隔离,如react 用 css-module 方式,vue的scoped