初识Nuxt

项目初始化

新建项目

1
pnpm dlx nuxi init nuxt3-demo

接入 tailwindcss

1
pnpm add @nuxtjs/tailwindcss -D

另外 tailwindcss 需要在 nuxt.config.ts 中配置一下

1
modules: ["@nuxtjs/tailwindcss"]

基础

路由

Nuxt 路由基于 vue-router ,在 pages 目录下创建的文件都会自动生成路由

例如如上结构的路由大概是:

1
2
3
4
5
6
7
8
9
10
[
{
path: '/',
component: './pages/index.vue',
},
{
path: '/welcome',
component: './pages/welcome.vue',
}
]

NuxtPage

在 pages 下写了文件,但目前页面其实还不能按照上面的路由展示,需要在 app.vue 中给一个入口,就是 <NuxtPage>,当然如果项目只有一个页面就使用app.vue就行了。

1
2
3
<template>
<NuxtPage></NuxtPage>
</template>

路由导航

Nuxt 提供 <NuxtLink>标签可以用来进行跳转,该标签有一个 to 属性声明跳转的地址。

1
<NuxtLink to="/home">link</NuxtLink>

还有一个帮助函数 navigateTo 可以用编程的方式进行导航,需要注意的是,调用该函数需要用 return 或者 await 处理结果。并且 navigateTo 还可以跳转到外部的地址,当跳转到外部地址时第二个参数必须有 external: true,不然会报错

1
2
3
4
5
6
// vue 内跳转
await navigateTo('/home')
// 跳转外部地址
await navigateTo('https://nuxt.com', {
external: true
})

动态路由

当文件名包含了[],就会被转换为动态路由参数,例如新建一个 list-[id].vue 文件:

1
2
3
4
5
<template>
<div>
{{ $route.params.id }}
</div>
</template>

直接访问 /list-1 可以看到页面显示 1

嵌套路由

当需要二级路由,就需要通过文件结构来处理,pages/detail/name.vue 就会生成对应的路由 ‘/detail/name’ ,另外当存在 ‘/detail’ 路由时,也就是

name 作为字路由,需要在 detail.vue 中提供入口 <NuxtPage> 才能将二级路由页面显示出来,另外也可以在 detail 目录下使用动态路由的方式创建 [id].vue 这样就通过 /detail/1 的方式访问即可。

Layout 布局

根目录下的 Layouts 目录用于存放页面布局,该目录下的组件都可以通过 <NuxtLayout> 标签使用,该标签有一个属性 name 用于指定使用哪个 Layout ,如不指定,默认使用 default。在 Layout 中,还需要写一个插槽来展示 Layout 包裹的组件。

1
2
3
4
<template>
<div>Header</div>
<slot />
</template>

静态资源

需要打包工具处理的资源放在 assets 目录下,不需要处理的放在 public 目录下,访问 public 下的资源直接通过/ 例如 '/pic.png',访问 assets 目录下的资源需要从根目录开始,在 Nuxt 中路径有的会存在别名,可以通过别名来访问。

1
2
3
4
5
6
7
8
{
"~~": "/<rootDir>",
"@@": "/<rootDir>",
"~": "/<rootDir>",
"@": "/<rootDir>",
"assets": "/<rootDir>/assets",
"public": "/<rootDir>/public"
}

全局样式

在 app.vue 引入的 css 都会作用于全局,另一种方法就是通过 nuxt.config.ts 进行配置,在 assets 新建一个 global.css 作为全局样式。

1
2
3
4
export default defineNuxtConfig({
css: ["assets/global.css"],
// ...
})

组件 Components

Nuxt 组件放在根目录下的 components 目录下,这个目录下的组件当被使用到的时候无需引入和注册,组件名是路径和文件名以大驼峰,例如 ‘components/nav/bar.vue’ 就是 <NavBar>

异常处理

Vue 渲染过程中的错误

通过 Nuxt 提供的插件,在 plugins 目录下新建 error-handler.js 来处理 vue 渲染过程中的错误。

1
2
3
4
5
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.config.errorHandler = (..._args) => {
console.log('vue error handler')
}
})

