Nuxt3 常用插件集成方案

7,797 阅读10分钟

ElementPlus 集成

按需加载

目前来看按需加载仅仅针对组件,对于样式并不能实现按需加载。

安装 Element 组件库和图标插件。

pnpm install element-plus @element-plus/icons-vue

新建 assets/index.scss 并输入内容;注意: 使用 .scss 文件,请先安装对应的 loaderpnpm install sass sass-loader -D

@use "element-plus/dist/index.css";

然后在 nuxt.config.ts 中增加如下内容。

export default defineNuxtConfig({
  // 定义需要全局加载的文件。
  css: ['~/assets/index.scss'],
  build: {
    // 生产环境使用 babel 转译依赖。
    transpile: process.env.prod ? ['element-plus']: []
  }
});

最后在 app.vue (这只是示例,实际你可以在任何地方去使用) 中去使用它。

<template>
  <div id="app">
    <ElConfigProvider size="small" :z-index="3000" :local="zhCn">
      <ElButton type="primary" size="small">按钮</ElButton>
      <ElIcon :size="20">
        <Edit />
      </ElIcon>
    </ElConfigProvider>
  </div>
</template>

<script setup lang="ts">
import { ElConfigProvider, ElButton, ElIcon } from 'element-plus';
import { ElIcon } from '@element-plus/icons-vue';
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
</script>

在引用国际化时可能会有些错误,找不到模块“element-plus/dist/locale/zh-cn.mjs”或其相应的类型声明 个人目前的解决方案是在项目的根目录新建 index.d.ts 然后输入以下内容。如果有更好的方法,欢迎分享。

declare module 'element-plus/dist/locale/zh-cn.mjs'
declare module 'element-plus/dist/locale/en.mjs'

全量加载

新建 plugins/element-plus.ts 在此文件中新增如下配置,注册全局组件。

与按需加载唯一的区别可能就是每次使用时不再需要 import { * } from 'element-plus';但实际上按需加载也可以在这里面做,具体方案可以参考下面的 vant 以及 antd 的集成方案,也是我认为比较合理的方式。

import ElementPlus from 'element-plus';
import * as ElementPlusIcons from '@element-plus/icons-vue';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(ElementPlus);
  for (const [key, component] of Object.entries(ElementPlusIcons)) {
    nuxtApp.vueApp.component(key, component);
  }
});

自动导入

安装插件 unplugin-vue-components Vue 组件的按需自动导入

pnpm install unplugin-vue-components -D

然后在 nuxt.config.ts 中增加如下配置。遗憾的是 ElementPlusicon 并不支持这样的操作,或者说是我没有找到对应的方案。

import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

export default defineNuxtConfig({
  vite: {
    plugins: [
      Components({ 
        resolvers: [ElementPlusResolver()]
      })
    ],
    ssr: {
      noExternal: ['element-plus'],
    }
  }
});

vant 集成

按需加载

实际上 vant 的按需加载完全可以按照 element-plus 那样照葫芦画瓢来做,但这里示范官网比较推荐的方式来演示。

需要注意的是:vant 同样不支持样式的按需引入;ok 先安装组件库。

pnpm add vant

plugins/vant.ts 中去引入全局的样式并且注册组件。

import { Row, Col, Icon, Image, Cell, Button } from 'vant';
import 'vant/lib/index.css';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp
    .use(Row)
    .use(Col)
    .use(Image)
    .use(Icon)
    .use(Cell)
    .use(Button);
});

最后由于 nuxt 框架本身的限制,还需要在 nuxt.config.ts 添加一些配置。问题溯源

export default defineNuxtConfig({
  experimental: {
    externalVue: true
  }
});

plugins/vant.ts 中注册后的组件则不需要在像 ElementPlus 一样手动导入在使用,可以直接在项目中使用。

<template>
  <VanButton type="primary" size="small">按钮</VanButton>
</template>

全量加载

全量的加载也仅仅只是在导入组件时的语法差异上有所体现。

