Webpack5 基础 & Vue3 项目搭建

3,360 阅读9分钟

一、概念

官网解释:
webpack 是一个用于现代 JavaScript 应用程序的__静态模块打包工具__。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph) ,此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle

二、起步

创建 webpack-demo-main 文件夹。

cd webpack-demo-main // 进入文件目录
npm init -y   // npm初始化  -y的意思是一路yes。省去了回车步骤
npm install webpack webpack-cli --save-dev 

打开项目目录下的 package.json 文件,展开此文件如下所示:

{
  "name": "webpack-demo-main",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

在该文件中,我们新增 private: true 参数,该参数是确保私有。移除掉 main 参数。
现在,我们将创建以下目录结构、文件和内容:

webpack-demo-main
  |- package.json
+ |- index.html
+ |- /src
+   |- index.js

index.js

//index.js
function comp() {
  const el = document.createElement('div');

  el.innerHTML = '<i>你好, X1AXX1A</i>'

  return el;
}

document.body.appendChild(comp());

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>X1AXX1A</title>
  </head>
  <body>
    <script src="./src/index.js"></script>
  </body>
</html>

三、管理资源

3.1 配置入口与输出

在项目根目录创建 webpack.config.js 文件,并输入以下代码:

//webpack.config.js
const path = require('path');

module.exports = {
    mode: 'development',//development开发环境,production生产模式
    entry: './src/index.js',//入口文件
    output: {
        filename: '[name].[contenthash].js',// 输出文件
        path: path.resolve(__dirname, 'dist'),// 输出文件存放地址
    },
};

entry 属性可以设置项目的入口文件
output 指示 webpack 如何去输出、以及在哪里输出你的「bundle、asset 和其他你所打包或使用 webpack 载入的任何内容」。
|-filename 设置输出的文件名
|-path 输出文件存放目录。

Node.js中,__dirname 总是指向 被执行的js文件 的绝对路径。
我们在 webpack.config.js 文件中打印一下 __dirname 看看,这就是当前的 webpack.config.js 所在的绝对路径了。

关于 path.resolve 方法会将路径或路径片段的序列解析为绝对路径。给定的路径序列会从右到左进行处理,后面的每个 path 会被追加到前面,直到构造出绝对路径。

path.resolve('/目录1/目录2', './目录3');
// 返回: '/目录1/目录2/目录3'

path.resolve('/目录1/目录2', '/目录3/目录4/');
// 返回: '/目录3/目录4'

path.join 方法会将所有给定的 path 片段连接到一起(使用平台特定的分隔符作为定界符),然后规范化生成的路径。如果连接后的路径字符串为长度为零的字符串,则返回 '.',表示当前工作目录。

path.join('/目录1', '目录2', '目录3/目录4', '目录5', '..');
// 返回: '/目录1/目录2/目录3/目录4'

我们将新建 dist 目录,将 index.html 移到 dist 目录下,并改一下 script 引入路径

//index.html
//<script src="./src/index.js"></script>
<script src="main.js"></script>

package.json 文件中 scripts 字段内定义脚本命令。

//...
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack" 
},
//...

然后执行

npm run build

dist 文件夹中就多出了一个 main.js 文件。

3.2 加载css

为了在 JavaScript 模块中 import 一个CSS文件,你需要安装 style-loadercss-loader ,并在module 配置中添加这些 loader

npm install --save-dev style-loader css-loader

然后在 webpack.config.js 中添加如下代码:

  const path = require('path');
 
 module.exports = {
   entry: './src/index.js',
   output: {
     filename: '[name].[contenthash].js',
     path: path.resolve(__dirname, 'dist'),
   },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
 };

模块 loader 可以链式调用。链中的每个 loader 都将对资源进行转换。链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。

所以应保证 loader 的先后顺序:style-loader 在前,而 css-loader 在后。如果不遵守此约定,webpack 可能会抛出错误。

css-loader 会处理import require() @import url 引入的内容,将其转化成js对象。

我们先将 style-loader 去掉,做个测试,在index.js中尝试先 import 一个 CSS 文件然后打印出来,我们打印出来的 style 被 css-loader 转成了js数组了。如下所示:
修改 index.js

import style from './index.css'

