Tiktok minis与传统小程序对比

TikTok Minis 不是传统小程序页面运行时,更接近 运行在 TikTok 客户端 WebView 里的 Web 应用方案。

  • 页面本身仍然是标准 Web 工程
  • 客户端能力通过TTMinis 暴露

开发Tiktok minis

安装命令行界面工具

注意node版本不能太低,通常要求大于18

npm install tiktok-minis-cli -g --registry=https://registry.npmjs.org/

指定 npm 仓库地址(官方源):–registry=https://registry.npmjs.org/

查看安装结果

minis -v

使用minis init生成配置文件 minis.config.json

minis init

执行minis init,提示用户确认项

√ Current app local dev server Node port: 3000
√ Business page local host (optional, default: localhost): localhost
√ Build output directory: dist  
√ HTML entry relative path: index.html

Build output directory默认是dist,uniapp项目应改为打包H5所在的web目录,例如:unpackage/dist/build/web

完成后在项目根目录生成minis.config.json文件,如下

{
  "dev": {
    // Minis 应用项目本地启动时候的端口
    "port": "3000",
    // Minis 应用项目本地启动时候的 host,可以不填
    "host": "localhost",
  },
  "build": {
    // minis 应用构建后的文件目录
    "outputDir": "unpackage/dist/build/web",
    // minis 应用源代码 index.html 在项目中的相对路径
    "htmlEntry": "public/index.html"
  },
  "domain": {
    // 配置白名单
		"trustedDomains": ["https://**.com/"],
		// 兼容不同版本
		"allowList": ["https://**.com/"]
	}
}

初始化 TikTok Minis SDK

将 TikTok Minis SDK 添加到您的 index.html 文件中,并使用您项目的唯一客户端密钥初始化它。注意这段代码需要放入打包后的index.html内。

UniApp 项目是用 HBuilderX 或者 vue-cli 构建的 ,打包到 web 的 index.html 默认是由系统生成的,所以会去掉了之前加在根目录 index.html 里面的 SDK脚本,会造成 minis build 校验报错。

所以最方便的方式是设置脚本,让每次打包自动在打包后的unpackage/dist/build/web/index.html带上TikTok Minis SDK并初始化

1、在根目录创建scripts文件夹
2、在scripts文件夹内创建inject-minis-sdk.js文件,在每次执行构建后,自动把 TikTok Minis SDK 初始化代码注入到打包产物的 unpackage/dist/build/web/index.html里,并且避免重复注入。代码如下,其中const CLIENT_KEY = ‘your_client_key_here’;改为真实CLIENT_KEY

const fs = require('fs');
const path = require('path');

const CLIENT_KEY = 'your_client_key_here';
const SDK_SRC = 'https://connect.tiktok-minis.com/drama/sdk.js';
const targetFile = path.resolve(__dirname, '../unpackage/dist/build/web/index.html');

function fail(message) {
  console.error(`[inject-minis-sdk] ${message}`);
  process.exit(1);
}

if (!fs.existsSync(targetFile)) {
  fail(`未找到构建产物: ${targetFile}。请先完成 H5 打包。`);
}

let html = fs.readFileSync(targetFile, 'utf8');

