Vue3造轮子项目gu-ui笔记
全局安装(全局安装 create-vite-app)
yarn global add create-vite-app@1.18.0
或者
npm i -g create-vite-app@1.18.0
创建vue3项目
cva gulu-ui-1 或者 create-vite-app gulu-ui-1
其中 gulu-ui-1 可以改为任意名称
打开项目(上面的步骤完成后)运行一下命令
//进入项目目录
cd gulu-ui-1
//安装依赖
yarn
//开启项目
yarn dev
小知识点
vite 文档给出的命令是
npm init vite-app <project-name>
yarn create vite-app <project-name>
等价于
全局安装 create-vite-app 然后
cva <project-name>
等价于
npx createa-vite-app <project-name>
即 npx 会帮你全局安装用到的包
知识点2(Vue2和Vue3的区别)
90%的写法完全一致,除了一些几点
- Vue3的Template支持多个根标签,Vue2不支持
- Vue 3 有 createApp(),而 Vue 2 的是 new Vue()
- createApp(组件),new Vue({template, render})
引入 Vue Router 4
使用过一下命令查看版本
npm info vue-router versions
安装(vue-router)
yarn add vue-router@4.0.0-beta.3
初始化 vue-router(创建路由)
- 创建组件设置路由
//router.ts文件
import Doc from './views/Doc.vue'
import Home from './views/Home.vue'
import SwitchDemo from './components/SwitchDemo.vue'
import ButtonDemo from './components/ButtonDemo.vue'
import DialogDemo from './components/DialogDemo.vue'
import TabsDemo from './components/TabsDemo.vue'
import DocDemo from './components/DocDemo.vue'
//引入createWebHashHistory创建history路由迷失
//引入createRouter创建路由router
import {createWebHashHistory,createRouter} from 'vue-router'
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}
],
},
],
})
router.afterEach(() => {
console.log("路由切换了");
});
在main.ts挂在路由
//main.ts文件
import './index.scss'
import './lib/guIu.scss'
import { createApp } from 'vue'
import App from './App.vue'
//引入router路由
import {router} from './router'
import 'github-markdown-css'
import marked from './components/marked.vue'
const app = createApp(App)
app.use(router)
//挂载到组件上
app.mount('#app')
app.component('Marked',marked)
导航点击图标像是隐藏侧边栏
需要用到 provide / inject (实现功能)
provide / inject 是什么?
**定义说明:**这对选项是一起使用的。以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
通俗的说就是:组件得引入层次过多,我们的子孙组件想要获取祖先组件得资源,那么怎么办呢,总不能一直取父级往上吧,而且这样代码结构容易混乱。这个就是这对选项要干的事情。
**provide:**是一个对象,或者是一个返回对象的函数。里面呢就包含要给子孙后代的东西,也就是属性和属性值。
**inject:**一个字符串数组,或者是一个对象。属性值可以是一个对象,包含from和default默认值。
//App.vue中设置provide
<script lang="ts" >
//引入provide, ref
import { provide, ref } from 'vue'
import {router} from './router'
export default {
name: 'App',
setup(){
const width = document.documentElement.clientWidth
const menuVisible = ref(width>500?true:false)
router.beforeEach(()=>{
if(width<500){
menuVisible.value=false
}
})
//设置数据
provide('menuVisible',menuVisible)
}
}
</script>
在APP组件中使用provide设置变量,子组件中使用inject接受,使用时要引入
//Doc.组件 menuVisible为 true显示,false隐藏
<template>
<div class="layout">
<Topanv 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></router-view>
</main>
</div>
</div>
</template>
<script lang="ts">
//引入inject
import { inject, Ref } from 'vue';
import Topanv from '../components/Topanv.vue'
export default {
components: { Topanv },
setup() {
//获取menuVisible
const menuVisible = inject<Ref<boolean>>('menuVisible')
return {menuVisible}
}
};
</script>
在Topanv.vue 组件接收menuVisible,点击按钮改变menuVisible的值实现侧边栏显示和隐藏
<template>
<div class="topanv">
<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">
//引入inject
import { inject, Ref } from 'vue'
export default {
setup() {
//获取menuVisible
const menuVisible = inject<Ref<boolean>>('menuVisible')
const toggleMenu = ()=>{
//改变menuVisible的值
menuVisible.value = !menuVisible.value
}
return {toggleMenu}
},
}
</script>
知识点3
- provide / inject的使用
//父组件中设置provide
//引入provide, ref
import { provide, ref } from 'vue'
//传入变量
provide('menuVisible',menuVisible)
=======
//子组件中使用inject接收变量
//引入inject
import { inject, Ref } from 'vue'
//接收变量
const menuVisible = inject<Ref<boolean>>('menuVisible')
- 嵌套路由的设置(children)
{
path:'/Doc',
component:Doc,
children:[
{path:'',component:DocDemo},
{path:'switch',component:SwitchDemo},
{path:'button',component:ButtonDemo},
{path:'dialog',component:DialogDemo},
{path:'tabs',component:TabsDemo}
],
},
Switch组件(点一下就开,再点一下就关)
第一步:需求分析(搞清楚你做的是个什么玩意)
第二步:API 设计
- Switch 组件怎么用
<Switch value="true" /> value 为字符串 "true"
<Switch value="false" /> value 为字符串 "false"
<Switch :value=" true " /> value 为布尔值 true
<Switch :value=" false " /> value 为布尔值 false
总结
- 当 value 为字符串 "true" 或布尔值 true 时,显示为开
- 其他情况一律显示为关
Switch组件的实现
<template>
<button @click="toggle" class="guIu-Switch" :class="{ 'guIu-checked': value }">
<span></span>
</button>
</template>
<script lang="ts">
export default {
props: {
value: {
type: Boolean,
},
},
setup(props, context) {
const toggle = () => {
context.emit("update:value", !props.value);
};
return { toggle };
},
};
</script>
知识点
- props(父子组件的传值)
//父组件传值
<Switch value="add" />
//组组件使用props接收
<script lang="ts">
export default {
//此时为接收参数的类型
props: {
value: {
type: Boolean,
},
},
setup(props, context) {
const toggle = () => {
//使用props.value获取参数
context.emit("update:value", !props.value);
};
return { toggle };
},
};
</script>
- 子组件使用context中的emit向父组件传值
//父组件
<Switch v-model:value="bool" />
相当于
<Switch value="bool" @update:value"bool =$event" />
//子组件
<script lang="ts">
export default {
//此时为接收参数的类型
props: {
value: {
type: Boolean,
},
},
//在vue2中使用this.emit("update:value", !props.value);实现子组件向父组件长材
//在vue3中不能使用this,使用内置的context来实现 context.emit("update:value", !props.value);
setup(props, context) {
const toggle = () => {
context.emit("update:value", !props.value);
};
return {toggle };
},
};
知识点总结
Vue 3 编程模型 内部数据 V.S. 父子数据
注意
为什么事件名是 input 你可以改成其他名字,用 input 是我的个人习惯 event 的值是 emit 的第二个参数 emit(事件名, 事件参数)
v-model(对「父子之间的数据交流」进行简化)
Vue 3 的 v-model
要求
属性名任意,假设为 x 事件名必须为 "update:x"
效果
<Switch :value="y" @update:value="y = $event"/> 可以简写为
文档
这是 Vue 2 到 Vue 3 的一个大变动(breaking change) 文档里面有详细的介绍
Vue3总结
- value="true" 和 :value="true" 的区别
- 使用 CSS transition 添加过渡动画
- 使用 ref 创建内部数据
- 使用 :value 和 @input 让父子组件进行交流(组件通信)
- 使用 $event
- 使用 v-model
注意:框架就是把你框起来:不准改 props
Vue 2 和 Vue 3 的区别
- 新 v-model 代替以前的 v-model 和 .sync
- 新增 context.emit,与 this.$emit 作用相同
Button 组件
参考一下别人的 Button
- 参考AntD、 Bulma、Element、iView、Vuetify 等
需求
- 可以有不同的等级(level)
- 可以是链接,可以是文字
- 可以 click、focus、鼠标悬浮
- 可以改变 size:大中小
- 可以禁用(disabled)
- 可以加载中(loading)
第二步:API 设计
Button 组件怎么用
<Button
@click=?
@focus=?
@mouseover=?
theme="button or link or text"
level="main or normal or minor"
size="big normal small"
disabled
loading
></Button>
Button 组件
<template>
<button class="guIu-button" :class="classes" :disabled="disabled">
<span v-if="loading" class="gulu-loadingIndicator"></span>
<slot />
</button>
</template>
<script lang="ts">
import { computed } from "vue";
export default {
props: {
theme: {
type: String,
default: "button",
},
size: {
type: String,
default: "normal",
},
level: {
type: String,
default: "normal",
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
},
setup(props) {
const { theme, size, level } = props;
const classes = computed(() => {
return {
[`guIu-theme-${theme}`]: theme,
[`guIu-size-${size}`]: size,
[`guIu-level-${level}`]: level,
};
});
return { classes };
},
};
</script>
知识点
computed的使用
<script>
// 引入computed
import { computed } from "vue";
export default {
props: {
....
},
setup(props) {
const { theme, size, level } = props;
const classes = computed(() => {
return {
[`guIu-theme-${theme}`]: theme,
[`guIu-size-${size}`]: size,
[`guIu-level-${level}`]: level,
};
});
return { classes };
},
};
</script>
组件添加样式类
实例
//组件内接收theme
<button :class="{[`guIu-theme-${theme}`]:theme}"></button>
小结
Vue 3 属性绑定
- 默认所有属性都绑定到根元素
- 使用 inheritAttrs: false 可以取消默认绑定
- 使用 $attrs 或者 context.attrs 获取所有属性
- 使用 v-bind="$attrs" 批量绑定属性
- 使用 const {size, level, ...xxx} = context.attrs 将属性分开
代码
<template>
<div :size="size">
<button v-bind="rest">
<slot />
</button>
</div>
</template>
<script lang="ts">
export default {
inheritAttrs: false,
props: {
},
setup(props, context) {
const { size, ...rest } = context.attrs;
return { size, rest };
},
};
</script>
<style lang="scss" scoped>
div {
border: 1px solid red;
}
</style>
注意:UI 库的 CSS 注意事项
不能使用 scoped
- 因为 data-v-xxx 中的 xxx 每次运行可能不同
- 必须输出稳定不变的 class 选择器,方便使用者覆盖
必须加前缀
- .button 不行,很容易被使用者覆盖
- .guIu-button 可以,不太容易被覆盖
- .theme-link 不行,很容易被使用者覆盖
- .guIu-theme-link 可以,不太容易被覆盖
CSS 最小影响原则(你的 CSS 绝对不能影响库使用者)
Dialog 组件(对话框)
Dialog 组件
参考一下别人的 Button
- 参考AntD、 Bulma、Element、iView、Vuetify 等
需求
- 点击后弹出
- 有遮罩层 overlay
- 有 close 按钮
- 有标题
- 有内容
- 有 yes / no 按钮
第二步:API 设计
Dialog 组件怎么用
<Dialog visible title="标题" @yes="fn1" @no="fn2"></Dialog>
Dialog 组件
<template>
<template v-if="visible">
<Teleport to="body">
<div class="guIu-dialog-overlay" @click="noclose"></div>
<div class="guIu-dialog-wrapper">
<div class="guIu-dialog">
<header>
<slot name="title"/>
<span class="guIu-dialog-close" @click="close"></span>
</header>
<main>
<slot name="content" />
</main>
<footer>
<Button level="main" @click="ok">OK</Button>
<Button @click="Cancel">Cancel</Button>
</footer>
</div>
</div>
</Teleport>
</template>
</template>
<script lang="ts">
import Button from "./Button.vue";
export default {
components: { Button },
props: {
visible: {
type: Boolean,
default: false,
},
noclose: {
type: Boolean,
default: true,
},
ok: {
type: Function,
},
Cancel: {
type: Function,
},
},
setup(props, context) {
const close = () => {
context.emit("update:visible", false);
};
const noclose = () => {
if (props.noclose) {
close();
}
};
const ok = () => {
if (props.ok?.() !== false) {
close();
}
};
const Cancel = () => {
props.Cancel?.()
close();
};
return { close, noclose, ok, Cancel };
},
};
</script>
知识点
slot插槽的使用(slot标签的位置会被替换成填入的内容)
- 一般情况(没有名字的插槽)
实例代码
//Dialog组件
<template>
<div>
<slot/> <!--会被替换成小明 -->
</div>
</template>
======
//使用Dialog组件
<template>
<Dialog>小明</Dialog>
</template>
<script>
import Dialog from "./Dialog.vue"
export default{
components:{Dialog}
}
</script>
- 使用有名称的插槽(给slot标签添加name属性)
实例实例代码
//Dialog组件
<template>
<div>
<h1>姓名</h1>
<slot name="name"/> <!--会被替换<p>小明 好好</p> -->
<h1>年龄</h1>
<slot name="age" /> <!--会被替换<p>26岁</p> -->
</div>
</template>
======
//使用Dialog组件
<template>
<Dialog>
<template v-slot:name>
<p>小明 好好</p>
</template>
<template v-slot:gae>
<p>26岁</p>
</template>
</Dialog>
</template>
<script>
import Dialog from "./Dialog.vue"
export default{
components:{Dialog}
}
</script>
新组件:Teleport(传送 / 传送门)
Vue 3.0 新增了一个内置组件teleport,主要是为了解决以下场景:
有时组件模板的一部分逻辑上属于该组件,而从技术角度来看,最好将模板的这一部分移动到 DOM 中 Vue app 之外的其他位置
作用 (官方起名叫瞬移)
这个组件不会渲染出任何元素,该组件有个属性to Vue在渲染这个元素时,把teleport里面的元素渲染到了to属性指定的地方,把元素移动到指定的地方去渲染 而且不影响数据的响应式。
解决的问题(把 Dialog 移到 body 下防止 Dialog 被遮挡)
使用方法(使用to属性绑定为body)
<template>
<template v-if="visible">
//把组件移动到body里
<Teleport to="body">
<div class="guIu-dialog-overlay" @click="noclose"></div>
<div class="guIu-dialog-wrapper">
<div class="guIu-dialog">
<header>
<slot name="title"/>
<span class="guIu-dialog-close" @click="close"></span>
</header>
<main>
<slot name="content" />
</main>
<footer>
<Button level="main" @click="ok">OK</Button>
<Button @click="Cancel">Cancel</Button>
</footer>
</div>
</div>
</Teleport>
</template>
</template>
动态挂载组件(使用 createApp, h函数)
//引入Dialog组件
import Dialog from './Dialog.vue'
//引入createApp, h
import { createApp, h } from 'vue'
//创建openDialog
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,
}
)
}
})
//挂载到div上
app.mount(div);
}
Tabs 组件
参考一下别人的对话框
- 参考AntD、AntD Vue、 Bulma、Element、iView、Vuetify 等
需求
- 点击 Tab 切换内容
- 有一条横线在动
第二步:API 设计
Tabs 组件怎么用(创建tabs组件、tab组件)
//tab组件
<template>
<div>
<slot />
</div>
</template>
========
//tabs组件
<template>
<div class="guIu-tabs">
<div class="guIu-tabs-nav" ref="container">
<div
class="guIu-tabs-nav-item"
v-for="(t, index) in titles"
@click="select(t)"
:ref="
(el) => {
if (t === selected) selectedItem = el;
}
"
:class="{ selected: t === selected }"
:key="index"
>
{{ t }}
</div>
<div class="guIu-tabs-nav-indicator" ref="indicator"></div>
</div>
<div class="guIu-tabs-content">
<component
class="guIu-tabs-content-item"
v-for="(c, index) in defaults"
:key="index"
:class="{ selected: c.props.title === selected }"
:is="c"
>
</component>
</div>
</div>
</template>
<script lang="ts">
import { computed, onMounted, watchEffect, onUpdated, ref } from "vue";
import Tab from "./Tab.vue";
export default {
props: {
selected: {
type: String,
},
},
setup(props, context) {
const selectedItem = ref<HTMLDivElement>(null);
const indicator = ref<HTMLDivElement>(null);
const container = ref<HTMLDivElement>(null);
onMounted(() => {
watchEffect(() => {
const { width } = selectedItem.value.getBoundingClientRect();
indicator.value.style.width = width + "px";
const { left: left1 } = container.value.getBoundingClientRect();
const { left: left2 } = selectedItem.value.getBoundingClientRect();
const left = left2 - left1;
indicator.value.style.left = left + "px";
});
});
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,
selectedItem,
indicator,
container,
};
},
};
</script>
知识点
使用vue内置组件component
-
Props:
is-string | Component | VNode
-
用法:
渲染一个“元组件”为动态组件。依
is的值,来决定哪个组件被渲染。is的值是一个字符串,它既可以是 HTML 标签名称也可以是组件名称。
<!-- 动态组件由 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>
结合内置组件的用法:
内置组件 KeepAlive、Transition、TransitionGroup 和 Teleport 都可以被传递给 is,但是如果你想要通过名字传入它们,就必须注册。例如:
const { Transition, TransitionGroup } = Vue
const Component = {
components: {
Transition,
TransitionGroup
},
template: `
<component :is="isGroup ? 'TransitionGroup' : 'Transition'">
...
</component>
`
}
如果你传递组件本身到 is 而不是其名字,则不需要注册。
结合 VNode 的用法
在高阶使用场景中,通过模板来渲染现有的 VNode 有时候会是很有用的。通过 <component> 可以实现这种场景,但它应该被视为一种回退策略,用来避免将整个模板改写为 render 函数。
<component :is="vnode" :key="aSuitableKey" />
以这种方式混用 VNode 与模板的注意事项是你需要提供一个合适的 key attribute。VNode 将被认为是静态的,所以除非 key 发生变化,任何更新都将被忽略。key 可以设置在 VNode 或者 <component> 标签上,但无论哪种方式,你都需要在想要 VNode 重新渲染时更改它。如果这些节点具有不同的类型,比如将 span 更改为 div,那么此注意事项将不适用。 文档链接
用 JS 获取插槽内容
- 使用context.slots.default(确认子组件的类型)
- Tabs组件中的代码判断子组件的类型是否是Tab标签
。。。
<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>
vue3中在 setup 内注册生命周期
onMounted / onUpdated /(钩子的使用)
实例代码
。。。
<script>
import {onMounted,onUpdated,watchEffect } from 'vue'
export default{
setop(){
onMounted(){
console.log('组件挂载完毕时触发')
},
onUpdated(){
console.log('组件更新时触发')
}
}
}
</script>
在setup中使用watchEffect
- 立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
实例代码(在count变化时watchEffect函数执行)
。。。
<script>
import {watchEffect } from 'vue'
export default{
setop(){
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
}
</script>
TypeScript 泛型
const indicator = ref <HTMLDivElement> (null)
获取宽高和位置
const { width, left } = el.getBoundingClientRect()
ES 6 析构赋值的重命名语法
const { left: left1 } = x. getBoundingClientRect()
const { left: left2 } = y. getBoundingClientRect()
官网装修 知识点
router active class(高亮当前路由(模糊匹配)
用 exact 可以精确匹配
exact
类型: boolean
默认值: false
“是否激活”默认类名的依据是包含匹配。 举个例子,如果当前的路径是 /a 开头的,那么 也会被设置 CSS 类名。 按照这个规则,每个路由都会激活 !想要链接使用“精确匹配模式”,则使用 exact 属性:
<!-- 这个链接只会在地址为 / 的时候被激活 -->
<router-link to="/" exact></router-link>
添加文章 (支持 Markdown)
- 安装Markdown
yarn add Markdown
yarn add github-markdown-css //github Markdown样式
- 自制 Vite 插件(创建vite.config.ts)
// @ts-nocheck
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 请注意,当前文件的后缀从 .js 改为了 .ts
// 如果你看到这行注释,请确认文件后缀是 .ts
// 然后就可以删掉本注释了!!!!!!!!!!!!!!!!
import { md } from "./plugins/md";
export default {
plugins: [md()]
};
- 创建md.ts文件
// @ts-nocheck
import path from 'path'
import fs from 'fs'
import marked from 'marked'
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 请注意,当前文件的后缀从 .js 改为了 .ts
// 如果你看到这行注释,请确认文件后缀是 .ts
// 然后就可以删掉本注释了!!!!!!!!!!!!!!!!
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)
}]
}
}
最终使用
<template>
<article class="markdown-body" v-html="md">
</article>
</template>
<script>
//引入md文件
import md from '../mardown/get-started.md';
export default {
data() {
return {
md//返回字符串形式的nd文具店内容
}
}
}
</script>
优化创建markdown组件
<template>
<article class="markdown-body" v-html="content">
</article>
</template>
<script lang="ts">
import {ref} from 'vue'
export default {
props: {
path: {
type: String,
required: true
}
},
setup(props) {
const content = ref < string > (null)
import(props.path).then(result => {
content.value = result.default
})
return {
content
}
}
}
</script>
官网装修最后创建demo组件
知识点
使用baseParse配置获得组件的信息(vite.config.ts文件)
import { md } from "./plugins/md";
import fs from 'fs'
//引入baseParse
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()
}
}
};
demo组件
<template>
<div class="demo">
<h2>{{component.__sourceCodeTitle}}</h2>
<div class="demo-component">
<component :is="component" />
</div>
<div class="demo-actions">
<Button @click="hideCode" v-if="codeVisible">隐藏代码</Button>
<Button @click="showCode" v-else>查看代码</Button>
</div>
<div class="demo-code" v-if="codeVisible">
<pre class="language-html" v-html="html" />
</div>
</div>
</template>
<script lang="ts">
import Button from '../lib/Button.vue'
import 'prismjs';
import 'prismjs/themes/prism.css'
import {
computed,
ref
} from 'vue';
const Prism = (window as any).Prism
export default {
components: {
Button
},
props: {
component: Object
},
setup(props) {
const html = computed(() => {
return Prism.highlight(props.component.__sourceCode, Prism.languages.html, 'html')
})
const showCode = () => codeVisible.value = true
const hideCode = () => codeVisible.value = false
const codeVisible = ref(false)
return {
Prism,
html,
codeVisible,
showCode,
hideCode
}
}
}
</script>
。。。
使用 Prism.js 实现代码高亮
- 安装prismjs
yarn add prismjs
简介
Prism 是一款轻量、可扩展的代码语法高亮库,使用现代化的 Web 标准构建。
-
为什么选择 Prism?
极致易用 引用 prism.css 和 prism.js,使用合适的 HTML5 标签(code.language-xxxx),搞定!
天生伶俐 语言的 CSS 类是可继承的,所以你只需定义一次就能应用到多个代码片段。
轻如鸿毛 代码压缩后只有 1.6KB。每添加一个语言平均增加 0.3-0.5KB,主题在 1KB 左右。
快如闪电 如果可能,支持通过 Web Workers 实现并行。
轻松扩展 定义新语言或扩展现有语法,或者新增功能都非常简单。
丰富样式 所有的样式通过 CSS 完成,并使用合理的类名如:.comment, .string, .property 等。
line-numbers便是显示行号,language-markup就是语言。
使用实例
<template>
<pre class="language-html" v-html="html" />
</template>
<script>
//引入prismjs
import 'prismjs';
//引入prismjs的主题
import 'prismjs/themes/prism.css'
export default{
setup(){
//使用Prism.highlight生成HTML
const html = computed(() => {
return Prism.highlight(string(代码字符串),Prism.languages.html, 'html')
})
return {html}
}
}
</script>
一键发布
创建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@gitee.com:frankfang/gulu-ui-website-1.git &&
git push -f -u origin master &&
cd -
发布到npm
打包库文件(vite 对此功能不支持,需要自行配置 rollup)
- 创建 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';
- rollup.config.js(告诉 rollup 怎么打包)
//先安装 rollup-plugin-esbuild rollup-plugin-vue rollup-plugin-scss sass rollup-plugin-terser
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: 'Gulu',
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
请先全局安装 rollup(或者局部安装)
yarn global add rollup
npm i -g rollup
发布 dist/lib/ 目录(其实就是上传到 npm 的服务器)
- 添加 files 和 main
...
"files": [
"dist/lib/*"
],
"main": "dist/lib/gulu.js",
...
- 登录npm(先把npm切换到npm源)
npm login 登录
登录之后才能 npm publish 上传
使用 npm logout 可以退出登录
一些细节
name
- package 的 name 必须是小写字母,可用 - 或 _ 连接
- package 的 name 不能跟 npm 上现有的 name 重名
version
- 每次 publish 的版本不能与上一次的相同
- 所以从第二次开始,必须先改 version 再 publish