(上篇)梳理Webpack5基础配置、从0搭建一个Vue3+Ts项目(字数8k+)

前言

作为一个前端工程师,前端工程化这个词不绝于耳,而Webpack在前端工程化中扮演着至关重要的角色。 要明白webpack为什么重要,就要知道它为什么出现,解决了什么问题。

前端模块化从文件划分模块、命名空间划分模块、IIFE通过约定来实现模块化,到CommonJS、ES Module通过行业规范实现模块化,如今ES Module已经是最正统最主流的模块化规范,但是它依然还存在兼容问题,所以开发者还需要解决兼容问题。而且模块化开发,会划分出很多文件,每个文件就是一个模块,文件过大,浏览器请求、加载文件时间过长,影响页面渲染速度,文件过多浏览器请求频繁,也影响性能,所以需要对这些文件进行合并拆分。而项目复杂后,html、css、图片、字体文件等也需要模块化来管理。

于是webpack顺势而出,它是一个现代化的模块打包工具,支持js、css等不同种类资源的模块化(项目中使用的每个文件都是一个模块),同时对这些资源做兼容性处理,最后对这些资源文件根据需要做拆分合并压缩后打包为静态资源(html、css、js)

目前,webpack已经到了 webpack 5.89.0 了,应该静下心来好好看一看官网,这篇文章整理一下webpack5的基础配置,并用webpack5从零搭建一个vue3+ts项目,下篇文章整理webpack5的优化配置、分析相关原理!

一、基础配置

首先当然是先初始化一个项目,前提已经装好node

bash 复制代码
 npm init  或者  npm init -y

然后安装webpack,如果需要在命令行中调用 webpack,就需要安装webpack-cli

bash 复制代码
npm i webpack webpack-cli -D

然后建一个src目录和src/index.js、src/main.js,

js 复制代码
// index.js
import getName from "./main";
console.log(getName());

// main.js
export default function getName() {
  return "webpack5";
}

从 v4.0.0 开始,webpack 可以不用再引入一个配置文件 来打包项目,webpack打包入口默认值是 ./src/index.js,主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。

所以暂时先在命令行直接执行webpack来打包

bash 复制代码
webpack

如果环境变量没有修改就会报错 webpack: command not found,因为局部安装webpack的时候,安装到了node的bin下,并不是系统环境变量,可以用以下几种方法解决:

  • 1、在package.json的scripts中添加脚本,"build":"webpack",命令行执行npm run build
  • 2、全局安装webpack再执行webpack (不推荐)
  • 3、命令行执行 node_modules/.bin/webpack
  • 4、命令行执行 npx webpack

npx 会在当前目录下的./node_modules/.bin里去查找是否有可执行的命令,没有找到的话再从全局里查找是否有安装对应的模块,全局也没有的话就会自动下载对应的模块,用完就删,不会占用本地资源。

打包结果,代码做了压缩:

js 复制代码
// /dist/main.js
(()=>{"use strict";console.log("webpack5")})();

1、引入配置文件

项目根目录下创建一个 webpack.config.js 文件,webpack 会自动使用它。

可以自己修改入口 entry 和出口 output

js 复制代码
// webpack.config.js

const path = require("path");

module.exports = {
  // 入口
  entry: "./src/main.js",
  output: {
    // path 要用绝对路径
    path: path.resolve(__dirname, "dist"),
    // 文件名
    filename: "my-first-webpack.bundle.js",
    // 在生成文件之前清空 output 目录
    clean: true
  },
};

如果你不喜欢webpack.config.js这个名字,假如你想改成like.js,在命令行执行npx webpack --config like.js,或者在package.json的scripts中修改 "build":"webpack --config like.js",再执行npm run build

上面的打包结果中发现webpack5打包默认输出就是一个箭头函数的IIFE,为了兼容低版本,要去掉这个箭头函数, 需要在出口处配置

js 复制代码
module.exports = {
  output:{
    environment: {     
      arrowFunction: false,
    }
  }  
}

2、加载 css

要知道,webpack只能理解 JavaScriptJSON 文件,对于其他文件类型,就需要使用 loader,loader 用于对模块的源代码进行转换,loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。

准备一个css文件

css 复制代码
/* /src/style/index.css */

body h1{
  background-color: #e0f314;
  color: red;
}

webpack 的其中一个强大的特性就是能通过 import 导入任何类型的模块(例如 .css 文件)

js 复制代码
// /src/index.js

import "./style/index.css";

const h1 = document.createElement("h1");
h1.innerHTML = "hello, Webpack5";
document.body.appendChild(h1);

在根目录下建一个index.html

html 复制代码
<!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>
    <script src="./dist/main.js"></script>
  </body>
</html>

此时打包不出意外将报错,

bash 复制代码
ERROR in ./src/style/index.css 2:18
Module parse failed: Unexpected token (2:18)
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
| body: {
>   background-color: #e0f314;
|   color: red;
| }
 @ ./src/index.js 4:0-27

需要一个合适的loader才能解析css文件,这个合适的loader就是 css-loader

bash 复制代码
npm i css-loader -D
js 复制代码
// webpack.config.js
module.exports = {
  module: {
    // rules是数组,因为可能会处理很多其他资源模块,需要其他loader
    // test 匹配哪些文件需要转换
    // use 需要使用什么loader,如果要处理匹配到的这个资源需要多个loader,就写成数组形式
    rules: [{ test: /\.css$/, use: "css-loader" }],
    // 只用一个loader,还可以写成
    rules: [{ test: /\.css$/, loader: "css-loader" }],
  },
};

再次打包不会报错,但是页面样式没生效,因为css-loader只做了一件事,在webpack打包前解析css文件,并没有将css加入html中,所以还需要一个 style-loader

bash 复制代码
npm i style-loader -D

loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)

js 复制代码
// webpack.config.js

module.exports = {
  module: {
    // 从右到左,先用css-loader解析,再用style-loader插入style标签到html中
    rules: [{ test: /\.css$/, use: ["style-loader", "css-loader"]],
    // use里面直接写字符串是简写,如果要颗粒度更细的配置,就写成对象,如
      rules: [
      {
        test: /\.css$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              // css-loader的配置项
            },
          },
        ],
      },
    ],
  },
};

style-loader是通过创建style标签将样式插入到文档中

3、加载 sa(c)ss

平时项目中一般都是用sass(scss)或者less,它们肯定是没法直接跑在浏览器上的,所以也需要 sass-loader

bash 复制代码
npm install sass-loader sass -D

sass-loader依赖sass包,都需要安装

scss 复制代码
/* /src/style/index.scss */

$fontSize: 20px;
$color: rgb(86, 221, 145) 6;

