构建工具-webpack

一、webpack介绍

什么是构建工具:

构建工具让我们不用关心生产的代码,也不用关心代码如何在浏览器运行,只需要关心我们的开发怎样写的舒服就好。

1. 使用分类

关于webpack的使用,基本都围绕“配置”展开,而这些配置大致可划分为两类:

  • 流程类:作用于流程中某个或若干个环节,直接影响打包效果的配置项
  • 工具类:主流程之外,提供更多工程化能力的配置项

2. 流程类配置

  • 输入entry
    • entry
    • context
  • 模块解析require, import
    • resolve
    • externals
  • 模块转译module
    • module
  • 后处理output
    • optimization(代码分割)
    • mode
    • target

3. 配置总览

在这里插入图片描述

在这里插入图片描述

二、webpack使用(基础)

1. 使用步骤

  1. 初始化项目
npm init -y
  1. 安装依赖 webpack, webpack-cli
  • webpack: 核心依赖
  • webpack-cli: 命令行工具
npm i webpack webpack-cli -D
  1. 在项目中创建 src 目录,在 src 目录中创建 index.js 文件

webpack 默认会打包 src 目录下的 index.js 文件,后面可以通过 webpack.config.js 文件来修改默认的打包文件

  1. 构建 src/index.js 文件

打包后的文件会被打包到 dist 目录下

npx webpack
npx webpack --mode development # 指定开发环境
  • package.json 文件中添加 scripts 字段
"scripts": {
    "dev": "webpack",
    "start": "webpack --mode development",
    "build": "webpack --mode production"
}

2. 配置文件

  • webpack.config.js:默认的配置文件(是在 node 中运行的,使用 commonjs 规范)
module.exports = {
  // 配置打包模式:development 开发模式 production 生产模式
  mode: 'development' // 和上面目录效果一样
}

3. entry

打包的入口文件,可以是一个字符串,也可以是一个数组,也可以是一个对象

module.exports = {
  // 配置打包模式:development 开发模式 production 生产模式
  mode: 'development',
  // 配置入口文件(默认是src/index.js)
  // entry: './src/index.js',
  entry: {
    index: ['./src/index.js', './src/main.js']
  }
  // entry: {
  //    index: './src/index.js',
  //    main: './src/main.js'
  // },
}
  1. 字符串
{
  "entry": "./src/index.js"
}
  1. 数组

index.jsmain.js 多个文件打包到一个文件中

{
  index: ['./src/index.js', './src/main.js']
}
  1. 对象

每个文件对应一个入口文件,生成多个文件 dist/index.jsdist/main.js

{
  index: "./src/index.js",
  main: "./src/main.js",
}

4. output

  • path:打包后的文件路径(要求绝对路径)
  • filename:打包后的文件名
  • clean:打包前清空打包目录
{
    output: {
        // 打包后的文件名
        // filename: '[name].js' // name 是占位符,会被 entry 中的 key 替换
        filename: '[name]-[hash].js', // hash 是占位符,会被一个随机的字符串替换
        // 打包后的文件路径,要求绝对路径
        path: path.resolve(__dirname, 'dist'),
        clean: true // 每次打包,自动清空打包目录
    }
}

5. loader

5.1 loader概念

帮助webpack将不同类型的文件转换为webpack能够识别的模块

5.2 loader的执行顺序

(1)、分类
  • pre:前置loader
  • normal:普通loader(默认)
  • inline:内联loader
  • post:内置loader
(2)、执行顺序
  • loader的优先级:pre > normal > inline > post
  • 相同优先级的loader执行顺序为:从右到左,从下到上

例如:

// 此时loader执行顺序:loader3 -> loader2 -> loader1
module: {
  rules: [
    {
      test: /\.js$/,
      loader: "loader1",
    },
    {
      test: /\.js$/,
      loader: "loader2",
    },
    {
      test: /\.js$/,
      loader: "loader3",
    },
  ],
},
// 此时loader执行顺序:loader1 -> loader2 -> loader3
module: {
  rules: [
    {
      enforce: "pre",
      test: /\.js$/,
      loader: "loader1",
    },
    {
      // 没有enforce就是normal
      test: /\.js$/,
      loader: "loader2",
    },
    {
      enforce: "post",
      test: /\.js$/,
      loader: "loader3",
    },
  ],
},
(3)、使用loader的方式

配置方式:在webpack.config.js文件中指定loader(pre、normal、post loader)

  • 配置方式:在webpack.config.js文件中指定loader(pre、normal、post loader)
  • 内联方式:在每个import语句中指定loader(inline loader)
(4)、inline loader(不建议写)
  1. 用法:import Styles from 'style-loader!css-loader?modules!./style.css';

  2. 含义:

  • 使用css-loaderstyle-loader处理styles.css文件
  • 通过!将资源中的loader分开
  1. inline loader可以通过添加不同的前缀,跳过其他类型的loader
  • !跳过normal loader

import Styles from '!style-loader!css-loader?modules!./style.css';

  • -!跳过pre和normal loader

import Styles from '-!style-loader!css-loader?modules!./style.css';

  • !!跳过pre、normal和post loader

import Styles from '!!style-loader!css-loader?modules!./style.css';

5.3 基本使用

loader:用于对模块的源代码进行转换,可以将文件从不同的语言(如 JavaScriptCSSLESS)转换为 JavaScript 模块,以便在浏览器中使用

只要进行源代码进行转换的都是 loader

例如:在 index.js 文件中引入 css 文件

import './style/index.css'

const h1 = document.createElement('h1')
h1.innerText = 'hello webpack'
document.body.appendChild(h1)

console.log('hello webpack')

执行 npx webpack 命令后,会报错,因为 webpack 只能打包 JavaScript 文件,不能打包 css 文件

ERROR in ./src/style/index.css 1:3
Module parse failed: Unexpected token (1:3)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> h1 {
|     color: #369;
|     font-family: Arial, Helvetica, sans-serif;
 @ ./src/index.js 1:0-26

上面的报错信息告诉我们,需要一个合适的 loader 来处理这个文件类型,目前没有配置 loader 来处理这个文件类型

  • 解决步骤:
  1. 安装 css-loaderstyle-loader
# `css-loader`:将 `css` 文件转换为 `JavaScript` 模块
# `style-loader`:将 `css` 文件插入到 `head` 标签中
npm i css-loader style-loader -D
  1. webpack.config.js 文件中添加 loader

配置 loader 是通过 module.rules 数组来配置的

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  // 配置 loader
  module: {
    rules: [
      {
        test: /\.css$/i, // 正则表达式,匹配.css 结尾的文件
        use: ['style-loader', 'css-loader'] // use 数组中的 loader 从右到左执行
      },
      {
        test: /\.(jpg|png|svg)$/i, // 图片类型文件,webpack默认支持,使用 type: asset/resource 类型的 loader 来处理
        type: 'asset/resource'
      }
    ]
  }
}

⚠️ 注意:use 数组中的 loader 从右到左执行,先执行 css-loader,再执行 style-loader

总结:webpack 只能打包 JavaScript 文件,如果想要打包其他类型文件,需要添加对应文件的 loader

图片资源:

type:asset/resourceasset的区别:

  • asset/resource:原封不动的图片 => 类似file-loader(放入文件夹)
  • asset:可以转base64,也可以原封不动输出 -> 类似 url-loader(转base64)+ file-loader

5.4 babel-loader的使用

babel:是一个 JavaScript 编译器,将 ES6 代码转换为 ES5 代码。可以将 JavaScript 新特性转换可以兼容的旧版本的 JavaScript 代码

babel主要作用:

  1. 转jsx
  2. 语法转换
  3. polyfill(主要是依赖core-js)

补充:core-js原理:去方法的原型上面找,如果没有找到,表示浏览器不支持,core-js就手动写一个方法添加到原型上。

  • 使用步骤:
  1. 安装 babel-loader@babel/core@babel/preset-env

babel-loader:将 babel 转换为 webpack 可以识别的 loader

@babel/corebabel 的核心库

@babel/preset-envbabel 的预设库,内置了一些常用的 babel 插件

npm i babel-loader @babel/core @babel/preset-env -D
  1. webpack.config.js 文件中添加 loader
{
  test: /\.m?js$/i,
  exclude: /node_modules/, // 排除 node_modules 目录下的文件
  use: {
    loader: 'babel-loader', // 使用的 loader
    options: {
      presets: ['@babel/preset-env'] // 预设库
    }
  }
}
  1. package.json 设置兼容性
{
  "browserslist": {
    "production": [">0.2%", "not dead", "not op_mini all"],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
  // "browserslist": [
  //    "defaults" // 使用默认的浏览器列表
  // ]
}

5.5 常见loader

在这里插入图片描述

5.6 自定义loader

  • 使用步骤:
  1. webpack.config.js
 module: {
  rules: [
    {
      test: /\.js$/,
      use: [path.join(__dirname, './custom-loader')]
    }
  ]
}
  1. custom-loader.js
module.exports = function (source, sourceMap, data) {
  // source: 为 loader 的输入
  // data: 别的loader传递的数据
  // 可能为文件内容,也可能是上一个 loader 处理的结果
  console.log(source)
  return source
}

5.7 同步loader

同步loader中不能执行异步操作

  • webpack.config.js中
 module: {
    rules: [
        {
            test: /\.js$/,
            use: ['./loader/sync-loader.js']
        }
    ]
},
  • loader/sync-loader.js
// 同步loader
// 写法1
module.exports = function(source) {
	return source
}

// 写法2
module.exports = function (source, sourceMap, meta) {
  /**
   * 同步loader,必须调用callback
   * 参数1:错误信息,有错误信息就传,没有就传null
   * 参数2:loader处理后的内容
   * 参数3:继续传递sourceMap,不然后续中断生成不了sourceMap
   * 参数4:给下一个loader传递的参数
   */
  this.callback(null, source, sourceMap, meta)
}

写法2在一些对前后loader关联多的时候用

5.8 异步loader

// 异步loader
module.exports = function (source, sourceMap, meta) {
  // 调用 async 方法,返回值是 callback
  const callback = this.async()
  setTimeout(() => {
    callback(null, source, sourceMap, meta)
  }, 1000)
}

5.9 raw loader

row loader 接受到的 source 是Buffer(二进制数据),用于处理图片等资源

// 写法1
// row loader 接受到的 source 是Buffer(二进制数据)
module.exports = function (source) {
  return source
}
module.exports.raw = true

// 写法2
function RawLoader(source) {
  return source
}
RawLoader.raw = true
module.exports = RawLoader

5.10 pitch loader

需要向外暴露一个pitch方法,如果该方法return了一个值,那么就会中断后续的执行

module.exports = function (source, sourceMap, meta) {
  console.log(source, 'source')
  return source
}

module.exports.pitch = function (remainingRequest) {
  // remainingRequest 剩下还需要处理的loader(以inline loader形式输出)
  console.log('pitch', remainingRequest)
  // return 'pitch-loader'
}

use: ['./loader/pitch-loader1.js', './loader/pitch-loader2.js', './loader/pitch-loader3.js']

normal loader:1 2 3

pitch:1 2 3

1、正常情况:先执行 pitch 1、2、3,再执行 loader 3、2、1

2、在pitch2中return了,那么先执行1、2,然后跳过pitch3、loader3、2,去执行loader1(因为loader1是pitch1的)

5.11 loader API

方法名含义用法
this.async异步回调 loader。返回 this.callbackconst callback = this.async()
this.callback可以同步或者异步调用的并返回多个结果的函数this.callback(err, content, sourceMap?, meta?)
this.getOptions(schema)获取 loader 的 optionsthis.getOptions(schema)
this.emitFile产生一个文件this.emitFile(name, content, sourceMap)
this.utils.contextify返回一个相对路径this.utils.contextify(context, request)
this.utils.absolutify返回一个绝对路径this.utils.absolutify(context, request)

文档:https://www.webpackjs.com/api/loaders/#the-loader-context

5.12 自定义loader:clean-log-loader

作用:去掉打包结果中所有的console.log语句

module.exports = function (source, sourceMap, meta) {
  return source.replace(/console\.log\(.*\);?/g, '')
}

5.13 自定义loader:banner-loader

作用:给文件内容中添加作者信息

  • webpack.config.js
{
    test: /\.js$/,
    use: [
        {
            loader: './loader/custom/banner-loader.js',
            options: {
                author: 'wifi歪f'
            }
        }
    ]
}
  • loader/custom/banner-loader.js

通过 this.getOptions 可以获取到 loader 上下文对象。

const schema = require('./schema.json')

module.exports = function (source, sourceMap, meta) {
  // this.getOptions 可以获取到 loader 上下文对象。
  // schema:对options进行校验,要符合JSON Schema的规则
  const options = this.getOptions(schema) // 不传参数,也能拿到数据,传了后schema会做类型校验
  console.log(options);
  
  const prefix = `
    /**
     * Author: ${options.author}
     */
  `
  return prefix + source
}
  • loader/custom/schema.json
{
    "type": "object",
    "properties": { // 参数
        "author": {
            "type": "string"
        }
    },
    "additionalProperties": false // 是否允许追加参数
}

例如:在webpack.config.js中追加参数

不符合JSON Schema规则,会报错

use: [
    {
        loader: './loader/custom/banner-loader.js',
        options: {
            author: 'wifi歪f',
            age: 18 // 不符合JSON Schema规则
        }
    }
]

5.14 自定义loader:babel-loader

(1)、babel基础
  1. babel工具包:
  • @babel-parser:将js代码解析为抽象语法树AST(解析
  • @babel/core:核心库(包含了解析、转换、生成的功能)
  • @babel/generator:把转换后的抽象语法书(AST)生成目标代码(生成
  • @babel/code-frame
  • @babel/runtime
  • @babel/template
  • @babel/traverse:是一个用于对抽象语法树(AST)进行递归遍历和更新的工具库,它可以通过访问和修改AST节点来实现代码转换。(转换

@babel-parser -> @babel/traverse -> @babel/generator

  1. babel的使用

通过transform方法进行转换,options传入的是预设

预设文档:https://www.babeljs.cn/docs/presets

import * as babel from "@babel/core";

// options传入的是预设
babel.transform(code, options, function(err, result) {
  result; // result => { code, map, ast }
});
(2)、自定义babel-loader开发
  1. 安装依赖
npm i @babel/core @babel/preset-env -D
  1. webpack.config.js
 use: [
    {
        loader: './loader/babel-loader/index.js',
        options: {
            presets: ['@babel/preset-env']
        }
    }
]
  1. loader/babel-loader/index.js
const schema = require('./schema.json')
const babel = require('@babel/core')

module.exports = function (source, sourceMap, meta) {
  // 异步loader
  const callback = this.async()
  const options = this.getOptions(schema)
  // 使用babel对代码进行编译转换
  babel.transform(source, options, (err, result) => {
    if (err) return callback(err)
    else callback(null, result.code)
  })
}
  1. loader/babel-loader/schema.json
{
    "type": "object",
    "properties": {
        "presets": {
            "type": "array"
        }
    },
    "additionalProperties": true
}

5.15 自定义loader:file-loader

  1. 安装依赖
npm i loader-utils -D
  1. webpack.config.js
{
  test: /\.(png|jpe?g)$/,
  use: [
    {
      loader: './loader/file-loader/index.js'
    }
  ],
  type: 'javascript/auto' // 阻止webpack默认处理图片资源,只使用loader处理
}
  1. loader/file-loader/index.js

步骤参照的是:type: assets打包后的图片资源格式

// 处理图片、字体文件数据,都是buffer数据,需要使用 raw loader 才能处理
const loaderUtils = require('loader-utils')

module.exports = function (source) {
  // 根据文件内容生成一个带hash值的文件名(使用第三方 loader-utils)
  /**
   * 参数1:this,loader的上下文对象
   * 参数2:文件名模板,支持变量,如 [hash]、[ext]、[path]、[name]
   * 参数3:传递的参数,{content: 文件内容}
   */
  let filename = loaderUtils.interpolateName(this, '[hash].[ext]', {
    content: source
  })
  filename = `images/${filename}`  
  // 将文件输出出去
  this.emitFile(filename, source) // loader API:emitFile用于输出文件
  // 返回文件路径
  return `module.exports = "${filename}"`
}

module.exports.raw = true

6. plugin

plugin:用来扩展 webpack 的功能。

6.1 html-webpack-plugin

作用:自动生成 html 文件,自动引入打包后的 js 文件

  • 使用步骤:
  1. 安装 html-webpack-plugin
npm i html-webpack-plugin -D
  1. webpack.config.js 文件中添加 plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development',
  output: {
    clean: true
  },
  plugins: [
    // new HtmlWebpackPlugin(), // 自动生成 html 文件,自动引入打包后的 js 文件
    new HtmlWebpackPlugin({
      title: 'webpack app', // template的title会优先于这个title
      template: './public/index.html' // 模板文件
    })
  ]
}

6.2 mini-css-extract-plugin

作用:将 css 提取到单独的文件中,为每个包含 CSS 的 Js 文件创建一个 CSS 文件,需要配置loader(module)和plugins

  • 使用步骤:
  1. 安装mini-css-extract-plugin
npm install mini-css-extract-plugin -D
  1. webpack.config.js 文件中添加 loader

**注意:**使用了MiniCssExtractPlugin后,需要去掉style-loader,因为他会将css打包到js中去

module: {
   rules: [
      {
        test: /\.css$/i,
        // use: ['style-loader', 'css-loader']
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test: /\.less$/i,
        // use: [ 'style-loader', 'css-loader', 'less-loader']
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
      }
    ]
}
  1. webpack.config.js 文件中添加 plugins
plugins: [
    new MiniCssExtractPlugin({
      filename: 'static/css/main.css'
    })
]
  • 完整代码:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true
  },
  entry: './src/index.js',
  output: {
    filename: '[hash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        // use: ['style-loader', 'css-loader']
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test: /\.less$/i,
        // use: ['style-loader', 'css-loader', 'less-loader']
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'static/css/main.css'
    }),
    new HtmlWebpackPlugin({
      title: 'webpack app',
      template: './public/index.html'
    })
  ],
  devtool: 'source-map'
}