通过 Nuxt 钩子捕获 Vue 传播上来的错误
第一个钩子 app:error:整个应用层面的错误捕获
第二个钩子 vue:error:仅 Vue 层面的错误捕获。

1
2
3
4
5
6
7
8
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('app:error', (..._args) => {
console.log('app:error')
})
nuxtApp.hook('vue:error', (..._args) => {
console.log('app:error')
})
})

触发顺序:errorHandler -> vue:error -> app:error。

自定义错误页面

在根目录下创建 error.vue 文件,当抛出异常时会跳转到这个页面。另外手动抛出错误可以使用 showError() 参数是一个字符串或者一个错误对象

1
2
showError('404')
showError(new Error('404'))

组件错误处理

Nuxt 提供 NuxtErrorBoundary 处理组件级的错误

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<NuxtErrorBoundary>
<!-- 默认插槽显示正常内容 -->
<!-- 触发一个错误 -->
<ThrowError></ThrowError>
<!-- error插槽显示错误时内容 -->
<template #error="{ error }">
<p class="my-4 text-xl text-gray-500">发生了一些错误 {{ error }}</p>
<NButton type="success" @click="error.value = null"> 修正错误 </NButton>
</template>
</NuxtErrorBoundary>
</template>

当 ThrowError 组件抛出错误时,会触发 error 插槽的内容。

配置 meta

通过 app.head 配置

在 nuxt.config.ts 中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default defineNuxtConfig({
app: {
head: {
title: 'nuxt3-demo',
meta: [
{
name: 'keywords',
content: 'nuxt3-demo'
},
{
name: 'description',
content: 'nuxt3-demo'
}
]
}
}
})

通过 useHead 在组件内配置

1
2
3
4
5
6
7
8
<script setup>
useHead({
title: "Counter",
meta: [
{name: "description", content: "counter"},
]
})
</script>

useHead 中的 title 会直接替换原本的 title,如果想保留原本的 title 可以使用 titleTemplate 配置。

通过组件标签配置

