webpack5+vue3搭建一个基础的h5项目结构

1,456 阅读17分钟

一,概述

本文主要是使用webpack5+vue3+vw适配+axios+vue-router+vuex+vant搭建一个基础的h5项目结构。其中每一节都是独立的配置,想要webpack搭配其他的也可以参考下。至于项目的代码,放在本文最后面,不想看的可以直接下载使用。

为了自己后续搭建h5项目能有个干净的项目框架直接使用,而不是每次都得重新搭建。每一个步骤都是我实践过的,其中存在的一些注意点和原理,我也会简单提及,算是学习实践的一篇笔记吧。

二,初始化项目

第一步:初始化package.json

npm init -y

第二步:安装webpack

npm install webpack webpack-cli webpack-dev-server -D
-D 等价于 --save-dev; 开发环境时所需依赖
-S 等价于 --save; 生产环境时所需依赖

第三步:新建src和config文件夹

新建src文件夹,并写上代码

./src/main.js

console.log("hello webpack");

新建config文件夹,并新建webpack.config.js文件

./config/webpack.config.js

const path = require("path");
module.exports = {
  entry: path.resolve(__dirname, "../src/main.js"), // 入口文件,打包从这个文件开始
  output: {
    path: path.resolve(__dirname, "../dist"),//出口文件,打包生成的文件放置到这个文件夹下
    filename: "./js/[name].[chunkhash].js"		//打包成的文件名。name取的原始文件名,chunkhash生成哈希值,这样每次打包出来不一样,避免浏览器缓存读取旧文件。
  },
  mode: "development"  //开发模式
};

再在根目录下运行命令行:

npx webpack --config ./config/webpack.config.js
//因为我们把webpack配置文件放置到config文件夹内了,所以要用--config来指定配置文件的路径。
//npx 作用:直接调用项目内部安装的模块,而无需再输入模块路径。npx webpack等价于./node_modules/.bin/webpack --config ./config/webpack.config.js

于是就可以看到根目录生成了dist文件,完成了基本的打包配置。

image-20220410113414963

第四步:在package.json的script中添加命令

第三步中,我们的运行命令是手动打的,不方便,于是可以在package.json的script中添加命令:

"scripts": {
    "dev": "webpack --config ./config/webpack.config.js  --progress --color -w"
 },
//--progress: 启用在构建过程中打印编译进度
//--color: 启用控制台颜色
//--watch或-w: 监听文件变化

于是在项目根目录执行npm run dev即可完成第三步的打包工作。

第五步:配置开发服务器

在第二步的时候,我们安装了这个依赖:webpack-dev-server,它的作用是在本地起一个服务器,当我们运行webpack的时候,打包出来的文件会放置在电脑的内存中(不放dist里了),然后在这个服务器上运行。

现在在config/webpack.config.js中进行配置这个开发服务器:

module.exports = {
	 //...其他配置
  devServer: {
    hot: true, //模块的热替换
    open: true, // 编译结束后自动打开浏览器
    port: 8080, // 设置本地端口号
    host: "localhost" //设置本地url
  }
};

然后修改package.json文件:

"dev": "webpack server --config ./config/webpack.config.js  --progress --color",

会报404,因为这时候打开的浏览器,是localhost:8080,它默认打开的是该目录下的index.html文件,目前这样写是没有这个文件的,所以会报404,需要引入一个插件来生成index.html文件。

第六步:创建 html 文件

安装依赖:

npm install html-webpack-plugin -D

然后在根目录新建public文件夹,放置index.html和 favicon.ico文件,htmlWebpackPlugin.options.title是读取html-webpack-plugin插件中配置的值。等运行wenpack打包的时候,就会给它赋值了。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" href="./favicon.ico" type="image/png" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

修改 webpack.config.js 配置HtmlWebpackPlugin:

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

module.exports = {
    // ...其他配置
     plugins: [
        new HtmlWebpackPlugin({
          template: "./public/index.html", //用来做模板的html的文件路径(从项目根目录开始)
          filename: "index.html", //生成的html的名字
          title:'webpack5的项目配置',//这个就对应上文的title
          inject: "body" //打包出来的那个js文件,放置在生成的body标签内
        })
      ],
}

这个HtmlWebpackPlugin的作用,就是以你在public/index.html为初始的模板,配置生成html文件。当我们运行npm run dev的时候。就会生成对应的index.html文件,并且自动引入我们打包出来的js文件,这时候再npm run dev就不会报404了。至此,webpack的初始化工作完成。

image-20220410142259724

三,分环境打包配置

在实际开发中,会有开发环境,测试环境生产环境等。而不同环境我们打出来的包应该是不同的,有些配置是开发环境所特有的,而又有一些配置是生产环境所特有的,为了便维护配置文件,我们可以分环境对webpack进行配置。

第一步,先安装cross-env

它的作用是设置node环境变量, 因为他可以跨终端进行设置.所以项目中基本上都是使用它。

npm install cross-env -D

第二步,修改package.json的script

"dev": "cross-env envMode=dev webpack server --config ./config/webpack.config.js  --progress --color",

