wheel-amazing-ui 开发笔记以及部分 BUG 解决记录

490 阅读5分钟

搭建环境

  • 安装 vite
  1. 全局安装 vite-creat-app: yarn global add create-vite-app@1.18.0
  2. 安装结束后,有两个全局命令可以使用: create-vite-app 或者简写:cva
  • 创建项目
  1. 使用 cva 项目名 或者 create-vite-app 项目名
  2. 执行 vite 提供的三个命令,安装依赖,启动项目
  3. 将 main.js 改为 ts 文件,不要忘记改 index.html 中的引用

引入使用 Vue Router 4

  1. 使用命令行查看 vue-router 的所有版本号: npm info vue-router version
  2. 安装最新版本 yarn add vue-router@4.0.14
  3. 初始化 vue-router
    • 新建 history 对象
    • 新建 router 对象
      • main.ts 内:
          import Home from './views/home.vue'
          import {createWebHashHistory,createRouter} from 'vue-router'
          const history = createWebHashHistory()
          const router = createRouter({
          history:history,
              routes:[
                  {path:'/', component: Home}
              ]
          })
          const app = createApp(App)
          app.use(router)
          app.mount('#app')
      
      • 改为 ts 后,vue 文件引入报错:ts 无法理解 .vue 结尾的文件
      • 解决方法:
        • src 目录创建 d.ts 文件:比如 shims-vue.d.ts
            declare module '*.vue' {
                import {ComponentOptions} from 'vue'
                const componentOptions: ComponentOptions
                export default componentOptions
            }
        
        • 项目根目录创建 tsconfig.json
            {
                "compilerOptions": {
                "target": "esnext",
                "module": "esnext",
                "strict": false,
                "jsx": "preserve",
                "moduleResolution": "node"
                }
            }
        

封装 Topnav

初始化 home、doc 页面

使用 provide 和 inject 实现点击切换 aside

// TODO: 当前为手机页面时,点击任何地方都实现隐藏 aside,并添加隐藏动画(使用 vant)

配置 doc 页面 aside 的子路由

使用后置路由守卫来实现点击 aside 后显示内容并关闭 aside

  1. 将 router 相关的代码封装至新的文件: route.ts
  2. 文件中导出 router 以用来其他文件导入并使用路由守卫
  3. 在 app 文件中使用 router.afterEach 实现跳转路由后自动关闭 aside // TODO: 当前判断路由关闭的逻辑范围太广,只要路由跳转就会触发,目前思路为判断所有 aside 路由,待优化

制作 switch 组件

  1. 效果设计:抄 vant 的样式
  2. API: 组件接受一个 value,当 value 的值为 true 时,显示为开,false 为关;disable 为 true 时禁用
  3. 新建 lib 文件夹以存放所有组件,components 下引用 lib 中的组件
  • 笔记:一个元素绑定 class::class={checked},意思为如果 checked 为 true,那么就有 checked 类,如果为 false,那么久没有 checked 类,以上是简写,全写为::class={checked:checked}
  1. 实现点击切换 switch
  2. 添加切换动画
  3. 优化组件
    • 问题一:无法控制 switch 更新后的状态
    • 问题二:外界无法获取当前状态
    • 解决: switch 的 value(当前状态的值) 是外界传入的
    • vue3 的 v-model 写法:v-model:value="value",相当于 vue2 的 snyc 修饰符(vue3 的 v-model 也改了)

制作 button 组件

  1. 设计需求:
    • 可以有不同的等级(level)
    • 可以是链接,可以是文字
    • 可以 click、focus、mouseover
    • 可以改变 size: 大、中、小
    • 可以禁用 (disabled)
    • 可以是加载中
  2. 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>
    
  3. 注意的问题:
    • 外界传入的事件、属性,默认会绑定在组件的最外层元素上。
      1. 组件内使用 inheritAttrs:false,关闭继承所有事件和属性
      2. 在组件内需要继承的元素上使用 v-band="$attrs" 继承外部属性和事件(以对象形式传入)
      • context.attrs 可以获取所有属性和事件
      • 分开绑定使用剩余操作符
  4. props 和 attrs 区别
    1. props需要先声明才能获取值,而attrs则不用
    2. props声明过的属性,attrs里面不会在出现
    3. props不包含事件,attrs包含
    4. props支持string以外的类型,而attrs只有string类型