const sdkScriptRegex = /<script[^>]*src=["']https:\/\/connect\.tiktok-minis\.com\/drama\/sdk\.js["'][^>]*><\/script>/ig;
const initScriptRegex = /<script>\s*TTMinis\.init\(\{[\s\S]*?clientKey\s*:\s*["'][^"']+["'][\s\S]*?\}\);\s*<\/script>/ig;

html = html.replace(sdkScriptRegex, '');
html = html.replace(initScriptRegex, '');

const snippet = `<script src="${SDK_SRC}"></script><script>TTMinis.init({clientKey:'${CLIENT_KEY}'});</script>`;
const headMatch = html.match(/<head[^>]*>/i);

if (!headMatch) {
  fail('未找到 <head> 标签,无法注入 Minis SDK。');
}

const headStart = headMatch.index + headMatch[0].length;
const tail = html.slice(headStart);
const firstScriptOffset = tail.search(/<script\b/i);

if (firstScriptOffset >= 0) {
  const insertAt = headStart + firstScriptOffset;
  html = `${html.slice(0, insertAt)}${snippet}${html.slice(insertAt)}`;
} else {
  const closeHeadOffset = tail.search(/<\/head>/i);
  if (closeHeadOffset < 0) {
    fail('未找到 </head> 标签,无法注入 Minis SDK。');
  }
  const insertAt = headStart + closeHeadOffset;
  html = `${html.slice(0, insertAt)}${snippet}${html.slice(insertAt)}`;
}

fs.writeFileSync(targetFile, html, 'utf8');
console.log(`[inject-minis-sdk] 注入完成: ${targetFile}`);

添加完成后,在minis build后会在unpackage/dist/build/web/index.html内自动注入如下代码,作用是完成TikTok Minis SDK的引入并初始化

<!-- index.html -->
<head>
  <script src="https://connect.tiktok-minis.com/drama/sdk.js"></script>
  <script>
    TTMinis.init({
      clientKey: "your_client_key_here",
    });
  </script>
</head>

TikTok Minis SDK (TTMinis):一个在 TikTok 应用内运行的客户端 JavaScript SDK。它提供了应用内能力,如登录和授权、支付、UI 触发器、奖励广告、生命周期钩子、网络和功能检测。

在项目根目录下创建package.json文件

代码如下

{
	"scripts": {
		"dev": "node scripts/serve-web-build.js",
		"start": "node scripts/serve-web-build.js",
		"minis:prepare": "node scripts/inject-minis-sdk.js",
		"minis:build": "npm run minis:prepare && npx -y tiktok-minis-cli build",
		"minis:dev:raw": "npm run minis:prepare && npx -y tiktok-minis-cli dev",
		"minis:dev": "node scripts/minis-dev-safe.js"
	}
}

TikTok小程序本地测试

如果没有使用到TikTok Minis SDK的功能,可直接基于浏览器进行开发调试。涉及到登录、授权、广告、支付、订阅等依赖客户端环境才需要如下操作进行本地调试

1、本地调试需要在项目根目录的index.html也要添加TikTok Minis SDK的引入并初始化

<!-- index.html -->
<head>
  <script src="https://connect.tiktok-minis.com/drama/sdk.js"></script>
  <script>
    TTMinis.init({
      clientKey: "your_client_key_here",
    });
  </script>
</head>

完整代码如下

<!DOCTYPE html>
<html lang="zh-CN">
	<head>
		<meta charset="UTF-8" />
		<!-- Tiktok Minis Init Start -->
		<script src="https://connect.tiktok-minis.com/drama/sdk.js"></script>
		<script>
			TTMinis.init({
				clientKey: 'your_client_key_here',
			});
		</script>
		<!-- Tiktok Minis Init End -->
		<script>
			var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
				CSS.supports('top: constant(a)'))
			document.write(
				'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
				(coverSupport ? ', viewport-fit=cover' : '') + '" />')
		</script>
		<title></title>
		<link rel="icon" type="image/svg+xml" href="static/favicon.svg" />
		<!--preload-links-->
		<!--app-context-->
	</head>
	<body>
		<div id="app"><!--app-html--></div>
		<script type="module" src="/main.js"></script>
	</body>
</html>

2、在根目录创建的scripts文件夹内创建minis-dev-safe.js文件,作用是自动清端口 -> 自动同步 IP -> 自动起本地服务 -> 启动 minis”的一键流程。代码如下

const { execSync, spawn } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');

const BRIDGE_PORT = 3000;
const DEFAULT_MINIS_HOST = '127.0.0.1';
const DEFAULT_MINIS_PORT = 18080;

function getListeningPidsByPort(port) {
  const output = execSync('netstat -ano -p tcp', {
    stdio: ['ignore', 'pipe', 'ignore'],
    encoding: 'utf8'
  });

  const pids = new Set();
  const lines = output.split(/\r?\n/);
  for (const line of lines) {
    const normalized = line.trim().replace(/\s+/g, ' ');
    if (!normalized.startsWith('TCP ')) {
      continue;
    }

    const parts = normalized.split(' ');
    if (parts.length < 5) {
      continue;
    }

    const localAddress = parts[1] || '';
    const state = (parts[3] || '').toUpperCase();
    const pid = parts[4];

    const portSuffix = `:${port}`;
    if (state === 'LISTENING' && localAddress.endsWith(portSuffix) && /^\d+$/.test(pid)) {
      pids.add(pid);
    }
  }

  return Array.from(pids);
}