主要是加了cross-env envMode=dev这个的作用就是在本地的node全局变量process.env上增加一个名为envMode,值为dev的变量。

怎么测试是否成功添加呢?可以在webpack.congfig.js中打印一下,然后控制台就会打印出来了,为什么是控制台而不是浏览器?那是因为webpack的程序是在node环境中运行,从而打包出我们想要的bundle.js之类的文件。所以说,是基于node才有了前端工程化。

image-20220410175528129

第三步:将需要区分环境的变量导入项目中

目前,我们只是通过cross-env在node的全局变量中添加了需要的环境变量,而项目代码(src文件夹下的代码),并不能读取这些变量。但区分环境配置不同的参数又是实际开发中必不可少的。比如测试环境和生产环境的接口地址不同,开发环境需要打开Vconsole,而生产环境需要关闭等。

这就需要我们能够在项目中读取这些变量。就需要另一个库来实现:

Dotenv 是一个零依赖的模块,它能将环境变量中的变量从 .env 文件加载到 process.env 中。也就是项目代码中能够读取到process.env的值。

npm i dotenv -D

然后再在webpack.config.js中进行配置:

const webpack = require("webpack");
const envMode = process.env.envMode;
require("dotenv").config({ path: `.env` });//这个的作用是执行根目录下的.env文件,把里面的变量放到process.env上
require("dotenv").config({ path: `.env.${envMode}` });//这个的作用是执行根目录下的.env.${envMode}文件,把里面的变量放到process.env上,和.env相同的部分会覆盖它
console.log("-------", envMode);
// 正则匹配以 VUE_APP_ 开头的 变量
const prefixRE = /^VUE_APP_/;
let env = {};
// 只有 NODE_ENV,BASE_URL 和以 VUE_APP_ 开头的变量将通过 webpack.DefinePlugin 静态地嵌入到客户端侧的代码中
for (const key in process.env) {
  if (key == "NODE_ENV" || key == "BASE_URL" || prefixRE.test(key)) {
    env[key] = JSON.stringify(process.env[key]);
  }
}
module.exports = {
 ...//
  plugins: [
    new webpack.DefinePlugin({
      // 定义环境和变量
      "process.env": {
        ...env
      },
       __VUE_OPTIONS_API__: false,//避免控制台警告信息
      __VUE_PROD_DEVTOOLS__: false
    }),
    ...//
  ]
};

注意到,它是需要手动获取到变量的,主要就是这两行代码:

require("dotenv").config({ path: `.env` });//这个的作用是执行根目录下的.env文件,把里面的变量放到process.env上
require("dotenv").config({ path: `.env.${envMode}` });//这个的作用是执行根目录下的.env.${envMode}文件,把里面的变量放到process.env上,和.env相同的部分会覆盖它

这样配置之后,就可以和vue-cli创建的项目一样,在根目录创建环境变量并使用了,可以参考

第四步:根目录创建.env等文件

.env

# 公共环境配置
VUE_APP_TITLE = "webpack5+vue3"

.env.dev

NODE_ENV = 'development'
#开发环境要代理,这个地方要写
VUE_APP_baseUrl = '/api'
VUE_APP_SHOWCONSOLE=true

.env.testing

NODE_ENV = 'testing'
VUE_APP_baseUrl = '../../'
VUE_APP_SHOWCONSOLE = true

.env.prod

NODE_ENV = 'production'
VUE_APP_baseUrl = '../../'
VUE_APP_SHOWCONSOLE = false

这样配置之后,当我们运行npm run dev的时候,就会把.env.env.dev文件中的环境变量放置到项目的全局变量process.env中,我们可以在项目中把它打印出来:

image-20220410183940808

同样的,如果我们在package.json中运行的脚本是envMode=test的话,这里获取到的变量就会是.env.env.testing两个文件的并集了。项目代码中就能够使用这些变量来处理不同环境中的逻辑了。

第五步:区分开发环境和生产环境的webpack配置

第三第四步主要是为了项目代码中能够使用环境变量,当我们执行完第二步,webpack能根据我们的命令行区分开发和生产环境时,便可以执行区分开发和生产的配置了。

从上文可以知道。webpack配置文件最终是导出一个对象给webpack使用,而我们实际开发中,开发环境是有自己特有的配置,生产环境有自己特有的配置,还有一部分公用的配置。

于是就可以把开发的配置抽离出来,生产的配置抽离出来,公用的配置抽离出来。如下图所示:

image-20220410190317640

这就有个问题,某个环境的配置如何和公用配置合并呢?理想的情况是取并集,并且是对象深层的并集。

有一个插件提供了这个功能,现在安装下这个插件:

npm i webpack-merge -D

然后新建三个文件,把我们之前做的配置按照开发环境、生产环境和公用配置进行拆分,并删除之前的配置文件:

webpack.base.conf.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");
const envMode = process.env.envMode;
require("dotenv").config({ path: `.env` });
require("dotenv").config({ path: `.env.${envMode}` });
console.log("-------", envMode);
// 正则匹配以 VUE_APP_ 开头的 变量
const prefixRE = /^VUE_APP_/;
let env = {};
// 只有 NODE_ENV,BASE_URL 和以 VUE_APP_ 开头的变量将通过 webpack.DefinePlugin 静态地嵌入到客户端侧的代码中
for (const key in process.env) {
  if (key == "NODE_ENV" || key == "BASE_URL" || prefixRE.test(key)) {
    env[key] = JSON.stringify(process.env[key]);
  }
}

module.exports = {
  entry: path.resolve(__dirname, "../src/main.js"), // 入口
  plugins: [
    new webpack.DefinePlugin({
      // 定义环境和变量
      "process.env": {
        ...env
      },
       __VUE_OPTIONS_API__: false,//这两个配置是引入vue后,避免控制台警告信息的
      __VUE_PROD_DEVTOOLS__: false
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html", //用来做模板的html的文件路径
      filename: "index.html", //生成的html的名字
      title: "webpack5的项目配置", //这个就对应上文的title
      inject: "body" //打包出来的那个js文件,放置在生成的body标签内
    })
  ]
};

webpack.dev.conf.js

const path = require("path");
const { merge } = require("webpack-merge");
const BaseWebpackConfig = require("./webpack.base.conf");
module.exports = merge(BaseWebpackConfig, {
  mode: "development",
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "./js/[name].[chunkhash].js"
  },
  devServer: {
    hot: true, //模块的热替换
    open: true, // 编译结束后自动打开浏览器
    port: 8080, // 设置本地端口号
    host: "localhost" //设置本地url
  }
});

webpack.prod.conf.js

const path = require("path");
const { merge } = require("webpack-merge");
const BaseWebpackConfig = require("./webpack.base.conf");
module.exports = merge(BaseWebpackConfig, {
  mode: "production",
  output: {
    path: path.resolve(__dirname, "../dist"),
    filename: "./js/[name].[chunkhash].js"
  }
});

然后修改packgae.json中的script为:

  "scripts": {
    "dev": "cross-env envMode=dev webpack serve --config ./config/webpack.dev.conf.js  --color ",
    "testing": "cross-env envMode=testing webpack --config ./config/webpack.prod.conf.js  --color",
    "build": "cross-env envMode=prod webpack --config ./config/webpack.prod.conf.js  --color"
  },

然后运行npm run dev,就可以执行./config/webpack.dev.conf.js中的打包代码配置了,达到的效果和之前一样:

image-20220410193209897

四,配置babel,es6转es5

有些浏览器或者浏览器版本还不支持es6的语法,所以需要babel把es6转化为es5。

第一步:安装babel相关的依赖

npm install  babel-loader @babel/core @babel/preset-env -D

第二步:安装处理async和await的依赖

npm install  @babel/runtime @babel/plugin-transform-runtime -D

第三步:配置webpack

因为这属于生产环境和开发环境都需要用到的配置,所以在webpack.base.js中进行配置。

module.exports = {
	...//
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
            plugins: [["@babel/plugin-transform-runtime"]],
            //开启缓存
            cacheDirectory: true
          }
        },
        exclude: /node_modules/
      }
    ]
  }
}

值得注意的是:若不想将配置写在配置文件中,可在项目根目录创建 babel.config.jsbabelrc.js 文件(优先级更高)。

这样配置之后,遇到js文件,就会使用babel进行编译处理。

五,引入vue框架

第一步,安装识别解析vue文件的依赖

npm i vue-loader @vue/compiler-sfc -D
npm i vue -S

第二步,创建vue文件,并且在入口文件创建vue实例

入口文件main.js

import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.mount("#app");

App.vue文件

<template>
  <div class="app-box">测试页面</div>
</template>
<script setup></script>
<style></style>

第三步,配置webpack可以识别vue文件

同样是通用配置,在webpack.base.js中配置:

const { VueLoaderPlugin } = require("vue-loader/dist/index");
module.exports = {
	...//其他配置
  module: {
    rules: [
     ...//其他配置
      {
        test: /\.vue$/,
        loader: "vue-loader"
      }
    ]
  },
  plugins: [
   ...//其他配置
    new VueLoaderPlugin()
  ]
};

配置完成后可以在浏览器上看到:

image-20220410234408018

六,解析scss文件

第一步,安装依赖

npm install css-loader sass sass-loader mini-css-extract-plugin -D

第二步,配置webpack

还是在通用文件中进行配置:

//1,引入
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
//2,plugins中使用
 plugins: [
    new MiniCssExtractPlugin({
      filename: "assets/styles/[contenthash].css" //配置css打包之后的存放路径(这个配置contenthash在开发环境会导致热更新css报错,开发环境用name)
    })
  ]
 //3,module.rule中配置规则
 	{
        test: /\.(scss|css)$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          "css-loader",
          "sass-loader"
        ]
      }

值得注意的是以前我们都是使用style-loader将css挂在dom上,现在的做法是使用MiniCssExtractPlugin插件将css抽离出来成为一个文件,然后再在html中使用link来引入了。

