When a codebase grows around two ecosystems, the expensive part is rarely the first page. It is the long tail of stable native WeChat Mini Program pages, shared utilities, and components that cannot move all at once. New projects may use uni-app, but existing native mini programs often still hold online activity pages, complex legacy flows, and code that is not worth rewriting for its own sake. The result is a divided toolchain: uni-app utility libraries, business components, and hooks are unavailable to the native side.
A full remigration is usually not the first choice. Activity pages are often independent and change infrequently, so rebuilding them brings limited return. Existing features may be complex, while future work mostly adds new pages rather than redesigning old ones. A progressive hybrid approach is therefore more efficient and cost-effective. In practice, two scenarios appear repeatedly:
- The main framework is being rebuilt with uni-app, but many native subpages—such as campaign pages—must continue to work.
- A stable old project only needs new, complex, relatively independent modules added on top.
uni-app supports several official hybrid patterns with native mini programs: converting native mini program source to uni-app source with conversion tools; turning native code into mini program components and integrating those into a uni-app project; or keeping the native mini program and building some new features with uni-app. These options did not directly cover the cases above. Since several projects were involved, the work was generalized into a reusable, extensible hybrid setup.
Reusing native mini program pages inside uni-app
The core idea is to place native pages into the uni-app build output and register them as pages. To keep this practical, the process needs automation, which makes a Vite plugin a good fit.
A typical project layout looks like this:
.
├── src/ # uni-app project main directory
│ ├── pages/ # uni-app pages
│ ├── components/ # uni-app components
│ └── ...
├── miniprogram/ # native WeChat mini program code directory
│ ├── components/ # native components to reuse
│ ├── pages/ # native pages to reuse
│ └── ...
├── vite.config.ts # Vite configuration file
└── ...
The plugin has four main responsibilities:
- Build phase: automatically sync target files from the
miniprogramdirectory into thedistbuild output. - Resource management: move common modules from the native project—utility libraries, dependency packages, and so on—into a separate
sharednamespace, then update module references automatically to avoid collisions with uni-app build output. - Configuration updates: maintain
app.jsonautomatically, including page route registration and global component declarations. - Developer experience: watch the file system and trigger incremental builds when files under
miniprogramchange.
The pseudocode below sketches the flow:
function uniWxCopyPlugin(options) {
let publicBasePath = '' // Vite's base configuration
let configPath = '' // final output directory
let isDev = false // whether development environment
return {
name: 'vite-plugin-uni-wx-copy',
// Execute after Vite config resolution
configResolved(config) {
// Check whether WeChat mini program environment
if (config.define['process.env.UNI_PLATFORM'] !== '"mp-weixin"') {
return
}
publicBasePath = config.base
isDev = config.mode === 'development'
},
// Execute after build completes
writeBundle(options) {
const p = options.dir // build output directory
// If no output directory or base config, do not execute
if (!p || !publicBasePath) {
return
}
// Calculate final output directory
configPath = resolve(publicBasePath, p)
// 1. Copy native components to shared directory
copy(
'miniprogram/components' ->
configPath + '/shared/components'
)
// 2. Copy pages
copy(
'miniprogram/pages/index' ->
configPath + '/pages/index'
)
// 3. Replace component reference paths in pages
replace(
file: configPath + '/pages/**/*.json',
from: '/components/',
to: '/shared/components/'
)
// 4. Watch file changes in development mode
if (isDev) {
watch('miniprogram/**/*', (changedFile) => {
if (changedFile.includes('pages/')) {
copy(
changedFile ->
configPath + '/' + changedFile
)
}
else if (changedFile.includes('components/')) {
copy(
changedFile ->
configPath + '/shared/' + changedFile
)
replace(
file: configPath + '/pages/**/*.json',
from: '/components/',
to: '/shared/components/'
)
}
})
}
}
}
}
With this plugin, each project can enable the hybrid setup through configuration:
import uni from "@dcloudio/vite-plugin-uni";
import { defineConfig } from "vite";
import uniWxCopy from "vite-plugin-uni-wx-copy";
export default defineConfig({
plugins: [
uni(),
uniWxCopy({
rootDir: "../miniprogram",
// Copy shared resources
copy: [
{
sources: ["components", "static", "utils"],
dest: "shared/",
shared: true,
},
],
// Main package pages
pages: ["pages/index", "pages/page1", "pages/page2"],
// Subpackage configuration
subPackages: [
{
root: "subpackages",
pages: ["detail"],
},
],
// Rewrite app.json to add global components
rewrite: [
{
file: "app.json",
write: code => {
const appJson = JSON.parse(code);
appJson.usingComponents = {
...appJson.usingComponents,
"app-btn": "/shared/components/app-btn/app-btn",
};
return JSON.stringify(appJson, null, 2);
},
},
],
}),
],
});
One critical problem remains: sharing state between native pages and the uni-app shell—runtime data such as environment configuration and user information. Because business contexts differ, the state-sharing layer needs to be customizable. The approach uses the mini program global instance getApp() as a bridge across stacks and implements core state management and updates in the uni-app project. During the build, string matching replaces methods in the native project with the unified getApp() interface.
For example, an authentication method can be redirected to the implementation provided by uni-app:
// auth.getUserInfo( -> auth.getApp().getUserInfo(
{
replaceRules: {
from: /auth.getUserInfo\(/g,
to: 'getApp().getUserInfo(',
files: [
...pages.map(page => path.join(configPath, page, '**/*.js')),
...shared.sources.map(dir => path.join(configPath, shared.dest, dir, '**/*.js')),
],
},
}
Another example changes the native project's development environment dynamically:
{
rewrite: [
{
file: "shared/config/index.js",
write: code => {
// eslint-disable-next-line no-param-reassign
code = code.replace(
/export const DEV =(.+)/,
`export const DEV = ${mode === "development" ? "true" : "false"}`
);
return code;
},
},
];
}
The implementation is available as vite-plugin-uni-wx-copy.
Integrating uni-app into a native mini program
This pattern reverses the direction: the uni-app build output is integrated into the native mini program and its pages are registered there. It relies on a packaging mode from uni-app: mixed subpackages.
In simple terms, a uni-app project is built as a subpackage of a mini program. This fits cases where new functionality should be embedded in an existing mini program while still being distributable to app, H5, and other targets. A single WeChat mini program subpackage is limited to 2 MB, so the project can be split into multiple subpackages as needed without affecting distribution to other platforms.
Assume the layout is:
.
├── miniprogram/ # native WeChat mini program project directory
│ ├── app.js
│ ├── app.json
│ └── ...
└── uni-app-project/ # uni-app project directory
├── src/
├── vite.config.ts
└── ...
In the uni-app project's package.json, configure subpackage build commands. Depending on the case, multiple subpackages may be used:
{
"scripts": {
"dev": "run-p 'dev:**'",
"build": "run-p 'build:**'",
"dev:pkg-a": "uni -p pkg-a --subpackage=pkg-a",
"build:pkg-a": "uni build -p pkg-a --subpackage=pkg-a",
"dev:pkg-b": "uni -p pkg-b --subpackage=pkg-b",
"build:pkg-b": "uni build -p pkg-b --subpackage=pkg-b"
},
"uni-app": {
"scripts": {
"pkg-a": {
"title": "pkg-a",
"env": {
"UNI_PLATFORM": "mp-weixin"
},
"define": {
"MP-PKG-A": true
}
},
"pkg-b": {
"title": "pkg-b",
"env": {
"UNI_PLATFORM": "mp-weixin"
},
"define": {
"MP-PKG-B": true
}
}
}
}
}
In the uni-app project's pages.json, use conditional compilation to assign pages to subpackages:
{
"pages": [
// #ifdef MP-PKG-A
{
"path": "pages/index/index"
},
// #endif
// #ifdef MP-PKG-B
{
"path": "pages/detail/index"
}
// #endif
]
}
The Vite plugin for this direction has two core jobs:
- Build phase: place the uni-app build output into the native mini program project.
- Developer experience: when uni-app hot reloads, update only the changed parts.
The pseudocode looks like this:
function uniSubpackageCopyPlugin(options) {
return {
name: "vite-plugin-uni-subpackage-copy",
// Execute after other plugins
enforce: "post",
// Execute after build completes
async writeBundle(output) {
// 1. If rewrite rules are configured, process file rewrites first
if (options.rewrite) {
for (const rule of options.rewrite) {
// Read file
const content = readFile(rule.file);
// Process content with rewrite function
const newContent = rule.write(content);
// Write new content
writeFile(rule.file, newContent);
}
}
// 2. Use rsync to sync subpackage files from uni-app build directory to native project
rsync({
from: options.subpackageDir, // uni-app subpackage build directory
to: options.rootDir, // native project directory
// Preserve file attributes, recursive sync, compressed transfer
flags: "avz",
// Delete files in target directory that are not in source directory
delete: true,
});
},
};
}
Configure the plugin in the uni-app project's vite.config.ts:
import uni from "@dcloudio/vite-plugin-uni";
import { defineConfig } from "vite";
import uniSubpackageCopy from "vite-plugin-uni-subpackage-copy";
export default defineConfig({
plugins: [
uni(),
process.env.UNI_PLATFORM === "mp-weixin" &&
uniSubpackageCopy({
rootDir: "../miniprogram",
subpackageDir: process.env.UNI_SUBPACKAGE,
}),
],
});
The implementation is available as vite-plugin-uni-subpackage-copy.