Skip to content

读懂项目结构

fjs create 生成的项目很小,但每个文件都有它的角色。这一篇逐个讲清楚,并说明哪些是你写的、哪些是工具生成的、哪些是构建产物

目录总览

text
my-app/
├─ package.json          依赖、脚本,以及 "fjs" 构建配置字段
├─ app.config.ts         原生应用配置:包名、权限、版本、屏幕方向、小程序 appid
├─ vite.config.ts        浏览器端 Vite 配置(挂了 fjs() 插件)
├─ index.html            浏览器端入口 HTML
├─ tsconfig.json         TS + Volar 配置(让编辑器认识 fjs 的标签)
├─ src/
│  ├─ main.ts            应用入口:各端共用
│  ├─ Shell.vue          页面外壳:每个页面都被它包一层
│  ├─ fjs-global.d.ts    把 fjs 的类型引进来
│  └─ pages/
│     └─ index.vue       首页 → 路由 "/"
└─ .gitignore

随着开发,还会陆续出现这些目录(都是约定目录,放进去就生效,不用注册):

目录作用详见
src/pages/每个 .vue 文件就是一个页面,路径即路由页面与路由
src/components/普通 Vue 组件,自己 import 使用写第一个页面
src/plugins/app.use() 类插件(pinia、i18n),自动收集添加插件
src/modules/本地模块:可以直接 npm publish 的包创建模块
src/workers/Worker 脚本,构建成 /workers/<name>.js事件、网络与状态
src/main.dart项目级的 Dart 代码,注入 Flutter 宿主Flutter 宿主
public/原样拷贝的静态文件,按根路径访问静态资源
html/<web-view> 打开的本地网页静态资源

入口:src/main.ts

ts
import { createFjsApp } from 'fjs/app';
import { routes } from 'fjs/pages';
import { plugins } from 'fjs/plugins';
import Shell from './Shell.vue';

createFjsApp({
  routes,
  plugins,
  shell: Shell,
}).mount();