UI 库的 CSS 注意事项

  1. 不能使用 scoped
    • 因为 data-v-xxx 中的 xxx 每次运行都不同
    • 必须输出稳定不变的 class 选择器,方便使用者覆盖
  2. 类名必须加前缀
    • .button 不行
    • .wheel-button 可以

制作 Dialog 组件

  1. 需求分析:
    • 点击后弹出
    • 遮罩层
    • close 按钮
    • 标题
    • 内容
    • yes / no 按钮
  2. API
        <Dialog
            :visible="true"
            title="标题"
            @yes="fn1"
            @no="fn2"
        ></Dialog>
    
  3. 知识点: 具名插槽
    • 作用:用于一个组件有多个插槽
        <template v-slot:content>
            <h1>content 插槽</h1>
        </template>
        <template v-slot:title>
            <h1>title 插槽</h1>
        </template>
    <hr>
        <slot name="content"/>
        <slot name="title"/>
    
    
  4. 如果因为层叠上下文的原因,使当前组件被其他元素遮挡(其父元素的z-index太低),使用 teleport 将其移动到 body,或者 app 里面
  5. 一句话打开 dialog,技术点:动态更新组件,不用声明 visibel 变量

制作 Tabs 对话框组件

  1. 需求分析
    • 点击 Tab 切换内容
    • 切换时有一条横线在动的动画效果
  2. API
        <Tabs>
            <Tab title="导航一">内容一</Tab>
            <Tab title="导航一"><Component1></Tab>
            <Tab title="导航一"><Component1 x="hi"></Tab>
        </Tabs>
        // or
        <Tabs :data="[
            {title:"导航一",content:"内容一"},
            {title:"导航二",content: Component},
            {title:"导航三",content: h(Component1,{x:"hi"})}
        ]"/>
    

如何在运行时确认子组件的类型(检查 context.slots.default() 数组,检查用户写的是 TabsPart,而不是 div),这叫防御性编程

  • vue 组件的实质: <component :is="d[0]"></component>
  • 获得 VNode 虚拟节点: context.slots.default(),VNode 的 type 会和其子组件全等
  • 获取子组件(TabPart)外部传入的属性
        <TabsPart>
            <TabPart title="xxx" ></TabPart>
            <TabPart title="yyy" ></TabPart>
        </TabsPart>
    
        // TabsPart 组件内
        ...
        setup(props,context){
            const defaults = context.slots.default()
            const titles = default.map((tag)=>{tag.props.title}) // ['xxx','yyy']
            return {titles}
        }
    
<template>
  <div>
    <component :is="d[0]"></component>
    <component :is="d[1]"></component>
  </div>
</template>
<script lang="ts">
import TabPart from "./TabPart.vue";
export default {
  setup(props, context) {
    // console.log(context.slots.default());
    const d = context.slots.default();
    console.log(d[0].type === TabPart);

    d.map((tag) => {
      console.log(tag.type === TabPart);
      if (tag.type !== TabPart) {
        throw new Error("TabsPart 的子标签必须是 TabPart");
      }
    });
    return {
      d,
    };
  },
};
</script>
  1. 显示被选择的 title
  2. 制作会动的横线:动态控制 div 的宽度
    • 在相应的导航下面加一个 div,class='wheel-tabs-nav-item-indicator'
        .wheel-tabs-nav-item-indicator {
            position: absolute;
            height: 3px;
            background: #40a9ff;
            left: 0;
            bottom: -1px;
            width:100px
        }
    
    • 用 js 来计算 div 的宽度
      1. 文档中使用 ref 在 v-for 生成的列表中获取当前选择的 div 元素 :ref="el => {if(el) divs[i] = el}" el 是当前选中的元素,divs[i],divs 是可以在 js 中获取的包含所有元素的变量,i 是 v-for 的 index
      2. 声明 divs,命名为 navItems const navItems = ref<HTMLDivElememt[]>([]),因为获取的是数组,所以初始化为空数组
      3. 找到当前选中的元素,也就是 class 为 selected 的元素
          const divs = navItems.value // 获取所有
          const result = divs.find((div)=>{div.classList.contains('selected')})
      
      1. 获取选中元素的宽度 result.getBoundingClientRect().width
      2. 使用 ref 获取横线
          <div ref="indicator"></div>
      
          const indicator = ref<HTMLDivElememt>(null)
      
      1. 将获取的选中的元素的宽度赋值给 indicator indicator.value.style.width = result.getBoundingClientRect().width + 'px'
    • 用 js 来控制 div 的位置
      1. div 的位置就是当前选中的元素的 left 坐标减去第一个的 left 坐标(也就是整个容器的 left)
      2. 用 ref 获取容器 container
      3. 容器的 left:const {left:left1} = container.value.getBoundingClientRect()
      4. 第二个 left: const {left:left2} = result.getBoundingClientRect()
      5. 最终: const left = left2 - left1; indicator.value.style.left = left + 'px'
      6. 最终代码要在 onMounted(第一次执行一次) 和 onUpdated(后来每一次更新执行) 里都写一遍

