使用Webpack5创建Vue2项目及优化

2022-09-27 15:51:30 浏览数 (1)

前言

之前我们大多都是用Vue-Cli来创建项目,但是Vue-Cli已经停止更新了,并且Vue-Cli相当于一堆插件的集合体,我们想替换以下,或者想根据我们的项目优化以下,提升编译的性能,这时候可以自己用Webpack来配置项目。

在搭建的时候最头疼的是两个问题

  • 依赖下载不下来
  • 依赖之间不兼容

安装cnpm 可以解决依赖无法下载的问题

代码语言:javascript复制
npm install -g cnpm --registry=https://registry.npm.taobao.org

配置步骤

基本配置

创建项目文件夹 webpack01

进入项目文件夹根目录,运行

代码语言:javascript复制
npm init

安装基础依赖

代码语言:javascript复制
npm i -D webpack@5.74.0 webpack-cli@4.10.0 webpack-dev-server@4.10.0
npm i -D html-webpack-plugin@5.5.0
npm i vue@2.6.11
npm i -D vue-loader@15.10.0 vue-template-compiler@2.6.11

注意

vue-template-compiler要和vue的版本一致 html-webpack-plugin@5.x才支持webpack@5.x

创建以下文件夹及文件

/public/index.html

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>标题</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

/src/App.vue

代码语言:javascript复制
<template>
  <div>Hello</div>
</template>

<script>
export default {
  name: "App"
}
</script>

<style scoped>

</style>

/src/main.js

代码语言:javascript复制
import App from './App';
import Vue from "vue";
new Vue({
  render: h => h(App)
}).$mount("#app");

/webpack.config.js

代码语言:javascript复制
const path = require('path');
const {VueLoaderPlugin} = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  watch: true,
  module: {
    rules: [
      {
        test: /.vue$/,
        loader: 'vue-loader'
      },
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({template: './public/index.html'}), //JS或者CSS文件可以自动引入到html中
  ],
  resolve: {
    extensions: ['.js', '.css', '.vue'],  //配置后缀名
  },
  devServer: {
    port: 8080,
    hot: true,
    open: true,
    static: {
      directory: path.join(__dirname, './'),
      watch: true
    }
  }
}

pacakge.json 中添加 scripts 配置

代码语言:javascript复制
{
  "name": "webpack01",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack --mode production"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "~5.5.0",
    "vue-loader": "~15.10.0",
    "vue-template-compiler": "~2.6.11",
    "webpack": "~5.74.0",
    "webpack-cli": "~4.10.0",
    "webpack-dev-server": "~4.10.0"
  },
  "dependencies": {
    "vue": "~2.6.11"
  }
}

这时候就能运行了

代码语言:javascript复制
npm run start

打包

代码语言:javascript复制
npm run build

查看webpack的版本

代码语言:javascript复制
npx webpack --version

Vue Loader简介

https://vue-loader.vuejs.org/zh/guide/#vue-cli

Vue Loader 的配置和其它的 loader 不太一样。

除了通过一条规则将 vue-loader 应用到所有扩展名为 .vue 的文件上之外,请确保在你的 webpack 配置中添加 Vue Loader 的插件:

代码语言:javascript复制
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  module: {
    rules: [
      // ... 其它规则
      {
        test: /.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    // 请确保引入这个插件!
    new VueLoaderPlugin()
  ]
}

这个插件是必须的!

它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。

例如,如果你有一条匹配 /.js$/ 的规则,那么它会应用到 .vue 文件里的 <script> 块。

处理HTML

HTML中根据变量取值

代码语言:javascript复制
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const IS_PRODUCTION = process.env.NODE_ENV === "production";

// 生产配置
const cdn_production = {
  js: ["/librarys/vue@2.6.11/vue.min.js"]
};
// 开发配置
const cdn_development = {
  js: ["/librarys/vue@2.6.11/vue.js"]
};

module.exports = {

  externals: {
    BMap: "BMap",
    vue: "Vue",
    "vue-router": "VueRouter",
    vuex: "Vuex",
    echarts: "echarts",
    axios: "axios",
    "view-design": "iview",
    mathjs: "math",
    xlsx: "XLSX2",
    "xlsx-style": "XLSX",
    "crypto-js": "CryptoJS",
    "v-viewer": "VueViewer",
    AgoraRTC_N: "AgoraRTC",
    html2canvas: "html2canvas"
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      cdn: IS_PRODUCTION ? cdn_production : cdn_development
    }), //JS或者CSS文件可以自动引入到html中
  ],
}

