前言: 大家好,本次学习的是将Vue2项目升级至Vue3项目,并通过webpack工具对项目构建以及打包已可视化的方式进行性能优化。首先在开始之前思考一个小问题
ue-cli在本地开发模式下,为什么采用express启动静态资源服务器?
- 解决线上部署后的资源路径问题
- 解决history模式下的URL fallback问题
进入正题!
一:项目初始化
克隆需要升级的项目
vue
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
"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
vue
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
vue
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构建依赖后,任然无法正常打开
升级vuex
vue
import { createStore } from "vuex";
export default createStore({
state,
getters,
actions,
mutations,
});
升级vue-router
vue
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;
入口文件
vue
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);
此时页面已经显示,但是提示接口报错
修改接口地址
这个项目采用了线上地址即可
javascript
const response = await fetch(
"http://cangdu.org:8001" + url,
requestConfig
);
- 修改图片请求地址路径 修改为线上接口
javascript
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/";
}
此时页面就可以成功进行访问
二:项目打包构建优化
构建性能优化
- 构建速度分析:影响构建性能和开发效率
- 构建体积分析:影响页面访问
构建性能优化常用方法:
- 通过多进程加快构建速度
- 通过分包减小构建目标容量
- 减少构建目标加快构建速度
查看构建速度与体积
构建速度分析: speed-measure-webpack-plugin
**环境变量: cross-env **
javascript
npm i -D speed-measure-webpack-plugin // 可以在控制台看到输出
npm i -D cross-env // 环境变量
- package.json
cross-env MEASURE=true 来控制是否需要开启构建日志
javascript
"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"
},
查看打包体积
javascript
npm install --save-dev webpack-bundle-analyzer
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
plugins: [new BundleAnalyzerPlugin()],
- 这里也支持输出的文件类型,以及通过环境变量来控制是否进行输出
javascript
new BundleAnalyzerPlugin({
// 通过环境变量指定是否输出 为 xxx 格式
analyzerMode: process.env.MEASURE === "true" ? "server" : "disabled",
}),
多进程/多实例
适合重体力活的时候开启加快打包 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!
javascript
// 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专门有个插件提供优化
使用vue-cli 官方的方法
Type: boolean Default: require('os').cpus().length > 1是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。 cli.vuejs.org/zh/config/#...
javascript
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引用分包文件
定义分包
javascript
// 使用分包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(),
}),
],
};
javascript
"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目录下出现的三个文件
拆除分包
如果不做处理,执行build,还会将vue等文件打包,需要使用 DllRefernecePlugin 来拆除
javascript
// 拆除分包
new DllReferencePlugin({
context: __dirname,
// 指定需要拆分的文件路径
manifest: path.resolve(__dirname, "./dll/vue-manifest.json"),
}),
通过下图,可以看到和Vue全家桶相关的文件就没有了。
将生成好的vue.dll.js拷贝至dist目录下
上面步骤是将拆分的包单独成了一个文件夹,dist中并没有当前的包资源,所以需要将这部分内容每次build只要拷贝至dist目录即可
javascript
// 拷贝dll 拆分的第三方包文件
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, "./dll/vue.dll.js"),
to: path.resolve(__dirname, "./dist/js/vue.dll.js"),
},
],
}),
html模版文件引入拆分包的资源
最后在dist中 html还需要引入拆分的包资源 add-asset-html-webpack-plugin插件可以做这件事,并且会将文件自动引入dist文件夹中,所以这里就不需要拷贝了! 如果不成功,将build命令换成 vue-cli-service build vue-cli-service serve
javascript
// 在模版中引入拆分的第三方包
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, "./dll/vue.dll.js"),
}),
启动项目,也是可以看到拆分后的文件引入
利用缓存提升二次构建速度
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。
purgecss-webpack-plugin去除无用css
javascript
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"),
};