我的webpack进化史-构建篇

999 阅读9分钟

开篇还是我的一些碎碎念!

前面我们刚刚把webpack最基础的加载css,加载图片,一些基本配置都写完啦!那么这次接着上周的代码,我们继续捣鼓!我之前用vue脚手架搭的项目写项目时就会很奇怪,为什么我每次一保存它就会自动帮我更新页面呀!为什么每次我启动项目都会自动帮我打开浏览器?由于当时的我脑子里充满了业务需求,这些东西通通被我抛到脑后,这次扫盲过程才发现之前经典webpack面试题目--热更新(HMR 全称 HotModuleReplacement) 就是讲这个的呀!唉,年少不知webpack。这次构建篇还是会和之前基础篇一样,我按我自己平常写项目的思路把我不明白不清楚的问题都会首先抛出来,期间碰到过的问题也全部贴出来,然后在贴详细代码。要是有大佬路过,请留步!请赐教!求指导!代码完整连接指路👉 github.com/xiaoshanweb…

本篇主要讲三部分

I. webpack中加载vue,react文件

在基础篇文章里,我们讲了些基本配置。但是在webpack中如何配置vue,react?

1: babel

在实现加载vue,react文件之前,我们必须要先了解下babel这个插件。babel他其实就是将非ECMAScript 2015的语言,向后转化为兼容的javascript。将我们的vue语法,jsx转换成js,箭头函数转成function,const转成var等等。

npm install @babel/plugin-transform-arrow-functions -D 将箭头函数转换的插件
npm install @babel/plugin-transform-block-scoping -D 将const转成var

有一点要说的是,babel一个独立的工具,它不和和webpack等构建工具配置。如果要转化的内容很多,我们也不可能一个一个去安装插件,所以babel还有一个概念 预设preset ,我们可以直接给webpack提供一个预设,webpack会根据我们的预设去加载插件,并且将它传递给babel。我们比较常见的预设是 env react TypeScript

npm install @babel/preset-env
npm install --save-dev @babel/core
2: 加载react文件
npm i --save-dev react react-dom
npm install @babel/preset-react -D

先把react的基本配置写好,index.html文件下新增一个id为root的标签,根目录下新建一个Babel.config.js文件

新建一个babel.config.js的文件,将我们需要加载的插件写入。如果不想新建一个文件也可以在webpack下配置去加载插件

module.exports = {
  presets: [
    ["@babel/preset-env"],
    ["@babel/preset-react"],
  ],
}

在webpack.config.js的rules里面配置babel-loader

{
    test: /\.jsx?$/i,
    exclude: /node_modeuls/,
    use: {
      loader: 'babel-loader',
      // options: {
      //   presets: [
      //     ["@babel/preset-env", {
      //       // targets: ["chrome 88"]
      //       // enmodules: true
      //     }]
      //   ]
      //   // plugins: [
      //   //   "@babel/plugin-transform-arrow-functions",
      //   //   "@babel/plugin-transform-block-scoping"
      //   // ]
      // }
    }
 }

新建一个reactFile.jsx文件

import React, { Component } from 'react';

export default class ReactApp extends Component {
 constructor(props) {
   super(props);

   this.state = {
     message: "Hello React"
   }
 }

 render() {
   return (
     <div>{this.state.message}</div>
   )
 }
}

index.js

import React from 'react'
import ReactApp from './reactFile.jsx'
import ReactDOM from 'react-dom';

ReactDOM.render(<ReactApp />, document.getElementById("root"));

index.html

  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>
    <%= htmlWebpackPlugin.options.title %>
  </title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

效果图

image.png

3: 加载vue文件
npm i --save-dev vue vue-loder vue-templete-compiler

vue文件的加载相对来说比较简单,先把vue文件建起来并挂载,

<template>
 <div class="vue">我是Vue文件</div>
</template>
<style scoped>
.vue {
 color: red;
 font-size: 20px;
}
</style>
import './index.less'
import Icon from './img.jpeg'
import printMe from './print'

import Vue from 'vue'
import VueApp from './App.vue'

import React from 'react'
import ReactApp from './reactFile.jsx'
import ReactDOM from 'react-dom';

