Nuxt3 初次使用记录

1,046 阅读8分钟

作为Nuxt 的初学者,在首次使用 Nuxt 框架进行项目开发时,遇到了很多问题,记录一下:

Nuxt.js 是什么

Nuxt.js 是一个基于 Vue.js 的通用应用框架,旨在简化Vue.js 应用的开发,特别是在服务端渲染(SSR)、静态站点生成等方面提供了便捷的配置和工具。

在学习 Nuxt 之前需要掌握以下前置知识:

  • Vue.js
  • HTML、CSS、JavaScript
  • 前端构建工具(Webpack、babel 等)
  • 服务端知识(如 Node.js、Express 等)
  • Git 版本控制工具

如何搭建一个 Nuxt.js 项目

  1. 创建一个新的入门项目
    npx nuxi@latest init <project-name>
    
  2. 使用 Visual Atudio Code 打开项目,并安装依赖:
    code <project-name>
    yarn
    
  3. 启动项目
    yarn dev --open
    

Nuxt3 常用组件使用

NuxtPage

使用 <NuxtPage> 组件,才能显示位于 pages/ 目录中的页面。

NuxtImg

<img /> 标签改用 <NuxtImg />

  • 使用内置提供程序优化本地和远程映像
  • 将 src 转换为提供程序优化的 URL
  • 根据 width 和 height 自动调整图片大小
  • 提供 sizes 选项时生成响应式尺寸
  • 支持本机延迟加载以及其他 <img> 属性

NuxtLink

自定义配置

nuxt3 自定义路由

在 nuxt3 中,路由是由页面文件中的目录结构定义的。因为底层使用的 Vue-router,所以可以自定义路由。

app 目录下新建 router.option.ts 文件,自定义正则表达式来匹配动态路径

const reRouter = [
  {
    path: '/product/:modelTypeNo([a-z0-9]+)/:id(\\d+)/:param(canshu|tupian|zixun|dealer)?/',
    component: () => import('../pages/productDetail/[id].vue'),
  },
];
export default {
  routes: () => [...reRouter],
};

环境配置

.env,默认;
.env.development,本地开发环境;
.env.test,测试环境;
.env.production,生产环境

BASE_URL = xxx   # 接口域名
URL = xxx  #前端域名
ENV = 'development'  ## 环境名称

nuxt.config.ts 文件中进行配置:

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      env: process.env.ENV,
    },
  },
});

通过 useRuntimeConfig(),获取 nuxt.config.ts 中配置的 runtimeConfig

const config = useRuntimeConfig();
console.log(config.public.env);

package.json文件中配置,不同环境执行不同的命令:

  "scripts": {
    "dev": "nuxt dev",
    "dev:test": "nuxt dev --dotenv .env.test",
    "dev:prod": "nuxt dev --dotenv .env.production",
    "build": "nuxt build",
    "build:test": "nuxt build --dotenv .env.test",
    "build:prod": "nuxt build --dotenv .env.production",
  },

assetsInlineLimit

图片转 base64 编码的阈值(默认是 4096字节)。为防止过多的 http 请求,nuxt 会将小于此阈值的图片转为 base64 格式,可根据实际需求进行调整。

// nuxt.confug.ts
export default defineNuxtConfig({
  vite: {
    build: {
      assetsInlineLimit: 5,
    },
  },
})

Nuxt3 使用 Unocss

unocss 默认 1rem = 4px,也就是说如果你设置了 m-20(20的单位是像素),那么实际的效果就是5rem,这样的效果是不对的,所以,我们需要改一下 Unocss 的配置

// 1. 安装
yarn add @unocss/preset-rem-to-px -D
yarn add @unocss/preset-uno -D

// 2. 配置
// uno.config.ts
import { defineConfig } from 'unocss';
import presetRemToPx from '@unocss/preset-rem-to-px';
import presetUno from '@unocss/preset-uno';

export default defineConfig({
  // ...UnoCSS options
  presets: [
    presetUno(),
    presetRemToPx({
      baseFontSize: 4, // 设置为4,实现 w-1=1px
    }),
  ],
});

// 3. 结合 px-to-vw,实现页面自适应
yarn add postcss-px-to-viewport-8-plugin -D
// nuxt.config.ts
export default defineNuxtConfig({
  postcss: {
    plugins: {
      'postcss-px-to-viewport-8-plugin': {
        unitToConvert: 'px', // 需要转换的单位,默认为"px"
        viewportWidth: 375, // 设计稿的视口宽度
        unitPrecision: 5, // 单位转换后保留的精度
        propList: ['*', '!font-size'], // 能转化为vw的属性列表,!font-size表示font-size后面的单位不会被转换
        viewportUnit: 'vw', // 希望使用的视口单位
        fontViewportUnit: 'vw', // 字体使用的视口单位
        // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
        // 下面配置表示类名中含有'keep-px'都不会被转换
        selectorBlackList: ['keep-px'],
        minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
        mediaQuery: false, // 媒体查询里的单位是否需要转换单位
        replace: true, //  是否直接更换属性值,而不添加备用属性
        exclude: [/node_modules/], // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
        include: [/src/], // 如果设置了include,那将只有匹配到的文件才会被转换
        landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
        landscapeUnit: 'vw', // 横屏时使用的单位
        landscapeWidth: 1338, // 横屏时使用的视口宽度
      },
    },
  },
});