function comp() {
    const el = document.createElement('div');
  
    el.innerHTML = '<i>你好, X1AXX1A</i>'
    console.log(style)
  
    return el;
  }
  
  document.body.appendChild(comp());

同目录新增 index.css 文件

div {
    color: red
}

因为 style 打印出来的是个数组,页面是无法直接使用的,可以看到字体没有变红色。

接下来我们将 style-loader 加回去。重新运行,字体变红色了。
所以 style-loader 主要是将 css-loader 生成的数组加到js中。

3.3 加载图片

webpack 5
可以使用内置的Asset Modules,它允许使用资源文件(字体,图标等)而无需配置额外 loader

webpack 5 之前,通常使用的:

  • raw-loader 将文件导入为字符串
  • url-loader 将文件作为 data URI 内联到 bundle 中
  • file-loader 将文件发送到输出目录 资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader。
  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间 自动选择 。之前通过使用 url-loader ,并且配置资源体积限制实现。 所以我们只需要新增如下代码,即可。
{
    test: /\.(jpe?g|png|gif|svg)$/i,
    type: 'asset',//在导出一个 data URI 和发送一个单独的文件之间自动选择
}

具体参考官方Asset Modules教程

3.4 解析(resolve)

常见有aliasextensions
resolve.alias
创建 importrequire 的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块
在 src 下创建 assets 目录,存放一个图片,然后在 index.js 中导入一个图片的时候可以这么写。

import imgSrc from '@/assets/img.png'

resolve.extentions
尝试按顺序解析这些后缀名,即导入文件的时候不需要写后缀名,webpack 会按配置的后缀名顺序解析找到对应文件。

module.exports = {
  //...
  resolve: {
        extensions: ['.vue', '.js'], //表示在import 文件时文件后缀名可以不写
        alias: {
            '@': path.join(__dirname, 'src')
            // 这里的别名配置需与 jsconfig 中的 paths 别名一致
            // import的文件在src下component里的时候可以直接写成 @/component/...
        }
  },
  //...
}

但是使用 alias 的时候,可能会发现路径和函数的智能提示不见了,如果路径名称很复杂的话很容易写错而且也不方便。

这时候可以在根目录新增jsconfig.json

{
    "compilerOptions": {
        "baseUrl": "./", // 基本目录,用于解析非相对模块名称
        "paths": {
            "@/*": ["src/*"] // 指定要相对于 baseUrl 选项计算别名的路径映射
        },
      "experimentalDecorators": true //为ES装饰器提案提供实验支持
    },
    "exclude": ["node_module", "dist"]
}

注意:若项目集成 TypeScript 的时候,配置文件则是 tsconfig.json,设置 allowJs: true,jsconfig.json 才生效。

3.5 将ES6+ 转 ES5

为兼容广大浏览器,需要将js语法解析成ES5版本的。这时候就第三方的 loaderBabel 来帮助 webpack 来处理 ES6+ 语法。

3.5.1 Babel是什么呢?

Babel 是一个 JavaScript 编译器 Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

babel-loader允许你使用 Babelwebpack 转译 JavaScript 文件。

安装相关依赖包

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

关于@babel 7 的一大调整,原来的 babel-xx 包统一迁移到 babel 域下

  • @babel-core Babel 的核心功能包含在 @babel/core 模块中
  • @babel/preset-env 为目标浏览器中没有的功能加载转换插件 webpack.config.js
//...
rules: [
    {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ['babel-loader']
    }
]
//...

在根目录创建.babelrc文件

{
    "presets": [
        ["@babel/preset-env"]
    ]
}

index.js 中添加一下代码

// Babel 测试
console.log([1,2,3].findIndex(x => x === 4))
console.log('abc'.padStart(10))


箭头函数被转换了,说明是成功的。

@babel/preset-env 提供了一个 useBuiltIns 参数,设置值为 usage 时,就只会包含代码需要的 polyfill 。有一点需要注意:配置此参数的值为 usage ,必须要同时设置 corejs (如果不设置,会给出警告,默认使用的是"corejs": 2) ,注意: 这里仍然需要安装 @babel/polyfill(当前 @babel/polyfill 版本默认会安装 "corejs": 2)