另外,这个use中的数组是有顺序要求的。先执行sass-loader转化为一般的css文件,再执行css-loader转化为webpack能够理解的模块进行打包,最后使用MiniCssExtractPlugin在html文件中引入。

然后运行项目,可以看到已经能识别scss了。

image-20220411000855383

第三步,使用postcss插件给css添加兼容性前缀

一些css3的代码,一些浏览器不支持。需要添加浏览器前缀。就需要使用到这个插件。

npm i postcss-loader autoprefixer -D

然后修改刚刚设置的css配置,在scss-loader后面增加这个postcss-loader,也就是转化成普通的css后需要增加浏览器前缀。

{
       test: /\.(scss|css)$/,
       use: [
         {
            loader: MiniCssExtractPlugin.loader
         },
          "css-loader",
          "postcss-loader",
          "sass-loader"
       ]
 }

但是增加前缀是需要使用postcss-loader的一个插件。

于是需要再在项目根目录新建一个文件:postcss.config.js文件

module.exports = () => {
  return {
    plugins: {
      autoprefixer: {}
    }
  };
};

这里需要配置浏览器的兼容列表,但是更好的做法是在package.json文件中进行配置:

"browserslist": [
    "> 0.1%",
    "last 2 versions"
  ],

于是运行项目就可以看到增加了前缀了:

image-20220411002220063

七,处理图片这类静态资源

我们在代码中直接写:

<div class="app-box">
    <img src="./assets/img/pic.jpeg" alt="" />
  </div>

是会报错的,因为这时候webpack还不能解析图片等静态资源。

webpack5 之前我们处理静态资源比如。图片字体之类的资源的时候等,需要用到url-loader,file-loader,raw-loader,webpack5则放弃了这三个loader,这三个loader在github上也停止了更新。 webpack5使用四种新增的内置资源模块(Asset Modules)替代了这些loader的功能。

asset/resource 将资源分割为单独的文件,并导出url,就是之前的 file-loader的功能.
asset/inline 将资源导出为dataURL(url(data:))的形式,之前的 url-loader的功能.
asset/source 将资源导出为源码(source code). 之前的 raw-loader 功能.
asset 自动选择导出为单独文件或者 dataURL形式(默认为8KB). 之前有url-loader设置asset size limit 限制实现。

第一步,全局设置默认的文件输出地址

在公共配置里设置assetModule的文件输出路径:

output: {
    assetModuleFilename: "assets/images/[contenthash][ext]" //自定义asset module资源打包后的路径和名字
  },

第二步,在modele.rules中进行配置

因为asset类型可以设置文件大小,所以对于图片的处理我们通常使用它。(大于多少kb的使用单独文件的url,小图片则转为dataurl,即base64的形式。)

{
         test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 
         type: 'asset', // asset 资源类型可以根据指定的图片大小来判断是否需要将图片转化为 base64
          generator: {
            filename: 'assets/images/[hash][ext][query]' // 局部指定输出位置,这里配置的文件输出路径优先级比第一步配置的高
          },
          parser: {
            dataUrlCondition: {
              maxSize: 30 * 1024 // 限制于 30kb
          }
   } 
},

值得注意的是,这里配置的文件输出路径优先级更高。

第三步,分别引入大图片和小图片作为测试

如下图,可以看到大图片是打包成文件了,然后依据路径引入,而小图片,则是直接编译成base64:

image-20220411221246799

八,配置 alias 路径别名

在我们引入文件或者资源的时候,常常使用相对路径的话,要是文件路径特别深的话。就得写好多好长的路径,这样子显然不方便。于是就可以配置路径别名的方式来简化这一过程。

还是在公用配置文件中写:

function resolve(dir) {
  return path.join(__dirname, '..', dir)
}
module.exports = {
  ...//其他配置
  resolve: {
    alias: {
      "@": resolve("src"),
      "@components": resolve("src/components"),
      "@assets": resolve("src/assets"),
      "@img": resolve("src/assets/img"),
      "@utils": resolve("src/utils"),
      "@api": resolve("src/api"),
      "@css": resolve("src/assets/css"),
      "@plugins": resolve("src/plugins")
    }
  }
}

这样配置之后。如上文我们使用图片就可以:

<template>
  <div class="app-box">
    <div class="img1-box">
      <h4>这个是大图片</h4>
      <img src="@img/pic.jpeg" alt="" />
    </div>
    <div class="img2-box">
      <h4>这个是小图片</h4>
      <img src="@img/test.png" alt="" />
    </div>
  </div>
</template>

九,打包时清除上次构建dist目录

webpack5.20以下版本清除dist文件内容一般使用插件 clean-webpack-plugin, 5.20版本以后output新增特性clean,用于清除dist文件。

因为只有打包出来才会生成dist文件,所以我们把这个配置放在生产的配置文件中:

//webpack.prod.conf.js
module.exports = {
  //...
  output: {
    clean: true, // Clean the output directory before emit.
  },
};

十,直接复制指定文件到dist下指定路径

一个完整的项目,不仅有开发依赖的各种文件需要打包上传到服务器上,也许还会有各种开发文档,项目中没引用过的图片、设计图、或者会有一些静态页面不需要编译,也要上传到服务器上,以方便预览。