6.3 plugin工作原理

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。
webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。

6.4 webpack内部的钩子

(1)、什么是钩子

钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。

(2)、Tapable
  1. Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。

  2. Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

  • tap:注册同步钩子。

  • tapAsync:回调函数方式注册异步钩子。

  • tapPromise:Promise 方式注册异步钩子。

6.5 plugin构建对象

(1)、Compiler(提供webpack的钩子)

compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。

这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。

它有以下主要属性:

  • compiler.options 可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
  • compiler.inputFileSystemcompiler.outputFileSystem 可以进行文件操作,相当于 Nodejs 中 fs。
  • compiler.hooks 可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。

compiler hooks 文档:https://www.webpackjs.com/api/compiler-hooks/

// tap为Tapable插件接口
compiler.hooks.someHook.tap('MyPlugin', (params) => {
  /* ... */
});
(2)、Compilation(对资源的处理)

compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。

一个 compilation 对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

它有以下主要属性:

  • compilation.modules 可以访问所有模块,打包的每一个文件都是一个模块。
  • compilation.chunks chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。
  • compilation.assets 可以访问本次打包生成所有文件的结果。
  • compilation.hooks 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。
(3)、生命周期

Compiler:只会创建一次

Compilation:会创建多次

在这里插入图片描述

6.6 自定义插件

插件都是围绕着“钩子”展开,在编译的某个环节触发钩子,某种程度上可以理解为 —— 事件

在这里插入图片描述

  • 时机:compiler.hooks.compilation
  • 参数(上下文):compilation
  • 交互:dependencyFactores.set
// custom-plugin.js
/**
 * 1、webpack加载webpack.config.js配置文件,此时会new CustomPlugin(),执行插件的constructor
 * 2、webpack创建compiler对象,
 * 3、遍历所有plugins中的插件,此时会执行插件的apply方法
 * 4、执行剩下的流程(触发各个hooks事件)
 */
class CustomPlugin {
  constructor() {
    console.log('触发了')
  }
  
  apply(compiler) {
    // compiler.hooks.thisCompilation 当 webpack 开始构建的时候,会触发这个钩子,此时可以获取到 compilation 对象
    // compilation对象 是每次创建的上下文实例
    compiler.hooks.thisCompilation.tap('CustomPlugin', (compilation) => {
      console.log(compilation)
    })
  }
}

module.exports = CustomPlugin
// webpack.config.js
const CustomPlugin = require('./custom-plugin.js')
export default {
  plugins: [
    new CustomPlugin()
  ]
}

6.7 注册hooks

hooks分类:

  • Compiler的hooks
  • Compilation的hooks
class TestPlugin {
  constructor() {
    console.log('TestPlugin')
  }

  apply(compiler) {
    /**
     * 由官方文档,environment是同步钩子,Tapable是tap注册
     * compiler: webpack实例
     * hooks: webpack实例上的钩子
     * environment:钩子名
     * tap:注册钩子(Tapable)
     * tap的参数1: 插件名
     * tap参数2: environment钩子回调函数,参数根据官方文档来定
     */
    compiler.hooks.environment.tap('TestPlugin', () => {
      console.log('environment')
    })

    // emit钩子(异步串型钩子AsyncParallelHook:可以做异步操作,但是必须按顺序执行)
    compiler.hooks.emit.tap('TestPlugin', (compilation) => {
      console.log('emit tap')
    })
    compiler.hooks.emit.tapAsync('TestPlugin', (compilation, callback) => {
      setTimeout(() => {
        console.log('emit tapAsync')
        callback()
      }, 1000)
    })
    compiler.hooks.emit.tapPromise('TestPlugin', (compilation) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log('emit tapPromise')
          resolve()
        }, 1000)
      })
    })

    // make钩子(异步并行钩子AsyncSeriesHook:一开始全部触发)
    compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
      // 根据生命周期图所示,compilation钩子不能emit中触发,在需要在compilation hooks(钩子)触发前注册
      compilation.hooks.seal.tap('TestPlugin', () => {
        console.log('seal')
      })
      setTimeout(() => {
        console.log('make tapAsync')
        callback()
      }, 1000)
    })
  }
}

module.exports = TestPlugin

输出:

TestPlugin
environment
make tapAsync
seal
emit tap
emit tapAsync
emit tapPromise

6.8 通过node调试查看compiler和compilation

  1. 在需要的地方添加debug
debugger
console.log(compiler)
debugger
console.log(compilation)
  1. package.json配置脚本
"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js --mode development"
  1. 执行脚本后,再浏览器任意窗口打开控制台

在这里插入图片描述

点击绿色的图标

在这里插入图片描述

6.9 自定义插件:BannerWebpackPlugin

作用:给打包输出文件添加注释

步骤:

  1. 打包输出前添加注释:需要使用 compiler.hooks.emit 钩子, 它是打包输出前触发。
  2. 如何获取打包输出的资源?compilation.assets 可以获取所有即将输出的资源文件。

