技术栈:
Vite + Vue 3 + Vue Router + TypeScript
vite 搭建官网
创建项目
yarn global add create-vite-app //安装vite
cva <your-project-name> //创建项目,cva是缩写
cd <your-project-name>
yarn //安装依赖,相当于 yarn install
yarn dev
1.2 初始化项目
- index.html 项目首页
<div id="app"></div>容器- Vue 3 创建应用实例
//main.js
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
- 根组件
每个应用都需要一个“根组件”,其他组件将作为其子组件。
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
- 挂载应用
- 应用实例必须在调用了
.mount()方法后才会渲染出来。
<div id="app"></div>
createApp(App).mount('#app')
app.mount('#app') //简写为
- 对比Vue 2 的写法
new Vue({
router: router,
store,
render: h => h(App)
}).$mount('#app');
- 安装
Vue 3 Snippets插件,快捷输入temp模板提示
Vue 3 的
<template>支持多个根标签,Vue 2 不支持
//Frank.vue
<template>
<div>
我的第一个组件
<div>
</template>
- 引入
Frank组件
//App.vue
<template>
<div>hi<div>
<Frank />
</template>
<script>
import Frank from './components/Frank.vue'
export default {
name:'App',
component:{ Frank }
}
1.3 安装vue-router
npm info vue-router versions //查所有版本号
yarn add vue-router@4.0.0-beta.3
- 初始化仓库
1.4 初始化vue-router
// router.ts
import { createWebHashHistory, createRouter } from "vue-router";
//引入 History型路由、创建路由
import Home from './components/Home.vue'
import Doc from './components/Doc.vue'
const history = createWebHashHistory();
const router = createRouter({
history: history,
routes: [
{ path: "/", component: Home },
{ path: "/xxx", component: Doc }
]
})
- 解决不识别
.vue后缀模块的报错,引入TS只能理解.ts文件,无法理解.vue文件:搜索Vue 3 can not find module,如果之后还是有这种出现下划线情况,就打开这个文件,再关闭,或者是在引入的组件中加入空的script标签,vscode就是这么智障。 - 解决方法:创建shims-vue.d.ts文件,告诉ts理解
.vue文件
//src/shims-vue.d.ts
declare module '*.vue'{
import { ComponentOptions } from "vue"
const componentOptions:ComponentOptions
export default componentOptions
}
- 使用
router
//main.ts
const app = createApp(App);
app.use(router);
app.mount("#app");
//对比Vue2用法
import Vue from 'vue';
import VueRouter, {RouteConfig} from 'vue-router';
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
{path:'/', redirect:'/money'},
{path:'/money', component:Money},
{path:'*', component: NotFound}
];
const router = new VueRouter({routes});
export default router;
- 展示组件
//App.vue
<template>
<div>导航栏 | <router-link to="/">Home<router-link> | <router-link to="/xxx">Doc<router-link><div>
<hr/>
<router-view />
</template>
<script>
export default {
name:'App',
}
</script>
1.5 创建首页和文档页
- 封装导航栏组件
//Topnav.vue
<template>
<div class="topnav">
<div class="logo">LOGO</div>
<ul class="menu">
<li>菜单1</li>
<li>菜单2</li>
</ul>
<span class="toggleAside"></span>
</div>
</template>
- 写Home主页
<template>
<div>
<Topnav />
<div class="banner">
<h1>gulu-ui</h1>
<h2> nb class UI </h2>
<div class="actions">
<a href="https://github.com">GitHub</a>
<router-link to="/doc">开始</router-link>
</div>
</div>
</div>
</template>
<script lang="ts">
import Topnav from "../components/Topnav.vue";
export default {
components: { Topnav },
};
</script>
- 写Doc文档页
<template>
<div class="layout">
<Topnav class="nav" />
<div class="content">
<aside>
<h2>组件列表</h2>
<ol>
<li>
<router-link to="/doc/switch">Switch 组件</router-link>
</li>
<li>
<router-link to="/doc/button">Button 组件</router-link>
</li>
<li>
<router-link to="/doc/dialog">Dialog 组件</router-link>
</li>
<li>
<router-link to="/doc/tabs">Tabs 组件</router-link>
</li>
</ol>
</aside>
<main>
<router-view />
</main>
</div>
</div>
</template>
<script lang="ts">
import Topnav from "../components/Topnav.vue";
export default {
components: { Topnav }
};
</script>
- 解决写
sass时报错找不到模块: - 解决方法:安装sass,
yarn add -D sass@1.26.10,使其安装在package.json文件的devDependencies中,重新更新配置yarn,再次yarn dev报错清除。
1.6 用<依赖注入>实现切换功能
需求:点击切换aside实现点一次显示,再点一次隐藏。用menuVisible = true/false控制是否显示
- 用
ref()定义响应式变量(创建任何值类型)
//`ref()` 将传入参数的值包装为一个带 `.value` 属性的 ref 对象:
import { ref } from 'vue'
const menuVisible = ref(false)
- 组件间数据传递
父-子组件传递数据用
props。
祖-子组件传递数据用依赖provide注入inject
- 依赖
provide
import { provide } from 'vue'
export default {
setup() {
provide("menuVisible", menuVisible)
}
}
//第一个参数(注入名),类型:String /Symbol。后代组件用注入名来查找期望注入的值。
//第二个参数是提供的值,值可以是任意类型,包括响应式的状态(ref)
- 注入
inject
import { inject, Ref } from "vue"; //标注类型时自动引入ref
export default {
setup() {
const menuVisible = inject<Ref<boolean>>("menuVisible"); // 访问到变量并标注类型<Ref>,再标记数据类型<Ref<boolean>>
},
};
- 父组件标记
menuVisible变量
//App.vue
<template>
<router-view />
</template>
<script lang="ts">
import { ref, provide } from "vue"; // 安装`auto import`插件,自动引入
import { router } from "./router";
export default {
name: "App",
setup() {
const menuVisible = ref(false) //默认是看不见
provide("menuVisible", menuVisible); // 标记变量可被子组件访问
},
};
</script>
- 子组件访问到变量
//Topnav.vue
<template>
<div class="topnav">
<div class="logo">LOGO</div>
<ul class="menu">
<li>菜单1</li>
<li>菜单2</li>
</ul>
<span class="toggleAside"></span>
</div>
</template>
<script lang="ts">
import { inject, Ref } from "vue"; //标注类型时自动引入ref
export default {
setup() {
const menuVisible = inject<Ref<boolean>>("menuVisible"); // 访问到变量并标注类型<Ref>,再标记数据类型<Ref<boolean>>
},
};
</script>
- 子组件修改变量,点击元素时,把变量
boolean值取反
//Topnav.vue
<template>
<div class="topnav">
<div class="logo">LOGO</div>
<ul class="menu">
<li>菜单1</li>
<li>菜单2</li>
</ul>
<span class="toggleAside" @click="toggleMenu"></span>
</div>
</template>
<script lang="ts">
import { inject, Ref } from "vue"; //标注类型时自动引入ref
export default {
setup() {
const menuVisible = inject<Ref<boolean>>("menuVisible"); // 访问到变量并标注类型<Ref>,再标记数据类型<Ref<boolean>>
const toggleMenu = () => {
menuVisible.value = !menuVisible.value;
};
return { toggleMenu };
},
};
</script>
//Doc.vue
<template>
<div class="layout">
<Topnav class="nav" />
<div class="content">
<aside v-if="menuVisible"> //menuVisible 变化
<h2>组件列表</h2>
<ol>
<li>
<router-link to="/doc/switch">Switch 组件</router-link>
</li>
<li>
<router-link to="/doc/button">Button 组件</router-link>
</li>
<li>
<router-link to="/doc/dialog">Dialog 组件</router-link>
</li>
<li>
<router-link to="/doc/tabs">Tabs 组件</router-link>
</li>
</ol>
</aside>
<main>
<router-view />
</main>
</div>
</div>
</template>
<script lang="ts">
import Topnav from "../components/Topnav.vue";
import { inject, Ref } from "vue";
export default {
components: { Topnav },
setup() {
const menuVisible = inject<Ref<boolean>>("menuVisible"); // get
return { menuVisible };
},
};
</script>
1.7 适配手机页面上的切换按钮
- 页面宽度低于500px时,隐藏菜单,并在左上角出现切换按钮
//Topnav.vue
@media(max-width:500px){
> .menu{display:none}
> .logo{margin: 0 auto}
> .toggleAside{display:inline-block}
}
- 调整Doc文档页面
//Doc.vue
...
@media(max-width:500px){
position:fixed;
top:0;
left:0;
padding-top:70px;
}
- PC端设置
menuVisible默认值为true
<template>
<router-view />
</template>
<script lang="ts">
import { ref, provide } from "vue";
import { router } from "./router";
export default {
name: "App",
setup() {
const width = document.documentElement.clientWidth;
const menuVisible = ref(width <= 500 ? false : true); //小于500px为手机
provide("menuVisible", menuVisible); // set
router.afterEach(() => {
if (width <= 500) {
menuVisible.value = false;
}
});
},
};
</script>
1.8 路由间切换,实现嵌套路由
- 加入
Vue Router时,将组件映射到路由上
//App.vue
<template>
<router-view /> //路由出口,路由匹配到的组件将渲染到这里
</template>
//Doc.vue
<router-link to="/doc/switch">Switch 组件</router-link>
//导航,传递 to 指定链接
- 路由配置
//router.ts
...
const history = createWebHashHistory();
export const router = createRouter({
history: history,
routes: [
{ path: "/", component: Home },
{
path: "/doc",
component: Doc,
children: [
{ path: "", component: DocDemo }, //二级组件加入根路由
{ path: "switch", component: SwitchDemo },
{ path: "button", component: ButtonDemo },
{ path: "dialog", component: DialogDemo },
{ path: "tabs", component: TabsDemo },
],
},
],
});
- 对应的路由展示:
//Doc.vue
<template>
<div class="layout">
<Topnav class="nav" />
<div class="content">
<aside v-if="menuVisible">
<h2>组件列表</h2>
<ol>
<li>
<router-link to="/doc/switch">Switch 组件</router-link>
</li>
<li>
<router-link to="/doc/button">Button 组件</router-link>
</li>
<li>
<router-link to="/doc/dialog">Dialog 组件</router-link>
</li>
<li>
<router-link to="/doc/tabs">Tabs 组件</router-link>
</li>
</ol>
</aside>
<main>
<router-view />
</main>
</div>
</div>
</template>
- 实现手机端点击路由组件后,自动消除组件列表
//App.vue
<template>
<router-view />
</template>
<script lang="ts">
import { ref, provide } from "vue";
import { router } from "./router";
export default {
name: "App",
setup() {
const width = document.documentElement.clientWidth;
const menuVisible = ref(width <= 500 ? false : true);
provide("menuVisible", menuVisible); // set
router.afterEach(() => { //设置手机端自动消除组件列表
if (width <= 500) {
menuVisible.value = false;
}
});
},
};
</script>
2. Switch开关组件
主要需求:实现切换、动画,外部控制初始及更新状态,事件响应及V-model简化,样式优化
Switch 开关组件怎么用
//不加冒号 value为字符串"true" 显示为开 <Switch value="true"> //加冒号 value为变量-布尔值true 显示为开 <Switch :value="true">
2.1 初始化
//src/lib/Switch.vue
<template>
<div>Switch组件</div>
</template>
//src/components/SwitchDemo.vue
<template>
<div>
<Switch /> //嵌套路由
</div>
</template>
<script lang="ts">
import Switch from "../lib/Switch.vue";
export default {
components: { Switch },
};
</script>
2.2 实现切换、动画
- 写开关样式,并初步实现hover之后,就向右滚动,初步实现开关切换;
- 添加点击事件,初始默认为false,点击后状态为true;
页面
yarn dev时为空白,有可能是代码拼写错误导致
//src/lib/Switch.vue
<template>
<!-- 若x为ture 则checked选中 -->
<button @click="toggle" :class="{ checked: checked }"><span></span></button>
</template>
<script lang="ts">
import { ref } from "vue";
export default {
setup() { //初始化
const checked = ref(false);
const toggle = () => {
checked.value = !checked.value; //单击按钮触发,value取反
};
return { checked, toggle };
},
};
</script>
...
button {
...
background: grey; //初始默认为灰色
}
button.checked {
background: blue; //选中打开时为蓝色
}
button.checked > span {
left: calc(100% - #{$h2} - 2px);
}
2.3 添加value属性和input事件
- 外界无法知道当前状态是开还是关,以及初始状态
- SwitchDemo组件(父组件)控制Switch组件(子组件)的开关,构造一个value属性值为变量y和input事件等于
y=$event - 父组件通过添加
value属性和input事件,两者的作用是:value属性控制每一次的状态,都是在触发事件之后更新一次,switch再次渲染一次;input事件可以通过$event拿到最新的值,
//SwitchDemo
<template>
<div>
<Switch :value="y" @input="y = $event" />
</div>
</template>
<script lang="ts">
import { ref } from "vue";
import Switch from "../lib/Switch.vue";
export default {
components: { Switch },
setup() {
const y = ref(true);
return { y };
},
};
- 子组件用
props接收value值,用context.emit('input')发出input事件
2.4 补充说明
$event的值是emit的第二个参数,emit(事件名, 事件参数)- Vue 3官方命名把
context.emit('input')命名为:context.emit('update:value')与props.value对应 - 用
v-model简写:
<Switch :value="y" @input="y = $event" />
<Switch v-model:value="y" /> //v-model简写
3. 制作Tabs组件
3.1 如何检查子组件的类型
- 检查
context.slots.default()数组
先把context的内容log出来,结果中有slots属性:
setup(props, context) {
console.log({ ...context });
},
再把context.slots的内容log出来,结果中有default属性,经查需要打印default()函数:
console.log({ ...context.slots.default() });
打印出的函数内容,有0和1属性,就是对应的tab
- 通过
const defaults = context.slots.default()的结果获取外面传入的子内容,可以用<component :is="defaults[0]" />展示出所有子内容,再去看每个子内容的标签是什么
console.log(defaults[0].type === Tab);
//true
- 意味着
defaults[0].type就是Tab.vue,也就是所有的.vue文件最终都被Vue转换为对象。
setup(props, context) {
const defaults = context.slots.default();
defaults.forEach((tag) => {
if (tag.type !== Tab) {
throw new Error("Tabs 子标签必须为 Tab");
} //防御型编程(若TabsDemo中输入的是div就报错)
});
// forEach遍历数组,将遍历到的元素传递给回调函数,
//第一个参数tag是遍历的数组内容,
//省略了第二个参数是对应的数组索引
//第三个参数是数组本身
详说数组遍历:
forEach( )方法不会返回执行结果,而是undefined,被调用时,不会改变原数组map( )方法会分配内存空间存储新数组并返回,不修改调用他的原数组本身- jQuery里的
$each(arr|obj, function(k,v))遍历数组和对象
3.2 如何渲染嵌套的组件
<template>
<div>
Tabs 组件
<component :is="defaults[0]" />
<component :is="defaults[1]" />
</div>
</template>
改写上面的写法:
<template>
<div>
// v-for 和 key 得成对出现,不能只写一个
<div v-for="(t, index) in titles" :key="index">{{ t }}</div>
<component v-for="(c, index) in defaults" :is="c" :key="index" />
</div>
</template>
<script lang="ts">
import Tab from "./Tab.vue";
export default {
setup(props, context) {
const defaults = context.slots.default();
defaults.forEach((tag) => {
if (tag.type !== Tab) {
throw new Error("Tabs 子标签必须为 Tab");
}
});
const titles = defaults.map((tag) => {
return tag.props.title;
});
return {
defaults,
titles,
};
},
};
</script>
4.3 显示被选中的导航(切换标签页)
用
selected标记被选中的标签页
- selected用
index表示,不推荐- selected用
name表示,不方便- selected用
title表示,有漏洞
- 用
title实现切换导航:
//TabsDemo.vue
<Tabs selected="导航1">
<Tab title="导航1" selected>内容1</Tab>
<Tab title="导航2">内容2</Tab>
</Tabs>
//Tabs.vue
<div class="gulu-tabs-nav-item"
:class="{ selected: t === selected }"
v-for="(t, index) in titles"
:key="index"> {{ t }}</div>
...
props: {
selected: {
type: String,
},
},
- 用
<component :is="current" :key="selected" />实现切换内容:
//Tabs.vue
<div class="gulu-tabs-content">
<component :is="current" :key="current.props.title" />
</div>
<script lang="ts">
import { computed } from "vue";
export default {
setup(){
const current = computed(() => {
return defaults.find((tag) => tag.props.title === props.selected);
});
return {current}
}
}
4.4 动态设置导航条的宽度
- 先加一个div,然后对应添加样式
<div class="gulu-tabs-nav-indicator"></div>
- 解决导航下划线宽度过长,使下划线的宽度与导航1(title)保持一致,用
ref与v-for结合使用
<div class="gulu-tabs-nav-item" :class="{ selected: t === selected }" v-for="(t, index) in titles"
:ref="(el) => {if (el) navItems[index] = el;}" @click="select(t) :key="index">
{{ t }}
</div>
<div class="gulu-tabs-nav-indicator" ref="indicator"></div>
<script lang="ts">
import { computed, onMounted, ref } from "vue";
export default {
setup() {
const divs = navItems.value;
const result = divs.filter((div) =>
div.classList.contains("selected")
)[0];
const { width } = result.getBoundingClientRect();
indicator.value.style.width = width + "px";
});
return {navItems,indicator}
}
</script>
4.5 动态设置导航条的位置
<div class="gulu-tabs-nav" ref="container">
const container = ref<HTMLDivElement>(null);
return {container}
- 用CSS实现导航条滑动效果
.gulu-tabs-nav-indicator{transition: all 250ms;}
5. 官网装修
- 从设计站点的模板中取主题颜色:
- 生成渐变色效果CSS:
- 应用到项目中,注意删掉
topnav和banner中的颜色样式
//Home.vue
<div class="topnavAndBanner">
<Topnav />
<div class="banner">
...
.topnavAndBanner {
background: linear-gradient(
145deg,
rgba(227, 255, 253, 1) 0%,
rgba(183, 233, 230, 1) 100%
);
}
- 优化按钮、文字、链接的样式
//index.scss
a {
text-decoration: none; //清除文本修饰
color: inherit;
&:hover {
text-decoration: underline;
cursor: pointer;
}
}
body {
font-size: 16px;
line-height: 1.5;
color: #1d2c40;
}
//Home.vue
$green: #02bcb0;
$border-radius: 4px;
$color: #007974;
.banner{
color: $color;
> .actions
padding: 8px 0;
a {
margin: 0 8px;
background: $green;
color: white;
display: inline-block;
padding: 8px 24px;
border-radius: $border-radius;
&:hover {
text-decoration: none;
}
}
}
如果之前预选的颜色放到页面中后,效果不达标,还可以去控制台重新选效果,再把满意的颜色重填到代码中。
- 添加icon或logo
素材在iconfont.cn找,选中号icon后,添加入库,再添加至项目,可修改icon名称,选择symbol选项卡,查看在线链接,生成代码:
icon引入到源码中,有两种方式,一种是直接拷贝项目下面生成的symbol代码:
//index.html
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script src="//at.alicdn.com/t/c/font_3670626_ci4r8085hdb.js"></script> //symbol代码
</head>
- 使用icon,加入通用css代码
在iconfont项目中,找使用帮助,搜索Symbol引入
//index.scss
.icon {
width: 1em; height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
- 页面中使用icon,挑选相应图标并获取类名(标签中的类名要与iconfont项目中的类名一致),应用于页面:
//topnav.vue
<div class="logo">
<svg class="icon">
<use xlink:href="#icon-gears-ui"></use>
</svg>
</div>
...
.topnav {
> .logo > svg {
width: 32px;
height: 32px;
}
}
//Home.vue
<div class="features">
<svg class="icon">
<use xlink:href="#icon-Vue"></use>
</svg>
<svg class="icon">
<use xlink:href="#icon-ts"></use>
</svg>
<svg class="icon">
<use xlink:href="#icon-lights"></use>
</svg>
</div>
...
.features {
> svg {
width: 64px;
height: 64px;
}
}
-
icon引入到源码中,第二种方法是把### symbol代码的JS文件复制到本地:在浏览器中打开链接,将内容粘贴到
lib/svg.js文件中,再从main.ts中引入这个文件import './lib/svg.js',也是可以的。 -
使用 clip-path 画圆弧
.topnavAndBanner {
background: linear-gradient(
145deg,
rgba(227, 255, 253, 1) 0%,
rgba(183, 233, 230, 1) 100%
);
clip-path: ellipse(80% 60% at 50% 40%);
}
- 响应式页面
先写手机样式+@media(min-width:600px)+@media(min-width:800px)+(min-width:1200px)
6. 部署发布官网
就是把dist目录(存放所有代码打包之后的结果)上传到网上
6.1 本地测试
- 删除项目中的
dist文件夹 - 打开
.gitignore文件
node_modules
.DS_Store
dist
/dist
*.local
/.history
yarn build得到新的dist目录
- 本地测试网站是否成功运行
yarn add http-server //本地测试网站是否可以正常使用 ^14.1.1
http-server dist -c-1 //默认打开8080端口页面
6.2 部署到GitHub
- GitHub默认会吞掉带下划线的文件
_, - 新建仓库 gulu-ui-website
- 关闭自动翻译功能,确保链接为SSH
cd dist
git init
git add .
git commit -m "2nd commit"
git branch -M main
git remote add origin git@github.com:jianlong5296/gulu-ui-14.git
git push -u origin main
git push -f -u origin main //更新时覆盖之前的
6.3 打包库文件 ,自行配置rollup
-
- 创建 lib/index.ts,将所有需要导出的东西导出
import Switch from './Switch.vue'
export {Switch}
//合并简写为如下:
export {default as Switch} from './Switch.vue'
-
- 告诉rollup怎么打包
$ yarn add --dev rollup-plugin-esbuild rollup-plugin-vue rollup-plugin-scss sass rollup-plugin-terser
目前的bug,手机端,Switch开关组件index层比菜单高
参考
jirengu.com