@mixin baseStyle {
  background-color: aqua;
  border: 1px solid rgb(238, 245, 255);
}

h2 {
  @include baseStyle();
  font-size: $fontSize;
  color: $color;
}
js 复制代码
// /src/index.js

import "./style/index.scss";

const h2 = document.createElement("h2");
h2.innerHTML = "hello, scss";
document.body.appendChild(h2);
js 复制代码
module.exports = {
  module: {
    rules: [
     
      {
        test: /\.s[ac]ss$/,
         // 从右到左,现将sass编译成css,再处理css,再加入style
        use: ["style-loader", "css-loader", "sass-loader"],
      },
    ],
  },
};

如果要用到less,也是需要 less-loader,一样的道理

4、处理 css 新特性

部分css3的新属性在一些浏览器上还处于试验阶段,所以为了有效的显示css3的样式,对应不同的浏览器内核需要不同的前缀声明。

这就需要用到 PostCSS, PostCSS是一个用 JavaScript 工具和插件转换 CSS 代码的工具,利用从 Can I Use 网站获取的数据为 CSS 规则添加特定厂商的前缀。 Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。

postcss-loader 使用 PostCSS处理 CSS 的 loader,autoprefixer 是 postcss 的一个插件

bash 复制代码
npm i -D postcss-loader postcss autoprefixer
css 复制代码
/* /src/style/index.css */

::placeholder {
  color: red;
  font-size: 1.5em;
}

::selection {
  background-color: cyan;
}

body {
  user-select: none;
}
js 复制代码
// webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        // 先加前缀,再解析css文件,再插入style
        use: [
          "style-loader",
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                //postcss插件
                plugins: ["autoprefixer"],
              },
            },
          },
        ],
      }
    ],
   
  },
};

也可以将postcss的配置提取到一个单独的文件 postcss.config.js 中,在根目录下建一个postcss.config.js,webpack调用postcss-loader的时候会读取到postcss.config.js里面的配置

js 复制代码
// postcss.config.js
module.exports = {
  // postcss插件
  plugins: ["autoprefixer"],
};

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader", "postcss-loader"],
      }
    ],
  },
};

也可以使用postcss-preset-env,它包含 autoprefixer,不仅可以添加前缀,还能处理其他的css新特性

bash 复制代码
npm i -D postcss-preset-env
css 复制代码
/* /src/style/index.css */

:root {
  --mainColor: #12345678;
}

body {
  color: var(--mainColor);
  font-family: system-ui;
  /* 这个属性原本属于微软扩展的一个非标准、无前缀的属性,叫做 word-wrap,*/
  /* 后来在大多数浏览器中以相同的名称实现。*/
  /* 目前它已被更名为 overflow-wrap,word-wrap 相当于其别称。 */
  overflow-wrap: break-word;
  user-select: none;
}

a {
  color: rgb(0 0 100% / 90%);

  &:hover {
    color: rebeccapurple;
  }
}
js 复制代码
// postcss.config.js

module.exports = {
  // postcss插件
  plugins: ["postcss-preset-env"]
};

5、处理图片资源

webpack5 可以使用内置的 资源模块 轻松处理图像、字体资源,不需要像webpack5之前一样使用loader,资源模块类型有4 种:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
js 复制代码
// src/index.js

import "./style/index.css";
import imgUrl from "./assets/10kb.jpeg"; //大小10kb

const imgEL = new Image();
imgEL.src = imgUrl;
document.body.appendChild(imgEL);
css 复制代码
/* /src/style/index.css */
h1 {
  background: url("../assets/242kb.jpg"); /*大小242kb*/
}

资源类型设置为 type: "asset/resource",打包成单独文件,并把路径注入到包文件dist/main.js中

js 复制代码
// webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader", "postcss-loader"],
      },
      {
        test: /\.(png|svg|jpe?g|gif)$/,
        // type 属性设置资源模块类型
        // "asset/resource"无论图像大小,都会打包成单独的文件
        type: "asset/resource",
        generator: {
          // 生成的图片在assets目录下
          // 名字为原来的名字name截取10位hash值,保留原来的扩展名
          filename: "assets/[name]_[contenthash:10][ext]",
        },
      },
    ],
  },
};

资源类型设置为 type: "asset/inline",把所有图片文件都作为 data URI 注入到dist/main.js中。

js 复制代码
// webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader", "postcss-loader"],
      },
      {
        test: /\.(png|svg|jpe?g|gif)$/,
        // "asset/inline" 无论图像大小,图像文件默认编码为base64注入js文件中
        type: "asset/inline",       
      },
    ],
  },
};

资源类型设置为 type: "asset" ,默认小于 8kb 的文件,将会视为 inline 模块类型默认编码成base64注入包文件,否则会被视为 resource 模块类型单独打成文件。可以设置 Rule.parser.dataUrlCondition.maxSize选项来修改此条件:

js 复制代码
// webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader", "postcss-loader"],
      },
      {
        test: /\.(png|svg|jpe?g|gif)$/,
        type: "asset",
        parser: {
        // 如果一个模块源码大小小于 `maxSize`,那么模块会被作为一个 Base64 编码的字符串注入到包中, 
        // 否则模块文件会被生成到输出的目标目录中。
        // 大图片打包成base64注入js文件,使js文件变大,影响下载速度,所以最好单独打包下载
        // 小图片单独打包下载,浏览器请求频繁,也影响性能,可以直接打成base64
          dataUrlCondition: {
            maxSize: 20 * 1024,
          },
        },
        generator: {
          filename: "assets/[name]_[contenthash:10][ext]",
        },
      },
    ],
  },
};

自定义输出文件名方式, generator.filenameoutput.assetModuleFilename 相同,但仅适用于 type:assettype:asset/resource 模块类型。

6、babel 转译 js

babel-loader 使用 Babel (一个强大的js编译器)会加载 ES2015+ 代码并将其转换为 ES5。

bash 复制代码
npm install -D babel-loader @babel/core @babel/preset-env

npm install core-js@3
  • core-js:它是JavaScript标准库的 polyfill(垫片/补丁)包集合。
  • babel-loader:在Webpack里使用babel转译js文件,
  • @babel/core:是babel的大脑,负责调度babel的各个功能模块,
  • @babel/preset-env:babel预设,一组babel插件的集合,就不需要一个个去设置babel的插件,但是preset-env只能进行语法转换,不能弥补浏览器缺失的一些新功能,比如一些内置对象和方法,所以需要corejs来弥补老版浏览器缺失的新功能, 参数选项
    • targets :不配置,会查询package.json中的browserslist,或者项目根目录下的.browserslistrc,都不设,@babel/preset-env默认会使用 browserslist config sources,实际上是 "targets": "> 0.25%, not dead";
    • useBuiltIns'usage',只对使用到的功能打补丁,core-js自动被引入按需打包;'entry',在index.ts入口import 'core-js',把所有的polyfill打包;
    • corejs :此选项仅在与 useBuiltIns: usageuseBuiltIns: entry 一起使用时有效,一般使用版本 3,会 polyfill 实例方法,而 corejs2 不会