实现:

  1. webpack.config.js
plugins: [
    new BannerWebpackPlugin({
        author: 'wifi歪f' // 会传入到插件的options中
    })
]
  1. plugins/banner-plugins.js
class BannerWebpackPlugin {
  constructor(options = {}) {
    this.options = options
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'BannerWebpackPlugin',
      (compilation, callback) => {
        // 获取输出的资源
        const assets = compilation.assets

        // 过滤只保留js和css资源
        const extensions = ['js', 'css']
        const assetsArr = Object.keys(assets).filter((filepath) => {
          const splitted = filepath.split('.')
          const extension = splitted[splitted.length - 1] // 文件扩展名
          return extensions.includes(extension)
        })

        const prefix = `/*
* Author: ${this.options.author}
* ${new Date().toLocaleString()}
*/`
        // 遍历剩下资源,添加注释
        assetsArr.forEach((asset) => {
          // 获取资源内容
          const source = compilation.assets[asset].source()

          // 改变输出的资源
          compilation.assets[asset] = {
            // source:返回输出文件内容
            source: () => prefix + source,
            // 输出文件大小
            size: () => (prefix + source).length
          }
        })

        callback()
      }
    )
  }
}

module.exports = BannerWebpackPlugin

6.10 自定义插件:CleanWebpackPlugin

作用:在 webpack 打包输出前将上次打包内容清空。

步骤:

  1. 如何在打包输出前执行?需要使用 compiler.hooks.emit 钩子, 它是打包输出前触发。

  2. 如何清空上次打包内容?

    1. 获取打包输出目录:通过 compiler 对象。
    2. 通过文件操作清空内容:通过 compiler.outputFileSystem 操作文件。

实现:

主要将output.clean关掉

class CleanWebpackPlugin {
    apply(compiler) {
        // 1. 注册钩子,在打包输出之前 emit
        compiler.hooks.emit.tapAsync(
            'CleanWebpackPlugin',
            (compilation, callback) => {
                // 2. 获取打包输出目录
                const outputPath = compiler.options.output.path
                // 3. 通过fs删除打包输出目录下的所有文件
                const fs = compiler.outputFileSystem
                this.removeFiles(fs, outputPath)
                callback()
            }
        )
    }

    removeFiles(fs, filepath) {
        // 删除打包目录下的所有资源,需要先将目录下的资源删除,再删除目录(不能直接删除这个目录)
        // 1. 读取目录下的所有资源
        const files = fs.readdirSync(filepath)
        // 2. 遍历所有资源,删除
        files.forEach((file) => {
            // 拼接路径
            const curPath = `${filepath}/${file}`
            // 判断是否是目录
            const stat = fs.statSync(curPath)
            if (stat.isDirectory()) { // 是目录
                // 递归删除
                this.removeFiles(fs, curPath)
            } else {
                // 删除文件
                fs.unlinkSync(curPath)
            }
        })
    }
}

module.exports = CleanWebpackPlugin

6.11 自定义插件:AnalyzeWebpackPlugin

作用:分析 webpack 打包资源大小,并输出分析文件。

步骤:

  1. compiler.hooks.emit, 它是在打包输出前触发,我们需要分析资源大小同时添加上分析后的 md 文件。

实现:

class AnalyzeWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('AnalyzeWebpackPlugin', (compilation) => {
      // 遍历所有文件,得到其大小
      /**
       * 将对象变成一个二维数组
       *  对象:
       *      {
       *          key1: value1,
       *          key2: value2
       *      }
       *  二维数组:
       *      [
       *          [key1, value1],
       *          [key2, value2]
       *      ]
       */
      const assets = Object.entries(compilation.assets)

      /**
       * md表格语法格式
       * | 资源名称 | 资源大小 |
       * | --- | --- |
       * | key1 | value1 |
       */
      let content = `| 资源名称 | 资源大小 |
| --- | --- |`

      assets.forEach(([filename, file]) => {
        content = content + `\n| ${filename} | ${file.size()} |`
      })

      // 生成md文件
      compilation.assets['analyze.md'] = {
        source: () => content,
        size: () => content.length
      }
    })
  }
}

module.exports = AnalyzeWebpackPlugin

7. loader和plugin在运行时机上的区别

  • loader 运行在打包文件之前上
  • plugins 在整个编译周期都起作用

对于 loader,实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将 A.scSS 或 A.less 转变为 B.css,单纯的文件转换过程。

在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。

三、webpack使用(高级)

1. devServer 开发服务器

devServer:是一个开发服务器,用于开发环境,它可以在内存中生成打包后的文件,不会在磁盘中生成打包后的文件,也不会在磁盘中生成 source-map 文件,它可以在浏览器中访问打包后的文件,也可以在浏览器中访问 source-map 文件

  • 使用步骤:
  1. 安装 webpack-dev-server
npm i webpack-dev-server -D
  1. 运行命令 webpack serve
npx webpack serve
npx webpack serve --open --port 8080 # 自动打开浏览器,端口号为8080

也可以在 webpack.config.js 文件中添加 devServer

{
    devServer: {
      	hot: true, // hmr热加载
        port: 3000,
        open: true
    }
}

2. hmr

提升打包构建速度:hmr(热模块替换),在程序运行中,添加或删除模块,无需重新加载整个页面。

{
    devServer: {
      	hot: true, // hmr热加载
        port: 3000,
        open: true
    }
}

js文件一般是不支持热模块替换的,需要单独添加:

if (module.hot) {
  	// 只有添加了的文件才会实现热模块替换
    module.hot.accept('./count.js')
  	module.hot.accept('./count2.js', () => {
        console.log('accept')
    })
}

3. source-map

source-map:是一个映射文件,用于将打包后的文件映射到源代码文件,方便调试

  • 使用步骤:
  1. webpack.config.js 文件中添加 devtool
  • devtool
    • cheap-module-source-map:只映射行,没有列映射
    • source-map:既有行,又有列
    • inline-source-map
{
  devtool: 'inline-source-map'
}

4. tree-shaking

tree-shaking—树摇,用于删除Dead Code,就是将引入后未使用的代码,进行删除过滤,不打包到最终产物里(只能对esmodule进行过滤,而commonjs不行)

对于esm的话也是有讲究的,默认导出是不会过滤的,只有按需导出,未使用的才会被过滤掉

  • 使用步骤:开启tree-shaking
    • "mode": "production"
    • optimization.usedExports: true
export default {
  mode: 'production',
  optimization: {
  	usedExports: true
  }
}

5. OneOf

OneOf每个文件只能被其中一个loader处理,匹配上了第一个就不会去判断下面了

module: {
  rules: [
    {
      oneOf: [
        {
          test: /\.css$/i,
          use: [MiniCssExtractPlugin.loader, 'css-loader']
        },
        {
          test: /\.less$/i,
          use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
        }
      ]
    }
  ]
},

6. Include-Exclude(包含、排除)

开发时用到的第三方库,所有的依赖文件都下载到node_modules中,而这些文件是编译后的,可以直接使用。所以在对js处理时,需要排除node_modules中的文件

  • Include(包含):只处理某些文件
  • Exclude(排除):不处理某些文件
{
  test: /\.js$/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env']
      }
    }
  ],
  exclude: /node_modules/, // 排除node_modules 文件夹下的文件
  include: path.resolve(__dirname, 'src') // 只包含 src 文件夹下的文件
}

7. Cache(缓存)

打包时,js文件都要经过eslint和babel编译,速度比较慢,可以缓存之前的eslint缓存和babel编译结果,这样第二次打包速度就会更快

  • Babel
{
  test: /\.js$/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env'],
        cacheDirectory: true, // 开启babel缓存
        cacheCompression: false, // 关闭缓存文件的压缩
      }
    }
  ],
  exclude: /node_modules/, // 排除node_modules 文件夹下的文件
  include: path.resolve(__dirname, 'src') // 只包含 src 文件夹下的文件
}
cache