HTML中取值

代码语言:javascript复制
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
  <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
  <script src="/librarys/axios@0.21.1/axios.min.js"></script>
  <script src="/librarys/vue-router@3.2.0/vue-router.min.js"></script>
  <script src="/librarys/vuex@3.2.0/vuex.min.js"></script>

处理JS

安装babel

添加依赖

代码语言:javascript复制
npm i -D babel-loader@8.2.5 @babel/core@7.18.13
npm i -D @babel/preset-env@7.18.10 @babel/polyfill@7.12.1
npm i -D @babel/plugin-transform-runtime@7.18.10
npm i -S @babel/runtime@7.18.9 @babel/runtime-corejs2@7.18.9

@babel/plugin-transform-runtime有三大作用,其中之一就是自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers里的辅助函数来替代。这样就减少了我们手动引入的麻烦。

现在我们除了安装@babel/runtime包提供辅助函数模块,还要安装Babel插件@babel/plugin-transform-runtime来自动替换辅助函数。

作用

babel-loader:只是和webpack之间的桥梁,并不会把es6语法进行转换。 @babel/preset-env @babel/polyfill是做转换的。 以上babel的配置是官网提供主要用来解决业务代码js语法转译用的,当要生成类库或者组件库时上面这种配置会污染全局变量,需要使用@babel/plugin-transform-runtime

在根目录下创建 babel 配置文件 .babelrc:

代码语言:javascript复制
{
  "presets": ["@babel/preset-env"],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 2,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
      }
    ]
  ]
}

注意:"corejs": 2, // 这里设置2是因为上面安装的版本是 @babel/runtime-corejs2

配置webpack.config.js设置使用babel的规则

代码语言:javascript复制
module.exports = {
  module: {
    rules: [
      { test: /.js$/, exclude: /node_modules/, loader: "babel-loader" }
    ]
  }
}

缓存

代码语言:javascript复制
{
  test: /.js$/i,
  include: resolve('src'),
  exclude: /node_modules/,
  use: [
    {
      loader: 'babel-loader',
      options: {
        cacheDirectory: true // 启用缓存
      }
    },
  ]
},

处理CSS

其中less和sass任选其一即可。

处理css文件

添加依赖

代码语言:javascript复制
npm i -D style-loader@3.3.1 css-loader@6.7.1

在webpack.config.js这个配置文件设置匹配css文件处理的插件

代码语言:javascript复制
{ test: /.css$/, use: ['style-loader', 'css-loader'] },

处理less

添加依赖

代码语言:javascript复制
npm i less-loader@11.0.0 less@4.1.3 -D

在webpack.config.js配置文件设置匹配less文件的处理

代码语言:javascript复制
{ test: /.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },

处理sass

安装sass-loader node-sass工具来处理sass文件

代码语言:javascript复制
npm i sass-loader node-sass -D
npm i sass fiber -D

在webpack.config.js配置文件设置匹配scss文件的处理

代码语言:javascript复制
{ test: /.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },

处理URL

图片

安装url-loader

代码语言:javascript复制
npm i url-loader@4.1.1 file-loader@6.2.0 -D

在webpack.config.js中添加处理url路径的loader模块:

代码语言:javascript复制
{test: /.(jpg|png|gif|bmp|jpeg)$/, use: 'url-loader?esModule=false&limit=500&name=imgs/[hash:8]-[name].[ext]'},

上面这种输入参数的方式还有另一种方式,以对象的键值对方式,如下:

代码语言:javascript复制
{
  test: /.(jpg|png|gif|bmp|jpeg|jfif)$/,
  use: [{
    loader: 'url-loader',
    options: {
      esModule: false,
      limit: 500,   //是把小于500B的文件打成Base64的格式,写入JS
      name: 'imgs/[hash:8]-[name].[ext]' // [hash:8] 在名称前面设置8位哈希值,[name] 设置文件的原名, [ext] 设置文件的原后缀
    }
  }]
},// 处理 图片路径的 loader

对比

file-loader 可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存。此外,这意味着 你可以就近管理图片文件,可以使用相对路径而不用担心部署时 URL 的问题。使用正确的配置,webpack 将会在打包输出中自动重写文件路径为正确的 URL。 url-loader 允许你有条件地将文件转换为内联的 base-64 URL (当文件小于给定的阈值),这会减少小文件的 HTTP 请求数。如果文件大于该阈值,会自动的交给 file-loader 处理。

字体

不要把字体也用url-loader 来处理,把字体文件转成base64是浏览器无法识别的

代码语言:javascript复制
{
  test: /.(woff|woff2|eot|ttf|otf)$/i,
  loader: 'file-loader',
  options: {
    esModule: false
  }
}

音频

代码语言:javascript复制
{
  test: /.(mp3)(?.*)?$/,
  loader: 'url-loader',
  options: {
    name:'audios/[name].[ext]',
    limit:10
  }
}

静态文件处理

https://www.webpackjs.com/plugins/copy-webpack-plugin/#install

https://github.com/webpack-contrib/copy-webpack-plugin/tree/v9.1.0

代码语言:javascript复制
npm install copy-webpack-plugin@9 -D

配置

代码语言:javascript复制
//webpack.config.js
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const mCopyWebpackPlugin =  new CopyWebpackPlugin({
  patterns: [
    {
      from: "public",
      to: "./",
      toType: "dir",
      globOptions: {
        ignore: [
          "**/index.html",
        ],
      },
    },
  ],
});

module.exports = {
  plugins:[
    mCopyWebpackPlugin,
  ]
}

注意:

版本不同,配置也不一样。 to配置的相对路径是相对于发布目录的。 如果from所在目录中排除文件后没有文件的时候会报错。

我的配置

package.json

代码语言:javascript复制
{
  "name": "webpack01",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack --mode production"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "~7.18.13",
    "@babel/plugin-transform-runtime": "~7.18.10",
    "@babel/polyfill": "~7.12.1",
    "@babel/preset-env": "~7.18.10",
    "babel-loader": "~8.2.5",
    "cache-loader": "^4.1.0",
    "copy-webpack-plugin": "~9.1.0",
    "css-loader": "~6.7.1",
    "file-loader": "~6.2.0",
    "html-webpack-plugin": "~5.5.0",
    "less": "~4.1.3",
    "less-loader": "~11.0.0",
    "style-loader": "~3.3.1",
    "thread-loader": "~3.0.4",
    "url-loader": "~4.1.1",
    "vue-loader": "~15.10.0",
    "vue-template-compiler": "~2.6.11",
    "webpack": "~5.74.0",
    "webpack-cli": "~4.10.0",
    "webpack-dev-server": "~4.10.0",
    "webpackbar": "~5.0.2"
  },
  "dependencies": {
    "@babel/runtime": "~7.18.9",
    "@babel/runtime-corejs2": "~7.18.9",
    "vue": "~2.6.11"
  }
}

webpack.config.js

代码语言:javascript复制
const path = require('path');

function resolve(dir) {
  return path.join(__dirname, dir);
}

const IS_PRODUCTION = process.env.NODE_ENV === "production";
const {VueLoaderPlugin} = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin');

// 复制插件
const CopyWebpackPlugin = require('copy-webpack-plugin');
const mCopyWebpackPlugin = new CopyWebpackPlugin({
  patterns: [
    {
      from: "public",
      to: "./",
      toType: "dir",
      globOptions: {
        ignore: [
          "**/index.html",
        ],
      },
    },
  ],
});

// 生产配置
const cdn_production = {
  js: ["/librarys/vue@2.6.11/vue.min.js"]
};
// 开发配置
const cdn_development = {
  js: ["/librarys/vue@2.6.11/vue.js"]
};

// 进度条
const WebpackBar = require('webpackbar');
let progressPlugin = new WebpackBar({
  color: "#85d",  // 默认green,进度条颜色支持HEX
  basic: false,   // 默认true,启用一个简单的日志报告器
  profile: false,  // 默认false,启用探查器。
})