function component() {
  var element = document.createElement('div')
  element.innerHTML = 'Hello Webpack'
  element.classList.add('color_red')

  var img = new Image(300, 300)
  img.src = Icon
  element.appendChild(img)

  var btn = document.createElement('button')
  btn.innerHTML = '点击我'
  btn.onclick = printMe
  element.appendChild(btn)
  console.log(111)
  console.log(222)
  console.log(333)
  return element
}

document.body.appendChild(component())

ReactDOM.render(<ReactApp />, document.getElementById("root"));

new Vue({
  render: h => h(VueApp)
}).$mount('#app')

index.html

  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>
    <%= htmlWebpackPlugin.options.title %>
  </title>
</head>

<body>
  <div id="app"></div>
  <div id="root"></div>
</body>

</html>

其次在webpack.config.js里配置,vue-loader会匹配所有.vue文件,VueLoaderPlugin会将所有匹配出来的vue文件进行处理。不过在下载vue-loader时要注意,vue-loader的版本不要太高,下载15.x.x版本就可以。引入VueLoaderPlugin的方法有两种,一种是直接require('vue-loader/lib/plugin'),也可以下载vue-loder-plugin直接引用。但两者相差不大,在vue-loder-plugin中他也是去引用vue-loader/lib/plugin

// const VueLoaderPlugin = require('vue-loader/lib/plugin')
const VueLoaderPlugin = require('vue-loader-plugin')

 {
   test: /\.vue$/i,
   use: 'vue-loader'
 },
 
 new VueLoaderPlugin()

效果图

image.png

II. 自动编译

在上一篇文章,我们是通过build方法来查看代码的每次更新,webpack要实现自动编译到底是提供了什么方法?

1:watch
watch是可以监听文件的变化,通过live-server插件(vs插件)提供本地服务在在每次修改文件后自动刷新页面
缺点:
    a:效率不是很高,我们还是要手动build之后再启动watch;
    b:它对所有的diamanté都重新进行编译,而且编译成功后,都会产生新的文件(比如文件操作,file等)
    c:虽然可以监听到文件变化,但实际上没有自动刷新浏览器的功能
  
//package.json
"watch": "webpack --watch",
//或者webpack.config.js
module.exports={
    watch:true,
    ...
}
2:webpack-dev-server

在开发中,我们是希望在没有live-server的情况下,依然可以具备实施刷新加载的功能。webpack-dev-server在编译之后是不会向watch会产生一些新的输出文件,它生成文件的都是存在内存中。它让使用者可以配置一个地址,规定我们必须用过这个地址去调试,开发。

  npm install --save-dev-webpack-dev-server

在package.json中写脚本命令,有一点要注意的是,我在查看webpack-dev-server的过程中看到有两种启动方式。第一种是直接用web-dev-server插件去启动,第二种是webpack有内置server服务,但是用第二种方式去启动就必须保证webpack的版本要兼容webpack-dev-server,最麻烦的事这两种启动方式不能并存。

"start": "webpack-dev-server",
要求的web-dev-server的版本

"start-other": "webpack serve",
要求的webpack和web-dev-server的版本
"webpack": "^5.48.0",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2"

随后在webpack.config.js中配置devServer,devServer的配置相对来还是很常见,

contentBase: 这个属性其实接触并不多,但如果index.html里面有额外的使用某些存放在pubilc下的静态资源,在index.html里面这样引入,但是浏览器根本无法通过这个路径去引入的,所以可以通过设置contentBase来制定我们要从那里去取这个文件。contentBase和publicPath这两个概念我在刚开始看的时候就很容易搞混,而且在发布的时候,如果后端是在一台服务器上部署h5和pc端,那这个时候publicPath就需要区分h5是从那里进入,pc是从那里进入。publicPath这个比较重要,我们在下面再来细说。

devServer: {
   host: '127.0.0.1',//主机地址
   port: 8000,//端口号
   open: true//是否项目启动成功后自动打开浏览器
 },

这样直接启动命令,在修改文件后,就会发现浏览器自动刷新啦!webpack-dev-server的配置相对来说还是比较简单。大部分的处理webpack-dev-server它自己搞了,但是如果我需要自己配置热更新的过程怎么办?我不想要框架集成的怎么办?我之前写ssr(服务器渲染)的时候就碰到过这种问题,通过node启动项目,没有热更新,需要我自己去配置。