首先说一下使用 core-js@3 的原因,core-js@2 分支中已经不会再添加新特性,新特性都会添加到 core-js@3。例如你使用了 Array.prototype.flat(),如果你使用的是 core-js@2,那么其不包含此新特性。为了可以使用更多的新特性,建议大家使用 core-js@3

npm install --save @babel/polyfill core-js@3
//.babelrc
{
    "presets": [
        ["@babel/preset-env", {   
            "useBuiltIns": "usage",
            "corejs": 3
        }]
    ]
}

3.5.2 @babel/plugin-transform-runtime

Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend / _classCallCheck。默认情况下会被添加到每一个需要它的文件中。这样会造成重复引入,@babel/plugin-transform-runtime可以解决这种情况。

转换插件 @babel/plugin-transform-runtime 通常仅在开发中使用,但是运行时需要依赖 @babel/runtime ,所以 @babel/runtime 必须要作为生产依赖被安装。

npm i -D @babel/plugin-transform-runtime
npm i @babel/runtime      //这里不带-D哦

presets后面添加一行配置

//.babelrc
{
    "presets": [
        ["@babel/preset-env", {
            "useBuiltIns": "usage",
            "corejs": 3
        }]
    ],
    "plugins": [
        "@babel/plugin-transform-runtime"
    ]
}

避免全局污染,我们使用依赖 @babel/runtime-corejs3,再修改一下配置文件

npm install @babel/runtime-corejs3 --save
//.babelrc
{
    "presets": [
        ["@babel/preset-env"]
    ],
    "plugins": [
        ["@babel/plugin-transform-runtime", {
            "corejs": 3
        }]
    ]
}

四、管理输出

在上面我们的测试例子中,生成的 main.js 需要我们手动的在 index.html 中引入。然而随着应用程序增长,并且一旦开始在文件名中使用 contenthash 并输出 多个 bundle ,如果继续手动管理 index.html 文件,就会变得困难起来。

这时候就需要用到 HtmlWebpackPlugin

npm install --save-dev html-webpack-plugin

webpack.config.js

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

module.exports = {
    //...
    plugins: [
        new HtmlWebpackPlugin({})
    ],
    module: {
    //...

HtmlWebpackPlugin 会新生成 index.html 文件,替换我们的原有文件。

接下来我们在根目录新建 public 文件夹,将 dist 中的 index.html 移动到 public 文件夹下,并修改 webpack.config.js 文件

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

module.exports = {
  //...
  plugins: [
    new HtmlWebpackPlugin({
      title: '我是webpack.config配置的标题',
      template: './public/index.html',
      //压缩HTML
      minify: {
        removeComments: true, // 移除HTML中的注释
        collapseWhitespace: true // 删除空白符与换行符
      }
    })
  ],
  module: {
  //...

这样表示以 public 下的 index.html 为引用模板。动态的引入编译后的相关服务资源。若是在配置文件中配置 title 属性,就需要我们修改html文件中的 title 来获取该配置选项。如下所示:

<!--index.html-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
  </body>
</html>

由于 webpack 将生成文件并放置在 /dist 文件夹中,但是它不会追踪哪些文件是实际在项目中用到的,导致 dist 内有很多多余文件,这时就需要配合使用 clean-webpack-plugin 插件,创建前清空 dist 文件夹

npm install --save-dev clean-webpack-plugin

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    //...
    plugins: [
    	new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: '我是webpack.config配置的标题',
            template: './public/index.html',
            //压缩HTML
            minify: {
                removeComments: true, // 移除HTML中的注释
                collapseWhitespace: true // 删除空白符与换行符
            }
        })
    ],
    module: {
    //...

五、开发环境

5.1 热更新

在每次编译代码时,手动运行 npm run build 会显得很麻烦。webpack-dev-server 插件正好解决这个问题。

webpack-dev-server 主要提供两个功能

  1. 为静态文件提供web服务
  2. 自动刷新和热替换(HMR) 自动刷新指当前修改webpack会进行自动编译,更新网页内容
    热替换指运行时更新各种模块,即局部刷新
npm install --save-dev webpack-dev-server

webpack.config.js

module.exports = {
    //...
    devServer: {
        contentBase: './dist'
    },
    //...
}

以上配置告知 webpack-dev-server ,将 dist 目录下的文件serve到 localhost:8080 下。(译注: serve ,将资源作为 server 的可访问文件)

webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。如果你的页面希望在其他不同路径中找到 bundle 文件,则可以通过 dev server配置中的 publicPath 选项进行修改。

修改package.json添加运行代码

{
   //...
   "scripts": {
    	"test": "echo \"Error: no test specified\" && exit 1",
    	"dev": "webpack serve --open chrome",
    	"build": "webpack"
    },
    //...
}

然后在控制台执行 npm run dev 即可看到实现了热更新,但是却类似F5刷新的那种更新。 我们要实现的热更新,添加一行 hot: true 即可

devServer: {
   contentBase: './dist',
   hot: true
 },

5.2 代理配置

在日常开发过程中,经常遇到跨域问题,这里就可以用到 devServerproxy 配置代理,来实现跨域请求。

webpack.config.js

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': 'http://localhost:3000',
    },
  },
};

