前言
Youtube上的前端网红「Theo」在React文档仓库发起了一个Pull request,号召React文档不要再默认推荐CRA(create react
app),而是应该将Vite作为构建应用的首选。
vite的影响力已经从vue蔓延到了react,可见在前端工程化开发中,它已经越来越流行,是时候该从webpack切换到vite了。
为什么使用vite
Vite 采用了基于ES Module的开发服务器。进行本地开发时,使用HMR,速度快了很多。相较于webpack,有很多优势:
更快的热更新机制
更快的打包效率
更简单的配置
在做kintone开发时,我也很想尝试使用vite进行开发,构建。所以也查询了一些相关文章。
但是只看到了一些介绍如何使用vite(rollup)打包kintone自定义js的文章。但是却没有看到如何利用vite进行kintone开发的文章。所以我想到开发一个vite插件来解决这个问题。
vite-plugin-kintone-dev
npm链接:https://www.npmjs.com/package/vite-plugin-kintone-dev
这个插件实现的功能:
支持使用vite创建kintone自定义js,hmr让你的开发快如闪电
支持react,vue等不同的前端框架
构建时支持打包并自动上传kintone
实践:kintone手机版自定义
这次我们结合vite插件,以kintone手机版的自定义开发为范例,给大家做演示。
技术栈:vite4 + vue3 + vant4
1. 使用vite脚手架初始化vue项目
首先通过vite脚手架工具。创建一个vue项目
npm create vue@latest
设置项目名: kintone-mobile-custom(这是我的预设)
选择vue,TypeScript。然后根据需求进行选择。并进行初始化安装.
cd kintone-mobile-customnpm install
2. 安装kintone开发的vite插件
npm install -D vite-plugin-kintone-dev
第一次启动时,会自动检查你的env文件的设置模版。如果没有配置,会启动命令行交互,让你输入配置信息。同时自动更新你的env文件。
如果你的env文件设置有误,可以自行去修改。
(serve模式下为".env.development"文件, build模式下为".env.production"文件)
插件的参数说明
{ outputName: "mobile", //最后打包的名字 upload: true }
配置vite
vite.config.ts
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import kintoneDev from "vite-plugin-kintone-dev"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), kintoneDev({ outputName: "mobile", upload: true }), ], });
3. 安装其他的一些库
图片库
接下来我们再配置一个图片库。
非常常用的一个插件图库插件Unplugin Icons
npm install -D unplugin-icons unplugin-vue-components
它的具体设置,请参考它的官网。
添加所有icon资源,不用担心,真正打包时,它是按需加载的。
npm i -D @iconify/json
手机ui库
然后我们使用vant这个库来做为手机开发的ui库。
https://vant-contrib.gitee.io/vant/#/zh-CN
npm install vant
tsconfig.app.json配置
tsconfig.app.json
... "compilerOptions": { "types": ["unplugin-icons/types/vue"], ... } ...
vite的最后配置
vite.config.ts
import { fileURLToPath, URL } from "node:url"; import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import Icons from "unplugin-icons/vite"; import IconsResolver from "unplugin-icons/resolver"; import Components from "unplugin-vue-components/vite"; import { FileSystemIconLoader } from "unplugin-icons/loaders"; import kintoneDev from "vite-plugin-kintone-dev"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ kintoneDev({ outputName: 'mobile', upload: true }), vue(), Components({ resolvers: [IconsResolver()], }), Icons({ compiler: "vue3", customCollections: { "my-icons": FileSystemIconLoader("./src/assets/icons"), }, }), ], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, });
代码的开发
1. 修改main.ts
初始化vue,并 将它的根节点挂载在手机的门户上方的空白部分的元素
src/main.ts
import 'vant/lib/index.css' import './assets/main.css' import { createApp } from 'vue' import App from './App.vue' import router from './router' import { Tabbar, TabbarItem } from 'vant' kintone.events.on('mobile.portal.show', (event) => { const app = createApp(App) app.use(router) app.use(Tabbar) app.use(TabbarItem) app.mount(kintone.mobile.portal.getContentSpaceElement()!) return event })
2. 隐藏掉原先的手机画面
src/assets/main.css
/* @import './base.css'; */ .gaia-mobile-v2-portal-announcement-container, .gaia-mobile-v2-portal-appwidget-container, .gaia-mobile-v2-portal-spacewidget-container { display: none; } .van-hairline--top-bottom::after, .van-hairline-unset--top-bottom::after { border-width: 0; } .gaia-mobile-v2-viewpanel-header { background-color: #4b4b4b; } .gaia-mobile-v2-viewpanel-contents { border-radius: 0; } .van-tabbar { width: 100vw; } .van-tabbar--fixed { left: unset; } .gaia-mobile-v2-portal-header-container .gaia-mobile-v2-portal-header::after { background: none; } .group-module-background { background-color: rgba(255, 255, 255); border-radius: 10px; box-shadow: 0 0 5px 0 #ced3d4; }
3. 添加tabbar
src/App.vue
<script setup> import { ref } from "vue" import type { Ref } from 'vue' const active: Ref<number> = ref(0) </script> <template> <router-view v-slot="{ Component }"> <keep-alive> <component :is="Component" /> </keep-alive> </router-view> <van-tabbar route v-model="active" active-color="#febf00" inactive-color="#b8b8b5"> <van-tabbar-item replace to="/"> <span>Home</span> <template #icon="props"> <i-mdi-home /> </template> </van-tabbar-item> <van-tabbar-item replace to="/contacts"> <span>Contacts</span> <template #icon="props"> <i-mdi-contacts /> </template> </van-tabbar-item> <van-tabbar-item replace to="/space"> <span>Space</span> <template #icon="props"> <i-mdi-shape-circle-plus /> </template> </van-tabbar-item> <van-tabbar-item replace to="/star"> <span>Star</span> <template #icon="props"> <i-mdi-star /> </template></van-tabbar-item> <van-tabbar-item replace to="/todo"> <span>Todo</span> <template #icon="props"> <i-mdi-tooltip-edit /> </template></van-tabbar-item> </van-tabbar> </template> <style scoped> .tabbar-icon { font-size: 1em; } </style>
4. 模拟一个订单列表应用
我们在kintone上创建一个应用。并准备以下字段。
フィールド名 | フィールドコード | Type |
---|---|---|
title | title | 文字列 (1行) |
num | num | 数値 |
desc | desc | 文字列 (1行) |
price | price | 数値 |
thumb | thumb | 文字列 (1行) |
接着请自行添加一些数据。并且设置【API token】
5. 安装kintone js sdk
npm install @kintone/rest-api-client
6. 添加kintone的ts声明
1. 安装kintone的ts生成工具
npm install -D @kintone/dts-gen
2. 根据应用生成ts声明,请根据自己的环境输入
npx @kintone/dts-gen --base-url https://xxxx.cybozu.com -u xxxx -p xxxx --app-id xx
3. tsconfig.app.json
{ ... "files": ["./node_modules/@kintone/dts-gen/kintone.d.ts"], ... }
4. 添加restapi返回数据的ts类型
types/restApiRecords.ts
import { KintoneRecordField } from '@kintone/rest-api-client' export type GoodListAppRecord = { $id: KintoneRecordField.ID $revision: KintoneRecordField.Revision thumb: KintoneRecordField.SingleLineText num: KintoneRecordField.Number title: KintoneRecordField.SingleLineText price: KintoneRecordField.Number desc: KintoneRecordField.SingleLineText 更新人: KintoneRecordField.Modifier 创建人: KintoneRecordField.Creator 更新时间: KintoneRecordField.UpdatedTime 记录编号: KintoneRecordField.RecordNumber 创建时间: KintoneRecordField.CreatedTime }
5. kintone sdk:@kintone/rest-api-client@4.1.0的ts的bug应对
@kintone/rest-api-client的4.1.0有bug,在vite新版中,模块解析部分使用了typescript5中的新的配置bundler。所以我们把它改为node解析
node_modules/@vue/tsconfig/tsconfig.json
// modify "moduleResolution": "bundler" => "node" "moduleResolution": "node"
7. 封装kintone api请求
src/service/kintoneApi.ts import { KintoneRestAPIClient } from '@kintone/rest-api-client' import type { GoodListAppRecord } from '@/types/restApiRecords' export class KintoneApi { client: KintoneRestAPIClient constructor() { this.client = new KintoneRestAPIClient({}) } public async getAllRecords(app: string) { return await this.client.record.getAllRecords<GoodListAppRecord>({ app }) } }
8. env文件添加一些kintone的配置
添加kintone应用的app id和api token
.env
VITE_APP_ID=xxx
9. 创建页面
先删除view下原有的文件添加以下文件。
view/Home.vue
view/Contacts.vue
view/Space.vue
view/Star.vue
view/Todo.vue
在这些页面写一些模拟内容
Home页通过获取kintone的数据来模拟订单数据
src/view/Home.vue
<template> <List v-model:loading="loading" :finished="finished" @load="onLoad"> <Card v-for="(item, index) in list" :key="index" :num="item.num.value" :price="item.price.value" :desc="item.desc.value" :title="item.title.value" :thumb="item.thumb.value" /> </List> </template> <script setup> import { ref } from 'vue' import type { Ref } from 'vue' import { Card, List } from 'vant' import { KintoneApi } from '@/service/kintoneApi' import type { GoodListAppRecord } from '@/types/restApiRecords' const list: Ref<GoodListAppRecord[]> = ref([]) const loading = ref(false); const finished = ref(false); const onLoad = () => { const kintoneClient = new KintoneApi() const appId = import.meta.env.VITE_APP_ID kintoneClient.getAllRecords(appId).then((res) => { list.value = res loading.value = false; finished.value = true; }) }; </script>
其他页面准备一些模拟数据,比如Contacts.vue
view/Contacts.vue
<template> <div> <h1>Contacts</h1> </div> </template>
10. 设置路由
这边需要使用createWebHashHistory这种方式创建路由。因为createWebHistory方式刷新时会被后端路由解析。
src/router/index.ts
import { createRouter, createWebHashHistory } from "vue-router"; const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), routes: [ { path: "/", name: "home", component: () => import("../views/Home.vue"), }, { path: "/contacts", name: "contacts", component: () => import("../views/Contacts.vue"), }, { path: "/space", name: "space", component: () => import("../views/Space.vue"), }, { path: "/star", name: "star", component: () => import("../views/Star.vue"), }, { path: "/todo", name: "todo", component: () => import("../views/Todo.vue"), }, ],}); export default router;
启动项目
npm run dev
启动后,kintone首页自定义会自动上传vite_plugin_kintone_dev_module_hack.js文件
最后的成果图
我们看到一个简单的手机自定义骨架就出来了。
这只是一个最简单的范例,实际上,还需要考虑的地方还有很多,比如集中式状态管理,页面生命周期缓存,数据下拉刷新等等。如果你对手机版感兴趣,我这边有个三年前的项目。不过当时使用的还是vue2。可以看看。https://github.com/kintone-samples/sample-kintone-mobile-customize-CN
构建阶段
npm run build
运行完,会自动上传打包后的代码到kintone。
示例项目github地址
在react中使用的范例
本插件同样适用于使用vite在kintone中构建react应用。
使用react范例可参考:
https://github.com/GuSanle/vite-plugin-kintone-dev/tree/main/example/react-kintone-vite-demo
插件原理
插件的原理是通过js自定义,hack出一个类型为module的script标签。用来加载main.ts文件。
不过基于vue和react的不同,还需要结合vite的文档做一些代码的inject。
可能存在的一些问题
如果开发时遇到事件句柄的注册时机的问题,可以尝试在使用kintone事件后挂载后,使用如下代码解决问题。 (构建时,可以删除,因为构建时,不再使用esm模式,不存在异步加载问题。) src/main.ts的示例: src/main.ts
import { createApp } from "vue"; import App from "./App.vue"; kintone.events.on("app.record.detail.show", (event) => { const app = createApp(App); app.mount(kintone.app.record.getHeaderMenuSpaceElement()!); return event; }); //通过手动执行kintone事件,来解决异步事件执行时机问题 const event = new Event("load"); // @ts-ignore cybozu.eventTarget.dispatchEvent(event);
总结
使用了vite之后,hmr技术使得我们对代码进行修改后,会马上在页面上体现,非常的迅速,开发体验非常好。
kintone结合vite的开发就讲到这里。如果你对我这个插件或者这个手机范例感兴趣,欢迎一起交流,给个star。谢谢。