这时候在node_modules下面会生成.chche目录

  • eslint
plugins: [
    new ESLintPlugin({
      caches: true,
      cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache')
    })
]

8. Thread(多进程打包)

  • 安装依赖
npm i thread-loader -D
  • 使用

terser-webpack-plugin用于压缩js

const TerSerWebpackPlugin = require('terser-webpack-plugin') // webpack内置
const os = require('os')
// 获取cpu核数
const threads = os.cpus().length

// 放到需要的loader前 
{
  test: /\.js$/,
  use: [
    {
      loader: 'thread-loader', // 开启多进程
      options: {
        works: threads // 进程数量
      }
    },
    {
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env'],
        cacheDirectory: true, // 开启babel缓存
        cacheCompression: false, // 关闭缓存文件的压缩
      }
    }
  ],
  exclude: /node_modules/, // 排除node_modules 文件夹下的文件
  include: path.resolve(__dirname, 'src') // 只包含 src 文件夹下的文件
}

// 写法1: 在plugins
plugins: [
  new TerSerWebpackPlugin({
    parallel: threads // 开启多进程压缩
  }),
]

// 写法2: optimization
plugins: [],
optimization: {
  minimizer: [ // 需要压缩的内容
    new TerSerWebpackPlugin({
      parallel: threads // 开启多进程压缩
    })
  ]
},

9. 优化代码运行性能 code split—optimization(代码分割)

code split:代码分割

打包代码时,会将所有的js文件打包到一个文件中,体积会变得很大。需要将代码进行分割,生成多个js文件,渲染哪个页面,就加载某个js文件,这样就可以提升运行性能。

9.1 code split:多入口

entry: { // 多入口
    app: './src/app.js',
    main: './src/main.js'
},

9.2 code split:多入口提取公共模块

问题发现:

9.1中,我们对app和main两个文件进行了分别打包,同时在app和main中引入公共模块math.js

// app.js
import { sum } from "./math";

console.log('app.js');
console.log(sum(1,2,3), 'app');
// main.js
import { sum } from "./math";

console.log('main.js');
console.log(sum(1,2,3), 'main');

看打包产物:

  • app的
(()=>{"use strict";console.log("app.js"),console.log([1,2,3].reduce(((o,e)=>o+e),0),"app")})();
  • main的
(()=>{"use strict";console.log("main.js"),console.log([1,2,3].reduce(((o,e)=>o+e),0),"main")})();

上面我们可以看到:在app和main两个文件中都对公共模块进行了分别打包,造成了重复代码,我们希望对sum也进行单独拆分,让app和mian引入sum即可

使用:optimization.splitChunks

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: { // 多入口
        app: './src/app.js',
        main: './src/main.js'
    },
    output: {
        filename: '[name]-[hash].js',
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html'
        })
    ],
    optimization: {
        // 代码分割配置
        splitChunks: {
            chunks: 'all', // 对所有模块都进行分割
            // 默认配置
            // minSize: 20000, // 模块超过20kb才进行分割
            // minRemainingSize: 0, // 类似于miniSize,最后确保提取的文件大小不能为0
            // minChunks: 1, // 模块至少使用1次才进行分割
            // maxAsyncRequests: 30, // 按需加载时并行加载的最大个数
            // maxInitialRequests: 30, // 入口js文件最大并行请求数
            // enforceSizeThershold: 50000, // 超过50kb的模块强制进行分割(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
            // cacheGroups: { // 组,哪些模块要打包到一个组
                // defaultVendors: { // 组名,匹配所有模块,包括node_modules中的模块
                //     test: /[\\/]node_modules[\\/]/,
                //     priority: -10, // 优先级,越大越优先
                //     reuseExistingChunk: true // 如果该模块已经被提取过,就直接复用,而不是重新打包
                // },
                // default: { // 其他未匹配到的模块,打包到一个组中,组名为default
                //     minChunks: 2, // 至少被2个模块共享,这里minChunks权重更大
                //     priority: -20,
                //     reuseExistingChunk: true // 复用已打包的模块
                // }
            // },
            // 修改配置
            cacheGroups: {
                // 其他没有写的配置会用上面的默认值
                default: { // 其他未匹配到的模块,打包到一个组中,组名为default
                    minSize: 0, // 最小文件体积
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true
                }
            }
        }
    }
}

9.3 code split:多入口按需加载(懒加载)

实现按需加载,动态导入模块配置:

发现问题:

当在app中引入count方法,当没有点击按钮,count文件也会被引入,我需要在点击按钮,动态引入count文件

import { sum } from "./math";
import { count } from "./count";

console.log('app.js');
console.log(sum(1,2,3), 'app');

document.querySelector('#btn').onclick = function () {
    count(1,2)
}

实现:

import动态导入,特点:会将动态导入的文件单独打包,在需要的时候自动加载,打包后的文件名由webpack自动生成

import('./count').then(res => {
    console.log(res.count(1,2), 'btn');
}).catch(err => {
    console.log(err);
})
import { sum } from "./math";
// import { count } from "./count";

console.log('app.js');
console.log(sum(1,2,3), 'app');

document.querySelector('#btn').onclick = function () {
    // import动态导入,特点:会将动态导入的文件单独打包,在需要的时候自动加载,打包后的文件名由webpack自动生成
    import('./count').then(res => {
        console.log(res.count(1,2), 'btn');
    }).catch(err => {
        console.log(err);
    })
}

9.4 code split:单入口

开发时可能是单页面应用(SPA),只有一个入口(单入口),配置入下:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: './src/main.js', // 单入口
    output: {
        filename: 'js/[name]-[hash].js',
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html'
        })
    ],
    optimization: {
        // 代码分割配置
        splitChunks: {
            chunks: 'all', // 对所有模块都进行分割
        }
    }
}

9.5 code split:给动态导入模块命名

动态导入的文件,文件名是随机的数字,要想给它命名,需要按照以下要求配置

/* webpackChunkName: 'xxx' */:webpack魔法命名

  • js中:
document.querySelector('#btn').onclick = function () {
    // import动态导入,特点:会将动态导入的文件单独打包,在需要的时候自动加载,打包后的文件名由webpack自动生成
    import(/* webpackChunkName: 'count' */'./count').then(res => {
        console.log(res.count(1,2), 'btn');
    }).catch(err => {
        console.log(err);
    })
}
  • webpack.config.js(可以不写)

给打包输出的其他文件命名:chunkFilename

 output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    chunkFilename: 'js/[name].chunk.js'
},
  1. 只写/* webpackChunkName: 'xxx' */,生成的名字为:xxx.js

  2. /* webpackChunkName: 'xxx' */chunkFilename,生成的名字为:xxx.chunk.js

9.6 code split:统一命名

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  entry: './src/main.js', // 单入口
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    // 打包输出的其他文件的文件名
    chunkFilename: 'js/[name].chunk.js',
    // 静态资源打包路径
    assetModuleFilename: 'static/media/[name].[hash][ext]'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new MiniCssExtractPlugin({
      filename: 'static/css/main.css',
      chunkFilename: 'static/css/[name].chunk.css'
    })
  ],
  optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: 'all' // 对所有模块都进行分割
    }
  }
}

10. Preload和Prefetch

在前面做了代码分割(code split),也做了动态import导入(也叫懒加载),但是如果动态加载的文件很大,也会造成卡顿。可以通过浏览器空闲时间,加载后续使用的代码,就需要用上preloadprefetch技术

定义:

  • preload:浏览器立即加载资源
  • prefetch:在浏览器空闲时间加载资源