module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  externals: {
    "vue": "Vue",
    "vue-router": "VueRouter",
    "vuex": "Vuex",
    "axios": "axios",
  },
  watch: true,
  module: {
    rules: [
      {
        test: /.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /.js$/, exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader', // 开启多进程打包
            options: {
              worker: 3,
            }
          },
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true // 启用缓存
            }
          },
        ]
      },
      {test: /.css$/, use: ['style-loader', 'css-loader']},
      {test: /.less$/, use: ['style-loader', 'cache-loader', 'css-loader', 'less-loader']},
      {
        test: /.(jpg|png|gif|bmp|jpeg|svg)$/,
        use: 'url-loader?esModule=false&limit=500&name=imgs/[hash:8]-[name].[ext]'
      },
      {
        test: /.(woff|woff2|eot|ttf|otf)$/i,
        loader: 'file-loader',
        options: {
          esModule: false
        }
      },
      {
        test: /.(mp3)(?.*)?$/,
        loader: 'url-loader',
        options: {
          name: 'audios/[name].[ext]',
          limit: 10
        }
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      cdn: IS_PRODUCTION ? cdn_production : cdn_development
    }),
    mCopyWebpackPlugin,
    progressPlugin,
  ],
  resolve: {
    extensions: ['.js', '.css', '.json', '.vue'],  //配置后缀名
    alias: {
      '~': resolve('src'),
      '@': resolve('src'),
      'components': resolve('src/components'),
    }
  },
  devServer: {
    port: 8080,
    hot: true,
    open: true,
    static: {
      directory: path.join(__dirname, './'),
      watch: true
    }
  }
}

.babelrc

代码语言:javascript复制
{
  "presets": ["@babel/preset-env"],
    "plugins": [
      [
        "@babel/plugin-transform-runtime",
        {
          "corejs": 2,
          "helpers": true,
          "regenerator": true,
          "useESModules": false
        }
      ]
    ]
}

/public/index.html

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>标题</title>
</head>

<body>
  <div id="app"></div>
</body>
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<script src="/librarys/vue-router@3.2.0/vue-router.min.js"></script>
<script src="/librarys/vuex@3.2.0/vuex.min.js"></script>
<script src="/librarys/axios@0.21.1/axios.min.js"></script>
<style>
  body{
    margin: 0;
    padding: 0;
  }
</style>
</html>

/src/App.vue

代码语言:javascript复制
<template>
  <div class="app">
    <div class="div1">Hello</div>
    <div class="div2">Word</div>
    <img src="/imgs/qrcode.png" alt="">
    <img src="@/assets/imgs/test.png" alt="">
  </div>
</template>

<script>
export default {
  name: "App"
}
</script>

<style lang="less" scoped>

.app{
  background: #f3f3f3;
  width: 100vw;
  height: 100vh;
  font-size: 60px;

  .div1{
    font-size: 60px;
  }

  .div2{
    font-size: 80px;
  }
}
</style>

/src/main.js

代码语言:javascript复制
import App from './App';
import Vue from "vue";
new Vue({
  render: h => h(App)
}).$mount("#app");

优化

优化构建速度

耗时分析

首先安装一下

代码语言:javascript复制
npm i -D speed-measure-webpack-plugin

修改我们的配置文件 webpack.config.js

代码语言:javascript复制
// 费时分析
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
module.exports = {
  plugins: [
    new SpeedMeasurePlugin(),
  ],
}

范围优化

resolve

1、alias

alias 用的创建 importrequire 的别名,用来简化模块引用,项目中基本都需要进行配置。

代码语言:javascript复制
const path = require('path')
// 路径处理方法
function resolve(dir){
  return path.join(__dirname, dir);
}

module.exports = {
  resolve:{
    // 配置别名
    alias: {
      '~': resolve('src'),
      '@': resolve('src'),
      'components': resolve('src/components'),
    }
  }
};

配置完成之后,我们在项目中就可以

代码语言:javascript复制
// 使用 src 别名 ~ 
import '~/fonts/iconfont.css'