这里导入的 fjs/* 都不是真实存在的 npm 包,而是构建时解析的虚拟模块

导入是什么
fjs/appcreateFjsApp:按平台创建应用。Flutter 上每个路由是一个原生页面;Web 上是 vue-router
fjs/pages扫描 src/pages/**/*.vue 生成的路由表
fjs/plugins收集 src/plugins/*.ts 和模块组件生成的插件列表
fjs运行时 API:invokeHostfetchtoast、element API 等
fjs/routeruseRouter / useRoute / onPageSettled

同一份 main.ts 在各端都能跑,因为构建时 fjs/appfjs/router 会被 alias 到对应平台的实现。你的业务代码不需要写任何 if (isWeb)

外壳:src/Shell.vue

vue
<template>
  <safe-area>
    <view class="shell">
      <slot />
    </view>
  </safe-area>
</template>

shell 包在每一个页面外面,页面内容放进它的默认插槽。它还会拿到一个 route prop(当前页的 path、query、meta),所以导航栏、tabBar 这类每页都有的东西只写一次:

vue
<script setup lang="ts">
import type { RouteLocation } from 'fjs/router';
defineProps<{ route: RouteLocation }>();
</script>

<template>
  <safe-area>
    <view class="shell">
      <text class="nav-title">{{ route.meta.title }}</text>
      <scroll-view class="body"><slot /></scroll-view>
    </view>
  </safe-area>
</template>

注意标签

你会发现模板里写的不是 div / span,而是 view / text / safe-area。这些是 ufjs 的内置标签,在 Flutter 上对应 Widget,在浏览器里对应 DOM 组件,在小程序里对应同名组件。见内置组件

页面:src/pages/index.vue

vue
<route>
{"title": "my-app"}
</route>

<template>
  <view class="page">
    <text class="title">my-app</text>
  </view>
</template>

<style scoped>
.page { flex-grow: 1; align-items: center; justify-content: center; }
.title { font-size: 24px; font-weight: 700; color: #111827; }
</style>

除了标准的 <template> / <script> / <style>,页面还可以有一个 <route> 块,里面是 JSON:title 等字段会进 route.metaplatforms 可以限制页面只在某一端出现。

配置文件

package.jsonfjs 字段

构建相关的配置写在 package.jsonfjs 字段里(默认没有,需要时再加):

json
{
  "fjs": {
    "shared": ["pinia"],
    "performance": { "nodeBudget": 800 },
    "mp": { "exclude": ["example/"] }
  }
}

完整字段见配置参考

app.config.ts

原生应用本身的配置:

ts
import { defineConfig } from '@ufjs/cli/config';

export default defineConfig({
  version: '1.2.0+3',
  android: {
    applicationId: 'com.acme.demo',
    permissions: ['android.permission.INTERNET'],
  },
  ios: {
    bundleIdentifier: 'com.acme.demo',
    infoPlist: { NSCameraUsageDescription: '用于扫描二维码' },
  },
  wxmp: { appid: 'wx1234567890abcdef' },
});

fjs run 生成 Flutter 宿主时会把这些写进 AndroidManifest.xmlInfo.plistpubspec.yaml

vite.config.ts

ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { fjs } from '@ufjs/cli/vite';

export default defineConfig({
  plugins: [fjs(), vue()],
  build: { outDir: 'dist/web' },
});

只有浏览器端用它。fjs() 插件负责生成虚拟模块、把内置标签映射到 DOM 组件、改写 fjs 特有的 CSS。

tsconfig.jsonfjs-global.d.ts

jsonc
{
  "vueCompilerOptions": {
    "plugins": ["@ufjs/runtime/volar"],  // 让 Volar 按 fjs 的类型理解 text/image/button…
    "strictTemplates": true
  }
}
ts
// src/fjs-global.d.ts
/// <reference types="@ufjs/runtime/ambient" />   // 所有 fjs/* 虚拟模块的类型
import '@ufjs/runtime/vue-global';                 // 内置标签的 props / 事件类型

有了这两处,编辑器里写 <image mode="..."> 会有补全,属性写错是编译错误。

自动生成的文件

构建、fjs dev 或 Vite 启动时,工具链会在 src/ 下生成几份声明文件,给编辑器提供补全。不要手改,也不要提交(模板的 .gitignore 已经忽略):

文件内容
src/fjs-routes.d.ts路由名 → 路径,router.push({ name }) 有补全和拼写检查
src/fjs-modules.d.ts本地模块的裸导入类型
src/fjs-components.d.ts模块提供的全局组件类型
src/fjs-assets.d.tspublic/html/ 下的文件,<image src> 有补全

构建产物

目录由谁产生内容
dist/app/fjs buildApp 端 JS bundle / .fjsbundle 字节码
dist/web/vite build / fjs build --web静态站点
dist/mp/fjs build --mp小程序工程,用微信开发者工具打开
.fjs/flutter/fjs run / fjs build --release生成的 Flutter 宿主工程

.fjs/flutter 是一个完整的 Flutter 项目,被 gitignore,随时可以删掉重建。当你需要长期改它(签名、原生插件)时,用 fjs host eject 把它移进仓库,见 Flutter 宿主

阅读示例项目

仓库里有几个完整示例,是学习写法的最好材料:

示例看什么
examples/hello-fjs组件画廊,所有内置组件、路由、tabBar、主题切换、canvas、WebGL、Worker,同时跑 Flutter / Web / 小程序
demo标准 Vue 3 + Vite 项目,含 pinia、fetch、拖拽、模块 @ufjs/iconmind
examples/hello-js不用 Vue,直接调 element API
packages/fjs-iconmind一个完整的模块:API + Flutter Widget + Web 替身 + 构建钩子

推荐的阅读顺序:先看 hello-fjs/src/main.tsShell.vue(外壳怎么做 NavBar 和 TabBar),再看 src/pages/index.vue(首页怎么读路由表生成目录),然后挑感兴趣的组件页看。

基于 MIT 协议发布