Nuxt 提供 , <Base>, <NoScript>, <Style>, <Meta>, <Link>, <Body>, <Html> , <Head> 标签来配置 header。</p> <h2 id="常用-meta-配置"><a href="#常用-meta-配置" class="headerlink" title="常用 meta 配置"></a>常用 meta 配置</h2><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs html"> <span class="hljs-comment"><!-- 文本设置 GB2312:中国信息交换用汉字编码字符集(简体中文) GBK:汉字扩展规范(扩大汉字收录,增加繁体中文,蒙语,藏语等少数民族的文字) UTF-8: 万国码 --></span><br><span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>></span><br><span class="hljs-comment"><!-- 标题 --></span><br><span class="hljs-tag"><<span class="hljs-name">title</span>></span>Document<span class="hljs-tag"></<span class="hljs-name">title</span>></span><br><span class="hljs-comment"><!-- 关键字 100字节 --></span><br><span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"keywords"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">""</span> /></span><br><span class="hljs-comment"><!-- 描述 80-120汉字 --></span><br><span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">""</span> /></span><br><span class="hljs-comment"><!-- 一般在移动端中使用,width=device-width 设置网页的宽度(viewport)和设备的宽度一样,这样横向就不会出现滚动条,用户浏览体验会大幅提升;后面的几个设置不允许用户手动缩放。--></span><br><span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"</span>/></span><br></code></pre></td></tr></table></figure> <p>搜索引擎认知优先级 title > description > keywords</p> <h1 id="路由中间件"><a href="#路由中间件" class="headerlink" title="路由中间件"></a>路由中间件</h1><h2 id="匿名路由中间件"><a href="#匿名路由中间件" class="headerlink" title="匿名路由中间件"></a>匿名路由中间件</h2><p>匿名中间件就是在单个页面执行,没办法复用。</p> <figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-title function_">definePageMeta</span>({<br> <span class="hljs-title function_">middleware</span>(<span class="hljs-params">to, <span class="hljs-keyword">from</span></span>) {<br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">'匿名中间件'</span>);<br> }<br>})<br></code></pre></td></tr></table></figure> <h2 id="命名路由中间件"><a href="#命名路由中间件" class="headerlink" title="命名路由中间件"></a>命名路由中间件</h2><p>根目录下的 middleware 目录用来存放路由中间件</p> <figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// middleware/auth.js</span><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title function_">defineNuxtRouteMiddleware</span>(<span class="hljs-function">(<span class="hljs-params">to, <span class="hljs-keyword">from</span></span>) =></span> {<br> <span class="hljs-comment">// 验证权限</span><br> <span class="hljs-keyword">if</span> (<span class="hljs-title function_">isAuthenticated</span>() === <span class="hljs-literal">false</span>) {<br> <span class="hljs-keyword">return</span> <span class="hljs-title function_">navigateTo</span>(<span class="hljs-string">'/login'</span>)<br> }<br>})<br></code></pre></td></tr></table></figure> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs vue"><!-- pages/auth.vue--><br>definePageMeta({<br> middleware: 'auth'<br>})<br></code></pre></td></tr></table></figure> <p>当进入到页面时触发路由中间件,进行鉴权,无权限跳转到 login</p> <h2 id="全局路由中间件"><a href="#全局路由中间件" class="headerlink" title="全局路由中间件"></a>全局路由中间件</h2><p>在 middleware 目录下新建 mid.global.js</p> <figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title function_">defineNuxtRouteMiddleware</span>(<span class="hljs-function">(<span class="hljs-params"><span class="hljs-keyword">from</span>, to</span>) =></span> {<br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">"全局中间件"</span>)<br>})<br></code></pre></td></tr></table></figure> <h2 id="工具方法"><a href="#工具方法" class="headerlink" title="工具方法"></a>工具方法</h2><ol> <li>abortNavigation:留在 from 页面</li> <li>navigateTo:跳转到某个页面</li> </ol> <h1 id="插件"><a href="#插件" class="headerlink" title="插件"></a>插件</h1><h2 id="注册"><a href="#注册" class="headerlink" title="注册"></a>注册</h2><p>根目录下的 plugin 目录下的文件或者文件夹下的 index.js 文件会被注册成插件,<br>另外可通过 1.name.js 给文件名加数字的方式控制插件的顺序<br>还可以通过 .server.js / .client.js 给插件区分环境</p> <h2 id="NuxtApp"><a href="#NuxtApp" class="headerlink" title="NuxtApp"></a>NuxtApp</h2><p>使用 defineNuxtPlugin 时会传入一个参数 nuxtApp</p> <figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title function_">defineNuxtPlugin</span>(<span class="hljs-function">(<span class="hljs-params">nuxtApp</span>) =></span> {<br> <span class="hljs-comment">// ...</span><br>})<br></code></pre></td></tr></table></figure> <p>nuxtApp 中有一些属性和方法:</p> <ul> <li>provide (name, value):定义全局变量和方法;</li> <li>hook(name, cb):定义 nuxt 钩子函数;</li> <li>vueApp:获取 vue 实例;</li> <li>ssrContext:服务端渲染时的上下文;</li> <li>payload:从服务端到客户端传递的数据和状态;</li> <li>isHydrating:用于检测是否正在客户端注水过程中。</li> </ul> <p>获取全局注册的属性和方法</p> <p>当使用 provide 定义了一些属性和方法时,通过 useNuxtApp() 获取</p> </div> <hr/> <div> <div class="post-metas my-3"> <div class="post-meta mr-3 d-flex align-items-center"> <i class="iconfont icon-category"></i> <span class="category-chains"> <span class="category-chain"> <a href="/categories/%E6%8A%80%E6%9C%AF%E5%8D%9A%E5%AE%A2/" class="category-chain-item">技术博客</a> </span> </span> </div> <div class="post-meta"> <i class="iconfont icon-tags"></i> <a href="/tags/Nuxt/" class="print-no-link">#Nuxt</a> <a href="/tags/SSR/" class="print-no-link">#SSR</a> </div> </div> <div class="license-box my-3"> <div class="license-title"> <div>初识Nuxt</div> <div>https://l1ushun.github.io/2024/05/24/nuxt/</div> </div> <div class="license-meta"> <div class="license-meta-item"> <div>作者</div> <div>liu shun</div> </div> <div class="license-meta-item license-meta-date"> <div>发布于</div> <div>2024年5月24日</div> </div> <!-- --> <!-- <div class="license-meta-item">--> <!-- <div>许可协议</div>--> <!-- <div>--> <!-- --> <!-- --> <!-- --> <!-- <a class="print-no-link" target="_blank" href="https://creativecommons.org/licenses/by/4.0/">--> <!-- <span class="hint--top hint--rounded" aria-label="BY - 署名">--> <!-- <i class="iconfont icon-by"></i>--> <!-- </span>--> <!-- </a>--> <!-- --> <!-- --> <!-- </div>--> <!-- </div>--> <!-- --> </div> <div class="license-icon iconfont"></div> </div> <div class="post-prevnext my-3"> <article class="post-prev col-6"> <a href="/2024/07/28/this-topic/" title="关于原型的 JS 题"> <i class="iconfont icon-arrowleft"></i> <span class="hidden-mobile">关于原型的 JS 题</span> <span class="visible-mobile">上一篇</span> </a> </article> <article class="post-next col-6"> <a href="/2024/05/22/qiankun-baics/" title="微前端 qiankun"> <span class="hidden-mobile">微前端 qiankun</span> <span class="visible-mobile">下一篇</span> <i class="iconfont icon-arrowright"></i> </a> </article> </div> </div> <article id="comments" lazyload> <div id="giscus" class="giscus"></div> <script type="text/javascript"> Fluid.utils.loadComments('#giscus', function() { var options = {"repo":"L1ushun/l1ushun.github.io","repo-id":"R_kgDOJ_203Q","category":"Announcements","category-id":"DIC_kwDOJ_203c4CYJ9O","theme-light":"light","theme-dark":"dark","mapping":"pathname","reactions-enabled":1,"emit-metadata":0,"input-position":"top","lang":"zh-CN"}; var attributes = {}; for (let option in options) { if (!option.startsWith('theme-')) { var key = option.startsWith('data-') ? option : 'data-' + option; attributes[key] = options[option]; } } var light = 'light'; var dark = 'dark'; window.GiscusThemeLight = light; window.GiscusThemeDark = dark; attributes['data-theme'] = document.documentElement.getAttribute('data-user-color-scheme') === 'dark' ? dark : light; for (let attribute in attributes) { var value = attributes[attribute]; if (value === undefined || value === null || value === '') { delete attributes[attribute]; } } var s = document.createElement('script'); s.setAttribute('src', 'https://giscus.app/client.js'); s.setAttribute('crossorigin', 'anonymous'); for (let attribute in attributes) { s.setAttribute(attribute, attributes[attribute]); } var ss = document.getElementsByTagName('script'); var e = ss.length > 0 ? ss[ss.length - 1] : document.head || document.documentElement; e.parentNode.insertBefore(s, e.nextSibling); }); </script> <noscript>Please enable JavaScript to view the comments</noscript> </article> </article> </div> </div> </div> <div class="side-col d-none d-lg-block col-lg-2"> <aside class="sidebar" style="margin-left: -1rem"> <div id="toc"> <p class="toc-header"> <i class="iconfont icon-list"></i> <span>目录</span> </p> <div class="toc-body" id="toc-body"></div> </div> </aside> </div> </div> </div> <a id="scroll-top-button" aria-label="TOP" href="#" role="button"> <i class="iconfont icon-arrowup" aria-hidden="true"></i> </a> <div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel" aria-hidden="true"> <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document"> <div class="modal-content"> <div class="modal-header text-center"> <h4 class="modal-title w-100 font-weight-bold">搜索</h4> <button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body mx-3"> <div class="md-form mb-5"> <input type="text" id="local-search-input" class="form-control validate"> <label data-error="x" data-success="v" for="local-search-input">关键词</label> </div> <div class="list-group" id="local-search-result"></div> </div> </div> </div> </div> </main> <footer> <div class="footer-inner"> <div class="footer-content"> <i class="iconfont icon-love"></i> </div> </div> </footer> <!-- Scripts --> <script src="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.js" ></script> <link rel="stylesheet" href="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.css" /> <script> NProgress.configure({"showSpinner":false,"trickleSpeed":100}) NProgress.start() window.addEventListener('load', function() { NProgress.done(); }) </script> <script src="https://lib.baomitu.com/jquery/3.6.4/jquery.min.js" ></script> <script src="https://lib.baomitu.com/twitter-bootstrap/4.6.1/js/bootstrap.min.js" ></script> <script src="/js/events.js" ></script> <script src="/js/plugins.js" ></script> <script src="https://lib.baomitu.com/typed.js/2.0.12/typed.min.js" ></script> <script> (function (window, document) { var typing = Fluid.plugins.typing; var subtitle = document.getElementById('subtitle'); if (!subtitle || !typing) { return; } var text = subtitle.getAttribute('data-typed-text'); typing(text); })(window, document); </script> <script src="/js/img-lazyload.js" ></script> <script> Fluid.utils.createScript('https://lib.baomitu.com/tocbot/4.20.1/tocbot.min.js', function() { var toc = jQuery('#toc'); if (toc.length === 0 || !window.tocbot) { return; } var boardCtn = jQuery('#board-ctn'); var boardTop = boardCtn.offset().top; window.tocbot.init(Object.assign({ tocSelector : '#toc-body', contentSelector : '.markdown-body', linkClass : 'tocbot-link', activeLinkClass : 'tocbot-active-link', listClass : 'tocbot-list', isCollapsedClass: 'tocbot-is-collapsed', collapsibleClass: 'tocbot-is-collapsible', scrollSmooth : true, includeTitleTags: true, headingsOffset : -boardTop, }, CONFIG.toc)); if (toc.find('.toc-list-item').length > 0) { toc.css('visibility', 'visible'); } Fluid.events.registerRefreshCallback(function() { if ('tocbot' in window) { tocbot.refresh(); var toc = jQuery('#toc'); if (toc.length === 0 || !tocbot) { return; } if (toc.find('.toc-list-item').length > 0) { toc.css('visibility', 'visible'); } } }); }); </script> <script src=https://lib.baomitu.com/clipboard.js/2.0.11/clipboard.min.js></script> <script>Fluid.plugins.codeWidget();</script> <script> Fluid.utils.createScript('https://lib.baomitu.com/anchor-js/4.3.1/anchor.min.js', function() { window.anchors.options = { placement: CONFIG.anchorjs.placement, visible : CONFIG.anchorjs.visible }; if (CONFIG.anchorjs.icon) { window.anchors.options.icon = CONFIG.anchorjs.icon; } var el = (CONFIG.anchorjs.element || 'h1,h2,h3,h4,h5,h6').split(','); var res = []; for (var item of el) { res.push('.markdown-body > ' + item.trim()); } if (CONFIG.anchorjs.placement === 'left') { window.anchors.options.class = 'anchorjs-link-left'; } window.anchors.add(res.join(', ')); Fluid.events.registerRefreshCallback(function() { if ('anchors' in window) { anchors.removeAll(); var el = (CONFIG.anchorjs.element || 'h1,h2,h3,h4,h5,h6').split(','); var res = []; for (var item of el) { res.push('.markdown-body > ' + item.trim()); } if (CONFIG.anchorjs.placement === 'left') { anchors.options.class = 'anchorjs-link-left'; } anchors.add(res.join(', ')); } }); }); </script> <script> Fluid.utils.createScript('https://lib.baomitu.com/fancybox/3.5.7/jquery.fancybox.min.js', function() { Fluid.plugins.fancyBox(); }); </script> <script>Fluid.plugins.imageCaption();</script> <script src="/js/local-search.js" ></script> <!-- 主题的启动项,将它保持在最底部 --> <!-- the boot of the theme, keep it at the bottom --> <script src="/js/boot.js" ></script> <noscript> <div class="noscript-warning">博客在允许 JavaScript 运行的环境下浏览效果更佳</div> </noscript> </body> </html>