前言: 大家好,本次学习的是将Vue2项目升级至Vue3项目,并通过webpack工具对项目构建以及打包以可视化的方式进行性能优化。首先在开始之前思考一个小问题:

ue-cli在本地开发模式下,为什么采用express启动静态资源服务器?

  • 解决线上部署后的资源路径问题
  • 解决history模式下的URL fallback问题

进入正题!

一:项目初始化

克隆需要升级的项目

git clone https://github.com/bailicangdu/vue2-elm.git
cd vue-elm-master

将项目资料下载下后进行下载依赖确保项目可以正常启动!

小瑜在学习中发现node-scssb依赖报错,尝试使用npm工具安装依赖,并且将node版本降到14 或者16再将package.json中的对应的node-cscc版本锁定删除 再npm install 即可!

升级Vue依赖

"vue":"^3.0.0",
"vue-router":"^4.0.0-0",
"vuex": "^4.0.0-0"

配置启动插件和编译依赖:安装 vue3 的启动包 @vue/cli-service 和编译包 @vue/compiler-sfc @vue/component-compiler-util

npm install @vue/cli-service @vue/compiler-sfc @vue/component-compiler-utils -D

设置路径别名

  • 创建vue.config.js文件

vueclic 设置路径参考 cli.vuejs.org/zh/guide/we… 参考webpack.base.conf.js中的resolve:alias

const path = require("path");
// vue.config.js
module.exports = {
  configureWebpack: {
    resolve: {
      alias: {
        src: path.resolve(__dirname, "./src"),
        assets: path.resolve(__dirname, "./src/assets"),
        components: path.resolve(__dirname, "./src/components"),
      },
    },
  },
};

此时启动项目成功但是网页报错,说明vue2的项目及时升级vue3构建依赖后,任然无法正常打开

手摸手将Vue2升级Vue3 + 打包构建优化

升级vuex

import { createStore } from "vuex";
export default createStore({
  state,
  getters,
  actions,
  mutations,
});  

升级vue-router