// 使用 src 别名 @ 
import '@/fonts/iconfont.css'

// 使用 components 别名
import footer from "components/footer";

2、extensions

webpack 默认配置

代码语言:javascript复制
module.exports = {
  resolve: {
    extensions: ['.js', '.css', '.json', '.vue'],  //配置后缀名
  },
};

如果用户引入模块时不带扩展名,例如

代码语言:javascript复制
import file from '../path/to/file';

那么 webpack 就会按照 extensions 配置的数组从左到右的顺序去尝试解析模块

需要注意的是:

  1. 高频文件后缀名放前面;
  2. 手动配置后,默认配置会被覆盖

如果想保留默认配置,可以用 ... 扩展运算符代表默认配置,例如

代码语言:javascript复制
module.exports = {
  //...
  resolve: {
    extensions: ['.ts', '...'], 
  },
};

3、modules

告诉 webpack 解析模块时应该搜索的目录,常见配置如下

代码语言:javascript复制
const path = require('path');

// 路径处理方法
function resolve(dir){
  return path.join(__dirname, dir);
}

module.exports = {
  //...
  resolve: {
     modules: [resolve('src'), 'node_modules'],
  },
};

告诉 webpack 优先 src 目录下查找需要解析的文件,会大大节省查找时间。

4、resolveLoader

resolveLoader 与上面的 resolve 对象的属性集合相同, 但仅用于解析 webpack 的 loader 包。

一般情况下保持默认配置就可以了,但如果你有自定义的 Loader 就需要配置一下,不配可能会因为找不到 loader 报错。例如:我们在 loader 文件夹下面,放着我们自己写的 loader。我们就可以怎么配置

代码语言:javascript复制
const path = require('path');

// 路径处理方法
function resolve(dir){
  return path.join(__dirname, dir);
}

module.exports = {
  //...
  resolveLoader: {
    modules: ['node_modules',resolve('loader')]
  },
};
externals

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。例如,从 CDN 引入 jQuery,而不是把它打包:

1、引入链接

代码语言:javascript复制
<script
        src="https://code.jquery.com/jquery-3.1.0.js"
        integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
        crossorigin="anonymous">
</script>

2、配置 externals

代码语言:javascript复制
module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};

3、使用 jQuery

代码语言:javascript复制
import $ from 'jquery';

$('.my-element').animate(/* ... */);

我们可以用这样的方法来剥离不需要改动的一些依赖,大大节省打包构建的时间。

缩小范围

在配置 loader 的时候,我们需要更精确的去指定 loader 的作用目录或者需要排除的目录,通过使用 includeexclude 两个配置项,可以实现这个功能,常见的例如:

  • include:符合条件的模块进行解析
  • exclude:排除符合条件的模块,不解析
  • exclude 优先级更高

例如在配置 babel 的时候

代码语言:javascript复制
const path = require('path');

// 路径处理方法
function resolve(dir){
  return path.join(__dirname, dir);
}

module.exports = {
  //...
  module: { 
    noParse: /jquery|lodash/,
    rules: [
      {
        test: /.js$/i,
        include: resolve('src'),
        exclude: /node_modules/,
        use: [
          'babel-loader',
        ]
      },
      // ...
    ]
  }
};
noParse
  • 不需要解析依赖的第三方大型类库等,可以通过这个字段进行配置,以提高构建速度
  • 使用 noParse 进行忽略的模块文件中不会解析 importrequire 等语法
代码语言:javascript复制
module.exports = {
  //...
  module: { 
    noParse: /jquery|lodash/,
    rules:[]
  }
};
IgnorePlugin

防止在 importrequire 调用时,生成以下正则表达式匹配的模块:

  • requestRegExp 匹配(test)资源请求路径的正则表达式。
  • contextRegExp 匹配(test)资源上下文(目录)的正则表达式
代码语言:javascript复制
new webpack.IgnorePlugin({ resourceRegExp, contextRegExp });

以下示例演示了此插件的用法。

1、安装 moment 插件(时间处理库)

代码语言:javascript复制
npm i -S moment

2、配置 IgnorePlugin

代码语言:javascript复制
// 引入 webpack
const webpack = require('webpack')