主页装修

  • 取色
    1. https://dribbble.com/ 搜索 website 免费
    2. ThemeForest.net 付费
  1. 首页渐变背景
    • 首页最外层添加 topNavAndBanner,添加样式,使用 css gradient 添加渐变色
  2. 给文字和链接添加样式 *
  3. 使用 iconfont 添加 logo 和 icon
    • iconfont.cn 搜索关键词
    • 选中 icon 加入购物车
    • 添加至项目,没有项目就新建一个
    • 点击 Symbol,修改 icon 的名称
    • 点击生成代码
    • 使用 script 标签将生成的代码引入项目(index.html 的 head 里)
    • 点击使用帮助,找到 symbol引用 并照做
    • xlink:href="#icon-名称" 可指定用哪个 icon
    • 如果新增了 icon,需要重新点击生成代码,并更新 script 标签
  4. 使用 clip-path 画圆弧 clip-path:ellipse(80% 60% at 50% 40%)
  5. 完善细节

文档页装修

  1. 创建更多路由
  2. 高亮当前路由(active class)
  3. 引入 GitHub 的 Markdown 样式
    • 安装 github-markdown-css
    • main.ts 中引入import 'github-markdown-css'
    • 为容器添加 class='markdown-body'
  4. 支持直接引入 markdown
    • 在项目的根目录(src 外面),创建 plugin 目录中创建 md.ts 文件
        import { marked } from 'marked';
        // @ts-nocheck
        // import path from 'path'
        // import fs from 'fs'
        const path = require('path')
        const fs = require('fs')
    
        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)
                }]
            }
        }
    
    • md.ts 中引用了 marked,所以创建完 md.ts 后安装 marked yarn add --dev marked
    • 创建 vite.config.ts
        import { md } from './plugin/md';
    
        export default {
            plugins:[md()]
        }
    
    • 这时就可以使用 markdown 来写了,新建 md 文件,写完后引入
        <template>
            <article v-html="md" class="markdown-body">
            </article>
        </template>
    
        <script>
            import md from 'xxxx'
            export default {
                setup(){return {md}}
            }
        </script>
    
  5. 消除引入 markdown 文件出现的三处重复
    • 新建组件 Mrakdown.vue
    • 使用异步引入
        <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(){
                const content = ref<string>(null)
                import(props.path).then(result =>{
                    // 为什么要点 default?通过动态的 import 一个东西都有一个 default
                    content.value = result.default
                })
                return {content}
            }
        }
        </script>
    
    • 全局注册 Markdown 组件
    • 使用异步引入组件可以消除重复,但是在请求过程中会比直接引入要慢
  6. 进一步优化:在 router 文件内使用 h 函数实现异步加载组件
    • router 的 component 可以接受一个 h 函数(key 是为了使 vue 能检测到异步组件的变化,不加则只会在刷新的时候更新页面)
        import Markdown from 'xxx'
        ...
        // h 函数的第二个参数是传入 Markdown 组件的属性
        // path 的相对路径,是相对于 Markdown 来说的
        {
            path:'introduce',
            component: h(Markdown,{path:'xxx',key:'1'})
        }
    
  7. 终极优化:封装 md 函数
        const md = filename => {
            return h(Markdown,{path:`../../markdown/${filename}.md`,key:filename})
        }
    