假如现在,对 /api/users 的请求会将请求代理到 http://localhost:3000/api/users。

如果不希望传递/api,则需要重写路径:

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        pathRewrite: { 
          '^/api': '' 
        },
      },
    },
  },
};

这时候,对 /api/users 的请求会将请求代理到http://localhost:3000/user

默认情况下,将不接受在 HTTPS 上运行且证书无效的后端服务器。 如果需要,可以这样修改配置:

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'https://other-server.example.com',
        secure: false,
      },
    },
  },
};

如果想将多个特定路径代理到同一目标,则可以使用一个或多个带有 context 属性的对象的数组:

module.exports = {
  //...
  devServer: {
    proxy: [
      {
        context: ['/auth', '/api'],
        target: 'http://localhost:3000',
      },
    ],
  },
};

默认情况下,代理时会保留主机头的来源,可以将 changeOrigin 设置为 true 以覆盖此行为。

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
};

更多教程可以查看官方文档

5.3 DevTool

为了更容易地追踪 error 和 warning,JavaScript 提供了 source maps 功能,可以将编译后的代码映射回原始源代码。devtool 控制是否生成,以及如何生成 source map 。不同的 devtool 设置,会导致性能差异。具体参照官方教程
先来试试 inline-source-map ,修改一下配置文件。
webpack.confog.js

module.exports = {
    mode: 'development',
    entry: './src/index.js',//入口文件
    output: {
        filename: '[name].[contenthash].js',// 输出文件
        path: path.resolve(__dirname, 'dist'),// 输出文件存放地址
    },
    devtool: "inline-source-map",//这里添加一行
    //...

然后我们在index.js中输入 console.llog('这是一个错误') ,然后打包项目。

打包的结果,生成一个 map 文件内容对应的 base64 插入到 main.bundle.js 里面去
试试source-map

可以看到,多出了.map文件,main.bundle.js中末端有指明具体引用的map文件

比如有一个场景是我们需要调试一个线上项目,既不能将 source map 添加到代码中,又需要调试的情况。就可以通过生成一个 .map 文件,通过添加到浏览器进行调试,如下图所示。

这样就可以在本地调试部署到线上的项目了。

避免在生产中使用 inline-*** 和 eval-***,因为它们会增加 bundle 体积大小,并降低整体性能。

我们在生产环境配置 devtool: 'source-map' 就行了,但是部署的时候不要将map文件也部署上去;或者不配置devtool选项也行,这样就不生成map文件了。

5.4 外部扩展(externals)

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。意思就是说在项目中通过 import 引入的依赖在打包的时候不会打包到 bundle 中去,而是通过 script 引入的方式去访问这些依赖。

例如,从 CDN 引入 jQuery,而不是把它打包:

index.html

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>

webpack.config.js

module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};

这样就剥离了那些不需要改动的依赖模块,我们在 index.js 中引入 jq 看看打印结果:

import $ from 'jquery';
console.log($('#app'));

5.5 缓存

SplitChunksPlugin 可以用于将模块分离到单独的 bundle 中。webpack 还提供了一个优化功能,可使用 optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk。将其设置为 single 来为所有 chunk 创建一个 runtime bundle

将第三方库(library)(例如 lodashreact)提取到单独的 vendor chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。

我们在 webpack.config.js 中添加代码