module.exports = {
  plugins:[ // 配置插件
    new webpack.IgnorePlugin({
      resourceRegExp: /^./locale$/,
      contextRegExp: /moment$/,
    }),
  ]  
};

目的是将插件中的非中文语音排除掉,这样就可以大大节省打包的体积了

多进程

配置在 thread-loader 之后的 loader 都会在一个单独的 worker 池(worker pool)中运行

1、安装

代码语言:javascript复制
npm i -D thread-loader@3.0.4

2、配置

代码语言:javascript复制
const path = require('path');

// 路径处理方法
function resolve(dir){
  return path.join(__dirname, dir);
}

module.exports = {
  //...
  module: { 
    rules: [
      {
        test: /.js$/i,
        include: resolve('src'),
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader', // 开启多进程打包
            options: {
              worker: 3,
            }
          },
          'babel-loader',
        ]
      },
      // ...
    ]
  }
};

缓存

利用缓存可以大幅提升重复构建的速度

JS缓存

babel-loader 开启缓存

  • babel 在转译 js 过程中时间开销比价大,将 babel-loader 的执行结果缓存起来,重新打包的时候,直接读取缓存
  • 缓存位置: node_modules/.cache/babel-loader

配置

代码语言:javascript复制
const path = require('path');

// 路径处理方法
function resolve(dir){
  return path.join(__dirname, dir);
}

module.exports = {
  module: { 
    noParse: /jquery|lodash/,
    rules: [
      {
        test: /.js$/i,
        include: resolve('src'),
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true // 启用缓存
            }
          },
        ]
      },
    ]
  }
}
CSS缓存

cache-loader

  • 缓存一些性能开销比较大的 loader 的处理结果
  • 缓存位置:node_modules/.cache/cache-loader

1、安装

代码语言:javascript复制
npm i -D cache-loader@4.1.0

2、配置 cache-loader

代码语言:javascript复制
module.exports = {
 module: { 
    // ...
    rules: [
      {
        test: /.(s[ac]|c)ss$/i, //匹配所有的 sass/scss/css 文件
        use: [
          'style-loader',
          'cache-loader', // 获取前面 loader 转换的结果
          'css-loader',
          'postcss-loader',
          'sass-loader', 
        ]
      }, 
    ]
  }
}

less

代码语言:javascript复制
{test: /.less$/, use: ['style-loader','cache-loader', 'css-loader', 'less-loader']},
其他

hard-source-webpack-plugin

hard-source-webpack-plugin 为模块提供了中间缓存,重复构建时间大约可以减少 80%,但是在 webpack5 中已经内置了模块缓存,不需要再使用此插件

持久化缓存

通过配置cache缓存生成的 webpack 模块和 chunk,来改善构建速度。

代码语言:javascript复制
module.exports = {
  cache: {
    type: 'filesystem',
  },
};

优化构建结果

优化构建结果是为了让打包出来的文件尽可能小,这样势必会增加构建时间。

结果分析

借助插件webpack-bundle-analyzer我们可以直观的看到打包结果中,文件的体积大小、各模块依赖关系、文件是够重复等问题,极大的方便我们在进行项目优化的时候,进行问题诊断。

1、安装

代码语言:javascript复制
npm i -D webpack-bundle-analyzer

2、配置插件

代码语言:javascript复制
// 引入插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  // ...
  plugins:[ 
    // ...
    // 配置插件 
    new BundleAnalyzerPlugin({
      // analyzerMode: 'disabled',  // 不启动展示打包报告的http服务器
      // generateStatsFile: true, // 是否生成stats.json文件
    })
  ],
};

3、修改启动命令

代码语言:javascript复制
"scripts": {
   "analyzer": "cross-env NODE_ENV=prod webpack --progress --mode production"
 },

4、执行编译命令 npm run analyzer

打包结束后,会自行启动地址为 http://127.0.0.1:8888 的 web 服务

如果,我们只想保留数据不想启动 web 服务,这个时候,我们可以加上两个配置

代码语言:javascript复制
new BundleAnalyzerPlugin({
   analyzerMode: 'disabled',  // 不启动展示打包报告的http服务器
   generateStatsFile: true, // 是否生成stats.json文件
})