js 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
     {
        test: /\.m?js$/,
        // 排除node_modules,不用转译
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              [
                "@babel/preset-env",
                {
                  // 设置兼容目标浏览器
                  targets: {
                    chrome: "58",
                    ie: "11",
                  },
                  // 自动引入core-js根据目标浏览器按需使用打补丁
                  useBuiltIns: "usage",
                  //  一般是指定 3,这个会 polyfill 实例方法,而 corejs2 不会
                  corejs: 3,
                },
              ]           
            ],
          },
        },
      },
    ],
  },
};

@babel/preset-env 转换用到的一些辅助代码是直接注入到模块里的,没有做抽离,多个模块可能会重复注入,并且用到的 polyfill 代码 core-js以及它提供的 PromiseSetMap 等内置函数也是全局导入 ,会污染全局作用域,如果代码是一个打算发布以供其他人使用的库,那么它就会成为一个问题。

解决这个问题就要使用 @babel/plugin-transform-runtime 插件,可以重用 Babel 的注入帮助代码以节省代码大小,依赖模块@babel/runtime 以避免编译输出中的重复。同时也通过设置选项corejs进行polyfill,依赖@babel/runtime-corejs3,corejs: 2 仅支持全局变量(例如 Promise)和静态属性(例如 Array.from),而 corejs: 3 还支持实例属性(例如 [].includes)

bash 复制代码
pnpm add -D @babel/plugin-transform-runtime
pnpm add @babel/runtime
pnpm add @babel/runtime-corejs3

启用@babel/plugin-transform-runtime插件后,@babel/preset-env 中的 useBuiltIns 选项不得设置。 否则,此插件可能无法完全沙盒化环境。

js 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
     {
        test: /\.m?js$/,
        // 排除node_modules,不用转译
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              [
                "@babel/preset-env",
                {
                  // 设置兼容目标浏览器
                  targets: {
                    chrome: "58",
                    ie: "11",
                  },
                  // 自动引入core-js根据目标浏览器按需使用打补丁
                  //  useBuiltIns: "usage",
                  //  一般是指定 3,这个会 polyfill 实例方法,而 corejs2 不会
                  //  corejs: 3,
                },
              ]           
            ],
            plugins: [
              [
                "@babel/plugin-transform-runtime",
                {
                 // 从@babel/runtime-corejs3 引入polyfill
                  corejs: 3,
                },
              ],
            ],
          },
        },
      },
    ],
  },
};

注意: babel插件在预设前运行,@babel/plugin-transform-runtime 在 @babel/preset-env 之前调用的,将不兼容的语法和api转换了,但是他没有target配置,不能按需polyfill,所以最后打包文件会变大。

和postcss一样也可以将babel的配置提取到一个单独的文件 babel.config.js 中,在根目录下建一个babel.config.js,webpack调用babel-loader的时候,babel会读取到babel.config.js里面的配置

js 复制代码
// babel.config.js
module.exports = {
  presets: [
    // 预设从后往前先处理ts,再处理js
    [
      "@babel/preset-env",
      {
        // 设置兼容目标浏览器
        targets: {
          chrome: "58",
          ie: "11",
        },
        // 自动引入core-js根据目标浏览器按需使用打补丁
        // useBuiltIns: "usage",
        //  一般是指定 3,这个会 polyfill 实例方法,而 corejs2 不会
        // corejs: 3,
      },
    ],
    [
      "@babel/preset-typescript",
      {
        //表示每个文件都应该被解析为 TS、TSX
        allExtensions: true,
      },
    ],
  ],
  plugins: [
    [
      "@babel/plugin-transform-runtime",
      {
        // 从@babel/runtime-corejs3 引入polyfill
        corejs: 3,
      },
    ],
  ],
};

// webpack.config.js
module.exports = {
  module: {
    rules: [
     {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: "babel-loader",    
      },
    ],
  },
};

babel的配置文件在根目录下还可以写成其他形式:

  • babel.config.json
  • .babelrc
  • .babelrc.js
  • .babelrc.json
  • 直接在package.json中写配置

7、转译 ts 文件

方式一:

如果已经使用babel-loader转译代码,可以使用 @babel/preset-typescript 以让 Babel 处理 JavaScript 和 TypeScript 文件,而不需要额外使用 loader。与 ts-loader 相反,底层的 @babel/plugin-transform-typescript 插件不执行任何类型检查

bash 复制代码
npm i -D @babel/preset-typescript
js 复制代码
// babel.config.js
module.exports = {
 // 预设是从后往前执行的,ts先编译成js,再编译js
  presets: ["@babel/preset-env",  "@babel/preset-typescript"],
};

// webpack.config.js
module.exports = {
  module: {
    rules: [
     {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: ["babel-loader"],    
      },
    ],
  },
};

方式二:

使用ts-loader,在根目录创建 tsconfig.json

bash 复制代码
npm install -D typescript ts-loader
json 复制代码
//  tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "allowJs": true,
    "moduleResolution": "node"
  }
}

ts-loader 使用 TypeScript 编译器 tsc,并依赖于 tsconfig.json 配置,不要将 module设置为 "CommonJS",否则 webpack 将无法对代码进行 tree shaking

js 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
     {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: ["ts-loader"],    
      },
    ],
  },
};

区别:

  • ts-loader编译是会进行语法检查的,只能在入口文件全量引入polyfill
  • babel-loader只会负责编译,并不会进行语法检查,可以按需引入polyfill

8、省略扩展名extensions

配置 resolve.extensions,默认数组'.js', '.json', '.wasm'能够使用户在引入模块时不带扩展,把用的最多的文件类型放前面,可以加快webpack解析速度。

webpack尝试按顺序解析这些后缀名。如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件,并跳过其余的后缀。使用 resolve.extensions覆盖默认数组 ,这就意味着 webpack 将不再尝试使用默认扩展来解析模块。然而你可以使用 '...' 访问默认拓展名。

js 复制代码
// webpack.config.js
module.exports  = {
  resolve: {
    extensions: [".vue", ".ts", ".scss", "..."],
  },
};

9、配置路径别名

resolve.alias配置import或者require引入路径的别名,假如在项目中,引入一个模块:

js 复制代码
import moduleName from "../../../../util/axios-util.js"

使用相对路径的方式很繁琐,那就可以如下配置:

js 复制代码
const path = require('path')
const resolve = dir => path.resolve(__dirname, dir)
module.exports = {
    resolve: {
      alias: {
        "@": resolve("./src"),
        "@/util": resolve("./src/moduleA/moduleB/moduleC/util")
      }
   },
 }

然后就可以这样引入了:

js 复制代码
import moduleName from "@/util/axios-util.js"

10、模块解析

webpack的 模块解析 就像node中require查找路径一样。一个模块可以作为另一个模块的依赖模块,然后被后者引用,所依赖的模块可以是来自应用程序的代码或第三方库,模块解析就是要从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。

webpack 能解析三种文件路径:

1、绝对路径: 不需要解析了,一步到位。

2、相对路径: 使用 importrequire 的资源文件所处的目录,被认为是上下文目录。在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径。

3、模块路径:

js 复制代码
import 'module';
import 'module/lib/file';
  • (1)、按照在 resolve.modules(数组) 中指定的所有目录中检索模块,数组指定的目录优先级从左到右,权重依次降低 ,默认是从node_modules中搜索模块,如果想要添加一个优先于node_modules搜索:

    js 复制代码
     module.exports = {
      //...
      resolve: {
        modules: [path.resolve(__dirname, 'src'), 'node_modules'],
     },
    }

    如果搜索到的模块里面有package.json文件,在 resolve.exportsFields(数组)配置选项中指定的字段会被依次去package.json里面查找对应字段,默认是exports字段。如果查找exports字段,那就根据 package.json中exports的导出规则 查找模块。

    根据以上这一番操作,webpack解析出来的路径要么是一个确定文件,要么是文件夹

  • (2)、路径是文件

    • 有扩展名,直接找到打包
    • 没有扩展名,那就按resolve.extensions来分析
  • (3)、路径是文件夹

    • ① 如果文件夹中包含 package.json 文件,则会根据 resolve.mainFields 配置中的字段顺序查找,如果目标环境 targetwebworker, web 或者没有指定,那resolve.mainFields默认值是['browser', 'module', 'main'],如果target是其他,默认值是['module', 'main'],然后在 package.json 中根据resolve.mainFields字段顺序确定文件路径。

    • ② 如果不存在 package.json 文件或根据 resolve.mainFields没有找到有效路径,则会根据 resolve.mainFiles 配置选项中指定的文件名顺序查找,默认为['index'],看是否能匹配到一个存在的文件名。

    • ③ 然后就根据resolve.extensions来分析

注意:

可以通过配置别名resolve.alias来替换初始模块路径,resolve.alias 优先级高于以上三种模块解析方式。

二、 搭建一个 vue3 + ts 工程

完整基本配置github地址:github.com/Xluo766/ln-...

技术栈: vue3 + typescript + scss + webpack5 + pnpm

bash 复制代码
mkdir ln-webpack5-vue3-ts
cd ln-webpack5-vue3-ts
pnpm init 
pnpm add webpack webpack-cli -D

1、使用ts编写webpack配置文件

使用Typescript来编写 webpack.config.ts的配置,需要先安装必要的依赖,比如 Typescript 以及其相应的类型声明

js 复制代码
pnpm add -D typescript ts-node @types/node @types/webpack 

同时创建 tsconfig.json 文件,在 compilerOptions 配置中,设置:

  • "module": "commonjs",因为 ts-node 不支持 commonjs 以外的其他模块规范
  • "target": "es5",
  • "esModuleInterop": true,用于兼容处理导入模块时的不同规范
  • "allowSyntheticDefaultImports": true,在静态类型检查时,把 import 没有 exports.default 的报错忽略掉

比如:由于 lodash 没有默认导出,因此现在需要修改 lodashts 文件中的引入

ts 复制代码
 // import _ from 'lodash';
 import * as _ from 'lodash';
  function component() {
    const element = document.createElement('div');
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    return element;
  }
  document.body.appendChild(component());

 // 如果想在 TypeScript 中继续使用像 import _ from 'lodash'; 的语法,
 // 让它被作为一种默认的导入方式,需要在 tsconfig.json 中设置
 // "allowSyntheticDefaultImports" : true 和 "esModuleInterop" : true
json 复制代码
// tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es5",
    "allowJs": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}
ts 复制代码
// webpack.config.ts

import * as webpack from "webpack";
// 配置config的时候有类型提示
const config: webpack.Configuration = {
  entry: "./src/index.ts",
};

export default config;

2、转译 .ts 文件

bash 复制代码
pnpm add ts-loader -D
ts 复制代码
// webpack.config.ts
import * as webpack from "webpack";
const config: webpack.Configuration = {
  entry: "./src/index.ts",
  module: {
    rules: [
      {
        test: /\.ts$/,
        //排除 node_modules 目录
        exclude: /node_modules/,
        use: ["ts-loader"],
      },
    ],
  },
};
export default config;

ts-loader 使用 TypeScript 编译器 tsc,并依赖于 tsconfig.json 配置,如果将 module设置为 "CommonJS",webpack 将无法对代码进行 tree shaking,但是 ts-node又不支持 commonjs 以外的其他模块规范,webpack5官网给出了解决办法

如果要为 tsc 保持 "module": "ESNext"配置,那就在tsconfig.json中添加 ts-node 设置

json 复制代码
// tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "ESNext",
    "target": "es5",
    "allowJs": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS"
    }
  }
}

示例:

ts 复制代码
// src/main.ts
export function log() {
  console.log("tree-shaking");
}
export function sum(a: number, b = 6) {
  return a + b;
}

// src/index.ts
import { sum } from "./main.ts"; //下面会通过配置省略扩展名.ts
const count = sum(5, 5);
console.log(count);

//打包后,删除了log函数
// dist/main.js
(()=>{"use strict";var o,s=(void 0===(o=5)&&(o=6),5+o);console.log(s)})();

3、转译 .vue 文件

首先安装 vue-loader

bash 复制代码
pnpm add vue-loader -D

然后给ts-loader加上配置参数 appendTsSuffixTo: [/\.vue$/],从vue文件分离出来的ts内容会加上ts后缀给ts-loader处理;同时加入一个vue-loader的插件VueLoaderPlugin,它的职责是将定义过的其它规则复制并应用到 .vue 文件里相应语言的块。例如,如果你有一条匹配 /.ts$/ 的规则,那么它会应用到 .vue 文件里的 <script lang='ts'>

在webpack中,loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。想要使用一个插件,你只需要 require()或者import 它,然后使用 new 操作符来创建一个插件实例,把它添加到 plugins 数组中

ts 复制代码
// webpack.config.ts
import * as webpack from "webpack";
import { VueLoaderPlugin } from "vue-loader";

