从0配置一个自己的webpack,以及和vite的区别

800 阅读26分钟

一、前言

注意:本文仅作为webpack学习之用,实际项目中还是更推荐使用成熟的脚手架搭建,然后根据本文教程去定制改造。

Webpack 功能非常强大,包括:模块打包、代码分割、按需加载、HMR、Tree-shaking、文件监听、sourcemap、Module Federation、devServer、DLL、多进程等等

二、webpack相关知识

首先简单了解下webpack的几个知识点:

1、package.json中^和~的区别

'~'(波浪符号):会匹配最新的小版本依赖包,更新到中间那位数字中最新的版本。放到我们的例子中就是:"exif-js": "~2.3.0",这个库会去匹配更新到 2.3.x 的最新版本,如果出了一个新的版本为2.4.0,则不会自动升级。波浪符号是曾经npm安装时候的默认符号,现在已经变为了插入符号。

'^'(插入符号):会匹配最新的大版本依赖包,更新到第一位数字中最新的版本。放到我们的例子中就是:"vue": "^2.2.2", 这个库会去匹配 2.x.x 中最新的版本,但是他不会自动更新到3.0.0。

'*':安装最新版本的依赖包,x.x.x

没有任何前缀符号,就是指定版本

2、package.json中dependencies 和devDependencies区别

dependencies:用于生产环境,运行时依赖

devDependencies:用于开发环境,开发时依赖

npm install xxx -g 表示全局安装,通常用于安装脚手架等工具

npm install xxx –save(-S) 表示本地安装,会被加至dependencies部分

npm install xxx --save-dev(-D) 表示本地安装,会被加至devDependencies部分

npm install会默认下载dependencies和devDependencies中的所有依赖包

  • 如webpack、html-webpack-plugin、babel等工具包就安装在devDependencies开发环境下,因为这些包用于将您的代码转换和捆绑到bundle.js文件中的普通 javascript 中,

在生产环境中,您将运行您的代码,bundle.js构建/生成的代码将不再需要这些依赖项。

  • 项目部署到开发环境所必须的依赖包则安装在dependencies生产环境下

在项目编译时dependencies、devDependencies里的依赖其实没有影响,最重要的区别体现在:

npm包发布的时候,其他的开发者可以从你发布的npm包中下载dependencies里的依赖包,而不能下载devDependencies里的内容。

当然,你放在哪都是规范问题, 在打包的时候是不会关注这2个字段的,打包的时候是直接读的node_modules,

还是建议放的规范些,这样能让接手的人更加快速地理解你这个插件属于哪个分类。

3、package.json和package-lock.json的区别

  • package.json 是在运行 “ npm init ”时生成的,主要记录项目依赖,有以下结构
"name": "pg-publish",   name:项目名
"version": "1.0.0",    version:版本号
"description": "Pangu Publish Web",
"author": "hzyuqiubin@corp.netease.com",
"private": true,    private:希不希望授权别人以任何形式使用私有包或未发布的;
"scripts":
"dependencies":   dependencies:指定了项目运行时所依赖的模块;
"devDependencies":   devDependencies:指定项目开发时所需要的模块,也就是在项目开发时才用得上;
"engines":
"browserslist":
  • package-lock.json是在运行“npm install”时生成的一个文件,用于记录当前状态下项目中实际安装的各个package的版本号、模块下载地址、及这个模块又依赖了哪些依赖。

为什么有了package.json,还需要package-lock.json文件呢?

当项目中已有 package-lock.json 文件,在安装项目依赖时,将以该文件为主进行解析安装指定版本依赖包,而不是使用 package.json 来解析和安装模块。

因为 package 只是指定的版本不够具体,而package-lock 为每个模块及其每个依赖项指定了版本,位置和完整性哈希,所以它每次创建的安装都是相同的。无论你使用什么设备,或者将来安装它都无关紧要,每次都应该给你相同的结果。

当版本升级,使用 npm install 命令时,会安装 package.json 中指定的大版本的最新版本。如 package.json 中指定版本"dependencies": { "webpack": "^2.0.0" },则 package-lock.json 会按照 {"webpack": "2.7.0"} 版本升级。在保证大版本号前提下的最新版本。webpack "2.7.0" 是 "2.x.x" 的最高版本。

4、Webpack的热更新原理:

Webpack 的热更新这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

核心就是客户端从服务端拉取更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上webpack-dev-server与浏览器之间维护了一个 Websocket,

当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。

客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

image.png

5、webpack 五个核心配置项

①entry  入口文件 指定哪个文件为入口文件

②output  打包后的文件(也就是bundle)应该放在哪里 path配置

③loader:文件转换器。例如把es6转为es5,scss转为css等

④plugin:扩展webpack功能的插件。在webpack构建的生命周期节点上加入扩展hook,添加功能。

⑤mode  用来定义  生产环境或者开发环境。

6、Loader和Plugin的区别:

区别一:

Loader 用来告诉webpack如何转换某一类型的文件。loader用于加载某些资源文件。因为webpack本身只能打包common.js规范的js文件,对于其他资源如css,img等,是没有办法加载的,这时就需要对应的loader将资源转化,从而进行加载。

