第15期 - 栀子花香

封面图来源于张圆附近满墙的栀子花,喜欢的花铺满整个面非常的不错。

技术笔记-webpack相关

webpack 原理

devtool 调试 以便于在 index.js 文件中设置断点、查看变量、单步执行代码等

webpack可以将前端各种资源(包括CSS及其预编译方案、JS及其预编译方案)统一打包为.js文件和资源文件。

基本打包流程:
读取文件,分析模块依赖
对模块进行解析执行(深度遍历). 针对不同的模块使用不同的 loader. 编译模块,生成抽象语法树(AST)
遍历 AST,输出 JS.

chunk 在浏览器中加载时,可以通过网络请求获取,用于构建页面和提供功能。
bundle 包含多个 chunk,每个 chunk 可对应多个 module.

初始化项目对浏览器请求项目资源 http://trackercollect.xx.com/appInfo/bundle 为了更好地管理和优化资源加载,开发团队会将不同类型的静态资源(如 JavaScript、CSS、图片等)托管在不同的域名下,这样可以利用浏览器的并行下载机制提升页面加载速度。将特定功能或服务模块托管在单独的域名下,可以增加安全性和隔离性

webpack-dev-server 开发服务器

在本地开发环境中,通常会配置开发服务器(如 webpack-dev-server),它可能会实时打包并提供打包后的资源,负责本地文件的编译和输出以及监听,包括 bundle 文件。 将所有模块打包到一个bundle文件,我们会得到一个很大的文件,然而我们浏览器是可以并行下载多个文件的,这样下载一个大文件没法利用并行下载能力,导致资源加载速度较慢;
整个项目的代码被打包成一个大的 bundle,但在实际运行中,某些功能或页面可能并不需要在初始加载时加载全部代码,而是在用户操作或特定条件下才进行加载。这时就需要单独请求和加载特定的 chunk 文件。
一个bundle里的东西并不需要一次性加载,需要按照路由按需加载,这个时候就需要按需加载,拆分成不同的chunk. 必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积,编译的时候正确判断到底加载了那些模块,静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码.

热更新

为了实现热更新, 本地开发环境通常会请求 websocket 实现 允许开发者在不重新加载整个页面的情况下对代码进行更新和调试。
ws://xx.d.xxx.com:8081/sockjs-node/522/mnryftkf/websocket. 启动websocket服务,可以建立服务器和浏览器之间的双向通信。当监听到本地代码发生改变时,主动向浏览器发送新hash以及ok字段。

websocket仅仅用于客户端(浏览器)和服务端进行通信。而真正做事情的活还是交回给了webpack。热更新检查事件是调用reloadApp方法。利用node.js的EventEmitter,发出webpackHotUpdate消息

模块打包

读取webpack的配置参数;
启动webpack,创建Compiler对象并开始解析项目;
从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树,组装代码块 chunk. 对不同文件类型的依赖模块文件使用对应的Loader进行编译
整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。

其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compiler和compilation两个核心对象实现。

compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。 compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。

而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。

异步模块动态导入

采用JSONP的思路,首先,将动态引入模块单独打成一个js文件;其次,在import执行时创建script标签传入src为引入模块地址;从而实现动态加载的效果,注意,JSONP必然是异步的,所以必须要结合Promise;

根据 installedChunks 检查是否加载过该 chunk. 假如没加载过,则发起一个 JSONP 请求去加载 chunk. 设置一些请求的错误处理,然后返回一个 Promise。

  • 如果chunk没有被加载过,会为这个chunk创建一个promise对象.
  • 将promise对象存在promises数组中.
  • 将promise的resolve 和 reject存在installedChunks[chunkId]中.

详解 loader 和 plugin

loader

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 加载 模块时预处理文件。 webpack 只能处理 JavaScript 和 JSON 文件,通过配置不同的 loaders,可以扩展 Webpack 处理其他类型的文件

告诉 webpack 编译器(compiler) babel-loader 通过 Babel 转译 JavaScript 文件

module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
 }

webpack 对每个 .css 使用 css-loader,以及对所有 .ts 文件使用 ts-loader

module.exports = {
  module: {
    rules: [
      { test: /\.css$/, use: 'css-loader' },
      { test: /\.ts$/, use: 'ts-loader' },
    ],
  },
};

注意点

  • 在 webpack 配置中定义 rules 时,要定义在 module.rules 而不是 rules 中。
  • loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)。

loader 特性

loader 支持链式调用 loader 可以是同步的,也可以是异步的。 插件(plugin)可以为 loader 带来更多特性

loader 解析

遵循特定的规范进行解析,且 每个 loader 会接收前一个 loader 处理后的输出作为输入,最终输出给下一个 loader

module.exports = {
  module: {
    rules: [
    ],
  },
};

每次文件系统访问文件都会被缓存,以便于更快触发对同一文件的请求,启用 Watch 模式 则 webpack 将继续监听任何已解析文件的更改

module.exports = {
  //...
  watch: true,
};

https://webpack.docschina.org/configuration/watch/#watch

JavaScript 文件处理,将现代 JavaScript 语法转换为兼容更旧浏览器的语法。

{
     test: /\.js$/,  // 匹配所有以 .js 结尾的文件
     exclude: /node_modules/,  // 排除 node_modules 文件夹
     use: 'babel-loader',  // 使用 babel-loader 处理匹配的文件
   },

为同一个文件类型指定多个 loaders,形成一个处理链 这段配置会将 .scss 文件依次通过 sass-loader(将 Sass 转换为 CSS)、css-loader(解析 CSS)、style-loader(将 CSS 插入到 DOM 中)。

{
     test: /\.scss$/,  // 匹配所有以 .scss 结尾的文件
     use: ['style-loader', 'css-loader', 'sass-loader'],  // 使用 style-loader 和 css-loader
   },

Loaders 可接受 options 进行配置,或在 .babelrc 或 babel.config.js 文件中配置,根据你指定的浏览器兼容性目标自动决定需要的 Babel 插件和 polyfills。babel-loader 会自动读取 babel.config.js 文件中的配置

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: ['@babel/preset-env'],
    },
  },
}

处理多种文件类型,Webpack 的 loader 系统可以处理多种文件类型,不限于 JavaScript 和 CSS

{
  test: /\.(woff|woff2|eot|ttf|otf)$/,
  use: 'file-loader',
}

编写一个 loader

https://webpack.docschina.org/contribute/writing-a-loader/

plugin

loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量 用法:在 webpack 配置中,向 plugins 属性传入一个 new 实例

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件

module.exports = {
   plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};

自定义一个 plugin

webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。 pluginName 为驼峰式命名的插件名称

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log('webpack 构建正在启动!');
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;