function killPid(pid) {
  execSync(`taskkill /PID ${pid} /F`, {
    stdio: ['ignore', 'ignore', 'ignore']
  });
}

function clearPort(port) {
  const pids = getListeningPidsByPort(port);
  if (pids.length === 0) {
    console.log(`[minis-dev-safe] ${port} 端口空闲`);
    return;
  }

  for (const pid of pids) {
    try {
      killPid(pid);
      console.log(`[minis-dev-safe] 已释放 ${port} 端口,占用 PID: ${pid}`);
    } catch (error) {
      console.warn(`[minis-dev-safe] 释放 PID ${pid} 失败: ${error.message}`);
    }
  }
}

function runMinisDevRaw() {
  try {
    execSync('npm run minis:dev:raw', {
      stdio: 'inherit'
    });
  } catch (error) {
    process.exit(typeof error.status === 'number' ? error.status : 1);
  }
}

function readMinisConfig() {
  const configPath = path.resolve(__dirname, '../minis.config.json');
  try {
    const text = fs.readFileSync(configPath, 'utf8');
    const parsed = JSON.parse(text);
    const host = (parsed.dev && parsed.dev.host) || DEFAULT_MINIS_HOST;
    const port = Number((parsed.dev && parsed.dev.port) || DEFAULT_MINIS_PORT);
    return { host, port };
  } catch (error) {
    return { host: DEFAULT_MINIS_HOST, port: DEFAULT_MINIS_PORT };
  }
}

function getPreferredLanIPv4() {
  const interfaces = os.networkInterfaces();
  const candidates = [];

  for (const entries of Object.values(interfaces)) {
    for (const entry of entries || []) {
      if (!entry || entry.family !== 'IPv4' || entry.internal) {
        continue;
      }
      const ip = entry.address || '';
      if (ip.startsWith('169.254.')) {
        continue;
      }
      const score = ip.startsWith('192.168.') ? 3 : ip.startsWith('10.') ? 2 : ip.startsWith('172.') ? 1 : 0;
      candidates.push({ ip, score });
    }
  }

  if (candidates.length === 0) {
    return null;
  }

  candidates.sort((a, b) => b.score - a.score);
  return candidates[0].ip;
}

function syncMinisHostToLanIp() {
  const configPath = path.resolve(__dirname, '../minis.config.json');
  const current = readMinisConfig();
  const lanIp = getPreferredLanIPv4();

  if (!lanIp || current.host === lanIp) {
    return current;
  }

  try {
    const text = fs.readFileSync(configPath, 'utf8');
    const parsed = JSON.parse(text);
    parsed.dev = parsed.dev || {};
    parsed.dev.host = lanIp;
    if (!parsed.dev.port) {
      parsed.dev.port = DEFAULT_MINIS_PORT;
    }
    fs.writeFileSync(configPath, `${JSON.stringify(parsed, null, '\t')}\n`, 'utf8');
    console.log(`[minis-dev-safe] 已自动更新 minis.config.json dev.host: ${current.host} -> ${lanIp}`);
    return { host: lanIp, port: Number(parsed.dev.port) || DEFAULT_MINIS_PORT };
  } catch (error) {
    console.warn(`[minis-dev-safe] 自动更新 dev.host 失败: ${error.message}`);
    return current;
  }
}

function startServeWebBuild(host, port) {
  const scriptPath = path.resolve(__dirname, './serve-web-build.js');
  const child = spawn(process.execPath, [scriptPath], {
    detached: true,
    stdio: 'ignore',
    env: {
      ...process.env,
      HOST: host,
      PORT: String(port)
    }
  });
  child.unref();
  console.log(`[minis-dev-safe] 已启动本地应用服务: http://${host}:${port}`);
}

const minisConfig = syncMinisHostToLanIp();
clearPort(BRIDGE_PORT);
clearPort(minisConfig.port);
startServeWebBuild(minisConfig.host, minisConfig.port);
runMinisDevRaw();