const config: webpack.Configuration = {
  //...
  plugins: [new VueLoaderPlugin()],
  module: {
    rules: [
      {
        test: /\.ts$/,
        //排除 node_modules 目录
        exclude: /node_modules/,
        use: {
          loader: "ts-loader",
          options: {
            // 从vue文件分离出来的ts内容会加上ts后缀给ts-loader处理
            appendTsSuffixTo: [/\.vue$/],
          },
        },
      },
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
    ],
  },
};
export default config;

由于.vue不是一个常规文件,为了引入vue文件不报错,需要告诉ts编译器.vue是个什么类型,在src下建一个shims-vue.d.ts文件

ts 复制代码
// src/shims-vue.d.ts
declare module "*.vue" {
  import type { DefineComponent } from "vue";
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

测试一下打包:

ts 复制代码
// src/index.ts
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.mount("#app");
html 复制代码
// src/App.vue
<script setup lang="ts">
import { ref } from "vue";
const count = ref(1);
</script>
<template>
  <h1 @click="count++">webpack5 + vue3 +ts</h1>
  <h3>数字:{{ count }}</h3>
</template>
html 复制代码
<!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="app"></div>
    <script defer src="./dist/main.js"></script>
  </body>
</html>

4、用babel转译.vue和.ts文件

bash 复制代码
pnpm add -D babel-loader @babel/core @babel/preset-env @babel/preset-typescript

npm install core-js@3
ts 复制代码
// webpack.config.ts

import * as webpack from "webpack";
import { VueLoaderPlugin } from "vue-loader";

const config: webpack.Configuration = {
  // ...
  plugins: [new VueLoaderPlugin()],
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            // 预设从后往前先处理ts,再处理js
            presets: [
              [
                "@babel/preset-env",
                {
                  targets: { chrome: "58", ie: "11" },
                  useBuiltIns: "usage",
                  corejs: 3
                },
              ],
              [
                "@babel/preset-typescript",
                {
                  //表示每个文件都应该被解析为 TS、TSX
                  allExtensions: true,
                },
              ],
            ],
          },
        },
      },
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
    ],
  },
};
export default config;

5、自动生成html入口

目前测试打包后的代码是在根目录下建了一个index.html,把打包后的默认的/dist/main.js手动引入,用live server打开index.html看页面效果,如果更改入口起点的名称,或者添加一个新的入口起点,那么会在构建时重新命名生成的 bundle,而 index.html 仍然在引用旧的名称,每次都需要手动改。

HtmlWebpackPlugin插件可以解决这个问题,该插件将自动生成一个 HTML5 文件, 在 body 中使用 script 标签引入所有 webpack 生成的 bundle。

bash 复制代码
pnpm add -D html-webpack-plugin

在根目录下建一个 public/index.html作为模板,模板中htmlWebpackPlugin.options.title会在webpack编译时被替换

html 复制代码
<!-- /public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" href="./favicon.ico" />
    <!-- 文档标题会被自动替换 -->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

<%= EJS %>是EJS语法,利用普通的 JavaScript 代码生成 HTML ,html中的js代码编译时会被替换。

将图标favicon.ico放到public文件夹下,开发环境使用webpack-dev-server,devServer.static 配置默认将public作为静态资源目录,打包后默认放在根目录下,跟index.html同级,启动服务后,可以通过http://{host}/favicon.ico进行访问。

如果发现图标不显示,而且浏览器根本没请求这个图标,这是缓存问题,stackoverflow有答案

ts 复制代码
// webpack.config.ts
import * as webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";

const config: webpack.Configuration = {
  plugins: [
    //...
    new HtmlWebpackPlugin({
      // 会替换掉 html 中 htmlWebpackPlugin.options.title
      title: "webpack5+Vue3+ts",
      // 以public里面的indexx.html为模板,生成html
      template: "./public/index.html",
    }),
  ]
  // ...
};
export default config;

6、处理css、scss,单独提取

包括vue文件style里面写的css、scss和单独的css、scss文件,同时将处理后的css单独打包,而不再像上面基础配置那样用style标签插入html。

需要用到一个插件MiniCssExtractPlugin,会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,当然也是和css-loader,sass-loader这些一起使用。同时也需要HtmlWebpackPlugin,如果在 webpack 的输出中有任何 CSS 资源(例如,使用MiniCssExtractPlugin 提取的 CSS),HtmlWebpackPlugin会把这些css资源在 HTML 文件 <head> 元素中的 <link> 标签内引入

bash 复制代码
pnpm add -D mini-css-extract-plugin css-loader sass-loader sass postcss-loader postcss postcss-preset-env
ts 复制代码
// webpack.config.ts
import * as webpack from "webpack";
import MiniCssExtractPlugin from "mini-css-extract-plugin";

const config: webpack.Configuration = {
  //...
  plugins: [
   //...
    new MiniCssExtractPlugin({
      // 只影响最初加载的输出css文件,name:入口名称
      filename: "style/[name]_[contenthash:8].css",
      // 按需加载的 chunk 文件命名,id:内部chunk id
      chunkFilename: "style/[id].css",
    }),
  ],
  module: {
    rules: [
     //...
      {
        test: /\.s?css$/,
        // 从下往上,解析scss文件,处理兼容性,处理css文件,提出文件
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: ["postcss-preset-env"],
              },
            },
          },
          "sass-loader",
        ],
      },
    ],
  },
};
export default config;

示例:

ts 复制代码
// 入口index.ts
import "./style/index.scss";
less 复制代码
// src/style/index.scss
$baseColor1: rgb(128, 255, 0);
$baseColor2: #fe3333;

@mixin baseStyle {
  background-color: aqua;
  border: 1px solid rgb(238, 245, 255);
}

body {
  background-color: #d8c7c7;
}

// /src/App.vue 
<style lang="scss" scoped>
@import "./style/index.scss";

h3 {
  color: $baseColor1;
  @include baseStyle();
}

h1 {
  background-color: $baseColor2;
  user-select: none;
}
</style>

打包后:

7、打包图片资源

ts 复制代码
// webpack.config.ts
import * as webpack from "webpack";

const config: webpack.Configuration = {
 //...
  module: {
    rules: [
      //...
      {
        test: /\.(png|svg|jpe?g|gif)$/,
        type: "asset",
        parser: {
          // 图片小于20kb,会被解析为一个 Base64 编码的字符串注入到包中,
          dataUrlCondition: {
            maxSize: 20 * 1024,
          },
        },
        generator: {
          // 此选项被称为文件名,但还是可以使用像 'js/[name]/bundle.js' 这样的文件夹结构
          filename: "image/[name]_[hash:10][ext]",
        },
      },
    ],
  },
};
export default config;