Plugin 就是插件,可以扩展 Webpack 的功能,比如压缩打包,优化,不只局限于资源的加载。在 Webpack 运行的生命周期中会广播出许多事件,Plugin可以监听这些事件。

区别二:

Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。

Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

7、webpack构建流程(原理)

从启动构建到输出结果一系列过程:

(1)初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果。

(2)开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的 run 方法开始执行编译。

(3)确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去。

(4)编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。

(5)完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry配置生成代码块chunk。

(6)输出完成:输出所有的chunk到文件系统。

-5aab8242d687e3d5.jpg

8、webpack的各个阶段以及重要的钩子

阶段关键钩子说明
创建编译器:createCompiler()environment读取环境
创建编译器:createCompiler()afterEnvironment读取环境后触发
创建编译器:createCompiler()initialize初始化
编译器运行:compiler.run()beforeRun运行前的准备活动,主要启动了文件读取功能
编译器运行:compiler.run()run“机器”已经跑起来了,在编译之前有缓存,则启用缓存,这样可以提高效率。
编译器编译:compiler.compile(onCompiled)beforeCompilebeforeCompile开始编译前的准备,创建的ModuleFactory,创建Compilation,并绑定ModuleFactory到Compilation上。同时处理一些不需要编译的模块,比如ExternalModule(远程模块)和DllModule(第三方模块)
编译器编译:compiler.compile(onCompiled)compile进行编译
编译器编译:compiler.compile(onCompiled)make编译的核心流程
编译器编译:compiler.compile(onCompiled)afterCompile编译结束
编译结束后进行输出(onCompiled())shouldEmit获取compilation发来的电报,确定编译时候成功,是否可以开始输出了。
编译结束后进行输出(onCompiled())emit输出文件
编译结束后进行输出(onCompiled())afterEmit输出完毕
编译结束后进行输出(onCompiled())done所有流程结束

下面就开始实现自己搭建webpack啦~

三、webpack搭建--基础篇

1、新建一个文件夹作为示例项目,项目根目录运行命令初始化package.json

npm init -y

然后生成了package.json文件

(如果报错或者后面报错,那试着把初始化package.json里的name和version版本号删除或者改为你安装的版本号和另取名字,不然执行后面的会报错)

然后按照以下目录先创建空文件(最好执行,不然后面会报错):

image.png

2、入口entry、出口output

安装webpack依赖,会生成node_modules文件

npm i -D webpack@4.44.2 webpack-cli@3.3.12
  • /config/webpack.base.config.js 写入内容:
const path = require('path')

module.exports = {
  entry: { // 入口配置
    app: './src/index.js'
  },
  output: { // 出口配置
    filename: 'js/[name].[contenthash:8].js',
    path: path.resolve(__dirname, '../dist'),
  }
}
  • package.json 里写入script命令:
"scripts": {
    "build": "webpack --config ./config/webpack.base.config.js"
  },
  • 然后就可以运行查看效果了:
npm run build

正常的话,会在项目根目录生成dist文件夹,里面就是打包后的文件。

image.png

多入口和多出口的配置

多个入口:数组写法:
//这种写法最后会将两个js合成一个js叫build.js
entry: ["./src/index.js", "./src/main.js"],
output: {
  filename: "build.js",   //单出口
  path: resolve(__dirname,"build")
}

多个入口:对象写法:
//这种写法有几个入口文件就会形成几个chunk,输出几个bundle chunk的名称跟output 中的filename挂钩
//fiename的值改为"[name].js" 表示下标为one文件生成的chunk名称就是one 这样的话可以做到不同的页面加载不同的js页面
entry: {
  one: "./src/index.js",
  two: "./src/main.js"
},
output: {
  filename: "[name].js",    //多出口配置,有几个入口文件,就可以打包出几个文件
  path: resolve(__dirname,"build")
}

3、html模板 html-webpack-plugin

使用 html-webpack-plugin 插件来配置html模板文件的关联,这样打包后的js、css等会自动引入到html中,就可以访问html文件查看效果了。

  • 安装插件:
npm i -D html-webpack-plugin@4.5.0
  • /config/webpack.base.config.js 添加plugins,配置html-webpack-plugin:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: { // 入口配置
    app: './src/index.js'
  },
  output: { // 出口配置
    filename: 'js/[name].[contenthash:8].js',
    path: path.resolve(__dirname, '../dist'),
  },
  // 在这里添加
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',   //模板,打包后会自动合并到dist的index.html中
      inject: 'body',
      hash: false
    }),
  ],
}
  • /public/index.html 写入代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge, chrome=1">
  <title>Title</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
  • /src/index.js 写入代码:
console.log('wu')
  • 最后运行命令打包:

npm run build,打包后会在dist下生成index.html,打开该html查看控制台输出效果。

image.png

image.png

4、js编译 react、babel

用哪个框架都行,目的都是学习webpack的配置,以react示例。

  • 安装react依赖:
npm i -S react react-dom
  • /src/index.js 替换为以下代码:
import React from 'react'
import ReactDOM from 'react-dom'