共同点:

  • 都会加载资源,但是不会执行
  • 都有缓存

区别:

  • preload加载优先级高,prefetch加载优先级低
  • preload只能加载当前页面需要使用的资源,prefetch可以加载当前页面资源,也可以加载下一个页面使用的资源

总结:

  • 当前页面优先级高的资源用preload加载
  • 下一个页面需要的资源用prefetch加载

使用1:

  • preload
  1. 安装
npm i @vue/preload-webpack-plugin -D
  1. 使用
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin')

plugins: [
  new PreloadWebpackPlugin({
    rel: 'preload', // 以preload方式加载
    as: 'script'
  })
]

注意:main入口文件还是正常引入,只是其他资源会添加上rel="preload"

<link
  href="js/count.chunk.js"
  rel="preload"
  as="script"
/>

使用2:

webpackPreload: true

import(/* webpackChunkName: 'count', webpackPreload: true */'./count')
  • prefetch

webpackPrefetch: true

import(/* webpackChunkName: 'count', webpackPrefetch: true */'./count').then(res => {
    console.log(res.count(1,2), 'btn');
}).catch(err => {
    console.log(err);
})

11. Network Cache

发生问题:

A模块使用了B模块的内容,当B模块文件的内容发生变化,打包后AB模块文件hash名都会变化(原因:A模块引入B,B的hash变化,所以导致A引入B的文件名变化,A的hash也会变化),当模块依赖过多,就会导致很多文件缓存实效

解决:

将A保存(引入)B文件的hash值(也就是引入的文件名路径)单独保存到一个文件,叫runtime运行时文件。当B模块变化,只会导致B模块和runtime文件发生变化,A文件名不变

plugins: [],
optimization: {
    // 代码分割配置
  splitChunks: {
    chunks: 'all' // 对所有模块都进行分割
  },
  runtimeChunk: {
    name: entrypoint => `runtime~${entrypoint.name}`
  },
}

12. 解决js兼容性问题Core-js

之前我们使用babel对js进行兼容性处理,其中使用@babel/preset-env智能预设来处理兼容性问题,它可以将es6的一些语法进行编译转换,比如箭头函数、扩展运算符…,但是如果是async函数,promise对象,数组的一些方法(比如includes…),它没办法处理。

所以此时js代码仍然存在兼容性问题,core-js就是专门来做es6以上api的polyfill

polyfill又叫做垫片/补丁,用于提供原生不支持的功能

  • babel和core-js的区别
    • babel:处理新语法
    • core-js:处理新api

用法:

  1. 全部引入
npm i core-js
// mian.js
import 'core-js'
  1. 按需引入
// main.js
import 'core-js/es/promise'
  1. 自动按需引入(通过babel)

webpack.config.js

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: [
        {
          loader: 'babel-loader', // 通过babel.config.js具体配置
        }
      ],
    },
  ]
}

babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage', // 按需加载,自动引入
        corejs: 3 // 指定core-js版本
      }
    ]
  ]
}

为了效果明显,在package.json中添加:

"browserslist": [ // 需要兼容ie11,效果更明显
  "last 2 versions",
  "not dead",
  "ie 11"
]

就不需要单独/全部引入了

13. PWA 离线访问

pwa:是一种可以提供类似native app(原生应用)的web app技术,让它在离线的时候也能继续运行,内部通过Service Workers技术实现的

使用:

  1. 安装依赖
npm install workbox-webpack-plugin --save-dev
  1. webpack.config.js
const WorkboxPlugin = require('workbox-webpack-plugin');

plugins: [
 new WorkboxPlugin.GenerateSW({
   // 这些选项帮助快速启用 ServiceWorkers
   // 不允许遗留任何“旧的” ServiceWorkers
   clientsClaim: true,
   skipWaiting: true,
 }),
],
  1. 需要在main.js中注册Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then((registration) => {
        console.log('SW registered: ', registration)
      })
      .catch((registrationError) => {
        console.log('SW registration failed: ', registrationError)
      })
  })
}

14. @babel/plugin-transform-runtime 减少babel生成文件的体积

babel为每个编译的文件都插入了辅助代码,使代码体积过大。babel对一些公共方法使用了非常小的辅助代码,比如:_extend,默认情况下会被添加到每个需要的文件中,可以将这些辅助代码作为一个独立的模块,来避免重复引入。

@babel/plugin-transform-runtime:禁用了babel自动对每个文件的runtime注入,而是引入@babel/plugin-transform-runtime,并且使所有辅助代码从这里引用

使用:

  1. 安装
npm i @babel/plugin-transform-runtime -D
  1. 使用
{
  loader: 'babel-loader',
  options: {
    presets: ['@babel/preset-env'],
    cacheDirectory: true, // 开启babel缓存
    cacheCompression: false, // 关闭缓存文件的压缩
    plugins: ['@babel/plugin-transform-runtime'], // 减少代码体积
  }
}

15. 总结

  • 提升开发体验
    • source-map:能够准确定位代码后代码的错误提示
  • 提升打包构建速度
    • hmr
    • OneOf
    • Include/Exclude
    • Cache
    • Thead
  • 减少代码体积
    • tree shaking
    • @babel/plugin-transform-runtime:减少babel生成文件的体积
  • 优化代码运行性能
    • code-split
    • preload/prefetch
    • network cache
    • core-js
    • pwa

四、React-Cli

1、开发环境配置

1.1 基本配置

  1. 初始化package.json
npm init -y
  1. 安装webpack依赖
npm i webpack webpack-cli webpack-dev-server -D
  1. 安装loader的对应依赖
  • style-loader
  • css-loader
  • less
  • less-loader
  • sass
  • sass-loader
  • postcss-loader
  • postcss-preset-env
  • @babel/core
  • babel-loader
  • @babel/preset-env
  • @babel/preset-react
npm i style-loader css-loader less less-loader sass sass-loader postcss-loader postcss-preset-env @babel/core babel-loader @babel/preset-env @babel/preset-react -D
  1. 安装plugins的对应依赖
  • eslint
  • eslint-webpack-plugin
  • eslint-config-react-app
  • html-webpack-plugin
npm i eslint eslint-webpack-plugin eslint-config-react-app html-webpack-plugin -D
  1. 新建并配置webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const EslintWebpackPlugin = require('eslint-webpack-plugin');

const getStyleLoaders = (...pre) => {
    return [
        'style-loader',
        'css-loader',
        {
            loader: 'postcss-loader',
            options: {
                postcssOptions: {
                    plugins: [
                        'postcss-preset-env' // 处理样式兼容性问题,需要配合package.json中的browserslist配置(来确定兼容性版本)
                    ]
                }
            }
        },
        ...pre
    ].filter(Boolean)
}

