vue脚手架多页自动化生成实践

news2024/11/17 9:28:51

在这里插入图片描述

前言

在前端开发过程中,常常面对多种业务场景。到目前为止,前端对于不同场景的处理通常会采用不同的渲染方案来组合处理,常见的渲染方案包括:CSR(Client Side Rendering)、SSR(Server Side Rendering)、SSG(Static Site Generation)、ISR(Incremental Site Rendering)、DPR(Distributed Persistent Rendering)、NSR(Native Side Rendering)以及ESR(Edge Side Rendering)等。在目前项目开发过程中,遇到了需要构建门户类应用的需求,而团队主要技术栈以Vue为主,整个技术方案以Vue全家桶进行构建。因此,本文旨在针对门户类应用的场景下的Vue脚手架构建方案的一些总结和分析,通过自动化的配置脚本来生成模板化的多页应用实践,以期能够给读者提供一个基于Vue全家桶的门户类工程构建方案。

架构

图片

对于门户类型的应用,由于其大部分内容变动内容较少,而对于部分关键页面却会有动态更新的要求,因而在通常会采用多页形式的处理配合部分单页应用中的优势进行处理。因而,在技术选型方面,团队采用了预渲染配合多页的方式实现门户类SEO及首屏加载快的需求。同时,结合单页应用的优势,在多页中的部分关键页面中采用单页中的优点,如:路由切换快、用户体验好等。综上,架构风格采用ISR的增量渲染方案,由于项目背景的特殊性,无法配合常规CDN等部署方案特点,但可以使用云原生相关的中间件实现类似效果,整体部署仍以“云+端”的形式为主。

目录

selfService├─portal
├─ build                                // vue cli打包所需的options中内容一些抽离,对其中做了环境区分
|   ├─ demo
|   |    ├─config.json
|   |    ├─configureWebpack.js
|   ├─ dev
|   |    ├─ config.json
|   |    ├─ configureWebpack.js
|   ├─ production
|   |    ├─ config.json
|   |    ├─ configureWebpack.js
|   ├─ chainWebpack.js
|   ├─ configureWebpack.js
|   ├─ devServer.js
|   ├─ pages.js
|   ├─ routes.js
|   ├─ index.js
|   ├─ utils.js
├─ deploy                                 // 不同环境的部署
|   ├─ demo
|   |    ├─ default.conf
|   |    ├─ Dockerfile
|   |    ├─ env.sh
|   ├─ dev
|   |    ├─ default.conf
|   |    ├─ Dockerfile
|   |    ├─ env.sh
|   ├─ production
|   |    ├─ default.conf
|   |    ├─ Dockerfile
|   |    ├─ env.sh
|   ├─ build.sh
├─ public
|   ├─ pageA                              // pageA的html,这里可以存放一些静态资源,非构建状态下的js、css等
|   |    ├─ index.html
|   ├─ pageB                              // pageB的html,这里可以存放一些静态资源,非构建状态下的js、css等
|   |    ├─ index.html
|   ├─ favicon.ico
├─ src
|   ├─ assets                             // 存放小资源,通常为必须,如:logo等,其他静态资源请放入cdn或者public下
|   |    ├─ logo.png
|   ├─ components                         // 公共组件,可抽离多个静态页面的公共组件
|   |    ├─ Header.vue
|   ├─ router
|   |    ├─ pageA                         // pageA的router,使用了history模式
|   |        ├─ index.js
|   |    ├─ pageB                         // pageB的router,使用了history模式
|   |        ├─ index.js
|   ├─ store
|   |    ├─ pageA                         // pageA的Vuex
|   |        ├─ index.js
|   |    ├─ pageB                         // pageB的Vuex
|   |        ├─ index.js
|   ├─ views
|   |    ├─ pageA                         // pageA的页面,写法和之前一个的单页应用一致
|   |        ├─ main.js                   // 注入了mode,挂载到了vue的原型上,使用this可以获取环境变量
|   |        ├─ pageA.vue
|   |    ├─ pageB                         // pageB的页面,写法和之前一个的单页应用一致
|   |        ├─ main.js                   // 注入了mode,挂载到了vue的原型上,使用this可以获取环境变量
|   |        ├─ pageB.vue
├─ scripts                 
├─ babel.config.js                        // 配置es转化语法
├─ vue.config.js                          // vue cli打包相关配置
├─ app.json                               // 存放各个多页应用的public、router、vuex、views入口地址

