简介:该教程兼容pc 移动端,兼容原理:同时封装了pc框架element-plus、和移动端框架vant,根据不同客户端类型渲染不同的组件,同一个组件需要根据客户端类型同时封装pc和移动端,如果觉得开发麻烦,可忽略兼容部分教程,根据需要分别构建pc、和移动端
nuxt3:https://www.nuxt.com.cn/docs/guide/directory-structure/pages
vue3:https://cn.vuejs.org/
nuxt.config:https://www.nuxt.com.cn/docs/api/configuration/nuxt-config
elementPlus:https://element-plus.gitee.io/zh-CN/
vant:https://vant-contrib.gitee.io/vant/#/zh-CN
axios:https://www.axios-http.cn/docs/api_intro
一、构建项目
访问https://codeload.github.com/nuxt/starter/tar.gz/refs/heads/v3,下载压缩报解压到项目下,或者运行npx nuxi@latest init vue_common,需把typescript更新为4.3.5版本
yarn install 安装依赖
yarn dev -o 启动
二、路由,新建pages文件夹
<template>
2131
</template>
<template>
<NuxtPage/>
</template>
<template>
demo父级页面
<NuxtPage/>
</template>
<script lang="ts" setup>
definePageMeta({
title: 'My home page'
})
</script>
//demo>test>index.vue
<template>
demo测试页
</template>
<script lang="ts" setup>
const route = useRoute()
console.log(route.meta.title)
</script>
浏览器访问:http://localhost:3000/demo/test
//demo>test1>index.vue
<template>
demo测试页1
</template>
浏览器访问:http://localhost:3000/demo/test1
<template>
动态路由1:{{ $route.params.group }} - {{ $route.params.id }}
</template>
浏览器访问:http://localhost:3000/dynamicGroup/123
三、根目录下创建静态资源文件 assets
<i class="iconfont icon-usename"></i>
yarn add sass
/**
* 涉及全局px需要在此,定义样式,并在globals引用,用来兼容pc和移动,不用兼容可以直接写在 globals,也可以全局引入@import "globalsIndex"
*/
body{
font-size: $font_size
}
/**
* 涉及px单位的,需要做pc和移动端的兼容;不需要兼容,可去掉此代码;
*/
.pc{
@import "globalsIndex";
}
.mobile{
@import "globalsIndex";
}
$font_size: 14px;//基础字体大小
@import "./variable";
@import "./globals";
@import "./normalize";
@import "../font/iconfont.css";
export default defineNuxtConfig({
...
css: [
"@/assets/css/iframe.scss"
],
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "assets/css/variable.scss";',
}
}
}
},
...
})
export default defineNuxtConfig({
...
alias: {
"@img": "~assets/image/",
},
...
})
<img src="@img/idCard.png"/>
四、状态管理器 pinia https://pinia.web3doc.top/introduction.html
yarn add @pinia/nuxt --dev
export default defineNuxtConfig({
...
modules: ["@pinia/nuxt"], //配置Nuxt3的扩展的库
...
})
export default defineNuxtConfig({
...
alias: {"@store":'store'},
...
})
import { defineStore } from "pinia";
interface IsMobileinterface {
isMobile: boolean;
}
const useMobile = defineStore("mobile", {
state: ():IsMobileInterface => {
return {
isMobile: false,
};
}
});
export default useMobile
//app.vue
<template>
<NuxtPage/>
</template>
<script lang="ts" setup>
import {onMounted} from "vue";
import {storeToRefs} from "pinia";
import useMobile from "@store/useMobile";
const mobileStore = useMobile();
let {isMobile} = storeToRefs(mobileStore);
onMounted(()=>{
//判断是哪个客户端(pc,mobile),主要用来兼容样式
if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) {
//增加全局class,用于设置全局样式
document.getElementsByTagName('html')[0].className = 'mobile';
isMobile.value = true;
}else{
//增加全局class,用于设置全局样式
document.getElementsByTagName('html')[0].className = 'pc';
isMobile.value = false;
}
});
</script>
yarn add @pinia-plugin-persistedstate/nuxt --dev
export default defineNuxtConfig({
...
modules: [ //配置Nuxt3的扩展的库
"@pinia/nuxt",
'@pinia-plugin-persistedstate/nuxt',
],
...
})
//localStorage
import { defineStore } from "pinia";
interface IsMobileInterface {
isMobile: boolean;
}
const useMobile = defineStore("mobile", {
state: ():IsMobileInterface => {
return {
isMobile: false,
};
},
persist: {
storage: persistedState.localStorage,
}
});
export default useMobile
//sessionStorage
import { defineStore } from "pinia";
interface IsMobileInterface {
isMobile: boolean;
}
const useMobile = defineStore("mobile", {
state: ():IsMobileInterface => {
return {
isMobile: false,
};
},
persist: {
storage: persistedState.sessionStorage,
}
});
export default useMobile
//cookie
import { defineStore } from "pinia";
interface IsMobileInterface {
isMobile: boolean;
}
const useMobile = defineStore("mobile", {
state: ():IsMobileInterface => {
return {
isMobile: false,
};
},
persist: {
storage: persistedState.cookiesWithOptions({
sameSite: false,
}),
}
});
export default useMobile
export default defineNuxtConfig({
...
modules: [
"@pinia/nuxt",
"@pinia-plugin-persistedstate/nuxt"
],
piniaPersistedstate: {
cookieOptions: {
sameSite: "strict",
},
storage: "localStorage"//默认储存方式
},
...
})
五、集成postcss
yarn add postcss-px-to-viewport-8-plugin --dev
export default defineNuxtConfig({
...
postcss: {
plugins: {
"postcss-px-to-viewport-8-plugin": {
viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度
viewportHeight: 912, // 视窗的高度,对应的是我们设计稿的高度
unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
propList: ['*'],
selectorBlackList: [/^.pc/],
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false, // 允许在媒体查询中转换`px`,
exclude: [/pc[\s\S]*.vue/,/element-plus/,/elementTheme.scss/], //设置忽略文件,用正则做目录名匹配
}
}
}
...
})
参数 | 说明 | 类型 | 默认值 |
unitToConvert | 需要转换的单位,默认为 px | string | px |
viewportWidth | 设计稿的视口宽度,如传入函数,函数的参数为当前处理的文件路径,函数返回 undefind 跳过转换 | number | Function | 320 |
unitPrecision | 单位转换后保留的精度 | number | 5 |
propList | 能转化为 vw 的属性列表 | string[] | ['*'] |
viewportUnit | 希望使用的视口单位 | string | vw |
fontViewportUnit | 字体使用的视口单位 | string | vw |
selectorBlackList | 需要忽略的 CSS 选择器,不会转为视口单位,使用原有的 px 等单位 | string[] | [] |
minPixelValue | 设置最小的转换数值,如果为 1 的话,只有大于 1 的值会被转换 | number | 1 |
mediaQuery | 媒体查询里的单位是否需要转换单位 | boolean | false |
replace | 是否直接更换属性值,而不添加备用属性 | boolean | true |
landscape | 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape) | boolean | false |
landscapeUnit | 横屏时使用的单位 | string | vw |
landscapeWidth | 横屏时使用的视口宽度,,如传入函数,函数的参数为当前处理的文件路径,函数返回 undefind 跳过转换 | number | 568 |
exclude | 忽略某些文件夹下的文件或特定文件,例如 node_modules 下的文件,如果值是一个正则表达式,那么匹配这个正则的文件会被忽略,如果传入的值是一个数组,那么数组里的值必须为正则 | Regexp | undefined |
include | 需要转换的文件,例如只转换 'src/mobile' 下的文件 ( include: /\/src\/mobile\// ),如果值是一个正则表达式,将包含匹配的文件,否则将排除该文件, 如果传入的值是一个数组,那么数组里的值必须为正则 | Regexp | undefined |
六、配置页面标题、icon、meta等
export default defineNuxtConfig({
...
app: {
head: {
title: "项目标题",
meta: [
{name: "description", content: "项目的重点信息描述--" },
{name: "keywords", content: "项目关键词" },
{name:"applicable-device", content:"pc,mobile"}, // 移动pc适配
],
link: [{
rel:"icon",
type: "image/x-icon",
href:"/new_favicon.ico"
}],
}
}
...
})
七、集成ui框架
yarn add vant
yarn add @vant/nuxt --dev
export default defineNuxtConfig({
modules: [
"@vant/nuxt"
],
vant: { /** Options */ }
})
<van-button type="primary" @click="showToast('toast')">button</van-button>
:root:root {
--van-primary-color: red;
}
//iframe.scss
@import "./vanTheme";
@import "./globals";
@import "./normalize";
@import "../font/iconfont.css";
yarn add element-plus
yarn add @element-plus/nuxt --dev
export default defineNuxtConfig({
...
modules: [
"@element-plus/nuxt"
],
elementPlus: { /** Options */ }
...
})
<el-button type="primary">button</el-button>
:root:root{
--el-color-primary: green;
}
//iframe.scss
@import "./elementTheme";
@import "./globals";
@import "./normalize";
@import "../font/iconfont.css";
<template>
<el-button class="button">
<slot/>
</el-button>
</template>
<style lang="scss" scoped>
@import "pc";
</style>
//pc.scss
@import "index";
<template>
<van-button class="button">
<slot/>
</van-button>
</template>
<style lang="scss" scoped>
@import "mobile";
</style>
//mobile.scss
@import "index";
<template>
<ClientOnly>
<PcButton v-if="!mobileStore.isMobile">
<slot/>
</PcButton>
<MobileButton v-else>
<slot/>
</MobileButton>
</ClientOnly>
</template>
<script lang="ts" setup>
const mobileStore = useMobile();
</script>
.button{
font-size: 14px;
height: 32px;
padding: 4px 15px;
border-radius: 6px;
}
export default defineNuxtConfig({
...
components: [{
path: '~/components/',
pathPrefix: false, //只根据名称使用组件
}],
...
})
<Button>button</Button>
八、构建布局页面Layouts
布局放在layouts目录中,使用时将通过异步导入自动加载。布局是通过添加到的app.vue,或者手动指定它作为的一个prop,或者在pages页改变它
<template>
一些在所有页面共享的默认布局
<slot />
</template>
//app.vue
<template>
<NuxtLayout>
<NuxtPage/>
</NuxtLayout>
</template>
<template>
<slot name="header"/>
自定义布局
<slot />
</template>
//app.vue
<template>
<NuxtLayout name="custom">
<NuxtPage/>
</NuxtLayout>
</template>
//index.vue
<template>
<Button>button</Button>
</template>
<script lang="ts" setup>
definePageMeta({
layout: "custom",
});
</script>
//index.vue
<template>
<Button @click="enableCustomLayout">button</Button>
</template>
<script lang="ts" setup>
function enableCustomLayout () {
setPageLayout('custom')
}
definePageMeta({
layout: false,
});
</script>
//index.vue
<template>
<NuxtLayout name="custom">
<template #header>重写布局</template>
当前页面
</NuxtLayout>
</template>
<script lang="ts" setup>
definePageMeta({
layout: false,
});
</script>
九、服务端目录service
server/api中的文件在它们的路由中会自动以/api作为前缀。 对于添加没有/api前缀的服务器路由,可以将它们放到 server/routes目录中,每个文件都应该导出一个用defineEventHandler()定义的默认函数,处理程序可以直接返回JSON数据,一个Promise或使用event.node.res.end()发送响应
export default defineEventHandler(() => 'service demo')
浏览器:http://localhost:3000/routes
页面:await useFetch('/routes')
export default defineEventHandler(() => 'service api demo')
浏览器:http://localhost:3000/api/demo
页面:await useFetch('/api/demo')
//routes>routes.post.ts
export default defineEventHandler(() => 'service router post")
//api>demo.post.ts
export default defineEventHandler(() => 'service api post")
页面:await useFetch('/routes',{ method: 'post'})
页面:await useFetch('/api/demo',{ method: 'post'})
//routes>par>[name].post.ts
import {H3Event,H3EventContext} from "h3";
export default defineEventHandler((event:H3Event) => {
let params = event.context.params as H3EventContext;
return `service router post ${params.name}`
});
//api>[name].post.ts
import {H3Event,H3EventContext} from "h3";
export default defineEventHandler((event:H3Event) => {
let params = event.context.params as H3EventContext;
return `service api post ${params.name}`
});
页面:await useFetch('par/routerName',{ method: 'post'});
页面:await useFetch('/routerName',{ method: 'post'});
import { createRouter, defineEventHandler, useBase } from "h3";
const router = createRouter()
router.get('/slugGet', defineEventHandler(() => 'slugGet'))
router.post('/slugPost', defineEventHandler(() => 'slugPos'))
export default useBase('/api/slug', router.handler);
浏览器:http://localhost:3000/api/slug/slugGet
页面:await useFetch('/api/slug/slugGet');
页面:await useFetch('/api/slug/slugPost',{ method: 'post'});
import {H3Event, H3EventContext} from "h3";
export default defineEventHandler(async (event:H3Event) => {
const body = await readBody(event);
const query = getQuery(event);
let params = event.context.params as H3EventContext;
const config = useRuntimeConfig();
const cookies = parseCookies(event)
const id = parseInt(params.id) as number;
// if (!Number.isInteger(id)) {
// throw createError({
// statusCode: 400,
// statusMessage: "错误处理",
// })
// }
// setResponseStatus(200, '21321')
return {
text:`service router post ${params.name}`,
params,
body,
query,
config,
cookies
}
});
页面:await useFetch('api/apiName',{
method: 'post',
body: { test: 'body' },
query:{ test: 'query' }
});
中间件处理程序将在每个请求上运行,再运行任何其他服务器路由,以添加或检查标头、记录请求或扩展事件的请求对象,相当于api拦截器
export default defineEventHandler((event) => {
console.log('New request: ' event.node.req.url)
})
//nuxt.config.ts
export default defineNuxtConfig(
...
nitro: {
storage: {
redis: {
driver: "redis",
port: 3000,
host: "localhost",
username: "",
password: "",
db: 0,
tls: {}
}
}
}
...
});
export default defineEventHandler(async (event) => {
const body = await readBody(event)
let params = event.context.params as H3EventContext;
await useStorage().setItem('redis:Storage', body)
return `service router post ${params.name}`
})
export default defineEventHandler(async (event) => {
const data = await useStorage().getItem('redis:Storage')
return data
});
页面:
const { data: resDataSuccess } = await useFetch('/api/apiName', {
method: 'post',
body: { text: 'storage' }
})
const { data: resData } = await useFetch('/api/demo')
yarn add axios
页面:import axios from "axios";
await axios.post("http://localhost:3000/api/apiName",{ test: 'axios' })
/**
* @description axios公共请求封装
* */
import axios, {AxiosResponse, InternalAxiosRequestConfig} from "axios";
/**
* @description 定义相关接口或者枚举
* */
//请求枚举
export enum MethodEnum {
Get='GET',
Post='POST'
}
//返回结果
export interface ResponseResultInterface<Body> {
Header:{},
Body: Body
}
//请求参数
export interface RequestInterface<params> {
url:string,
params?:params,
method?:MethodEnum
}
/**
* 封装axios
* */
// 添加请求拦截器
axios.interceptors.request.use( (config:InternalAxiosRequestConfig)=>{
return config;
}, (error)=>{
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use( (response:AxiosResponse) => {
return response;
}, (error) => {
return Promise.reject(error);
});
/**
* @method useAxiosRequest axios请求封装
* @param requestPar { RequestInterface } 请求url
* @return Promise
* */
const useAxiosRequest= async <params,responseData>(requestPar:RequestInterface<params>):Promise<responseData> => {
const requestResult:AxiosResponse = await axios({
method: requestPar.method || MethodEnum.Post,
url: requestPar.url,
data: requestPar.params
});
return requestResult.data as responseData;
};
export default useAxiosRequest;
/**
* @description useFetch请求封装
* */
import {FetchContext} from "ofetch";
import {_AsyncData} from "#app/composables/asyncData";
/**
* @description 定义相关接口或者枚举
* */
//请求枚举
export enum MethodEnum {
Get='GET',
Post='POST'
}
//返回结果
export interface ResponseResultInterface<Body> {
Header:{},
Body: Body
}
export interface RequestInterface<params>{
url:string,
method?: MethodEnum,
params?: params
}
/**
* @method useRequest useFetch请求封装
* @param requestPar {RequestInterface} 请求参数
* */
const useRequest = async <params,responseData>(requestPar:RequestInterface<params>):Promise<responseData>=>{
const requestResult:_AsyncData<any, any> = await useFetch(requestPar.url,{
method:requestPar.method || MethodEnum.Post,
params:requestPar.params || {},
//请求拦截
onRequest(requestOption:FetchContext):void {
// options.headers = options.headers || {}
},
//错误请求拦截
onRequestError(RequestErrorOption:FetchContext):void {
// throw createError({
// statusCode: 500,
// statusMessage: reqUrl,
// message: '自己后端接口的报错信息',
// })
},
//响应拦截
onResponse(ResponseOption:FetchContext):void {
},
//响应错误拦截
onResponseError(responseErrorOption:FetchContext):void {
// throw createError({
// statusCode: 500,
// statusMessage: reqUrl,
// message: '自己后端接口的报错信息',
// })
}
});
return requestResult.data.value as responseData;
}
export default useRequest;
十、插件plugins
自动plugins目录中的文件,并在创建Vue应用程序时加载它们。可以在文件名中使用.server或.client后缀来只在服务器端或客户端加载插件。
export default defineNuxtPlugin(nuxtApp => {
return {
provide: {
demo: (msg: string) => `demo ${msg}`
}
}
});
页面:<template>
<div>插件:{{ $demo('插件') }}</div>
</template>
<script lang="ts" setup>
const { $demo } = useNuxtApp()
</script>
export default defineNuxtPlugin(nuxtApp => {
return {
provide: {
demo1: (msg: string) => `demo1 ${msg}`
}
}
});
export default defineNuxtPlugin(nuxtAp => {
return {
provide: {
demo2: (msg: string) => `${nuxtApp.$demo1('demo2')} ${msg}`
}
}
});
export default defineNuxtPlugin((nuxtApp) => {
// 客户端 & 服务端
nuxtApp.hook("app:created", (vueApp) => {
console.log("app:created");
});
// 服务端
nuxtApp.hook("app:beforeMount", (vueApp) => {
console.log("app:beforeMount");
});
// 客户端 & 服务端
nuxtApp.hook("vue:setup", () => {
console.log("vue:setup");
});
// 服务端
nuxtApp.hook("app:rendered", (renderContext) => {
console.log("app:rendered");
});
// 客户端
nuxtApp.hook("app:mounted", (vueApp) => {
console.log("app:mounted");
});
});
import {DirectiveBinding, VNode} from "@vue/runtime-core";
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.directive('focus', {
// 在绑定元素的 attribute 或事件监听器被应用之前调用
created:(el)=>{},
// 在绑定元素的父组件挂载之前调用
beforeMount:(el)=>{},
// 在包含组件的 VNode 更新之前调用
beforeUpdate:(el)=>{},
// 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
updated:(el)=>{},
// 在绑定元素的父组件卸载之前调用
beforeUnmount:(el)=>{},
// 卸载绑定元素的父组件时调用
unmounted:(el)=>{},
// 绑定元素的父组件被挂载时调用
mounted:(el)=>{
el.focus()
},
//渲染时向元素添加属性
getSSRProps:(binding:DirectiveBinding, vnode:VNode) =>{
return {
id:'test'
}
}
})
});
页面:<input v-focus/>
十一、创建公共hooks
Nuxt 3使用composables/目录,将hooks自动导入vue程序
export default defineNuxtConfig({
...
imports: {
dirs:
"composables/**"
]
},
...
});
页面使用:const mobileStore = useMobile();//相比之前不需要在import导入
十二、其他
#开发环境配置
VITE_HOST=http://127.0.0.1:3000/ #本地请求ip 端口
VITE_PROXY=http://127.0.0.1 #需要代理的ip
VITE_PROXY_3002=3002 #需要代理的端口
import{loadEnv}from"vite";
/**
*@description定义env接口*/
interfaceENV_CONFIG{
host:string
host1:string
}
const envScript=(process.envasany).npm_lifecycle_script.split('');
const envName=envScript[envScript.length-1];//通过启动命令区分环境
const envData=loadEnv(envName,'env') as ENV_CONFIG|unknown;
export default defineNuxtConfig({
...
runtimeConfig: {
public: envData // 把env放入这个里面,通过useRuntimeConfig获取
},
vite: {
envDir: '~/env', // 指定env文件夹
},
...
});
页面:const envConfig = useRuntimeConfig();
package.json配置:"dev": "nuxt dev --mode dev",
import{getRequestHeaders, getRequestURL, H3Event, HTTPMethod} from "h3";
const {public: {
VITE_PROXY,
VITE_PROXY_3002
}} =useRuntimeConfig();
interfaceApiInterface{
[propName:string]:string
}
//把已有的ip和端口。组成请求的url
constapiUrlOption:ApiInterface ={
3002:`${VITE_PROXY}:${VITE_PROXY_3002}`
}
//请求路由配置,getArticles是后端提供的api
constapiRouter:ApiInterface ={
"/getArticles":`${apiUrlOption["3002"]}/getArticles`
};
export defaultdefineEventHandler(async (event:H3Event):Promise<any> => {
const urlInfo:URL = getRequestURL(event);
const requestUrl:string =apiRouter[urlInfo.pathname];
if(requestUrl){
const method:HTTPMethod =getMethod(event);
// const query:QueryObject =getQuery(event);
const headers =getRequestHeaders(event);
const options: any = {
responseType: 'json',
}
options.headers = headers;
options.method = method;
if (method != 'GET') {
options.body = awaitreadBody(event);
}
return await $fetch(requestUrl,options);
}
});
页面:const envConfig:RuntimeConfig = useRuntimeConfig();
await useFetch(`${VITE_HOST}getArticles`)
十三、项目部署
server {
listen 9001;
server_name localhost;
# server_name btyhub.site, www.btyhub.site;
# ssl两个文件,放在 nginx的conf目录中
# ssl_certificate btyhub.site_bundle.pem;
# ssl_certificate_key btyhub.site.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# 代理到Next的服务,默认3000端口,也可以在start的时候指定
location / {
proxy_pass http://127.0.0.1:7000/;
}
}
{
"name": "nuxt-app",
"private": true,
"scripts": {
"start": "cross-env PORT=7000 HOST=127.0.0.1 node .output/server/index.mjs"
}
}
let exec = require("child_process").exec;
exec("yarn start", {windowsHide: true});
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved