主要用于整理开发过程中遇到的问题,以及在我开发过程中,需要用到的模块的开发,我觉得重要的,我就记录一下!
vue3.0+ts
rem布局
更好适应不同移动端scss
swiper.js
首页轮播+部分内容滑动+搜索页 tabnode.js
基础
基础
完成
完成
完成
完成
完成
完成
完成
完成
完成
完成
完成
未开始
未开始
完成
开发中
未开始
未开始
此处主要展示某些模块或者组件的实现方式;
安装 vue cli 最新版
npm install -g @vue/cli
# OR
yarn global add @vue/cli
使用 cli 创建项目
vue create appName
此时会有弹出 Please pick a preset,如果以前没有安装过,请选择 Manually select features
后回车;因为我们需要设置某系我们需要的配置!
现在提示:Check the features needed for your project , 这里是上下键切换后 按空格进行选择或者取消,这里我们需要选择的比较多;
Choose Vue version
选择版本Bable
将 ES6 编译成 ES5TyperScript
JS 超集,主要是类型检查Router
路由Vuex
状态管理CSS Pre-processors
css 预编译 (稍后会对这里进行配置)Liner / Formatter
代码检查工具这里几个都有*号后回车 进入下一步
选择 3.x 后回车
进入 Use class-style component syntax? 这里输入 Y
是否使用 Class 风格装饰器?
即原本是:home = new Vue()
创建 vue 实例
使用装饰器后:class home extends Vue{}
进入 Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? 这里输入 Y
进入 Use history mode for router 路由使用历史模式? (y/n 都可) 我输入 n
进入 Pick a CSS pre-processor 这里看你习惯选择 我们选择 Sass/SCSS (with node-sass)
进入 Pick a linter / formatter config 选择 ESLint with error prevention only
进入 Pick additional lint features 代码检查方式 选择 Lint on save
进入 Where do you prefer placing config for… 选择 In package.json
进入 Save this as a preset for future projects? 是否在以后的项目中使用以上配置? 可以保存 可以不保存 此处是为了下次直接使用 跳过选择 (我选择否 n)
开始安装搭建 (如果是首次创建 会让你选择使用 npm 还是 yarn 进行安装)
安装完成后
cd xxxx
npm run serve #即可运行项目
首先安装对应的依赖;
npm install amfe-flexible --save
npm install postcss-px2rem --save
在 main.ts 中引入
import "amfe-flexible";
在 package.json 中配置 px2rem-loader
"postcss": {
"plugins": {
"autoprefixer": {},
"postcss-px2rem": {
"remUnit": 37.5 //这里指的是设计稿的宽度 (370 / 10)
}
}
},
配置完成后 重新启动项目就好啦, 使用过程中 px 会转换成 rem 如果没有 vscode 插件去改格式的话,可以使用大写的 px 保留不被转化;
如果使用的是vscode
且需要 rem 提示的话 需要安装一个插件 cssrem
这里不介绍此插件用法
很多时候,某个组件我们会在很多地方使用,每次都需要进行组件的引入,此时我们需要设置对应的全局组件,以供我们在需要时调用!
首先我们做好准备 在utils/types.ts
中定义一种类型 withInstall
type withInstall<T> = T & { install(app: App): void };
export { withInstall };
写好组件后 需要在组件目录下创建一个 index.ts 文件对组件进行导出 比如我们的 input 组件
input.jsx
组件文件,index.scss
样式文件,
import { App } from "vue";
import { withInstall } from "../utils/types";
import MInput from "./input";
MInput.install = (app: App) => {
app.component(MInput.name, MInput);
};
export default MInput as withInstall<typeof MInput>;
在我们组件的位置 例如 /src/components 下创建index.ts
文件
import { App } from "vue";
import MInput from "./input/index"; //自定义的组件的位置
const components = [MInput]; //可以多个组件
export { MInput }; //可以单个导出
// 注册组件
export default function(app: App) {
components.forEach((item) => app.component(item.name, item));
}
在 mian.ts 中引入/src/components/index.ts
import { createApp } from "vue";
import App from "./App.vue";
import { MInput } from "./components";
createApp(App)
.use(MInput)
.mount("#app");
这样就注册全局成功了,但是我这种时属于写 ui 组件库的时候的用法,可以随时添加你想变成全局组件的组件,方便注册!
此处的通过方法调用,就和ElementUI
的
this.$comfirm(message, title, { xxx: xxx }); //使用confirm 方法一致
温馨说明:我根据 vant 的 dialog 组件的写法来写的 confirm 组件
首先创建我们的 confirm 组件,代码在下面 我不做过多讲解,需要引入我们创建的另外一个 popup 组件 结合使用,此处请看代码,有点乱 还没完整 半成品
import { defineComponent, PropType, CSSProperties, ref } from "vue";
import Popup from "../Popup/Popup";
import "./index.scss";
import { ClickEventFuncType } from "@/utils/types";
export type ConfirmAction = "confirm" | "cancel";
export type ConfirmMessage = string | (() => JSX.Element);
export type ConfirmMessageAlign = "left" | "center" | "right";
export default defineComponent({
name: "Confirm",
props: {
callback: Function as PropType<(action?: ConfirmAction) => void>,
modelValue: {
type: Boolean,
default: false,
},
title: String,
width: [Number, String],
message: [String, Function] as PropType,
messageAlign: String as PropType,
},
emits: ["confirm", "cancel", "update:modelValue"],
setup(props, { emit, slots }) {
const updateShow = (value: boolean) => emit("update:modelValue", value);
const close = (action: ConfirmAction) => {
updateShow(false);
if (props.callback) {
props.callback(action);
}
};
const getActionHandler = (action: ConfirmAction) => () => {
// should not trigger close event when hidden
if (!props.modelValue) {
return;
}
emit(action);
close(action);
};
const onCancel = getActionHandler("cancel");
const onConfirm = getActionHandler("confirm");
const popStyle = ref({
width: "6.5rem",
borderRadius: "0.35rem",
overflow: "hidden",
backgroundColor: "#F1F0F0",
textAlign: "center",
});
return () => {
const { modelValue, title, message } = props;
return (
null,
}}
>
{title}
取消
确定
);
};
},
});
第二步创建一个另外的文件 confirm-function-call.tsx
此处某些东西原本是定义在 untils/index.ts 下面的 但是为了方便 此时全部定义到这个文件当中,后面可以自行抽离
首先引入相关模块
import {
App,
Component,
createApp,
reactive,
nextTick,
getCurrentInstance,
ComponentPublicInstance,
} from "vue";
import MConfirm, {
ConfirmAction,
ConfirmMessage,
ConfirmMessageAlign,
} from "./Confirm"; //这里是组件需要的
定义相关的函数 和 类型
export const extend = Object.assign;
export const inBrowser = typeof window !== "undefined";
const camelizeRE = /-(\w)/g;
export function camelize(str: string): string {
return str.replace(camelizeRE, (_, c) => c.toUpperCase());
}
export type ComponentInstance = ComponentPublicInstance<{}, any>;
export type Interceptor = (...args: any[]) => Promise | boolean;
export type WithInstall = T & {
install(app: App): void;
} & EventShim;
//注册组件
export function withInstall(options: T) {
(options as Record).install = (app: App) => {
const { name } = options as any;
app.component(name, options);
app.component(camelize(`-${name}`), options);
};
return options as WithInstall;
}
//代码虽然写在这 但是自己却并没有真正搞懂
export function useExpose>(apis: T) {
const instance = getCurrentInstance(); //获取到组件实例
if (instance) {
extend(instance.proxy, apis);
}
}
// 其实这里在vant 中是定义popup的 但是被我改成了我的confirm
// 这里需要注意一个点就是 这个modelValue 在我们外面组件化使用时 就是v-model
export function usePopupState() {
const state = reactive<{
modelValue: boolean;
[key: string]: any;
}>({
modelValue: false,
});
const toggle = (modelValue: boolean) => {
state.modelValue = modelValue;
};
const open = (props: Record) => {
extend(state, props);
nextTick(() => toggle(true));
};
const close = () => toggle(false);
useExpose({ open, close, toggle });
return {
open,
close,
state,
toggle,
};
}
// 此处就是挂载组件了 具体可以看代码 很明了
export function mountComponent(RootComponent: Component) {
const app = createApp(RootComponent);
const root = document.createElement("div");
document.body.appendChild(root);
return {
instance: app.mount(root),
unmount() {
app.unmount();
document.body.removeChild(root);
},
};
}
具体的对组件的操作
export type ConfirmOptions = {
title?: string;
width?: string | number;
message?: ConfirmMessage;
beforeClose?: Interceptor;
teleport?: string;
messageAlign?: ConfirmMessageAlign;
cancelButtonText?: string;
showCancelButton?: boolean;
showConfirmButton?: boolean;
cancelButtonColor?: string;
confirmButtonText?: string;
confirmButtonColor?: string;
};
let instance: ComponentInstance;
function initInstance() {
const Wrapper = {
setup() {
const { state, toggle } = usePopupState();
return () => (
);
},
};
({ instance } = mountComponent(Wrapper));
}
function Confirm(options: ConfirmOptions) {
/* istanbul ignore if */
if (!inBrowser) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
if (!instance) {
initInstance();
}
instance.open(
extend({}, Confirm.currentOptions, options, {
callback: (action: ConfirmAction) => {
(action === "confirm" ? resolve : reject)(action);
},
})
);
});
}
Confirm.defaultOptions = {
title: "",
width: "",
message: "",
callback: null,
teleport: "body",
beforeClose: null,
messageAlign: "",
cancelButtonText: "",
cancelButtonColor: null,
confirmButtonText: "",
confirmButtonColor: null,
showConfirmButton: true,
showCancelButton: false,
};
Confirm.currentOptions = extend({}, Confirm.defaultOptions);
Confirm.alert = Confirm;
Confirm.confirm = (options: ConfirmOptions) =>
Confirm(extend({ showCancelButton: true }, options));
Confirm.close = () => {
if (instance) {
instance.toggle(false);
}
};
Confirm.setDefaultOptions = (options: ConfirmOptions) => {
extend(Confirm.currentOptions, options);
};
Confirm.resetDefaultOptions = () => {
Confirm.currentOptions = extend({}, Confirm.defaultOptions);
};
Confirm.Component = withInstall(MConfirm);
Confirm.install = (app: App) => {
app.use(Confirm.Component);
app.config.globalProperties.$confirm = Confirm;
};
export { Confirm };
上述代码时可以使用的,但是自身并不是理解得透彻, 使用时很简单
import { Confirm } from "@/components/Confirm";
const MConfirm = Confirm.Component; //这样可以根据组件的方法去使用
//tsx中使用:
//函数使用
Confirm.confirm({
title: "dadsa",
message: "dddd",
})
.then((res) => {
console.log("点击了确定");
})
.catch(() => {
console.log("点击了取消");
});
我们使用最简单的方法,但是该方法无法有滚动的效果
代码也很简单 在你每个导航下面新增一下内容,需要注意的是本身需要设置相对定位
div {
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 100%;
width: 0;
height: 2px;
background-color: #000;
transition: all 0.3s ease-out;
}
}
&-active {
color: #000;
font-weight: 600;
&::after {
width: 100%;
left: 0;
}
& + .nav::after {
left: 0;
}
}
将下划线设置为绝对定位,通过 js 进行定位宽度
先设置 scss
&-underline {
position: absolute;
bottom: 0;
display: block;
height: 4px;
border-radius: 4px;
background-color: red;
transition: all 0.3s ease-out;
margin-bottom: 0.1rem;
}
再通过 js 控制宽度和向左的距离
//获取当前导航本身
const navActive: any = document.querySelector(".msearch-nav-active");
//传递给函数 元素宽度 距离左边的距离
moveUnderLine(navActive.offsetWidth, navActive.offsetLeft);
//定义moveUnderLine函数 多处使用
const moveUnderLine = (width: number | string, left: number | string) => {
navUnderlinStyle.value = {
width: width + "px",
left: left + "px",
};
};
因为我们导航本身也属于滚动属性,而且设置了滚动时会居中显示,这样与我们下划线的绝对定位产生了错位!
此时需要将下划线,导航放在同一个 div 中 并且 div 的定位属性设置为 relative
这样才能让导航条跟随导航的滚动的位置一致;下面展示导航滚动自动居中的 js 代码
//获取激活导航元素
const navActive: any = document.querySelector(".msearch-nav-active");
//获取整个滚动区域的宽度(设置多宽就是多少)
const navWidth = navBoxRef.value.offsetWidth;
if (navActive) {
//获取激活导航的距离左边的距离
const navOffsetWidth = navActive.offsetLeft;
//中间值 通过偏移宽度减去元素本身宽度再除以2
const diffWidth = (navWidth - navActive.offsetWidth) / 2;
//需要滚动的距离targetWidth
const targetWidth = navOffsetWidth - diffWidth;
//设置滚动距离
navBoxRef.value.scrollLeft = targetWidth;
//上面下划线跟随滚动
moveUnderLine(navActive.offsetWidth, navOffsetWidth);
}
这里有个不足之处在于 vue3.0 的监听事件,我不知道怎么拿到路由的 from 和 to 只能拿到 route 的部分信息
效果图
好的开始,我们需要辅助定义一下属性 在每个路由中定义个 index 代表我们的层级
好的开始,我们需要辅助定义一下属性 在每个路由中定义个 index 代表我们的层级
{
path: "/",
name: "Home",
component: () => import("../views/home/index.vue"),
meta: {
keepAlive: true, //是否需要缓存
title: "梦回云音乐-首页",
index: 1,
},
},
{
path: "/sheetList",
name: "sheetList",
component: () => import("../views/musicSheet/index.vue"),
meta: {
title: "梦回云音乐-歌单",
index: 2,
},
},
然后在App.vue
中定义使用transition
下面看代码
这里是关键的一个步骤 需要去监听我们的 route 的变化 但是我拿去不到当前的层级和要去的层级,所以只能暂时使用vue2.x
的方法 不要写在 setup 里面
watch: {
$route(to, from) {
//禁止刷新当前页时 触发动画效果
if (from.meta.index === undefined) {
to.meta.transitionName = "";
return;
}
if (to.meta.index > from.meta.index) {
to.meta.transitionName = "jump";
} else {
to.meta.transitionName = "back";
}
},
},
后面定义我们的 css 过渡代码 动画样式
.back-enter-active,
.back-leave-active,
.jump-enter-active,
.jump-leave-active {
will-change: transform;
transition: all 0.5s;
width: 100%;
position: absolute;
z-index: 99;
}
.jump-enter-from {
opacity: 0;
transform: translate3d(100%, 0, 0);
}
.jump-leave-active {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
.back-enter-from {
opacity: 0;
transform: translate3d(-100%, 0, 0);
}
.back-leave-active {
opacity: 0;
transform: translate3d(+100%, 0, 0);
}
此方法在点击返回和事件返回都能很好的展现,但是当我们用滑动返回或者前进时,动画会执行两遍,我想的是通过判断手势方向来取消动画效果(暂时还没开写)
希望有大佬告知我如何快捷知道它是通过什么返回或者前进的。
实现效果:
首先定义我们的 themes 主题文件 —
//themes.scss
$themes: (
light: (
//背景色
background_color: #f8f8f8,
//主文本色,,,,,
text-color: #000,
//次要背景色
secondary-bg: #fff,
),
dark: (
//背景
background_color: #080808,
//主文本色,,,
text-color: #f8f8f8,
//次要背景色
secondary-bg: #1a1919,
),
);
定义操作文件
//handle.scss
//遍历主题map
@mixin themeify {
@each $theme-name, $theme-map in $themes {
//!global 把局部变量强升为全局变量
$theme-map: $theme-map !global;
//判断html的data-theme的属性值 #{}是sass的插值表达式
//& sass嵌套里的父容器标识 @content是混合器插槽,像vue的slot
[data-theme="#{$theme-name}"] & {
@content;
}
}
}
//声明一个根据Key获取颜色的function
@function themed($key) {
@return map-get($theme-map, $key);
}
//获取背景颜色
@mixin background_color($color) {
@include themeify {
background: themed($color) !important;
}
}
//获取字体颜色
@mixin font_color($color) {
@include themeify {
color: themed($color) !important;
}
}
最后是使用方法,需要引入 scss
//example.scss
@import "@/assets/css/handle.scss"; //你自己的路径
* {
//括号里你可以引入你其他设计好的颜色
@include background_color("background_color");
@include font_color("text-color");
}
这里主要用于我在写代码过程中遇到的部分问题,以及解决办法
tps:此方法只能用于移动端
给 modal 层设置 onTouchMove 时阻止默认行为,代码如下:
//定义事件
const preventTouchMove = (event: TouchEvent) => {
if (props.lockScroll) {
event.preventDefault();
}
};
//在tsx中使用
;
但是我内部还有滚动时,有时候也会带动底层的滚动,这里到现在也没解决,希望大佬帮忙提供好的解决方案
主体不会向上移动相应内容,但是会受到父级 overflow 的影响 不能设置 overflow
仅仅在父元素内生效,
父元素的高度不能低于 sticky 元素的高度
必须指定 top、bottom、left、right4 个值之一,否则只会处于相对定位
.xxx {
position: sticky;
top: 0;
z-index: 100;
}
警告代码:
lot "default" invoked outside of the render function: this will not track dependencies used in the slot. Invoke the slot function inside the render function instead. |
找了很久但是没有找到具体的解决办法,但是代码正常运行,
如果不想看见此代码,可以设置关闭 vue 的代码警告(自行百度),不使用 tsx 语法进行开发,也可以不触发此提示,
如果有更好的解决办法,请告知
在 tsx 语法中,需要先引入 Transition 后才能使用,
import { Transition } from "vue";
需要注意,在 tsx 语法中 如果自行控制display
的状态 是无法触发 Transition 的过渡动画的;
在 tsx 语法中,我们可以使用v-show
但是不能使用v-if
否则会报错 v-show
可以触发过渡动画
其实本身 v-if 也是一个三元运算符 很简单的操作
但是至于为什么我 dispaly 使用无法触发动画效果 很无奈
因为某些时候我们在使用 vue3.0 的 ref 创建时会像下面这样创建变量
const xxxref = ref(null);
然乎再去绑定到 dom 上面
但是在我们使用的时候,就会 tslint 会有提示 对象可能为 null
因为我们没有定义对应的类型判断 导致会这样 我们可以换种写法 给他一个类型
const xxxref = ref(null); //or
const xxxref = ref();
其实两行代码一样 ref 会自行推断类型 当然你有特殊的值 可以定义好后 再使用
因为此程序是音乐播放器,则需要使用到 audio 标签去进行音乐的播放
autoplay
自动播放 接受 BooleanonPlay
播放onPause
暂停onEnded
播放结束onTimeupDate
播放时间更新播放时 类似setInterval
循环click
,touchmove
等等函数,需要使用到event时遇到的问题此问题非使用组件时,组件上直接使用这些方法 会让你自己 emit 定义好了才能使用 否则会报错
当我们要组织默认事件,或者使用函数传递参数时,在vue3
+tsx
中是不能直接传递参数的,需要声明相应的类型,如下所示
tsx 语法中传递一个index
时
在函数中接受时,定义 type,后才能使用相应的值
type ClickHandler = (index: number) => (e: MouseEvent) => void;
const clickHandler: ClickHandler = (index) => (e) => {
e.preventDefault();
store.commit("setPlayCurrntIndex", index);
};
首先在 html 界面 meta 中加入相关配置
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
user-scalable=no
禁止缩放
viewport-fit=cover
安全区适配时使用
在需要配置安全适配的 css 代码中加入
padding-bottom: constant(safe-area-inset-bottom); ///兼容 IOS<11.2/
padding-bottom: env(safe-area-inset-bottom); ///兼容 IOS>11.2/
需要注意的是:此方法只适合定位在底部的内容
需要设置各自内容的高度,不能根据 slider 自身高度进行滚动
错误提示:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a1duWUk5-1632624676993)(C:\Users\12743\AppData\Roaming\Typora\typora-user-images\image-20210905003941646.png)]
state.historySearch = JSON.parse(localStorage.getItem("historySearch"));
解决办法:
state.historySearch = JSON.parse(localStorage.getItem("historySearch") || "[]");
错误原因
提示不要使用’{}‘或’[]’,因为 JSON.parse(’{}’)、JSON.parse(’[]’) 为 true !!
vue2.x
版本时 可以通过父组件 visible.sync=“xxxxx” 然后子组件使用update:xxx
即可更改值
vue3.0
版本中 已经丢弃sync
,需要使用v-model
绑定的值 才能通过update:xxx
进行修改
千万别把定义的 ref 数据写在了 setup 的 return 里 子组件无法正常使用 这里给大家看看错误代码
setup() {
//应该放在这
return () => {
//记住 千万不能写在这!!!!!
const show = ref(false);
const percentage = ref(10);
const changeShow = () => {
show.value = !show.value;
};
const format = (percentage: number | string) => {
return Number(percentage) === 100 ? "满" : `${percentage}%`;
};
return (
点我试试
);
};
},
就是在页面上加上 下面这句代码后
-webkit-overflow-scrolling: touch;
属性后,在安卓上的z-index是有效的,但在苹果上的z-index却始终没有效果,去掉后正常使用
接口地址:
网易云接口
前端地址