前言 由于vue3。2版本的发布,复制代码codepre4。4封装SVG的图标组件 svg图标比较小,而且都是可读的xml文本,所以我们把它直接放在项目中即可,通过vitepluginsvgicons插件,实现自动引入svg图标。 配置vite。config。ts:plugins:〔viteSvgIcons({iconDirs:〔resolve(process。cwd(),srcassetsicons)〕,symbolId:icon〔dir〕〔name〕,}),〕复制代码 封装一个vue组件:templatesvgariahiddentrueuse:xlink:hrefsymbolId:fillcolorsvgtemplate复制代码 首先将下载的。svg图标放入assetsicons文件夹下svgiconnamecolor复制代码name放置在assetsicons文件夹下的文件名。color颜色填充,使用此项会默认覆盖图标颜色。5。按需自动引入组件 unpluginvuecomponents〔77〕是一款非常强大的插件(极力推荐),核心功能就是帮助你自动按需引入组件,Treeshakable,只注册你使用的组件。这里说一下他的两个核心使用方式和配置方式。 此插件不仅支持vue3,同时也支持vue2,并且支持Vite、Webpack、VueCLI、Rollup。5。1安装与配置 安装:npmiunpluginvuecomponentsD复制代码 配置:vite。config。tsimportComponentsfromunpluginvuecomponentsviteexportdefaultdefineConfig({plugins:〔Components({options}),〕,})复制代码 这里的options可以配置一些选项,后面提到的组件库注册会使用到。5。2改变全局组件注册方式 我们通常将全局的组件封装在srccomponents中,然后通过app。component()注册全局组件。使用此插件后,无需手写注册,直接在模板中使用组件即可: 这里引入官方的示例:templateHelloWorldmsgHelloVue3。0Vitetemplate复制代码 自动编译为:templateHelloWorldmsgHelloVue3。0Vitetemplate复制代码5。3自动引入组件库 在使用组件库时,常规组件我们也会注册到全局,如果使用局部注册由于页面中会使用到多个组件,会非常麻烦,所以这个功能绝佳,例如我们使用antdesignvue组件库。 直接在模板中使用即可,无需手动注册或局部引用:template按钮abuttontemplate复制代码 当然,你还需要在vite中引入它的解析器:importComponentsfromunpluginvuecomponentsviteimport{AntDesignVueResolver}fromunpluginvuecomponentsresolversexportdefaultdefineConfig({plugins:〔Components({resolvers:〔AntDesignVueResolver(),〕})〕,})复制代码 目前支持的解析器,根据你的喜好去选择:AntDesignVue〔78〕ElementPlus〔79〕ElementUI〔80〕HeadlessUI〔81〕IDux〔82〕NaiveUI〔83〕PrimeVue〔84〕Vant〔85〕VEUI〔86〕VarletUI〔87〕ViewUI〔88〕Vuetify〔89〕VueUseComponents〔90〕Quasar〔91〕6。样式 项目中最好使用通用样式,可以创建srcstyles目录存放,这里推荐一些分类:stylesantd组件库样式覆盖,命名自取,这里以antdesign为例color。less颜色index。less入口global。less公共类transition。less动画相关variable。less变量复制代码6。1预设基础样式 相信用过normalize〔92〕的同学不在少数,它可以重置css样式,使各浏览器效果保持一致。后面的章节会提到tailwind。css,它内置了预设样式重置的功能,与normalize还是有一定的区别,有兴趣的同学可以了解一下〔93〕。6。2CSS预处理器 虽然vite原生支持lesssassscssstylus,但是你必须手动安装他们的预处理器依赖,例如:npminstallDless复制代码 如何选择预处理器? 推荐使用你是所使用的组件库的样式语言,因为css预处理器学会一种后,入手其他几乎没有学习成本。6。3开启scoped 没有加scoped属性,会编译成全局样式,造成全局污染。stylescopedstyle复制代码6。4深度选择器 有时我们可能想明确地制定一个针对子组件的规则。 如果你希望scoped样式中的一个选择器能够作用得更深,例如影响子组件,你可以使用操作符。有些像Sass之类的预处理器无法正确解析。这种情况下你可以使用deep或::vdeep操作符取而代之两者都是的别名,同样可以正常工作。7。布局 页面整体布局是一个产品最外层的框架结构,往往会包含导航、页脚、侧边栏等。在页面之中,也有很多区块的布局结构。在真实项目中,页面布局通常统领整个应用的界面,有非常重要的作用,所以单独拆分出来也是非常有必要的。 在脚手架中,所有的通用布局组件都应该放在srclayouts中,这种封装比较简单,这里就不贴代码了,大家按照自己实际情况自行发挥,在此仅提供一下封装思路。7。1常规的布局 BasicLayout 基础页面布局,包含了头部导航,侧边栏等。 BlankLayout 空白的布局。7。2特殊的布局 RouteLayout 如果你的项目在路由切换中需要对某些二级页面进行缓存,那么推荐你创建一个RouteLayout,通过路由meta中的配置,返回routerview或者使用keepalive包裹的routerview。 UserLayout 用于用户登录注册等页面抽离出来。 PageLayout 基础布局,包含了面包屑等信息,内含slot。8。集成Tailwind。css Tailwind。css〔94〕在我第一次看到它的时候,内心是比较反感的,但实际上手之后又觉得真香。从vue2项目中,我已经引入了tailwind,整体的开发结果就是,基本很少再使用标签去转本定义一些class和样式,毕竟起名字这种事,一个是涉及到规范,一个是涉及到英语。如果你选择tailwind,CSS预处理器的作用就会显得微乎其微,因为你无需再自定定义各种变量和mixins。 总体来说,学习成本并不高,花上两个小时足够上手,记住不用死记硬背那些类名。8。1效率提升 很多人总是说样式要与HTML分离,现在为什么又要提倡tailwind这种与HTML紧密结合的工具?这是因为现在使用vue这类框架已经高度组件化,样式分离是为了方便复用和维护,但在组件化面前样式分离只能是降低开发效率。 下面介绍一下tailwind提供了哪些提升效率的功能:提供了大量的功能类,极大的提高了可维护性。响应式设计,各种设备一把梭。悬停、焦点和其它状态。深色模式。支持配置,例如颜色方面很难做到跟你的设计师统一。不用为起名字而纠结???8。2JIT模式 如果你的环境支持postcss8(vuecli构建的vue2项目是postcss7),那么JIT模式直接带你起飞。超快的构建速度。支持变体,你甚至可以这么写sm:hover:active:disabled:opacity75。支持任意样式,例如md:top〔113px〕。开发和生产环境结果是一致的,(我在vue2项目中就遇到过组件库构建结果不一致的问题)。 如果你使用vscode那你一定要安装TailwindCSSIntelliSense〔95〕插件,它可以自动补全类名,显著降低学习成本。8。3关于打包体积 使用默认配置,未压缩是3739。4kB,Gzip压缩是293。9kB,Brotli压缩是73。2kB。这似乎看起来很大,这是因为tailwind提供了成千上万的功能类,其中绝大部分你不会使用到。 当构建生产时,你应该使用purge选项来treeshake优化未使用的样式,并优化您的最终构建大小当使用Tailwind删除未使用的样式时,很难最终得到超过10kb的压缩CSS。 还有一点,AtomCSS极大的提升了样式的复用程度,从而直接降低了构建体积。9。vuex替代方案pinia 由于vuex4对typescript的支持让人感到难过,所以状态管理弃用了vuex而采取了pinia〔96〕。 忘记在哪看到,尤大好像说pinia〔97〕可能会代替vuex,所以请放心使用。9。1为什么采用Pinia?Pinia的API设计非常接近Vuex5的提案〔98〕。(作者是Vue核心团队成员)无需像Vuex4自定义复杂的类型来支持typescript,天生具备完美的类型推断。模块化设计,你引入的每一个store在打包时都可以自动拆分他们。无嵌套结构,但你可以在任意的store之间交叉组合使用。Pinia与Vuedevtools挂钩,不会影响Vue3开发体验。 下面简单的介绍一下如何使用Pinia,并对比vuex有哪些区别与注意事项,具体请参考官方文档〔99〕。9。2创建Store Pinia已经内置在脚手架中,并且与vue已经做好了关联,你可以在任何位置创建一个store:import{defineStore}frompiniaexportconstuseUserStoredefineStore({id:user,state:()({}),getters:{},actions:{}})复制代码 这与Vuex有很大不同,它是标准的Javascript模块导出,这种方式也让开发人员和你的IDE更加清楚store来自哪里。 Pinia与Vuex的区别:id是必要的,它将所使用store连接到devtools。创建方式:newVuex。Store(。。。)(vuex3),createStore(。。。)(vuex4)。对比于vuex3,state现在是一个函数返回对象。没有mutations,不用担心,state的变化依然记录在devtools中。9。3State 创建好store之后,可以在state中创建一些属性了:state:()({name:codexu,age:18})复制代码 将store中的state属性设置为一个函数,该函数返回一个包含不同状态值的对象,这与我们在组件中定义数据的方式非常相似。 在模板中使用store: 现在我们想从store中获取到name的状态,我们只需要使用以下的方式即可:h1{{userStore。name}}h1constuserStoreuseUserStore()return{userStore}复制代码 注意这里并不需要userStore。state。name。 虽然上面的写法很舒适,但是你一定不要用解构的方式去提取它内部的值,这样做的话,会失去它的响应式:const{name,email}useUserStore()复制代码9。4Getters Pinia中的getter与Vuex中的getter、组件中的计算属性具有相同的功能,传统的函数声明使用this代替了state的传参方法,但箭头函数还是要使用函数的第一个参数来获取state,因为箭头函数处理this的作用范围:getters:{nameLength(){returnthis。name。length},nameLength:statestate。name。length,nameLength:()this。name。length}复制代码9。5Actions 这里与Vuex有极大的不同,Pinia仅提供了一种方法来定义如何更改状态的规则,放弃mutations只依靠Actions,这是一项重大的改变。 Pinia让Actions更加的灵活:可以通过组件或其他action调用可以从其他store的action中调用直接在商店实例上调用支持同步或异步有任意数量的参数可以包含有关如何更改状态的逻辑(也就是vuex的mutations的作用)可以patch方法直接更改状态属性actions:{asyncinsertPost(data){awaitdoAjaxRequest(data);this。name。。。;}}复制代码9。6Devtools 脚手架已内置下面的代码,这将添加devtools支持:import{createPinia,PiniaPlugin}frompiniaVue。use(PiniaPlugin)constpiniacreatePinia()复制代码 时间旅行功能貌似已经可以使用了,这块后续会关注。10。基于mitt处理组件间事件联动 如果你曾经是Vue2。x的开发者,那么请阅读下面引用官方文档〔100〕的一段话:我们从实例中完全移除了on、off和once方法。emit仍然包含于现有的API中,因为它用于触发由父组件声明式添加的事件处理函数。 在Vue3中,已经不可能使用这些API从组件内部监听组件自己发出的事件了,该用例暂没有迁移的方法。但是该eventHub模式可以被替换为实现了事件触发器接口的外部库,例如mitt或tinyemitter。10。1为什么选择mitt?足够小,仅有200bytes。支持全部事件的监听和批量移除。无依赖,不论是什么框架都可以直接使用。10。2严重警告 我们已经无法在项目中使用eventBus,仅推荐你在特殊场合下使用mitt,它并不是开发的常态,你一定要确保知道自己在做什么?否则你的项目将难以维护!!!10。3如何使用mitt? 在使用mitt前建议请阅读官方文档〔101〕: 脚手架默认提供一个可以直接使用的对象:复制代码 当然你也可以引入已经安装好的mitt:importmittfrommittconstemittermitt()复制代码 mitt提供了非常简单的API,下面代码是官方演示:listentoaneventemitter。on(foo,econsole。log(foo,e))listentoalleventsemitter。on(,(type,e)console。log(type,e))fireaneventemitter。emit(foo,{a:b})clearingalleventsemitter。all。clear()workingwithhandlerreferences:functiononFoo(){}emitter。on(foo,onFoo)listenemitter。off(foo,onFoo)unlisten复制代码11。异步请求 绝大多数项目想必逃脱不了接口的对接,如果你的项目存在大量的接口,我建议做到以下几点:封装请求。统一的API接口管理。Mock数据功能(根据需求斟酌使用)。 上述的主要目的就是在帮助我们简化代码和利于后期的更新维护。11。1基于axios的封装 相信开发过vue2项目的同学已经对axios非常熟悉的,在这里提供一些封装的思路:通过import。meta。env。VITEAPPBASEURL获取环境变量,配置baseURL,如果接口存在多个不同域名,可以通过js变量控制。设置timeout请求超时、断网情况处理。设置请求头,携带token。异常拦截处理,后端通过你携带的token判断你是否过期,如果返回401你可能需要跳转到登录页面,并提示需要重新登录。响应拦截,通常后端返回code、data、msg,如果是请求正常,我们可以直接返回data数据,如果是异常的code,我们也可以在这里直接弹出报错提示。无感刷新token,如果你的token过期,可以通过后端返回的refreshToken调用刷新接口,获取新的token。当然这里涉及到很多细节,例如终端请求、重新发送请求、重新请求列队。中断请求,例如页面切换时,我们要中断正在发生的请求。 相关代码(仅供参考)〔102〕11。2为axios增加泛型的支持 到目前为止,axios请求返回的类型是any,这时我们对请求后的数据进行操作时,没有享受到ts带来的类型提示,这显然不符合我们的预期。 这时我们要做的就是重新声明axios模块:新建一个shims。d。ts,然后在调用时加上泛型。import{AxiosRequestConfig}declaremoduleaxios{exportinterfaceAxiosInstance{Tany(config:AxiosRequestConfig):PromiseT;requestTany(config:AxiosRequestConfig):PromiseT;getTany(url:string,config?:AxiosRequestConfig):PromiseT;deleteTany(url:string,config?:AxiosRequestConfig):PromiseT;headTany(url:string,config?:AxiosRequestConfig):PromiseT;postTany(url:string,data?:any,config?:AxiosRequestConfig):PromiseT;putTany(url:string,data?:any,config?:AxiosRequestConfig):PromiseT;patchTany(url:string,data?:any,config?:AxiosRequestConfig):PromiseT;}}复制代码 做好这一步后,你就必须在创建接口时,声明请求相应数据的类型。11。3封装更方便的useRequest 设想一下,编写请求代码时,我们通常会定义这么几个变量:data:储存请求数据loading:请求加载状态 尤其是loading,我们需要在请求前设置为true,请求结束后设置为false。 上面的封装方式,是对基础的功能封装,因为我们在使用vue3,所以可以进行再一次的封装成为hook,我们使用起来会更加方便。 例如下面这个样子: 使用useRequest定义一个接口:exportdefaultgetUserInfo(id){returnuseRequest({method:get,url:apiuser,params:{id}})}复制代码 使用此接口:const{data,loading}getUserInfo();复制代码 注意这里的data是响应式的。 这是我想到的一种思路,目前还没有做很好的封装,相关代码仅供参考〔103〕,你也可以借鉴一些成熟方案,比如vueuse中的useFetch〔104〕,但是他是基于FetchAPI设计的,并不符合我的预期要求,有更好的方案请大家在下面留言。11。4统一的API接口管理 自从前端和后端分家之后,前后端接口对接就成为了常态,而对接接口的过程就离不开接口文档,比较主流就是Swagger,但是如何在前端项目中更好的去管理跟后端对接的接口呢? 在src目录中创建api目录,内部目录应按照后端制定的模块创建。 每个模块中创建多个ts文件,一个接口应对应一个ts文件,其中包含了以下内容:请求参数的类型声明。响应数据的类型声明。返回定义好的请求函数(url、method、params、data等)。 统一去定义和管理API接口,只要后端规范的命名和你认真的写好类型声明,对前端来说typescript就是最好的接口文档。11。5mock vite使用mock数据非常简单,你可以使用vitepluginmock〔105〕插件,如果你了解mockjs,你可以快速上手。12。路由 路由和菜单是组织起一个应用的关键骨架。12。1创建路由三部曲 通常一个项目需要做到这几步:使用createRouter创建路由,这时候根据需求选择Hash路由或者History路由。根据业务需求配置路由,注意这里很可能就用到前文提到过的布局组件。如果有权限相关的业务,你需要创建permission。ts在路由钩子触发时做一些事情。 如果你的页面比较多,建议你创建routes目录,分模块声明路由。 参考代码〔106〕12。2使用meta丰富你的路由 vuerouter4。x支持typescript,配置路由的类型是RouteRecordRaw,这里meta可以让我们有更多的发挥空间,这里提供一些参考:title:页面标题,通常必选。icon?:图标,一般配合菜单使用。auth?:是否需要登录权限。ignoreAuth?:是否忽略权限。roles?:RoleEnum〔〕;可以访问的角色keepAlive?:是否开启页面缓存hideMenu?:有些路由我们并不想在菜单中显示,比如某些编辑页面。order?:菜单排序。frameUrl?:嵌套外链。 这里只提供一些思路,每个项目多多少少会涉及到这些问题,具体如何实现请查阅资料自行解决。13。项目性能与细节优化13。1开启gzip 开启gzip可以极大的压缩静态资源,对页面加载的速度起到了显著的作用。 使用viteplugincompression〔107〕可以gzip或brotli的方式来压缩资源,这一步需要服务器端的配合,vite只能帮你打包出。gz文件。此插件使用简单,你甚至无需配置参数,引入即可。13。2页面载入进度条 页面路由切换时,附带一个加载进度条会显得非常友好,不至于白屏时间过长,让用户以为页面假死。 这时候我们可以用到nprogress〔108〕,在路由切换时开启和关闭:importNProuter。beforeEach(async(to,from,next){NProgress。start();});router。afterEach((to){NProgress。done();});复制代码13。3Title 在不同的路由下显示不同的标题是常规的操作,我们可以通过路由钩子获取meta中的title属性改变标签页上的title。 你可以使用vueuse提供的useTitle〔109〕,或者window。document。title自行封装。 你也可以通过环境变量将你的主标题拼接在路由标题的后面:const{VITEAPPTITLE}import。meta。复制代码13。4解决移动端使用vh的问题 有兴趣的同学可以尝试一下chrome移动端浏览器上的100vh,是真正的视口高度的100嘛。 为了解决这一问题,我们可以通过postCss插件解决。 安装postcssviewportheightcorrection〔110〕插件:npminstallDpostcssviewportheightcorrection复制代码 在postcss。config。js中增加plugin:module。exports{plugins:{postcssviewportheightcorrection:{},},}复制代码 添加这一段js代码在全局,你可以直接添加在index。html上即可:constcustomViewportCorrectionVfunctionsetViewportProperty(doc){letprevClientHconstcustomVar{customViewportCorrectionVariablevh};functionhandleResize(){const{clientHeight}if(clientHeightprevClientHeight)requestAnimationFrame(functionupdateViewportHeight(){doc。style。setProperty(customVar,{clientHeight0。01}px);prevClientHeightclientH});}handleResize();returnhandleR}window。addEventListener(resize,setViewportProperty(document。documentElement));复制代码13。5可以常驻的JavaScript库前文提到过的vueuse〔111〕,非常强大,强烈建议尝试。lodash〔112〕,用了都说好,早用早下班。14。代码风格与流程规范14。1ESLint 不管是多人合作还是个人项目,代码规范都是很重要的。这样做不仅可以很大程度地避免基本语法错误,也保证了代码的可读性。 这里推荐使用airbnb规范。 配置参考〔113〕14。2StyleLint 尽管前文提到过tailwind,可以让你几乎不写css,但是涉及到团队协作,这一点也要严谨。 StyleLint是一个强大的、现代化的CSS检测工具,与ESLint类似,是通过定义一系列的编码风格规则帮助我们避免在样式表中出现错误,配合编辑器的自动修复,可以很好的统一团队项目css风格。 配置参考〔114〕14。3代码提交规范 在多人协作的背景下,git仓库和workflow的作用很重要。而对于commit提交的信息说明存在一定规范,现使用commitlinthusky规范gitcommitm中的描述信息。我们都知道,在使用gitcommit时,git会提示我们填入此次提交的信息。可不要小看了这些commit,团队中规范了commit可以更清晰的查看每一次代码提交记录,还可以根据自定义的规则,自动生成changeLog文件。 提交格式(注意冒号后面有空格):type〔optionalscope〕:description复制代码type:用于表明我们这次提交的改动类型。optionalscope:可选,用于标识此次提交主要涉及到代码中哪个模块。description:一句话描述此次提交的主要内容,做到言简意赅。 Type类型build:编译相关的修改,例如发布版本、对项目构建或者依赖的改动chore:其他修改,比如改变构建流程、或者增加依赖库、工具等ci:持续集成修改docs:文档修改feat:新特性、新功能fix:修改bugperf:优化相关,比如提升性能、体验refactor:代码重构revert:回滚到上一个版本style:代码格式修改,注意不是css修改test:测试用例修改 关于commitlinthusky的配置文章有很多,大同小异,请根据自己的实际情况配置。15。编写使用文档 做到这一步,你的整个脚手架开发已经接近于尾声,但是你做了这么多,你的同事并不知道如何使用,甚至你过一段时间也会忘记,所以你必须养成良好的编写文档习惯。15。1使用vitepress搭建文档 这里我推荐使用vuepress或者vitepress,说实话你只写文档vitepress会让你更舒服,因为它很快。 vitepress〔115〕很适合构建博客网站、技术文档,就是因为它可以直接用markdown进行书写,所有写过博客的人,都应该对它不陌生。一个。md文件,即可生成一张页面,十分方便。 创建一个vitepress文档实在是太过于简单,你可以参考官方文档,或者参考我的文档〔116〕。15。2文档部署 如果你的团队可以帮助你搭建CICD自动部署是再好不过了,如果没有这个条件,你也可以通过github提供的actions功能,完成自动部署。 代码参考〔117〕16。插件 如果你想更痛快的用上述功能,建议你安装下面的插件。16。1VSCode插件VueLanguageFeatures(Volar)〔118〕,你现在查Volar可能找不到,你需要的是这个。Vue3Snippets〔119〕,vue3快捷输入。TailwindCSSIntelliSense〔120〕,tailwind代码提示。Stylelint〔121〕PrettierCodeformatter〔122〕ESLint〔123〕16。2Chrome插件Vue。jsdevtools〔124〕,你当然要安装支持vue3的版本,而且此版本对pinia支持的也非常友好。