3: webpack-dev-middleware

webpack-dev-middlewarek可以说是一个封装器,他可以把webpack处理过的文件发送到一个sever。webpack-dev-server在内部使用了它(对哦,没错哦,webpack-dev-server内部是使用的他来实现热更新哦)。然而webpack-dev-middleware也可以作为一个单独的package来使用,这样就方便使用者根据需求进行更多的自定义。

例子我实在官方网站看到的,我就直接用了。当然如果你对node比较熟悉,你用koa搭建也是可以的,官方的例子是express搭建的。先安装express,webpack-dev-middleware

npm install --save-dev express webpack-dev-middleware

随后新建一个server.js文件。先将我们需要用到的express,webpack,webpack-dev-middleware引入。创建一个app,并且监听3000的端口号。config拿到了所有的配置信息,再将config传递给webpack,webpack将所有的配置信息进行编译。将webpack编译成功之后会生成一个compiler对象传递给webpackDevMiddleware。处理完之后webpackDevMiddleware是会生成一个express的中间件

const webpackDevMiddleware = require('webpack-dev-middleware')
const webpack = require('webpack')
const express = require('express')

const app = express()
// 加载配置信息
const config = require('./webpack.config')
// 将配置信息传递给webpack编译
const compiler = webpack(config)

// 将编译后的结果返回给webpackDevMiddleware,之后的请求webpackDevMiddleware()返回给中间件处理
app.use(webpackDevMiddleware(compiler))

app.listen(4000, () => {
  console.log("服务已经开启在3000端口上~");
});

项目结构如下

image.png

ok,代码写完后,我们来测试下。打开终端输入 node server.js,当控制台出现输出就代表运行成功了,接下来在浏览器中输入http://127.0.0.1:4000

image.png

image.png

4:HMR(HotModuleReplacement)

HMR--热更替/热更新。专业点说就是指在程序运行过程中添加,删除,更替模块不需要重新刷新整个页面。在默认情况下,webpack-dev-server已经支持HMR,我们只需要开启就可以。

devServer: {
    // contentBase: './dist',
    host: '127.0.0.1',
    port: 8000,
    open: true,
    hot: true // 开启HMR
  },

当浏览器出现这几个的时候代表已经连接成功。

image.png

我们说的三个热更新方法其实都是刷新的整个页面,如果我希望每次我修改之后,只刷新我更改过的页面,就可以用module.hot.accept这个方法,在每个页面上添加在进而控制当前页面是否要更新。但是大型项目根本不允许我们这样去配置,在vue,react中我们要如何配合热更新。vue-loader当前已经支持热更新了,所以无需我们再去配置。react官方提供了react-refresh。

npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

修改webpack.config.js和babel.config.js文件


const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

plugins:[
  new ReactRefreshWebpackPlugin()
]
module.exports = {
  presets: [
    ["@babel/preset-env"],
    ["@babel/preset-react"],
  ],
  plugins: [
    ['react-refresh/babel']
  ]
}
HMR原理

看下面这张图(把王红元老师的图搬来了,要是有不妥之处,立马删!)。我们可以知道浏览器是通过Socket与webpack-dev-server进行通信的。源代码在webpack中被打包编译到内存中,浏览器通过http请求向服务器请求资源。但是http请求是短链接,每次都需要浏览器主动发出请求,服务器才会响应结果。

而我们需要实现的状态是,在浏览器没有发出请求的情况下,服务器依然向浏览器推送数据或资源,但是短链接是不可能实现的这种需求。

socket是可以解决这个问题。他是服务器和浏览器端之间的桥梁。当我们启动devServer的时候,socket在服务器和浏览器之间建立了一个webSocket长连接。当我们某个文件修改保存后,webpack将改文件打包编译之后的json,js文件,通过socket告知浏览器。浏览器接收到后做出响应,刷新对应的文件。

image.png

bug集锦

2021-08-06

第一个bug,这种bug以我目前的智商无法解释为什么要这么改。我百度的原因是说webpack-dev-server的版本和webpack-cli版本不匹配所以报错, 原先的版本

"webpack-cli": "^4.7.2","webpack-dev-server": "^3.11.2" 修改之后的版本 "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.2"

image.png