function App() {
  return (
    <div>
      <div className="test">domdomdom</div>
    </div>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
)
  • 添加babel:

由于react使用的jsx语法,不是js标准语言语法,所以需要借助babel插件来转码,当然babel用处远不止这些,比如用babel将es6+代码转为兼容性更好的代码。

安装babel相关依赖:

  • 注意 babel-loader与babel-core的版本对应关系,不然会报错

1、babel-loader 8.x 对应babel-core 7.x

2、babel-loader 7.x 对应babel-core 6.x

npm uninstall babel-loader

npm install babel-loader@8.1.0

npm i -D babel-loader@8.1.0 @babel/core @babel/preset-env @babel/preset-react
  • 根目录下新建 babel.config.js 写入代码:
module.exports = {
  presets: [
    '@babel/preset-env',   //默认配置
    '@babel/preset-react'
  ],
}
  • /config/webpack.base.config.js 添加module,配置babel的rules:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: ......,
  output: ......,
  plugins: ......,
  // 在这里添加代码
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        options: {
          cacheDirectory: true
        },
        loader: 'babel-loader'
      }
    ]
  }
}
  • 运行 npm run build,然后打开打包后的index.html查看效果。

成功输出

image.png

5、配置分离 webpack-merge

webpack配置里可以指定mode属性来把运行环境划分为development和production,

使用webpack-merge插件可以针对不同mode环境使用不同的webpack配置,插件帮我们智能合并配置。

  • 安装依赖:
npm i -D webpack-merge@4.2.2
  • /config/webpack.dev.config.js 写入代码:
const merge = require('webpack-merge')
const common = require('./webpack.base.config')

module.exports = merge(common, {
  mode: 'development',
  output: {
    filename: 'js/[name].js',
  },
})
  • /config/webpack.prod.config.js 写入代码:
const merge = require('webpack-merge')
const common = require('./webpack.base.config')

module.exports = merge(common, {
  mode: 'production',
})
  • package.json 里修改scripts命令:
"scripts": {
    "start": "webpack --config ./config/webpack.dev.config.js",
    "build": "webpack --config ./config/webpack.prod.config.js"
  },

注意 build命令里的webpack.base.config.js换成了webpack.prod.config.js。 这样就分了开发环境和生产环境。

6、清空目录 clean-webpack-plugin

使用clean-webpack-plugin插件可以在build打包之前自动删除上次打包的dist文件夹,防止冗余文件的产生。

  • 安装依赖:
npm i -D clean-webpack-plugin
  • /config/webpack.prod.config.js 里添加plugins配置:
const merge = require('webpack-merge')
const common = require('./webpack.base.config')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = merge(common, {
  mode: 'production',
  // 在这里添加代码
  plugins: [
    new CleanWebpackPlugin(),
  ],
})
  • npm run build 查看dist文件夹还有没有之前遗留的js文件。

js冗余文件已删除

image.png

7、热更新 webpack-dev-server

使用webpack-dev-server插件,在webpack运行时自动启动一个本地服务器运行打包后的html文件,配合热更新,实现代码改动后实时查看效果。

安装依赖:

npm i -D webpack-dev-server@3.11.0
  • /config/webpack.dev.config.js 里添加devServer和plugins配置:
const merge = require('webpack-merge')
const common = require('./webpack.base.config')
const webpack = require('webpack')

module.exports = merge(common, {
  mode: 'development',
  output: {
    filename: 'js/[name].[hash:8].js',
  },
  // 在这里添加代码
  devServer: {
    open: true,
    port: 9000,
    compress: true,
    hot: true,
    inline: true,
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ],
})
  • package.json 里修改scripts的start命令:
"start": "webpack-dev-server --inline --progress --config ./config/webpack.dev.config.js",
  • npm start 查看效果:修改内容不需要再手动启动更新了

image.png

8、源码追踪 devtool、source map

devtool用于配置source map选项,帮助我们调试时追踪原始源代码,有多种source map格式供选择,具体可以参考文档,

综合构建速度和使用效果,建议选择 cheap-module-eval-source-map,各方面都比较均衡。

  • /config/webpack.dev.config.js 里添加devtool配置:
module.exports = merge(common, {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map', // 在这里添加即可;source-map作用是在浏览器控制台调试时可以追踪源码
  // devtool: process.env.NODE_ENV === 'development' ? 'source-map' : undefined,
  output: ......,
  devServer: ......,
  plugins: ......,
})
  • /src/index.js 里添加一条console语句:
import React from 'react'
import ReactDOM from 'react-dom'

console.log(123) // 在这里添加即可

function App () {
  ......
}
......
  • 在chrome控制台找到打印结果行,点击该行右侧的文件路径查看源代码,通过对比添加和不添加devtool时的源码来理解source map的作用。

未添加插件:

image.png

添加插件后:

image.png

9、样式相关 loader

style-loader 用来将CSS插入到页面的 <style> 标签中和引入css代码,不能单独使用;

css-loader 用于解析css文件生成css代码,比如处理CSS中 @import、url() 等语句,给style-loader使用;

less-loader 用于将less文件转换为css文件,给css-loader使用;

  • 安装依赖:

解析:如果less-loader或style-loader或css-loader版本过高会报如下错误,所以这里选择降低less-loader和style-loader和css-loader的版本

npm i -D less@3 less-loader@7 css-loader@5 style-loader@2
  • src文件夹下创建文件 index.less:

写入:

@color: red;