这里就需要用到插件copy-webpack-plugin

第一步:安装copy-webpack-plugin

npm i copy-webpack-plugin -D

第二步:配置将public中favicon.ico文件复制到dist根目录

因为之前我的html模板中引入favicon.ico是写死了相对路径,npm run build生成的dist下不会有这个文件,所以会报404。为了解决这一问题,可以正好用这个插件把public文件夹下的favicon.ico复制到dist根目录下:

<link rel="icon" href="./favicon.ico" type="image/png" />

因为这是打包dist文件时才需要执行,所以写在生产的配置文件中:

const copyWebpackPlugin = require("copy-webpack-plugin");
function resolve(dir) {
  return path.join(__dirname, "..", dir);
}
module.exports = merge(BaseWebpackConfig, {
	...//其他配置
 plugins: [
    //静态资源输出到根目录
    new copyWebpackPlugin({
      patterns: [
        {
          from: resolve("/public"),
          to: resolve("/dist"), //放到output文件夹下
          globOptions: {
            dot: true,
            gitignore: false,
            ignore: [
              // 配置不用copy的文件
              "**/index.html"
            ]
          }
        }
      ]
    })
  ]
 }

于是运行npm run build,就可以看到dist根目录下会出现favicon.ico文件了。

image-20220411230130519

十一,配置引入字体文件

和第十步一样,使用webpack的asset module内置模块。

 {
        test: /\.(eot|svg|ttf|woff|woff2|)$/,
        type: 'asset/resource',
        generator: {
          // 输出文件位置以及文件名
          filename: 'assets/fonts/[hash:8].[name][ext]'
        }
},

十二,配置压缩

1,压缩css

之前上文第六步,已经安装了抽离css为单独文件,然后link便签引入html的插件。现在来安装压缩css的插件。

第一步:安装插件
npm install css-minimizer-webpack-plugin -D
第二步:配置
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
 ...//其他配置
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ],
  },
};

查看配置之后的对比:

image-20220417111618216

2,压缩 html 文件

在上文的第二节的第六步,已经安装并配置了html-webpack-plugin,这里就不再次安装,而是直接在生产配置文件webpack.prod.conf.js中增加配置,因为之前的文件是写在webpack.base.conf.js中,这里再写,当运行npm run build的时候,就会有冲突(base中配置了,prod中又配置),所以呢,就需要把第二节的第六步中的配置转移到开发环境配置中去:

于是公用配置:

//webpack.base.conf.js
-  const HtmlWebpackPlugin = require("html-webpack-plugin");

   module.exports = {
    plugins: [
	...//其他配置
-        new HtmlWebpackPlugin({
-          template: "./public/index.html", //用来做模板的html的文件路径
-          filename: "index.html", //生成的html的名字
-          title: "webpack5的项目配置", //这个就对应上文的title
-          inject: "body" //打包出来的那个js文件,放置在生成的body标签内
-        }),

      ]
    };

开发环境配置:

//webpack.base.conf.js
+  const HtmlWebpackPlugin = require("html-webpack-plugin");

   module.exports = {
    plugins: [
	...//其他配置
+        new HtmlWebpackPlugin({
+          template: "./public/index.html", //用来做模板的html的文件路径
+          filename: "index.html", //生成的html的名字
+          title: "webpack5的项目配置", //这个就对应上文的title
+          inject: "body" //打包出来的那个js文件,放置在生成的body标签内
+        }),

      ]
    };

生产环境配置:

//webpack.prod.conf.js

+  const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    // ...
    plugins: [
+        new HtmlWebpackPlugin({
+          template: "./public/index.html", //用来做模板的html的文件路径
+          filename: "index.html", //生成的html的名字
+          title: "webpack5的项目配置", //这个就对应上文的title
+          inject: "body", //打包出来的那个js文件,放置在生成的body标签内
+          minify: {
+            collapseWhitespace: true, // 去掉空格
+            removeComments: true // 去掉注释
+          }
+        })
        // ...
    ]
}

对比开发环境打包出来的html和生产的:

image-20220417121326621

3,压缩js文件

第一步:安装依赖
npm install terser-webpack-plugin -D
第二步:配置依赖
const TerserWebpackPlugin = require('terser-webpack-plugin');

module.exports = {
    // ...
    optimization: {
        minimize: true,
        minimizer: [
            new TerserWebpackPlugin()
        ]
    },
    // ...
}
第三步:配置测试环境和生产环境的js压缩

在实际开发过程中,测试环境的配置和生产基本一样,所以共用prod文件进行配置。但是还是有一丢丢区别。比如在压缩js的时候。生产环境我们希望去除console,而测试环境又希望保留。于是就需要区分配置。这个前提是要让webpack区分测试环境和生产环境。

在上文,第三节的第五步,已经区分环境打包了。并且注入了环境变量。

image-20220417135434601

于是,当我们运行npm run testing的时候,process.env.NODE_ENV=testing。而当我们运行npm run build的时候,process.env.NODE_ENV=production