import Vant from 'vant';
import 'vant/lib/index.css';

export default defineNuxtPlugin(nuxtApp => {
  nuxtApp.vueApp.use(Vant);
});

viewport 移动端适配

安装 postcss-px-to-viewport 插件用来转换单位

pnpm install postcss-px-to-viewport -D   

随后在 nuxt.config.ts 中增加如下配置后重启项目即可生效。

export default defineNuxtConfig({
  postcss: {
    plugins: {
      'postcss-px-to-viewport': {
        viewportWidth: 375
      }
    }
  }
});

插件的配置信息显然不止这么多,查看更多配置请移步 文档

启动项目后在终端中可能会出现 WARN⚠️ postcss-px-to-viewport: postcss.plugin was deprecated. Migration guide: https://evilmartians.com/chronicles/postcss-8-plugin-migration

postcss-px-to-viewport 这个插件配不上 v8 版本的 postcss,换个插件即可。

pnpm install postcss-px-to-viewport-8-plugin

在使用的层面与之前完全一致包括它的配置项。

rem 移动端适配

rem 布局需要依赖两个插件配合 postcss-pxtorem 负责将像素转换为 remlib-flexible 则是用来设置基准值。

pnpm install postcss postcss-pxtorem amfe-flexible -D

nuxt.config.ts 中设置插件内容

export default defineNuxtConfig({
  postcss: {
    plugins: {
      'postcss-pxtorem': {
        rootValue: 37.5,
        propList: ['*']
      }
    }
  }
});

最后在 plugins/vant.ts 中引入 lib-flexible 设置基准值。注意该库的执行时机

import Vant from 'vant';
import 'vant/lib/index.css';
// 保证下面的代码在客户端中执行。
if (process.client) {
  import('amfe-flexible');
}
// ....

插件的配置信息显然不止这么多,查看更多配置请移步 文档

lib-flexible 有兴趣可以查看 文档

如果你的设计图不是 375 而是其他的大小可以参考 vant 官方配置

个人见解:从 lib-flexible 的文档中来看,目前来说这已经不算是一个完美的方案了,比较推荐实用 viewport 方式做移动端开发。

自动引入

安装自动引入插件 unplugin-vue-components

pnpm isntall unplugin-vue-components -D

nuxt.config.ts 中增加如下配置。

import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';

export default defineNuxtConfig({
  vite: {
    plugins: [
      Components({
        resolvers: [VantResolver({ importStyle: 'css' })]
      })
    ],
    ssr: {
      noExternal: ['vant']
    }
  },
  experimental: {
    externalVue: true
  }
});

此时如果你使用 rem 的适配方案就会出现样式的问题。还记得在 plugins/vant.ts 中导入的 lib-flexible 文件吗?改为自动导入后,实在没有合适的时机去导入这个文件,你当然可以使用 cdn 的方式去配置,比如下面这样,但我不是很推荐。

export default defineNuxtConfig({
  app: {
    head: {
      script: [
        { src: 'http://amfe-flexible' }
      ]
    }
  }
});

也许此时可以弃用 rem 的适配方案,使用 viewport 方案就不会遇到以上问题,这也是目前比较推荐的方式。

当然自动引入也会存在一些不方便的地方,比如 Toast Dialog Notify ImagePreview 这些组件的样式需要你自行去导入。文档

antd 集成

按需加载

安装组件库和图标插件。

pnpm install ant-design-vue @ant-design/icons-vue

plugins/antd.ts 中注册组件。注意:antdicon 并不是那么的纯粹,所以不能像 ElementPlus 那样批量的注册

import { Button, Col, Row } from 'ant-design-vue';
import { MessageOutlined , MessageFilled } from '@ant-design/icons-vue';
import 'ant-design-vue/dist/antd.css';