.test {
  color: @color;
}
  • /src/index.js 引入该less使用:
import React from 'react'
import ReactDOM from 'react-dom'
import './index.less'
  • /config/webpack.base.config.js 里配置loader:
module: {
  rules: [
    {
      test: /\.jsx?$/,
      options: {
        cacheDirectory: true
      },
      loader: 'babel-loader'
    },
    // 接上,追加以下代码
    {
      test: /\.css$/,
      use: ['style-loader', 'css-loader']
    },
    {
      test: /\.less$/,
      use: ['style-loader', 'css-loader', 'less-loader']
    },
  ]
}

注意:rules里的use数组在解析时是按从右往左解析的,需要注意顺序。

所以执行顺序是less-loader -> css-loader -> style-loader

其实为啥是从右往左,而不从左往右,只是Webpack选择了compose方式,而不是pipe的方式而已,在技术上实现从左往右也不会有难度。
函数组合:函数组合是函数式编程中非常重要的思想。
函数组合的两种形式:一种是pipe,另一种是compose。前者从左向右组合函数,后者方向相反。

build打包后start查看样式,发现颜色已经改变:

image.png

10、css工具集 postcss

postcss 是一个允许使用 JS 插件转换样式的工具集合;

postcss-loader 用于webpack中对css做进一步处理的loader;

autoprefixer插件 属于postcss的一个插件,配合postcss-loader可以自动给css样式添加浏览器前缀,以兼容低版本浏览器;

browserslist 用于指定项目运行的目标浏览器范围,能被autoprefixer和babel等识别,根据目标浏览器范围做兼容适配;

PostCSS Preset Env插件可以让你使用更新的css语法特效并实现向下兼容;

postcss-pxtorem 可以实现将px转换为rem。

安装依赖:

这里的postcss-loader和上面同理也要降低版本,如果不降低版本也会报错。

less-loader、postcss-loader的版本要相互匹配,版本相差不能太大

我的版本
"css-loader": "^5.2.7",
"less": "^3.13.1",
"less-loader": "^7.3.0",
"style-loader": "^2.0.0",
"postcss": "^8.4.21",
"postcss-loader": "^4.3.0",
"autoprefixer": "^10.4.13",
"browserlist": "^1.0.1",
npm i -D postcss@8 postcss-loader@4 autoprefixer browserlist
  • /src/index.less添加样式:
@color: red;

.test {
  color: @color;
  display: flex;
  justify-content: center;
}
  • /config/webpack.base.config.js 里修改css和less的loader配置:
module.exports = {
  module: {
    rules: [
      ......
      // 修改css和less的loader配置
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'postcss-loader']
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
      },
    ]
  }
}
  • 项目根目录新建文件 postcss.config.js:
module.exports = {
  plugins: {
    autoprefixer: {}
  }
}
或者在rules中这样写:
{
        test: /\.less$/,
        use: ['style-loader', 'css-loader', {
          loader: 'postcss-loader',
          options: {
            plugins: [require('autoprefixer')]
          }
        }, 'less-loader']
      },
  • package.json 里添加browserlist配置:
{
  "dependencies": ......,
  "devDependencies": ......,
  // 在这里追加
  "browserslist": [
    "> 1% in CN",
    "last 2 versions",
    "not ie <= 8"
  ]
}
  • npm start 运行,chrome开发者工具查看文字的css样式,看flex相关样式是否自动加上了浏览器前缀。

image.png

11、文件处理(打包 图片、字体、媒体、等文件) file-loader、url-loader

file-loader用于打包静态文件并将引入路径和js关联;

url-loader用于处理图片资源的打包,低于指定大小时会将资源转换为base64格式使用,其他情况处理和file-loader一样。

  • 安装依赖:
npm i -D file-loader url-loader
  • /config/webpack.base.config.js 里添加loader配置:
module.exports = {
  module: {
    rules: [
      ......
      // 接上,追加以下代码
      {
        test: /\.(jpe?g|png|gif)$/i,
        options: {
          esModule: false,
          limit: 4096, // 配置低于4k的图片会转为base64格式
        },
        loader: 'url-loader',
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i, // 处理字体文件
        options: {
          esModule: false
        },
        loader: 'file-loader'
      },
    ]
  }
}

12、js压缩 terser-webpack-plugin

terser-webpack-plugin 用于对js做代码压缩及代码混淆等处理,对es6+支持更好,替代以前的uglifyjs-webpack-plugin。

webpack 5.x 匹配terser-webpack-plugin 5.x

webpack 4.x 匹配terser-webpack-plugin 4.x

安装依赖:

npm i -D terser-webpack-plugin@4.2.3
  • /config/webpack.prod.config.js 里添加配置:
const merge = require('webpack-merge')
const common = require('./webpack.base.config')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = merge(common, {
  mode: 'production',
  // 在这里添加代码
  plugins: [
    new CleanWebpackPlugin(),
  ],
  optimization: {
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin({
        terserOptions: {
          compress: {
            pure_funcs: ['console.log'] // 删除console.log代码
          }
        }
      }),
    ],
  },
})

13、css分离 mini-css-extract-plugin

mini-css-extract-plugin用于将打包后的css单独抽离出来,webpack打包时默认是将css整合进js里通过动态创建style标签实现的,