3、在根目录创建的scripts文件夹内创建serve-web-build.js文件,这个文件是一个“本地静态资源服务器”,专门把 H5 打包产物目录对外提供给 TikTok Minis 调试环境访问,同时补上 CORS,保证手机端/调试页能正常访问你的 H5 构建产物。代码如下

const fs = require('fs');
const path = require('path');
const http = require('http');

const HOST = process.env.HOST || 'localhost';
const PORT = Number(process.env.PORT || 8080);
const ROOT_DIR = path.resolve(__dirname, '../unpackage/dist/build/web');

const MIME_TYPES = {
  '.html': 'text/html; charset=utf-8',
  '.js': 'application/javascript; charset=utf-8',
  '.css': 'text/css; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.svg': 'image/svg+xml',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.gif': 'image/gif',
  '.webp': 'image/webp',
  '.ico': 'image/x-icon',
  '.woff': 'font/woff',
  '.woff2': 'font/woff2'
};

function send(res, status, body, type) {
  const requestOrigin = res.req && res.req.headers ? res.req.headers.origin : '';
  const allowOrigin = requestOrigin || '*';
  res.writeHead(status, {
    'Content-Type': type || 'text/plain; charset=utf-8',
    'Cache-Control': 'no-cache',
    'Access-Control-Allow-Origin': allowOrigin,
    'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Requested-With',
    'Access-Control-Max-Age': '86400',
    Vary: 'Origin'
  });
  res.end(body);
}

function safeResolve(urlPath) {
  const cleanPath = decodeURIComponent(urlPath.split('?')[0]).replace(/\\/g, '/');
  const normalized = path.posix.normalize(cleanPath);
  const relative = normalized.startsWith('/') ? normalized.slice(1) : normalized;
  const resolved = path.resolve(ROOT_DIR, relative);
  if (!resolved.startsWith(ROOT_DIR)) {
    return null;
  }
  return resolved;
}

if (!fs.existsSync(ROOT_DIR)) {
  console.error('[serve-web-build] 未找到目录:', ROOT_DIR);
  console.error('[serve-web-build] 请先进行 H5 打包,确保存在 unpackage/dist/build/web');
  process.exit(1);
}

const server = http.createServer((req, res) => {
  if ((req.method || '').toUpperCase() === 'OPTIONS') {
    send(res, 204, '', 'text/plain; charset=utf-8');
    return;
  }

  const filePath = safeResolve(req.url || '/');
  if (!filePath) {
    send(res, 403, 'Forbidden');
    return;
  }

  let target = filePath;
  if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
    target = path.join(target, 'index.html');
  }

  if (!fs.existsSync(target)) {
    const fallback = path.join(ROOT_DIR, 'index.html');
    if (fs.existsSync(fallback)) {
      const html = fs.readFileSync(fallback);
      send(res, 200, html, 'text/html; charset=utf-8');
      return;
    }
    send(res, 404, 'Not Found');
    return;
  }

  const ext = path.extname(target).toLowerCase();
  const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
  const content = fs.readFileSync(target);
  send(res, 200, content, mimeType);
});

server.listen(PORT, HOST, () => {
  console.log(`[serve-web-build] Serving ${ROOT_DIR}`);
  console.log(`[serve-web-build] http://${HOST}:${PORT}`);
});

4、安装内测客户端,目前只支持安卓;iOS 需要上传到平台后扫码查看效果
M包(地区:US等)
T包(地区:JP等)

5、在项目根目录终端运行测试

npm run minis:dev

运行成功后,浏览器如图,然后使用第四步下载的Tiktok安装包登录后扫码
运行成功

(1.)保证电脑和手机处在连接一个 WIFI 局域网
(2.)保证电脑和手机均可访问外网
(3.)需要在Tiktok开发者后台添加测试权限
(4.)在手机端需要前台运行tiktok、屏幕保持常亮。息屏或者切屏会断连
(5.)不支持热更新,代码更改后,需要先重新打包项目然后再运行npm run minis:dev
添加测试权限

测试完成后,TikTok小程序打线上安装包

(1.)先执行打包项目
(2.)在根目录下执行npm run minis:build,完成打包

npm run minis:build

npm run minis:build