const iconMap = { MessageOutlined, MessageFilled };
export default defineNuxtPlugin((nuxtApp) => {
  // 注册组件
  nuxtApp.vueApp
    .use(Button)
    .use(Col)
    .use(Row);
  // 注册 Icon 组件
  for (let key in iconMap) {
    nuxtApp.vueApp.component(key, (iconMap as any)[key]);
  }
});

或者你也可以像 ElementPlus 那样在 .vue 文件中那样导入并且直接使用它,不过并不是很推荐那种方式。

随后在 index.vue 中使用组件。

<template>
  <div class="index-page">
    <MessageOutlined />
    <AButton type="primary" @click="showMessage">按钮</AButton>
  </div>
</template>
  
<script setup lang='ts'>
// 类似于 message 这种方法还是需要手动的导入。
import { message } from 'ant-design-vue'
  
const showMessage = () => {
  message.success({ content: 'success' });
}
</script>

全量加载

只需要修改导入的方式即可完成全量的加载。

import antd from 'ant-design-vue';
// ....

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(antd);
  // ....
}); 

自动导入

安装插件 unplugin-vue-components Vue 组件的按需自动导入

pnpm install unplugin-vue-components -D

然后在 nuxt.config.ts 中增加如下配置,注意的是像 message 这种组件的样式以及需要自行导入。

import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';

export default defineNuxtConfig({
  // message 组件的样式文件
  css: ['ant-design-vue/es/message/style/css'],
  vite: {
    plugins: [
      Components({ 
        resolvers: [AntDesignVueResolver({resolveIcons: true})]
      })
    ],
    ssr: {
      noExternal: ['moment', 'compute-scroll-into-view', 'ant-design-vue','@ant-design/icons-vue'],
    }
  }
});

tailwindcss 集成

tailwind 的集成倒是非常简单,按照 文档 跟下来就可以了,不过既然是汇总还是简单记录下。

下载相关依赖包。

pnpm install -D @nuxtjs/tailwindcss tailwindcss@latest postcss@latest autoprefixer@latest

nuxt.config.tsmodules 中注册插件。

export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss']
});

生成 tailwind.config.js,并增加 purge 选项,在生产环境可以对未使用样式的文件进行 tree shaking

# 生成配置文件
npx tailwindcss init
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './components/**/*.{vue,js}',
    './layouts/**/*.vue',
    './pages/**/*.vue',
    './plugins/**/*.{js,ts}',
    './nuxt.config.{js,ts}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

最后在 assets/css/tailwind.css 中增加以下内容(没有该文件则手动创建)。

@tailwind base;
@tailwind components;
@tailwind utilities;

其实完成上面的一系列操作后,tailwind 在项目中已经可以使用了。但是 css 文件内可能会出现 Unknown at rule @tailwindcss(unknownAtRules) 这样的错误,其实原因也很简单就是 css 不认识该语法。

根目录下新增 .vscode/settings.json 并增加以下内容解决以上问题。这属实是 🤣 物理外挂了;

{
  "css.lint.unknownAtRules": "ignore"
}

Pinia 集成

nuxt 推荐使用内部提供的 useState 方法来做 store,这确实是一个好的办法,但对于一些企业级的应用,pinia 应该更加的适合,当然 pinia 也是 nuxt 官方比较推荐的仓库管理工具。

如果你想了解更多官方推荐插件 Nuxt Module

nuxt 集成 pinia 需要两个包支持。

pnpm install @pinia/nuxt pinia

然后在 nuxt.config.tsmodules 中注册插件。更多的配置参考

export default defineNuxtConfig({
  modules: ['@pinia/nuxt']
});

至此插件就已经可以使用了,但与普通的 vue 项目中使用方式略显不同,减少了 createPinia 的步骤。

创建 /store 目录并在 /store/counter.ts 中写入示例代码。

import { defineStore } from 'pinia';

export const useCounterStore = defineStore({
  id: 'counter-store',
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: state => state.count * 2
  },
  actions: {
    countPlusOne() {
      this.count++
    }
  }
})