而这个插件将css剥离出来,能减少不必要的js代码及dom操作,提升页面加载性能。

安装依赖:

npm i -D mini-css-extract-plugin@1
  • /config/webpack.base.config.js 里配置plugins和loader:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  entry: { // 入口配置
    app: './src/index.js'
  },
  output: { // 出口配置
    filename: 'js/[name].[contenthash:8].js',
    path: path.resolve(__dirname, '../dist'),
  },
  // 在这里添加
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',   //模板,打包后会自动合并到dist的index.html中
      inject: 'body',
      hash: false
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[id].[contenthash:8].css',
      ignoreOrder: true
    }),
  ],
  // 在这里添加代码
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        options: {
          cacheDirectory: true
        },
        loader: 'babel-loader'
      },
      // 接上,追加以下代码
      // 修改css和less的loader配置
      // 修改css和less的loader,替换掉style-loader
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      },
      {
        test: /\.less$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
      },
      // 接上,追加以下代码
      {
        test: /\.(jpe?g|png|gif)$/i,
        options: {
          esModule: false,
          limit: 4096, // 配置低于4k的图片会转为base64格式
        },
        loader: 'url-loader',
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i, // 处理字体文件
        options: {
          esModule: false
        },
        loader: 'file-loader'
      },
    ]
  }
}
  • npm run build 查看打包目录是否生成单独的css文件。
  • 注意:mini-css-extract-plugin需要配合html-webpack-plugin才能自动加载到html上,否则只会分离,不会自动引入。

image.png

14、css压缩 optimize-css-assets-webpack-plugin

optimize-css-assets-webpack-plugin 插件用于对css文件做压缩处理,默认使用cssnano压缩。

需要配合mini-css-extract-plugin插件,先将css分离后再压缩。

安装依赖:

npm i -D optimize-css-assets-webpack-plugin
  • /config/webpack.prod.config.js 里添加plugins:
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = merge(common, {
  plugins: [
    ......,
    // 接上,在这里追加
    new OptimizeCssAssetsPlugin(),
  ]
})
  • npm run build 查看打包后的css文件是否已被压缩。

15、快捷路径 alias

alias是webpack内置支持的一个属性,用来指定快捷路径标识,配置后就能方便的书写引入路径。

  • /config/webpack.base.config.js里配置:
module.exports = {
  entry: ......,
  output: ......,
  // 接上,在这里追加
  resolve: {
    alias: {
      '@': path.resolve(__dirname, '../src'),
      // 或者'@': resolve('src'),
    }
  },
}
  • /src/index.js修改文件的引入路径:
import './index.less' 替换为 import '@/index.less'
// import utils from '@/utils'
// import editTerms from './components/editTerms.vue'
// import { termsList, termsEnable } from '@/api/terminology'
  • npm start查看是否正常运行

image.png

四、webpack搭建--进阶篇

1、chunk分离 splitChunks

splitChunks用于代码分离,有利于性能优化。模块是否分离的判断原则:体积大、稳定不变。

  • 浏览器在加载文件后会将其缓存下来,下次加载该文件时直接从本地缓存里读取,加快访问速度。

  • webpack打包默认会将import同步引入的代码打包成一个文件,而使用splitChunks可以将该文件分离成多个。

  • 分离稳定不变的代码能保证每次打包后分离出来的文件保持不变,这样分离后的文件就能被浏览器缓存且缓存不会在项目更新发布后失效,这就是splitChunks的主要作用。

  • /config/webpack.prod.config.js添加splitChunks:

