Vite 项目配置模块分包

使用 Vite 开发项目,build 后发现所有的逻辑文件都被打包在一个入口 js 文件中,如图: 这样的打包结果显示不是合理的,当项目大了之后,js 入口文件会变得非常大,影响加载速度。还有就是对于项目中长期不变的 js 引入文件,我们不应该使其每次打包都重新生成 hash 名,这样会导致每次重新 build 后,客户端因为文件 hash 值的变化,都要重新下载这些固定文件内容,在网速不好的情况会影响使用体验。所以针对这点,需要做 build 优化,拆分模块文件。 但是 Vite 文档中没有找到这种功能的配置,其实 Vite 没有实现这个功能,它的打包是依赖 rollup 的,所以我们可以查找 rollup 的相关功能。Vite 打包是支持配置 rollup 功能以改变打包结果的,在vite.config.js 中配置 build.rollupOptions即可。 在查看 rollup 文档后得知,拆分包功能需要配置 output.manualChunks。manualChunks 是一个对象,对象的每一个属性名都是要生成的 chunk 名前缀,而属性值则是需要打入的模块包名。例如我们需要将 vue 文件单独拆分出来,则可以配置: { // 忽略其他配置 build: { rollupOptions: { output: { manualChunks: { vue: ['vue'] } } } } } 此时再执行打包,结果如下: 可以看到 vue 文件是被单独拆分出来了,且后续再修改项目代码时,这个文件指纹是不会再变的,因为开发中不可能修改 vue 源码。同样的道理,对于引用的第三方库,我们都应该拆分出来,可以放在一个 chunk 中,也可以单独拆分,如下: { // 忽略其他配置 build: { rollupOptions: { output: { manualChunks: { // 放在一个 chunk 中 chunk: ['vue','vue-router','vuex'], // 也可以单独细分 vue:['vue'], vuex:['vuex'], 'vue-router':['vue-router'], } } } } } 至此,我们解决了基本需求。还有一个问题就是如果项目中需要拆分的模块很多,我们一个个去列举模块也不太现实,这时候就可以换个配置模式。manualChunks 是支持配置成一个函数的,函数的入参是每一个被打包的模块id,我们可以判断这个 id 是否是来自node_modules 路径的,如果是则代表是第三方库,就可以统一打包进一个chunk中,配置如下: { // 忽略其他配置 build: { rollupOptions: { output: { manualChunks(id) { if (id.includes("node_modules")) { return "vendor"; } }, }, }, } } 更多关于 manualChunks 的配置,可参考文档:output-manualchunks
 · 4 min read 

自动路由和自动导入功能配置

在写自己的一个 Vite+Vue 项目时,考虑加入自动路由和自动导入功能。分别使用插件:unplugin-vue-router 和 unplugin-auto-import。 unplugin-vue-router 实现自动路由 自动路由要达到的效果是:无需手动添加路由路径,而是根据文件路径自动生成路由配置。 这里以 Vite 项目为例,安装好插件后,在 vite.config.ts 中配置如下: import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // 引入 vite 适配插件 import VueRouter from 'unplugin-vue-router/vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [VueRouter(), vue()] // VueRouter 插件必须放在 Vue 插件之前 }) 如果是 Typescript 项目,运行项目后,会在项目中自动生成一个 typed-router.d.ts文件,这个文件在后续开发中会自动生更新路由映射类型,为防止 ts 报错,所以需要将这个文件引入到 tsconfig.json 文件中,同时需要将 moduleResolution 项设置为 bundler : { "include": [ // other files... + "./typed-router.d.ts" ], "compilerOptions": { // ... + "moduleResolution": "bundler", // ... } } 另外,如果项目中有 env.d.ts文件,例如 Vite + Typescript 创建的项目就会有 vite-env.d.ts文件,则需要添加类型到其中: /// <reference types="vite/client" /> + /// <reference types="unplugin-vue-router/client" /> 配置完成后,需要修改 router.ts 文件,不再直接使用 vue-router 模块: - import { createRouter, createWebHistory } from 'vue-router' + import {createRouter,createWebHistory} from 'vue-router/auto' + import {routes} from 'vue-router/auto-routes' const router = createRouter({ history:createWebHistory(), - routes: [ - { - path: '/', - component: () => import('src/pages/Home.vue'), - } - ] + routes, }) export default router 上面代码中的 routes 变成自动生成的,它是基于文件系统的,默认路由文件映射目录是 src/pages,测试创建 pages/index.vue文件,可以看到 typed-router.d.ts文件中,已自动生成路由的映射: 排除目录 有些时候,pages目录中可能会有一些其他的非页面内容的目录,比如 components目录,我们不希望它生成路由映射。unplugin-vue-router 支持排除目录设置,在 exclude数组中,列出你不想要生成路由的文件即可: import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import VueRouter from "unplugin-vue-router/vite"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ VueRouter({ + routesFolder: "src/pages", // 这里可指定页面目录 + exclude: ["src/pages/components/**/*.vue"], // 这里排除了pages目录中components目录文件的路由映射 }), vue(), ], }); 至此,自动路由配置完成,更多配置功能,详见文档:configuration。 unplugin-auto-import 实现自动导入 自动导入要达到的效果是:使用过一些公共库或者是频繁导入使用的模块时,无需手动添加导入就可直接使用。 还是 Vite 项目为例,安装插件后,在 vite.config.ts 中配置: // vite.config.ts import AutoImport from 'unplugin-auto-import/vite' export default defineConfig({ plugins: [ AutoImport({ /* options */ }) ] }) 想要自动导出模块,需要在插件中具体配置。不是所有的插件或库都支持自动导入,首先官方默认提供了一些预设 presets,在这个预设表中的插件,都可以直接配置成字符串即可。比如我希望自动导入 vue 和 vue-router中的方法,即可配置成: // vite.config.ts import AutoImport from 'unplugin-auto-import/vite' export default defineConfig({ plugins: [ AutoImport({ imports: ['vue', 'vue-router'] }) ] }) 此时如果是 Typescript 项目,运行项目后,会自动生成 auto-imports.d.ts文件,文件中会根据配置自动生成类型声明,防止 ts 报错,同样我们需要将此文件包含到 tsconfig.json 文件中: { "include": [ // other files... + "./auto-imports.d.ts" ], } 观察 auto-imports.d.ts文件,会发现已经有vue和 vue-router的内置模块的类型声明了: 现在使用 vue 模块开发时,就不需要再手动导入了: <template> <div>{{ str }}</div> </template> <script setup lang="ts"> - import { ref } from "vue"; const str = ref("hello world!"); </script> 结合 unplugin-vue-router 使用 如果使用的插件库不在官方预设内,有些插件会自己实现,例如 unplugin-vue-router。 在使用 unplugin-vue-router 时,我们不直接使用 vue-router,希望是自动导入插件中的方法,而此插件刚好也暴露了自己实现的自动导入预设,所以可修改配置如下: port { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import VueRouter from "unplugin-vue-router/vite"; import AutoImport from "unplugin-auto-import/vite"; + import { VueRouterAutoImports } from 'unplugin-vue-router' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ VueRouter({ routesFolder: "src/pages", exclude: ["src/pages/components/**/*.vue"], }), vue(), AutoImport({ imports: [ "vue", - "vue-router", + VueRouterAutoImports ], }), ], }); 再查看auto-imports.d.ts文件,发现也已经有了对应方法的类型声明: 自动导入自己写的工具模块 有时候我们也希望可以将自己写的工具方法实现自动导入,好在插件中可以配置。 举个例子,假设我们有一个 utils目录,其中有文件date.ts是封装了一些日期方法,有文件storage.ts文件封装了缓存方法,再来一个 index.ts作为入口文件: export function getTime() { return new Date().getTime() } export function getStorage(key: string) { return localStorage.getItem(key) || sessionStorage.getItem(key) } export { getTime } from './date' export { getStorage } from './storage' 这个时候,我们希望 utils目录中所有的方法都能自动导入,就可以配置 dirs项: AutoImport({ imports: ['vue', 'vue-router'], dirs: [ 'src/utils' // 配置工具类的目录即可 ] }) 配置完成后,观察 auto-imports.d.ts文件,可以发现所有的方法都已声明了类型,可以直接使用了。
 · 7 min read 

检测对象是否存在循环引用

在使用 JSON.stringify() 的时候,如果对象存在循环引用,就会报错。利用这一点,我想到可以用来检测对象是否存在循环引用。代码实现如下: function hasCircularReference(obj) { try { JSON.stringify(obj) return false } catch (e) { return e.message.indexOf('Converting circular structure to JSON') > -1 } } 出于好奇,我在想是否可以手动实现一个用于检测循环引用的方法,在跟 GPT 沟通后,实现了如下代码: function hasCircularReference2(obj) { const seenObj = new Set() // 存储已经访问过的对象引用 function detect(obj) { if (seenObj.has(obj)) { // 判断是否访问过该对象,如果访问过则返回 true return true } seenObj.add(obj) // 如果没有访问过该对象,则将其添加到集合中 for (const key in obj) { if (obj.hasOwnProperty(key) && detect(obj[key])) { // 遍历判断对象属性是否包含循环引用,如果包含则返回 true return true } } return false } // 返回检测结果 return detect(obj) }
 · 2 min read 

使用 Astro 开发一个博客

如你所见,此博客使用 Astro 开发的,整个站点还是比较简陋的,但足够用于发布文章。 在此之前,我折腾过很多博客,从最早的 WordPress 到后来的 Hexo,再到 Typecho 和 Hugo,我以为我会停留在使用 Hugo 上,但最终还是想自己从零开发一个博客。 其实在使用 Astro 开发之前,我已经使用 Nuxt 和 NuxtContent开发过一个静态博客,使用也是一样的,本地编写 md 文件,然后编译成静态文件部署。但不管是在开发中还是部署过程中,体验都不是很好,总感觉像是个半成品,虽然我要做的东西很简单,但体验不好就是不好,所以在短暂停止后,我又用 Astro 开发了这个博客,不得不说,框架真好用,文档也是写得真好,从上手到开发完,也就一天时间,当然成果还是粗糙的,不过我已经满意了,后面可以慢慢优化。 再说说为什么非要自己开发一个博客。最主要原因还是在各种现有博客框架中,选主题选到颓废,到最后啥文章笔记都没有写,全在折腾博客外观了。我承认好看的博客确实很吸引人,也容易让自己更愿意去维护内容,但是往往折腾外观的精力消耗得更多,倒也不说是本末倒置,把折腾外观当成爱好也行,但我又有点不想这样,因为我平时也是会记笔记的,但写到自己博客上的却也不多,因为还在纠结怎么显示效果好呢。也想过在一些主题上修改,但这些主题基本用的模板语法,我看着头痛,懒,不想折腾,也不愿意在别人代码中大肆修改,累。那就尝试自己开发吧,毕竟程序员开发自己的博客,也算是一个项目锻炼了。 关于博客布局样式,我常常纠结。浏览过很多博客,到现在我真是既想全要,又想全不要,大概就是每一种博客布局都有优缺点吧。现在我也想明白了,直接当成笔记列表来开发吧,内容直观,同时对我开发适配也简单,没办法啊,设计能力有限。 较早之前的记录就不搬运了,新博客内容不多,所以先就这样了。有一些想加的功能,会在后期考虑加入。先充实些内容才是重点。
 · 4 min read 

uniapp 开发记录

去年我用 uniapp 帮公司开发了一个跨平台 App,最近又改版优化了一版。App 虽然简单,但是也踩了不少坑,所以准备用此文记录一下。 初始化项目 uniapp 支持两种初始化项目方式,一个是用他们的编辑器 HBuilderX(后简称 HBX) 可视化创建,一个就是利用 vue-cli 命令行方式创建。但是开发 APP 始终需要用 HBX 来进行云端打包,所以我就直接用 HBX 来初始化项目的。 但是我觉得这个编辑器不好用,所有开发时用 HBX 运行项目,然后在 VSCode 中写代码,并配置了 prettier,保存自动格式化代码。 现在 uniapp 搞了个 HBuilderX cli 命令来运行或者打包 app,我浅看了下,本质还是需要安装 HBuilderX 的,只是可以通过命令来操作 HBX ,没啥意义。文档在这里:HBuilderX cli 我个人更希望 uniapp 官方拥抱 VSCode 这些成熟的编辑器,只专注做 sdk。 开发运行 关于基座 开发 app 时,可以使用模拟器或者真机来调试,此时安装的是基座包。uniapp 提供标准基座,也可以自定义基座。 标准基座使用的是 DCloud 的包名、证书和三方 SDK 配置,是可以直接安装调试的,但是 ios 的真机调试用不了。 只推荐自定义基座,或者只能使用自定义基座,因为可以使所有的 app 配置生效,比如 App 名称和图标,权限设置,三方 sdk 配置等等,都需要生成自定义基座来生效。参考文档:自定义基座 关于证书 开发 App 就一定涉及证书,打自定义基座时也需要证书。只有安卓提供了测试证书,但是最后发布还是需要自定义证书的。安卓和苹果的证书教程: ios 证书,文档:https://ask.dcloud.net.cn/article/152 android 证书,文档:https://ask.dcloud.net.cn/article/35777 关于组件 开发跨平台 App,需要考虑兼容性。官方自带的组件一般是可以兼容 android 和 ios 的。但是如果使用插件时,需要注意下有的插件是针对某一平台开发的,就不会兼容另一个平台。 另外 UI 组件的选择,我这里推荐 uview-ui,比官方出的 uni-ui 好用些,但是只兼容 Vue2。所以开发 App 我建议使用 Vue2 语法版本,因为很多插件都没更新到 Vue3 语法,即使出了也有不少问题。 指定页面调试 有时候开发的页面层级比较深,调试时需要能够直达页面比较快捷。此时可以设置 condition 这个配置项,在 pages.json 中配置页面路径列表,开发时即可选择打开指定页面。如图: 其他 别的关于 app 的配置,参考文档就好,没啥大坑的。 还要提及下,如果新勾选了一些要用到的模块,需要重新打自定义基座并重新安装,这样真机调试才可以看到效果,当然这些都会有提示。 另一个就是配置文件 manifest.json支持可视化修改,也可以支持源码视图修改。如果你用别的编辑器,直接源码视图修改的话,要注意自动格式化的问题。因为这份配置文件里有很多注释,本来 json 文件是不支持这些注释的,在别的编辑器上可能会报错,保存时可能会格式化掉一些内容。 组件自动引用 HBuilderX 提供 easycom 组件模式,即使用组件前不需要手动引用,可以直接使用,只要符合规范的组件都会自动引用。那么这里就有一个坑,一定要严格遵循它的路径规则,即: 安装在项目根目录的 components 目录下,并符合components/组件名称/组件名称.vue 安装在 uni_modules 下,路径为uni_modules/插件ID/components/组件名称/组件名称.vue 我最开始自定义组件目录下用的是 index.vue,导致引用失败。当然这是他们的默认规则,你也可以去 pages.json 中自定义规则。 路由拦截 由于用的不是 vue-router,所以路由拦截就不好实现了。但好在 uniapp 提供了一个 api 拦截器,即 addInterceptor。它的作用是可以拦截 uniapp 中的一些 api,比如 request,navigateTo等。 如果需要拦截路由,拦截 navigateTo 或者 redirectTo 即可,可以在执行 api 之前进行一些判断。示例代码: function addInterceptor() { // 需要拦截的 api 列表 let list = ['navigateTo', 'redirectTo'] // 遍历执行拦截操作 list.forEach((item) => { uni.addInterceptor(item, { invoke(res) { // return true 表示放行,return false 表示不继续执行 if (res.url === '/pages/index/index') { return true } else { // return false 之前可以进行一些操作,比如重定向 return false } }, fail(err) { // 失败回调拦截 console.log(err) } }) }) } 图层层级 这是最坑的一点。开发 App 时,有些组件是原生渲染的,这就意味着无法使用 z-index 控制它的层级,就会出现遮挡内容的情况。 如果你使用 nvue 开发的页面,就没有关系,因为此时页面全是原生渲染的,可以覆盖层级。但是如果你页面已经用 vue 开发完,但是后来加了原生组件的需求,那么就有点难受了。 如果只想在原生组件上覆盖一些内容,那还好做,使用 cover-view 组件可以覆盖内容到原生组件上,具体看文档。 还有一类场景就是原生组件遮挡了页面中的内容,比如页面滑动时遮挡了底部操作栏,popup 弹窗被原生组件遮挡等。这种场景优点难办,但也有办法。一个就是使用 cover-view组件重写被遮挡内容,页面布局有点受限,但基本能实现。另一个办法就是使用原生子窗体 subNVue,关于它的介绍,引用官方定义: subNvue,是 vue 页面的原生子窗体,把 weex 渲染的原生界面当做 vue 页面的子窗体覆盖在页面上。它不是全屏页面,它给 App 平台 vue 页面中的层级覆盖和原生界面自定义提供了更强大和灵活的解决方案。它也不是组件,就是一个原生子窗体。 其实就是一个局部的 nvue 内容,可以覆盖住原生组件。关于它的使用就是需要在项目中创建一个 nvue 组件,然后去 pages.json 中对应的页面配置中配置style -> app-plus -> subNVues即可,可以参考这篇文档:subNVue 原生子窗体开发指南。 待续 后面想起来再加~
 · 9 min read