自定义 unocss class

// nuxt.config.js
export default defineNuxtConfig({
  unocss: {
    nuxtLayers: true,
    rules: [
      [
        'text-ellipsis',
        { 'text-overflow': 'ellipsis', overflow: 'hidden', 'white-space': 'nowrap' },
      ],
      [
        'text-2-ellipsis',
        {
          overflow: 'hidden',
          ' word-break': 'break-all',
          'text-overflow': 'ellipsis',
          display: '-webkit-box',
          '-webkit-line-clamp': '2',
          '-webkit-box-orient': 'vertical',
        },
      ],
    ],
  },
});

指令转换器

一般我们是将 unocss 名定义在 class 属性中,但是可能会导致 class 属性很长,这时候我们就可以用到指令转换器

<div class="fixed w-375 box-border top-0 z-999 px-25 py-10 flex flex-items-center justify-center bg-#fff h-46"></div>

UnoCSS 的指令转换器,用于 @apply@screen 和 theme() 指令:@unocss/transformer-directives

  1. 安装:
yarn add -D @unocss/transformer-directives
  1. 设置:
// uno.config.ts
import { defineConfig } from 'unocss'
import transformerDirectives from '@unocss/transformer-directives'

export default defineConfig({
  // ...
  transformers: [
    transformerDirectives(),
  ],
})
  1. 使用:
.custom-div {
  @apply text-center my-0 font-medium;
}

// 被转换为:
.custom-div {
  margin-top: 0rem;
  margin-bottom: 0rem;
  text-align: center;
  font-weight: 500;
}

⏩ 更多可查看文档

图片预设

Unocss 可以为任何图标提供纯 css 解决方案,即可以将 svg 图片转为 class。

可以使用 FileSystemIconLoader 方法从文件系统加载自定义图标。

  1. 配置
// uno.config.ts
import { defineConfig, presetIcons } from 'unocss';

import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';

export default defineConfig({
  presets: [
    presetIcons({
      collections: {
        //把自己的svg文件转换为class,这里的my-icon名称随便取,使用的时候通过i-custom:[filename]。
        custom: FileSystemIconLoader('./src/assets/imgs'),
      },
    }),
  ],
});

  1. 使用
<div class="i-custom:[imgs中的文件名(不需要加后缀)]"></div>

ssr

项目路径中不能有中文,否则会报错“500 Internal Server Error”。

获取接口数据时,定义的接口方法不需要放在生命周期中执行,直接放在 <script>里执行

如果页面渲染有问题,可以先去看是不是html标签使用不当。

title 标签中的 / 在源代码中被转码成 &#x2F;

image.png

原因:Nuxt3 默认会对特殊字符进行 HTML 实体编码,以防止 XSS 攻击
解决方法: 使用服务端中间件(在 /server/middleware/ 目录下)

