基于vite的vue3轮子笔记

1,389 阅读7分钟

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)

导航点击图标像是隐藏侧边栏

1649643152882.png

1649643372033.png

需要用到 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组件(点一下就开,再点一下就关)

第一步:需求分析(搞清楚你做的是个什么玩意)

1649645661684.png

第二步: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. 父子数据

1649647005300.png

1649647083708.png

注意

为什么事件名是 input 你可以改成其他名字,用 input 是我的个人习惯 event是个什么玩意是尤雨溪创造的一个变量,不喜欢你就去找尤雨溪event 是个什么玩意 是尤雨溪创造的一个变量,不喜欢你就去找尤雨溪 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>

结合内置组件的用法:

内置组件 KeepAliveTransitionTransitionGroupTeleport 都可以被传递给 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 等。

官网地址:prismjs.com/download.ht…

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