module.exports = {
    entry: './src/main.js',
    output: {
        path: undefined, // 开发模式下不需要配置输出路径
        filename: 'js/[name].js',
        chunkFilename: 'js/[name].chunk.js',
        // [hash:10]:取hash的前10位,[ext]:取文件扩展名
        assetModuleFilename: 'assets/[hash:10][ext]' // 配置打包后的静态资源路径
    },
    module: {
        rules: [
            // 处理css
            {
                test: /\.css$/,
                use: getStyleLoaders()
            },
            // 处理less
            {
                test: /\.less$/,
                use: getStyleLoaders('less-loader')
            },
            // 处理sass
            {
                test: /\.s[ac]ss$/,
                use: getStyleLoaders('sass-loader')
            },
            // 处理图片
            {
                test: /\.(png|jpe?g|gif|webp|svg)$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        // 小于10kb的图片会被base64处理
                        maxSize: 10 * 1024
                    }
                }
            },
            // 处理其他资源
            {
                test: /\.(woff2?|eot|ttf|otf)$/,
                type: 'asset/resource',
                generator: {
                    filename: 'font/[hash:10][ext]'
                }
            },
            // 处理js、jsx
            {
                test: /\.jsx?$/,
                include: path.resolve(__dirname, './src'),
                use: [{
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: true, // 开启缓存
                        cacheCompression: false // 关闭缓存文件压缩
                    }
                }]
            }
        ]
    },
    plugins: [
        // 配置html模板
        new HtmlWebpackPlugin({
            template: './public/index.html',
            favicon: './public/favicon.svg'
        }),
        // eslint
        new EslintWebpackPlugin({
            context: path.resolve(__dirname, 'src'), // 检测哪些文件
            exclude: 'node_modules',
            cache: true, // 开启缓存
            cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache') // 缓存路径
        })
    ],
    mode: 'development',
    devtool: 'cheap-module-source-map',
    resolve: {
        alias: {
            '@': path.resolve(__dirname, '../src')
        },
        extensions: ['.js', '.json', '.jsx', '.tsx', '.ts'] // 配置省略文件后缀
    },
    devServer: {
        host: 'localhost',
        port: '3000',
        open: true,
        hot: true,
      	historyApiFallback: true, // 解决前端刷新404问题
    },
    optimization: { // 配置代码分割
        splitChunks: {
            chunks: 'all'
        },
        runtimeChunk: { // 运行时代码
            name: entrypoint => `runtime~${entrypoint.name}`
        }
    }
}
  1. 新建并配置babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    ['@babel/preset-react', { runtime: 'automatic' }]
  ]
}
  1. 新建并配置.eslintrc.js
module.exports = {
    extends: ['react-app']
}
  1. package.json中新增browserslist
{
  "dependencies": {
		...
  },
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
}
  1. 安装react相关依赖
npm i react react-dom react-router-dom
  1. 在public目录下新建index.html和放入小图标

需要给<div id="root"></div>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
  1. src下新建如下:
  • src/main.js(不要写成jsx,因为webpack.config.js中写的entry是main.js)
  • src/App.jsx
  • src/index.scss
  • src/views/home.jsx
  • src/views/about.jsx
// main.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { BrowserRouter } from 'react-router-dom'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
)
// App.jsx
import React from 'react'
import './index.scss'
import { Routes, Route, Outlet, Navigate, NavLink } from 'react-router-dom'
import Home from './views/home'
import About from './views/about'

export default function App() {
  return (
    <>
      <h1 className='h1-title'>Hello React-Cli</h1>
      <NavLink to='/home'>Home</NavLink> | 
      <NavLink to='/about'>about</NavLink>
      <hr />
        <Routes>
        <Route path='/' element={<Navigate to="/home" />} />
          <Route
            path='/home'
            element={<Home />}
          >
          </Route>
          <Route
            path='/about'
            element={<About />}
          >
          </Route>
        </Routes>
        <Outlet />
    </>
  )
}
// index.scss
.h1-title {
    color: royalblue;
}
  1. package.json配置启动命令
"scripts": {
  "dev": "webpack serve",
  "dev2": "webpack serve --config webpack.config.js" // 也行
}

1.2 报错:

  1. Using babel-preset-react-app requires that you specify NODE_ENV or BABEL_ENV environment variables. Valid values are “development”, “test”, and “production”. Instead, received: undefined.

原因:缺少环境变量

解决:安装cross-env

安装依赖

npm i cross-env -D

将启动命令换成:

"scripts": {
  "dev": "cross-env NODE_ENV=development webpack serve",
}
  1. [eslint] Error while loading rule ‘flowtype/define-flow-type’: context.getAllComments is not a function Occurred while linting xxx

原因:ESLint 插件或规则与当前使用的 ESLint 版本不兼容导致的

解决:安装eslint-plugin-flowtype@latest

安装依赖

npm install eslint-plugin-flowtype@latest --save-dev

修改.eslintrc.js

module.exports = {
  extends: 'react-app',
  plugins: ['flowtype'],
  rules: {
      'flowtype/define-flow-type': 'off', // 禁用该规则
      'flowtype/use-flow-type': 'off', // 禁用该规则
      // 其他规则
  }
};

1.3 优化配置

  1. hmr

js代码更新不会触发hmr,在开发react时,可以用它的插件来实现hmr

插件:react-refresh

1.4 效果:

在这里插入图片描述

2、生产环境配置

  1. 安装依赖
  • mini-css-extract-plugin:提取css成单独的文件
  • css-minimizer-webpack-plugin:压缩css
  • terser-webpack-plugin:压缩js(webpack内置)
npm i mini-css-extract-plugin css-minimizer-webpack-plugin -D
  1. webpack.prod.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const EslintWebpackPlugin = require('eslint-webpack-plugin');
// 提取css成单独的文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 压缩css
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
// 压缩js
const TerserWebpackPlugin = require('terser-webpack-plugin');

const getStyleLoaders = (...pre) => {
    return [
        MiniCssExtractPlugin.loader,
        'css-loader',
        {
            loader: 'postcss-loader',
            options: {
                postcssOptions: {
                    plugins: [
                        'postcss-preset-env'
                    ]
                }
            }
        },
        ...pre
    ].filter(Boolean)
}

module.exports = {
    entry: './src/main.js',
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'js/[name].js',
        chunkFilename: 'js/[name].chunk.js',
        assetModuleFilename: 'assets/[hash:10][ext]',
        clean: true
    },
    module: {
        rules: [
            // 处理css
            {
                test: /\.css$/,
                use: getStyleLoaders()
            },
            // 处理less
            {
                test: /\.less$/,
                use: getStyleLoaders('less-loader')
            },
            // 处理sass
            {
                test: /\.s[ac]ss$/,
                use: getStyleLoaders('sass-loader')
            },
            // 处理图片
            {
                test: /\.(png|jpe?g|gif|webp|svg)$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
                        // 小于10kb的图片会被base64处理
                        maxSize: 10 * 1024
                    }
                }
            },
            // 处理其他资源
            {
                test: /\.(woff2?|eot|ttf|otf)$/,
                type: 'asset/resource',
                generator: {
                    filename: 'font/[hash:10][ext]'
                }
            },
            // 处理js、jsx
            {
                test: /\.jsx?$/,
                include: path.resolve(__dirname, './src'),
                use: [{
                    loader: 'babel-loader',
                    options: {
                        cacheDirectory: true, // 开启缓存
                        cacheCompression: false // 关闭缓存文件压缩
                    }
                }],
            }
        ]
    },
    plugins: [
        // 配置html模板
        new HtmlWebpackPlugin({
            template: './public/index.html',
            favicon: './public/favicon.svg'
        }),
        // eslint
        new EslintWebpackPlugin({
            context: path.resolve(__dirname, 'src'), // 检测哪些文件
            exclude: 'node_modules',
            cache: true, // 开启缓存
            cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache') // 缓存路径
        }),
        // 提取css
        new MiniCssExtractPlugin({
            filename: 'css/[name]-[contenthash:10].css',
            chunkFilename: 'css/[name]-[contenthash:10].chunk.css'
        })
    ],
    mode: 'production',
    devtool: 'source-map',
    resolve: {
        alias: {
            '@': path.resolve(__dirname, '../src')
        },
        extensions: ['.js', '.json', '.jsx', '.tsx', '.ts'] // 配置省略文件后缀
    },
    optimization: { // 配置代码分割
        splitChunks: {
            chunks: 'all'
        },
        runtimeChunk: { // 运行时代码
            name: entrypoint => `runtime~${entrypoint.name}`
        },
        minimizer: [
            // 压缩css
            new CssMinimizerWebpackPlugin(),
            // 压缩js
            new TerserWebpackPlugin()
        ]
    }
}
  1. 配置脚本
