Vite环境搭建
创建项目
arduino
npm config set registry https://registry.npmmirror.org // 注意要先换成taobao的源
yarn global add create-vite-app
cva gulu-ui-1
cd gulu-ui-1
npm install (or `yarn`)
npm run dev (or `yarn dev`)
VS Code插件
arduino
auto import
生态
csharp
// 安装 vue-router
npm info vue-router versions
yarn add vue-router@4.0.0-beta.3
// 安装 sass
yarn add -D sass@1.26.10
初始化vue-router
php
// 新建history对象;router对象
const history = createWebHashHistory()
const router = createRouter({
history,
routes: [
{ path: '/', component: Lu }
]
})
// app.use(router)
const app = createApp(App)
app.use(router)
解决ts找不到模块
shims-vue.d.ts
declare module '*.vue' {
import { ComponentOptions } from "vue";
const ComponentOptions: ComponentOptions
export default ComponentOptions
}
功能
基本组件
1. 点击切换aside ------ 父组件provide,子组件inject
2. 路由切换:
兄弟路由:A组件router-link to"组件B"
父子路由:A1组件router-link to"组件A2",router-view
{path="/b" , component=B}
3. doc点击switch显示(可滚动)组件,手机端切换路由隐藏aside;PC端不隐藏(afterEach + css实现)
目前为止官网基本搭建完成。
存在的bug:1.只有在doc页面切换路由才关闭aside 而非 app.vue中的只要路由切换就关闭 2.手机端home页面不应该出现toggleAside标志,但他是镶嵌在tapnav上的
4. Switch组件
确定需求 --> API设计 --> 写代码
初步完成组件动画:绑定class(设置点击前后两个css) + 点击事件(改变class的值) + transition动画
弊端:无法设置value的初始值是开还是关
优化:父子组件props传参 + emit改变事件
SwitchDemo
// 添加value属性和input事件
<Switch value:"y" @input="y=$emit" /> // input可写成`update:value
<img src="setup" alt="" width="50%" />(){
const y = ref(false) // 可以设置初始值
return {y}
}
Switch
<button @click="toggle" :class="{checked:value}"> <span></span> </button>
props:{
value: Boolean
},
setup(props,context){
const toggle=()=>{
context.emit('input', !props.value) // input可写成`update:value
}
}
vue2:input规定写成update:value
vue3:v-model
vue2 --> vue3变化:
5. Button组件
需求:有等级(主要/默认)、可以是链接或文字、可以click/focus/鼠标悬浮、可以改变size、可以禁用(disabled)、可以加载中(loading)
API设计:
注意:
vue3事件绑定的问题 :click事件会自动追加到被引用的Button组件最外层元素(div)上,这会导致点击button外的红色框也会触发事件
解决方法:
- 将所有事件绑定到指定元素:让div不继承 + 批量绑定
- 将指定事件绑定在指定元素:让div不继承 + 批量绑定 + 剩余操作符
注意:
UI组件库的注意事项:(switch.vue)
最小css原则:不能影响用户的css
arduino
// 以gulu-开头,含有 gulu- 的css
[class^="gulu-"], [class*=" gulu-"] { css样式 }
初步创建theme属性的button
注意这三种写法:1,2是正解(true在props规定是什么类型就是什么类型),3true就变成了字符串
前两个组件知识点总结:
6. Dialog组件
需求:点击有弹出、有遮罩overlay、有close按钮、有标题内容、有yes/no按钮
API设计:
关闭dialog:不能修改props【可关闭的地方:x、ok/cencel、遮罩(可选,默认禁用/开启)】
事件没有返回值,ok和cancel用函数
注意:
防止dialog被遮挡:Teleport将dialog组件移到body下就可以提高它的css优先级
DialogDemo
<div style="position: relative; z-index: 1;"> // Dialog对话框放在1里,z-index就是1中的10,比1小
<Button @click="toggle">toggle</Button>
<Dialog v-model:visible="x" :closeOnClickOverlay="false" :ok="f1" :cancel="f2">
//...插槽内容...
</Dialog>
</div>
<div style="position: relative; z-index: 2; width: 300px;
height: 300px; background: red;"></div> // 红色遮挡z-index为3
Dialog移到body下
<template v-if="visible"> // Dialog的z-index现在是10
<Teleport to="body">
<div class="gulu-dialog-overlay" @click="onClickOverlay"></div>
...Dialog内容
</div>
</Teleport>
</template>
换个思路,不用v-model绑定visible,直接调一个函数打开Dialog。实现思路:调用函数将dialog挂载到div上
DialogDemo
<Button @click="showDialog">showDialog</Button>
const showDialog = ()=> {
openDialog({
title: h('strong', {}, '标题'),
content: '你好',
ok() {
console.log('ok')
},
cancel() {
console.log('cancel')
}
})
}
openDialog.ts
import Dialog from "./Dialog.vue";
import { createApp, h } from "vue";
export const openDialog = (options) => {
const { title, content, ok, cancel } = options;
const div = document.createElement('div');
document.body.appendChild(div);
const close = () => {
app.unmount(div);
div.remove();
};
const app = createApp({
render() {
return h(
Dialog,
{
visible: true,
"onUpdate:visible": (newVisible) =>
{
if (newVisible === false) {
close();
}
},
ok, cancel
},
{
title,
content,
}
);
},
});
app.mount(div);
};
7. Tabs组件
需求:点击Tab切换内容、下划线在动
API设计:(第二种兼容性更好)
注意:
如果用户手贱把<Tabs><Tab></Tab></Tabs>
中的tab标签写错了怎么办,如何判断子组件类型并拿到传给子组件的内容 (因为直接这样写是不显示Tab组件里的内容的):context.slots.default()
和<component :is="defaults[0]" />
就能拿到给子组件传的值 (Tabs.vue)
检查类型:
没见过的语法:<component :is="xxx">
表示动态切换要渲染的组件(相当于插槽,用于显示Tab组件内容)
Tabs.vue
<template>
<div>
<div v-for="(t,index) in titles" :key="index">{{ t }}</div>
// <component :is="xxx">表示动态切换要渲染的组件
<component v-for="(c,index) in defaults" :is="c" :key="index" />
</div>
</template>
<script lang="ts">
import Tab from './Tab.vue'
export default {
components: {
Tab
},
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>
Tab.vue
<div>
<slot></slot>
</div>
TabsDemo.vue
<div>
<h1>Tab组件</h1>
<Tabs>
<Tab title="导航1">内容1</Tab>
<Tab title="导航2">内容2</Tab>
</Tabs>
</div>
实现切换标签页:css + v-model:selected="x"
+ select函数
实现思路:在TabsDemo双向绑定selected"开关"并赋予初始值【注意不要直接修改porps,所以用selected="x"
】,当点击title时触发select函数改变selected的值,设置被选中 的title显示content的css,未选中的隐藏
Tabs.vue
<template>
<div class="gulu-tabs">
<div class="gulu-tabs-nav">
<div class="gulu-tabs-nav-item" v-for="(t,index) in titles" @click="select(t)" :class="{selected: t=== selected}" :key="index">{{t}}</div>
</div>
<div class="gulu-tabs-content">
<component class="gulu-tabs-content-item" :class="{selected: c.props.title === selected }" v-for="c in defaults" :is="c" />
</div>
</div>
</template>
<script lang="ts">
import Tab from './Tab.vue'
import { computed } from 'vue'
export default {
props: {
selected: {
type: String
}
},
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
})
const select = (title: string) => {
context.emit('update:selected', title)
}
return {
defaults,
titles,
select
}
}
}
</script>
<style lang="scss">
$blue: #40a9ff;
$color: #333;
$border-color: #d9d9d9;
.gulu-tabs {
&-nav {
display: flex;
color: $color;
border-bottom: 1px solid $border-color; // 设置底部边框为1像素实线,颜色为变量 $border-color 定义的颜色
&-item {
padding: 8px 0; // 设置内边距
margin: 0 16px; // 设置外边距
cursor: pointer; // 设置鼠标悬停时显示手型指示器
&:first-child { // 选择第一个子元素
margin-left: 0; // 设置左外边距为0
}
&.selected {
color: $blue;
}
}
}
&-content {
padding: 8px 0;
&-item {
display: none; // 设置显示方式为不可见
&.selected {
display: block; // 设置显示方式为块级元素
}
}
}
}
</style>
点击title下划线滑动【没写完!!!】
优化:watchEffect代替onMounted和onUpdated
装修官网首页
渐变:css Gradient
使用开发者工具调整css颜色:选中元素点击color
php
// 使用css变量封装颜色
$green: #02bcb0;
background: $green
添加icon/logo:去icon font.cn找对应图标的symble【前两种过时了】
-
方法1. 使用官网链接:script引入
<script src="//at.alicdn.com/t/font_2057051_9ta5kvxsl9d.js"></script>
-
方法2. 点进链接下载到本地:无需script引入,将本地js文件添加到项目中,并引入到main.ts里
import './lib/svg.js'
画圆弧:
- 方法1:有瑕疵
css
border-bottom-left-radius: 50% 40px;
border-bottom-right-radius: 50% 40px;
- 方法2:
clip-path: ellipse(80% 60% at 50% 40%)
装修官网文档页
简单高亮当前路由:.router-link-active { color: red; }
复杂高亮:加到li标签
css
ol {
> li {
>a {
display: block;
padding: 4px 16px;
text-decoration: none;
}
.router-link-active {
background: white;
}
}
}
}
引入github-markdown样式:github-markdown-css
arduino
yarn add github-markdown-css
import 'github-markdown-css' // main.ts
<article class="markdown-body"> // Install.vue
直接引入markdown文件:自制Vite插件,搜索Writing a vite plugin. With Vue 3 on the horizon, learn how to... | by Andrew Walker | Medium
使用步骤:
- 创建plugin/md.ts
javascript
// @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)
}]
}
}
- 安装marked【md.ts引用了marked所以要安装】(不要用最新版本,否则和node不匹配)
csharp
yarn add --dev marked@1.1.1 // 或者
npm i -D marked
- 创建vite.config.ts
javascript
// @ts-nocheck
import { md } from "./plugins/md";
export default {
plugins: [md()]
};
- 使用:创建markdown/xx.md,并在要引用的组件里引用该文件
markdown/intro.md
# 介绍
King UI 是一款基于 Vue 3 和 TypeScript 的 UI 组件库。
xxx。
下一节:[安装](#/doc/install)
views/intro.vue
<template>
<article class="markdown-body" v-html="md">
</article>
</template>
<script lang="ts">
import md from '../markdown/intro.md'
export default {
data() {
return { // md其实是字符串
md
}
}
}
</script>
打包后本地预览:
csharp
yarn global add http-server
http-server dist/ -c-1
优化:事不过三
封装markdown.vue
Markdown.vue
<template>
<article class="markdown-body" v-html="content">
</article>
</template>
<script lang="ts">
export default {
props: {
content: {
type: String,
required: true
},
}
}
</script>
全局引入:让某个东西在项目任何地方无需导入直接使用
main.ts
import Markdown from './components/Markdown.vue';
app.component("Markdown", Markdown)
静态导入路径:Vite也希望你使用静态导入而非动态导入
解决引入vue/md后缀的文件标红:shims-vue.d.ts
展示源代码【方便用户拷贝】:vue-loader的Custom Blocks
vite.config.ts
// @ts-nocheck
import { md } from "./plugins/md";
import fs from 'fs'
import { baseParse } from '@vue/compiler-core'
export default {
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()
}
}
};
SwitchDemo.vue
<pre>{{Switch1Demo.__sourceCode}}</pre> // 展示源代码
Switch1.demo.vue
<demo>
常规用法
</demo>
<template>
<Switch v-model:value="bool" />
</template>
<script lang="ts">
import Switch from '../lib/Switch.vue'
import {
ref
} from 'vue'
export default {
components: {
Switch,
},
setup() {
const bool = ref(false)
return {
bool
}
}
}
</script>
高亮源代码:prismjs + v-html
typescript
yarn add prismjs@1.21.0
// SwitchDemo.vue
<pre class="language-html" v-html="Prism.highlight
(Switch1Demo.__sourceCode, Prism.languages.html, 'html')" />
import 'prismjs/themes/prism.css'
import 'prismjs'
const Prism = (window as any).Prism // window上没有这个属性,强制让ts不报错
部署到GitHub
上传代码前先build,并使用http-server dist -c-1
检测网站是否正常
修改build path的配置:上传后路径会出现_assets导致出错,要把它去掉
vite.config.ts
export default {
base: './',
assetsDir: 'assets',
}
上传部署代码:cd dist
,然后按照github文档操作
一键部署:sh deploy.sh // 确保路径为项目所在路径
由于每次更改代码再上传,都需要先删除dist,再重新上传非常麻烦,所以我们用一个脚本一键部署。记得勾上开发者模式的"禁用缓存"。
deploy.sh
rm -rf dist &&
yarn build &&
cd dist &&
git init &&
git add . &&
git commit -m "update" &&
git branch -M master &&
git remote add origin git@github.com:atlfsj/vue3-gulu-aurora-ui.git &&
git push -f -u origin master &&
cd -
echo https://atlfsj.github.io/vue3-gulu-aurora-ui/index.html#/
小bug:Tab组件开发环境和生产环境的tag.type不等于Tab
解决方法:投机取巧,给Tab组件加一个name,只对比tag.type.name是否和Tab.name相等
Tab.vue
<script lang="ts">
export default {
name: 'GuluTab'
}
</script>
Tabs.vue
defaults.forEach((tag) => {
// @ts-ignore // 避免ts检查
if (tag.type.name !== Tab.name) {
throw new Error('Tabs 子标签必须是 Tab')
}
})
移动svg.js:如果你的代码使用了本地svg,请将它移动到assets/svg.js并修改引用
打包成库文件上传到npm【vite不支持该功能,要自行配置roll up】
步骤:
- 创建 lib/index.ts,将所有需要导出的东西导出
lib/index.ts
export { default as Switch } from './Switch.vue';
export { default as Button } from './Button.vue';
export { default as Tabs } from './Tabs.vue';
export { default as Tab } from './Tab.vue';
export { default as Dialog } from './Dialog.vue';
export { openDialog as openDialog } from './openDialog';
这几行代码相当于import组件再export default组件的简写,而且 as 后可以给组件重命名
- 创建rollup.config.js,告诉 rollup 怎么打包
rollup.config.js
// 请先安装 rollup-plugin-esbuild rollup-plugin-vue rollup-plugin-scss sass rollup-plugin-terser
// 为了保证版本一致,请复制我的 package.json 到你的项目,并把 name 改成你的库名
import esbuild from 'rollup-plugin-esbuild'
import vue from 'rollup-plugin-vue'
import scss from 'rollup-plugin-scss'
import dartSass from 'sass';
import { terser } from "rollup-plugin-terser"
export default {
input: 'src/lib/index.ts',
output: {
globals: {
vue: 'Vue'
},
name: 'vue3-gulu-aurora-ui',
file: 'dist/lib/gulu.js',
format: 'umd',
plugins: [terser()]
},
plugins: [
scss({ include: /\.scss$/, sass: dartSass }),
esbuild({
include: /\.[jt]s$/,
minify: process.env.NODE_ENV === 'production',
target: 'es2015'
}),
vue({
include: /\.vue$/,
})
],
}
- 运行 rollup -c【将index.ts编译成dist/lib/gulu.js】
- 由于版本问题,实际需要直接复制我的配置yarn.lock和package.json
- 正常的步骤:
修改Switch.vue的css
Switch.vue
// 修改css内容
在代码全局搜索 $h/2和$h2/2,替换成 math.div($h, 2);
@use "sass:math"; // 这一句要写在 css 最上面
下载安装运行rollup依赖
sql
// 安装rollup-plugin...(rollup.config.js注释里)
yarn add --dev rollup-plugin...
// 全局安装 rollup@2(或者局部安装)
yarn global add rollup@2 或
npm i -g rollup@2
yarn install
rollup -c
- 发布 dist/lib/ 目录,上传到 npm 的服务器
添加files和main
package.json
"files": [
"dist/lib/*"
],
"main": "dist/lib/gulu.js",
确保使用npm官方源:
arduino
npm config get registry
npm config set registry https://registry.npmjs.org/
先登录npm再上传
arduino
npm adduser
npm publish
npm logout // 退出登录
每次修改代码要先删node_modules,install,rollup -c,修改版本号后再上传npm
一些package.json细节:
经验:
- 在给命令行使用代理后你会发现无法传代码到GitHub。 坑:ssh: connect to host github.com port 22: Connection refused(重点是最后一句)
- git使用技巧:
c
git log // 查看提交历史
git checkout 【hash】 // 进入某一次提交的代码
- 从vue2升级到vue3:vue-codenmod