module.exports = merge(common, {
  optimization: {
    ......,
    // 接上,在这里追加
    splitChunks: {
      chunks: 'all',
      maxAsyncRequests: 8,
      maxInitialRequests: 6,
      minSize: 10000,
      cacheGroups: {
        react: { // 分离react和react-dom
          name: 'chunk-react',
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, // 匹配规则
          priority: 20 // 匹配优先级
        },
        vendors: { // 其他npm依赖(生产环境)
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: { // 组件公共抽离
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    }
  },
})
  • /config/webpack.dev.config.js里添加chunkFilename:
module.exports = merge(common, {
  output: {
    filename: 'js/[name].js',
    // 在这里添加
    chunkFilename: 'js/[name].js',
  },
})
  • /config/webpack.base.config.js里添加chunkFilename:
module.exports = {
  output: {
    filename: 'js/[name].[contenthash:8].js',
    path: path.resolve(__dirname, '../dist'),
    // 在这里添加
    chunkFilename: 'js/[name].[contenthash:8].js',
  },
}
  • chunkFilename就是用来配置splitChunks分离出来的文件名

  • npm run build查看打包后的文件,多打包几次,对比分离出来的chunk文件名是否有变化。

image.png

2、自定义常量 DefinePlugin

DefinePlugin 是webpack内置的一个插件,允许创建一个在编译时可以配置的全局常量,配置后就可以在代码里使用这个常量了。

  • /config/webpack.base.config.js 里添加plugins:
const webpack = require('webpack')

module.exports = {
  plugins: [
    ......,
    // 接上,在这里追加
    new webpack.DefinePlugin({
      VERSION_H5: +new Date() // 这里添加了VERSION_H5
    }),
  ]
}

需要注意,如果给定义的常量赋值为string类型时需要带上原始引号,可以通过单引号包裹双引号的方式 或通过JSON.stringify包裹,例如 ‘“abc”’ 或 JSON.stringify(‘abc’)

  • /src/index.js 里添加一条打印语句:
console.log(VERSION_H5)
  • npm start 查看chrome控制台输出结果是否符合预期

image.png

3、样式隔离 css modules

css modules是一种防止css样式污染的模块化解决方案。

接下来就配置.module.css或.module.less后缀的文件自动以css modules方式处理。

  • /config/webpack.base.config.js 里配置loader:
// 为了代码简洁,在这里封装了一下
const cssTest = /\.css$/
const lessTest = /\.less$/
const cssModuleTest = /\.module\.css$/
const lessModuleTest = /\.module\.less$/
const baseCssUse = [
  MiniCssExtractPlugin.loader, 
  'css-loader', 
  'postcss-loader'
]
const baseCssModuleUse = [
  MiniCssExtractPlugin.loader, 
  {
    loader: 'css-loader',
    options: {
      modules: {
        localIdentName: "[name]_[local]__[hash:5]"
      }
    },
  }, 
  'postcss-loader'
]

module.exports = {
  module: {
    rules: [
      ......,
      // 把之前的css和less的配置 替换成以下代码
      {
        test: cssTest,
        exclude: cssModuleTest,
        use: baseCssUse
      },
      {
        test: lessTest,
        exclude: lessModuleTest,
        use: [...baseCssUse, 'less-loader']
      },
      {
        test: cssModuleTest,
        use: baseCssModuleUse
      },
      {
        test: lessModuleTest,
        use: [...baseCssModuleUse, 'less-loader']
      },
      ......,
    ]
  },
}
  • src目录下新建index.module.less:
.name {
  text-decoration: line-through;
}
  • /src/index.js添加代码:
import React from 'react'
import ReactDOM from 'react-dom'
// import './index.less'
import '@/index.less'
import style from './index.module.less'

// console.log(123) // 在这里添加即可
// 找个合适的地方添加就行 
console.log(VERSION_H5)

function App() {
  return (
    <div>
      <div className="test">domdomdom11</div>
      <div className={style.name}>demo222</div>
      <div>
        <img src={icon} alt=""/>
      </div>
    </div>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
)
  • 配置完后,npm start重启项目,chrome控制台查看元素及样式效果。

4、兼容处理 polyfill

es6包含新的语法和新的api,新api是用更底层的语言实现的,新语法默认可以被babel降级处理,但新api默认不会处理,例如数组的find、Object.assign、promise等,需要配置polyfill来处理。

从babel v7.4版本开始,官方不再推荐使用@babel/polyfill,更推荐直接使用core-js/stable和regenerator-runtime/runtime。

安装依赖:

npm i -S core-js regenerator-runtime
  • 在入口文件 /src/index.js里最顶部导入:
// 必须在入口文件最顶部导入
import "core-js/stable"
import "regenerator-runtime/runtime"

// 然后再导入其他的
......
  • babel.config.js里修改@babel/preset-env配置:
module.exports = {
  presets: [
    // '@babel/preset-env',   //默认配置
    // 在这里修改 @babel/preset-env 的配置
    [
      '@babel/preset-env',
      {
        modules: false,
        useBuiltIns: 'entry',
        corejs: {
          version: '3.8', // 你的core-js版本号前两位
          proposals: true,
        },
      },
    ],
    // 其他的保持不变
    '@babel/preset-react'
  ],
}
  • 以上是我个人推荐的配置方式,缺点是有全局命名空间的污染,但优点是支持更全,其他诸如 useBuiltIns: usage 以及 @babel/plugin-transform-runtime 方式的优缺点正好相反,参考文档

5、代理 proxy

实际项目中,本地开发一般都会遇到接口跨域的问题,协议、域名、端口号 这三项任意一项不一致就会跨域,在devServer里配置proxy代理可以解决跨域问题。

在配置代理之前,最好在你的所有api请求地址之前都加一个代理标识,用于代理的匹配拦截,告诉代理服务器哪些请求需要被代理,这里就暂定代理标识为/proxy。

  • /config/webpack.dev.config.js里配置devServer:
const path = require('path');

module.exports = merge(common, {
  devServer: {
    contentBase: path.resolve(__dirname, '../dist'),
    open: false,
    port: 9000,
    compress: true,
    hot: true,
    inline: true,
    proxy: {
      '/proxy': {
        target: 'https://192.111:8800',
        ws: true,
        changeOrigin: true,
        secure: false,
        pathRewrite: {
          '^/proxy': ''
        }
      }
    }
  },
})
  • target 目标地址,有端口号的需要带上端口号;
  • ws 配置是否支持 websocket;
  • changeOrigin 配置是否支持虚拟主机站点,我也不清楚具体啥意思;
  • secure 是否开启安全验证,目标地址为https时需设置secure为false;
  • pathRewrite 路径重写,上述是配置了代理后将/proxy替换为空字符串,即实际接口地址不再需要携带/proxy。
  • 更多配置参考http-proxy-middleware文档。

6、脚本变量 cross-env

cross-env是一款运行跨平台设置和使用环境变量的脚本。使用cross-env在scripts脚本命令里配置自定义变量可以实现命令行快捷切换环境配置的功能。

比如配置不同的测试环境使用不同的接口地址,传统方式可能是直接在devServer里修改proxy代理地址的代码,人工修改代码容易出错,在多人开发时也容易出现代码冲突,如果使用cross-env配置的变量进行判断设置对应的代理地址,通过切换scripts命令来切换代理就变的方便多了。

安装依赖:

npm i cross-env -D
  • package.json里修改scripts:
"scripts": {
    "start": "npm run start:test1",
    "start:test1": "cross-env MY_TYPE=test1 webpack-dev-server --progress --config ./config/webpack.dev.config.js",
    "start:test2": "cross-env MY_TYPE=test2 webpack-dev-server --progress --config ./config/webpack.dev.config.js",
    "build": "webpack --config ./config/webpack.prod.config.js"
  },
  • 上述配置就是设置了MY_TYPE这个变量,两个命令设置的值分别是test1和test2,运行npm run start:test1时在webpack配置文件里就可以通过process.env.MY_TYPE获取到值。

cross-env自动把我们设置的变量加在了process.env这个对象上,但是process.env只能在node环境里获取到,而在浏览器环境里获取不到。

不过还记得上面第4条介绍的DefinePlugin吗,利用DefinePlugin我们可以添加个浏览器环境也能用的process.env对象,方式如下:

  • /config/webpack.base.config.js里定义DefinePlugin插件配置:
new webpack.DefinePlugin({
      // VERSION_H5: +new Date() // 这里添加了VERSION_H5
      'process.env': Object.keys(process.env).reduce(
        (env, key) => {
          env[key] = JSON.stringify(process.env[key]);
          return env;
        }, 
        {}
      )
    }),
  • 然后在 /src/index.js里添加打印语句:
console.log(process.env.NODE_ENV) 
console.log(process.env.MY_TYPE)
  • 分别运行命令 npm run start:test1和 npm run start:test2查看浏览器打印结果。

7、ts的支持 ts-loader

如果项目使用typescript,需要额外配置。

  • 安装依赖:
npm i -D typescript@4.0.5 ts-loader@7.0.5
  • 项目根目录添加 tsconfig.json文件,写入以下内容:
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true
  }
}
  • /config/webpack.base.config.js里配置 resolve 和 loader:
module.exports = {
  ......
  resolve: {
          ......
          // 接上,追加以下代码,表示引用文件时如果没带后缀会按照此顺序依次查找
          extensions: ['.tsx', '.ts', '.js'],
   },
   ......,
   module: {
    rules: [
      ......
      // 接上,追加以下代码
      {
        test: /\.tsx?$/,
        use: ['ts-loader']
      },
    ]
  }
}

好了,构建到这就结束了,配置完相信大家对webpack会更加了解了~

github地址

五、vite

  • vite可以理解为都封装好了的webpack,比webpack更简单,webpack相比更底层一点,vite不用再配置loader这些,比如css less它可以直接打包编译,不用配置css-loader less-loader这些

  • vite它又是基于这个esbuild去做一个编译,而打包的话它是基于rollup的

1、webpack和vite的区别

(1)开发环境区别

  • vite 自己实现 server,不对代码打包,充分利用浏览器对<script type=module>的支持,即<script type=module src="main.js">

    • 假设 main.js 引入了 vue,该 server 会把 import {createApp} from 'vue' 改为 import {createApp} from "/node_modules/.vite/vue.js" 这样浏览器就知道去哪里找 vue.js 了
  • webpack-dev-server 常使用 babel-loader 基于内存打包,比 vite 慢很多

    • 比如请求 main.js,main.js 如果引用了 vue.js,webpack-dev-server 会把 vue.js 代码拷贝到main.js,即该 server 会把 vue.js 的代码(递归地)打包进 main.js

(2)生产环境区别

  • vite 使用 rollup + esbuild(用 GO 语言写的) 来打包 JS 代码
  • webpack 使用 babel(用JS写的)来打包 JS 代码,比 esbuild 慢很多
    • 那webpack 能使用 esbuild 吗?可以,但需要自己配置(很麻烦)

(3)文件处理时机

  • vite 只会在你请求某个文件的时候处理该文件
  • webpack 会提前打包好 main.js,等你请求的时候直接输出打包好的 JS 给你

2、为什么Vite启动更快

底层语言:

  • 从底层原理上来说,Vite是基于esbuild预构建依赖。而esbuild是采用go语言编写,因为go语言的操作是纳秒级别,而js是以毫秒计数,所以vite比用js编写的打包器快10-100倍。

启动方式:

  • webpack原理图

image.png

webpack基于commonjs,会先打包合并,然后启动开发服务器(分析依赖=> 编译打包=> 交给本地服务器进行渲染),然后请求服务器时 直接给予打包结果,更改一个模块,其他有依赖关系的模块都会重新打包。

  • vite原理图

image.png

而vite基于es6module,vite是直接启动开发服务器,请求哪个模块再对该模块进行实时编译(启动服务器=> 请求模块时按需动态编译显示)

由于现代浏览器本身就支持ES Module,会自动向依赖的Module发请求,(类似于UI库elementUI的按需引入),服务端按需编译返回,改动一个模块仅仅会重新请求该模块。

vite充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack那样进行打包合并。 所以vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。

这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。

3、vite热更新原理

webpack:HMR时需要把改动模块及相关依赖全部编译

image.png

vite:HMR时只需让浏览器重新请求该模块,同时利用浏览器的缓存(源码模块协商缓存,依赖模块强缓存)来优化请求

image.png

Vite 的热加载原理,其实就是在客户端与服务端建立了一个 websocket 连接,当代码被修改时,服务端发送消息通知客户端去请求修改模块的代码,完成热更新。


  • 服务端:服务端做的就是监听代码文件的改变,在合适的时机向客户端发送 websocket 信息通知客户端去请求新的模块代码。
  • 客户端:Vite 中客户端的 websocket 相关代码在处理 html 中时被写入代码中。可以看到在处理 html 时,vite/client 的相关代码已经被插入。

Vite 会接受到来自客户端的消息。通过不同的消息触发一些事件。做到浏览器端的即时热模块更换(热更新)。 包括 connect、vue-reload、vue-rerender 等事件,分别触发组件vue 的重新加载,render等。

4、vite的优缺点

vite优点:

  • 开发环境中,无需打包操作,可快速的冷启动
  • 轻量快速的热重载
  • 按需编译,不再等待整个应用编译完成

vite缺点:

  • 不支持commonjs,不支持非现代浏览器
  • 热更新常常失败,原因不清楚,页面刷新可解决
  • 有些功能 rollup 不支持,需要自己写 rollup 插件

5、vite的底层原理

一、核心设计思想

  1. 开发阶段不打包(No-Bundle Dev Server)​
    传统工具(如 Webpack)在开发时需打包所有代码,导致启动和热更新缓慢。
    Vite ​直接以原生 ESM 形式加载源码,浏览器按需请求模块,跳过打包步骤。
  2. 按需编译(On-Demand Compilation)​
    仅编译当前页面所需的模块,而非整个项目,极大减少编译量。

二、核心架构

  1. 开发服务器(Dev Server)​
  • 原生 ESM 加载
    浏览器直接通过 <script type="module"> 加载源码,例如: <script type="module" src="/src/main.js"></script>

    • (对于项目中引入的第三方库) 假设 main.js 引入了 vue,该 server 会把 import {createApp} from 'vue' 改为 import {createApp} from "/node_modules/.vite/vue.js" 这样浏览器就知道去哪里找 vue.js 了

    • webpack-dev-server 常使用 babel-loader 基于内存打包,比 vite 慢很多。 比如请求 main.js,main.js 如果引用了 vue.js,webpack-dev-server 会把 vue.js 代码拷贝到main.js,即该 server 会把 vue.js 的代码(递归地)打包进 main.js

  • 请求拦截与编译
    Vite 的服务器拦截浏览器对模块的请求,动态编译以下两类文件:

    • 源码文件(如 .vue.jsx)​:通过插件(如 @vitejs/plugin-vue)实时编译为 ESM。
    • 依赖模块(如 node_modules)​:预构建为 ESM(后文详述)。
  • 热模块替换(HMR)​
    仅重新编译变更的模块,并通过 WebSocket 通知浏览器局部更新,无需刷新页面。

  1. 预构建(Pre-Bundling)​
  • 目的

    • 将 CommonJS/UMD 格式的依赖(如 lodash)转换为 ESM。
    • 合并多个小文件(如 lodash 的数百个子模块)为单个文件,减少 HTTP 请求。
  • 实现
    使用 ​esbuild​(Go 语言编写,速度极快)在首次启动时预构建依赖,结果缓存到 node_modules/.vite

  1. 生产构建(Production Build)​
  • 生产环境仍使用 ​Rollup​(而非 esbuild)进行打包,原因:

    • Rollup 的 Tree-Shaking 更成熟,适合生成优化后的代码。
    • esbuild 的代码压缩和拆分功能尚不完善。
  • 通过 vite build 命令触发 Rollup 打包,生成静态文件。

三、关键技术实现

  1. 依赖解析与导入重写
  • 裸模块(Bare Module)重写
    浏览器无法直接识别 import lodash from 'lodash',Vite 将其转换为: import lodash from '/node_modules/.vite/lodash.js' // 预构建后的路径

  • 动态导入(Dynamic Import)支持
    自动处理动态导入的模块路径,例如: const module = await import('./' + path + '.js') // 转换为有效 ESM 路径

  1. 模块热更新(HMR)​
  • 变更检测
    通过文件系统监听(如 chokidar)捕获文件修改。

  • 更新策略

    • 边界更新:若模块无 HMR 处理函数,向上冒泡到最近的接受者。
    • 状态保留:通过代理对象保留组件状态(如 Vue 组件的 data)。
  1. ESM 兼容性处理
  • 浏览器兼容性
    通过 @vitejs/plugin-legacy 生成传统脚本(针对不支持 ESM 的旧浏览器)。
  • Polyfill 注入
    自动注入 import.meta.env动态导入 等特性的 Polyfill。

此文到这就结束啦,本菜菜才疏学浅,若有错误,欢迎大佬指正~