搭建环境
- 安装 vite
- 全局安装 vite-creat-app:
yarn global add create-vite-app@1.18.0
- 安装结束后,有两个全局命令可以使用:
create-vite-app
或者简写:cva
- 创建项目
- 使用
cva 项目名
或者create-vite-app 项目名
- 执行 vite 提供的三个命令,安装依赖,启动项目
- 将 main.js 改为 ts 文件,不要忘记改 index.html 中的引用
引入使用 Vue Router 4
- 使用命令行查看 vue-router 的所有版本号:
npm info vue-router version
- 安装最新版本
yarn add vue-router@4.0.14
- 初始化 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" } }
- src 目录创建 d.ts 文件:比如
封装 Topnav
初始化 home、doc 页面
使用 provide 和 inject 实现点击切换 aside
// TODO: 当前为手机页面时,点击任何地方都实现隐藏 aside,并添加隐藏动画(使用 vant)
配置 doc 页面 aside 的子路由
使用后置路由守卫来实现点击 aside 后显示内容并关闭 aside
- 将 router 相关的代码封装至新的文件:
route.ts
- 文件中导出 router 以用来其他文件导入并使用路由守卫
- 在 app 文件中使用 router.afterEach 实现跳转路由后自动关闭 aside // TODO: 当前判断路由关闭的逻辑范围太广,只要路由跳转就会触发,目前思路为判断所有 aside 路由,待优化
制作 switch 组件
- 效果设计:抄 vant 的样式
- API: 组件接受一个 value,当 value 的值为 true 时,显示为开,false 为关;disable 为 true 时禁用
- 新建 lib 文件夹以存放所有组件,components 下引用 lib 中的组件
- 笔记:一个元素绑定 class:
:class={checked}
,意思为如果 checked 为 true,那么就有 checked 类,如果为 false,那么久没有 checked 类,以上是简写,全写为::class={checked:checked}
- 实现点击切换 switch
- 添加切换动画
- 优化组件
- 问题一:无法控制 switch 更新后的状态
- 问题二:外界无法获取当前状态
- 解决: switch 的 value(当前状态的值) 是外界传入的
- vue3 的 v-model 写法:v-model:value="value",相当于 vue2 的 snyc 修饰符(vue3 的 v-model 也改了)
制作 button 组件
- 设计需求:
- 可以有不同的等级(level)
- 可以是链接,可以是文字
- 可以 click、focus、mouseover
- 可以改变 size: 大、中、小
- 可以禁用 (disabled)
- 可以是加载中
- 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>
- 注意的问题:
- 外界传入的事件、属性,默认会绑定在组件的最外层元素上。
- 组件内使用
inheritAttrs:false
,关闭继承所有事件和属性 - 在组件内需要继承的元素上使用
v-band="$attrs"
继承外部属性和事件(以对象形式传入)
- context.attrs 可以获取所有属性和事件
- 分开绑定使用剩余操作符
- 组件内使用
- 外界传入的事件、属性,默认会绑定在组件的最外层元素上。
- props 和 attrs 区别
- props需要先声明才能获取值,而attrs则不用
- props声明过的属性,attrs里面不会在出现
- props不包含事件,attrs包含
- props支持string以外的类型,而attrs只有string类型
UI 库的 CSS 注意事项
- 不能使用 scoped
- 因为 data-v-xxx 中的 xxx 每次运行都不同
- 必须输出稳定不变的 class 选择器,方便使用者覆盖
- 类名必须加前缀
- .button 不行
- .wheel-button 可以
制作 Dialog 组件
- 需求分析:
- 点击后弹出
- 遮罩层
- close 按钮
- 标题
- 内容
- yes / no 按钮
- API
<Dialog :visible="true" title="标题" @yes="fn1" @no="fn2" ></Dialog>
- 知识点: 具名插槽
- 作用:用于一个组件有多个插槽
<template v-slot:content> <h1>content 插槽</h1> </template> <template v-slot:title> <h1>title 插槽</h1> </template> <hr> <slot name="content"/> <slot name="title"/>
- 如果因为层叠上下文的原因,使当前组件被其他元素遮挡(其父元素的z-index太低),使用
teleport
将其移动到 body,或者 app 里面 - 一句话打开 dialog,技术点:动态更新组件,不用声明 visibel 变量
制作 Tabs 对话框组件
- 需求分析
- 点击 Tab 切换内容
- 切换时有一条横线在动的动画效果
- 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>
- 显示被选择的 title
- 制作会动的横线:动态控制 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 的宽度
- 文档中使用 ref 在 v-for 生成的列表中获取当前选择的 div 元素
:ref="el => {if(el) divs[i] = el}"
el 是当前选中的元素,divs[i],divs 是可以在 js 中获取的包含所有元素的变量,i 是 v-for 的 index - 声明 divs,命名为 navItems
const navItems = ref<HTMLDivElememt[]>([])
,因为获取的是数组,所以初始化为空数组 - 找到当前选中的元素,也就是 class 为 selected 的元素
const divs = navItems.value // 获取所有 const result = divs.find((div)=>{div.classList.contains('selected')})
- 获取选中元素的宽度
result.getBoundingClientRect().width
- 使用 ref 获取横线
<div ref="indicator"></div>
const indicator = ref<HTMLDivElememt>(null)
- 将获取的选中的元素的宽度赋值给 indicator
indicator.value.style.width = result.getBoundingClientRect().width + 'px'
- 文档中使用 ref 在 v-for 生成的列表中获取当前选择的 div 元素
- 用 js 来控制 div 的位置
- div 的位置就是当前选中的元素的 left 坐标减去第一个的 left 坐标(也就是整个容器的 left)
- 用 ref 获取容器 container
- 容器的 left:
const {left:left1} = container.value.getBoundingClientRect()
- 第二个 left:
const {left:left2} = result.getBoundingClientRect()
- 最终:
const left = left2 - left1; indicator.value.style.left = left + 'px'
- 最终代码要在 onMounted(第一次执行一次) 和 onUpdated(后来每一次更新执行) 里都写一遍
- 在相应的导航下面加一个 div,
主页装修
- 取色
https://dribbble.com/
搜索 website 免费ThemeForest.net
付费
- 首页渐变背景
- 首页最外层添加 topNavAndBanner,添加样式,使用 css gradient 添加渐变色
- 给文字和链接添加样式 *
- 使用 iconfont 添加 logo 和 icon
- iconfont.cn 搜索关键词
- 选中 icon 加入购物车
- 添加至项目,没有项目就新建一个
- 点击 Symbol,修改 icon 的名称
- 点击生成代码
- 使用 script 标签将生成的代码引入项目(index.html 的 head 里)
- 点击使用帮助,找到
symbol引用
并照做 xlink:href="#icon-名称"
可指定用哪个 icon- 如果新增了 icon,需要重新点击生成代码,并更新 script 标签
- 使用 clip-path 画圆弧
clip-path:ellipse(80% 60% at 50% 40%)
- 完善细节
文档页装修
- 创建更多路由
- 高亮当前路由(active class)
- 引入 GitHub 的 Markdown 样式
- 安装 github-markdown-css
- main.ts 中引入
import 'github-markdown-css'
- 为容器添加
class='markdown-body'
- 支持直接引入 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>
- 消除引入 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 组件
- 使用异步引入组件可以消除重复,但是在请求过程中会比直接引入要慢
- 进一步优化:在 router 文件内使用 h 函数实现异步加载组件
- router 的 component 可以接受一个 h 函数(key 是为了使 vue 能检测到异步组件的变化,不加则只会在刷新的时候更新页面)
import Markdown from 'xxx' ... // h 函数的第二个参数是传入 Markdown 组件的属性 // path 的相对路径,是相对于 Markdown 来说的 { path:'introduce', component: h(Markdown,{path:'xxx',key:'1'}) }
- 终极优化:封装 md 函数
const md = filename => { return h(Markdown,{path:`../../markdown/${filename}.md`,key:filename}) }
switch 组件 如何展示源代码:
- 抽离需要展示的代码到新的组件
switchDemo1.vue
,新组建内只有用户需要 copy 的代码 - 获取组件内的源代码,需要使用
vue-loader
的Custom 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 标签(template 标签上面):
};
```
* 组件中加入自定义块 <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>
- 引入 css:
- 封装优化:用一个组件来展示所以组件的 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
- 如果有 dist 目录,则删除 dist 目录
- 在 .gitignore 添加一行 /dist/ 然后提交代码
- 运行 yarn build 创建最新的 dist
- 运行 hs dist -c-1 在本地测试
- 部署到 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 版本
- 解决:
rollup.config.ts
中加入配置external: ['vue'],
上面笔记已经加上了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个小时