于是在webpack.prod.conf.js中就可以这样配置js的压缩。

const TerserWebpackPlugin = require('terser-webpack-plugin');
//只有生产的包会清除log
const consoleObj = function () {
  if (process.env.NODE_ENV === "production") {
    return {
      // 多进程
      parallel: true,
      //删除注释
      extractComments: false,
      terserOptions: {
        compress: {
          // 生产环境去除console
          drop_console: true,
          drop_debugger: true
        }
      }
    };
  } else {
    return {
      // 多进程
      parallel: true
    };
  }
};

module.exports = {
    // ...
    optimization: {
        minimize: true,
        minimizer: [
            new TerserWebpackPlugin(consoleObj())
        ]
    },
    // ...
}

但是我执行npm run testing的时候,遇到了一个bug。报错了:

WARNING in DefinePlugin
Conflicting values for 'process.env.NODE_ENV'

意思就是process.env.NODE_ENV已经存在,所以冲突了,查看了官网:

Tells webpack to set process.env.NODE_ENV to a given string value. optimization.nodeEnv uses DefinePlugin unless set to false. optimization.nodeEnv defaults to mode if set, else falls back to 'production'.

是因为optimization.nodeEnv,如果不设置值的话,默认会取mode中设置的值,而恰好我mode的设置:开发环境是development,生产环境是production。和这两个文件的配置一样,所以不会冲突:

image-20220417140952871

而这时候,我运行npm run testing的时候,因为执行的还是生产配置的文件,其中的mode已经设置为production了。于是会报这个冲突的错误。

解决办法是在webpack.base.conf.js中添加这一行:

module.exports={
	...//其他配置
	optimization: {
    	nodeEnv: false
  	}
}

这样配置之后,process.env.NODE_ENV就不会从配置文件的mode取值了,而是直接用我们自己配置的webpack.DefinePlugin:

image-20220417142041733

第四步:对比配置前后的js文件是否压缩

image-20220417143835050

十三,打包友好提示

到目前为止,我们一运行打包,控制台会输出这么多东西:

image-20220417151541770

为了简化,我们在webpack.base.conf.js加入stats: "errors-only",这样只有在报错的时候会输出。

image-20220417151807190

为了更好的查看报错的信息,我们可以继续安装插件:

第一步:安装依赖

npm install friendly-errors-webpack-plugin  -D

第二步:进行配置

plugins: [
    
    new FriendlyErrorsWebpackPlugin({
      // 成功的时候输出
      compilationSuccessInfo: {
        messages: [`已经打包成功啦~`]
      },
      // 是否每次都清空控制台
      clearConsole: true
    })
  ],

十四,分析打包文件大小

有的时候,我们需要对项目进行优化。比如说减少首屏加载时间等,就需要直观地查看每个文件打包出来的大小。于是就需要这么一个插件。来查看打包文件的大小。

第一步:安装依赖

npm install webpack-bundle-analyzer -D

第二步:修改 webpack.prod.conf.js 文件

 const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
  // ...
  plugins: [
       new BundleAnalyzerPlugin({
      analyzerMode: process.env.analyMode == "true" ? "server" : "disabled", //这个配置后默认是不会启用这个插件
      generateStatsFile: false, // 是否生成stats.json文件
      statsOptions: { source: false }
    })
    // ...
  ],
};

第三步:修改 package.json 文件

因为查看生产配置才有意义,所以就写在生产配置中了:

{
    "scripts": {
     // ...
     "analyzer": "cross-env envMode=prod analyMode=true webpack --config ./config/webpack.prod.conf.js  --color --progress"
  },
}

控制台执行 npm run analyzer 系统自动启动打包报告的HTTP服务器:

image-20220417160233894

它会帮助我们统计出每个文件的三种大小:

stat size(统计尺寸)
parsed size(解析大小,webpack打包出来的文件大小)
Gzipped size(压缩大小,在开启了gzip后达到的大小)

十五,引入vue-router

第一步:安装依赖

npm install vue-router -S

第二步:编写相关的代码

//home页面
<template>
  <div class="home-box">
    <div class="home-title">首页页面</div>
    <button class="home-btn" @click="goToMine">前往我的页面</button>
  </div>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
function goToMine() {
  router.push("/mine");
}
</script>
<style></style>
//mine页面
<template>
  <div class="mine-box">
    <div class="mine-title">我的页面</div>
    <button class="mine-btn" @click="goToHome">前往首页</button>
  </div>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
function goToHome() {
  router.push("/home");
}
</script>
<style></style>
//router.js
import { createRouter, createWebHashHistory } from "vue-router";
import Home from "@/views/home/index.vue";
import Mine from "@/views/mine/index.vue";
const routerHistory = createWebHashHistory();
const router = createRouter({
  history: routerHistory,
  routes: [
    {
      path: "/",
      redirect: "/home",
      component: Home,
      children: [
        {
          path: "/home",
          name: "home",
          component: Home,
          meta: {
            keepAlive: false
          }
        }
      ]
    },
    {
      path: "/mine",
      name: "Mine",
      component: Mine,
      meta: {
        keepAlive: false
      }
    }
  ]
});
export default router;

