Vue3造轮子开发流程(gulu-ui-1)

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外的红色框也会触发事件

解决方法:

  1. 将所有事件绑定到指定元素:让div不继承 + 批量绑定
  1. 将指定事件绑定在指定元素:让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%)

装修官网文档页

复杂高亮:加到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

使用步骤:

  1. 创建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)
        }]
    }
}
  1. 安装marked【md.ts引用了marked所以要安装】(不要用最新版本,否则和node不匹配)
csharp 复制代码
yarn add --dev marked@1.1.1   // 或者
npm i -D marked
  1. 创建vite.config.ts
javascript 复制代码
// @ts-nocheck

import { md } from "./plugins/md";

export default {
    plugins: [md()]
};
  1. 使用:创建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】

步骤:

  1. 创建 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 后可以给组件重命名

  1. 创建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$/,
    })
  ],
}
  1. 运行 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    
  1. 发布 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细节:

经验:

  1. 在给命令行使用代理后你会发现无法传代码到GitHub。 坑:ssh: connect to host github.com port 22: Connection refused(重点是最后一句)
  2. git使用技巧:
c 复制代码
git log    // 查看提交历史
git checkout 【hash】   // 进入某一次提交的代码
  1. 从vue2升级到vue3:vue-codenmod
相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
迷糊的『迷』2 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
web135085886352 小时前
uniapp小程序使用webview 嵌套 vue 项目
vue.js·小程序·uni-app
陈大爷(有低保)2 小时前
uniapp小案例---趣味打字坤
前端·javascript·vue.js
cronaldo913 小时前
研发效能DevOps: Vite 使用 Element Plus
vue.js·vue·devops
百罹鸟3 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
Java_慈祥3 小时前
慈様や 前端学习导航👩🏻‍🚀🚀
前端·javascript·vue.js
编程百晓君5 小时前
一文解释清楚OpenHarmony面向全场景的分布式操作系统
vue.js
暴富的Tdy5 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se5 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel