webpack打包手动实现

303 阅读14分钟

实操流程

1.初始化

在终端输入 npm init,会自动生成一个package.json的文件,这就是管理项目中使用包的一个文件

  "name": "webpacktest",
  "version": "1.0.0",
  "description": "test webpack how to do",
  "main": "index.js",   // 这是项目的入口文件
  "scripts": {   // scrpit里 打包,启动,测试各种操作的脚本启动入口
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

image.png

2.初始化一个前端项目

安装webpack

npm  i webpack  webpack-cli  -D  

  前面的“webpack"指得是安装,后后面的"webpack-cli"指的是安装了webpack-cli后,就可以把webpack这个词当作命令来用了

  • webpack:是webpack工具的核心包
  • webpack-cli:提供了一些在终端中使用的命令
  • -D(--save-dev):表示项目开发期间的依赖,也就是:线上代码中用不到这些包了 安装完以后的截图 比起前一步时,多了两个依赖 image.png
如何打包一个文件

在同层目录下,新建一个main.js的文件修改package.json里script的打包命令 webpack ./main.js

分析: 在同层目录中,自动生成一个dict文件夹,里面就是打包生成的文件。 你会发现打包的文件名和你原始生成的没什么区别,因为现在还没有新增打包命名的一些模块。 打包的文件会压缩一些空格之类的。

在build打包时,也可以新增--mode development 指令,说明目前打包的环境

对一个前端页面打包

前面我们实现了对一个js文件的打包,下面开始新建一个html+css+js的打包

步骤:

  1. 在目录下新建一个src文件夹,在文件夹里新增对应的文件即可
  2. 同样修改 package.json,新增语句"buildSrc": "webpack ./src/index.js --mode development",采用npm run buildSrc,即可得到打包后的结果

到这里你会发现,其实一直在改build文件,那可不可以直接一个文件解决了,避免一直重复build,新增命令

统一webpack配置

将脚本命令改为"build" : "webpack",这个默认新增语句 webpack.config.js,意思: 当执行npm  run  build  时,会自动找到webpack.config.js文件,执行里面的内容 新建一个webpack.config.js文件,代码如下:

const path = require('path');

module.exports = {
    entry: './src/index.js',   // 入口文件
    output: {
        path: path.join(__dirname, './dist'),  // 出口文件
        filename: 'app.js',  // 出口文件名
    },
    mode: 'development', // 打包的模式
};

这个打包配置与前面 npm run buildSrc 一样,均可以对src里的文件打包

webpack-dev-server设置

webpack-dev-server 是使用webpack必备的功能插件

作用:为使用webpack打包提供一个服务器环境(打一个虚拟包放到服务器里面去)

  • 自动为我们的项目创建一个服务器
  • 自动打开浏览器
  • 自动监视文件变化,自动刷新浏览器(当改动一个或几个地方代码时,会自动刷新浏览器)

直白解释: 在使用脚手架生成的文件中,使用npm start就能实现项目的启动,以及更新了代码保存后,就可以直接刷新浏览器看到效果,就是这个插件的原因

大概流程是我们用webpack-dev-server启动一个服务之后,浏览器和服务端是通过websocket进行长连接,webpack内部实现的watch就会监听文件修改,只要有修改就webpack会重新打包编译到内存中,然后webpack-dev-server依赖中间件webpack-dev-middleware和webpack之间进行交互,每次热更新都会请求一个携带hash值的json文件和一个js,websocket传递的也是hash值,内部机制通过hash值检查进行热更新

先安装npm  i  webpack-dev-server,config.js中修改devServer的配置

 devServer: {
    open: true,   // 自动打开浏览器
    hot: true,   // 热更新,修改代码,保存后,会直接更新
    port: 3001,  // 打开的端口
    static: './dist',   // 想要展示的文件位置
},

写到这里,项目采用命令启动以后,是不是发现样式没有加载了,这就得开始使用loader解决这个问题了

webpack的loader

样式处理

1.先安装 style-loader,css-loader两个包

2.修改config配置

(这里使用的样式打包方式,是采用import导入的)

module: {
    rules: [
        {
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: path.resolve(__dirname, './node_modules'),
        },
        {
            test: /\.css$/,
            // 注意:webpack中的使用loader时,是倒序处理的,所以use中同样需要倒序放置
            use: ['style-loader', 'css-loader'],
        },
        {
            test: /\.scss$/,
            use: ['style-loader', 'css-loader', 'sass-loader'],
        },
    ],
},

Tip: 在html文件采用link标签引入样式,怎么解决打包问题

采用如下的编译器就可以啦

这里写一下不成熟的理解哈:是否是因为link标签引入,天然的自带了style-loader处理后的内容,所以只需要对css文件处理即可

 {
    test: /\.css$/i,
    exclude: /\.file.css$/i,
    loader: 'css-loader',
},
{
    test: /\.file.css$/i,
    type: 'asset/resource',
},
图片处理

img标签 src属性 引入的图片,自动被打包了

如果是在css文件中,url方式引入的,则需要添加处理器

webpack5目前是推荐资源模块来处理www.webpackjs.com/guides/asse… ,如下代码块所示:

{
    test: /\.(png|jpg)$/,
    type: 'asset/resource',
},
webpack的plugin插件

html-webpack-plugin是必备的插件

作用: (1)能够根据指定的模板文件index.html,自动生成一个新的index.html,并且注入到dist文件夹下

(2)能够自动引入js文件

const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'app.js',
    },
    mode: 'development',
    plugins: [
        new htmlWebpackPlugin({
            template: path.join(__dirname, './src/index.html'),
        }),
    ],
};
clean-webpack-plugin 清除上次打包依赖

1.npm i clean-webpack-plugin

2const {CleanWebpackPlugin} = require('clean-webpack-plugin');

3.在plugin中新增new CleanWebpackPlugin()

如何优化打包

考虑的点就是两个:

1.减少时间(提高速度)

2.减少体积

减少打包时间

1.优化loader

{
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
        loader: 'babel-loader',
        options: {
            cacheDirectory: true, // 启用缓存
        },
    },
    include: [resolve('src')], // 只在src文件夹下查找
    // 不去查找的文件夹路径,node_modules下的代码是编译过得,没必要再去处理一遍
    exclude: /node_modules/,
},

对于Loader来说,首先优化的当是babel了,babel会将代码转成字符串并生成AST,然后继续转化成新的代码,转换的代码越多,效率就越低。 首先可以优化Loader的搜索范围。另外可以将babel编译过文件缓存起来,以此加快打包时间,主要在于设置cacheDirectory

或者利用线程:合理配置Loader来减少对文件的处理时间,提升打包速度。可以使用thread-loader将Loader的执行转移到Worker池中,使用cache-loader缓存Loader的执行结果等。

减少打包体积

1.代码分离

通过将代码分割成不同的块,按需加载,可以减小初始加载的文件体积,提高页面加载速度。可以使用Webpack的内置功能,如动态import()语法或SplitChunksPlugin插件来实现代码分割。

懒加载实现: Webpack提供的动态import()语法或使用@babel/plugin-syntax-dynamic-import插件来实现按需加载。优化用户体验,首屏加载速度提升

2.静态资源体积优化

图片压缩:使用url-loaderfile-loader对图片进行处理,并使用压缩工具(如imagemin)进行图片压缩,以减小输出文件的体积。

test: /\.(png|jpe?g|gif)$/i,
use: [
  {
    loader: 'url-loader',
    options: {
      limit: 8192,
      outputPath: 'images',
      name: '[name].[hash:8].[ext]',
    },
  },
]

3.tree shaking减少输出的体积

使用optimization中的usedExports: true和sideEffects: false配合Babel的@babel/preset-env或TypeScript的tsconfig.json中的"module": "esnext",来消除未使用的代码,减小输出文件的体积。

4.css代码压缩

CSS代码压缩使用​​css-minimizer-webpack-plugin​​,效果包括压缩、去重。但是比较耗时,在生产环境使用比较合适

5.js代码压缩

JS代码压缩使用​​terser-webpack-plugin​​,实现打包后JS代码的压缩,也比较耗时,在生产环境使用比较合适

6.Gizp压缩代码体积

开启Gzip后,大大提高用户的页面加载速度,因为gzip的体积比原文件小很多,当然需要后端的配合,使用​​compression-webpack-plugin​​,也是在生产环境设置比较合适

webpack热更新原理

目前webpack5的devServer已经支持热更新监听了,在实际实现中,发现如果更新的是入口index.html的内容,其实不会自动更新,通过scrpit标签引入的展示内容,在保存后会自动更新。

思考 每次热更新时,可以看到控制台的source会给一个新的js文件,应该是更新的是js文件,这也是为啥通过script标签引入的文件可以实现自动更新的原因

devServer: {
    open: true,
    hot: true, // 这就是开启热更新
    port: 3001,
    // static: './dist',
    static: path.resolve(__dirname, 'dist'),
},

特性

模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面时丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。

原理

image.png

第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。

第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。

第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。

第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。

webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。

HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。

而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

思考💡:为什么需要热更新?

Hot Module Replacement(以下简称 HMR)是 webpack 发展至今引入的最令人兴奋的特性之一 ,当你对代码进行修改并保存后,webpack 将对代码重新打包,并将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样在不刷新浏览器的前提下就能够对应用进行更新。例如,在开发 Web 页面过程中,当你点击按钮,出现一个弹窗的时候,发现弹窗标题没有对齐,这时候你修改 CSS 样式,然后保存,在浏览器没有刷新的前提下,标题样式发生了改变。感觉就像在 Chrome 的开发者工具中直接修改元素样式一样。

