使用Vite搭建官网
项目搭建
创建项目
cd /c/Users/11691/Desktop/x
进入目录yarn global add create-vite-app@1.18.0
安装vite对应版本cva roll_ui
创建项目文件(cva是create-vite-app的简写)cd roll_ui
进入目录文件yarn
yarn install下载对应包
小知识
Vite的基本使用
vite 文档给出的命令是
npm init vite-app
yarn create vite-app
等价于
全局安装 create-vite-app 然后cva
等价于
npx createa-vite-app
注意项目名称最好用-连接,不要用驼峰命名
Vue 2 和 Vue 3 的区别
90% 的写法完全一致,除了以下几点
Vue 3 的 Template 支持多个根标签,Vue 2 不支持
Vue 3 有 createApp(),而 Vue 2 的是 new Vue()
Vue3是createApp(组件),而Vue2是 new Vue({template, render})
其他区别会在项目跟进中展开说明
初始化项目
项目文件目录介绍
index.html项目首页
script标签中type=”mudule”:
在script标签中写js代码,或者使用src引入js文件时,默认不能使用module形式,即不能使用import导入或导出文件,但是我们可以再script标签上加上type=module属性来改变方式。
main.js项目入口文件
APP.vue组件
提交代码
遇到问题:
vscode源代码管理出现几千个更改
原因:
在桌面或者某个常用的文件夹里用git初始化命令,导致大量文件被追踪;
解决方法:
- 找到被追踪的文件最顶层的文件夹;
- 删除.git文件夹;
- 重启Vs code。
引入Vue Router
专门和Vue3进行搭配的路由
使用命令行查看 vue-router 所有版本号_npm info vue-router versions_
安装vue-routeryarn add vue-router@4.0.0-beta.3
初始化vue-router
- 新建 history 对象
- 新建 router 对象
- 引入 TypeScript,重命名文件格式为ts
- vue-router的使用
- app.use(router)
const app = createApp(App);app.mount('#app')创建并挂载根实例:app.use(router) ;
- 通过调用 app.use(router),我们可以在任意组件中以 this.$router 的形式访问它,并且以 this.$route 的形式访问当前路由
- 添加
占位(内容可变区) - 添加
链接进行单页面内路由跳转(类似a链接,内容不变区)
- app.use(router)
//导入vue-router
import { createWebHashHistory, createRouter } from 'vue-router'
//创建vue-router需要用到的history对象和router对象
const history = createWebHashHistory()
const router = createRouter({
history: history,
routes: [
{ path: '/', component: HelloWorld }
]
})
//创建并挂载根实例,且使用use(router),确保整个应用支持路由。
const app = createApp(App)
app.use(router)
app.mount('#app')
//App组件内添加<router-view>和<router-link>
遇到问题:
TS文件无法理解导入的.vue文件,出现波浪线警告找不到模块“./components/HelloWorld.vue”或其相应的类型声明。
“Vue3 cant find module”
解决方法:
声明一个兼容vue的自定义.d.ts文件“shims-vue.d.ts”,放入源代码src目录中,因为其也属于源代码部分
declare module '*.vue' {
import { ComponentOptions } from "vue";
const componentOptions: ComponentOptions
export default componentOptions
}
搭建首页和文档页html结构、添加样式
封装Topnav组件
跨平台中文字体解决方案
- Fonts.css — 跨平台中文字体解决方案
打包了一些常见字体的名字,希望能覆盖 Windows、Mac 以及 Linux 的常见字体,方便引用。
font-family: -apple-system, "Noto Sans", "Helvetica Neue", Helvetica, "Nimbus Sans L", Arial, "Liberation Sans", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Source Han Sans SC", "Source Han Sans CN", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti", SimHei, "WenQuanYi Zen Hei Sharp", sans-serif;
//向右滑动查看更多
使用Provide和Inject实现切换功能
点击切换aside,一次显示,再点一次隐藏,使用provide/inject
补充:Vue3的ref引用
带 ref 的响应式变量
- setup意思是准备的意思,在props、data, methods和生命周期函数之前运行的,且setup只会在页面挂载组件的时候算一遍,这个方法中没法访问到this
- setup中使用vue3.0中提供的api(ref)来提供一个响应式的对象(和data中一样的响应式对象,data中直接返回一个对象),需要先引进,然后再使用,ref是一个函数,接收一个参数返回一个响应式的对象:在未来可以检测到改变,并且做出响应
- vue3.0的return 有意设计成这样,要精确控制哪些属性和方法被导出使用,可以更好的追踪引用和更新,在模板中想要使用,必须导出去
在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,如下所示:
import { ref } from 'vue'
const counter = ref(0)
ref 为我们的值创建了一个响应式引用。在整个组合式 API 中会经常使用引用的概念。
一般用法:使用 ref 创建内部数据,类似之前的data
补充:Vue3的methods写法改变
①需要先声明,搭配箭头函数,一般写在setup中
②按需return
//Vue2写法:
methods {
add() {
data.age++
}
}
//Vue3写法:
setup() {
...
const add = () => {
data.age++
}
return {
...,
add
}
}
手机页面适配
①首先考虑手机,再考虑pc
②手机适配界定范围为0-500px,大于500则为pc
③pc页面下的Doc页面下aside默认显示,手机页面下的Doc页面下的aside根据点击按钮进行切换显示与否
④判断用户浏览器页面宽度进行逻辑跳转
const width = document.documentElement.clientWidth //获取用户浏览器页面宽度
const menuVisible = ref(width <= 500 ? false : true) //逻辑跳转
同一页面内使用嵌套路由
①添加路由信息,在Doc页面内的子路由,需要在Doc路由中配置 children路由,同样是个数组,但path只写子路由名字
②添加路由显示占位,在Doc页面的main主内容区添加
③页面内路由切换后关闭aside
(如何关闭?
方法之一:router.afterEach()切换路由执行方法,在手机页面下切换Doc页面内的路由后关闭aside→缺点:路由切换的范围太大,应该针对某些路由切换进行方法更合适)
补充:afterEach()当路由切换后执行方法
afterEach
添加一个导航钩子,在每次导航后执行。返回一个删除注册钩子的函数。
封装一个单独的router.ts
便于全局使用和共享状态,而不受限于main.ts文件内
嵌套路由中设置二级组件的根
即提供一个空的嵌套路径表示根,应用场景:展示默认页面,避免空白页面展示
export const router = createRouter({
history: history,
routes: [
{ path: '/', component: Home },
{
path: '/doc', component: Doc, children: [
{ path: "", component: DocDemo }, //路径为空 即二级组件的根 展示Doc默认页面
{ path: "switch", component: SwitchDemo },
{ path: "button", component: ButtonDemo },
{ path: 'dialog', component: DialogDemo },
{ path: 'tabs', component: TabsDemo }
]
}
]
})
UI库中不能使用scoped
制作Switch组件
新建目录和文件:src——lib(存放所以组件)——Switch.vue
分别将组件导入components的各自Demo中以展示
整体步骤:
- 需求分析
- API设计(Switch组件怎么写)
- 写代码
- html
- css
- js
- 测试
- 改写
- 测试
- 改写
HTML骨架制作
CSS样式与动画
在scss中使用calc()
在 sass 中使用 calc,如果 calc 中包含一个变量,必须对这个变量使用sass的插值方法 : #{$variable}
正确的使用姿势是:
$padding: 10px;
$thirdWidth: 33.33333333%;
.content{
width: calc(#{$thirdWidth} - #{$padding});
}
JS添加交互
点击switch后进行切换状态
①初始化状态:记录状态true还是false,即点击前为true,点击后为false,反之亦然
setup() {
const checked = ref(false); //使用setup初始化状态,一般声明变量用ref引用
return { checked };
},
②结合class绑定style实现点击切换效果
使用对象语法::class="{ checked }
点击button后进行切换效果: const toggle = () => {checked.value = !checked.value; };
遇到问题:
①外部父组件无法控制Switch组件内部初始状态value(如:value初始状态为true或false在Switch组件内存中已写入确定值)
②Switch组件内部状态value改变后外部组件无法获取其更新(如:Switch组件的状态改变后SwitchDemo无法获取)
解决方法:
①SwitchDemo中的
②SwitchDemo中的
①和②结合起来的写法:父组件SwitchDemo中<Switch :value="y" @update:value="y = $event" />
//Switch子组件内
props: {
value: Boolean, //用props接收外部父级组件传入的数据
},
setup(props, context) {
const toggle = () => {
context.emit("updat:evalue", !props.value);
//context.emit()触发事件给外部父级组件,!props.value会被当做$event在父组件中使用
};
return { toggle };
},
①和②可简化为Vue3的v-model写法:<Switch v-model:value="y"/>
去掉后半部分添加的updatevalue事件,改为v-model添加在绑定的value前即可
→知识点:Vue的双向绑定
vue的数据双向绑定主要通过Object.defineProperty()方法来进行数据劫持以及发布者-订阅模式来实现的
v-model其实体现的就是Vue的双向绑定,Vue3的v-model代替以前的v-model和.sync
双向绑定可以简单理解为自动监听,会自动监听update:value事件
补充:setup的参数用法
setup默认接收两个参数:props和context
props接收父级数据,只可读不可改写,
若需修改父级组件数据,则需通过context的emit()方法触发父级组件的事件,类似Vue2的this.$emit()用法
补充:scss嵌套小知识
scss使用嵌套关系,&的用法:
有空格的话不需要&,没有空格则需要&
有空格代表在标签内部,没有空格表示并列关系即同时存在的意思
制作Button组件
- 需求分析
- API设计(Button组件怎么写)实例如下:
- 写代码 ```javascript
<a name="YngXw"></a>
### HTML骨架制作
<a name="zJzTY"></a>
#### 使用插槽slot
应用场景:向一个组件传递内容,如下所示:
```html
<child>若子组件没有slot,则这句话不会显示,
为了将这句话显示,需在子组件模板中相应位置添加slot</child>
插槽在哪里显示由子组件来进行确定,即子组件安装了一个坑,父组件有需要就可以在上面蹲坑
CSS样式与动画
Scss的颜色函数fade-out()
fade-out() 函数提升颜色的透明度,取值在 0 到 1 之间。.xkd{ content:fade-out(rgba(100, 100, 255, 0.7), 0.1);}
编译成 CSS 代码:.xkd { content: rgba(100, 100, 255, 0.6);}
Scss其他常用11个颜色函数:
函数 | 描述 |
---|---|
rgb() | 创建一个 Red-Green-Blue(RGB) 色 |
rgba() | 创建一个带有透明度值的颜色 |
hsl() | 通过色相、饱和度和亮度的值创建一个颜色 |
hsla() | 通过色相、饱和度、亮度和透明的值创建一个颜色 |
red() | 从一个颜色中获取其中红色值 |
lightness | 获取一个颜色的亮度值(0% - 100%) |
alpha | 将颜色的 alpha 通道返回为介于 0 和 1 之间的数字 |
opacity | 获取颜色透明度值(0-1) |
mix() | 把两种颜色混合起来 |
fade-in() | 降低颜色的透明度,取值在 0-1 之。 |
fade-out() | 提升颜色的透明度,取值在 0-1 之间。 |
UI库中不能使用scoped
因为 data-v-xxx 中的 xxx 每次运行可能不同必须输出稳定不变的 class 选择器,方便使用者覆盖
CSS最小影响原则
清楚你写的产品的定位是UI库,作为UI库,有些设定绝对不能影响库使用者,
比如有些样式用户可以自己定义修改,但作为有一些基础样式是不希望被用户覆盖的
在UI组件库文件夹中新建 组件的全局样式:libs——roll.scss,添加以组件特定前缀名开头的样式
当一些基础样式不希望被用户覆盖时,必须加前缀
.button 不行,很容易被使用者覆盖
.roll-button 可以,不太容易被覆盖
.theme-link 不行,很容易被使用者覆盖
.roll-theme-link 可以,不太容易被覆盖
补充:属性选择器匹配元素用法
[class^="roll-"], //属性选择器匹配元素用法;匹配class以'roll-'开头的元素
[class*=" roll-"] { //匹配class中包含' roll-'的元素
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 16px;
font-family: -apple-system, "Noto Sans", "Helvetica Neue", Helvetica, "Nimbus Sans L", Arial, "Liberation Sans", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Source Han Sans SC", "Source Han Sans CN", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti", SimHei, "WenQuanYi Zen Hei Sharp", sans-serif;
}
JS添加交互
补充:attribute 和 property 的区别
两个单词的中文翻译也都非常相近(property:属性,attribute:特性),但实际上,二者是不同的东西,属于不同的范畴。
- property是DOM中的属性,是JavaScript里的对象;
- attribute是HTML标签上的特性,它的值只能够是字符串;
简单理解,Attribute就是dom节点自带的属性,例如html中常用的id、class、title、align等。
而Property是这个DOM元素作为对象,其附加的内容,例如childNodes、firstChild等。
为了更好的区分attribute和property,基本可以总结为attribute节点都是在HTML代码中可见的,而property只是一个普通的名值对属性。
禁用 Attribute 继承(Vue3属性绑定的规则细节)
当子组件为单个根节点时,父组件向子组件标签绑定的非 prop 的 attribute 将自动添加到子组件的根节点的 attribute 中。举例如下所示:
//父组件中
<template>
<div class="father-wrapper">
<h1>father component</h1>
<ChildComp attribute1="hello"/> //父组件向子组件绑定非prop的attribute
</div>
</template>
//子组件中:单个根节点为child-wrapper的div,则父组件传来的attribute会传到最根节点处
<template>
<div class="child-wrapper">
<h1>child component</h1>
<div class="child-div">
这是子组件中的div block
</div>
</div>
</template>
遇到问题:
某些场景下,我们期望不让它继承到根节点的div.child-wrapper , 那该怎么办?
例如,我们需要加在 div.child-div上?
解决方法:
①禁止继承
export default {
inheritAttrs: false,
...
②在所需的节点处添加v-bind="$attrs"
批量绑定全部属性
在vue3中,可以通过$attrs或context.$attrs
获取到所有的 非prop的Attributes
<template>
<div class="child-wrapper">
<h1>child component</h1>
<div class="child-div" v-bind="$attrs"> <!-- <--这里 -->
这是子组件中的div block
</div>
</div>
</template>
另外情况:在子组件中可指定attribute继承给哪个节点
若父组件传给子组件的非prop的attribute有多个,且需要将其传给子组件模板中的不同节点(两个或以上,下图示例两个),可利用剩余操作符将其分成可能不平均的两部分:
//子组件中
<template>
<div :size = 'size'>
<button v-bind = 'rest'>
<slot />
</button>
</div>
</template>
<script lang = 'ts'>
export default {
inheritAttrs: false,
setup(props,context) {
const {size,...rest} = context.attrs //解构赋值+剩余操作符
// ...rest表示:将剩余属性装进rest这个变量,rest为自定义变量名
return {size,rest}
}
}
</script>
ES6的剩余操作符…rest
在ES6中。 三个点(…) 有2个含义。分别表示 扩展运算符 和 剩余运算符
扩展运算符应用场景:
- 数组展开
- 将一个数组插入到另一个数据中
- 字符串转数据
剩余运算符应用场景:
- 获取不定数量的参数(可替代arguments)
- 解构使用
扩展运算符和剩余运算符区别:
简单地说,在某种程度上,剩余操作符和扩展运算符相反,扩展运算符会“展开”数组变成多个元素,剩余操作符会收集多个元素和“压缩”成一个单一的元素。
剩余操作符 和 arguments参数:
剩余操作符用于获取函数不定数量的参数数组,这个API是用来替代arguments的,arguments参数是一个类数组对象
let a = (first, ...abc) => {
console.log(first, abc); // 1 [2, 3, 4]
};
a(1, 2, 3, 4);
让Button绑定事件
让Button绑定属性
代码片段示例:
props可静态传值也可动态传值
theme=’button’(默认) //button或者link等均为字符串,不需要通过v-bind绑定 (静态传值)
theme=’link’
theme=’text’
size=’big’
size=’small’
level=’main’
level=’danger’
:disabled = ‘true’ === disabled //true或false为布尔值,需要通过v-bind绑定,引号内为JS内容(动态传值)
:disbaled = ‘false’(不传则默认)
:loading = ‘true’ ===loading //true或false为布尔值,需要通过v-bind绑定,引号内为JS内容(动态传值)
:loading = ‘false’(不传则默认)
绑定class的对象语法
动态切换多个 class,可以与普通class共存
示例::class="{[
theme-${theme}]: theme}"
vue会根据键值是否为 Truthy 来自动的让某个class生效
→只有八个Falsy值(false,0,-0,0n,””, ‘’, ``, null, undefined,NaN),其他均为truthy值
Button的class使用计算属性
class原本使用属性选择器进行匹配,存在多个class比较复杂,可使用计算属性,在setup中添加计算属性,需从Vue3引入
ts中:
computed是一个函数,参数是一个回调函数,这个回调中可以处理想返回的值,
返回的是一个只读的响应式引用
computed是一个类似ref对象的一种数据类型,也可以直接在模板中使用
setup() {
...
const classes = computed(() => {
return {
[`gulu-theme-${theme}`]: theme,
[`gulu-size-${size}`]: size,
};
});
}
vue3 组件传值之 props 与 attrs 的区别
$attrs 属性可以看做 props 的加强版,用来简化 vue 组件传值
- props 要先声明才能取值,attrs 不用先声明
props 声明过的属性,attrs 里不会再出现
props 不包含事件,attrs 包含事件
- props 支持 string 以外的类型,attrs 只有 string 类型
制作Dialog组件
需求分析
API设计
<Dialog
visible
title="标题"
@yes="fn1" @no="fn2"
></Dialog>
HTML骨架制作
CSS样式与动画
JS添加交互
让Dialog绑定visible,结合点击事件
:visible=”x”
x为响应式引用的变量,通过点击Button切换展示Dialog与否
setup() {
const x = ref(false); // 声明x为布尔的引用值
const toggle = () => {
x.value = !x.value; //通过点击按钮实现切换展示与否的效果(结合点击事件)
};
return { x, toggle };
},
让Dialog支持关闭
共有四处支持Dialog关闭
①×
②外部黑色遮罩
③OK
④Cancel
注意:
遇到问题:
增加场景:用户不希望点击黑色遮罩层就关闭Dialog,则需要提供一个开放接口给用户
解决方法:
Dialog上绑定:closeOnClickOverlay动态属性,默认值为true
遇到问题:
增加场景:Dialog对话框需要用户添加内容后点击OK才能关闭Dialog,否则关闭不了,则需要在OK事件中添加判断机制,判断emit返回值是否有内容,但是!
emit()是没有返回值的,即默认返回undefined,因为事件是没有返回值,这是事件的特点
解决方法:
使用普通函数即可,函数有返回值,
通过在子组件上绑定函数,子组件接收props为函数类型,子组件再判断父组件中定义的函数的返回值,根据这个返回值做关闭操作与否
补充:ES6可选链操作符
props.ok && props.ok() ==== props.ok?.()
//如果props.ok存在则执行;
//如果props.ok不存在则直接返回undefined
如果给定的函数不存在为undefined或null,则返回 undefined。
补充:vue3具名插槽
遇到问题:
此时Dialog支持由父组件DialogDemo传递进来的title和content
但title是由prop传递进来且由{{}}展示的,类型声明只支持String字符串
而content由
那怎样让title也由slot传递进来,支持HTML标签呢?
解决方法:
使用Vue3具名插槽,支持同一模板渲染展示多个slot,让Dialog支持自定义title和content
应用场景:
当需要同一模板渲染多个插槽时,
基本使用: 遇到问题: 遇到问题: 关键难题:让展示Dialog的时候传递visible引用值为true 关键转折方法:销毁app;由于无法动态控制visible的值,故转换方法,通过监听update:visible事件,当触发事件的时候,销毁该app组件,相当于关闭Dialog 遇到问题:如何销毁app? 应用场景: h() 到底会返回什么? 其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。 需求分析 创建Tabs组件和Tab组件 在HTML中获取插槽内容只需要在子组件中挖坑使用 遇到问题: 解决方法: props接收is:依 is 的值,来决定哪个组件被渲染 遇到难题: 关键难题: 首先清楚内容是与标签页的标题title捆绑的,点击响应title则出现对应内容 要想弄清楚标题的内容,首先必须明白浏览器从获取到代码到页面展示过程发生了什么,
首先是解析DOM,生成DOM树,解析CSS文件,生成CSS树,然后DOM树和CSS树合并生成渲染树,再渲染生成页面。
结论1:由于DOM解析和CSS解析是分开的,所以CSS文件并不会阻塞DOM的解析。
页面渲染需要DOM树和CSS树合并生成渲染树,所以CSS文件会阻塞页面渲染。
结论2:CSS文件会阻塞页面渲染。
对于JS,首先要知道JS是单线程执行的,JS可以对DOM的结构进行操作,比如删除、修改、新增dom。所以遇到JS文件,页面会停止DOM解析,如果不停止DOM解析,即一边执行JS,一边解析DOM,解析完DOM之后,发现JS修改了DOM结构,那么之前解析的DOM不就白费了吗?浏览器才不会这么傻呢?所以
结论3:遇到JS文件,页面会停止DOM解析
JS文件还会去读取CSS,所以执行JS还必须等到CSS文件解析完之后,这就相等与CSS文件阻塞了JS
结论4:CSS文件会阻塞JS 遇到问题: 那用什么方法可以将selected挂到Tab上? 那selected通过什么手段渲染到组件上? 遇到问题: ①爷爷TabsDemo组件传值给爸爸Tabs组件 使用filter()方法,留下满足条件的数组项 filter函数, 过滤通过条件的元素组成一个新数组, 原数组不变 遇到问题: Element.getBoundingClientRect()返回元素的大小及其相对于视口的位置:left, top, right, bottom, x, y, width, 和 height这几个以像素为单位的只读属性 Node.contains()返回的是一个布尔值,来表示传入的节点是否为该节点的后代节点 注意: 立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。 遇到问题: UI设计平台找素材,本项目期待素材类型为简约渐变风格,几何设计 icontfont网站找图标素材 ③挑选相应图标并获取类名,应用于页面 注意: ①border-radius方法: 本项目涉及三种宽度范围的情况 文档页增加“介绍”“安装”“开始使用”路由,且页面详情完善 文档页的Aside侧边导航选中后高亮? router-link-exact-active 和 router-link-active router-link-exact-active 是精确匹配规则,即只有当前点击的router被匹配 示例: 首先使用html格式书写文档内容 github-markdown-css显示markdown样式: 遇到问题: 解决方法: 本项目中自制vite插件,不属于源代码,放在根目录下: 然后在vite.config.ts中引入: 然后在对应文档模板文件中,使用v-html嵌入md,此时md是一大段字符串 vite开发环境下使用的是浏览器原生的能力,而部署到生产环境下的时候使用的是rollup插件 补充:浏览器只认识js,在网络请求中的可以看出,浏览器会用js的方式加载css、ts,所以也需要用js来加载md,但浏览器默认没有该功能,故需要自制插件 遇到问题: let module = await import(‘/modules/my-module.js’); 然后在对应抽离出来的子组件(源代码的提供者组件)中添加 遇到问题: 然后在对应需要展示源代码的标签上添加v-html属性 其实就是把dist目录上传到网上,有文档才会有人用我的轮子 遇到问题: 即生成dist和上传dist再预览 注意:往后修改源码重新build的话也是如上步骤重新执行一遍,最后 git push -f 遇到问题: 修改path路径 + assets去掉下划线 用bash的一个应用程序sh运行deploy.sh脚本,脚本文件如下 补充:dist和/dist的区别:dist表示所有dist名称的文件,/dist表示只在根目录下的dist文件 让全世界的人都可以用到我写的UI库 vite对打包库文件功能不支持,需要自行配置rollup 补充:又 import 又 export 的话可以的缩写为一句,示例如下: 补充:发布库文件到npm需要在package.json中添加如下内容: 补充:查看&设置npm源 补充:使用git bash登录npm,使用npm logout退出登录 补充:package.json中的dependencies中添加: 遇到问题: 遇到问题: 解决方法: 使用vue cli脚手架 使用自己的UI库 补充:去掉Linter检查 遇到问题: 若轮子项目需要修改,注意修改完后: 注意:官网build流程一般写入deploy.sh自动化部署脚本,只要运行 补充:nrm快速切换源
外部父组件蹲坑,标签上添加 v-slot:name1
内部子组件挖坑,补充:Vue3的Teleport
由于CSS的堆叠上下文关系的问题,有些弹窗本应该出现在最顶层,但由于堆叠关系的原因,还是被另外一些标签遮挡住了,在本项目中,为了防止Dialog组件被遮挡,应该如何做?
解决方法:
使用Vue3的Teleport传送门,将Dialog传送到body下
关键是将Dialog不设于任何标签层级环境下,而是暴露在body下
基本使用:
在子组件内使用Teleport标签(Vue3内置)
Teleport标签内添加to属性,属性值可自定义指定传送目的地,一般为body<Teleport to="body">...(要被传送的内容)</Teleport>
如果 补充:使用渲染函数 动态挂载组件
此前的Dialog组件是通过visible动态控制的,visible是引用布尔值,通过改变其布尔值控制Dialog是否展示,需要UI库使用者提前设置visible的引用值为false还是true;
此后,想要一键打开Dialog,而不需要通过控制visible来间接改变,如点击Button的时候直接showDialog,不需要UI库使用者手动在源码中设置true或者false,而是通过UI界面点击Button进行切换展示即可,如何做?
解决方法:
新建openDialog函数:libs——openDialog.ts
以下示例关键6个点import Dialog from "./Dialog.vue" //导入Dialog以便页面展示Dialog
import { createApp, h } from "vue" //createApp用于
//h函数用于渲染Dialog
export const openDialog = (options) => {
//读取外部传入的选项,用户可自定义添加 ————————1.
const { title, content, ok, cancel } = options
//页面中创建div,div插入body ————————2.
const div = document.createElement('div')
document.body.append(div)
//声明close函数销毁app和移除div ————————3.
const close = () => {
app.unmount(div)
div.remove()
}
//通过createApp渲染Dialog ————————4.
const app = createApp({
render() {
//通过h函数传3种参,控制Dialog渲染 ————————5.
return h(
Dialog,
{visible: true, 'onUpdate:visible': (newVisible) => {if (newVisible === false) {close()}}, ok, cancel},
{ title, content }
)
}
})
//将Dialog挂载到新建的div中 ————————6.
app.mount(div)
}
关键方法:使用h函数参数来控制Dialog渲染
h() 函数是一个用于创建 VNode 的实用程序,即构造虚拟节点app.unmount(div)
//div为之前挂载点,故销毁app同样也是这个点,注意:销毁app后记得最好也移除div
绝大多数情况下使用模板来创建 HTML。然而在一些场景中,比如重复性很高的模板可以替换为render函数渲染出来,真的需要 JavaScript 的完全编程的能力。
当组件更改时,Render函数将重新运行,它将创建另一个虚拟节点。然后发送旧的 VNode 和新的 VNode 到Vue中进行比较并以最高效的方式在我们的网页上更新。//基本格式:
render() {
return h()
...
}
制作Tabs组件
API设计//标签形式
<Tabs>
<Tab title = '导航1'>内容1</Tab>
<Tab title = '导航2'> <Component1/> </Tab>
<Tab title = '导航3'> <Component1 x='hi' / > </Tab>
</Tabs>
HTML骨架制作
//TabsDemo模板
<template>
<div>Tabs示例</div>
<h1>示例1</h1>
<Tabs>
<Tab title="导航1">内容1</Tab>
<Tab title="导航2">内容2</Tab>
</Tabs>
</template>
vue3中检查子组件类型
考虑库使用者的角度,如在Tab组件中用户没有传tabs子组件,而是自己传自定义组件div,这样就无法达到库的使用效果,如果以防用户传错子组件?即如何检测用户传进来的子组件类型?
→ context.slots.default() 返回的是一个数组,可以遍历每一项对象,即每个组件就是一个对象,对象包含很多属性,其中有render函数
如何在运行时确认子组件的类型? <Tabs> //如何在Tabs组件运行时确认其子组件/标签的类型?
<Tab title="导航1">内容1</Tab>
<Tab title="导航2">内容2</Tab>
</Tabs>
setup函数中的形参context:涉及slots
context.slots.default()数组:放的是子组件们
context.slots.default()[i].type:可拿到运行时子组件的类型
防御型编程:判断是否与传入的Tab组件相同,不相同则抛出错误,thow new Error(‘…’)
补充:Vue3的component内置组件
<!-- 动态组件由 vm 实例的 `componentId` property 控制 -->
<component :is="componentId"></component>
<!-- 也能够渲染注册过的组件或 prop 传入的组件-->
<component :is="$options.components.child"></component>
<!-- 可以通过字符串引用组件 -->
<component :is="condition ? 'FooComponent' : 'BarComponent'"></component>
<!-- 可以用来渲染原生 HTML 元素 -->
<component :is="href ? 'a' : 'span'"></component>
如何检查UI库使用者正确声明Tabs父组件里的是Tab子组件,防止出现div等其他标签?(预防型编程)
解决方法:
遍历defaults数组中的每一项(将展示在界面的标签们),添加判断机制,若声明的不是Tab子标签,则丢出Error报错,并停止代码向下执行 const defaults = context.slots.default();
defaults.map((tag) => {
if (tag.type !== Tab) {
throw new Error("Tabs子标签必须是Tab");
}
})
如何显示Tab组件传入的title属性即导航1和内容1
分析难题:
细分难题为:
①内容1和内容2为子组件内容,→如何渲染嵌套组件(渲染component)?
②导航1和导航2为子组件的props →如何渲染嵌套组件内的props(渲染div)?
↓
遇到难题:
如何渲染嵌套的组件?即
当出现两层嵌套关系时(三种关系,爷爷、爸爸和儿子,爸爸套在爷爷里,儿子套在爸爸里)
且子组件有多个时
使用slot标签则会一次性渲染所有子组件,无法精确把控每个子组件的信息
此时不满足Tabs爸爸组件点击Tab出现对应Tab儿子组件的特殊需求
解决方法:
①用component内置组件自定义“插槽”+ v-for遍历defaults内所有组件 → 得到内容12,实现动态组件且可缓存
②用div渲染Tab组件内的props传递的属性title + v-for遍历defaults.props.title →得到导航12
关键方法:
console大法好
首先console出context,在几个参数中选择相关的slots
再选择默认的default或者相关的pops.. <div v-for="(t, index) in titles" :key="index">{{ t }}</div> //导航12
<component v-for="(c, index) in defaults" :is="c" :key="index"></component> //内容12
CSS样式与动画
Tabs组件使用CSS切换内容
关键:
动态绑定class + 判断点击的title与爷爷组件传入的props引用值是否一致,一致则标记为selected:class="{ selected: c.props.title === selected }"
缺点:
css加载会阻塞js执行,阻塞dom树的渲染
JS添加交互
使用JS切换标签页
如何显示被选中Tab?
→用selected标记被选中的Tab(标签页),由最外层Tabs绑定selected变量决定哪个Tab被选中
解决方法:
①selected用index表示,不推荐,新增和删除容易出现BUG
②selected用name表示,不方便,props传的是字符串,万一UI库使用者用错
③selected用title表示,有漏洞,万一使用者重复用title//index表示selected
<Tabs :selected="0">
<Tab title="导航1">内容1</Tab>
<Tab title="导航2">内容2</Tab>
</Tabs>
//name表示selected
<Tabs selected="tab1">
<Tab name="tab1" title="导航1">内容1</Tab>
<Tab name="tab2" title="导航2">内容2</Tab>
</Tabs>
//title表示selected -----✔
<Tabs selected="导航1">
<Tab title="导航1">内容1</Tab>
<Tab title="导航2">内容2</Tab>
</Tabs>
确定切换标签页使用title表示selected后,
再确定如何将选中的Tab组件显示其属性和内容?(导航1和内容1)
①显示被选中的导航?
②显示被选中的内容?
解决方法:
<Tabs selected="导航1"><Tab/>...</Tabs>
②爸爸Tabs组件接收props:{ selected: { type:String}}
并渲染到模板中 :class"{selected: t ==== selected}"
const current = defaults.filter((tag) => {
return tag.props.title === props.selected;
})[0]; //filter()返回值是一个数组,必须加index才能显示
补充:filter()和map()区别
map函数,遍历数组每个元素,并回调操作,需要返回值,返回值组成新的数组,原数组不变
filter是满足条件的留下,是对原数组的过滤;map则是对原数组的加工,映射成一一映射的新数组
选中Tab后标签页头 下方出现下划线indicator,
如何设置动态的indicator的宽度?使其宽度等同于标签页的title宽度?
(其实这个需求不太好,若每个indicator不一致则UI视觉不好~ 可以固定Tab标签页大小?效率更高?)
解决方法:
突破口:获取导航的宽度
如何获取?JS如何获取div宽度?
找到渲染导航的模板处,需要用ref引用它得到它具体各种信息,发现它是v-for出来的,无法直接获取需要使用动态ref,且因为它是多个div,故我们需要用数组引用存放它
使用Ref:const navItems = ref<HTMLDivElement[]>([]);
声明导航的引用const indicator = ref<HTMLDivElement>(null);
声明indicator的引用<div class="roll-tabs-nav" ref="container">
<div
class="roll-tabs-nav-item"
v-for="(t, index) in titles"
*/此处/* :ref="(el) => {if (t === selected) selectedItem = el}"
@click="select(t)"
:class="{ selected: t === selected }"
:key="index"
>
{{ t }}
</div>
<div class="roll-tabs-nav-indicator" ref="indicator"></div>
</div>
//在组件挂载的时候,找到类名包含selceted的div,获取其宽度
onMounted(() => {
const divs = navItems.value;
const result = divs.find((div) => div.classList.contains("selected"));
const { width } = result.getBoundingClientRect(); //获取节点宽度的方法
indicator.value.style.width = width + "px";
});
补充:Element.getBoundingClientRect()和Node.contains()
onMounted只在第一次渲染执行,故在第二次点击切换Tab的时候不生效,而点击切换Tab相当于更新组件,故需要在onUpadeted中再次执行相应更新操作补充:Vue3的watchEffect
可代替上文提到的onMounted和onUpdatedconst count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0 ——————————类似于onMounted
setTimeout(() => {
count.value++
// -> logs 1 ————————类似于onUpdated
}, 100)
watchEffect在onMounted之前就开始运行了,故容易出现onMounted之前访问不到组件,组件为undefined或null的情况
解决方法:
将watchEffect()函数包裹在onMounted()函数内,限定范围,使其开始于onMounted后,即等组件挂载渲染完成后再监听
完善官网细节
完善Home首页
设计取巧
到Drrible网站搜索素材
找到满意素材后吸取关键颜色(如网页整体背景颜色,按钮颜色和字体颜色,标题颜色…)
素材来源:Drrible/站酷/花瓣/cssgradient/webgradients
symbol引用图标
加入购物车,添加到项目中(同时也是代码项目,必须唯一专一)
symbol引用法:
①首先在项目文件的index.html中的head中添加script标签,在script标签的src属性中加入复制的代码(每次图标更新都需要重新引用)
②加入通用css代码(引入一次就行) .icon {
width: 1em; height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xxx"></use> //把xxx改为图标的名称
</svg>
本地引用图标
也可以使用本地的SVG,只要将之前复制的代码删掉前面//再粘贴到浏览器中,复制页面所有文本,
代码项目中新建svg.js文件,在main.ts引入svg文件;
将之前复制的所有文本粘贴到svg文件中即可制作圆弧曲线样式
border-bottom-left-radius:50% 40px;
border-bottom-left-radius:50% 40px
缺点:不是完美的圆弧,两边有圆角趋势
②clip-path:ellipse()方法:(✔)clip-path: ellipse(80% 60% at 50% 40%);
Grid布局
响应式页面
手机样式+@media(min-width:600px)ipad宽度600-800+ @media(min-width:800px)小的电脑屏幕 +(min-width:1200px) 大的电脑屏幕
重要程度和书写顺序依次递进
完善Doc文档页面
增加路由
①在views目录增加对应页面
②router.ts路由增加映射
③挂到Doc模板上
选中高亮提示(样式)
注意:router-link本质上会转化为a标签,而a标签是inline元素,要控制它的宽高则需要display:block
Vue路由自带选中标记class
router-link-active 默认是全包含匹配规则,即path名全包含在当前router path名开头的router也会被匹配到。
如果使用router-link-active:
/router 可以匹配/router 和 /router/2
而如果使用exact的话
/router 则只能匹配/router,不能匹配/router/2文档内容支持markdown
然后引用第三方库github-markdown-css,在对应article标签上添加对应类选择器即可
①yarn add github-markdown-css
②在main.ts全局导入
③在对应文档页的模板内的根标签上添加class=”markdown-body”
文档内容如何支持markdown编写?
首先清楚定位,本项目使用的是Vue3+Vite,而Vite使用的是浏览器自带能力和rollup,但是在本项目中没有引入rollup插件,且rollup运行在生产环境下的,开发环境下只有浏览器自带能力,而浏览器不支持md文件,只支持js文件,浏览器会将接收到的css/js文件等解析成js文件,
👉如何让UI库提供者在开发环境下直接用markdown编写文档内容?
vite使用markdown的解决方案
搜索Vue3 Vite plugin
vite是支持插件的 ,可以像webpack自制loader一样自制插件
简记vite和webpack区别
loader是webpack的概念
思路主要是将md文件转化为js文件// @ts-nocheck
import path from 'path'
import fs from 'fs'
import marked from 'marked'
const mdToJs = str => {
const content = JSON.stringify(marked(str))
return `export default ${content}`
}
export function md() {
return {
configureServer: [ // 用于开发环境
async ({ app }) => {
app.use(async (ctx, next) => { // koa
if (ctx.path.endsWith('.md')) {
ctx.type = 'js'
const filePath = path.join(process.cwd(), ctx.path)
ctx.body = mdToJs(fs.readFileSync(filePath).toString())
} else {
await next()
}
})
},
],
transforms: [{ // 用于 rollup //即生产环境
test: context => context.path.endsWith('.md'),
transform: ({ code }) => mdToJs(code)
}]
}
}
import { md } from "./plugins/md"
export default {
...
plugins: [md()],
...
<template>
<article class="markdown-body" v-html="md"></article>
</template>
import md from '../markdowns/intro.md'
<script>
data(){
return {md}
}
</script>
以上插件在开发环境下适用,但在生产环境下是否适用呢?可以适用!
组件模板重复,事不过三,如何减少重复?(优化代码)
解决方法:
可以写一个组件来代替这三个组件大部分重复的内容,而个性的内容则可以通过传参来显示,如何传参?
组件之间传参使用props,但是import导入文件发生在组件渲染之前,但import又不能直接写入export default中,故需要通过ES6异步的import()动态引入文件
注意:
<a name="z0tyv"></a>
#### 补充:动态import()异步引入
import()类似于 Node.js 的require()方法,区别主要是前者是异步加载,后者是同步加载,都是运行时加载<br />**遇到问题:**<br />如何拿到import返回的值?<br />`import(props.path)`返回一个 Promise 对象,这种写法是异步的,我们想拿到它的**返回值**的话**需要用await,而await又需要结合async,**但是setup不支持写async,故使用其他方法:<br />**解决方法:**<br />首先声明一个ref为null的容器,通过import()后then拿到import返回的结果来设置content的值<br />**缺点:**异步加载一般比同步慢一点
**遇到问题:**<br />在Aside中点击不同Tab的时候,内容区内容无变化,需要刷新才会获取对应新的内容,如何自动更新内容?<br />**解决方法:**<br />之前Tabs组件也遇到该难题(**component里面的current不变问题**),解决方案是在该子组件上**添加key值**,使其监听diff然后自动更新
**遇到问题:**<br />**component里的current不变的问题**<br />**解决方法:添加key**
<a name="TEC2o"></a>
#### 如何展示源代码
**遇到问题:**<br />为了方便UI库使用者拷贝源代码,如何展示源代码?<br />**解决方法:**<br />**①首先将SwitchDemo.vue中两个Switch组件分别抽离出来**<br />新建文件:components——Swicth1.demo.vue和Switch2.demo.vue,专门写给使用者拷贝的文件,只写用户需要的内容<br />引入模板:将Switch1.demo.vue和Switch2.demo.vue嵌入SwitchDemo.vue模板中<br />**②然后再分别获取到它们的源代码并展示**
**遇到问题:**<br />如何获取到各组件当中的源代码?以便在SwitchDemo模板中展示?
**解决方法:**<br />需要使用vue-loader 的 Custom Blocks 自定义块功能<br />为了注入自定义块的内容,我们将会使用vue-loader的custom blocks功能;打开vite官网查询<br />配置vite.config.ts;
```javascript
import { md } from "./plugins/md"
import fs from 'fs'
import { baseParse } from '@vue/compiler-core'
export default {
base: './',
assetsDir: 'assets',
plugins: [md()],
...
//获取对应组件的 源代码
vueCustomBlockTransforms: {
demo: (options) => {
const { code, path } = options
const file = fs.readFileSync(path).toString()
const parsed = baseParse(file).children.find(n => n.tag === 'demo')
const title = parsed.children[0].content
const main = file.split(parsed.loc.source).join('').trim()
return `export default function (Component) {
Component.__sourceCode = ${JSON.stringify(main)
}
Component.__sourceCodeTitle = ${JSON.stringify(title)}
}`.trim()
}
}
}
思路:
在Switch1.demo.vue组件的首行添加
如果发现该子展示组件(源代码提供者Swith1Demo.vue)有demo标签,则将除了demo标签外的所有源代码内容都放入父展示组件SwitchDemo.vue中的component组件中的sourceCode中prismjs 代码皮肤
如何给展示的源代码添加高亮背景及代码样式,提高阅读体验感?
解决方法:
使用prismjs 第三方库 结合v-html
注意:prismjs 导入方式:需要引入对应js和cssimport "prismjs";
const Prism = (window as any).Prism; //骗过ts
import "prismjs/themes/prism-okaidia.css"; //在node_module中找到
由于v-html属性过长,后期可以使用computed计算属性计算出来并返回,将其命名为html计算属性
源代码展示区的背景色可以通过 添加class= 'language-html'
开源
部署官网至github/gitee
yarn build 后开启hs服务器打开链接在线预览官网,发现文档页面无法显示内容,控制器报错找不到对应模块
(build之后不加载md文件)
报错原因:
我们在router.ts中写md文件的路径使用的是动态加载(即字符串拼接,程序执行到${)后读取到引用变量后才能完成),即执行md函数的时候才能获取到md文件内容,rollup不支持import()时拼字符串,因为它只能分析代码,不能运行代码
解决方法:
需要将动态加载改为静态
首先统一导入md文件,再传入md函数中用h函数渲染
注意:需要在shims-vue.d.ts中声明md格式,告诉ts这个路径对应的文件是什么部署到github步骤
按照以上流程部署dist目录到github的pages后,打开链接添加index.html后页面没有显示内容,控制台404,网络请求查看请求标头如下:
分析问题:
发现index.html请求并没有报错,对比index.html的请求标头的请求网址,猜测是目录层级问题导致,在js文件的请求网址的assets前添加Roll-UI-website一级,发现仍然报错,原因是:assets的存在触发了gitbub的潜规则,github会把下划线的部分删掉
查看报错的网页源代码发现,请求的文件均为 /开头的路径,即以https://chen-qionghua.github.io/作为根路径,必须改为以 ./开头的相对路径,即以https://chen-qionghua.github.io/Roll-UI-website/ 作为相对根路径注意 build path
在vite.config.ts中添加以下两行,修改根路径为相对根路径,相对Roll-UI-website为根路径, ./ 等价于 /Roll-UI-website/
回到根文件夹重新yarn build ,发现dist目录中原本的assets目录文件名 变成assets
使用bash一键部署
在根文件下执行命令行为 sh delpoy.sh
注意:cd - 等价于 cd .. (因为不一定回到main分支,一般回到上一个分支)部署到码云gitee
发布Roll UI库至npm
发布 dist/libs/目录(其实就是上传到npm的服务器)
步骤
npx rollup -c
相当build编译方向
一些细节
import Swicth from './Switch.vue'
export { Switch } from './Switch.vue'
//以上两句等价于下面一句
export { default as Switch } from './Swicth.vue' //把Swicth默认的东西 当作 Swicth 导出
"name": "roll_ui", //package的name必须是小写字母,且不能和npm上现有的name重名,可用-或_连接
"version": "0.0.1", //每次publish的版本不能和之前任意一次的相同
"files": [ "dist/libs/*" ], //需要上传的所有文件
"main": "dist/libs/roll.js", //上传文件中的主要文件
"module": "dist/libs/roll.esm.js",
npm config get registry //获取源
npm config set registry https://registry.npmjs.org/ //设置源为官方源
npm config set registry https://registry.npm.taobao.org/ //设置为淘宝源
补充:npm官网查看package"resolutions": {"node-sass":"npm:sass@^1.26.11"}
禁止npm下载node-sass,因为它下载特别慢
Yarn add 装包报错: operation not permitted, unlink …error An unexpected error occurred: "EPERM: operation not permitted, unlink 'C:\\Users\\xxx\\AppData\\Local\\Yarn\\Data\\global\\node_modules\\.bin\\serve'".
解决方法:C:\\Users\\xxx\\AppData\\Local\\Yarn\\Data\\global\\node_modules\\.bin\\
目录下的serve被占用
仔细想了一下,当前serve在另一个项目中在使用,把它关闭以后,重新运行yarn global add babel,就可以了
这个问题产生的原因就是在装包的时候,会删除之前的.bin文件再重新生成,由于文件被占用导致无法删除文件,因此就会报错,只需要关闭相应的占用程序即可。
配置好 rollup.config.js 和 package.json 后运行 rollup -c 报错 bash: rollup: command not found
npx rollup -c
npx 会自动查找当前依赖包中的可执行文件,如果找不到,就会去 PATH 里找。如果依然找不到,就会帮你安装
npx 甚至支持运行远程仓库的可执行文件
npx http-server 可以一句话帮你开启一个静态服务器$ npx http-server
npx: 23 安装成功,用时 48.633 秒
Starting up http-server, serving ./
Available on:
http://127.0.0.1:8080
http://192.168.5.14:8080
Hit CTRL-C to stop the server
解释rollup.config.js
import esbuild from 'rollup-plugin-esbuild' //把ts变成js
import vue from 'rollup-plugin-vue' //把vue变成js
import scss from 'rollup-plugin-scss' //把scss变成js
import dartSass from 'sass'; //支持上面scss的插件
import { terser } from "rollup-plugin-terser"//把js变丑让别人看不懂
export default {
input: 'src/libs/index.ts', //输入的文件
output: { //输出的文件
globals: {
vue: 'Vue' //全局的变量/外部依赖,不需要打包
},
name: 'Roll', //库名称
file: 'dist/libs/roll.js', //输出的文件,自动会添加css文件,故只需写js文件
format: 'umd', //统一的模块定义器umd,让所有人都可以使用我的文件
plugins: [terser()] //插件,丑化js
},
plugins: [ //其他插件
scss({ include: /\.scss$/, sass: dartSass }),
esbuild({
include: /\.[jt]s$/,
minify: process.env.NODE_ENV === 'production',
target: 'es2015'
}),
vue({
include: /\.vue$/,
})
],
}
模拟别人使用自己的组件
yarn global add @vue/cli
vue create .
初始化配置时注意去掉Linter检查
import { Button } from 'roll_ui' ; import 'roll_ui/dist/libs/roll.css
UI库使用者的页面无法显示组件,控制台警告Invalid VNode type: Symbol(Comment)
解决方法:
把和组件库运行时无关的依赖移至 devDependencies 和 peerDependencies
区分官网build和轮子build
sh deploy.sh
安装:npm i -g nrm --registry https://registry.npm.taobao.org
nrm ls
可以查看所有源nrm use taobao
切换到淘宝源nrm use npm 切换到npm
官方源