在文件中去使用导入并使用它。

<template>
  <div class="index-page">
    count: {{ store.count }}
    <br />
    doubleCount: {{ store.doubleCount }}
    <AButton type="primary" @click="store.countPlusOne">PlusOne</AButton>
  </div>
</template>
  
<script setup lang='ts'>
import { useCounterStore } from '~/store/count';
const store = useCounterStore();
</script>

持久化 store 集成

pinia 的持久化依赖于 pinia-plugin-persistedstate

pnpm install pinia-plugin-persistedstate

注册该插件的方式也有两种。分别为 客户端 和 ssr 两种方式。在开始之前可以先将 counterStore 中增加如下配置。更多配置参考

export const useCounterStore = defineStore({
  id: 'counter-store',
  state: () => ({/** ... */}),
  getters: {/** ... */},
  actions: {/** ... */},
  // 确认该仓库使用持久化
  persist: true
})

客户端持久化:plugins 下创建 persistedstate.client.ts 并输入以下内容。

import { createPersistedState } from 'pinia-plugin-persistedstate';

export default defineNuxtPlugin(nuxtApp => {     
  nuxtApp.$pinia.use(createPersistedState());
});

如果按照示例代码去测试是否成功,可能就碰到下面的问题了。 WeChat7b6388ed34f311a9bc64de287d11dc65.png 对应的解决方案就是需要在使用到 store 数据的模板中用 ClientOnly 组件包裹一下,类似于这样。

<ClinetOnly>
  <!-- .... -->
</ClientOnly>

ssr 持久化:plugins 下创建 persistedstate.ts 并输入以下内容。了解更多


import { createNuxtPersistedState } from 'pinia-plugin-persistedstate/nuxt';

export default defineNuxtPlugin(nuxtApp => {
  nuxtApp.$pinia.use(createNuxtPersistedState(
    useCookie, {
      cookieOptions: {
        maxAge: 3600,
        sameSite: 'strict',
      }
    }
  ));
});

异步数据获取方式

axios 暂不支持 👋🏻(并不代表不能用)。就目前(当前这个时间节点来看)无论是 @nuxtjs/axios 还是 nuxt3 自身都推荐使用 $fetch api 来获取服务端数据。但无论用哪种方式前端都绕不开一个话题。

解决跨域及官方推荐异步获取方式

跨域问题。网上找了一大堆的示例(说到这我忍不住吐槽一句做笔记之前能不能自测一下,全是教你怎么用代理,你自己试了吗?)代理这条路是行不通滴。这个问题的前世今生就看这个 issue这个也很有参考价值

  • 你用了 vite.server.proxy 的代理再使用 useFetch 就会出现 404 问题
  • 你不设置代理使用 useFetch 就会出现 CORS 问题

看官网示例中 useFetch 去请求 server/api 下的接口,这种肯定不会有跨域的情况出现,比如这样。

// server/api/calendar.ts
export default defineEventHandler(async () => {
  const res = await fetch('http://v.juhe.cn/calendar/day?date=xxx&key=xxx');
  const data = await res.json();
  return data;
});

// pages/xxx.vue
const getServerData = async () => {
  const { data } = await useFetch('api/calendar');
  console.log(data.value);
}

但是如果你采用了这种方案就意味着你的代码量会增加很多,而且还有很大的优化空间。从 nuxt rc13 开始推荐使用 devProxy,可以解决开发环境下的跨域问题。

export default defineNuxtConfig({
  // https://github.com/nuxt/framework/issues/8303 具体参考
  // 辅助参考以下 issues
  // https://github.com/nuxt/framework/discussions/1223
  // https://github.com/unjs/nitro/issues/113
  // https://github.com/nuxt/framework/issues/6650
  nitro: {
    // 更多选项 https://github.com/http-party/node-http-proxy#options
    devProxy: {
      '/api': {
        target: 'http://apis.juhe.cn',
        changeOrigin: true
      }
    }
  }
});