实践

配置

在这里插入图片描述

Vue脚手架中配置多页主要是使用Webpack中的pages入口配置,这里主要是修改vue.config.js中的pages的设置,代码如下:

const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
module.exports = {
    // ...
    pages: {
        page3:{
            entry: "src/views/page3/main.js",
            template: "public/page3/index.html",
            filename: "page3.html",
            title: "page3"
        }
    },
    configureWebpack: config => {
         config.plugins.push(
            new PrerenderSPAPlugin({
                staticDir: path.resolve(__dirname,'../../dist'),
                routes: [
                    '/page3'
                ],
                renderer: new Renderer({
                    less: false,
                    //renderAfterDocumentEvent: 'render-event',
                    //renderAfterTime: 5000,
                    //renderAfterElementExists: 'my-app-element'
                }),
            })
        )
    } 
}

其中,如果配置了pages,@vue/cli-service会先清除原有的entry,如果没有index,则devServer默认入口的根路径’/‘仍为index.html;如果有index的key值,则会进行相应的覆盖。在这里,对pages下的key值为对应多页的路径,如:上述代码下的page3,则对应的路径为’/page3.html’;pages下的value可以为字符串,也可以为对象,其中:entry为多页的入口(必选项)、template为模板来源、filename为打包后的输出名称以及title会通过html-webpack-plugin的插件对template中的<title><%= htmlWebpackPlugin.options.title %></title>进行替换。

而对于预渲染的应用,这里使用了prerender-spa-plugin和vue-meta-info来进行SEO及首屏加载优化,代码如下:

// ...
import MetaInfo from 'vue-meta-info'

Vue.use(MetaInfo)

new Vue({
    router,
    store,
    render: h => h(index),
    mounted () {
        document.dispatchEvent(new Event('custom-render-trigger'))
    }
}).$mount('#page3')

脚本

在这里插入图片描述

通过上述的配置,基本就可以实现一个 预渲染+多页 的vue脚手架搭建。但是,除了开发环境的配置,对于生产环境、部署等也需要进行一定的设置,这样频繁的操作就会带来一定的功效降低。因而,在前端工程化领域中,通常会进行一定的脚本化或者说脚手架方案的构建。这里,在目前项目中,团队对多页应用的配置进行了自动化的脚本实现。

生成多页的脚本主要通过page.js进行实现,代码如下:

const inquirer  = require('inquirer');
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const ora = require('ora');
const { transform, compose } = require('./utils');

const spinner = ora();

const PAGE_REG = /[a-zA-Z_0-9]/ig;
const rootDir = path.resolve(process.cwd(), '.');

// 判断dir目录下是否存在name的文件夹
const isExt = (dir, name)  => fs.existsSync(path.join(dir, name));

const APP_JSON_EJS = `{
    "pages": <%= page_name %> 
}`;

const INDEX_HTML_EJS = `<!DOCTYPE html>
<html lang="">
  <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">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <%= page_name %>
  </body>
</html>
`

const INDEX_VUE_EJS = `<%= page_name %>

<script>
export default {
components: {
},
data() {
  return {};
},
};
</script>

<style lang="less">
</style>`

const MAIN_JS_EJS = `<%= page_name %>`

const INDEX_ROUTER_EJS = `import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
    <%= page_name %>
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router`

const INDEX_STORE_EJS = `import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})
`

// inquirer list
const promptList = [
    {
        type: 'input',
        name: 'page_name',
        message: '请输入你想要创建的多页应用名称',
        filter: function (v) {
            return v.match(PAGE_REG).join('')
        }
    }
];

// nginx的default.conf所需添加内容
const addDefaultConf = page_name => {
    return `location /${page_name} {
    root   /usr/share/nginx/html;
    index  ${page_name}.html;
    try_files $uri $uri/ /${page_name}.html;
    gzip_static on;
}`
};

// page_name下的index.html
const addIndexHtml = page_name => {
    return `<div id="${page_name}" data-server-rendered="true"></div>`
};