import App from "../App.vue";
import { createRouter, createWebHashHistory } from "vue-router";
const routes = [
  {
    path: "/",
    component: App, //顶层路由,对应index.html
    children: [
      //二级路由。对应App.vue
      //地址为空时跳转home页面
      {
        path: "",
        redirect: "/home",
      },
      //首页城市列表页
      {
        path: "/home",
        component: () => import("../page/home/home"),
      },
      //当前选择城市页
      {
        path: "/city/:cityid",
        component: () => import("../page/city/city"),
      },
      //所有商铺列表页
      {
        path: "/msite",
        component: () => import("../page/msite/msite"),
        meta: { keepAlive: true },
      },
      //特色商铺列表页
      {
        path: "/food",
        component: () => import("../page/food/food"),
      },
      //搜索页
      {
        path: "/search/:geohash",
        component: () => import("../page/search/search"),
      },
      //商铺详情页
      {
        path: "/shop",
        component: shop,
        children: [
          {
            path: "foodDetail", //食品详情页
            component: () => import("../page/shop/children/foodDetail"),
          },
          {
            path: "shopDetail", //商铺详情页
            component: () => import("../page/shop/children/shopDetail"),
            children: [
              {
                path: "shopSafe", //商铺安全认证页
                component: () =>
                  import("../page/shop/children/children/shopSafe"),
              },
            ],
          },
        ],
      },
      //确认订单页
      {
        path: "/confirmOrder",
        component: () => import("../page/confirmOrder/confirmOrder"),
        children: [
          {
            path: "remark", //订单备注
            component: () => import("../page/confirmOrder/children/remark"),
          },
          {
            path: "invoice", //发票抬头
            component: () => import("../page/confirmOrder/children/invoice"),
          },
          {
            path: "payment", //付款页面
            component: () => import("../page/confirmOrder/children/payment"),
          },
          {
            path: "userValidation", //用户验证
            component: () =>
              import("../page/confirmOrder/children/userValidation"),
          },
          {
            path: "chooseAddress", //选择地址
            component: () =>
              import("../page/confirmOrder/children/chooseAddress"),
            children: [
              {
                path: "addAddress", //添加地址
                component: () =>
                  import("../page/confirmOrder/children/children/addAddress"),
                children: [
                  {
                    path: "searchAddress", //搜索地址
                    component: () =>
                      import(
                        "../page/confirmOrder/children/children/children/searchAddress"
                      ),
                  },
                ],
              },
            ],
          },
        ],
      },
      //登录注册页
      {
        path: "/login",
        component: () => import("../page/login/login"),
      },
      //个人信息页
      {
        path: "/profile",
        component: () => import("../page/profile/profile"),
        children: [
          {
            path: "info", //个人信息详情页
            component: () => import("../page/profile/children/info"),
            children: [
              {
                path: "setusername",
                component: () =>
                  import("../page/profile/children/children/setusername"),
              },
              {
                path: "address",
                component: () =>
                  import("../page/profile/children/children/address"),
                children: [
                  {
                    path: "add",
                    component: () =>
                      import("../page/profile/children/children/children/add"),
                    children: [
                      {
                        path: "addDetail",
                        component: () =>
                          import(
                            "../page/profile/children/children/children/children/addDetail"
                          ),
                      },
                    ],
                  },
                ],
              },
            ],
          },
          {
            path: "service", //服务中心
            component: () => import("../page/service/service"),
          },
        ],
      },
      //修改密码页
      {
        path: "/forget",
        component: () => import("../page/forget/forget"),
      },
      //订单列表页
      {
        path: "/order",
        component: () => import("../page/order/order"),
        children: [
          {
            path: "orderDetail", //订单详情页
            component: () => import("../page/order/children/orderDetail"),
          },
        ],
      },
      //vip卡页
      {
        path: "/vipcard",
        component: () => import("../page/vipcard/vipcard"),
        children: [
          {
            path: "invoiceRecord", //开发票
            component: () => import("../page/vipcard/children/invoiceRecord"),
          },
          {
            path: "useCart", //购买会员卡
            component: () => import("../page/vipcard/children/useCart"),
          },
          {
            path: "vipDescription", //会员说明
            component: () => import("../page/vipcard/children/vipDescription"),
          },
        ],
      },
      //发现页
      {
        path: "/find",
        component: () => import("../page/find/find"),
      },
      //下载页
      {
        path: "/download",
        component: () => import("../page/download/download"),
      },
      //服务中心
      {
        path: "/service",
        component: () => import("../page/service/service"),
        children: [
          {
            path: "questionDetail", //订单详情页
            component: () => import("../page/service/children/questionDetail"),
          },
        ],
      },
      //余额
      {
        path: "balance",
        component: () => import("../page/balance/balance"),
        children: [
          {
            path: "detail", //余额说明
            component: () => import("../page/balance/children/detail"),
          },
        ],
      },
      //我的优惠页
      {
        path: "benefit",
        component: () => import("../page/benefit/benefit"),
        children: [
          {
            path: "coupon", //代金券说明
            component: () => import("../page/benefit/children/coupon"),
          },
          {
            path: "hbDescription", //红包说明
            component: () => import("../page/benefit/children/hbDescription"),
          },
          {
            path: "hbHistory", //历史红包
            component: () => import("../page/benefit/children/hbHistory"),
          },
          {
            path: "exchange", //兑换红包
            component: () => import("../page/benefit/children/exchange"),
          },
          {
            path: "commend", //推荐有奖
            component: () => import("../page/benefit/children/commend"),
          },
        ],
      },
      //我的积分页
      {
        path: "points",
        component: () => import("../page/points/points"),
        children: [
          {
            path: "detail", //积分说明
            component: () => import("../page/points/children/detail"),
          },
        ],
      },
    ],
  },
];
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});
export default router;

入口文件

import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";
import router from "./router/router";
import { routerMode } from "./config/env";
import "./config/rem";
const app = createApp(App);
app.mount("#app");
app.use(store);
app.use(router);

此时页面已经显示,但是提示接口报错

手摸手将Vue2升级Vue3 + 打包构建优化

修改接口地址

这个项目采用了线上地址即可

const response = await fetch(
  "http://cangdu.org:8001" + url,
  requestConfig
);
  • 修改图片请求地址路径 修改为线上接口
if (process.env.NODE_ENV == "development") {
  imgBaseUrl = "//elm.cangdu.org";
} else if (process.env.NODE_ENV == "production") {
  baseUrl = "//elm.cangdu.org";
  imgBaseUrl = "//elm.cangdu.org/img/";
}