此时import引入图片,会提示 找不到模块"./assets/242kb.jpg"或其相应的类型声明。ts(2307),在项目根目录下建一个类型声明文件d.ts类型声明文件里面只有类型代码,没有具体的代码实现

ts 复制代码
// env.d.ts
declare module "*.jpg" {
  const src: string;
  export default src;
}
declare module "*.jpeg" {
  const src: string;
  export default src;
}
declare module "*.png" {
  const src: string;
  export default src;
}
declare module "*.gif" {
  const src: string;
  export default src;
}
declare module "*.svg" {
  const src: string;
  export default src;
}

8、设置路径别名、路径提示

resolve.alias,创建 importrequire 的别名,来确保模块引入变得更简单。

首先是配置webpack.config.ts让webpack在构建时能正确解析路径:

ts 复制代码
// webpack.config.ts

import * as webpack from "webpack";
import path from "path";

const resolve = (dir: string) => path.resolve(__dirname, dir);

const config: webpack.Configuration = {
  resolve: {
    alias: {
      "@": resolve("./src"),
      "@/assets": resolve("./src/assets"),
      "@/style": resolve("./src/style"),
    },
  }
}
export default config

然后是在tsconfig.json中配置让我们在项目中书写时有路径提示:

json 复制代码
// tsconfig.json

  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": [
        "src/*"
      ],
      "@/assets/*": [
        "src/assets/*"
      ],
      "@/style/*": [
        "src/style/*"
      ]
    }
  },

9、搭建开发环境

在此之前,看页面效果都是把代码打包后,手动将output目录下的入口文件引入html然后用live server开启一个本地服务,每次修改代码就要重新打包,然后刷新浏览器,很麻烦。所以需要用webpack搭建 开发环境:开启一个本地服务器,每次修改代码,webpack自动重新编译打包,同时页面自动刷新。

webpack-dev-server 插件可以在本地搭建一个实时重新加载的web服务器,它不会写入任何输出文件,而是将打包后的文件保留在内存中,然后将它们作为可访问资源部署在 server 中,就像是挂载在服务器根路径上的真实文件一样。

webpack-dev-server 会将在 output.path 中定义的目录中的 bundle 文件作为可访问资源部署在服务中,output.path没有配置,默认就是打包后的dist目录

bash 复制代码
pnpm add -D webpack-dev-server

package.json:

bash 复制代码
  "scripts": {
    "dev": "webpack serve --open"
  },

既然是开发环境,那就提供 模式(mode) 配置选项为development,告知 webpack 使用开发模式的内置优化,process.env.NODE_ENV 的值被自动设置为 'development'

遇到的问题:

1、 vue警告

vue3的github仓库中有解决办法,stackoverflow有讨论,需要通过DefinePlugin注入两个全局变量

js 复制代码
// webpack.config.ts

new webpack.DefinePlugin({
  __VUE_OPTIONS_API__: true,
  __VUE_PROD_DEVTOOLS__: false,     
}),

DefinePlugin 是webpack内置插件,定义全局变量,在 编译时 将代码中的变量替换为其他值或表达式。这在需要根据开发模式与生产模式进行不同的操作时,非常有用。

如果使用ts开发,在代码中使用DefinePlugin里面定义的变量,在d.ts文件里面定义一下全局变量,不然ts报未定义错误

注意看文档,如果值是字符串,会被当成代码执行;如果不是字符串,则将被转换成字符串(包括函数方法),再执行

比如:

  • 要传递字符串'webpack5',就要写成 "'webpack5'",或者JSON.stringify('webpack5')
  • 要传递布尔值,写成true或者JSON.stringify(true)'true'都一样,都会转成字符串运行,true运行还是true
  • 如果传递数字5,就写5,或者JSON.stringify(5)'5'但是'1+1'运行后就是2了
js 复制代码
new webpack.DefinePlugin({
  WEBPACK_STRING1: "webapck5", //使用会报错,会运行webpack5,但是没有定义
  WEBPACK_STRING2: "'webapck5'",
  WEBPACK_BOOLEAN1: "true",
  WEBPACK_BOOLEAN2: true,
  WEBPACK_NUMBER1: 5,
  WEBPACK_NUMBER2: "5",
  WEBPACK_NUMBER3: "1+1",
}),
    
// index.ts
console.log(
  WEBPACK_STRING2,
  WEBPACK_BOOLEAN1,
  WEBPACK_BOOLEAN2,
  WEBPACK_NUMBER1,
  WEBPACK_NUMBER2,
  WEBPACK_NUMBER3
);
// webapck5 true true 5 5 2

2、 使用 MiniCssExtractPlugin 插件在HRM时报错,github有好几条相关 issues

因为在上面第6步处理css/scss的时候,是使用MiniCssExtractPlugin单独提取的文件,并且在配置filename的时候使用了hash,在热更新的时候,重新的生成css文件被浏览器重新请求,issue上面回答说热更新不支持filename配置hash、fullhash、contenthash、chunkhash、modulehash,并且,官网上说hash、fullhash、modulehash都已经弃用了。

解决办法两个:

  • 1、issues的回答是开发环境就不要配置filename不要使用hash,因为关于css的打包和热更新相关代码比较复杂,会有破坏性更改,他们将会在下一个主要版本修复;
  • 2、开发阶段可以使用style-loder,不用MiniCssExtractPlugin打包成单独文件,也就不会有这个问题。

3、 webpack-dev-server的类型需要单独引入,github有 issues,否则报错 Object literal may only specify known properties, and 'devServer' does not exist in type 'Configuration'. ,webpack核心成员说了,将来可能把webpack-dev-server放到插件里面,所以webpack的配置类型里面没有放入devServer的类型。

ts 复制代码
// webpack.config.ts

import type { Configuration as WebpackConfiguration } from "webpack";
import type { Configuration as DevServerConfiguration } from "webpack-dev-server";

type Configuration = WebpackConfiguration & { devServer: DevServerConfiguration };

const config: Configuration = {
  //...
}
export default coonfig

devServer:

  • port:端口,默认8080,
  • hot:热更新,默认已开启,从 webpack-dev-server v4.0.0 开始,模块热替换 就已经是默认开启的
  • host:主机,默认值'127.0.0.1',如果想让本地服务器可以被外部访问,可以设置 host: '0.0.0.0',查看localhost、'127.0.0.1'、'0.0.0.0'的区别
  • proxy:配置代理,可以解决浏览器跨域问题
  • historyApiFallback:默认false,解决使用路由使用html5的history API刷新页面404问题
  • open:设置true服务启动后就打开浏览器,或者通过命令webpack serve --open
  • static:配置从目录提供静态文件的选项(默认是 'public' 文件夹),

devtool:

选择eval-source-map,它会生成用于开发环境的最佳品质的 source map。每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。

ts 复制代码
// webpack.config.ts