switch 组件 如何展示源代码:

  1. 抽离需要展示的代码到新的组件 switchDemo1.vue,新组建内只有用户需要 copy 的代码
  2. 获取组件内的源代码,需要使用vue-loaderCustom Blocks功能
    • 在组件中加入 demo 标签(template 标签上面):<demo>常规用法</demo>
    • 配置 vite.config.ts
    export default {
        plugins: [md()],
        vueCustomBlockTransforms: {
        // 遇到demo标签就执行以下代码
        demo: (options) => {
            const { code, path } = options;
            const file = fs.readFileSync(path).toString();
            // 判断虚拟节点是否存在tag为deom的标签
            const parsed = baseParse(file).children.find((n) => n.tag === 'demo');
            const title = parsed.children[0].content;
            // 删除demo标签
            const main = file.split(parsed.loc.source).join('').trim();
            // 组件对象增加__sourceCode属性,值为源代码
            return `export default function (Component) {
        Component.__sourceCode = ${JSON.stringify(main)}
        Component.__sourceCodeTitle = ${JSON.stringify(title)}
      }`.trim();
        }
    }
    

}; ``` * 组件中加入自定义块 <demo>xxx</demo> * 使用组件名.__sourceCode 获取代码:switchDemo1.__sourceCode

使用 prismjs.com 和 v-html 实现五彩斑斓的源代码

  • 安装: yarn add prismjs
  • 引入: import Prism from 'prismjs'
    • 发现报错: SyntaxError: The requested module '/@modules/prismjs/prism.js' does not provide an export named 'default' 没有默认的导出。
    • 搜索发现,因为这个库太老了,不支持 import 引入方法
    • 我尝试直接引入: import 'prismjs',然后将其放在 window 上:console.log(window.Prism)
    • 此时 ts 报错:window 上面没有 Prism 这个属性
    • 搜索后选择使用一个临时方案,将 window 强制指定为 any:const Prism = (window as any).Prism
  • 使用: 在 node_modules/prisms/thmes 文件夹下都是 css 主题,可选中使用
    • 引入 css:import "prismjs/themes/prism-okaidia.min.css"
    • 导出 Prism
    • <pre class="language-html" v-html="Prism.highlight(SwitchDemo1.__sourceCode,Prism.languages.html,'html')"></pre>
  • 封装优化:用一个组件来展示所以组件的 Demo
    • 新建组件 Demo.vue
        <template>
    <div class="demo">
        <h2>{{component.__sourceCodeTitle}}</h2>
    <div class="demo-component">
      <component :is="component"></component>
    </div>
    <div class="demo-actions">
      <ButtonPart>查看代码</ButtonPart>
    </div>
    <div class="demo-code">
      <pre
        class="language-html"
        v-html="
          Prism.highlight(
            component.__sourceCode,
            Prism.languages.html,
            'html'
          )
        "
      ></pre>
    </div>
    </template>
    <script lang="ts">
        import "prismjs";
        import "prismjs/themes/prism-okaidia.min.css";
        import ButtonPart from '../lib/ButtonPart.vue'
        const Prism = (window as any).Prism;
        export default {
        components: {ButtonPart},
        props: {
            component: Object,
        },
        setup() {
            return { Prism };
        };
    </script>
    
    • 使用 <Demo :componet="SwitchDemo1"/>

构建项目后,使用 http-server 打开发现无法加载 md 文件

  • 原因: 因为 rollup 不支持 import() 时拼接字符串(因为我用了动态导入)
  • 解决: 要么想办法支持(太麻烦),要么换成静态引入

上传到 github

  • yarn build 时要注意设置 build path
  1. 如果有 dist 目录,则删除 dist 目录
  2. 在 .gitignore 添加一行 /dist/ 然后提交代码
  3. 运行 yarn build 创建最新的 dist
  4. 运行 hs dist -c-1 在本地测试
  5. 部署到 github
    • 创建 git 仓库
    • cd dist
    • git init; git add .; git commit -m 'init'
    • 注意现在 dist 是一个套娃的 git 仓库
    • 新建远程仓库 wheel-website,并关联到 dist 目录
    • 开启 wheel-ui 的 pages
  • 部署后报错 404(路径错误 build path)

    • yarn build 之后,dist 目录的 _assets 因为有下划线,触发了 github 的潜规则
    • 来到 vite.config.ts 里,加两行:base:'./',assetsDir: 'assets',
    • 重新部署
    • cd dist; git init; git add .; git commit -m 'first commit';
    • git branch -M master; git remote add origin git@github.com:AndyDufr/wheel-ui.git;
    • git push -f -u origin master(-f false为强制覆盖); cd ..
  • 一键部署: deploy.sh

打包 npm

  • 创建 lib/index.ts
  • 创建 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()]
    }, {
        name: 'Gulu',
        file: 'dist/lib/gulu.esm.js',
        format: 'es',
        plugins: [terser()]
    }],
    plugins: [
        scss({ include: /\.scss$/, sass: dartSass }),
        esbuild({
            include: /\.[jt]s$/,
            minify: process.env.NODE_ENV === 'production',
            target: 'es2015'
        }),
        vue({
            include: /\.vue$/,
        })
    ],
    external: ['vue'],
}
  • 安装 rollup yarn global add rollup
  • 运行 rollup -c
  • 过程中遇到的报错
    • Cannot read property 'reactivityTransform' of undefined
    • Error: Transform failed with 1 error:C:\Users\Administrator\Desktop\vue3-gulu\wheel-ui\src\lib\ButtonPart.vue?vue&type=template&id=6e7660c8&lang.js:2:2: error: Unexpected "<"
    • 以上报错解决办法: 将 package.json 里 --dev 的依赖前面的 '^' 去掉重新 yarn install(打完包记得加回来)
    • rollup.config.ts 配置文件报错:import dartSass from 'sass'; sass 没有默认的导出项
    • 以上报错解决方法: 改为:const dartSass = require('sass')

发布到 npm

  • 打开 package.json,添加 files 和 main
{
  "name": "wheel-ui",
  "version": "0.0.4",
  "files": [
    "dist/lib/*"
  ],
  "main": "dist/lib/gulu.js",
  "module": "dist/lib/gulu.esm.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  ...
}
  • 使用 npm publish 命令上传,确保使用官方源,而不是淘宝源
  • 注册 npm
  • 登录: npm login,按要求输入用户名、密码、email,可能会有一次性密码
  • 发布报错: npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/wheel-ui - You do not have permission to publish "wheel-ui". Are you logged in as the correct user?
  • 解决:重名了,package.json,name 改成 wheel-ui-amazing,发布成功
  • npm logout 退出

部署gitee

  • 删除远程源 git remote rm origin

使用自己的库

  • 测试时发现一个严重错误:使用 Button 组件报错:Uncaught TypeError: Cannot read properties of null (reading 'isCE')
  • 搜索发现是因为我打包时,将 Vue 的代码一起打包进去。而我测试时,使用了和打包时不一致的 Vue 版本
  • 解决:
    1. rollup.config.ts 中加入配置 external: ['vue'],上面笔记已经加上了
    2. package.json中将 vue 依赖项移动到 dev 中

以上报错后续

  • 为了解决用户使用 Tab 组件报错Cannot read properties of null (reading 'getBoundingRect')的问题,我将 rollup 的版本降低,问题解决,然后可以正常打包,结果引发了新的问题:升级版本的代码用户在使用时无法正常渲染 html,我试着回滚版本,使用其他方式解决Cannot read properties of null (reading 'getBoundingRect')报错;比如改用 offsetWitdh 来替代,然后还是报错,折腾到半夜发现当时降低版本重新打包时,忘记将 vue 放回 dev 里。这个问题浪费了我将近6个小时