yarn global add create-vite-app
cva wheel-ui
// 等价于
npm init vite-app <



cd wheel-ui
yarn install
yarn dev

// 查看 vue-router 所有版本号
npm info nue-router versions

// 安装 vue-router
yarn add vue-router


yarn add -D sass

vite 文档给出的命令是

npm init vite-app 项目名

yarn create vite-app 项目名

等价于

yarn global creat-vite-app

cva 项目名

也等价于

npx create-vite-app 项目名

即 npx 会帮你全局安装用到的包

初始化 vue-router

新建 history 对象

新建 router 对象

app.use(router)

const history = createWebHashHistory();
const router = createRouter({
  history: history,
  routes: [{ path: "/", component: HelloWorld }],
});

const app = createApp(App);
app.use(router);
app.mount("#app");

添加 <router-view> <router-link>

<template>
  <div>
    导航栏 |
    <router-link to="/"> frank </router-link> |
    <router-link to="/xxx"> frank2</router-link>
  </div>
  <hr />
  <router-view />
</template>

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

provide inject

需求:点击子组件 A ,隐藏子组件 B 某一内容。

App.vue

<script lang="ts">
import { provide, ref } from "vue";
export default {
  name: "App",
  setup() {
    const asideVisible = ref(false); // set
    provide("xxx", asideVisible);
  },
};
</script>

子组件 A

<!-- Topnav.vue -->
<template>
  <div class="topnav">
    <div class="logo" @click="toggleAside">LOGO</div>
  </div>
</template>

<script lang="ts">
import Vue, { inject, Ref } from "vue";
export default {
  setup() {
    const asideVisible = inject<Ref<boolean>>("xxx");
    const toggleAside = () => {
      asideVisible.value = !asideVisible.value;
    };
    return { toggleAside };
  },
};
</script>

子组件 B

<!-- Doc.vue -->
<template>
  <div>
    <Topnav />
    <div class="content">
      <aside v-if="asideVisible">
        ...
      </aside>
    </div>
  </div>
</template>

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

TS 引入 .vue文件

找不到模块“./App.vue”或其相应的类型声明。ts(2307)

src 目录下创建以 .d.ts 为结尾的文件

// shims-vue.d.ts
declare module '*.vue' {
    import { ComponentOptions } from "vue";
    const componentOptions:ComponentOptions 
    export default componentOptions
}

error Command failed with exit code 1.

<center><img src="https://cdn.jsdelivr.net/gh/Drwna/image//images/Snipaste_2022-02-08_15-42-14.png" alt="Snipaste_2022-02-08_15-42-14" width="800px"></center>

<center><img src="https://cdn.jsdelivr.net/gh/Drwna/image//images/Snipaste_2022-02-08_15-42-14.png" alt="Snipaste_2022-02-08_15-42-14" width="800px"></center>

解决:

rm -rf node_modules/
yarn cache clean
yarn install

父子组件通信

父组件

<template>
  <Switch :value="y" @input="y = $event" />
</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 }">
    <span></span>
  </button>
</template>

<script lang="ts">
import { ref } from "vue";
export default {
  props: {
    value: Boolean,
  },
  setup(props, context) {
    const toggle = () => {
        // 核心
      context.emit("input", !props.value);
    };
    return { toggle };
  },
};
</script>

或者使用 v-model

父组件

<Switch v-model:value="y" />

子组件

context.emit("update:value", !props.value);

属性绑定

默认所有属性都绑定到根元素。

使用 inheritAttrs: false 可以取消默认绑定。

使用 $attrs 或者 context.attrs 获取所有属性。

使用 v-bind=“$attrs” 批量绑定属性。

使用 cons {size, ...rest} = context.attrs 将属性放分开。

父组件

// ButtonDemo.vue
<template>
  <div>Button 示例</div>
  <h1>示例1</h1>
  <div>
    <Button @click="onClick" 
            @mouseover="onClick" 
            size="small">你好</Button>
  </div>
</template>

<script lang="ts">
import Button from "../lib/Button.vue";
export default {
  components: { Button },
  setup() {
    const onClick = () => {
      alert("hello");
    };
    return { onClick };
  },
};
</script>

子组件

// Button.vue
<template>
  // 将 size 属性绑定到根元素
  <div :size="size">
      // 将其余的属性绑定到目标元素
    <button v-bind="rest">
      <slot />
    </button>
  </div>