import * as webpack from "webpack";
import MiniCssExtractPlugin from "mini-css-extract-plugin";

import type { Configuration as WebpackConfiguration } from "webpack"; 
import type { Configuration as DevServerConfiguration } from "webpack-dev-server";

type Configuration = WebpackConfiguration & { devServer?: DevServerConfiguration };

const config: Configuration = {
  // 开发模式
  mode: "development",
  devtool: "eval-source-map",
  plugins: [
    new MiniCssExtractPlugin({
      // 最初加载的输出css文件名,使用id
      filename: "style/[name]_[id].css",
    }),
    new webpack.DefinePlugin({
      __VUE_OPTIONS_API__: true,
      __VUE_PROD_DEVTOOLS__: false,
    }),
  ],
  devServer: {
    // port: 999, // 服务端口号,默认8080
    // hot: true, // 默认已开启
    // host: "0.0.0.0", // 允许外部访问
    historyApiFallback: true, // 解决history路由404问题
  },
};

export default config;

10、配置文件区分环境

development(开发环境)production(生产环境) 这两个环境下的构建目标存在着巨大差异。

开发环境目标: 强大的 source map 和一个有着 live reloading(实时重新加载) 或 hot module replacement(模块热替换) 能力的 localhost server。

生产环境目标: 关注点在于压缩 bundle、更轻量的 source map、资源优化等,通过这些优化方式改善加载时间

使用 webpack-merge做合并工作,有基本合并使用和更灵活的合并配置

bash 复制代码
pnpm add -D webpack-merge

在生产环境中设置devtool:

  • (none)(省略 devtool 选项) - 不生成 source map。这是一个不错的选择。
  • source-map - 整个 source map 作为一个单独的文件生成。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它。

生产环境复制静态资源:CopyWebpackPlugin

bash 复制代码
pnpm add -D copy-webpack-plugin

放在public文件中的favicon.icon,需要被放到最后的打包产物中,根目录下public文件夹在开发环境没有设置devServer.static的情况下,默认作为静态资源目录,但是在生产环境public不会自动打进包里面,所以需要用插件CopyWebpackPlugin;

ts 复制代码
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          // form:复制的文件或者目录
          from: "./public",
          // globOptions.ignore:不复制html,已经使用了插件HtmlWebpackPlugin,否则报重复错误
          globOptions: { ignore: ["**/index.html"] },
          // 默认就是配置的output输出目录,output.path没有配置,默认就是dist
          // to: "",
        },
      ],
    }),
  ],

配置文件使用 导出函数的方式,会接收两个参数,第一个参数是环境(environment),第二个参数 是传递给 webpack 的配置项,修改一下脚本:

json 复制代码
 // package.json
  "scripts": {
    "build": "webpack --env production",
    "dev": "webpack serve --env development --open"
  },

第一个参数env中将包含{production: true}或者{development: true}

ts 复制代码
// webpack.config.ts
import path from "path";
import { DefinePlugin } from "webpack";
import { merge } from "webpack-merge";
import { VueLoaderPlugin } from "vue-loader";
import CopyPlugin from "copy-webpack-plugin";
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import type { Configuration as WebpackConfiguration } from "webpack";
import type { Configuration as DevServerConfiguration } from "webpack-dev-server";

type Configuration = WebpackConfiguration & {
  devServe?: DevServerConfiguration;
};

type WebpackConfigEnv = {
  [key: string]: boolean | number | string;
  production: boolean;
  development: boolean;
};

const resolve = (dir: string) => path.resolve(__dirname, dir);

// 基本配置
const baseConfig = (env: WebpackConfigEnv): Configuration => {
  return {
    entry: "./src/index.ts",
    output: {
      clean: true,
      environment: {
        //打包输出普通函数IIFE
        arrowFunction: false,
      },
    },
    resolve: {
      //可以加快webpack解析速度
      extensions: [".vue", ".ts", ".scss", "..."],
      alias: {
        "@": resolve("./src"),
        "@/assets": resolve("./src/assets"),
        "@/style": resolve("./src/style"),
      },
    },
    plugins: [
      // 解析.vue文件必需插件
      new VueLoaderPlugin(),
      new HtmlWebpackPlugin({
        title: "webpack5+Vue3+ts",
        template: "./public/index.html",
      }),
      new DefinePlugin({
        __VUE_OPTIONS_API__: true,
        __VUE_PROD_DEVTOOLS__: false,
      }),
    ],
    module: {
      rules: [
       // ts-loader编译会进行语法检查,只能在入口文件全量引入polyfill
        {
          test: /\.ts$/,
          //排除 node_modules 目录
          exclude: /node_modules/,
          use: {
            loader: "ts-loader",
            options: {
              // 从vue文件分离出来的ts内容会加上ts后缀给ts-loader处理
              appendTsSuffixTo: [/\.vue$/],
            },
          },
        },
        // babel-loader编译不会进行语法检查,可以按需引入polyfill
        // {
        //   test: /\.ts$/,
        //   exclude: /node_modules/,
        //   use: "babel-loader",
        // },
        {
          test: /\.vue$/,
          loader: "vue-loader",
        },
        {
          test: /\.s?css$/,
          use: [
            // 生产环境单独打包,开发环境用style-loader
            env.production ? MiniCssExtractPlugin.loader : "style-loader",
            "css-loader",
            "postcss-loader",
            "sass-loader",
          ],
        },
        {
          test: /\.(png|svg|jpe?g|gif)$/,
          type: "asset",
          parser: {
            // 图片小于20kb,会被解析为一个 Base64 编码的字符串注入到包中,
            dataUrlCondition: {
              maxSize: 20 * 1024,
            },
          },
          generator: {
            // 此选项被称为文件名,但还是可以使用像 'js/[name]/bundle.js' 这样的文件夹结构
            filename: "image/[name]_[contenthash:10][ext]",
          },
        },
      ],
    },
  };
};

// 生产环境
const prodConfig: Configuration = {
  mode: "production",
  plugins: [
    new MiniCssExtractPlugin({
      // 最初加载的输出css文件名,开发环境不要用hash/fullhash/contenthash/chunkhash/modulehash
      // filename: "style/[name]_[contenthash:8].css",
      filename: "style/[name]_[id].css",
      // 按需加载的 chunk 文件名
      chunkFilename: "style/[id].css",
    }),
    new CopyPlugin({
      patterns: [
        {
          // form:复制的文件或者目录
          from: "./public",
          // globOptions.ignore:不复制html,已经使用了插件HtmlWebpackPlugin,否则报重复错误
          globOptions: { ignore: ["**/index.html"] },
          // 默认就是配置的output输出目录,output.path没有配置,默认就是dist
          // to: "",
        },
      ],
    }),
  ],
};

