做中后台系统的人几乎都绕不开“权限控制”这四个字。路由守卫和权限验证说白了一件事怎么让用户只能看到他该看的内容进他该进的页面。在Vue3 Vue Router 4这个组合下权限控制的实现方式和Vue2时代已经有一些明显的区别尤其是配合组合式API和Pinia之后代码组织方式、数据流度、TS支持都有了更好的解法。这篇博文我想用实际做过的项目来拆解一遍Vue3的路由守卫机制以及我自己在权限验证上踩过的一些坑和最终沉淀下来的稳定方案。这篇文章适合谁看正在做Vue3后台管理系统的同学、准备vue3面试时被问到“路由守卫和权限验证”这个经典问题的人、以及被动态路由和权限刷新搞得头大的朋友。看完之后你会对Vue Router 4的守卫机制有一个完整的认知也能照着一套可以落地的方案把动态菜单、页面权限、按钮权限串起来。1. 路由守卫机制先搞清楚守卫到底在哪个环节起作用1.1 三种路由守卫类型和它们的执行顺序Vue Router 4延续了Vue Router 3.x的守卫体系一共三类全局守卫、路由独享守卫、组件内守卫。很多项目里只用到了全局前置守卫但完整理解这三类守卫和执行顺序对排查权限问题是质的提升。全局守卫router.beforeEach、router.beforeResolve、router.afterEach注册在router实例上路由独享守卫beforeEnter写在某一个路由配置里组件内守卫beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave写在组件内部完整的导航解析流程是下面的顺序导航被触发比如调用router.push或用户点击router-link在失活的组件里调用beforeRouteLeave调用全局的beforeEach守卫在重用的组件里调用beforeRouteUpdate调用路由配置里的beforeEnter解析异步路由组件这一步对动态加载路由很重要因为路由级别懒加载也是在此时解析在被激活的组件里调用beforeRouteEnter调用全局的beforeResolve守卫导航被确认调用全局的afterEach守卫触发DOM更新用beforeRouteEnter中传给next的回调函数在组件实例创建后执行这个顺序一定要背下来。我在面试中聊路由守卫时经常先让候选人说一遍这个顺序能说全的人不多但这是排查权限死循环和时序问题的基础。1.2 beforeEach里到底能不能做“正经”的权限判断可以而且必须。全局前置守卫是权限验证的主战场原因很简单它是整个导航流程中最早能统一拦截的地方。我见过很多权限方案把逻辑写在组件内部的onMounted里然后判断没权限就router.push(/403)再配合一个v-if把页面内容藏起来。这个方案能用但它有一个致命问题用户能通过直接输入URL访问到组件代码虽然页面被v-if藏了但组件已经加载了如果里面发了数据请求还是可能拿到不该看的数据。而路由守卫是在组件创建之前执行的在beforeEach里拦截掉无权限的导航组件根本不会实例化更不会发请求。这才是真正的访问控制。需要注意一个点beforeEach里做异步操作比如拉取用户信息是完全支持的因为路由守卫支持返回Promise。这也是Vue Router 4相比老版本的一个现代用法不再推荐用next()回调作为主要控制手段而是直接return一个布尔值或路由地址。写法上更干净也更符合异步语义。router.beforeEach(async (to) { const token localStorage.getItem(token) if (!token to.name ! login) { return { name: login } } // 继续... })这种写法在Vue Router 4里是官方推荐风格。用next()回调在旧项目里经常出现“同时调用多个next”的问题改成返回值之后代码可读性高了一个档次。1.3 导航守卫里容易忽略的三个细节第一个细节afterEach拿不到next函数但也不需要。它只做导航确认后的收尾最常见的用途是设置页面标题、埋点上报、滚动位置恢复。权限判断千万别放到afterEach因为到这一步导航已经确认了拦不住了。第二个细节beforeEach里如果多次调用next()或者既return了又调用了next控制台会直接警告而且行为不可预期。组合式API时代用return更安全让每个分支只有一个出口。第三个细节守卫是异步链路中的一环不是全部。路由守卫内部可以await任何Promise但协议上如果守卫执行时间太长导航会一直等待。所以不要在守卫里做特别重的任务比如拉取超大列表数据。守卫只做“判断和取必要信息”尽量把数据预取放到组件内部或状态管理里去。2. 权限验证方案怎么选两种主流思路的取舍2.1 方案一前端静态路由表 角色拦截这是最容易理解、也最快上线的方案。所有的路由都注册进路由表只是在beforeEach里去判断当前用户的角色或权限码。const adminRoutes [ { path: /admin, component: () import(/views/admin/index.vue), meta: { roles: [admin] } }, { path: /user, component: () import(/views/user/index.vue), meta: { roles: [admin, user] } } ]在beforeEach里对比to.meta.roles和当前用户的角色集合。这个方案最明显的优势是简单直接路由明确不存在动态添加和删除的复杂逻辑。适合权限模型固定、角色少、页面不会频繁增删的中小型项目。它的短板同样明显路由都在前端菜单也是前端写好死的。一旦业务需要调整某个角色的菜单就得发版本改前端代码。稍微大一点的系统权限配置应该交给运营或超管在后台去配置前端要根据配置动态展示这时候方案一就非常吃力。2.2 方案二后端动态返回路由前端动态注册这是目前中大型后台系统的标准做法。后端基于RBAC模型用户-角色-权限把当前用户有权限访问的页面列表比如路由名称、路径、组件路径、图标等返回给前端。前端拿到这份动态路由数据之后通过router.addRoute()逐个注册同时把相同的数据转换成菜单渲染出来。这样做的好处是权限体系完全在服务端控制菜单和路由天然联动新增页面不需要改动已经上线的老逻辑权限调整即时生效。代价就是复杂度的提升。需要处理的细节更多比如路由表格式怎么定义、如何把后端返回的component字符串映射到实际的Vue组件、动态注册之后刷新页面状态怎么恢复、404页面要放在最后兜底等。这些都是接下来实操部分要解决的问题。2.3 方案对比以及我选择方案二的原因维度方案一静态路由角色拦截方案二后端动态路由实现复杂度低中高权限调整效率需要改前端发版后端配置即时生效刷新页面稳定性高需要配合动态恢复逻辑适用场景权限固定、页面少角色多、权限频繁调整菜单联动手动维护菜单和路由同一份配置驱动菜单和路由安全性路由仍在前端可访问更符合后端统一管控我在实际项目里两种都试过。最初的小型管理后台用的方案一后来用户角色从两个膨胀到十几个菜单逻辑开始混乱每次改权限都要前端发版被产品吐槽之后我彻底转向了方案二。方案二真正解决了三个痛点后端可以动态控制某个角色能看到哪些菜单、能进哪些页面页面和菜单由同一份数据驱动不会出现菜单和路由对不上的情况权限调整不需要前端介入运营同学改完配置刷新一下页面菜单就变了。虽然复杂度上来了但从工程长期维护的角度看这是值得的。3. 完整实操动态路由 权限验证的前后端链路3.1 登录态与用户信息的整体设计权限验证的前提是知道“当前用户是谁、他有什么权限”。这里需要理清两个概念登录态token证明用户已经通过身份认证存续时间短一般放localStorage或cookie中用户信息包括角色、权限码、菜单数据是登录态的附属数据应该放在全局状态里管理在Vue3中就是Pinia在Vue3项目里我的习惯是创建一个useUserStore来统一管理token和用户信息。token持久化到localStorage用户信息和权限数据存在Pinia里。为什么权限数据不持久化到localStorage因为权限数据是敏感信息而且是动态的持久化容易出现“改了权限不刷新就还是旧数据”的脏问题。每次刷新页面后从接口重新拉取用户信息和权限数据反而更可靠。export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: null, menus: [], permissions: [] }), actions: { async login(loginForm) { const res await api.login(loginForm) this.token res.token localStorage.setItem(token, res.token) await this.fetchUserInfo() }, async fetchUserInfo() { const res await api.getUserInfo() this.userInfo res.userInfo this.menus res.menus this.permissions res.permissions return res }, logout() { this.token this.userInfo null this.menus [] this.permissions [] localStorage.removeItem(token) router.push(/login) } } })这里的res.menus就是后端返回的可访问页面配置数据res.permissions是按钮级的权限码列表。一个负责决定“你能看到哪些页面”一个决定“页面里哪些按钮能点”两者配合使用。3.2 前端路由表设计静态路由和动态路由拆分动态路由方案里路由表不能是一张表了要拆成两块静态路由所有登录用户都能访问的公共页面比如登录页、404页、403页、首页可选看业务是否需要登录就能看。const constantRoutes [ { path: /login, name: login, component: () import(/views/login/index.vue), meta: { hidden: true } }, { path: /404, name: NotFound, component: () import(/views/error/404.vue), meta: { hidden: true } }, { path: /403, name: Forbidden, component: () import(/views/error/403.vue), meta: { hidden: true } } ]动态路由模板这是一个把后端返回的数据转换成路由配置的“映射表”。因为后端返回的是JSON数据不可能直接把组件路径指过去需要一个组件映射关系。const modules import.meta.glob(/views/**/*.vue) export function buildRoutes(serverMenus) { return serverMenus.map((menu) { const route { path: menu.path, name: menu.name, component: modules[/src/views/${menu.component}.vue], meta: { title: menu.title, icon: menu.icon, hidden: menu.hidden || false, permissions: menu.permissions || [] }, children: menu.children ? buildRoutes(menu.children) : [] } return route }) }这里关键是import.meta.glob(/views/**/*.vue)Vite会把这个目录下所有的Vue组件做一个懒加载映射然后通过后端返回的component字符串拼出对应的文件路径来取组件。这种方案避免了“动态匹配组件名”的花哨写法不容易出现线上加载不到组件的问题。3.3 beforeEach权限校验的完整链路这一节是核心。整个beforeEach的逻辑我用一张流程图式的文字描述来拆解读取to.meta和当前token白名单判断/login等页面无条件放行没有token重定向到/login带上redirect参数方便登录后跳回原页面有token但用户信息不存在刷新页面导致Pinia被清空调用fetchUserInfo拉取用户信息并用buildRoutes生成动态路由通过addRoute注册动态路由注册完之后重新return原来的to注意这里要重新到目标地址否则这次导航用的路由表还是旧的检查目标路由的meta.permissions或meta.roles若无权限则跳转/403const WHITE_LIST [/login] router.beforeEach(async (to) { const userStore useUserStore() // 1. 白名单直接放行 if (WHITE_LIST.includes(to.path)) { return true } // 2. 没有token去登录页 if (!userStore.token) { return { path: /login, query: { redirect: to.fullPath } } } // 3. 有token但没有用户信息说明是刷新页面重建用户信息和动态路由 if (!userStore.userInfo) { try { await userStore.fetchUserInfo() const dynamicRoutes buildRoutes(userStore.menus) dynamicRoutes.forEach((route) { router.addRoute(route) }) // 兜底404必须在动态路由之后添加 router.addRoute({ path: /:pathMatch(.*)*, name: NotFound, component: () import(/views/error/404.vue) }) // 重新进入当前目标让新的路由表生效 return { ...to, replace: true } } catch (error) { await userStore.logout() return { path: /login } } } // 4. 权限判断 if (to.meta.permissions to.meta.permissions.length 0) { const hasPermission to.meta.permissions.some((perm) userStore.permissions.includes(perm)) if (!hasPermission) { return { path: /403 } } } return true })这段代码是我在几个项目里逐步打磨出来的稳定版本。有几个点值得单独拿出来说return { ...to, replace: true }这里非常微妙。动态路由注册之后当前这次导航的to对象所依赖的路由记录还是旧的此时目标路由可能还没匹配上必须重新触发一次导航让新注册的路由参与下一次匹配。用replace: true可以避免在浏览器历史栈里多一条重复记录这个细节对用户体验很重要。兜底404路由要在动态路由注册完之后再添加因为/:pathMatch(.*)*是通配符如果一开始就注册他会把还没注册的动态路由地址全部拦截成404导致动态路由永远无法访问。3.4 动态菜单的生成与高亮逻辑菜单组件的核心问题是——如何让菜单和路由保持同步。我的做法是在侧边栏组件里直接用userStore.menus渲染菜单菜单数据结构同时包含title、icon、path、children。点击菜单时使用router.push跳转配合router-link或者直接用useRouter实例。菜单高亮用route.path判断。需要注意父子菜单的场景比如当前路由是/system/user但菜单父级是/system这个时候高亮逻辑不能只匹配route.path需要用route.matched来反向匹配菜单项的path。script setup import { useRoute } from vue-router import { useUserStore } from /store/user const route useRoute() const userStore useUserStore() const menus computed(() userStore.menus) const activeMenu computed(() route.meta.activeMenu || route.path) function handleMenuSelect(index) { router.push(index) } /script还可以给菜单数据里的每一项加一个hidden标记这样有些页面需要配置成路由可访问但不显示在菜单中实现这种场景就非常容易。3.5 按钮级权限自定义指令实现页面级的权限控制解决了接下来是按钮级权限。比如管理页面里“删除”按钮不是所有有该页面权限的人都能点。Linux权限模型里RWX权限位是纠结的前端按钮权限在RBAC模型里通常用“权限点”或“权限标识”控制。Vue3中可以用自定义指令实现比在每个按钮上写v-if然后手动判断权限更优雅。实现一个v-permission指令const permission { mounted(el, binding) { const requiredPermissions binding.value const userStore useUserStore() const hasPermission requiredPermissions.some((perm) userStore.permissions.includes(perm)) if (!hasPermission) { el.parentNode el.parentNode.removeChild(el) } } }使用方式el-button v-permission[system:user:delete]删除/el-button。指令的值可以传数组表示满足其一即可。指令的实现需要注意一个细节el被移除时如果有事件监听器或全局状态依赖可能造成内存泄漏。推荐用el.style.display none替代removeChild或者用v-if在组件层面控制更直观。实际上我更推荐在业务中用v-if加一个权限判断函数因为自定义指令对TS类型推断和一些动态判断场景支持不够灵活。写一个usePermission组合式函数export function usePermission() { const userStore useUserStore() const hasPermission (permission) { return userStore.permissions.includes(permission) } return { hasPermission } }然后在模板中el-button v-ifhasPermission(system:user:delete)删除/el-button这种方式非常直观普通HTML元素、组件都能用而且TS能正确推断类型。4. 常见问题与排查技巧实录4.1 刷新页面就白屏动态路由丢了这是动态路由方案最经典的问题。现象是登录后正常访问一刷新页面就空白控制台报错“No match found for location”。原因很简单Pinia里的用户信息在刷新后清空了动态路由还没重新注册组件就尝试渲染当前URL对应的路由自然匹配不到。解决办法就是上面代码里写的那样在beforeEach里判断没有userInfo时先fetchUserInfo、addRoute再return { ...to, replace: true }重新导航。这里有一个非常容易踩的坑白名单必须包含刷新后不需要用户信息就能访问的页面比如登录页。如果把所有页面都加了权限判断刷新时去拉用户信息接口耗时较长用户会看到一段白屏或loading动画体验不佳。可以配合一个全局loading进度条用NProgress在beforeEach里start、afterEach里done至少用户能看到反馈知道系统还在加载。4.2 守卫死循环控制台刷爆警告前端的经典“栈溢出”。最常见的原因是守卫里跳转的页面又被守卫拦截了。比如有token的情况下访问/login你写的是return { path: /login }但/login在白名单里所以不会循环但如果写的是在守卫里检测到token存在就强制跳首页而首页又因为某些条件不满足被重定向回/login就会死循环。我在实际项目里遇到过的一个典型的坑在beforeEach里拉取用户信息失败后调用logout()logout()里执行router.push(/login)然后这个新的导航再次进入beforeEach又因为某些判断条件重新走到失败的那个逻辑——最后变成死循环。解决方案的核心思路是给失败分支设置明确的出口。比如拉用户信息失败后直接清空token并return { path: /login }不要再调用会触导航跳转的函数。排查这类问题用好浏览器的打印或者加一个计数变量在超过N次后直接returntrue强制放行。4.3 动态路由注册“加不进去”有两个常见情况第一种是重复注册。用户从A角色切换到B角色登出再登录时动态路由如果重名Vue Router会警告“An existing route with name ... was registered”。解决方法是在登录获取新的动态路由前先遍历移除旧的动态路由。// 在登出时移除动态路由 function removeDynamicRoutes(routes) { routes.forEach((route) { if (route.name router.hasRoute(route.name)) { router.removeRoute(route.name) } if (route.children) { removeDynamicRoutes(route.children) } }) }第二种是import.meta.glob匹配不到组件文件。后端返回的component字符串路径如果写错了比如大小写不一致、多了一个斜杠组件就是undefined页面渲染空白且路由能匹配但不显示内容。这个问题排查起来很隐蔽最好开发时在buildRoutes函数里加一个debug输出把路径和组件映射关系打出来。4.4 手动输入地址直接访问无权限页面动态路由虽然可以避免无权限的路由出现在菜单里但用户如果手动输入URL比如从聊天记录里复制了别人分享的链接此时动态路由表里没有这个路由结果只有两种要么匹配到兜底404要么如果之前注册过比如用户曾经有权限后来权限被收回就能直接打开页面。权限被收回后依然能访问这其实是一个比较容易出安全问题的点。解决思路有两个层面后端接口必须做权限校验页面能不能打开只是体验层面的问题数据安全永远依赖后端前端在beforeEach里增加一层“路由权限点”校验如果to.meta.permissions存在且当前用户的permissions中没有对应权限直接重定向到404或403兜底页面在动态路由的buildRoutes生成的路由配置里给每个路由加上meta.permissions然后在beforeEach的最后做权限判断。这样即使路由在某个时刻被动态注册了但当用户权限不包含该路由的permissions要求时依然会被拦截。4.5 修改权限后刷新菜单没变化这是个缓存时序问题。用户权限在后台被修改之后如果前端还在使用Pinia里缓存的数据刷新页面时虽然会重新拉取用户信息但如果接口返回的数据有浏览器缓存或者拉取逻辑有问题菜单不会更新。我的排查思路是先确认接口是否真的返回了最新权限看Network面板然后确认Pinia里的menus是否被正确赋值最后确认buildRoutes生成的动态路由是否被重新注册并替换了旧路由。有一个容易忽略的点动态路由被addRoute进去之后它是“叠加”的不会自动清理已经失效的路由。如果用户从A角色换成B角色A角色的路由还在路由表里菜单虽然通过menus渲染变了但用户依然可以通过手动输入URL访问到A角色的页面。这就是为什么登出时一定要清动态路由登录新的角色时再注册新的路由这是权限安全的重要一环。4.6 一个排查速查表问题现象可能原因快速排查思路刷新白屏动态路由丢失在beforeEach里打断点看看有token后有没有重新fetchUserInfo和addRoute守卫死循环跳转目标再次被拦截在beforeEach开头加一个计数器超阈值放行404被提前匹配通配符路由位置不对确认/:pathMatch(.*)*只在所有动态路由之后添加菜单出来了但点击没反应菜单的path和路由path不一致对比menu.path和route.path检查有没有拼写差异直接输入URL能访问动态路由已注册但权限校验不完整给路由加meta.permissions守卫里加权限点校验addRoute报重复名称警告登出时没移除旧路由写一个递归removeRoute函数在登出时调用组件渲染不出来component映射失效检查buildRoutes里import.meta.glob路径和字符串是否精确匹配5. 最后再分享一点路由动画和滚动位置的小经验权限验证是硬需求但路由体验是软需求。很多项目专注于权限逻辑之后忽略了Vue Router 4自带的两个贴心功能scrollBehavior和路由转场动画。路由切换滚动位置复位非常影响体验Vue Router 4里一行配置就能搞定const router createRouter({ history: createWebHashHistory(), routes: constantRoutes, scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } else { return { top: 0 } } } })配合router-view v-slot{ Component }和transition可以做路由级别切换动画。但要注意一个性能点给所有路由统一加切换动画在低端设备上会出现明显的白屏闪烁。我的实践是只在少数需要强调的页面之间加过渡动画其他页面用即时切换反而更干净利落。路由守卫的最终价值是通过前端导航控制把“访问体验”和“安全边界”统一起来。动态路由方案虽然比静态方案复杂但从可维护性和权限模型的完整性来看它是中大型后台系统里最值得投资的一段代码。你在权限方案设计上有什么踩坑经历也欢迎以你自己的视角继续补充。
企业数字化 ERP 产品动态
相关推荐
Vue3路由守卫实战:从登录验证到动态路由权限控制 1. 为什么说路由守卫是后台管理系统绕不开的坎做 Vue3 后台管理系统,尤其是涉及到登录、权限、角色这些词的项目,路由守卫基本是你躲不掉的硬需求。我最早接触路由守卫是在 Vue2 时代,那时候用beforeEach写一堆逻辑,虽然能用但总觉… · 2026/9/24 20:37:48
AI批量测短视频钩子:3秒定生死的工业化创作流程 “3秒定生死”这句话,做短视频和短剧的人应该都不陌生。用户划不划走,基本就在开局那一瞬间决定。过去定开场钩子,全靠编导的网感和经验,一条片子行不行,先拍出来再说,不行就换,效率低ÿ… · 2026/9/24 20:37:22
《动手学大模型》48.2k star教程:从原理到微调部署的完整实操指南 我最早注意到《动手学大模型》这套教程,是在一次内部技术分享会上。当时同事把上海交通大学的开源仓库投到屏幕上,我看到 GitHub 标星已经冲到 48.2k,第一反应是“又一份被收藏吃灰的豪华资料”。真正改变我判断的,是我跟着 noteb… · 2026/9/24 20:37:22
HR数字化落地指南:从选型到实施的全流程解析 很多HR团队看着每天都很忙,但月底一算,真正花在事务性工作上的时间可能占了七成。入职离职手续、考勤异常核对、薪酬核算、社保增减员、招聘简历筛选,这些事每一件单拎出来都不算难,可它们堆在一起,会不断挤压你本来应… · 2026/9/24 21:11:02
远程访问NAS七种方案横评:从DDNS内网穿透到组网,一次讲透 每次出门前先把 NAS 里的工作文件复制到手机,备份完才敢拔电源——如果你还处于这个阶段,说明远程访问 NAS 这件事一直没找到顺手的路子。远程访问 NAS 的方案其实已经非常成熟,从老玩家熟知的 IPv4DDNS,到新兴的 IPv6 直连、frp … · 2026/9/24 21:11:02
Python实战IMDB情感分析:从TF-IDF到LSTM完整指南 简介:面向Python初、中级开发者及需要完成毕业设计或期末大作业的在校生,这套IMDB电影评论情感分析源码包完整覆盖了从数据清洗、分词、Word2Vec词向量训练,到句子切分、平均特征构建,再到随机森林分类评估的全流程。项目已通过导… · 2026/9/24 21:11:02
动态图神经网络异常流量检测:从PCAP到模型实战 简介:这份资源面向计算机、人工智能及网络安全方向的学习者与研究人员,提供一套基于动态图神经网络的异常流量检测完整实现方案,可用于毕业设计、课程设计或实际项目参考。压缩包共141个文件,约34.94MB,以60个Python源… · 2026/9/24 21:11:02
Linux性能排查利器:从strace到bpftrace,一文讲透trace工具家族 线上服务P99抖动到心慌,CPU、内存、IO看着都正常,这时候你会怎么办?如果第一反应只是打开top再盯一遍,那大概率还会盯着屏幕怀疑人生。我第一次遇到这个场景时,盯着监控面板看了一下午,最后是靠Linux trace… · 2026/9/24 21:11:02
37K Star开源AI网关,终结多模型接入混乱,统一管理与降本 最近在 GitHub 上刷到一个 37K Star 的开源项目,核心方向是开放 AI 网关。简单说,它就是把各家模型厂商的 API 统一收敛到一个入口后面,团队内部只需要管一个地址,就能把 GPT、Claude、国内模型、本地私有模型全部串起来。更实在的… · 2026/9/24 21:10:56
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44