Vue3+TS 造轮子

446 阅读3分钟

一、技术描述

  • 使用 create-vite-app 搭建官网。
  • 使用 Vue3 + typescript 实现,涵盖了Switch、Button、Dialog、Tabs常见UI组件。
  • 支持markdown语法高亮展示代码,在线查看说明文档和模块化的源代码。
  • 使用rollup进行项目库打包,并发布到npm供其他开发者使用。
  • 使用shell脚本自动化部署。
  • 响应式设计,同时支持手机端PC端的浏览。

二、效果预览

预览链接

PC 端首页

1.png

移动端首页

2.png

文档页

3.png

组件展示

4.png

三、准备工作

1. 安装软件

  • 安装nodejs稳定版
  • 安装npm,使用淘宝源加速,运行命令:npm config set registry registry.npm.taobao.org
  • 安装yarn
  • 安装vscode 或webstorm用于项目开发

2. 使用vite搭建

  • 全局安装create-vite-app
yarn global add create-vite-app

npm i -g create-vite-app
  • 创建项目
cva time-uicreate-vite-app time-ui //bobo-ui可以改成自己的项目名 

  • 打开项目
cd time-ui
  • 运行以下命令,打开本地网址,开始开发
yarn dev 

四、知识总结

1. vue-router

  • 安装: yarn add vue-router
  • 初始化:新建 history对象;新建router对象
//main.ts 文件
import { createApp } from 'vue';
import App from './App.vue';
import {createWebHashHistory,createRouter} from 'vue-router';
const history = createWebHashHistory();
const router = createRouter({
    history,
    routes:[
        {path:'/',component:Bobo},
        {path:'/xxx',component:Bobo2}
    ]
});

const app = createApp(App);
app.use(router);
app.mount('#app')
  • router-view & router-link
//App.vue 文件
<template>
  <div>导航栏|
    <router-link to="/">BOBO</router-link>|
    <router-link to="/xxx">BOBO2</router-link></div>
  <hr />
  <router-view />
</template>

<script>
export default {
  name: 'App',
}
</script>

2. provide 和 inject

//父组件做标记
<script lang="ts">
import {ref ,provide} from 'vue';
export default {
  name: 'App',
  setup(){
    const asideVisible = ref(false);
    provide('asideVisible',asideVisible);
  }

}
</script>

//子组件接收值
<template>
    <aside v-if="asideVisible">
        <h2>组件列表</h2>
    </aside>
</template>

<script lang="ts">
import {inject, Ref} from "vue";
export default {
  setup(){
    const asideVisible = inject<Ref<boolean>>('asideVisible')
    return {asideVisible}
  }
}
</script>

3. props传值和v-model

//父组件
<template>
  <div>
    <Switch :value = "y"  @update:value="y=$event"/>
  </div>
</template>

<script lang="ts">
import Switch from '../lib/Switch.vue'
import {ref} from 'vue';
export default {
  components:{Switch},
  setup(){
    const y = ref(false);
    return {y}
  }
}
</script>
//子组件
<template>
  <button @click="toggle" :class="{checked:value}"></button>
</template>

<script lang="ts">
export default {
  props:{
    value:Boolean
  },
  setup(props,context){
    const toggle = () =>{
      context.emit('update:value',!props.value)
    }
    return {toggle};
  }
}
</script>
//v-model
//父组件
<Switch v-model:value = "y" />

//子组件
context.emit('update:value',!props.value)

4. vue3属性绑定(Attrs)

<template>
  //父元素传过来的所有属性默认绑定到根元素上,可以不继承,并且拆分属性只绑定部分属性 
  <div :size = "size">
    <!-- <button v-bind="$attrs"> 把父元素传过来的所有属性绑定到button上 -->
    <button v-bind="rest"> <!-- 只绑定除size外的剩余属性 -->
      <slot/>
    </button>
  </div>
</template>

<script lang="ts">
export default {
  inheritAttrs:false,//模板根元素不继承父元素传过来的事件
  setup(props,context){
    const{onClick,onMouseOver,size,...rest}=context.attrs;//拿到父元素传的属性解构赋值
    return{onClick,onMouseOver,size,rest};
  }
}
</script>
  • 默认所有属性都绑定到根元素上。
  • 使用inheritAttrs: false可以取消默认绑定
  • 使用this.$attrs或者context.attrs获取所有属性
  • 使用 v-bind="$attrs" 批量绑定属性
  • 使用 const { size,...rest } = context.attrs将属性分开

5. 具名插槽

//父组件
<Dialog>
    <template v-slot:title>
      <strong>标题</strong>
    </template>
    <template v-slot:content>
      <strong>内容</strong>
    </template>
 </Dialog>
