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

前言
在前端开发过程中,常常面对多种业务场景。到目前为止,前端对于不同场景的处理通常会采用不同的渲染方案来组合处理,常见的渲染方案包括: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 = falsenew 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.jsconst 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.jsconst 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
相关文章:
vue脚手架多页自动化生成实践
前言 在前端开发过程中,常常面对多种业务场景。到目前为止,前端对于不同场景的处理通常会采用不同的渲染方案来组合处理,常见的渲染方案包括:CSR(Client Side Rendering)、SSR(Server Side Rendering)、SSG(Static Site Generati…...
【SQL语句优化】
SQL语句优化是提高数据库查询性能的重要手段之一,下面是几种常见的SQL语句优化方法和案例: 减少查询的数据量 减少查询的数据量:使用 WHERE 子句和索引来限制检索行数,只检索需要的行,避免检索全部行数据。 例子&am…...
阿里P8:做测试10年我的一些经验分享,希望你们少走弯路
我是在2015年毕业的,当时是读的普通本科,不上不下的专业水平,毕业的时候,恰好遇到了金融危机。校园招聘里阴差阳错的巧合,让我走上了软件测试工程师的道路。 入职第一天,来了个高大上的讲师,记…...
栈在括号匹配中的应用(栈/链栈 纯C实现)
目录 1 问题背景 2 具体思路 3 代码实现 3.1 顺序栈实现 3.2 链栈实现 1 问题背景 栈的括号匹配问题是指在给定一个字符串(包含多种括号),判断其中的括号是否能够正确匹配,即每个左括号是否有一个对应的右括号与之匹配&#x…...
C语言Switch语句用法
C switch 语句 一个 switch 语句允许测试一个变量等于多个值时的情况。每个值称为一个 case,且被测试的变量会对每个 switch case 进行检查。 语法 C 语言中 switch 语句的语法: switch(expression){case constant-expression :statement(s);break;…...
Curl编码请求参数,API接口请求示例参数
请求参数请求参数:num_iid610947572360 参数说明:num_iid:1688商品ID sales_data:&sales_data1 获取近30天成交数据 agent:&agent1 获取1688分销代发价格数据请求示例 测试入口 Curl PHP PHPsdk JAVA C# Python-- 请求示例 url 默认请求参数已经…...
【C/C++】类型限定符extern、const、Volatile、register
1、extern: 声明一个变量,extern声明的变量没有建立存储空间。 extern int a ; //变量在定义的时候创建存储空间。 ①当我们在编译器中试图运行以下代码,系统会报错。 错误原因是“无法解析外部符号_a”.系统认为变量a是没有开辟内存空间的…...
day54【代码随想录】二刷数组
文章目录前言一、二分查找(力扣724)二、移除元素(力扣27)【双指针】三、有序数组的平方(力扣977)【双指针】四、合并两个有序数组(力扣88)五、长度最小的子数组(力扣209&…...
哪个品牌蓝牙耳机性价比高?性价比高的平价蓝牙耳机推荐
现如今,随着蓝牙技术的进步,蓝牙耳机在人们日常生活中的便捷性更胜从前。越来越多的蓝牙耳机品牌被大众看见、认可。那么,哪个品牌的蓝牙耳机性价比高?接下来,我给大家推荐几款性价比高的平价蓝牙耳机,一起…...
揭秘关于TFRcord的五脏六腑
揭秘关于TFRcord的五脏六腑 前言:本篇文章将演示如何创建、解析和使用tf.Example消息,以及如何在.tfrecord文件之间对tf.Example消息进行序列化、写入和读取。 教程讲解使用的都是结构化数据,文章最后还会演示如果将图片写成.tfrecord文件&am…...
【Shell学习笔记】3.Shell 传递参数及数组
前言 本章介绍Shell的传递参数和数组。 Shell 传递参数 我们可以在执行 Shell 脚本时,向脚本传递参数,脚本内获取参数的格式为:$n。n 代表一个数字,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,…...
【终结Bug】ModuleNotFoundError: No module named ‘cv2’
解决方案: 打开 cmd键入 pip install opencv_python -i https://pypi.tuna.tsinghua.edu.cn/simple...
SQL Server2008详细安装步骤(保姆式教程)
安装包下载 链接:https://pan.baidu.com/s/1Rjx4DHJBeCW2asC_4Kzo6Q?pwdchui 提取码:chui 安装过程 1.解压后使用管理员身份打开安装程序 2.选择全新安装或向现有安装添加新功能 3.确认 4.输入产品密钥(上方网盘安装包里有࿰…...
Linux常用操作
Linux常用操作 前言常用命令:一些操作命令:前言 本文是笔者在使用cadence的过程中,操作linux的笔记,仅记录个人常用,持续更新 常用命令: (1)高频:会了这几个就能在文件…...
Golang 处理parquet文件实战教程
Parquet是Apache基金会支持的项目,是面向列存储二进制文件格式。支持不同类型的压缩方式,广泛用于数据科学和大数据环境,如Hadoop生态。 本文主要介绍Go如何生成和处理parquet文件。 创建结构体 首先创建struct,用于表示要处理…...
腾讯TIM实现即时通信 v3+ts实践
目录 初始化sdk 功能描述 初始化 准备 SDKAppID 调用初始化接口 监听事件 发送消息 创建消息 创建文本消息 登录登出 功能描述 登录 登出 销毁 登录设置 获取会话列表 功能描述 获取会话列表 获取全量的会话列表 历史消息 功能描述 拉取消息列表 分页拉取…...
华为OD机试 - 回文字符串(Java JS Python)
题目描述 如果一个字符串正读和反渎都一样(大小写敏感),则称它为一个「回文串」,例如: leVel是一个「回文串」,因为它的正读和反读都是leVel;同理a也是「回文串」art不是一个「回文串」,因为它的反读tra与正读不同Level不是一个「回文串」,因为它的反读leveL与正读不…...
APP测试的7大注意点。
1. 运行 1) App安装完成后的试运行,可正常打开软件。 2) App打开测试,是否有加载状态进度提示。 3) App⻚面间的切换是否流畅,逻辑是否正确。 4) 注册 同表单编辑⻚面 用户名密码⻓度 …...
设计模式-第4章(装饰模式)
装饰模式装饰模型装饰模式示例商场收银程序(简单工厂策略装饰模式实现)装饰模式总结装饰模型 装饰模式(Decorator),动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为…...
【算法设计-分治】快速幂与龟速乘
文章目录1. 快速幂2. 龟速乘3. 快速幂取模4. 龟速乘取模5. 快速幂取模优化1. 快速幂 算法原理: 计算 311: 311 (35)2 x 335 (32)2 x 332 3 x 3仅需计算 3 次,而非 11 次 计算 310: 310 (35)235 (32)2 x 332 3 x 3仅需计算…...
Rust 异步编程
Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...
微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...
[Java恶补day16] 238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度…...
【Oracle】分区表
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
yaml读取写入常见错误 (‘cannot represent an object‘, 117)
错误一:yaml.representer.RepresenterError: (‘cannot represent an object’, 117) 出现这个问题一直没找到原因,后面把yaml.safe_dump直接替换成yaml.dump,确实能保存,但出现乱码: 放弃yaml.dump,又切…...
CppCon 2015 学习:REFLECTION TECHNIQUES IN C++
关于 Reflection(反射) 这个概念,总结一下: Reflection(反射)是什么? 反射是对类型的自我检查能力(Introspection) 可以查看类的成员变量、成员函数等信息。反射允许枚…...
Spring事务传播机制有哪些?
导语: Spring事务传播机制是后端面试中的必考知识点,特别容易出现在“项目细节挖掘”阶段。面试官通过它来判断你是否真正理解事务控制的本质与异常传播机制。本文将从实战与源码角度出发,全面剖析Spring事务传播机制,帮助你答得有…...