module.exports = {
    //...
    // https://webpack.docschina.org/guides/caching/
    optimization: {
        moduleIds: 'deterministic',
        // 使用 optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk
        runtimeChunk: 'single',
        splitChunks: {
            // 利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,
            // 同时还能保证 client 代码和 server 代码版本一致。 这可以通过
            // 使用SplitChunksPlugin 插件的 cacheGroups 选项来实现。
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all',
                },
            },
        },
    },
}

可以看到,main文件大小从1.78MiB 缩减到 13.2KiB

image.png

image.png 在加入选项 moduleIds: 'deterministic' 之前重新运行,三个文件的 hash 都变化了。这是因为每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。因此,简要概括:

main bundle 会随着自身的新增内容的修改,而发生变化。
vendor bundle 会随着自身的 module.id 的变化,而发生变化。
manifest runtime 会因为现在包含一个新模块的引用,而发生变化。

第一个和最后一个都是符合预期的行为,vendor hash 发生变化是我们要修复的。我们将 optimization.moduleIds 设置为 deterministic

我们重新运行项目

image.png 现在,不论是否添加任何新的本地依赖,对于前后两次构建,vendor hash 都应该保持一致。

六、生产环境

6.1 配置

在开发环境中,我们需要:强大的 source map 和一个有着 live reloading(实时重新加载) 或 hot module replacement(热模块替换) 能力的 localhost server

而生产环境目标则转移至其他方面,关注点在于压缩 bundle、更轻量的 source map、资源优化等,通过这些优化方式改善加载时间。

由于遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

在根目录新建build文件夹,在 build 文件夹下新建 webpack.common.js 文件放置通用配置,以及创建生产模式webpack.prod.js 的和开发模式 webpack.dev.js 的配置文件,为了将这些配置合并在一起,我们将使用一个名为 webpack-merge 的工具。

此时 __dirname 的路径已经发生变化啦

npm install --save-dev webpack-merge

webpack.common.js

//webpack.common.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js',//入口文件
    output: {
        filename: '[name].[contenthash].js',// 输出文件
        path: path.resolve(__dirname, '../dist'),// 输出文件存放地址
    },
    resolve: {
        extensions: ['.vue', '.js'], //表示在import 文件时文件后缀名可以不写
        alias: {
            '@': path.join(__dirname, '../src')
            //import的文件在src下的时候可以直接写成 @/component/...
        }
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: '我是webpack.config配置的标题',
            template: './public/index.html',
            //压缩HTML
            minify: {
                removeComments: true, // 移除HTML中的注释
                collapseWhitespace: true // 删除空白符与换行符
            }
        })
    ],
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader'],
            },
            {
                test: /\.(jpe?g|png|gif|svg)$/i,
                type: 'asset',//在导出一个 data URI 和发送一个单独的文件之间自动选择
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            }
        ],
    },
    // https://webpack.docschina.org/guides/caching/
    optimization: {
        // deterministic 选项有益于长期缓存
        moduleIds: 'deterministic',
        // 使用 optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk
        runtimeChunk: 'single',
        splitChunks: {
            // 利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,
            // 同时还能保证 client 代码和 server 代码版本一致。 这可以通过
            // 使用SplitChunksPlugin 插件的 cacheGroups 选项来实现。
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all',
                },
            },
        },
    },
};

webpack.dev.js
开发环境:调试定位、热替换等

//webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
        contentBase: '../dist',
        hot: true
    },
});

webpack.prod.js
生产环境:代码压缩、公共模块分离、资源优化等

//webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    mode: 'production',
    devtool: "source-map",
});

然后我们再把 package.json 中的 script 重新指向新的配置,让 npm run dev script 中 webpack-dev-server, 使用 webpack.dev.js, 而让 npm run build script 使用 webpack.prod.js

package.json

//...
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack serve --open chrome --config build/webpack.dev.js",
    "build": "webpack --config build/webpack.prod.js"
  },
//...

6.2 css抽离与压缩

MiniCssExtractPlugin
MiniCssExtractPlugin 插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。

npm install --save-dev mini-css-extract-plugin

该插件会将css单独提取到一个样式文件中,并加到html中,所以这里就跟 style-loader 冲突了,我们修改一下配置文件。