然后App.vue

//App.vue
<template>
  <div id="app">
    <router-view v-if="!$route.meta.keepAlive" class="router" />
    <router-view
      v-if="$route.meta.keepAlive"
      v-slot="{ Component }"
      class="router"
    >
      <keep-alive>
        <component :is="Component" />
      </keep-alive>
    </router-view>
  </div>
</template>
<script setup></script>
<style lang="scss"></style>

最后main.js:

//main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index.js";
const app = createApp(App);
app.use(router).mount("#app");

运行项目,就可以看到:

image-20220417202508411

十六,引入vuex

虽然vue3的compositionAPI可以替代vuex。但我还是比较倾向于用vuex。感觉用vuex模块化管理页面的状态数据,然后compositionAPI用来封装抽离通用的功能性代码。

第一步:安装vuex

npm install vuex -S

第二步:模块化配置vuex

src/store/modules/user.js

const state = {
  userInfo: {}
};

const mutations = {
  setUserInfo(state, infoObj) {
    state.userInfo = infoObj;
  }
};
const actions = {
  async getUserInfo({ commit }) {
    //异步接口请求
    setTimeout(() => {
      let obj = {
        userName: "姓名",
        age: 18
      };
      commit("setUserInfo", obj);
    }, 0);
  }
};

export default {
  namespaced: true,
  state,
  mutations,
  actions
};

src/store/modules/index.js

import { createStore } from "vuex";
import user from "./modules/user";
const store = createStore({
  state: {},
  getters: {},
  actions: {},
  mutations: {},
  modules: {
    user
  }
});

export default store;

然后在main.js中:

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index.js";
import store from "./store/index.js";
const app = createApp(App);
app
.use(router)
+     .use(store)
.mount("#app");

最后在home页面使用:

<template>
  <div class="home-box">
    <div class="home-title">首页页面</div>
    <div class="home-content">
      <div class="content-title">测试store中的数据</div>
      <div class="user-name">{{ userObj.userName }}</div>
    </div>

    <button class="home-btn" @click="goToMine">前往我的页面</button>
  </div>
</template>
<script setup>
import { computed } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
const router = useRouter();
const store = useStore();
const userObj = computed(() => store.state.user.userInfo);
store.dispatch("user/getUserInfo");

function goToMine() {
  router.push("/mine");
}
</script>
<style></style>

于是就可以看到页面效果:

image-20220417204659388

十七,引入vant

第一步:安装依赖

npm i vant -S

第二步:按需引入vant

需要再安装一个依赖

npm i babel-plugin-import -D

第三步:新建babel.config.js

const presets = [];
const plugins = [
  //配置vant的按需引入,babel-plugin-import 是一款 babel 插件
  [
    'import',
    {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    },
    'vant'
  ]
];
module.exports = {
  plugins,
  presets
};

第四步:新建plugin文件夹按需引入vant组件

import { Button } from "vant";
export default (app) => {
  app.use(Button);
};

第五步:在main中注册

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index.js";
import store from "./store/index.js";
+   import installVant from "@/plugins/vant.js";
const app = createApp(App);
+    installVant(app);
app.use(router).use(store).mount("#app");

第六步:在页面中使用

image-20220418002512463

十八,配置vw自适应

第一步:安装依赖

npm i postcss-px-to-viewport -D

第二步:添加根目录下postcss.config.js配置

module.exports = ({ file }) => {
  const designWidth = file.includes(path.join("node_modules", "vant"))
    ? 375
    : 750;//这一步是因为vant的基准是375
  return {
    plugins: {
      autoprefixer: {},
      "postcss-px-to-viewport": {
        unitToConvert: "px", // 需要转换的单位,默认为"px"
        viewportWidth: designWidth, //  设计稿的视口宽度
        unitPrecision: 5, // 单位转换后保留的精度
        propList: ["*"], // 能转化为vw的属性列表
        viewportUnit: "vw", //  希望使用的视口单位
        fontViewportUnit: "vw", // 字体使用的视口单位
        selectorBlackList: [".ignore", ".hairlines", ".ig-"], // 需要忽略的CSS选择器
        minPixelValue: 1, // 最小的转换数值,如果为1的话,只有大于1的值会被转换
        mediaQuery: false, // 媒体查询里的单位是否需要转换单位
        replace: true, // 是否直接更换属性值,而不添加备用属性
        include: undefined, // 如果设置了include,那将只有匹配到的文件才会被转换,例如只转换 'src/mobile' 下的文件 (include: /\/src\/mobile\//)
        landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
        landscapeUnit: "vw", // 横屏时使用的单位
        landscapeWidth: 568 // 横屏时使用的视口宽度
      }
    }
  };
};

第三步:查看效果

image-20220423152521328

十九,引入axios

第一步:安装axios依赖

npm i axios -S

第二步:新建api文件夹,下新建request.js,封装axios

import axios from "axios";
import { Toast } from "vant";
import qs from "qs";
axios.defaults.headers["Content-Type"] = "application/json; charset=UTF-8";