思考💡:HMR是怎样实现自动编译的?

webpack通过watch可以监听文件编译完成和监听文件的变化,webpack-dev-middleware可以调用webpack的API监听代码的变化,webpack-dev-middleware利用sockjs和webpack-dev-server/client建立webSocket长连接。将webpack的编译编译打包的各个阶段告诉浏览器端。主要告诉新模块hash的变化,然后webpack-dev-server/client是无法获取更新的代码的,通过webpack/hot/server获取更新的模块,然后HMR对比更新模块和模块的依赖。

思考💡:模块内容的变更浏览器又是如何感知的?

webpack-dev-middleware利用sockjs和webpack-dev-server/client建立webSocket长连接。将webpack的编译编译打包的各个阶段告诉浏览器端。

思考💡:以及新产生的两个文件又是干嘛的?

d04feccfa446b174bc10.hot-update.json

告知浏览器新的hash值,并且是哪个chunk发生了改变

思考💡:怎么实现局部更新的?

当hot-update.js文件加载好后,就会执行window.webpackHotUpdate,进而调用了hotApply。hotApply根据模块ID找到旧模块然后将它删除,然后执行父模块中注册的accept回调,从而实现模块内容的局部更新。

思考💡:webpack 可以将不同的模块打包成 bundle 文件或者几个 chunk 文件,但是当我通过 webpack HMR 进行开发的过程中,我并没有在我的 dist 目录中找到 webpack 打包好的文件,它们去哪呢?

原来 webpack 将 bundle.js 文件打包到了内存中,不生成文件的原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于memory-fs,memory-fs 是 webpack-dev-middleware 的一个依赖库,webpack-dev-middleware 将 webpack 原本的 outputFileSystem 替换成了MemoryFileSystem 实例,这样代码就将输出到内存中。

思考💡:通过查看 webpack-dev-server 的 package.json 文件,我们知道其依赖于 webpack-dev-middleware 库,那么 webpack-dev-middleware 在 HMR 过程中扮演什么角色?

webpack-dev-middleware扮演是中间件的角色,一头可以调用webpack暴露的API检测代码的变化,一头可以通过sockjs和webpack-dev-server/client建立webSocket长连接,将webapck打包编译的各个阶段发送给浏览器端。

思考💡:使用 HMR 的过程中,通过 Chrome 开发者工具我知道浏览器是通过 websocket 和 webpack-dev-server 进行通信的,但是 websocket 的 message 中并没有发现新模块代码。打包后的新模块又是通过什么方式发送到浏览器端的呢?为什么新的模块不通过 websocket 随消息一起发送到浏览器端呢?

功能块的解耦,各个模块各司其职,dev-server/client 只负责消息的传递而不负责新模块的获取,而这些工作应该有 HMR runtime 来完成,HMR runtime 才应该是获取新代码的地方。再就是因为不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware 和 webpack 配合也可以完成模块热更新流程,在使用 webpack-hot-middleware 中有件有意思的事,它没有使用 websocket,而是使用的 EventSource。综上所述,HMR 的工作流中,不应该把新模块代码放在 websocket 消息中。

思考💡:浏览器拿到最新的模块代码,HMR 又是怎么将老的模块替换成新的模块,在替换的过程中怎样处理模块之间的依赖关系?

思考💡:当模块的热替换过程中,如果替换模块失败,有什么回退机制吗?

模块热更新的错误处理,如果在热更新过程中出现错误,热更新将回退到刷新浏览器

面试题:说一下webpack的热更新原理?

webpack通过watch可以监测代码的变化;webpack-dev-middleware可以调用webpack暴露的API检测代码变化,并且告诉webpack将代码保存到内存中;webpack-dev-middleware通过sockjs和webpack-dev-server/client建立webSocket长连接,将webpack打包阶段的各个状态告知浏览器端,最重要的是新模块的hash值。webpack-dev-server/client通过webpack/hot/dev-server中的HMR去请求新的更新模块,HMR主要借助JSONP。先拿到hash的json文件,然后根据hash拼接出更新的文件js,然后HotModulePlugin对比新旧模块和模块依赖完成更新。

一些踩坑

1.build命令时,文件的引入要使用相对路径

2.webpack的依赖如下图,但是一直报错 webpack-cli failed to load webpack.config.js and couldn't find the html-webpack-plugin类似这种错误时,更换node环境解决

image.png

3.如下图,设置webpackServer的启动文件时,一直报错,找不到对应的样式文件,修改为./src就好了,应该是这个访问的时候,需要能够访问整个对应文件,才能找到静态资源 image.png