这样再次执行打包的时候就只会产生 state.json 的文件了

压缩 CSS

1、安装 optimize-css-assets-webpack-plugin

代码语言:javascript复制
npm install -D optimize-css-assets-webpack-plugin

2、修改 webapck.config.js 配置

代码语言:javascript复制
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      // 添加 css 压缩配置
      new OptimizeCssAssetsPlugin({}),
    ]
  },
}

压缩 JS

在生成环境下打包默认会开启 js 压缩,但是当我们手动配置optimization选项之后,就不再默认对 js 进行压缩,需要我们手动去配置。

因为 webpack5 内置了terser-webpack-plugin插件,所以我们不需重复安装,直接引用就可以了,具体配置如下

代码语言:javascript复制
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  // ...
  optimization: {
    minimize: true, // 开启最小化
    minimizer: [
      // ...
      new TerserPlugin({})
    ]
  },
  // ...
}

清除无用的 CSS

purgecss-webpack-plugin 会单独提取 CSS 并清除用不到的 CSS

1、安装插件

代码语言:javascript复制
$ npm i -D purgecss-webpack-plugin

2、添加配置

代码语言:javascript复制
// ...
const PurgecssWebpackPlugin = require('purgecss-webpack-plugin')
const glob = require('glob'); // 文件匹配模式
// ...

function resolve(dir){
  return path.join(__dirname, dir);
}

const PATHS = {
  src: resolve('src')
}