// fixTitle.ts
export default defineEventHandler((event) => {
  // 获取原始的 end 方法
  const originalEnd = event.node.res.end;

  // 重写 end 方法
  event.node.res.end = function (chunk, ...args) {
    if (typeof chunk === 'string') {
      // 处理 HTML 实体编码
      chunk = chunk.replace(/<title[^>]*>(.*?)<\/title>/g, (match) => {
        return match
          .replace(/&#x2F;/g, '/') // 处理斜杠
          .replace(/&amp;/g, '&') // 处理 &
          .replace(/&#x5C;/g, '\\') // 处理反斜杠
          .replace(/&#x25;/g, '%'); // 处理百分号
      });
    }

    // 调用原始的 end 方法
    return originalEnd.call(this, chunk, ...args);
  };
});

设置 TDK

Nuxt 提供了三种方式来设置 TDK:

  1. nuxt.config.ts 文件中全局设置
export default defineNuxtConfig({
    app: {
      head: {
        title: '页面标题',
        meta: [
          {
            name: 'description',
            content: '页面描述',
          },
          {
            name: 'keywords',
            content: '关键字1, 关键字2',
          },
          {
            property: 'og:title',
            content: '分享出去的页面标题',
          },
        ],
      }
    }
})
  1. 使用 useHead
useHead({
    title: '页面标题',
    meta: [
      {
        name: 'description',
        content: '页面描述',
      },
      {
        name: 'keywords',
        content: '关键字1, 关键字2',
      },
      {
        name: 'og:title',
        content: '分享出去的页面标题',
      },
    ],
});
  1. 使用 useSeoMeta:这种方式可以帮助开发者避免 nameproperty 的使用错误。
useSeoMeta({
    title: '页面标题',
    description: '页面描述',
    keywords: '关键字1, 关键字2',
    ogTitle: '分享出去的页面标题',
});

将 TDK 源码放在一起

我们在设置完 tdk 之后,他们的源码可能不在一起

image.png

如果SEO有要求的话,我们可能就需要手动的调整源码,同样,我们可以创建一个服务端插件,来挪动 TDK 在源码中的位置:

// server/middleware/fixTDK.ts
export default defineEventHandler((event) => {
  // 获取原始的 end 方法
  const originalEnd = event.node.res.end;
  // 重写 end 方法
  event.node.res.end = function (chunk, ...args) {
    if (typeof chunk === 'string') {
      const tdkTags: string[] = [];
      const otherTags: string[] = [];
      const arr = chunk.split('\n');
      arr.forEach((line) => {
        if (
          line.includes('<title>') ||
          line.includes('<meta name="description"') ||
          line.includes('<meta name="keywords"')
        ) {
          tdkTags.push(line);
        } else {
          otherTags.push(line);
        }
      });
      chunk = [...otherTags.slice(0, 4), ...tdkTags, ...otherTags.slice(4)].join('\n');
    }
    // 调用原始的 end 方法
    return originalEnd.call(this, chunk, ...args);
  };
});

性能优化

页面缓存(lru-cache)

  1. 安装 lru-cache
    yarn add lru-cache
    
  2. server/middleware 目录下新建 page-cache 文件做页面缓存中间件
    Nuxt 将自动读取 ~/server/middleware 目录下的任何文件,为项目创建服务器中间件。
    import { LRUCache } from 'lru-cache';
    import { fromNodeMiddleware } from 'h3';
    
    // 合理配置
    const options = {
      max: 10000,
      ttl: 1000 * 60 * 60,
    };
    
    // 统一从路由配置文件读取需要cache的路由
    const CACHE_URL = ['/product/', '/video/'];
    
    const isPageOne = (pathname: string) => {
      return CACHE_URL.includes(pathname);
    };
    
    const cache = new LRUCache(options);
    
    export default fromNodeMiddleware((req, res, next) => {
      const pathname = req.originalUrl;
      if (isPageOne(pathname)) {
        const existsHtml = cache.get(pathname);
        if (existsHtml) {
          console.log('page cache hint---------------', pathname);
          // 不要忘了设置 Content-Type 不然浏览器有时候可能不会渲染而是触发下载文件
          res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
          return res.end(existsHtml.html, 'utf-8');
        } else {
          res.original_end = res.end;
          res.end = function (data) {
            if (res.statusCode === 200) {
              cache.set(pathname, { html: data });
            }
            res.original_end(data, 'utf-8');
          };
        }
      }
      next();
    });
    

KeppAlive

KeppAlive 用来缓存动态切换的路由组件

// app.vue
 <NuxtPage :keepalive="{ max: 2, exclude: 'pageA', include: 'pageB' }" />

安装百度统计

通过百度统计工具来收集和分析网站的访问数据,从而更好的了解用户行为、优化网站内容和提高用户体验.

// nuxt.config.ts
export default defineNuxtConfig({
    app: {
      head: {
        script: [
            {
                type: 'text/javascript',
                innerHTML: `var _hmt = _hmt || [];
                    (function () {
                        var hm = document.createElement('script');
                        hm.src = 'https://hm.baidu.com/hm.js?your_baidu_analytics_id';
                        var s = document.getElementsByTagName('script')[0];
                        s.parentNode.insertBefore(hm, s);
                    })();
                `,
              },
            ]
         }
     }
})

页面切换时触发百度统计:

// app.vue
const route = useRoute();
onMounted(() => {
    // 触发百度统计的页面追踪
    const trackPageview = () => {
        if (window._hmt) {
          window._hmt.push(['_trackPageview', route.fullPath]);
        }
    };

    // 初次加载时调用一次
    trackPageview();

    // 监听路由变化
    watch(
        () => route.fullPath,
        () => {
            trackPageview();
        },
    );
});

接入阿里云监控服务

  1. 安装 @arms/rum-browser
  2. plugins 文件夹下创建文件,注意该文件只在客户端运行,所以以.client格式命名。
// plugins/rum-browser.client.js

import ArmsRum from '@arms/rum-browser';

export default defineNuxtPlugin((nuxtApp) => {
  if (useRuntimeConfig().public.env === 'development') {
    return;
  }
  ArmsRum.init({
    pid: xxx,
    endpoint: 'https://f0mbzrjjy9-default-cn.rum.aliyuncs.com',
  });

  nuxtApp.hook('app:mounted', () => {
    console.log('App has been mounted!');
  });
});

持续更新⏳

007r.png