此时页面就可以成功进行访问

手摸手将Vue2升级Vue3 + 打包构建优化

二:项目打包构建优化

构建性能优化

  • 构建速度分析:影响构建性能和开发效率
  • 构建体积分析:影响页面访问

构建性能优化常用方法:

  • 通过多进程加快构建速度
  • 通过分包减小构建目标容量
  • 减少构建目标加快构建速度

查看构建速度与体积

构建速度分析: speed-measure-webpack-plugin

**环境变量: cross-env **

npm i -D speed-measure-webpack-plugin // 可以在控制台看到输出
npm i -D cross-env // 环境变量
  • package.json

cross-env MEASURE=true 来控制是否需要开启构建日志

 "scripts": {
    "serve": "cross-env MEASURE=true vue-cli-service serve",
    "local": "cross-env NODE_ENV=local node build/dev-server.js",
    "build": "vue-cli-service build"
  },

手摸手将Vue2升级Vue3 + 打包构建优化

查看打包体积

npm install --save-dev webpack-bundle-analyzer
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
plugins: [new BundleAnalyzerPlugin()],
  • 这里也支持输出的文件类型,以及通过环境变量来控制是否进行输出
new BundleAnalyzerPlugin({
  // 通过环境变量指定是否输出 为 xxx 格式
  analyzerMode: process.env.MEASURE === "true" ? "server" : "disabled",
}),

手摸手将Vue2升级Vue3 + 打包构建优化

多进程/多实例

适合重体力活的时候开启加快打包 js单线程,开启多线程构建速度(特别是多核CPU) require(“os”).cpus() 可以查看电脑CPU线程

thread-loader

webpack.docschina.org/loaders/thr… 使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。 在 worker 池中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能生成新的文件。
  • 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
  • 这些 loader 无法获取 webpack 的配置。

每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。 请仅在耗时的操作中使用此 loader!

// CPU线程数
console.log(require("os").cpus(), "require('os').cpus()");
rules: [
    {
      // js
      test: /.js$/,
      // 排除 node_modules
      exclude: /node_modules/,
      use: [
        {
          loader: "thread-loader",
          options: {
            // 开启几个 worker 进程来处理打包,默认是 os.cpus().length - 1
            // workers: 2,
          },
        },
      ],
    },
  ],
  • 开启和关闭分别需要多长时间

发现,并没有快多少,所以vue-cli专门有个插件提供优化

手摸手将Vue2升级Vue3 + 打包构建优化

使用vue-cli 官方的方法

Type: boolean Default: require(‘os’).cpus().length > 1是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。 cli.vuejs.org/zh/config/#…

module.exports = {
  parallel: true
}

分包 DllPlugin&DllReferencePlugin

DllPlugin: webpack.docschina.org/plugins/dll…
DllReferencePlugin: webpack.docschina.org/plugins/dll… 对于变化几率很小的一些第三方包,没有必要build的时候打包一次,可以把这些第三方包单独抽离出来,提高打包效率。webpack本身是要体现出模块间的依赖关系,当我们将一些包抽离出来后,维护之前的依赖关系就需要manifest.json这个文件,将vue、vue-router、vuex等基础包和业务基础包打包成一个文件,使用DLLPlugin进行分包,DllReferencePlugin对manifest.json引用。manifest.json是对分离出来的包的描述。

分包具体步骤:

  • 分包: 定义webpack.dll.config.js,使用DllPlugin配置分包,定义scripts命令,执行命令,完成分包
  • 拆除分包:在vue.config.js中,视同DllRefernecePlugin引用manifest文件拆除分包
  • 拷贝dll: 将dll拷贝至项目目录下
  • 引用dll: 将add-assent-html-webpack-plugin引用分包文件

定义分包