// page_name下的router
const addRouterIndex = page_name => {
    return `{
    path: '/',
    component: () => import('../../views/${page_name}/index.vue')
},`
};

// page_name下的views index.vue
const addViewsIndex = page_name => {
    return `<template>
    <div>
        ${page_name}
    </div>
</template>`
};

// page_name下的views main.js
const addViewsMain = page_name => {
    return `import Vue from 'vue'
import index from './index.vue'
import router from '../../router/${page_name}/index.js'
import store from '../../store/${page_name}/index.js'
import MetaInfo from 'vue-meta-info'

Vue.use(MetaInfo)

import axios from 'axios'

Vue.prototype.$mode = process.env.VUE_APP_MODE;

Vue.prototype.axios = axios;

Vue.config.productionTip = false

new Vue({
    router,
    store,
    render: h => h(index),
    mounted () {
        document.dispatchEvent(new Event('custom-render-trigger'))
    }
}).$mount('#${page_name}')`
};

// page_name下的pages.js
const addPages = page_name => {
    return JSON.stringify({
        entry: `src/views/${page_name}/main.js`,
        template: `public/${page_name}/index.html`,
        filename: `${page_name}.html`,
        title: `${page_name}`,
    })
}

const updateApp = page_name => {
    // 获取pages的数组
    const pages = require('../app.json')['pages'];
    if(pages.includes(page_name)) return true;
    pages.push(page_name);
    spinner.start()
    fs.writeFile(`${rootDir}/app.json`, transform(/<%= page_name %>/g, JSON.stringify(pages), APP_JSON_EJS), err => {
        spinner.color = 'red';
        spinner.text = 'Loading Update app.json'
        if(err) {
            spinner.fail(chalk.red(`更新app.json失败`))
            return false;
        } else {
            spinner.succeed(chalk.green(`更新app.json成功`))
            return true;
        }
    });
}

// 处理 public 文件夹下的核心逻辑
const processPublic = args => {
    const { page_name } = args;
    if(isExt(`${rootDir}/public`, page_name)) {
        return args;
    } else {
        fs.mkdirSync(`${rootDir}/public/${page_name}`)
    }
    fs.writeFileSync(
        `${rootDir}/public/${page_name}/index.html`, 
        transform(/<%= page_name %>/g, addIndexHtml(page_name), INDEX_HTML_EJS)
    );
    // 处理默认页面的跳转
    const content = require('../app.json')['pages'].map(page => {
        return `<li>
    <a href="/${page}.html">${page}</a>
</li>`
    }).join(`
`);
    const ejs_arr = fs.readFileSync(`${rootDir}/public/index.html`, 'utf-8').split(`<body>`);
    fs.writeFileSync(
        `${rootDir}/public/index.html`, 
        ejs_arr[0] + `<body>
`+`<h1>自服务门户</h1>
<ul>
    ${content}
</ul>` + `
</body>
</html>`
    );
    return args;
};

// 处理 src/views 文件夹下的核心逻辑
const processViews = args => {
    const { page_name } = args;
    if(isExt(`${rootDir}/src/views`, page_name)) {
        return args;
    } else {
        fs.mkdirSync(`${rootDir}/src/views/${page_name}`)
    }
    fs.writeFileSync(
        `${rootDir}/src/views/${page_name}/index.vue`, 
        transform(/<%= page_name %>/g, addViewsIndex(page_name), INDEX_VUE_EJS)
    );
    fs.writeFileSync(
        `${rootDir}/src/views/${page_name}/main.js`, 
        transform(/<%= page_name %>/g, addViewsMain(page_name), MAIN_JS_EJS)
    );
    return args;
};

// 处理 src/router 文件夹下的核心逻辑
const processRouter = args => {
    const { page_name } = args;
    if(isExt(`${rootDir}/src/router`, page_name)) {
        return args;
    } else {
        fs.mkdirSync(`${rootDir}/src/router/${page_name}`)
    }
    fs.writeFileSync(
        `${rootDir}/src/router/${page_name}/index.js`, 
        transform(/<%= page_name %>/g, addRouterIndex(page_name), INDEX_ROUTER_EJS)
    );
    return args;
};