module.exports = {
  plugins:[ // 配置插件
    // ...
    new PurgecssPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`, {nodir: true})
    }),
  ]
}

3、index.html 新增节点

代码语言:javascript复制
<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">
  <title>ITEM</title>
</head>
<body>
  <p></p>
  <!-- 使用字体图标文件 -->
  <i class="iconfont icon-member"></i>
  <div id="imgBox"></div>
  
   <!-- 新增 div,设置 class 为 used -->
  <div class="used"></div>
</body>
</html>

4、在 sass.scss 中添加样式

代码语言:javascript复制
.used {
  width: 200px;
  height: 200px;
  background: #ccc;
}

.unused {
  background: chocolate;
}

5、执行一下打包

我们可以看到只有 .used 被保存下来

如何证明是这个插件的作用呢?注释掉再打包就可以看到,.unused 也会被打包进去,由此可证…

Tree-shaking

Tree-shaking 作用是剔除没有使用的代码,以降低包的体积

了解更多 Tree-shaking 知识,推荐阅读 从过去到现在,聊聊 Tree-shaking

webpack5tree-shaking 中的配置

打开项目下 package.json, 加入配置 "sideEffects"

sideEffects 有三种情况

  1. sideEffects:true 所有文件都有副作用,全都不可 tree-shaking
  2. sideEffects:false 有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
  3. sideEffects:[] 部分 tree-shaking , 除了数组外都 tree-shaking

所谓 副作用 指的是 在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。

举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

对于某些代码,可能没有被导出和使用,但是却不能删除。

因为仅仅是引入这个文件(比如import './index.less' ),或者执行了某个表达式(比如Array.prototype.slice = null),都会对结果造成影响,所以不能被轻易删除。

webpack认为这些代码是有“副作用(Side Effects)”的。

Scope Hoisting

Scope Hoisting 即作用域提升,原理是将多个模块放在同一个作用域下,并重命名防止命名冲突,通过这种方式可以减少函数声明和内存开销

  • webpack 默认支持,在生产环境下默认开启
  • 只支持 es6 代码

优化运行时体验

运行时优化的核心就是提升首屏的加载速度,主要的方式就是:降低首屏加载文件体积,首屏不需要的文件进行预加载或者按需加载

splitChunks 分包配置

optimization.splitChunks 是基于 SplitChunksPlugin 插件实现的。默认情况下,它只会影响到按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。

webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹
  • 新的 chunk 体积大于 20kb(在进行 min gz 之前的体积)
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30

1、默认配置介绍

代码语言:javascript复制
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 有效值为 `all`,`async` 和 `initial`
      minSize: 20000, // 生成 chunk 的最小体积(≈ 20kb)
      minRemainingSize: 0, // 确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块
      minChunks: 1, // 拆分前必须共享模块的最小 chunks 数。
      maxAsyncRequests: 30, // 最大的按需(异步)加载次数
      maxInitialRequests: 30, // 打包后的入口文件加载时,还能同时加载js文件的数量(包括入口文件)
      enforceSizeThreshold: 50000,
      cacheGroups: { // 配置提取模块的方案
        defaultVendors: {
          test: /[/]node_modules[/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

2、项目中的使用

代码语言:javascript复制
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: { // 配置提取模块的方案
        default: false,
        styles: {
          name: 'styles',
          test: /.(s?css|less|sass)$/,
          chunks: 'all',
          enforce: true,
          priority: 10,
        },
        common: {
          name: 'chunk-common',
          chunks: 'all',
          minChunks: 2,
          maxInitialRequests: 5,
          minSize: 0,
          priority: 1,
          enforce: true,
          reuseExistingChunk: true,
        },
        vendors: {
          name: 'chunk-vendors',
          test: /[\/]node_modules[\/]/,
          chunks: 'all',
          priority: 2,
          enforce: true,
          reuseExistingChunk: true,
        },
        // ... 根据不同项目再细化拆分内容
      },
    },
  },
}

代码懒加载

针对首屏加载不太需要的一些资源,我们可以通过懒加载的方式去实现。

下面看一个小需求:点击图片给图片加一个描述

1、新建图片描述信息 desc.js

代码语言:javascript复制
const ele = document.createElement('div')
ele.innerHTML = '我是图片描述'
module.exports = ele

2、点击图片引入描述 index.js

代码语言:javascript复制
import './main.css';
import './sass.scss'
import logo from '../public/avatar.png'

import '@/fonts/iconfont.css'

const a = 'Hello ITEM'
console.log(a)

const img = new Image()
img.src = logo

document.getElementById('imgBox').appendChild(img)

// 按需加载
img.addEventListener('click', () => {
  import('./desc').then(({ default: element }) => {
    console.log(element)
    document.body.appendChild(element)
  })
})

prefetch 与 preload

上面我们使用异步加载的方式引入图片的描述,但是如果需要异步加载的文件比较大时,在点击的时候去加载也会影响到我们的体验,这个时候我们就可以考虑使用 prefetch 来进行预拉取

prefetch

prefetch (预获取):浏览器空闲的时候进行资源的拉取

改造一下上面的代码

代码语言:javascript复制
// 按需加载
img.addEventListener('click', () => {
  import( /* webpackPrefetch: true */ './desc').then(({ default: element }) => {
    console.log(element)
    document.body.appendChild(element)
  })
})

preload

  • preload (预加载):提前加载后面会用到的关键资源
  • 因为会提前拉取资源,如果不是特殊需要,谨慎使用

官网示例:

代码语言:javascript复制
import(/* webpackPreload: true */ 'ChartingLibrary');

其他插件

构建进度条插件

代码语言:javascript复制
npm i -D webpackbar@5.0.2

配置

代码语言:javascript复制
const WebpackBar = require('webpackbar');
let progressPlugin = new WebpackBar({
  color: "#85d",  // 默认green,进度条颜色支持HEX
  basic: false,   // 默认true,启用一个简单的日志报告器
  profile:false,  // 默认false,启用探查器。
})
plugins.push(progressPlugin)

当然里面还有一个属性就是reporters还没有写上,可以在里面注册事件,也可以理解为各种钩子函数。

如下:

代码语言:javascript复制
{ 
    start(context) {
      // 在(重新)编译开始时调用
      const { start, progress, message, details, request, hasErrors } = context
    },
    change(context) {
      // 在 watch 模式下文件更改时调用
    },
    update(context) {
      // 在每次进度更新后调用
    },
    done(context) {
      // 编译完成时调用
    },
    progress(context) {
      // 构建进度更新时调用
    },
    allDone(context) {
      // 当编译完成时调用
    },
    beforeAllDone(context) {
      // 当编译完成前调用
    },
    afterAllDone(context) {
      // 当编译完成后调用
    },
}

当然多数情况下,我们并不会使用这些,基本默认就足够了。

0 人点赞