在使用时则可以这样编写请求代码。

const { data, refresh } = useFetch(
  '/api/simpleWeather/query',
  { 
    params: { city: '上海', key: 'xxx' }
  }
);

关于 url 开头中的 /api/ 暂时不确定能不能通过配置在生产环境去掉(后续做项目时可以试下),目前能够预想到的操作就是通过 DotENV 根据不同的环境来读取对应的 baseURL 进行异步请求。

现在需要做的其实就是将 useFetch 进行二次封装即可应用到生产环境去使用它。比如这样

type RequestOptions = {
  method?: string;
  body?: Record<string, unknown>;
  pick?: string[];
  params?: Record<string, unknown>;
};

function getBaseURL() {
  const config = useRuntimeConfig();
  return process.dev ? config.API_URL : '';
}

export const useApi = async <Result = unknown>(endpoint: string, opts?: RequestOptions) => {
  const baseURL = getBaseURL();
  const headers = useRequestHeaders(['cookie']);

  return useFetch<string, Result>(endpoint, {
    method: opts?.method,
    body: opts?.body,
    baseURL,
    headers,
    params: opts?.params
  });
};

axios 获取数据方式

很无奈 axios 并没有像 nuxt2 一样拥有官方推荐的包。但是仍然不妨碍你去使用它,只是会相对麻烦一些。

<template>
  <div class="home">
    <VanButton type="primary" @click="getAxiosData">axios</VanButton>
  </div>
</template>

<script setup lang="ts">
import axios from 'axios';

const key = 'xxx';
const getAxiosData = async () => {
  const { data } = await axios.get('/api/simpleWeather/query', { params: { key, city: '上海' } });
  console.log(data);
};
</script>

如果你真的喜欢使用 axios 用来作为你获取异步数据的主要工具,你应该封装一下。参考

目前来看我心中理想的方式就是这样,在 utils 中封装之后在 composables 中将它变成 hook 最后应用到页面中。(当然思想可能不成熟,欢迎指正)

国际化 i18n 集成

目前社区主要有两种集成的方案 ① i18n 官网集成nuxt 推荐模块集成 其中各有千秋。但前者缺少一些特性的支持,目前来看我还是更希望使用后者。

入门示例

安装指定的 npm

pnpm add @nuxtjs/i18n@next --save

nuxt.config.ts 中添加如下配置

export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],
  i18n: {
    vueI18n: {
      // 使用 compositionApi
      legacy: false,
      // 默认的语言
      locale: 'zhCN',
      // 国际化信息
      messages: {
        zhCN: {
          welcome: '欢迎'
        },
        enUS: {
          welcome: 'welcome'
        }
      }
    }
  },
});

结合前面安装的组件库,在模板中去使用它

