拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Vue项目模板文件位置全解析:入口HTML、组件模板与工程模板

项目本地跑得好好儿的一部署上线就白屏控制台一堆Failed to load resourceGitHub 上有人丢了个项目问我“Vue 模板该放哪”我第一反应是查他的publicPath第二反应是问他用的 Webpack 还是 Vite。聊到最后发现很多人对“模板”这两个字理解得太笼统了——入口 HTML、template块、以及我们常说的 templates 目录在 Vue 工程里是三种完全不同的东西位置和约定也各自不同。这篇我就围绕“Vue 项目中文件模板应该放在哪里”这个话题把三类模板的存放位置、原理、配置方法和常见坑一次讲清楚。这事不搞明白你后面搞多页面、搞自动化生成、搞部署子路径都会莫名其妙地翻车。1. 先搞清楚你说的“模板”到底是哪一种我记得刚带新人的时候让他们改页面一上来就问“模板放哪”我愣了一下后来才意识到新手脑子里“模板”这个词是糊成一团的。在 Vue 项目里“模板”至少要分三层看。第一层是入口 HTML 模板也就是整个应用挂载的那个index.html里面有div idapp和script引入入口文件。这一层的位置决定了你构建出来dist/index.html长什么样也直接影响部署后能不能找到 JS/CSS 资源。第二层是组件模板就是我们常写在.vue单文件组件里的template标签或者是template选项里的字符串、render函数的h()。这一层解决的是“这个组件渲染成什么 DOM”它在源码组织结构里属于组件层面的东西。第三层是工程性模板文件比如你做代码生成器用的plop模板、初始化项目用的脚手架模板、业务里的“模板页”或“公共模板组件”。这类文件的位置取决于你的工具链约定和团队规范。很多人把这三层混在一起问所以你问“模板应该放在哪里”我只能先反问一句你是哪一种模板下面的内容我会一层一层拆开讲每一层都会给出具体的目录位置、配置方法还有那些文档里不会明说但实战中必踩的坑。2. 入口 HTML 模板Vue CLI 与 Vite 的两套规则2.1 Vue CLIWebpack的老规则public/index.html 与 publicPath如果你用的是 Vue CLI 创建的vue create xxx项目入口 HTML 模板默认放在public/index.html。注意是public目录不是src也不是项目根目录。这个位置在官方脚手架里是写死的约定Webpack 内部通过HtmlWebpackPlugin把这文件当作模板再注入打包生成的 JS 和 CSS 链接。这个public目录有几个独特行为新手十有八九搞不清楚第一public下的内容会被直接复制到dist根目录它不经过 Webpack 的编译流程。你往public里放一张logo.png打包后dist/logo.png就是原封不动的文件。所以静态资源有两种放法放src/assets会被处理加 hash、压缩、转 base64放public则原样输出。第二index.html里如果引用了绝对路径的资源比如link relicon href/favicon.ico这个路径在部署时受publicPath影响。publicPath默认是/意思是资源挂载在域名根路径下如果你的应用部署在https://example.com/admin/这种子路径就必须把publicPath改成/admin/否则浏览器会去https://example.com/favicon.ico找结果 404。修改方式是在项目根目录的vue.config.js里加module.exports { publicPath: process.env.NODE_ENV production ? /admin/ : / }这里环境判断是常规操作因为本地开发一般都在根路径起服务部署环境才需要子路径。第三如果你想自定义入口模板文件的位置不想放在public里Vue CLI 也支持在vue.config.js里配置// vue.config.js const path require(path) module.exports { pages: { index: { // 入口 JS必填 entry: src/main.js, // 模板文件默认是 public/index.html可以改成别的路径 template: templates/index.html, // 输出文件名默认 index.html filename: index.html } } }这个pages配置其实是给多页面应用准备的但你只搞一个页面时它也能帮你把模板路径指到任意位置。我这里给的是templates/index.html那templates目录就放在项目根目录它就充当入口模板的存放地。2.2 新时代的 Vite根目录 index.html 与 src 的紧密耦合如果你用的是 Vite情况不一样了。Vite 项目的入口模板默认放在项目根目录文件名叫index.html和src、public、vite.config.js平级。Vite 对index.html的定位很特殊它既不完全是静态文件也不完全是构建产物而是整个开发服务器的东西。Vite 的index.html里必须通过script typemodule src/src/main.js这种形式指定入口不能再像 Vue CLI 那样自动注入。这个文件同样可以自定义位置不过用的是更底层的build.rollupOptions.input// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], build: { rollupOptions: { input: path.resolve(__dirname, templates/index.html) } } })这样入口模板就改到了templates/index.html。但我要提醒一句除非团队里有明确规范否则不建议乱改这个默认位置因为 Vite 生态里很多插件、文档、社区示例都默认你用的是根目录index.html特立独行会带来额外的沟通成本。Vite 的public目录也是约定好的默认是项目根目录下的public。它的行为和 Vue CLI 类似构建时会原样复制到dist根目录。但 Vite 的base配置对应 Webpack 的publicPath同样会影响/xxx.png这类绝对路径资源的解析。2.3 public 目录放什么的实战建议我在组里定的规矩是这样的public目录只放三种东西。第一种是favicon.ico、robots.txt、sitemap.xml这类整站级静态文件它们不需要参与构建直接放这最省事。第二种是第三方 SDK 文件比如地图 SDK、支付 SDK如果它们体积大又基本不变放public里用script直接引避免打包时被处理或代码分割搞出幺蛾子。但要注意这类文件最好加版本号比如sdk-1.2.3.js不然缓存问题能折腾死人。第三种是后端接口返回的静态文件或者说你希望“用户手输 URL 能直接访问到”的文件比如分享页面用的模板 HTML、协议文档页面放public后会原样输出到可访问路径。src/assets里的图片、CSS、字体只要不是特别大我全都建议走构建流程。它们会享受 hash、压缩、按需加载的好处性能上比public硬塞更优。3. 组件模板放在哪单文件组件为核心3.1 SFC 里 template 的三种写法组件模板是 Vue 里最常用的“模板”它的存放规则本质上是你怎么写.vue文件的问题。.vue单文件组件的标准结构里template块就是组件的模板顺序一般放最上面紧跟script和style。这不是强制规定但所有 Vue 官方文档、企业项目都是这个约定你就别特立独行了。template可以理解为一块“活着的 HTML”它支持插值表达式{{ }}、指令v-if、v-for、事件绑定click等。Vue 3 里一个组件可以写多个根节点Vue 2 必须一个根节点包着——知道这一点很多报错就能看懂了。除了 SFC组件模板还有两种替代写法。一种是template选项写在 JS 里用模板字符串export default { template: div h1{{ title }}/h1 /div }这种写法适合快速原型、散装页面但有两个致命缺点一是没有编译时的优化需要带着“运行时编译器”才能跑会让包体积变大二是模板字符串里写复杂逻辑没有语法高亮也没有 lint出错了不好查。我基本只在写库的 demo 和测试用例里用。另一种是render函数。很多人以为render是高级玩法其实它就是“用函数描述模板”import { h } from vue export default { render() { return h(div, { class: card }, [ h(h1, this.title) ]) } }render函数的优势是灵活动态逻辑、函数式组件、插槽透传都比模板好使也没用模板字符串那样需要编译器的问题。代价就是写起来繁琐读起来费劲。正常业务项目里90% 的场景用 SFC 的template就够了render是给高级组件、库作者准备的。3.2 业务组件模板的组织位置说完“怎么写”接着说“放哪”。一个成熟 Vue 项目的src目录结构我一般推荐这样src/ ├── api/ # 接口请求 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── layouts/ # 布局组件含页面骨架子模板 ├── router/ # 路由配置 ├── store/ # 状态管理 ├── styles/ # 全局样式 ├── utils/ # 工具函数 ├── views/ # 页面组件路由视图 ├── App.vue # 根组件 └── main.js # 入口组件模板文件也就是.vue文件就散落在这几个目录里。具体放哪我的判断标准是“复用半径”如果某个组件只在某一个页面里用就跟页面文件放一起作为局部组件命名时加个components子目录比如src/views/user/components/UserList.vue。这样相关代码内聚不会把所有东西都堆到全局components里。如果多个页面、多个模块都要用才提升到src/components/。比如按钮、弹窗、表单控件、图片懒加载这些基础件。这类组件要尽量做成无业务逻辑、接口无关的纯 UI 组件。还有一类是“页面骨架模板”比如顶部导航 侧边栏 内容区组成的管理后台布局这不算普通组件它是嵌套路由的父组件我习惯单独放src/layouts/目录跟普通 components 区分开。这样做的原因是布局模板会使用router-view跟路由强相关混在公共组件里容易让人误解。路由懒加载时页面组件的模板文件路径也很讲究。路由配置里() import(../views/user/UserList.vue)这个相对路径必须写对否则编译报模块找不到。我建议路由文件统一按views层级来组织别名机制也配好比如import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /user, name: UserList, component: () import(/views/user/UserList.vue) } ] })这里的别名指向src目录是 Vue CLI 和 Vite 默认配好的。用它写路径少很多../的折磨这算项目管理模板文件的一个基础规范。3.3 动态组件、异步组件对模板位置的影响还有一类特殊组件叫动态组件它用component :isxxx来渲染不同的模板。这种组件的模板文件位置直接影响运行时的加载策略。比如你做一个表单生成器表单里可能有文本框、下拉框、日期选择器它们的模板各不相同。两种组织方式第一种是浅一点的把表单组件都放src/components/form/下然后用defineAsyncComponent按需加载script setup import { defineAsyncComponent, ref } from vue const formMap { input: defineAsyncComponent(() import(/components/form/InputField.vue)), select: defineAsyncComponent(() import(/components/form/SelectField.vue)), date: defineAsyncComponent(() import(/components/form/DateField.vue)) } const current ref(input) /script template component :isformMap[current] / /template第二种是重一点的在src/components/form/下建一个index.js统一注册导出各字段组件配合动态is字符串。这种方式代码结构更扁平但所有模板都会被打进主包里加载慢。我偏向第一种。组件模板文件按“字段类型”拆开每个文件只管一种渲染后续维护心智负担小得多。4. 工程性模板文件templates 目录与脚手架模板4.1 代码生成器模板别乱塞进 src这块是很多人忽略的。你说“Vue 项目中文件模板应该放在哪里”如果指的是那种“我点击一下自动生成一个页面/组件/接口模块”的模板那它属于工程化模板和运行时代码是两码事。最常用的是plop它是 Node.js 生态里的小型代码生成器。你写一堆handlebars模板文件.hbs运行plop它会读取你的输入替换模板里的占位符生成完整代码文件。以我的项目为例目录结构是这样├── plopfile.js # plop 的入口配置 ├── templates/ # 模板文件目录 │ ├── component/ │ │ ├── index.ts.hbs │ │ └── index.vue.hbs │ └── page/ │ ├── index.vue.hbs │ └── router.ts.hbs ├── src/ │ ...plopfile.js里定义生成规则// plopfile.js module.exports (plop) { plop.setGenerator(page, { description: 生成一个页面组件, prompts: [ { type: input, name: name, message: 页面名称是什么 } ], actions: [ { type: add, path: src/views/{{pascalCase name}}/index.vue, templateFile: templates/page/index.vue.hbs }, { type: add, path: src/views/{{pascalCase name}}/router.ts, templateFile: templates/page/router.ts.hbs } ] }) }这个templates/目录不能放在src里因为它是开发时工具链的一部分不是应用运行的一部分。放src里会被人误认为业务组件而且打包时如果被引用还会平白多出无用代码。正确做法是放项目根目录下和src、public、node_modules平级。handlebars模板本身也简单比如index.vue.hbs长这样template div class{{dashCase name}} h1{{titleCase name}}/h1 /div /template script setup langts defineOptions({ name: {{pascalCase name}} }) const loading ref(false) /script注意这里的{{ }}是 handlebars 的插值语法最后由 plop 替换成真实值的。这个机制跟 Vue 的模板插值长得像但运行阶段完全不同不要把两者混了。4.2 脚手架模板新项目初始化模板放哪如果你要给团队做一套统一的项目脚手架那就涉及“项目模板”的存放。目前主流的做法是单独开一个仓库存一份干净的初始项目代码然后用degit或create-vite的模板机制拉取。create-vite官方支持--template参数然后你可以在本地~/.vite或自己的私有仓库维护模板。这类模板和业务代码完全隔离属于“项目元数据”不存在“放哪个目录”的问题它有自己的仓库。如果是公司内部自研脚手架直接把模板目录作为依赖包发到私有 npm 仓库然后脚手架 CLI 从依赖包里复制模板文件。比如node_modules/ my-scaffold/ template/ src/ main.ts index.html package.json在脚手架代码里用fs.cpSync把这目录拷贝到目标项目目录再重写一些配置。这种方式的好处是模板版本跟着 npm 包走升级脚手架就能同步更新模板。4.3 公共模板组件的组织别惯着“复制粘贴党”另外还有一种“业务模板”说白了就是一段反复出现的页面结构。比如一个详情页左边图片右边参数下面说明——这个结构在很多商品站里出现。很多人会复制粘贴然后改个接口。时间一长代码里全是改了半截的复制品维护简直是灾难。正确操作是把这类“布局型模板”抽成公共组件放到src/components/下专设的目录比如src/components/PageBlocks/或者用ns前缀命名ProductDetail.vue、ArticleCard.vue。抽的时候要特别注意插槽设计让调用方可以通过插槽传内容而不是往组件里堆一堆开关属性template section classproduct-detail div classproduct-detail__image slot nameimage / /div div classproduct-detail__info slot nameinfo / /div slot / /section /template这样模板组件本身是稳定的变化的部分由使用方通过插槽填充。这才是模板文件该有的设计思路——稳定结构放公共组件易变内容走插槽或业务组件。5. 多页面应用模板与部署路径的经典坑5.1 配置多页面模板的完整示例前面提到的vue.config.js的pages配置是唯一官方推荐的多入口方案Vite 则用rollupOptions.input。多页面意味着你需要多个 HTML 模板不能所有页面共用public/index.html。正确做法是在public目录下建多个 HTML 文件public/ index.html # 主应用 login.html # 登录页 admin.html # 后台管理页然后在vue.config.js里分别指定module.exports { pages: { index: { entry: src/main.js, template: public/index.html, filename: index.html, chunks: [chunk-vendors, chunk-common, index] }, login: { entry: src/login.js, template: public/login.html, filename: login.html, chunks: [chunk-vendors, chunk-common, login] } } }这里有个细节chunks数组里的顺序不要乱改chunk-vendors第三方库和chunk-common公共业务代码要排前面不然 HTML 里脚本引用顺序会出问题浏览器加载 JS 时可能会因为依赖顺序不对而报错。Vite 的配置思路一样只是语法长这样import path from path import { defineConfig } from vite export default defineConfig({ build: { rollupOptions: { input: { main: path.resolve(__dirname, index.html), login: path.resolve(__dirname, login.html) } } } })注意 Vite 要求 HTML 文件在项目根目录或public下都行但推荐直接放根目录这样开发服务器访问路径最直观。不是不能放别处而是“最省心”的约定俗成。5.2 publicPath / base 配置与资源 404部署多页面项目时最经典的报错就是各页面扫出来的 JS、CSS 路径全是/js/xxx.js服务器上却没有。这十有八九是publicPathVite 里是base配错了。举例你的应用部署在https://example.com/portal/下面portal是站点子目录。那你必须告诉构建工具“所有资源链接都要加/portal/前缀”。Vue CLI 里module.exports { publicPath: /portal/ }Vite 里export default defineConfig({ base: /portal/ })如果漏了这步HTML 模板生成的script src/js/app.js会去请求https://example.com/js/app.js而真实文件在https://example.com/portal/js/app.js结果就是白屏。解决白屏的另一招是不要用绝对根路径改用相对路径module.exports { publicPath: ./ }这样生成的资源链接是./js/app.js基于当前 HTML 文件路径解析部署到任意子目录都能跑。但它有个短板就是路由使用了 HTML5 History 模式时子路由页面刷新会找不到资源需要服务器配置 fallback 到对应目录的index.html。所以相对路径只建议在纯静态托管、不做前端路由刷新兼容的场景下用。5.3 模板里访问环境变量入口模板文件里常常需要访问环境变量比如埋点 ID、CDN 域名。Vue CLI 的public/index.html里可以使用% %语法注入环境变量!DOCTYPE html html langzh-CN head meta charsetUTF-8 link relicon href% BASE_URL %favicon.ico title% htmlWebpackPlugin.options.title %/title /head body div idapp/div script window.__APP_CONFIG__ { env: % process.env.NODE_ENV % } /script /body /htmlVite 里则是用%语法!DOCTYPE html html langzh-CN head title%VITE_APP_TITLE%/title /head body div idapp/div script typemodule src/src/main.js/script /body /html记住 Vite 环境变量必须带VITE_前缀才能暴露到页面这是硬性约定不是想用什么就用什么。我见过有人写API_BASE_URL不带前缀结果模板里始终渲染成 undefined排查了半天。6. 常见问题与排查技巧实录6.1 模板白屏与资源加载失败白屏是最常见的问题。排查顺序我建议固定成一套第一步打开浏览器 DevTools 的 Network 面板看 HTML 文档是否加载成功状态码是不是 200。如果是 404检查入口模板路径配置是不是把模板文件名写错了。第二步看 HTML 里引用的 JS、CSS 链接鼠标放上去看 URL复制到新标签页打开。如果 404看是不是publicPath/base没有包含部署子路径。第三步看 JS 是否执行报错。如果 Console 提示Cannot read properties of undefined之类的大概率是入口 JS 没加载成功或者入口模块里的依赖路径有问题继续回第二步。我排这种问题从来不瞎翻代码按这个顺序能解决九成白屏。6.2 修改了模板文件但页面不生效开发时改了public/index.html或 Vite 根目录index.html发现浏览器刷新没用。这就是缓存问题。Vite 开发服务器对模板文件是有缓存的有时需要重启 dev server 才能看到变化。Vue CLI 同理不过它通常能热更新但保险起见改入口 HTML 之后重启一下开发服务器最稳。另外注意一个细节浏览器缓存。入口 HTML 通常默认 cache 策略比较保守如果配了强缓存刷新页面可能拿到的还是旧 HTML。遇到这种情况要么 CtrlF5 强制刷新要么 HTTP 响应头里加Cache-Control: no-cache。6.3 Webpack 模板加载器和 pug 模板语言用 Vue CLI 有时会遇到模板里写template langpug这是把 HTML 模板换成了 pug 语法。如果你用了别忘了装pug和pug-plain-loadernpm install -D pug pug-plain-loader然后 SFC 里这样写template langpug .user-card h1.title {{ user.name }} p.desc {{ user.bio }} /template注意pug 靠缩进区分层级缩进错了编译直接报错错得很抽象。所以团队里要统一缩进风格2 空格或 4 空格别一会儿空格一会儿 Tab模板报错时定位问题会让你怀疑人生。6.4 入口模板里使用了不存在的变量Vue CLI 的 HTML 模板是lodash.template语法如果你在public/index.html里写了% htmlWebpackPlugin.options.foo %但配置里htmlWebpackPlugin.options根本没有foo模板会直接渲染成空字符串不报错。这就有迷惑性了——页面没报错但就是少东西。排查时把渲染后的 HTML 源码打开右键查看网页源代码看到% %被渲染成了空字符串就去vue.config.js的pages配置里补上对应字段。同理Vite 的%VITE_XXX%环境变量如果没定义页面会直接把这个字符串留在 HTML 里一眼就能发现。6.5 组件模板循环依赖与递归组件最后提一个组件模板相关的高级坑递归组件。一个组件在自己的模板里调用自己比如目录树菜单template ul li v-fornode in nodes :keynode.id span{{ node.name }}/span TreeMenu v-ifnode.children :nodesnode.children / /li /ul /template script setup defineProps({ nodes: Array }) /script这里/TreeMenu.vue递归引用自己在 Vue 3script setup里因为组件名是自动推导的所以能正常工作。但如果是 Vue 2 选项式 API你必须给组件一个name字段才能递归export default { name: TreeMenu }否则直接报 “Unknown custom element: ”。这个坑是模板递归的经典问题面试也常考。文件放哪不重要重要的是递归组件的name必须固定千万别在多个文件里用同一个nameVue 会警告组件重名最后渲染的可能是完全预料之外的那个。6.6 模板文件编码与 BOM 的问题还有一个偏门但真实遇到的问题Windows 上保存模板文件时带上了 UTF-8 BOM导致 Vue 编译模板时在开头出现看不见的字符渲染出来的 DOM 第一个节点前会多一个零宽字符页面布局可能被莫名撑开或出现空白。解决方法是保证所有源文件统一用 UTF-8 无 BOM 保存。VS Code 里在设置里搜files.encoding改成utf8然后点击右下角编码信息可以转换。这个问题在团队协作时特别讨厌因为有人用记事本、有人用老编辑器混着来很容易踩雷。7. 给新项目的一套落地方案综合上面所有内容我给团队定的一套落地方案是这样的你可以直接参考。如果项目用 Vite入口模板固定放项目根目录index.html不动。public目录只放favicon.ico、robots.txt、外部 SDK。src/assets负责图片和样式资源。组件模板统一用.vue单文件组件按“页面专属组件放 views 下、公共组件放 components 下、布局放 layouts 下”的规则组织。如果项目用 Vue CLI老项目维护入口模板就是public/index.html多页面就加pages配置。代码生成器用 plop模板文件放根目录templates/跟业务代码隔离。所有文件保存时统一 UTF-8 无 BOM团队里用 EditorConfig 或 Prettier 统一格式。静态资源引用原则能走构建的走构建src/assets里的文件在模板和样式中通过相对路径或别名引用不走构建的才放public。部署子路径时第一时间配baseVite或publicPathVue CLI不要等服务上线了才想起来。按这套来基本不会出现“模板不知道放哪”的问题。总结成一句话就是入口模板跟着你的构建工具走组件模板内聚在你的页面/组件目录里工程性模板跟业务代码完全隔离。把这个边界想清楚文件系统的布局问题就解决了一大半。我自己实际办公桌也乱但代码目录必须清爽。很多时候项目做不好不是某个组件写得烂而是文件模板位置混乱导致的心智负担太大。一个刚进组的新人能快速找到所有模板文件在哪、公共组件在哪、入口模板在哪这个项目就已经赢在起跑线上了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门