// 处理 src/store 文件夹下的核心逻辑
const processStore = args => {
    const { page_name } = args;
    if(isExt(`${rootDir}/src/store`, page_name)) {
        return args;
    } else {
        fs.mkdirSync(`${rootDir}/src/store/${page_name}`)
    }
    fs.writeFileSync(
        `${rootDir}/src/store/${page_name}/index.js`, 
        INDEX_STORE_EJS
    );
    return args;
};

// 处理 build 文件夹下的核心逻辑
const processBuild = args => {
    const { page_name } = args;
    // 处理 build/page.js
    const pages  = require('../build/pages.js');
    if(Object.keys(pages).includes(page_name)) return args;
    pages[`${page_name}`] = JSON.parse(addPages(page_name));
    const PAGES_JS_EJS =`const pages = ${JSON.stringify(pages)}
module.exports = pages;
    `;
    fs.writeFileSync(
        `${rootDir}/build/pages.js`, 
        PAGES_JS_EJS
    );

    // 处理 build/routes.js
    const routes = require('../build/routes.js');
    if(routes.includes(`/${page_name}`)) return args;
    routes.push(`/${page_name}`);
    const ROUTES_JS_EJS =`const pages = ${JSON.stringify(routes)}
module.exports = pages;
    `;
    fs.writeFileSync(
        `${rootDir}/build/routes.js`, 
        ROUTES_JS_EJS
    );
    return args;
}

// 处理 deploy 文件夹下的核心逻辑
const processDeploy = args => {
    const { page_name } = args;
    const reg = new RegExp(`location /${page_name}`);
     ['demo', 'dev', 'production'].forEach(item => {
        const content = fs.readFileSync(`${rootDir}/deploy/${item}/default.conf`, 'utf-8');
        if(reg.test(content)) return args;
        const ejs_arr = content.split(`location  /api/`)
        fs.writeFileSync(
            `${rootDir}/deploy/${item}/default.conf`, 
            transform(/<%= page_name %>/g, addDefaultConf(page_name), ejs_arr[0] + `<%= page_name %>
location  /api/`+ ejs_arr[1])
        );
    });
    return args;
};

inquirer
    .prompt(promptList)
    .then(answers => {
        const page_name = answers.page_name;
        return updateApp(page_name)
    })
    .then(() => {
        const pages = require('../app.json')['pages'];
        pages.forEach(page => {
            console.log('page', page)
            compose(
                processDeploy,
                processBuild, 
                processStore, 
                processRouter, 
                processViews, 
                processPublic
            )({
                page_name: page
            });
        })
    })
    .catch(err => {
        if(err) {
            console.log(chalk.red(err))
        }
    })

为了更好的实现代码的优雅性,对代码工具进行了抽离,放入到utils.js中,代码如下:

// 将内容替换进ejs占位符
const transform = ($, content, ejs) => ejs.replace($,content);

// 将流程串联
const compose = (...args) => args.reduce((prev,current) => (...values) => prev(current(...values)));

module.exports = {
    transform,
    compose
}

总结

仅管到目前为止,单页应用仍是前端开发中的主流方案。但是,随着各大应用的复杂度提升,多种方案的建设也都有了来自业界不同的声音,诸如:多种渲染方案、Island架构等都是为了能更好的提升Web领域的体验与开发建设。技术方案的选择不只局限于生态的整合,更重要的是对合适场景的合理应用。

“形而上者谓之道,形而下者谓之器”,各位前端开发者不仅应该只着眼于眼前的业务实现,同时也需要展望未来,站在更高的视野上来俯视技术的走向与演进,共勉!!!

参考

  • vue-cli搭建自动化多页面项目(vue高阶)
  • Vue-cli配置多页面
  • vue预渲染之prerender-spa-plugin插件应用
  • 预渲染插件prerender-spa-plugin生成多页面
  • CSR、SSR、NSR、ESR傻傻分不清楚,一文帮你理清前端渲染方案!
  • vue项目改造SSR(服务端渲染)
  • 什么是SSR/SSG/ISR?如何在AWS上托管它们?
  • 卷起来,前端建站SSG,SSR,ISR,Hydration, Island…一网打尽
  • 你知道吗?SSR、SSG、ISR、DPR 有什么区别?
  • SSR、ISR、CSR、SSG有什么区别
  • 一文看懂Next.js渲染方法:CSR、SSR、SSG和ISR

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/375846.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年房地产行业研究报告