<template>
  <div class="home">
    <VanSwitch v-model="locale" active-value="zhCN" inactive-value="enUS"></VanSwitch>
    <div style="width: 100%; height: 200px; border: 1px dashed red; box-sizing: border-box; margin-top: 10px">
      {{ $t('welcome') }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n';

const { locale, t } = useI18n();
watch(
  () => locale.value,
  (val: string) => {
    console.log(t('welcome'));
  }
);
</script>

使用此种方式表面看没有任何问题,但如果你需要做国际化的 SEO 可能会有些困难。如果你确实有这个需求,可以去了解一些更高级的内容,比如 URL 路由。

国际化本地化

上面的方式集成后,虽然能够切换语言,但当查看网页源码时并没有除默认语言之外其他语言的内容。i18n 可以使用路由的功能使国际化更好的支持 SEO;此时其他语言就会以 举例: /en 的下展示

nuxt.config.ts 中新增如下配置

export default defineNuxtConfig({
  locales: [
    'zn-CN', 'en-US'
  ],
  defaultLocale: 'zh-CN',
});

切换语言的方式随之也要进行改变。

<template>
  <div>
    <NuxtLink :to="switchLocalePath('zh-CN')">中文</NuxtLink>
    <NuxtLink :to="switchLocalePath('en-US')">英文</NuxtLink>
    <div>{{ $t('welcome') }}</div>
  </div>
</template>

<script setup lang="ts">
const switchLocalePath = useSwitchLocalePath();
</script>

再次提示:如果你需要深度学习可以去 官网

代码及git规范

git 提交规范的问题,推荐使用 commitizenhusky 去做这件事情,如果你没有任何头绪可以参考 基于Vue3 + TS + Vite代码提交及代码风格规范

eslint 集成

eslint 推荐使用官网提供方式。nuxt eslint 配置 或者你也可以持续关注这个 issue 它可能给你带来不一样的集成方式。

安装相关 npm

pnpm add eslint @nuxtjs/eslint-config-typescript -D
# 不能下载到 devDependencies 中,不然 eslint 无法工作
pnpm add typescript 

项目根目录下新增 .eslintrc 文件配置规则。

{
  "env": {
    "node": true,
    "browser": true
  },
  "extends": [
    "@nuxtjs/eslint-config-typescript"
  ],
  // 新的规则可以配置在这里。
  "rules": {}
}

并修改 vscode 的配置,就可以实现保存自动 format code 目前来看 nuxt 是不推荐使用 prettier 对代码进行美化,同样不喜欢的还有另一位大佬 antfu,具体原因推荐查看 antfu博客:为什么我不用Prettier

"prettier.enable": false,
"editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
  "source.fixAll": false,
  "source.fixAll.eslint": true
},

或者我们也可以集成其他预设,@antfu/eslint-config 还是比较友好的,毕竟跟随大佬脚步。修改 .eslintrc

{
  "env": {...},
  "extends": [
    "@nuxtjs/eslint-config-typescript",
    "@antfu"
  ]
  ...
}

stylelint 集成

scss 为例,假如你使用 less 也可以作为参考,不过推荐看 vben 项目作为参考。

安装相关包

pnpm install stylelint postcss-html postcss-scss stylelint-config-recommended-vue stylelint-config-standard-scss stylelint-config-recess-order -D
  • stylelint-config-standard-scssscss 版本的基础配置;
  • stylelint-config-recommended-vue:有关 stylelint-config-recommended-vue 的配置;
  • stylelint-config-recess-ordercss 属性排序插件;
  • stylelint postcss-html postcss-scss:基准包及依赖包。

在项目根目录新建 stylelint.config.js 输入以下内容;更多配置参考stylelint 中文

module.exports = {
  root: true,
  extends: [
    'stylelint-config-standard-scss',
    'stylelint-config-recommended-vue',
    'stylelint-config-recess-order',
  ],
  customSyntax: 'postcss-html',
  overrides: [
    {
      files: ['**/*.{css,scss,vue}'],
      customSyntax: 'postcss-scss',
    },
  ],
  rules: {
    'at-rule-no-unknown': null,
    'scss/at-rule-no-unknown': [true,
      {
        ignoreAtRules: [
          'tailwind',
          'apply',
          'variants',
          'responsive',
          'screen',
          'function',
          'if',
          'each',
          'include',
          'mixin',
        ],
      },
    ],
    'selector-pseudo-class-no-unknown': [true,
      { ignorePseudoClasses: ['v-deep'] },
    ],
  },
  ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
}

安装 vscode 插件 stylelint。然后在项目中 .vscode/settings.json 中增加如下配置。配合上面 eslint 的相关配置即可实现 **/*.{css,scss,vue} 文件中样式的保存格式化。

{
  "css.lint.unknownAtRules": "ignore",
  "scss.lint.unknownAtRules": "ignore",
  "stylelint.validate": ["css","scss",],
}

当然你也可以把配置直接放在配置文件中,但是为了团队的协同,个人还是推荐放到项目的根目录下,但后面又引发的一个问题,我不确定把 .vscode/ 放到仓库中是否是比较正确的选择。