//子组件
<header>
 <slot name="title"/> 
</header>
<main>
  <slot name="content"/>
</main>

6. 渲染嵌套插槽

//父组件
<template>
  <Tabs>
    <Tab title="导航1">内容1</Tab>
    <Tab title="导航2">内容2</Tab>
  </Tabs>
</template>
//嵌套子组件 Tabs.vue
<template>
  <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(); //可以取到所有的Tab
    return {defaults};
}
</script>
//嵌套子组件 Tab.vue,父组件的内容1、内容2,会插到slot
<template>
  <div>
    <slot/>  
  </div>
</template>

7. Teleport

任意传送门:Teleport 是一种能够将我们的模板移动到 DOM 中 Vue app 之外的其他位置的技术

//直接将这一块传送到Dom节点里面的body
<Teleport to = "body">
<div>
一大块代码
</div>
</Teleport>

8. ref 和 getgetBoundingClientRect

<template>
<div class="time-tabs-nav-indicator " ref="indicator">aaa</div>  
</template>

<script lang="ts">
    const indicator = ref<HTMLDivElement>(null);
    const {width,height,top,left} = indicator.getBoundingClientRect();//获取indicator引用的div元素的宽高等
    return {indicator};
</script>

9. 引入SVG图标

  • iconfont官网选中图标加入购物车项目,打开购物车该,点击symbol,点击生成代码
  • 将生成的地址链接复制到项目中index.htmlheader
 <script src="//at.alicdn.com/t/font_2426847_zhxe2xwikqs.js"></script>
  • 点击旁边的帮助,搜索symbol引用,按文档指示把css复制到项目的index.css文件
.icon {
  width: 1em; height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
  • 在项目中加入svg图标
<svg class="icon" >
   <use xlink:href="#icon-Vue"></use> //icon-Vue为图标在iconfont的名字
</svg>

10. clip-path和border-radious都可以用于画圆弧

11. 支持github的markdown样式

markdown github地址

yarn add github-markdown-css //安装
//使用
import 'github-markdown-css' //在mian.ts引入,就可以直接使用
<template>
  <article class="markdown-body" >//加入class="markdown-body"就可以显示为markdown样式
   <h1>标题</h1>
   <p>扩大放大</P>
  </article>
</template>

12. 支持项目中引入markdown文件

  • src同级目录下新建plugins,添加文件md.ts
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:yarn add --dev marked
  • src同级目录下创建vite.config.ts,引入plugins/md.ts,传给plugins
import { md } from "./plugins/md";
export default {
    plugins: [md()]
};
  • 使用实例
<template>
  <article class="markdown-body" v-html="md">
  </article>
</template>

<script lang="ts">
import md from '../markdown/intro.md';
export default {
  data() {
    return {
      md
    }
  }
}
</script>

13. createApp和 h 函数

  • 使用createApp这个 API返回一个应用实例,并且可以通过链条的方式继续调用其他的方法
createApp(App).use(router).use(store).mount('#app')
//openDialog.ts
import Dialog from './Dialog.vue';
import {createApp,h} from 'vue';

export const openDialog = (options) =>{
    const {title,content,ok,cancel,closeOnClickOverlay}= 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,
                    closeOnClickOverlay
                },
                {title,content}
            )
        }
    });
    app.mount(div);
}

14.显示源代码

  • 修改vite.config.ts文件配置
import fs from 'fs'
import {baseParse} from '@vue/compiler-core'

export default {
    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()
        }
    }
};
  • 在 switch1demo中的template前加上<demo>常规用法</demo>

image.png

  • 在SwitchDemo.vue父组件引用Switch1Demo,添加预览
<div class="demo">
      <h2>常规用法</h2>
      <div class="demo-component">
        <component :is="Switch1Demo"/> //此处是组件
      </div>
      <div class="demo-actions">
        <Button>查看代码</Button>
      </div>
      <div class="demo-code">
        <pre>{{Switch1Demo.__sourceCode}}</pre> //此处展示组件的源代码
      </div>
</div>

15.高亮源代码

1、安装prismjs yarn add prismjs

2、在使用Demo的地方SwitchDemo.vue里import

import 'prismjs'
const Prism = (window as any) .Prism

3、查看node_modules里的prismjs里面的themes中和prism.css同级的其他css,就是可以引用的css样式,在使用Demo的地方SwitchDemo.vue里import

import 'prismjs/themes/prism.css'

4、使用Prism.highlight就可以对源代码进行高亮显示

<pre 
  class="language-html" 
  v-html="Prism.highlight(Switch2Demo.__sourceCode, Prism.languages.html, 'html')" 
/>