// 开发环境
const devConfig: Configuration = {
  mode: "development",
  devtool: "eval-source-map",
  devServer: {
    // port: 999, // 服务端口号,默认8080
    // hot: true, // 默认已开启
    // host: "0.0.0.0", // 允许外部访问
    historyApiFallback: true, // 解决history路由404问题
  },
};

export default (env: WebpackConfigEnv) => {
  return merge<Configuration>(
    baseConfig(env),
    env.production ? prodConfig : devConfig
  );
};

11、集成 eslint、prettier

eslint保证项目代码符合规范,prettier保证项目的代码统一格式。

先安装

bash 复制代码
pnpm add -D eslint

初始化eslint,使用npx找到node_modules下安装的eslint

js 复制代码
npx eslint --init

命令行会出现选择提示:

js 复制代码
? How would you like to use ESLint? ...
  To check syntax only
> To check syntax and find problems //默认选这个,只做代码校验,代码格式交给prettier去做
  To check syntax, find problems, and enforce code style
  
? What type of modules does your project use? ...                                                                                   
> JavaScript modules (import/export) // 选择默认es6                                                                                                     
  CommonJS (require/exports)
  None of these
  
? Which framework does your project use? ... 
  React
> Vue.js  // 选择在vue项目里面用
  None of these
  
? Does your project use TypeScript? >> No / Yes //选择结合ts

? Where does your code run? ...  (Press <space> to select, <a> to toggle all, <i> to invert selection)
√ Browser 
√ Node  // 浏览器和node都选上

? What format do you want your config file to be in? ... 
> JavaScript //选择生成 .eslintrc.js配置文件
  YAML  
  JSON

@typescript-eslint/eslint-plugin@latest eslint-plugin-vue@latest @typescript-eslint/parser@latest
? Would you like to install them now? >> No / Yes  //选no,它会用npm安装,我们自己用pnpm安装

手动安装依赖

bash 复制代码
pnpm add -D  @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-vue

对生成的.eslintrc.js文件里面的默认配置做一些修改

js 复制代码
// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:vue/vue3-essential"
  ],
  overrides: [
    {
      env: {
        node: true
      },
      files: [".eslintrc.{js,cjs}"],
      parserOptions: {
        sourceType: "script"
      }
    }
  ],
  parserOptions: {
    ecmaVersion: "latest",
    parser: "@typescript-eslint/parser",
    sourceType: "module"
  },
  plugins: ["@typescript-eslint", "vue"],
  rules: {},
  // 声明全局变量,同时在d.ts文件声明
  globals: {
    __VUE_OPTIONS_API__: "readonly"
  }
};

加上.eslintignore文件忽略校验

ignore 复制代码
node_modules
dist
*.d.ts
*.css
*.scss
*.ipg
*.png
*.jpeg
*.svg
*.gif

通过命令校验代码,修复.vue .js .ts结尾文件

json 复制代码
  "scripts": {
    "lint": "eslint . --ext .vue,.js,.ts --fix",
  },

安装prettier,通过代码格式化需要使用

bash 复制代码
pnpm add -D prettier

创建.prettierrc.js文件做代码格式化

js 复制代码
// .prettierrc.js

module.exports = {
  //使用tab进行缩进,默认为false,表示用空格进行缩减
  useTabs: false,
  // tab是空格的情况下,默认选择2个
  tabWidth: 2,
  // 单行字符数,默认80
  printWidth: 80,
  // 字符串是否使用单引号,默认为false,使用双引号
  singleQuote: false,
  // 行尾是否使用分号,默认为true
  semi: true,
  // 是否使用尾逗号,如对象最后一个属性不用加逗号
  trailingComma: "none"
};

创建.prettierignore文件忽略格式化

ignore 复制代码
node_modules
dist

同时VSCode需要安装prettier的插件,并且配置

  • 设置里面勾选 format on save
  • 设置里面 editor default format 选择 prettier

如果格式化没生效,重启vscode,重新打开项目,同时也可以通过脚本格式化:

json 复制代码
  "scripts": {
    "prettier": "prettier --write .",  
  },

12、集成 .editorconfig

保证在不同的IDE编辑器保持统一的代码风格。

下载vscode插件 :EditorConfig for VS Code, 创建.editorconfig

config 复制代码
root = true

# 表示所有⽂件适⽤
[*] 
# utf-8编码
charset = utf-8
# 缩进⻛格(tab | space),space空格缩进
indent_style = space 
# 缩进2个空格
indent_size = 2
# 控制换⾏类型(lf | cr | crlf)
end_of_line = lf 
# 去除⾏尾的任意空⽩字符
trim_trailing_whitespace = true 
# 始终在⽂件末尾插⼊⼀个新⾏
insert_final_newline = true 
# 表示仅 md ⽂件适⽤以下规则
[*.md] 
max_line_length = off
trim_trailing_whitespace = false

13、配置 Git 提交规范

有篇文章写的非常详细: 【vue3-element-admin】Husky + Lint-staged + Commitlint + Commitizen + cz-git 配置 Git 提交规范

总结

Webpack在前端工程化中提供的能力:

  1. 模块打包和依赖管理:Webpack可以将前端应用程序拆分为多个模块,并通过各种加载器(Loaders)和插件(Plugins)来处理和转换这些模块。它可以解析模块之间的依赖关系,并生成一个或多个打包后的文件,以供浏览器加载和执行。
  2. 资源管理和优化:Webpack不仅可以打包JavaScript模块,还可以处理其他类型的静态资源,如样式表(CSS、Sass、Less)、图片、字体等。通过加载器和插件,它可以对这些资源进行压缩、合并、优化和缓存等处理,以提高应用程序的加载性能和用户体验。
  3. 代码分割和懒加载:Webpack支持代码分割和懒加载,可以将应用程序代码拆分为多个块(chunks),并按需加载这些块。这种方式可以减小初始加载的文件大小,提高页面的加载速度,并实现按需加载,降低了用户首次访问时的等待时间。
  4. 开发环境和生产环境的配置:Webpack提供了强大的配置能力,可以根据开发环境和生产环境的需求来进行不同的配置。它支持开发服务器、热模块替换(Hot Module Replacement)、代码调试等功能,使开发人员能够更高效地进行开发和调试。
  5. 构建流程的自动化:Webpack可以与其他构建工具(如Grunt、Gulp)集成,并通过配置文件定义整个构建流程。它可以自动化处理资源依赖关系、编译、压缩、合并和输出最终的生产代码,简化了前端开发的构建过程,提高了开发效率。

参考文档

相关推荐
To_OC7 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic7 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy7 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志8 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble9 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯10 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜10 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司10 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g11 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森11 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http