"scripts": {
  "dev": "cross-env NODE_ENV=development webpack serve --config webpack.dev.js",
   "build": "cross-env NODE_ENV=production webpack --config webpack.prod.js"
}

五、Vue-Cli

1、开发环境配置

1.1 基本配置

  1. 初始化package.json
npm init -y
  1. 安装webpack依赖
  • webpack
  • webpack-cli
  • webpack-dev-server
  • cross-env
npm i webpack webpack-cli webpack-dev-server cross-env -D
  1. 安装loader
  • vue-style-loader(vue对样式的处理不采用style-loader,而是采用vue-style-loader)
  • css-loader
  • postcss-loader
  • postcss-preset-env
  • less
  • less-loader
  • sass
  • sass-loader
  • stylus
  • stylus-loader
  • vue-loader(用于编译.vue结尾的文件)
  • vue-template-compiler(vue编译模版)
  • @babel/core
  • @babel/preset-env
  • babel-loader
  • @vue/cli-plugin-babel
  • core-js
npm i vue-style-loader css-loader postcss-loader postcss-preset-env less less-loader sass sass-loader stylus stylus-loader vue-loader vue-template-compiler babel-loader @babel/core @babel/preset-env @vue/cli-plugin-babel core-js -D
  1. 安装plugin
  • html-webpack-plugin
  • eslint
  • eslint-webpack-plugin
  • eslint-plugin-vue
  • @babel/eslint-parser
npm i html-webpack-plugin eslint eslint-webpack-plugin eslint-plugin-vue @babel/eslint-parser -D
  1. webpack.dev.js

⚠️注意:vue文件对样式的处理不采用style-loader,而是使用vue-style-loader

/** @type {import('webpack').Configuration} */
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const EslintWebpackPlugin = require('eslint-webpack-plugin')

const getStyleLoaders = (...pre) => {
  return [
    // vue文件对样式的处理不采用style-loader,而是使用vue-style-loader
    'vue-style-loader',
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env' // 处理样式兼容性问题,需要配合package.json中的browserslist配置(来确定兼容性版本)
          ]
        }
      }
    },
    ...pre
  ].filter(Boolean)
}

const config = {
  entry: './src/main.js',
  output: {
    path: undefined,
    filename: 'js/[name].js',
    chunkFilename: 'js/[name].chunk.js',
    assetModuleFilename: 'assets/[name].[hash:6][ext]'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: getStyleLoaders()
      },
      {
        test: /\.less$/,
        use: getStyleLoaders('less-loader')
      },
      {
        test: /\.s[ac]ss$/,
        use: getStyleLoaders('sass-loader')
      },
      {
        test: /\.styl$/,
        use: getStyleLoaders('stylus-loader')
      },
      // 处理图片
      {
        test: /\.(png|jpe?g|gif|webp|svg)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            // 小于10kb的图片会被base64处理
            maxSize: 10 * 1024
          }
        }
      },
      // 处理其他资源
      {
        test: /\.(woff2?|eot|ttf|otf)$/,
        type: 'asset/resource',
        generator: {
          filename: 'font/[hash:10][ext]'
        }
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      // 处理js
      {
        test: /\.js$/,
        include: path.resolve(__dirname, './src'),
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 开启缓存
              cacheCompression: false // 关闭缓存文件压缩
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      favicon: './public/favicon.svg'
    }),
    new EslintWebpackPlugin({
      context: path.resolve(__dirname, 'src'),
      exclude: 'node_modules',
      cache: true, // 开启缓存
      cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache') // 缓存路径
    }),
    // 需要从vue-loader导入使用
    new VueLoaderPlugin(),
  ],
  mode: 'development',
  devServer: {
    host: 'localhost',
    port: '3000',
    open: true,
    hot: true
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    },
    extensions: ['.js', '.json', '.vue'] // 配置省略文件后缀
  },
  devtool: 'cheap-source-map',
  optimization: {
    // 配置代码分割
    splitChunks: {
      chunks: 'all'
    },
    runtimeChunk: {
      // 运行时代码
      name: (entrypoint) => `runtime~${entrypoint.name}`
    }
  }
}

module.exports = config
  1. .eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: ['plugin:vue/vue3-essential', 'eslint:recommended'], // 在eslint-plugin-vue这个包中
  parserOptions: {
    parser: '@babel/eslint-parser'
  }
}
  1. babel.config.js
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset',
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: 3
      }
    ]
  ]
}
  1. package.json
"scripts": {
    "dev": "cross-env NODE_ENV=development webpack serve --config webpack.dev.js",
    "build": "cross-env NODE_ENV=production webpack --config webpack.prod.js"
}
  1. 其余步骤就是vue部分了,略…

2、生产环境配置

  1. 安装依赖
  • mini-css-extract-plugin
  • css-minimizer-webpack-plugin
  • terser-webpack-plugin(wbepack内置,如果没有需要安装)
  1. webpack.prod.js

⚠️注意:打包就不采用vue-style-loader,而是使用MiniCssExtractPlugin

/** @type {import('webpack').Configuration} */
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const EslintWebpackPlugin = require('eslint-webpack-plugin')
// 提取css成单独的文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 压缩css
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
// 压缩js
const TerserWebpackPlugin = require('terser-webpack-plugin')

const getStyleLoaders = (...pre) => {
  return [
    // 打包就不采用vue-style-loader,而是使用MiniCssExtractPlugin
    MiniCssExtractPlugin.loader,
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
        postcssOptions: {
          plugins: [
            'postcss-preset-env' // 处理样式兼容性问题,需要配合package.json中的browserslist配置(来确定兼容性版本)
          ]
        }
      }
    },
    ...pre
  ].filter(Boolean)
}

const config = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'js/[name].js',
    chunkFilename: 'js/[name].chunk.js',
    assetModuleFilename: 'assets/[name].[hash:6][ext]',
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: getStyleLoaders()
      },
      {
        test: /\.less$/,
        use: getStyleLoaders('less-loader')
      },
      {
        test: /\.s[ac]ss$/,
        use: getStyleLoaders('sass-loader')
      },
      {
        test: /\.styl$/,
        use: getStyleLoaders('stylus-loader')
      },
      // 处理图片
      {
        test: /\.(png|jpe?g|gif|webp|svg)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            // 小于10kb的图片会被base64处理
            maxSize: 10 * 1024
          }
        }
      },
      // 处理其他资源
      {
        test: /\.(woff2?|eot|ttf|otf)$/,
        type: 'asset/resource',
        generator: {
          filename: 'font/[hash:10][ext]'
        }
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      // 处理js
      {
        test: /\.js$/,
        include: path.resolve(__dirname, './src'),
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 开启缓存
              cacheCompression: false // 关闭缓存文件压缩
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      favicon: './public/favicon.svg'
    }),
    new EslintWebpackPlugin({
      context: path.resolve(__dirname, 'src'),
      exclude: 'node_modules',
      cache: true, // 开启缓存
      cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache') // 缓存路径
    }),
    new VueLoaderPlugin(),
    // 提取css
    new MiniCssExtractPlugin({
      filename: 'css/[name]-[contenthash:10].css',
      chunkFilename: 'css/[name]-[contenthash:10].chunk.css'
    })
  ],
  mode: 'production',
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    },
    extensions: ['.js', '.json', '.vue'] // 配置省略文件后缀
  },
  devtool: 'source-map',
  optimization: {
    // 配置代码分割
    splitChunks: {
      chunks: 'all'
    },
    runtimeChunk: {
      // 运行时代码
      name: (entrypoint) => `runtime~${entrypoint.name}`
    },
    minimizer: [
      // 压缩css
      new CssMinimizerWebpackPlugin(),
      // 压缩js
      new TerserWebpackPlugin()
    ]
  }
}

module.exports = config
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值