(上篇)梳理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)集成,并通过配置文件定义整个构建流程。它可以自动化处理资源依赖关系、编译、压缩、合并和输出最终的生产代码,简化了前端开发的构建过程,提高了开发效率。

参考文档

相关推荐
刺客-Andy5 分钟前
React 第十九节 useLayoutEffect 用途使用技巧注意事项详解
前端·javascript·react.js·typescript·前端框架
谢道韫66610 分钟前
今日总结 2024-12-27
开发语言·前端·javascript
嘤嘤怪呆呆狗20 分钟前
【插件】vscode Todo Tree 简介和使用方法
前端·ide·vue.js·vscode·编辑器
ᥬ 小月亮33 分钟前
Js前端模块化规范及其产品
开发语言·前端·javascript
码小瑞1 小时前
某些iphone手机录音获取流stream延迟问题 以及 录音一次第二次不录音问题
前端·javascript·vue.js
weixin_1891 小时前
‌Vite和Webpack区别 及 优劣势
前端·webpack·vue·vite
半吊子伯爵1 小时前
开发过程优化·自定义鼠标右键菜单
前端·javascript·自定义鼠标右键菜单
xcLeigh1 小时前
HTML5实现好看的喜庆圣诞节网站源码
前端·html·html5
Tirzano1 小时前
vue3 ts 简单动态表单 和表格
前端·javascript·vue.js
杰~JIE1 小时前
前端工程化概述(初版)
前端·自动化·工程化·前端工程化·sop