export default function request(options) {
  return new Promise((resolve, reject) => {
    // 创建axios实例
    const service = axios.create({
      // axios中请求配置有baseURL选项,表示请求URL公共部分
      baseURL: process.env.VUE_APP_baseUrl, //baseURL 将会被加在 url 前面。
      // 超时
      timeout: 10000,
      withCredentials: true // 是否允许带cookie这些
    });
    // request拦截器
    service.interceptors.request.use(
      async (config) => {
        switch (config.method) {
          case "get":
            config.params = config.params;
            break;
          case "post":
            config.headers["Content-Type"] =
              "application/x-www-form-urlencoded; charset=UTF-8";
            config.data = qs.stringify(config.data);
            break;
          default:
        }
        return config;
      },
      (error) => {
        Toast.clear();
        console.log(error);
        Promise.reject(error);
      } //请求拦截器的报错处理
    );

    // 响应拦截器
    service.interceptors.response.use(
      (res) => {
        const handleErrCode = res.data.code;
        if (handleErrCode === "10000") {
          resolve(res.data);
        } else {
          console.log("其他状态码的处理逻辑");
          reject(res.data);
        }
      },
      (error) => {
        Toast.clear();
        console.log("err" + error);
        let { message } = error;
        if (message == "Network Error") {
          message = "网络异常,请检查网络";
        } else if (message.includes("timeout")) {
          message = "系统接口请求超时,请检查网络";
        } else if (message.includes("Request failed with status code")) {
          message = "系统接口" + message.substr(message.length - 3) + "异常";
        }
        Toast({
          message: message,
          type: "error",
          duration: 5 * 1000
        });
        return reject(error);
      }
    );
    service(options);
  });
}

第三步:引出get和post请求

api文件夹下新建http.js

import request from "@/api/request";
export function httpPost(data, url) {
  return request({
    url: url,
    method: "post",
    data
  });
}
export function httpGet(data, url) {
  return request({
    url: url,
    method: "get",
    data
  });
}

第四步:开发服务器的本地代理

注意到第二步的axios封装中,有这一行配置:

baseURL: process.env.VUE_APP_baseUrl, //baseURL 将会被加在 url 前面。

process.env.VUE_APP_baseUrl已经在上文第三节第四步,在环境变量配置文件中新建并注入项目中了:

image-20220423181901700

接下来,在webpack.dev.conf.js中配置开发服务器代理。因为我们在本地开发环境写代码的时候。请求的后端接口是跨域的,为了解决这个跨域问题,就可以利用devServer给我们提供的一个本服务器代理的方式。

原理大概如下图这般:

image-20220423183433443

因为浏览器有同源策略,直接访问服务端服务器会跨域,而服务器之间不会跨域。所以就在本地建立了一个代理来转发请求。

现在来配置这个代理服务器:

module.exports = merge(BaseWebpackConfig, {
  //..其他配置
  devServer: {
    hot: true, //模块的热替换
    open: true, // 编译结束后自动打开浏览器
    port: 8080, // 设置本地端口号
    host: "localhost", //设置本地url
    // 设置代理,用来解决本地开发跨域问题
    proxy: {
      "/api": {
        secure: false,
        changeOrigin: true,
        target:
          "https://www.fastmock.site/mock/88bbb3bb8d6ea3dc8f09431a61ce2e50/mymock_test",
        pathRewrite: { "^/api": "" }
      }
    }
  },
 }

开发环境下,我们已经配置了 process.env.VUE_APP_baseUrl=='/api'

第五步,发送请求

     <div class="five item">
        <div class="content-title">测试axios请求数据</div>
        <van-button type="success" class="home-btn" @click="testAxios">
          发送请求
        </van-button>
        <div class="content-request">{{ renderTest }}</div>
      </div>
import { httpGet } from "@/api/http.js";
    //发送请求
 async function testAxios() {
      try {
        let params = {};
        const res = await httpGet(params, "/justtest");
        renderTest.value = res.data.content;
      } catch (error) {}
}

页面效果:

222

二十,引入Vconsole

有的时候,我们的h5是需要内嵌在app上的,为了能够查看报错,可以安装一下Vconsole插件:

第一步:安装依赖

npm i vconsole -S

第二步,项目中配置

在上文的第三节第四步,我们已经分环境配置好变量并注入项目中了。于是就可以在main.js中配置:

image-20220423192239064

//main.js

// 开发测试环境显示console
if (process.env.VUE_APP_SHOWCONSOLE === "true") {
  let Vconsole = require("../node_modules/vconsole/dist/vconsole.min");
  new Vconsole();
}

第三步:查看效果:

image-20220423192347758

二十一,代码地址及小结

项目地址(本文内容在master分支上):

https://gitee.com/ling-xu/webapck5

小结:

第一次在掘金写博客,这篇文章算是开个头,后续再加上eslint配置这些。慢慢更新吧。

关于我:

一个毕业后在工厂拧了两年螺丝的前大国工酱,现前端新手村成员。写博客主要是为了分享和记录,内容比较简单,还望路过的大佬斧正。