// 使用分包DllPlugin 将变化几率很小的的第三方包单独抽离
// 这里的配置不放在vue.config.js的原因是因为这里的分包要先单独打出来
const path = require("path");
const { DllPlugin } = require("webpack");
// 输出路径
const dllPath = "../dll";
module.exports = {
  // 环境
  mode: "production",
  // 指定那些第三方包进行抽离分包
  entry: {
    vue: ["vue", "vue-router", "vuex"],
  },
  // 输出
  output: {
    path: path.resolve(__dirname, dllPath),
    filename: "[name].dll.js",
    // window引用时找到这个全局变量
    library: "[name]_[hash]",
  },
  plugins: [
    new DllPlugin({
      // 要生成的manifest文件的名称即路径
      path: path.resolve(__dirname, dllPath, "[name]-manifest.json"),
      // 必须要和output.library中保持一致
      name: "[name]_[hash]",
      context: process.cwd(),
    }),
  ],
};
 "scripts": {
    "serve": "cross-env MEASURE=true vue-cli-service serve",
    "build": "cross-env MEASURE=true vue-cli-service build",
  + "dll":"webpack --config build/webpack.dll.config.js"
  },

执行命令后就可以看到dll目录下出现的三个文件

手摸手将Vue2升级Vue3 + 打包构建优化

拆除分包

如果不做处理,执行build,还会将vue等文件打包,需要使用 DllRefernecePlugin 来拆除

// 拆除分包
new DllReferencePlugin({
  context: __dirname,
  // 指定需要拆分的文件路径
  manifest: path.resolve(__dirname, "./dll/vue-manifest.json"),
}),

通过下图,可以看到和Vue全家桶相关的文件就没有了。

手摸手将Vue2升级Vue3 + 打包构建优化

将生成好的vue.dll.js拷贝至dist目录下

上面步骤是将拆分的包单独成了一个文件夹,dist中并没有当前的包资源,所以需要将这部分内容每次build只要拷贝至dist目录即可

// 拷贝dll 拆分的第三方包文件
      new CopyWebpackPlugin({
        patterns: [
          {
            from: path.resolve(__dirname, "./dll/vue.dll.js"),
            to: path.resolve(__dirname, "./dist/js/vue.dll.js"),
          },
        ],
      }),

手摸手将Vue2升级Vue3 + 打包构建优化

html模版文件引入拆分包的资源

最后在dist中 html还需要引入拆分的包资源 add-asset-html-webpack-plugin插件可以做这件事,并且会将文件自动引入dist文件夹中,所以这里就不需要拷贝了! 如果不成功,将build命令换成 vue-cli-service build vue-cli-service serve

  // 在模版中引入拆分的第三方包
  new AddAssetHtmlWebpackPlugin({
    filepath: path.resolve(__dirname, "./dll/vue.dll.js"),
  }),

手摸手将Vue2升级Vue3 + 打包构建优化
启动项目,也是可以看到拆分后的文件引入
手摸手将Vue2升级Vue3 + 打包构建优化

利用缓存提升二次构建速度

cache默认生成在node_modules/.cache/terser-plugin文件下,通过SHA或者base64编码之前的文件处理结 ,井保存映射关系,万便下一次处理文件时可以查看之前同文件(同内容)是杏有可用缓仔,默认存放在 内存中,可以修改将缓存存放到硬盘中。 背景:Webpack4在运行时是有缓存的,只不过缓存只存在于内存中。所以,一旦Webpack的运行程序被关闭, 这些缓存就丢失了。这就导致我们npm run start/build的时候根本无缓存可用。而在Webpack 5中,cache 配 置除了原本的 true 和 false 外,还增加了许多子配置项.可以将缓存文件存储在硬盘中。

  • type: 缓存类型。值为“monmory”或 “filesystem”,分别代表基于内存的临时缓存
  • cacheDirectory: 缓存目录,node_modules/.cache/webpack
  • name:缓存名称
  • cacheLocation: 缓存正在的存放地址。默认视同的是上述两个属性的组合

使用缓存后,第一次build速度会降低,第二次及以后速度飞快,例如只要692ms。

手摸手将Vue2升级Vue3 + 打包构建优化

purgecss-webpack-plugin去除无用css

npm i -D purgecss-webpack-plugin
npm i -D glob
const glob = require('glob');
const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");
const PATHS = {
  src: path.join(__dirname, "src"),
};
npm i -D purgecss-webpack-plugin
npm i -D glob
const glob = require('glob');
const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");
const PATHS = {
  src: path.join(__dirname, "src"),
};
new PurgeCSSPlugin({
  paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),