</template>

<script lang="ts">
export default {
    // 取消默认绑定到根元素
  inheritAttrs: false,
  setup(props, context) {
    const { size, ...rest } = context.attrs;
    return { size, rest };
  },
};
</script>

开发 UI 库的 CSS 注意事项

不能使用 scoped

因为 data-v-xxx 中的 xxx 不能保证每次运行都相同,必须输出稳定不变的 class 选择器,方便使用者覆盖。

必须加前缀

.button 不行,很容易被使用者覆盖,.wheel-button 行,不太容易被覆盖。

.theme-link 不行,很容易被使用者覆盖,.wheel-theme-link 行,不太容易被覆盖。

插槽

具名插槽

<template>     
  <div class="wheel-dialog">
    <header>
        
      <slot name="title"/>
      <span @click="close"></span>
    </header>
    <main>
      <slot name="content"/>
    </main>
      
</template>
<template>
  <div>
      
    <template v-slot:content>
      <div>你好</div>
      <div>hello</div>
    </template>

    <template v-slot:title>
      <strong>xxx</strong>
    </template>

  </div>
</template>

Teleport

将 xxx 传送到 body 下

<Teleport to="body">
    xxx
</Teleport>

openDialog

openDialog.js

import Dialog from './Dialog.vue';
import {createApp, h} from 'vue';

export const openDialog = (options) => {
  const {title, content, closeOnClickOverlay, ok, cancel} = options;
  const div = document.createElement('div');
  document.body.appendChild(div);
  const close = () => {
    //@ts-ignore
    app.unmount(div);
    div.remove();
  };
  const app = createApp({
    render() {
      return h(Dialog, {
        visible: true,
        closeOnClickOverlay,
        'onUpdate:visible': (newVisible) => {if (newVisible === false) close(); },
        ok,
        cancel
      }, {title, content});
    }
  });
  app.mount(div);
};

vue 组件使用

const showDialog = () => {
      openDialog({
        title: '标题',
        content: '你好啊',
        closeOnClickOverlay: false,
        ok() {console.log('ok');},
        cancel() {console.log('cancel');}
      });
    };

检查子组件的类型

获取插槽内容 const defaults = context.slots.default()

<template>
  <div>
    Tabs 组件
  </div>
  <component :is="defaults[0]"/>
  <component :is="defaults[1]"/>
</template>

<script lang="ts">
import Tab from '../lib/Tab.vue';

export default {
  setup(props, context) {
    // 调试 检查子组件是否为 Tab 组件
    // console.log({...context.slots.default()[0]});
    // console.log({...context.slots.default()[1]});
    // const defaults = context.slots.default();
    // console.log(defaults[0].type === Tab);
    const defaults = context.slots.default();
    defaults.forEach(tag => {
      if (tag.type !== Tab) {
        throw new Error('Tabs 子标签必须是 Tab');
      }
    });
    return {defaults};
  }
};
</script>

获取 el 的 width、left const {width, height, left, top} = el.getBoundingClientRect()

CSS 最小影响原则

css 绝对不能影响库的使用者。

markdown

yarn add --dev marked

新建 plugins/md.ts

// @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)
    }]
  };
}

新建 vite.config.ts

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

export default {
  plugins: [md()]
};

使用

<template>
  <article class="markdown-body" v-html="md"></article>
</template>
<script lang="ts">
// intro.md 为写好的 markdown文件
import md from '../markdown/intro.md';
    
export default {
  data() { return {md}; }
};
</script>

消除重复 markdown

使用动态引入 import() ==异步操作==

Markdown.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(props) {
     // 核心
    const content = ref<string>();
    import(props.path).then(result => {
      console.log(result.default);
        // 动态引用 3s 后才会出现
      setTimeout(() => {
        content.value = result.default;
      }, 3000);
    });
    return {content};
  }
};
</script>

使用

Intro.vue

<template>
  <Markdown path="../markdown/intro.md"/>
</template>

<script lang="ts">
import Markdown from '../components/Markdown.vue';

export default {components: {Markdown},};
</script>

或者直接全局引入 Markdown 组件,就不需要每个组件单独引入了。

main.ts app.component('Markdown', Markdown)

进一步优化:

直接在 router.js 里渲染,删除无用的 intro.vue