打包完成后安装包路径如上图:minis build completed successfully. Archive output: C:\Users\admin_TTMinis_\mnw636fnrz67y91r\minis.zip。也可自行把编译后的项目文件压缩为.zip

如果你直接使用 minis build(不是 npm run minis:build),npm 脚本不会触发,自动注入也不会执行。

需要集成这些功能

功能APIs描述必需
静默登录TTMinis.login
https://open.tiktokapis.com/v2/oauth/token/
在不进行明确授权的情况下获取用户的 OpenID 和访问令牌是
明确授权TTMinis.authorize
https://open.tiktokapis.com/v2/oauth/token/
https://open.tiktokapis.com/v2/user/info/
获取用户名和头像等额外用户数据,需要通过授权屏幕获取用户同意否
应用内广告:激励视频广告TTMinis.createRewardedVideoAd将激励视频广告插入您的小程序;必须启用 IAA 才能进行配置是
应用内广告:插屏广告TTMinis.createInterstitialAd将插屏广告插入您的小程序;必须启用 IAA 才能配置是
应用内购买:金币(一次性付款)TTMinis.pay
https://open.tiktokapis.com/v2/minis/trade_order/create/
允许在您的小程序内使用金币进行一次性购买;必须启用 IAP 才能配置是
应用内购买:订阅TTMinis.createSubscription
https://open.tiktokapis.com/v2/minis/subscription/create/
允许在您的小程序内购买订阅;必须启用 IAP 才能配置是
导航栏TTMinis.setNavigationBarColor
TTMinis.getMenuButtonBoundingClientRect
在您的小游戏中设置和自定义导航栏是
示例代码
静默登录
TTMinis.login((res) => {
	const code = res.authResponse.code;
	if (code) {
		// 业务逻辑接口				
	} else {
	  // 处理异常				
	}
});

切记,登录权限需要在TikTok开发者后台完善基本信息,尤其是《服务条款》和《隐私政策》,否则获取TTMinis.login报错
完善基本信息

激励视频广告IAA(In-App Ads)
// 在<script>标签内声明变量
let rewardedAd = null

// 初始化激励视频广告方法
initTiktokAd() {
  // 初始化
	rewardedAd = TTMinis.createRewardedVideoAd({
		adUnitId: 'rewarded_ad_unit',
	});

  // 监控广告视频的关闭
	rewardedAd.onClose((res) => {
		if (res.isEnded) {
			console.log('用户完整观看激励视频');
			// 业务逻辑接口
	  } else {
			console.log('用户中途关闭激励视频');
			// 中途退出或者播放完成后,广告实例会被释放,您需要重新创建广告实例
			this.initTiktokAd()
		}
	});
},

// 在合适的地方调用广告展示方法
showAd() {
	rewardedAd.show().catch((err) => {
		console.error('rewardedAd show failed:', err);
	});
},

切记,在广告播放后,需要调用initTiktokAd()重置广告

应用内购买IAP(In-App Purchases)
TTMinis.pay((res) => {
	if (res.is_success) {
		// 业务逻辑接口
	} else {
	
	}
}, {
	trade_order_id: 'trade_order_id',
});
分享
TTMinis.on('minis.share', (share) => {
	TTMinis.share((res) => {
	  if (res.error) {
      console.error('share failed', res.error);
      return;
    }
		console.log('share success');
	}, {
		title: ‘分享标题’,
		desc: ‘分享描述’,
		imageUrl: ‘分享图片地址’,
		query: {
			// 分享透传参数
		}
	})
})

TTMinis.share() 需要与 TTMinis.on(‘minis.share’, callback) 配合使用。TikTok 小程序链接由监听小程序的 share 消息(由客户端用户触发)并在消息处理回调中执行 TTMinis.share() 生成。

页面截屏与关闭页面截屏
TTMinis.enableCapture((res)=> {
  console.log('enableCapture:', res.isSuccess, res.error);
});

TTMinis.disableCapture((res)=> {
  console.log('disableCapture:', res.isSuccess, res.error);
});

卸载命令行界面工具

npm uninstall -g tiktok-minis-cli

参考文档

Tiktok开发者官网:
https://developers.tiktok.com/
Tiktok官方文档:
https://developers.tiktok.com/doc/overview

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