第一章 行业发展概况 房地产业是指以土地和建筑物为经营对象&#xff0c;从事房地产开发、建设、经营、管理以及维修、装饰和服务的集多种经济活动为一体的综合性产业&#xff0c;是具有先导性、基础性、带动性和风险性的产业。主要包括&#xff1a;土地开发&#xff0c;房屋的…

Vue3 -- PDF展示、添加签名(带笔锋)、导出

文章目录笔锋签名方案一实现要点实现过程组件引用页面元素添加引用实现代码效果展示缺点方案二修改页面元素替换引用修改代码效果展示完整代码地址实现功能的时候采用了两个方案&#xff0c;主要是第一个方案最后的实现效果并不太理想&#xff0c;但实现起来比较简单&#xff0…

JavaScript手写题

一、防抖 function debounce(fn, delay200) {let timeout null; // 定时器控制return function(...args) {if (timeout) { // 定时器存在&#xff0c;表示某个动作之前触发过了clearTimeout(timeout); // 清除定时器timeout null;} else {// 对第一次输入立即执行fn.apply…

【Leetcode 剑指Offer】第 5 天 查找算法(中等)

查找算法剑指 Offer 04. 二维数组中的查找剑指 Offer 11. 旋转数组的最小数字剑指 Offer 50. 第一个只出现一次的字符Python字典基础哈希表&#xff08;python中是dict()&#xff09;有序哈希表第一个中等&#xff0c;后两个简单题。剑指 Offer 04. 二维数组中的查找 题&#…

Node.js的研究和应用

首先我们要知道什么是node.js? Node.js采用谷歌的V8引擎&#xff0c;是一个服务器端的、非阻断式I/O的、事件驱动的 JavaScript运行环境。 一、Node.js架构具体包含什么呢&#xff1f;咱们从一个图来看看会更加明白。 natives modules&#xff1a; 1.当前层内容由js实现 。…

不使用implements关键字实现实现类(类似于mapper)

首先&#xff0c;说明一下功能需求&#xff0c;平时定义一个接口&#xff0c;就要使用implements关键字来实现接口。那么&#xff0c;当不使用此关键字的时候&#xff0c;是否也能使相关接口也能够绑定实现类呢&#xff1f; 答案是肯定的。 此篇文章的主要功能有两个&#xf…

知识类问答数据集资源对外开放:百万级百度知道、社区问答及六大领域级小规模语料概述

随着chatgpt的火热&#xff0c;中文知识类问答数据集由收到诸多关注&#xff0c;其作为高质量的QA数据&#xff0c;可以用于SFT阶段以及pretrain预训练阶段。 本文主要介绍目前开源可下载的两个较大规模的知识类数据集&#xff0c;包括147万百度知道知识类数据集、425万社区问…

IOS逆向前期环境准备笔记

ios系统由于效验问题&#xff0c;只能升级不能降级&#xff0c;需要特别注意&#xff0c; 刷系统可以在爱思上搞定&#xff1b; 越狱推荐使用u盘镜像及本地启动盘制作&#xff1a; 注意&#xff0c;要进去bios,关闭安全启动&#xff0c;不然直接失败&#xff1a; Checkra1n镜…

linux升级gcc版本详细教程

0.前言一般linux操作系统默认的gcc版本都比较低&#xff0c;例如centos7系统默认的gcc版本为4.8.5。gcc是从4.7版本开始支持C11的&#xff0c;4.8版本对C11新特性的编译支持还不够完善&#xff0c;因此如果需要更好的体验C11以及以上版本的新特性&#xff0c;需要升级gcc到一个…

九、Vben之可拖拽穿梭框和水印背景如何添加

近期在开发的过程中遇到了两个难题&#xff0c;一个是目前的穿梭框不支持产品的要求&#xff0c;不能够上下拖拽&#xff0c;二是vben没有水印的api&#xff0c;需要我们自己来开发。 一、可拖拽穿梭框 做成功的效果如下&#xff1a; 思路&#xff1a; 将table放入transfer的…

操作系统权限提升(十七)之绕过UAC提权-Windows令牌概述和令牌窃取攻击

系列文章 操作系统权限提升(十二)之绕过UAC提权-Windows UAC概述 操作系统权限提升(十三)之绕过UAC提权-MSF和CS绕过UAC提权 操作系统权限提升(十四)之绕过UAC提权-基于白名单AutoElevate绕过UAC提权 操作系统权限提升(十五)之绕过UAC提权-基于白名单DLL劫持绕过UAC提权 操作系…

android EditText设置后缀

有两种实现方案。 方案一&#xff1a;是自己写一个TextWatcher。 方案二&#xff1a;是重写TextView的getOffsetForPosition方法&#xff0c;返回一个计算好的offset。 我在工作时&#xff0c;使用的是方案一。在离职之后&#xff0c;我还是对这个问题耿耿于怀&#xff0c;所以…

git在工作中的正常使用

开发A和B功能后进行发版。。 一、拉取代码 git clone http://ntc.ntsvars.com:8090/lvweijie/test.git二、开发功能A任务 创建A任务本地分支 #创建A分支&#xff0c;并切换A分支 git checkout -b A三、开发A任务 四、提交A功能文件到本地分支 git add .五、添加提交A功能备…

Tina_Linux打包流程说明指南_new

OpenRemoved_Tina_Linux_打包流程_说明指南_new 1 概述 1.1 编写目的 介绍Allwinner 平台上打包流程。 1.2 适用范围 Allwinner 软件平台Tina v3.0 版本以上。 1.3 相关人员 适用Tina 平台的广大客户&#xff0c;想了解Tina 打包流程的开发人员。 2 固件打包简介 固件…

Jenkins+Gitlab实现代码自动构建部署

一、环境准备 主机名ip安装软件jenkins192.168.75.149jenkinsgitlab192.168.75.147gitlabweb192.168.75.155部署应用 二、jenkins服务器配置 1、生产公钥 [rootjenkins ~]# ssh-keygen &#xff08;2&#xff09;获取公钥信息 公钥信息在配置 Gitlab SSH Keys 时用到。 &am…

Tcpdump抓包验证zookeeper的心跳机制

一、背景 在分布式系统中&#xff0c;zookeeper可以作为服务注册中心&#xff0c;所有提供服务的节点都可以在zookeeper上面注册&#xff0c;并作为一个node被组织起来&#xff0c;如下图&#xff1a; 在RPC框架中&#xff0c;这些服务提供者就是RPC服务的提供者。zookeeper注…

【测试】Python手机自动化测试库uiautomator2和weditor的详细使用

1.说明 我们之前在电脑操作手机进行自动化测试&#xff0c;基本上都是通过Appium的&#xff0c;这个工具确实强大&#xff0c;搭配谷歌官方的UiAutomator基本上可以完成各种测试&#xff0c;但缺点也很明显&#xff0c;配置环境太麻烦了&#xff0c;需要jdk、sdk等&#xff0c…

利用较新版本的IDEA 2022.3.2 创建Java Web的maven项目

1.创建项目 正常三步走&#xff0c;没什么可说的 2.用模板创建项目&#xff08;重要&#xff09; 第一步&#xff0c;一定要选Jakarta EE。这个模板是基于JavaWeb的一个标准模板&#xff0c;如果选了maven中的JavaWeb模板&#xff0c;那就变成了web目录在根目录下&#xff0c;…

测试跟踪模块UX交互升级,多个X-Pack功能开放至开源版,MeterSphere开源持续测试平台v2.7.0发布

2023年2月24日&#xff0c;MeterSphere一站式开源持续测试平台正式发布v2.7.0版本。 在这一版本中&#xff0c;MeterSphere在测试跟踪模块进行了UX交互升级&#xff0c;整个页面采用轻量化设计进行整体降噪&#xff0c;页面信息更加清晰易懂&#xff0c;操作流程更顺畅&#x…

【学习笔记】深入理解JVM之类加载机制

【学习笔记】深入理解JVM之类加载机制 以后基本上都在语雀上面更新&#xff0c;大家有兴趣可以看看嗷&#xff01; 首发地址&#xff1a; 知识库 文章流程图&#xff1a; 1、概述 首先我们先来看看一个 Class 文件所需要经过的一个流程图&#xff1a; 而我们今天要重点需讲的…