const history = createWebHashHistory();
// 核心
const md = filename => h(Markdown, {path: `../markdown/${filename}.md`, key: filename});

export const router = createRouter({
  history: history,
  routes: [
    {path: '/', component: Home},
    {
      path: '/doc',
      component: Doc,
      children: [
        {path: '', component: DocDemo},
          // 核心
        {path: 'intro', component: md('intro')},
        {path: 'install', component: md('install')},
        {path: 'get-started', component: md('get-started')},
          //
        {path: 'switch', component: SwitchDemo},
          ...
      ],
    },
  ],
});

展示源代码

配置 vite.config.ts

import {md} from './plugins/md';
import * as 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();
      //@ts-ignore
      const parsed = baseParse(file).children.find(n => n.tag === 'demo');
      //@ts-ignore
      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

<template>
  <div>
    <div class="demo-code">
        <!-- 用法 -->
      <pre>{{ Switch1Demo.__sourceCode }}</pre>
    </div>
  </div>
</template>

<script lang="ts">
export default {
  components: {Switch2Demo, Switch1Demo,Switch,Button},
  setup() {
    const bool = ref(false);
    return {bool, Switch1Demo, Switch2Demo};
  }
};
</script>

Switch1Demo.vue 组件开头添加上 <demo>xxx</demo>

高亮源代码

使用 prismjs

yarn add prismjs


导入
import Prism from 'prismjs';
import '../../node_modules/prismjs/themes/prism.min.css';

...
set(){
return (Prism)
}
...

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

Vue2 和 Vue3 的区别

  • Vue3 的 template 支持==多个跟标签==,Vue2 不支持。
  • Vue3 有 createApp() ,而Vue2 的是 new Vue()
  • Vue3 createApp(组件),Vue2 new Vue({template, render})

v-model 代替以前的 v-model.sync

新增 context.emit 和 this.$emit 作用相同。

部署上线注意事项

配置 vite.config.ts

添加以下代码,

base: './',
assetsDir: 'assets',

报错

runtime-core.esm-bundler.js?5c40:38 [Vue warn]: Invalid VNode type: Symbol(Comment) (symbol)

解决:

vue.config.js

const path = require(`path`);

module.exports = {
    configureWebpack: {
        resolve: {
            symlinks: false,
            alias: {
                vue: path.resolve(`./node_modules/vue`)
            }
        }
    }
};

打包部署

rollup -c

{
  "name": "whl-ui",
  "version": "0.0.2",
  "files": [
    "dist/lib/*"
  ],
  "main": "dist/lib/whl.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "resolutions": {
    "node-sass": "npm:sass@^1.26.11"
  },
  "dependencies": {
    "github-markdown-css": "4.0.0",
    "marked": "4.0.12",
    "prismjs": "1.21.0",
    "vue": "3.0.0",
    "vue-router": "4.0.0-beta.3"
  },
  "devDependencies": {
    "@types/prismjs": "^1.26.0",
    "@vue/compiler-sfc": "3.0.0",
    "rollup-plugin-esbuild": "2.5.0",
    "rollup-plugin-scss": "2.6.0",
    "rollup-plugin-terser": "7.0.2",
    "rollup-plugin-vue": "6.0.0-beta.10",
    "sass": "1.26.11",
    "vite": "1.0.0-rc.1"
  }
}

yarn build

{
  "name": "whl-ui",
  "version": "0.0.2",
  "files": [
    "dist/lib/*"
  ],
  "main": "dist/lib/whl.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "resolutions": {
    "node-sass": "npm:sass@^1.26.11"
  },
  "dependencies": {
    "github-markdown-css": "4.0.0",
    "marked": "4.0.12",
    "prismjs": "1.21.0",
    "vue": "^3.0.0",
    "vue-router": "4.0.0-beta.3"
  },
  "devDependencies": {
    "@types/prismjs": "^1.26.0",
    "@vue/compiler-sfc": "^3.0.0",
    "rollup-plugin-esbuild": "2.5.0",
    "rollup-plugin-scss": "2.6.0",
    "rollup-plugin-terser": "7.0.2",
    "rollup-plugin-vue": "6.0.0-beta.10",
    "sass": "1.26.11",
    "vite": "1.0.0-rc.13"
  }
}

Dragan
7 声望0 粉丝