.先展现示例
TabPage
实际应用示例
尾部贴完整代码,若只需要实现功能,直接复制代码即可。
1.首先分析 页面需求,需要添加 标签 ,点击标签传递参数。委托思路。
第一步:分析标签结构
标签页面:展示图标。文本信息。关闭按钮。
public class TabHeader : FlowLayoutPanel
受限声明一个控件,继承 FlowLayoutPanel 继承此控件主要是关键代码系统控件已经都具有,不用自己造轮子,按照真正开发思路,还是可以继承 control控件自定义分割,重新封装。根据个人喜好。
标签页 制作:
public class TabHeaderItem : WenControl
声明一个标签页 继承WenControl : 此控件是个人封装的基础控件,若要了解可以查看往期文章。或者开源搜索 WenSkin。可以找到相关介绍。
private TabHeader owner;
public TabHeaderItem(TabHeader owner, string path)
{
this.owner=owner;
this.Path=path;
this.Width=120;
this.Height=30;
WenButton button=new WenButton()
{
Size=new Size(16, 16),
Image=Properties.Resources.close,
ImageSize=new Size(14, 14),
TextImageRelation=TextImageRelation.Overlay,
Location=new Point(this.Width - 16 - 4, 7),
};
button.Click +=Button_Click;
this.Controls.Add(button);
this.Paint +=FileItemControl_Paint;
this.MouseLeave +=TabHeaderItem_MouseLeave;
this.MouseEnter +=TabHeaderItem_MouseEnter;
}
声明构造函数,部分细节后续代码注意讲解。
此处最主要是 尺寸信息 当然可以直接 size=new size();个人习惯。
声明一个关闭按钮。可以采用gdi画,也可以直接添加一个图片按钮。
private void Button_Click(object sender, EventArgs e)
{
owner.Controls.Remove(this);
this.Dispose();
}
文中就直接调用往期封装好的按钮控件。若需要了解可以跳转往期文章查看。讲解了关于按钮 封装。
this.Paint +=FileItemControl_Paint;
画标签 的文字和 内容,可以直接重写 也可以使用委托画,个人喜好。
private void FileItemControl_Paint(object sender, PaintEventArgs e)
{
var recImage=new Rectangle(4, 4, 22, 22);
var recStr=new Rectangle(28, 4, this.Width - 28 - 4 - 22 - 2, 22);
Graphics g=e.Graphics.SetGDIHigh();
g.DrawImage(Properties.Resources.file, recImage);
g.DrawString(FileText, this.Font, Brushes.White, recStr, ControlHelper.StringConters);
}
this.MouseLeave +=TabHeaderItem_MouseLeave;
this.MouseEnter +=TabHeaderItem_MouseEnter;
鼠标事件处理
public void ReBackColor()
{
if (owner.selectedItem==this)
{
this.BackColor=Color.FromArgb(63, 63, 70);
}
else
{
this.BackColor=Color.Transparent;
}
}
private void TabHeaderItem_MouseEnter(object sender, EventArgs e)
{
this.BackColor=Color.FromArgb(62, 62, 64);
}
private void TabHeaderItem_MouseLeave(object sender, EventArgs e)
{
ReBackColor();
}
主要是鼠标移动到指定位置改变颜色,离开后颜色会变化。直接修改背景颜色即可。
可以设置一个选中颜色
需要在主体中明一个选中选线。便于后续比较改变背景颜色。
#region 私有属性
private TabHeaderItem selectedItem;
#endregion
若需要暴露在 全部 可以将私有属性改为公有。然后声明变化。
接下来就是 添加标签代码。
文中有一个需求就是最新 添加标签在最前端,并选中。
public void Add(string path)
{
TabHeaderItem f=new TabHeaderItem(this, path);
foreach (TabHeaderItem item in this.Controls)
{
if (path==item.Path)
{
this.Controls.SetChildIndex(item, 0);
re(item);
return;
}
}
f.Click +=(s, e)=>
{
re(f);
};
this.Controls.Add(f);
re(f);
this.Controls.SetChildIndex(f, 0);
void re(TabHeaderItem item)
{
ItemChanged?.Invoke(this, new TabEventArgs(path));
var ci=selectedItem;
selectedItem=item;
ci?.ReBackColor();
item.ReBackColor();
}
}
若有其他需求可直接更改即可。
#region 委托
public delegate void TabEventHandler(object sender, TabEventArgs e);
public class TabEventArgs : EventArgs
{
public TabEventArgs(string path)
{
Path=path;
}
public string Path { get; set; }
}
public event TabEventHandler ItemChanged;
#endregion
至此,完整解决一个tab标签。
完整代码块
麻不烧的Github
配合着源码,用心看完这篇文章,你便领悟了封装的精髓,麻雀虽小,五脏俱全。
前记
业务代码之外的代码,我想称之为增值代码。
什么意思?
作为一个程序员,你应该除了完成领导安排的任务,你还应该有一些自己的时间,用来“玩”一些比较有意思的事情。
当现有框架、库满足不了我们需求的时候,我们应该尝试去自己造一些工具。也正是这些你所实现的,成就了他人,造就了自己。
不信,你且想一想,他人会关心你写的具体的业务逻辑代码吗?我想他们更关心的是,你写的插件,是如何使用的吧,以及方不方便他们借此完成他们自己业务代码。
再通俗一点,他们不会记住你,但是他们会记住你的Api,因而忆起你。
还有很重要的一点,所有的技术,都是服务于业务的,否则,就是扯皮。
背景
入职新公司以来,一直忙于开发业务,过程中,多处用到了领导写的牛逼工具。说实话,内心由衷的佩服,简直就是解放生产力,放到古代,就是要被封神滴。
举个例子:
领导花了一段时间,研究出了一个自动表单生成器。之前手写一个表单配置页,加上表单验证,可能需要半天,甚至更久。
现在呢?所有的表单、样式及验证,都可以通过代码配置实现,二十分钟可能就完成了。
由此,我悟出了一个道理:
重复地做一件事,不如用心地做“一件事”。
我想,你肯定也想成为他人口中的那个男人,但整天活在自己的世界里,你可能一时并不知道该如何去做,这里我想告诉你:
成长的一个关键性因素,就是来自于模仿。
对的,你可以先尝试着去阅读下他人的代码,看看别人的实现方式,再者可以去github上溜一圈,优秀项目太多了,仿着写去呗。
我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。
只要你想学,你就一定能学会,只不过是实现的方式好与坏而已,这些是需要后期不断完善的。
鉴于本篇文章快要跑题了,不再多述,进入正题...
正文
1.组件和插件的区别与联系
区别
联系
这里不做过多阐述,有兴趣可以参考下劳卜大大的这篇文章,写的很通俗易懂。
2.实现插件的必备因素
基础
你需要清楚的知道vue的一些高阶知识点以及相关内容,比如
技巧
以下这个技巧是今天开发的时候悟出来的,目测很有用:
别着急开发,先想着如何在开发中使用你的插件
什么意思?顺着我的思路捋下去
因为我想实现一个全局toast插件,大概用法
this.$toast('那个男人') // todo
光弹出文案不行,应该有一个控制弹出方向的变量
this.$toast('那个男人',{ position:'topCenter' })
全局toast的状态应该有多种,比如常见的成功、错误、警告、普通...
// 成功success // 错误error // 警告warning // 普通info this.$toast('那个男人',{ position:'topCenter', type:'success' })
应该有一个时间变量去控制多长时间自动消失toast
this.$toast('那个男人',{ position:'topCenter', type:'success', closeTime: 3 // 控制3秒后消失toast })
会不会存在一种业务场景,我们不需要自动消失toast
this.$toast('那个男人',{ position:'topCenter', type:'success', closeTime: 3 // 控制3秒后消失toast autoClose: false })
如果我想在toast结束后,触发一些回调动作,比如删除成功toast后刷新列表页面
this.$toast('那个男人',{ position:'topCenter', type:'success', closeTime: 3 // 控制3秒后消失toast autoClose: true, callback () { ... } })
toast的内容,可能会很长,因此应该有两个变量分别控制toast宽度和高度
this.$toast('那个男人',{ position:'topCenter', type:'success', closeTime: 3 // 控制3秒后消失toast autoClose: true, callback () { ... }, width:300, height:80 })
至此,基础功能应该都涵盖了,这个时候你要去考虑一些内建的问题
配置项多应该怎么解决-默认值
默认给个type呗,比如我的项目中默认的type是info,当我在使用的时候,没有传入type时,默认为info
因为大部分的toast场景都是短暂的停留在页面,所以autoClose设置为true
又因为大部分的toast文案比较短,所以我的默认toast长宽设置为300、80应该足够了
...
以上默认配置,都可以在使用的时候,传入参数覆盖默认参数
针对不同的状态,toast图标、颜色、标题之间有什么联系?
本地存一个map映射配置表,根据传入的type,我就可以准确的知道图标、颜色、标题应该是什么
总结几点:
实现
上文提到过,组件可以暴露数据给插件,对于这句话
我的理解是,组件是静态的,只是对外暴露一些参数入口props。插件,让我们可以动态的往其中注入一些自定义参数。具体的实现,还是在组件当中完成。
于是乎:我写了一个静态组件,通过props定义上文提到的相关变量
先看下script部分
再来看下html部分
可以看到,内部实现其实很简单,无非就是通过外部传入的props,控制内部的展示细节而已
到这里静态组件基本已经完成了(css样式代码不在这里贴了)
注意:
静态组件怎么变成插件使用呢?
这里不再做过多阐述,vue封装插件的常用方法主要有以下四种,有疑惑的话,建议观阅vue开发插件,当然我觉得你应该还需要去了解下Vue.extend的用法,插件的实现离不开它哦。
看下关键部分:该文件也是我们后期webpack打包(build)的入口文件
该文件内容涉及到的知识点,也是开发一个vue插件最核心的内容。里面的每一行代码,都充满了杀机~
至此,关于插件实现部分基本已经全部完成。
3.如何将自己的插件上传到npm上去
这里的话,网上的教程有很多,我理解你只需要了解以下几行代码的作用,就足够了
// webpack.config.js module.exports={ entry: process.env.NODE_ENV==='development' ? './src/main.js' : './src/index.js', output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: 'build.js', libraryTarget: 'umd' }, ... // package.json { "name": "mbs-toast", "description": "a toast plugin base on Vue2", "version": "1.0.0", "author": "xxx", "license": "MIT", "private": false, "main": "dist/build.js", "scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", ...
这里我用的模版是自己在官方webpack-simple模版的基础上做了一些定制化的,里面为了方便我平时开发,加入了scss、eslint,这样的话,后面就不用每次手动install了,有兴趣的可以看下README,定制一份属于自己的脚手架模板
在你了解了上述背景后,你只需要执行以下几步即可实现皆大欢喜
顺利的话,现在你已经可以在正式项目中,通过
npm install -S xxx 安装你的私有包了
最后在你的入口文件注册你的插件
import toastPlugin from 'xxx'Vue.use(toastPlugin) // 这里Vue.use的第二个参数,可以通过全局配置,做一些自定义配置,有需要的自行前往学习
到这里,所有的一切,已尘埃落定
你可以在代码中愉快的使用了
this.$toast('尘埃落定', { callback () { console.log('hello world') }, type: 'success', // position: 'topRight', autoClose: false})
最后
我在写这个插件之前,在Github上看到一个大神封装的插件。四个字描述下,叹为观止,有兴趣的一定要去看下,我相信爱学习的你,一定会收获满满。同时在开发该插件时,一些样式及动画,也做了相应的参考。
该插件的源码已经上传mbs-toast,方便大家参考。同时,上述提到的form表单生成器,我也尝试着自己实现了一遍,有兴趣的可以一起加入哦。所有的插件以及组件目前都汇总在麻不烧的Github里,文档和README正在不断完善中~
码字不易,且行且珍惜!
源自:https://juejin.im/post/5dc42069f265da4d3962a8e4
声明:文章著作权归作者所有,如有侵权,请联系小编删除。
小希这次带来了进阶版的Vue3 + Vite项目框架的封装搭建
本文主要的切入点有
一般情况下,项目开发只有一个入口,只需要配置一个入口,一个项目
但有时多个同业务、同类型的项目,有很多可以复用的业务,组件,工具类等,就可以放在同一个代码库里进行维护,不用新建多个代码库
每个项目都有自己独立的入口,可以独立打包并进行部署,低耦合,不会相互影响,同时还可以复用相同的组件,业务等,可以大大地提高开发效率和后期的维护
如上图所示,有两个项目,分别是app1,app2,每个项目都有自己独有的main.ts入口文件,App.vue文件,以及路由,仓库pinia,组件等,同时也有共有的组件,utils工具类等
在package.json中,将下图单入口的配置
改为
{
"scripts": {
"dev:app1": "vite serve src/app1/ --config ./vite.config.ts",
"dev:app2": "vite serve src/app2/ --config ./vite.config.ts",
"build:app1": "vue-tsc && vite build",
"build:app2": "vue-tsc && vite build"
},
这样配置可实现项目的独立运行,独立打包
在vite.config.ts中配置:
/* 项目名称 */
//采用这种方式可以动态获取项目名称,当然,如果项目少可以手动配置
let appName=process.env.npm_lifecycle_event
appName=appName.slice(appName.indexOf(':') + 1) //app1、app2
export default defineConfig({
root: `./src/${appName}/`,
build: {
rollupOptions: {
input: {
[appName]: path.resolve(__dirname, `src/${appName}/index.html`)
},
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]',
}
}
}
})
打包构建
pnpm build:app1
pnpm build:app2
基于多入口打包,也就是一个代码库同时维护多个同类型的项目情况下,可以通过配置实现自动化生成项目基础模板,这样,当需要在代码库新建一个新项目时,可以通过命令行快速创建
这个插件用来询问用户输入项目名称,这是一个比较在处理命令行交互比较常见的库
主要用于实现命令行交互式界面。帮助我们与用户进行交互式交流
它有几个特点:提供错误反馈,询问问题,解析输入,验证答案
详细可参考 命令行交互工具inquirer
安装
pnpm add inquirer@^8.0.0 -S
在package.json里添加
"scripts": {
"init-app": "node ./src/utils/initApp/index.ts"
}
当执行这个命令时,会自动去执行,在本地utils文件夹下的initApp文件里的js脚本,在src目录下会自动生成一个新的文件夹(项目)
在utils下新增initApp文件夹以及index.ts和temlate
在index.ts添加以下代码
#!/usr/bin/env node
console.log('您正在创建项目')
const path=require('path')
const fs=require('fs')
const inquirer=require('inquirer')
const stat=fs.stat
const targetDir=path.resolve(__dirname, './template')
//复制文件目录
const copyFile=(targetDir, resultDir)=> {
// 读取文件、目录
fs.readdir(targetDir, function (err, paths) {
if (err) {
throw err
}
paths.forEach(function (p) {
const target=path.join(targetDir, '/', p)
const res=path.join(resultDir, '/', p)
let read
let write
stat(target, function (err, statsDta) {
if (err) {
throw err
}
if (statsDta.isFile()) {
read=fs.createReadStream(target)
write=fs.createWriteStream(res)
read.pipe(write)
} else if (statsDta.isDirectory()) {
fs.mkdir(res, function () {
copyFile(target, res)
})
}
})
})
})
}
const question=[
{
type: 'input',
name: 'name',
message: '请输入项目名称:'
}
]
const createProject=()=> {
// 询问用户问题
inquirer
.prompt(question)
.then(({ name })=> {
// name 为输入的项目名称
name=name.trim()
if (!name) {
console.log('项目目录不能为空')
// 如果输入空,继续询问
createProject()
return false
}
// 目标路径,要放在module目录下
const resultDir=path.resolve(__dirname, '../../', name)
// fs.access()方法用于测试文件是否存在
fs.access(resultDir, function (err, data) {
if (err) {
// 创建文件
fs.mkdir(resultDir, function (err, data) {
if (err) {
throw err
}
// 复制模版文件
copyFile(targetDir, resultDir)
})
console.log(`${name} 项目已创建成功`)
} else {
console.log(`${name} 项目目录已存在,请输入其他名称`)
// 不存在,继续询问
createProject()
}
})
})
.catch((err)=> {
console.log(err)
})
}
createProject()
注:此代码copy==> Vue3项目框架搭建封装,一次学习,终身受益【万字长文,满满干货】
在temlate文件夹下新增项目所需要的文件目录,main.ts以及App.vue是必须的,因为它是独立的项目
app3项目自动生成
数据存储在缓存(内存)中,优点读写更快,可以保存任意的js类型数据和对象,比如当我们刷新浏览器的时候,数据会丢失,所以需要实现pinia持久化
安装
pnpm add pinia-plugin-persist
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
const pinia=createPinia()
pinia.use(piniaPersist)
createApp({})
.use(pinia)
.mount('#app')
// store/use-user-store.ts
import { defineStore } from 'pinia'
export const useUserStore=defineStore('storeUser', {
state: ()=> {
return {
firstName: 'S',
lastName: 'L',
accessToken: 'xxxxxxxxxxxxx'
}
},
actions: {
setToken (value: string) {
this.accessToken=value
}
},
persist: {
enabled: true,
//这里可以单独给每个字段配置存储的形式sessionStorage/localStorage
//paths配置state里的字段,不同的数据采取不同的存储方式
strategies: [
{ storage: sessionStorage, paths: ['firstName', 'lastName'] },
{ storage: localStorage, paths: ['accessToken'] },
],
}
})
strategies 字段说明:
属性 | 描述 |
key | 自定义存储的 key,默认是 store.$id |
storage | 可以指定localStorage/sessionStorage,或者自定义存储类型,默认为 sessionStorage |
paths | state 中的字段名,按组打包储存 |
也可以自定义存储类型,更多具体配置戳pinia-plugin-persist插件官网地址
源码解析
核心是通过 store.$subscribe去监听仓库数据,当仓库数据发生变化时会触发回调,更改本地缓存数据,当刷新后就会从本地缓存取出相关的数据
import { PiniaPluginContext } from 'pinia'
type Store=PiniaPluginContext['store']; //pinia插件上下文
type PartialState=Partial<Store['$state']>;
//调用函数将仓库数据存储到本地
export const updateStorage=(strategy: PersistStrategy, store: Store)=> {
const storage=strategy.storage || sessionStorage //可以自定义存储类型,默认为sessionStorage
const storeKey=strategy.key || store.$id //可以自定义存储的 key,默认是 store.$id
//判断是否有配置paths,如果没有就缓存一整个仓库中的state
if (strategy.paths) {
//遍历paths里面的字段,并通过 store.$state[key]获取相应的数据
const partialState=strategy.paths.reduce((finalObj, key)=> {
finalObj[key]=store.$state[key]
return finalObj
}, {} as PartialState)
//存储到本地
storage.setItem(storeKey, JSON.stringify(partialState))
} else {
storage.setItem(storeKey, JSON.stringify(store.$state))
}
}
export default ({ options, store }: PiniaPluginContext): void=> {
//判断enabled是否为true
if (options.persist?.enabled) {
const defaultStrat: PersistStrategy[]=[{
key: store.$id,
storage: sessionStorage,
}]
const strategies=options.persist?.strategies?.length ? options.persist?.strategies : defaultStrat
strategies.forEach((strategy)=> {
const storage=strategy.storage || sessionStorage
const storeKey=strategy.key || store.$id
//根据key判断是否在本地缓存中,如果在刷新后会从本地缓存中将数据赋给pinia仓库的state
const storageResult=storage.getItem(storeKey)
// 如果本地中存在同步数据,更新仓库state数据
//(比如浏览器刷新后会进行判断,如果有数据会赋值给pinia仓库的state,实现pinia持久化)
if (storageResult) {
store.$patch(JSON.parse(storageResult))
updateStorage(strategy, store)
}
})
//通过$subscribe监听state,仓库数据更改会触发回调同步更改本地数据
store.$subscribe(()=> {
strategies.forEach((strategy)=> {
updateStorage(strategy, store)
})
})
}
}
通过显示进度条的形式,来提高用户体验,可用在进入/离开路由时触发动画,也可在发接口时使用
安装
pnpm add nprogress -S
只需调用start()和done()即可控制进度条。
NProgress.start();
NProgress.done();
切换路由
router.beforeEach((to, from, next)=> {
NProgress.start()
next()
})
router.afterEach(()=> {
NProgress.done()
})
发请求时
// axios请求拦截器
axios.interceptors.request.use(
config=> {
NProgress.start() // 设置加载进度条(开始..)
return config
},
error=> {
return Promise.reject(error)
}
)
// axios响应拦截器
axios.interceptors.response.use(
function(response) {
NProgress.done() // 设置加载进度条(结束..)
return response
},
function(error) {
return Promise.reject(error)
}
)
其它详细配置请戳官网:ricostacruz.com/nprogress/
PostCSS 是一种 JavaScript 工具,可将你的 CSS 代码转换为抽象语法树 (AST),然后提供 API(应用程序编程接口)用于使用 JavaScript 插件对其进行分析和修改。
Autoprefixer主要功能是解析CSS并使用Can I Use中的值向CSS规则添加供应商前缀。以兼容各种浏览器,部分CSS属性需要加上不同的前缀以兼容不同的浏览器。通过配置Autoprefixer,自动为CSS属性添加对应浏览器的前缀。
postcss-px-to-viewport 用于将单位为 px 的尺寸转换为视口单位(vw, vh, vmin, vmax)
下面用到Autoprefixer和postcss-px-to-viewport这两个插件进行viewport适配
安装
pnpm add postcss-px-to-viewport -D
pnpm add autoprefixer -D
创建postcss.config.js并配置
// postcss.config.js
module.exports=()=> {
return {
plugins: {
autoprefixer: {},
'postcss-px-to-viewport': {
unitToConvert: 'px', // 需要转换的单位,默认为"px"
viewportWidth: 1920, // 设计稿的视口宽度
unitPrecision: 5, // 单位转换后保留的精度
propList: ['*'], // 能转化为vw的属性列表
viewportUnit: 'vw', // 希望使用的视口单位
fontViewportUnit: 'vw', // 字体使用的视口单位
selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
mediaQuery: false, // 媒体查询里的单位是否需要转换单位
replace: true, // 是否直接更换属性值,而不添加备用属性
exclude: undefined, // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
include: undefined, // 如果设置了include,那将只有匹配到的文件才会被转换
landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
landscapeUnit: 'vw', // 横屏时使用的单位
landscapeWidth: 1920 // 横屏时使用的视口宽度
}
}
}
}
效果如下
不同视口宽度,界面会响应性变化
作者:小希学前端
链接:https://juejin.cn/post/7327965216826032154
*请认真填写需求信息,我们会在24小时内与您取得联系。