//webpack.common.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
//...
  plugins: [
    new MiniCssExtractniPlugin({
      filename: '[name].[contenthash].css'
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
//...
};

MiniCssExtractPlugin 插件常用于生产环境,再开发环境我们还是可以继续用 style-loader

所以我们再修改下配置文件,在 webpack.dev.js 中继续用 style-loader,而 webpack.prod.js 中用MiniCssExtractPlugin

接下来压缩输出的css文件,安装 css-minimizer-webpack-plugin 插件。

npm i -D css-minimizer-webpack-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
//...
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[id].css',
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
  optimization: {
    minimizer: [
      new CssMinimizerPlugin({
          parallel: true // 使用多进程并发执行,提升构建速度
      }),
    ],
  },
};

我们执行打包命令,可以看到css已经被压缩啦。

这将仅在生产环境开启 CSS 优化。

如果还想在开发环境下启用 CSS 优化,将 optimization.minimize 设置为 true

不加上的时候,生成的 main 文件是31.4KiB

image.png 我们在webpack.dev.js中加上

//webpack.dev.js
module.exports = merge(common, {
    //...
    optimization: {
        minimize: true
    }
})

运行,可以看到,main 文件是13.2KiB

image.png

6.3 js压缩

TerserWebpackPlugin插件使用 terser 来压缩 JavaScript。

如果你使用的是 webpack v5 或以上版本,你不需要安装这个插件。webpack v5 自带最新的 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

为调试方便,开发过程中,不压缩代码。
我们加到生成环境中。

// webpack.prod.js
const TerserPlugin = require("terser-webpack-plugin");
module.exports = merge(common, {
    // ...
    optimization: {
          // https://webpack.docschina.org/plugins/terser-webpack-plugin/
          // 压缩js
          minimize: true
          minimizer: [
              // https://webpack.docschina.org/plugins/terser-webpack-plugin/
              // 压缩js
              new TerserPlugin({
                  parallel: true // 使用多进程并发执行,提升构建速度
              })
          ]
    }

})

运行,打包我们的项目,可以看到生成的js文件被压缩成一行。

image.png

6.4 图片压缩

哦吼,看到其他博文都有图片压缩的,我也加上。

来吧,跟着官网走一波

需要安装插件

npm install image-minimizer-webpack-plugin --save-dev

对于图片的优化有两种模式

一种是无损模式
一直是有损模式

官网推荐的无损插件有

npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo --save-dev

官网推荐的有损插件有

npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo --save-dev

在 webpack.prod.js 中新增代码

// webpack.prod.js
//...
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

module.exports = {
    //...
    plugins: [
        //...
        new ImageMinimizerPlugin({
            minimizerOptions: {
              // 无损设置
              plugins: [
                ['gifsicle', { interlaced: true }],
                ['jpegtran', { progressive: true }],
                ['optipng', { optimizationLevel: 5 }],
                [
                  'svgo',
                  {
                    plugins: [
                      {
                        removeViewBox: false,
                      },
                    ],
                  },
                ],
              ],
            },
        })
    ]
}

打包项目,可以看到 dist 内的图片变小了。

image.png image.png

注意:这里是基于“3.3 加载图片”中的文件匹配做处理。因为需要先识别到这类文件,才能对其进行压缩处理。之前文中漏了匹配jpeg文件了。导致压缩出错。感谢我不喝可乐提出问题。

七、集成Vue3

首先,要让 webpack 识别 .vue 文件。就需要 vue-loader 插件,安装插件。

npm install vue@next -S
npm install -D vue-loader@next @vue/compiler-sfc

注意:Vue2.x 时安装的是 vue-template-complier

vue-loader:解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理。

先来配置让 webpack 去识别.vue文件

//webpack.common.js
const { VueLoaderPlugin } = require('vue-loader/dist/index');

module.exports = {
	//...
    plugins: [
    	//...
    	new VueLoaderPlugin() //解析和转换.vue文件的插件
    ],
    module: [
    	rules: [
            //...
            {
                test: /\.vue$/,
                use: ['vue-loader']
            }
        ]
    ]
}

src目录下新建App.vue文件试试

<!--App.vue-->
<template>
    <div>你好,X1AXX1A</div>
</template>
<script>
import { defineComponent } from "vue"
export default defineComponent({
  setup() {
      console.log('我是App里的打印')
  }  
})
</script>
<style scoped>
@import '@/index.css';
</style>

修改index.js文件

//index.js
import App from './App.vue'
import { createApp } from 'vue'

createApp(App).mount('#app')

添加 id 为 app 的 div 到 index.html 中

<!--index.html-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

接下来,装上Vue全家桶 Vuexvue-routerAxios

npm install vuex@next --save

然后在 src 目录下新建 store 文件夹,并在 store 文件夹下新建 index.js 和 modules 文件夹。

modules 文件夹存放各类 store 模块。我们新增一个 user 模块,在 modules 下新建 user.js

// src/store/modules
const state = () => ({
  token: null
})

const getters = {
  token: (state) => state.token
}

const mutations = {
  SET_TOKEN (state, payload) {
    state.token = payload
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations
}

然后使用 require.context 动态加载 modules 文件夹下文件。

// src/store/index.js
import { createStore } from 'vuex'

const files = require.context('./modules', false, /\.ts$/)
const modules = {}

files.keys().forEach((key) => {
    modules[key.replace(/(\.\/|\.ts)/g, '')] = files(key).default
})
console.log('X1AXX1A modules', modules)
export default createStore({
    modules
})

打印看看获取到的 user 模块

image.png

随后,在入口文件中引入该文件

// index.js
import App from './App.vue'
import { createApp } from 'vue'
import store from './store'

createApp(App).use(store).mount('#app')

以上,就可以在组件中使用 store 了

我们在App.vue中打印

// App.vue
import { defineComponent } from "vue"
import { useStore } from 'vuex'

export default defineComponent({
  setup() {
    let store = useStore()
    console.log('store', store)
    console.log('我是App里123112的打印')
  }  
})

image.png

接下来安装路由器

npm install vue-router@next

在 src下新建 router 文件夹,并在 router 文件夹下新建 index.js,新增如下代码

// src/router/index.js

import { createRouter, createWebHashHistory } from 'vue-router'
import store from '../store'

const routes = [
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  },
  {
    path: '/',
    name: 'Index',
    component: () => import(/* webpackChunkName: "about" */ '@/views/Index.vue')
  },
  {
    // 路由配置'*'报错问题,替换成'/:catchAll(.*)'
    // caught Error: Catch all routes ("*") must now be defined using a param with a custom regexp
    path: '/:catchAll(.*)', // 此处需特别注意置于最底部
    redirect: '/404'
  }
]

const router = createRouter({
  history: createWebHashHistory(), // hash模式:createWebHashHistory,history模式:createWebHistory
  routes
})

export default router

在入口文件中引入该文件

// index.js
import App from './App.vue'
import { createApp } from 'vue'
import store from './store'
import router from './router'

createApp(App).use(store).use(router).mount('#app')

修改App.vue

// App.vue
<template>
  <router-view />
</template>
<script>
    import { defineComponent } from "vue"
    export default defineComponent({})
</script>

在 views 中新建Index.vue,在 Index.vue 中新增以下代码中,打印看看

// src/views/Index.vue
<template>
  <div>我是首页</div>
</template>
<script>
    import { defineComponent } from "vue"
    // import { useStore } from 'vuex'
    import { useRouter, useRoute } from 'vue-router'

    export default defineComponent({
      setup() {
        // let store = useStore()
        let router = useRouter()
        let route = useRoute()
        // console.log('store', store)
        console.log('router', router)
        console.log('route', route)
        console.log('我是App里123112的打印')
      }  
    })
</script>

image.png

接下来就是 axios

npm i axios

按我们的习惯封装axios,做请求拦截等。

import axios from 'axios'
//添加请求拦截器
axios.interceptors.request.use(function(config){
    //在发送请求之前做某事
    return config;
},function(error){
    //请求错误时做些事
    return Promise.reject(error);
});
  
//添加响应拦截器
axios.interceptors.response.use((response)=>{
    //对响应数据做些事
    return response;
},(error)=>{
    return Promise.reject(error);
});
 
export default axios

以上的准备做好之后,就可以开始我们的 Vue 3 项目之旅啦。

最后,谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。

参考: webpack官网
babel官网
不容错过的 Babel7 知识