返回笔记首页

Taro 深度应用完整指南

主题配置

一、React/Vue 跨端

1.1 技术实现方案

Taro 支持使用 React 或 Vue 语法开发跨端应用,通过编译时转换实现多端运行。

核心原理

plain
React/Vue 源码
    ↓
Taro 编译器 (Webpack/Vite)
    ↓
各平台代码 (小程序/H5/RN)
    ↓
Runtime 适配层
    ↓
目标平台运行

1.2 可运行代码示例

示例1: Vue3 跨端组件开发

vue
<!-- src/pages/index/index.vue -->
<template>
  <view class="container">
    <!-- 顶部导航 -->
    <view class="navbar">
      <text class="navbar-title">Taro Vue3 示例</text>
    </view>

    <!-- 功能卡片 -->
    <view class="card-list">
      <view
        v-for="item in features"
        :key="item.id"
        class="card"
        @tap="handleCardClick(item)"
      >
        <view class="card-icon">{{ item.icon }}</view>
        <view class="card-content">
          <text class="card-title">{{ item.title }}</text>
          <text class="card-desc">{{ item.desc }}</text>
        </view>
        <view class="card-arrow">›</view>
      </view>
    </view>

    <!-- 计数器示例 -->
    <view class="counter-section">
      <text class="section-title">计数器示例</text>
      <view class="counter">
        <button class="counter-btn" @tap="decrement">-</button>
        <text class="counter-value">{{ count }}</text>
        <button class="counter-btn" @tap="increment">+</button>
      </view>
      <text class="counter-info">当前计数: {{ count }}</text>
    </view>

    <!-- API 测试 -->
    <view class="api-section">
      <text class="section-title">API 测试</text>
      <button class="api-btn" @tap="testStorage">测试存储</button>
      <button class="api-btn" @tap="testRequest">测试请求</button>
      <button class="api-btn" @tap="testNavigation">测试路由</button>
      <view v-if="apiResult" class="api-result">
        <text>{{ apiResult }}</text>
      </view>
    </view>
  </view>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import Taro from '@tarojs/taro'

// 响应式数据
const count = ref(0)
const apiResult = ref('')

const features = ref([
  {
    id: 1,
    icon: '📱',
    title: '跨端开发',
    desc: '一套代码多端运行'
  },
  {
    id: 2,
    icon: '⚡',
    title: '高性能',
    desc: '原生级别的性能体验'
  },
  {
    id: 3,
    icon: '🎨',
    title: '组件化',
    desc: '完善的组件化方案'
  },
  {
    id: 4,
    icon: '🔧',
    title: '工具链',
    desc: '完整的开发工具链'
  }
])

// 生命周期
onMounted(() => {
  console.log('页面加载完成')
  loadUserData()
})

// 加载用户数据
const loadUserData = async () => {
  try {
    const data = await Taro.getStorage({ key: 'userData' })
    console.log('用户数据:', data)
  } catch (err) {
    console.log('暂无缓存数据')
  }
}

// 计数器方法
const increment = () => {
  count.value++
  Taro.showToast({
    title: `当前: ${count.value}`,
    icon: 'none',
    duration: 1000
  })
}

const decrement = () => {
  count.value--
  Taro.showToast({
    title: `当前: ${count.value}`,
    icon: 'none',
    duration: 1000
  })
}

// 卡片点击
const handleCardClick = (item) => {
  Taro.showModal({
    title: item.title,
    content: item.desc,
    showCancel: false
  })
}

// 测试存储 API
const testStorage = async () => {
  try {
    await Taro.setStorage({
      key: 'testData',
      data: {
        name: 'Taro',
        version: '3.x',
        timestamp: Date.now()
      }
    })

    const result = await Taro.getStorage({ key: 'testData' })
    apiResult.value = `存储成功: ${JSON.stringify(result.data)}`

    Taro.showToast({
      title: '存储测试成功',
      icon: 'success'
    })
  } catch (err) {
    apiResult.value = `存储失败: ${err.errMsg}`
  }
}

// 测试请求 API
const testRequest = async () => {
  try {
    Taro.showLoading({ title: '请求中...' })

    const res = await Taro.request({
      url: 'https://api.github.com/users/tarojs',
      method: 'GET'
    })

    Taro.hideLoading()

    apiResult.value = `请求成功: ${res.data.name}`

    Taro.showToast({
      title: '请求成功',
      icon: 'success'
    })
  } catch (err) {
    Taro.hideLoading()
    apiResult.value = `请求失败: ${err.errMsg}`

    Taro.showToast({
      title: '请求失败',
      icon: 'none'
    })
  }
}

// 测试路由导航
const testNavigation = () => {
  Taro.navigateTo({
    url: '/pages/detail/index?id=123&name=test'
  })
}
</script>

<style>
.container {
  min-height: 100vh;
  background: linear-gradient(180deg, #f5f7fa 0%, #fff 100%);
  padding: 20px;
}

.navbar {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  padding: 40px 20px 20px;
  border-radius: 16px;
  margin-bottom: 20px;
  box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
}

.navbar-title {
  color: #fff;
  font-size: 36px;
  font-weight: bold;
  text-align: center;
  display: block;
}

.card-list {
  margin-bottom: 30px;
}

.card {
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  margin-bottom: 15px;
  display: flex;
  align-items: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.06);
  transition: all 0.3s;
}

.card:active {
  transform: scale(0.98);
  box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}

.card-icon {
  font-size: 40px;
  margin-right: 15px;
}

.card-content {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.card-title {
  font-size: 32px;
  font-weight: bold;
  color: #333;
  margin-bottom: 8px;
}

.card-desc {
  font-size: 28px;
  color: #999;
}

.card-arrow {
  font-size: 48px;
  color: #ddd;
}

.counter-section,
.api-section {
  background: #fff;
  border-radius: 12px;
  padding: 25px;
  margin-bottom: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}

.section-title {
  font-size: 32px;
  font-weight: bold;
  color: #333;
  margin-bottom: 20px;
  display: block;
}

.counter {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-bottom: 20px;
}

.counter-btn {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
  font-size: 40px;
  font-weight: bold;
  display: flex;
  justify-content: center;
  align-items: center;
  border: none;
}

.counter-value {
  font-size: 48px;
  font-weight: bold;
  color: #333;
  margin: 0 40px;
  min-width: 80px;
  text-align: center;
}

.counter-info {
  text-align: center;
  font-size: 28px;
  color: #666;
  display: block;
}

.api-btn {
  width: 100%;
  height: 80px;
  background: #fff;
  border: 2px solid #667eea;
  color: #667eea;
  border-radius: 12px;
  font-size: 28px;
  margin-bottom: 15px;
}

.api-btn:active {
  background: #f0f4ff;
}

.api-result {
  background: #f0f4ff;
  border-radius: 8px;
  padding: 20px;
  margin-top: 15px;
}

.api-result text {
  font-size: 24px;
  color: #667eea;
  word-break: break-all;
}
</style>

示例2: 自定义 Hook 封装

javascript
// src/hooks/useRequest.js
import { ref } from 'vue'
import Taro from '@tarojs/taro'

/**
 * 请求 Hook
 * 封装通用的请求逻辑
 */
export function useRequest(options = {}) {
  const loading = ref(false)
  const error = ref(null)
  const data = ref(null)

  const request = async (config) => {
    loading.value = true
    error.value = null

    try {
      const res = await Taro.request({
        ...options,
        ...config
      })

      data.value = res.data
      return res.data
    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }

  return {
    loading,
    error,
    data,
    request
  }
}

// src/hooks/useStorage.js
import { ref, watch } from 'vue'
import Taro from '@tarojs/taro'

/**
 * 存储 Hook
 * 自动同步到本地存储
 */
export function useStorage(key, defaultValue) {
  const value = ref(defaultValue)
  const loading = ref(true)

  // 初始化时读取存储
  const init = async () => {
    try {
      const res = await Taro.getStorage({ key })
      value.value = res.data
    } catch (err) {
      value.value = defaultValue
    } finally {
      loading.value = false
    }
  }

  init()

  // 监听变化,自动保存
  watch(value, async (newValue) => {
    try {
      await Taro.setStorage({
        key,
        data: newValue
      })
    } catch (err) {
      console.error('保存失败:', err)
    }
  }, { deep: true })

  const remove = async () => {
    try {
      await Taro.removeStorage({ key })
      value.value = defaultValue
    } catch (err) {
      console.error('删除失败:', err)
    }
  }

  return {
    value,
    loading,
    remove
  }
}

// src/hooks/useShare.js
import { onShareAppMessage, onShareTimeline } from '@tarojs/taro'

/**
 * 分享 Hook
 * 统一处理分享逻辑
 */
export function useShare(options = {}) {
  const defaultOptions = {
    title: '分享标题',
    path: '/pages/index/index',
    imageUrl: ''
  }

  const shareOptions = { ...defaultOptions, ...options }

  // 分享给好友
  onShareAppMessage(() => {
    return {
      title: shareOptions.title,
      path: shareOptions.path,
      imageUrl: shareOptions.imageUrl
    }
  })

  // 分享到朋友圈
  onShareTimeline(() => {
    return {
      title: shareOptions.title,
      query: '',
      imageUrl: shareOptions.imageUrl
    }
  })
}

示例3: 使用 Hook 的实战页面

vue
<!-- src/pages/hooks-demo/index.vue -->
<template>
  <view class="page">
    <view class="section">
      <text class="section-title">请求 Hook 示例</text>

      <button class="btn" @tap="fetchData" :disabled="loading">
        {{ loading ? '加载中...' : '获取数据' }}
      </button>

      <view v-if="error" class="error-box">
        <text>请求失败: {{ error.errMsg }}</text>
      </view>

      <view v-if="data" class="data-box">
        <text>{{ JSON.stringify(data, null, 2) }}</text>
      </view>
    </view>

    <view class="section">
      <text class="section-title">存储 Hook 示例</text>

      <input
        v-model="userName.value"
        class="input"
        placeholder="输入用户名(自动保存)"
      />

      <input
        v-model.number="userAge.value"
        class="input"
        type="number"
        placeholder="输入年龄(自动保存)"
      />

      <text class="info">用户名: {{ userName.value || '未设置' }}</text>
      <text class="info">年龄: {{ userAge.value || '未设置' }}</text>

      <button class="btn-danger" @tap="clearStorage">清除存储</button>
    </view>
  </view>
</template>

<script setup>
import { useRequest } from '@/hooks/useRequest'
import { useStorage } from '@/hooks/useStorage'
import { useShare } from '@/hooks/useShare'

// 使用请求 Hook
const { loading, error, data, request } = useRequest({
  url: 'https://api.github.com/users/tarojs',
  method: 'GET'
})

const fetchData = async () => {
  try {
    await request()
  } catch (err) {
    console.error('请求失败:', err)
  }
}

// 使用存储 Hook
const userName = useStorage('userName', '')
const userAge = useStorage('userAge', 0)

const clearStorage = () => {
  userName.remove()
  userAge.remove()
}

// 使用分享 Hook
useShare({
  title: 'Taro Hook 示例',
  path: '/pages/hooks-demo/index'
})
</script>

<style>
.page {
  min-height: 100vh;
  background: #f5f5f5;
  padding: 20px;
}

.section {
  background: #fff;
  border-radius: 12px;
  padding: 25px;
  margin-bottom: 20px;
}

.section-title {
  font-size: 32px;
  font-weight: bold;
  color: #333;
  margin-bottom: 20px;
  display: block;
}

.btn,
.btn-danger {
  width: 100%;
  height: 80px;
  border-radius: 12px;
  font-size: 28px;
  border: none;
  margin-bottom: 15px;
}

.btn {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
}

.btn[disabled] {
  opacity: 0.6;
}

.btn-danger {
  background: #ee0a24;
  color: #fff;
}

.error-box {
  background: #fff1f0;
  border: 1px solid #ffa39e;
  border-radius: 8px;
  padding: 20px;
  margin-top: 15px;
}

.error-box text {
  color: #cf1322;
  font-size: 24px;
}

.data-box {
  background: #f0f9ff;
  border-radius: 8px;
  padding: 20px;
  margin-top: 15px;
  max-height: 400px;
  overflow-y: auto;
}

.data-box text {
  color: #0369a1;
  font-size: 24px;
  font-family: monospace;
  word-break: break-all;
  white-space: pre-wrap;
}

.input {
  width: 100%;
  height: 80px;
  background: #f5f5f5;
  border-radius: 8px;
  padding: 0 20px;
  font-size: 28px;
  margin-bottom: 15px;
}

.info {
  display: block;
  padding: 15px;
  background: #f5f5f5;
  border-radius: 8px;
  font-size: 28px;
  color: #666;
  margin-bottom: 15px;
}
</style>

二、编译原理解析

2.1 技术实现方案

Taro 的编译流程分为三个阶段:

编译流程

plain
1. Parse (解析)
   ├─ 使用 Babel 解析源码生成 AST
   └─ 识别组件、API、生命周期等

2. Transform (转换)
   ├─ 遍历 AST 进行转换
   ├─ 处理 JSX/Template 语法
   ├─ 适配不同平台 API
   └─ 优化性能

3. Generate (生成)
   ├─ 根据目标平台生成代码
   ├─ 小程序: wxml/wxss/js/json
   ├─ H5: html/css/js
   └─ RN: jsx

2.2 编译配置示例

config/index.js 完整配置

javascript
// config/index.js
const path = require('path')

const config = {
  // 项目名称
  projectName: 'taro-app',

  // 设计稿尺寸
  designWidth: 750,

  // 设计稿单位换算规则
  deviceRatio: {
    640: 2.34 / 2,
    750: 1,
    828: 1.81 / 2
  },

  // 源码目录
  sourceRoot: 'src',

  // 输出目录
  outputRoot: 'dist',

  // 编译插件
  plugins: [
    // 引入插件
    '@tarojs/plugin-html'
  ],

  // 全局变量
  defineConstants: {
    API_BASE_URL: JSON.stringify(process.env.API_BASE_URL || 'https://api.example.com')
  },

  // 文件 copy 配置
  copy: {
    patterns: [
      { from: 'src/assets/', to: 'dist/assets/' }
    ],
    options: {}
  },

  // 框架配置
  framework: 'vue3',

  // 编译器配置
  compiler: {
    type: 'webpack5',
    prebundle: {
      enable: false
    }
  },

  // 缓存配置
  cache: {
    enable: true
  },

  // 小程序配置
  mini: {
    // 压缩
    minifyXML: {
      collapseWhitespace: true
    },

    // 公共文件
    commonChunks: ['runtime', 'vendors', 'common'],

    // 添加 chunk
    addChunkPages(pages, pagesNames) {
      // 可以自定义分包逻辑
    },

    // 优化主包大小
    optimizeMainPackage: {
      enable: true,
      exclude: [/node_modules/]
    },

    // postcss 配置
    postcss: {
      pxtransform: {
        enable: true,
        config: {
          selectorBlackList: [/^\.adm-/, /^\.am-/] // 跳过某些类名
        }
      },
      url: {
        enable: true,
        config: {
          limit: 10240 // 小于 10kb 转 base64
        }
      },
      cssModules: {
        enable: false,
        config: {
          namingPattern: 'module',
          generateScopedName: '[name]__[local]___[hash:base64:5]'
        }
      }
    },

    // Webpack 配置
    webpackChain(chain) {
      // 修改 loader
      chain.merge({
        module: {
          rule: {
            myloader: {
              test: /\.txt$/,
              use: ['raw-loader']
            }
          }
        }
      })

      // 添加别名
      chain.resolve.alias
        .set('@', path.resolve(__dirname, '..', 'src'))
        .set('@components', path.resolve(__dirname, '..', 'src/components'))
        .set('@utils', path.resolve(__dirname, '..', 'src/utils'))

      // 添加插件
      chain.plugin('analyzer')
        .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [{
          analyzerMode: 'static',
          openAnalyzer: false
        }])
    }
  },

  // H5 配置
  h5: {
    publicPath: '/',
    staticDirectory: 'static',

    // 路由配置
    router: {
      mode: 'hash', // hash 或 browser
      basename: '/'
    },

    // 入口文件
    entry: {
      app: ['./src/app.js']
    },

    // 输出配置
    output: {
      filename: 'js/[name].[hash:8].js',
      chunkFilename: 'js/[name].[chunkhash:8].js'
    },

    // 静态资源
    imageUrlLoaderOption: {
      limit: 5000,
      name: 'static/images/[name].[hash:8].[ext]'
    },

    // postcss 配置
    postcss: {
      autoprefixer: {
        enable: true,
        config: {}
      }
    },

    // devServer
    devServer: {
      port: 10086,
      host: '0.0.0.0',
      hot: true
    },

    // Webpack 配置
    webpackChain(chain) {
      // H5 特定配置
      chain.resolve.alias
        .set('@tarojs/components$', '@tarojs/components/dist-h5/vue3/index.js')
    }
  },

  // RN 配置
  rn: {
    appName: 'TaroApp',
    output: {
      ios: 'ios/main.jsbundle',
      android: 'android/app/src/main/assets/index.android.bundle'
    },

    postcss: {
      cssModules: {
        enable: false
      }
    }
  }
}

// 开发环境配置
if (process.env.NODE_ENV === 'development') {
  config.mini.debugReact = true
  config.h5.devServer.proxy = {
    '/api': {
      target: 'http://localhost:3000',
      changeOrigin: true,
      pathRewrite: {
        '^/api': ''
      }
    }
  }
}

// 生产环境配置
if (process.env.NODE_ENV === 'production') {
  // 压缩配置
  config.mini.minifyXML = {
    collapseWhitespace: true
  }

  // CDN 配置
  config.h5.publicPath = 'https://cdn.example.com/'
}

module.exports = function (merge) {
  if (process.env.NODE_ENV === 'development') {
    return merge({}, config, require('./dev'))
  }
  return merge({}, config, require('./prod'))
}

三、自定义编译插件

3.1 技术实现方案

Taro 支持自定义编译插件,可以在编译过程中注入自定义逻辑。

3.2 可运行代码示例

示例1: 自定义编译插件

javascript
// plugins/taro-plugin-custom.js

/**
 * 自定义 Taro 编译插件
 * 实现功能:
 * 1. 自动注入环境变量
 * 2. 代码压缩优化
 * 3. 资源路径转换
 */
module.exports = (ctx, options) => {
  // ctx: 编译上下文
  // options: 插件配置选项

  console.log('自定义插件启动:', options)

  // 监听编译事件
  ctx.onBuildStart(() => {
    console.log('编译开始...')
  })

  ctx.onBuildFinish(() => {
    console.log('编译完成!')
  })

  // 修改 Webpack 配置
  ctx.modifyWebpackChain(({ chain }) => {
    // 添加全局变量
    chain.plugin('define')
      .tap(args => {
        args[0] = {
          ...args[0],
          'process.env.BUILD_TIME': JSON.stringify(new Date().toISOString()),
          'process.env.PLUGIN_VERSION': JSON.stringify(options.version || '1.0.0')
        }
        return args
      })

    // 添加自定义 loader
    chain.module
      .rule('custom-loader')
      .test(/\.custom$/)
      .use('custom-loader')
      .loader(require.resolve('./custom-loader'))
      .options(options.loaderOptions || {})
  })

  // 修改编译配置
  ctx.modifyBuildAssets(({ assets }) => {
    // 可以修改输出的资源
    Object.keys(assets).forEach(key => {
      if (key.endsWith('.js')) {
        // 在 JS 文件开头添加注释
        const banner = `/* Build Time: ${new Date().toISOString()} */\n`
        assets[key] = banner + assets[key]
      }
    })
  })

  // 修改小程序配置
  ctx.modifyMiniConfigs(({ configMap }) => {
    // 修改 app.json
    if (configMap.app) {
      configMap.app.window = {
        ...configMap.app.window,
        // 自定义窗口配置
        navigationBarBackgroundColor: options.themeColor || '#667eea'
      }
    }
  })

  // 注册命令
  ctx.registerCommand({
    name: 'custom',
    description: '自定义命令',
    fn() {
      console.log('执行自定义命令')
      // 自定义命令逻辑
    }
  })

  // 注册方法
  ctx.registerMethod({
    name: 'customMethod',
    fn(arg) {
      console.log('自定义方法:', arg)
      // 返回处理结果
      return `Processed: ${arg}`
    }
  })
}

// 插件配置 schema
module.exports.schema = {
  type: 'object',
  properties: {
    version: {
      type: 'string',
      description: '插件版本'
    },
    themeColor: {
      type: 'string',
      description: '主题颜色'
    },
    loaderOptions: {
      type: 'object',
      description: 'Loader 配置'
    }
  }
}

示例2: 自动路由插件

javascript
// plugins/taro-plugin-auto-route.js
const fs = require('fs')
const path = require('path')

/**
 * 自动路由插件
 * 根据 pages 目录自动生成路由配置
 */
module.exports = (ctx) => {
  ctx.onBuildStart(() => {
    const pagesDir = path.join(ctx.paths.sourcePath, 'pages')

    // 扫描 pages 目录
    const routes = scanPages(pagesDir)

    // 生成路由配置
    const routeConfig = generateRouteConfig(routes)

    // 写入配置文件
    const configPath = path.join(ctx.paths.sourcePath, 'app.config.js')
    updateAppConfig(configPath, routeConfig)

    console.log(`自动生成了 ${routes.length} 个路由`)
  })
}

// 扫描页面目录
function scanPages(dir, basePath = '') {
  const routes = []
  const files = fs.readdirSync(dir)

  files.forEach(file => {
    const fullPath = path.join(dir, file)
    const stat = fs.statSync(fullPath)

    if (stat.isDirectory()) {
      // 递归扫描子目录
      const subRoutes = scanPages(fullPath, path.join(basePath, file))
      routes.push(...subRoutes)
    } else if (file === 'index.vue' || file === 'index.jsx') {
      // 找到页面入口文件
      const routePath = basePath ? `pages/${basePath}/index` : 'pages/index/index'
      routes.push({
        path: routePath,
        name: basePath.replace(/\//g, '-') || 'index'
      })
    }
  })

  return routes
}

// 生成路由配置
function generateRouteConfig(routes) {
  return {
    pages: routes.map(route => route.path),
    subPackages: [] // 可以自动识别分包
  }
}

// 更新 app.config.js
function updateAppConfig(configPath, routeConfig) {
  if (fs.existsSync(configPath)) {
    let content = fs.readFileSync(configPath, 'utf-8')

    // 简单的字符串替换,实际项目中建议使用 AST
    content = content.replace(
      /pages:\s*\[[\s\S]*?\]/,
      `pages: ${JSON.stringify(routeConfig.pages, null, 2)}`
    )

    fs.writeFileSync(configPath, content)
  }
}

四、多端差异抹平

4.1 技术实现方案

通过统一的 API 层和条件编译,抹平不同平台的差异。

4.2 可运行代码示例

示例1: 统一 API 封装

javascript
// src/utils/platform.js

/**
 * 平台差异抹平工具类
 */
import Taro from '@tarojs/taro'

class PlatformAdapter {
  constructor() {
    this.env = process.env.TARO_ENV
  }

  /**
   * 获取系统信息
   */
  async getSystemInfo() {
    try {
      const info = await Taro.getSystemInfo()

      return {
        platform: info.platform,
        system: info.system,
        version: info.version,
        model: info.model,
        brand: info.brand,
        screenWidth: info.screenWidth,
        screenHeight: info.screenHeight,
        windowWidth: info.windowWidth,
        windowHeight: info.windowHeight,
        statusBarHeight: info.statusBarHeight,
        safeArea: info.safeArea,
        // 统一字段名
        pixelRatio: info.pixelRatio || info.devicePixelRatio
      }
    } catch (err) {
      console.error('获取系统信息失败:', err)
      return null
    }
  }

  /**
   * 选择图片 - 多端适配
   */
  async chooseImage(options = {}) {
    const defaultOptions = {
      count: 1,
      sizeType: ['compressed'],
      sourceType: ['album', 'camera']
    }

    const config = { ...defaultOptions, ...options }

    try {
      if (this.env === 'h5') {
        // H5 端使用 input file
        return await this._chooseImageH5(config)
      } else {
        // 小程序和 APP 端
        const res = await Taro.chooseImage(config)
        return {
          tempFilePaths: res.tempFilePaths,
          tempFiles: res.tempFiles
        }
      }
    } catch (err) {
      console.error('选择图片失败:', err)
      throw err
    }
  }

  /**
   * H5 选择图片实现
   */
  _chooseImageH5(config) {
    return new Promise((resolve, reject) => {
      const input = document.createElement('input')
      input.type = 'file'
      input.accept = 'image/*'
      input.multiple = config.count > 1

      input.onchange = (e) => {
        const files = Array.from(e.target.files)
        const tempFilePaths = []
        const tempFiles = []

        let completed = 0

        files.forEach((file, index) => {
          const reader = new FileReader()

          reader.onload = (event) => {
            tempFilePaths[index] = event.target.result
            tempFiles[index] = {
              path: event.target.result,
              size: file.size,
              type: file.type
            }

            completed++

            if (completed === files.length) {
              resolve({ tempFilePaths, tempFiles })
            }
          }

          reader.onerror = () => {
            reject(new Error('读取文件失败'))
          }

          reader.readAsDataURL(file)
        })
      }

      input.click()
    })
  }

  /**
   * 获取位置 - 多端适配
   */
  async getLocation(type = 'gcj02') {
    try {
      if (this.env === 'h5') {
        // H5 使用浏览器 API
        return await this._getLocationH5()
      } else {
        // 小程序和 APP
        const res = await Taro.getLocation({ type })
        return {
          latitude: res.latitude,
          longitude: res.longitude,
          accuracy: res.accuracy,
          speed: res.speed,
          altitude: res.altitude
        }
      }
    } catch (err) {
      console.error('获取位置失败:', err)
      throw err
    }
  }

  /**
   * H5 获取位置实现
   */
  _getLocationH5() {
    return new Promise((resolve, reject) => {
      if (!navigator.geolocation) {
        reject(new Error('浏览器不支持地理位置'))
        return
      }

      navigator.geolocation.getCurrentPosition(
        (position) => {
          resolve({
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
            accuracy: position.coords.accuracy,
            speed: position.coords.speed,
            altitude: position.coords.altitude
          })
        },
        (error) => {
          reject(error)
        },
        {
          enableHighAccuracy: true,
          timeout: 10000,
          maximumAge: 0
        }
      )
    })
  }

  /**
   * 拨打电话 - 多端适配
   */
  async makePhoneCall(phoneNumber) {
    try {
      if (this.env === 'h5') {
        window.location.href = `tel:${phoneNumber}`
      } else {
        await Taro.makePhoneCall({ phoneNumber })
      }
    } catch (err) {
      console.error('拨打电话失败:', err)
      throw err
    }
  }

  /**
   * 扫码 - 多端适配
   */
  async scanCode() {
    try {
      if (this.env === 'h5') {
        throw new Error('H5 暂不支持扫码功能')
      }

      const res = await Taro.scanCode({
        scanType: ['qrCode', 'barCode']
      })

      return {
        result: res.result,
        scanType: res.scanType
      }
    } catch (err) {
      console.error('扫码失败:', err)
      throw err
    }
  }

  /**
   * 设置导航栏标题 - 多端适配
   */
  async setNavigationBarTitle(title) {
    try {
      if (this.env === 'h5') {
        document.title = title
      } else {
        await Taro.setNavigationBarTitle({ title })
      }
    } catch (err) {
      console.error('设置标题失败:', err)
    }
  }

  /**
   * 获取网络类型 - 多端适配
   */
  async getNetworkType() {
    try {
      if (this.env === 'h5') {
        // H5 使用 Network Information API
        if (navigator.connection) {
          return {
            networkType: navigator.connection.effectiveType,
            isConnected: navigator.onLine
          }
        }
        return {
          networkType: 'unknown',
          isConnected: navigator.onLine
        }
      } else {
        const res = await Taro.getNetworkType()
        return {
          networkType: res.networkType,
          isConnected: res.networkType !== 'none'
        }
      }
    } catch (err) {
      console.error('获取网络类型失败:', err)
      return {
        networkType: 'unknown',
        isConnected: true
      }
    }
  }

  /**
   * 震动反馈 - 多端适配
   */
  async vibrate(duration = 15) {
    try {
      if (this.env === 'h5') {
        if (navigator.vibrate) {
          navigator.vibrate(duration)
        }
      } else {
        if (duration <= 15) {
          await Taro.vibrateShort({ type: 'light' })
        } else {
          await Taro.vibrateLong()
        }
      }
    } catch (err) {
      console.error('震动失败:', err)
    }
  }
}

export default new PlatformAdapter()

示例2: 使用平台适配器

vue
<!-- src/pages/adapter-demo/index.vue -->
<template>
  <view class="page">
    <view class="info-card">
      <text class="card-title">当前平台: {{ platformInfo.platform }}</text>
      <text class="card-info">系统: {{ platformInfo.system }}</text>
      <text class="card-info">型号: {{ platformInfo.model }}</text>
      <text class="card-info">屏幕: {{ platformInfo.screenWidth }} x {{ platformInfo.screenHeight }}</text>
    </view>

    <view class="button-list">
      <button class="demo-btn" @tap="testChooseImage">选择图片</button>
      <button class="demo-btn" @tap="testGetLocation">获取位置</button>
      <button class="demo-btn" @tap="testMakePhoneCall">拨打电话</button>
      <button class="demo-btn" @tap="testScanCode">扫码</button>
      <button class="demo-btn" @tap="testVibrate">震动</button>
    </view>

    <view v-if="result" class="result-card">
      <text class="result-title">操作结果:</text>
      <text class="result-text">{{ result }}</text>
    </view>
  </view>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import platform from '@/utils/platform'

const platformInfo = ref({})
const result = ref('')

onMounted(async () => {
  const info = await platform.getSystemInfo()
  platformInfo.value = info
})

const testChooseImage = async () => {
  try {
    const res = await platform.chooseImage({ count: 1 })
    result.value = `选择了 ${res.tempFilePaths.length} 张图片`

    Taro.showToast({
      title: '选择成功',
      icon: 'success'
    })
  } catch (err) {
    result.value = `选择失败: ${err.message}`
  }
}

const testGetLocation = async () => {
  try {
    const location = await platform.getLocation()
    result.value = `纬度: ${location.latitude}, 经度: ${location.longitude}`

    Taro.showToast({
      title: '获取成功',
      icon: 'success'
    })
  } catch (err) {
    result.value = `获取失败: ${err.message}`
  }
}

const testMakePhoneCall = async () => {
  try {
    await platform.makePhoneCall('10086')
    result.value = '拨打电话: 10086'
  } catch (err) {
    result.value = `拨打失败: ${err.message}`
  }
}

const testScanCode = async () => {
  try {
    const res = await platform.scanCode()
    result.value = `扫码结果: ${res.result}`

    Taro.showToast({
      title: '扫码成功',
      icon: 'success'
    })
  } catch (err) {
    result.value = `扫码失败: ${err.message}`

    Taro.showToast({
      title: err.message,
      icon: 'none'
    })
  }
}

const testVibrate = async () => {
  await platform.vibrate(30)
  result.value = '震动执行完成'
}
</script>

<style>
.page {
  min-height: 100vh;
  background: #f5f5f5;
  padding: 20px;
}

.info-card,
.result-card {
  background: #fff;
  border-radius: 12px;
  padding: 25px;
  margin-bottom: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}

.card-title,
.result-title {
  font-size: 32px;
  font-weight: bold;
  color: #333;
  margin-bottom: 15px;
  display: block;
}

.card-info {
  display: block;
  font-size: 28px;
  color: #666;
  margin-bottom: 10px;
}

.button-list {
  display: flex;
  flex-direction: column;
}

.demo-btn {
  height: 80px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
  border-radius: 12px;
  font-size: 28px;
  margin-bottom: 15px;
  border: none;
}

.result-text {
  font-size: 28px;
  color: #666;
  word-break: break-all;
  line-height: 1.6;
}
</style>

五、性能优化实践

5.1 技术实现方案

Taro 性能优化主要从以下几个方面入手:

优化策略

plain
1. 编译优化
   ├─ 代码分割
   ├─ Tree Shaking
   └─ 压缩混淆

2. 运行时优化
   ├─ 虚拟列表
   ├─ 图片懒加载
   └─ 防抖节流

3. 包体积优化
   ├─ 按需引入
   ├─ 分包加载
   └─ 资源压缩

5.2 可运行代码示例

示例1: 虚拟列表优化

vue
<!-- src/components/virtual-list/index.vue -->
<template>
  <scroll-view
    class="virtual-list"
    :scroll-y="true"
    :style="{ height: containerHeight + 'px' }"
    :scroll-top="scrollTop"
    @scroll="handleScroll"
  >
    <!-- 占位容器 -->
    <view :style="{ height: totalHeight + 'px', position: 'relative' }">
      <!-- 可见区域 -->
      <view
        :style="{
          transform: `translateY(${offsetY}px)`,
          position: 'absolute',
          left: 0,
          right: 0,
          top: 0
        }"
      >
        <view
          v-for="item in visibleData"
          :key="item.id"
          class="list-item"
          :style="{ height: itemHeight + 'px' }"
        >
          <slot :item="item"></slot>
        </view>
      </view>
    </view>
  </scroll-view>
</template>

<script setup>
import { ref, computed, watch } from 'vue'

const props = defineProps({
  data: {
    type: Array,
    default: () => []
  },
  itemHeight: {
    type: Number,
    default: 100
  },
  containerHeight: {
    type: Number,
    default: 600
  },
  bufferSize: {
    type: Number,
    default: 5
  }
})

const scrollTop = ref(0)
const currentScrollTop = ref(0)

// 计算总高度
const totalHeight = computed(() => {
  return props.data.length * props.itemHeight
})

// 计算可见数量
const visibleCount = computed(() => {
  return Math.ceil(props.containerHeight / props.itemHeight)
})

// 计算起始索引
const startIndex = computed(() => {
  const index = Math.floor(currentScrollTop.value / props.itemHeight)
  return Math.max(0, index - props.bufferSize)
})

// 计算结束索引
const endIndex = computed(() => {
  return Math.min(
    props.data.length,
    startIndex.value + visibleCount.value + props.bufferSize * 2
  )
})

// 计算偏移量
const offsetY = computed(() => {
  return startIndex.value * props.itemHeight
})

// 计算可见数据
const visibleData = computed(() => {
  return props.data.slice(startIndex.value, endIndex.value)
})

// 处理滚动
const handleScroll = (e) => {
  currentScrollTop.value = e.detail.scrollTop
}
</script>

<style scoped>
.virtual-list {
  position: relative;
  overflow: hidden;
}

.list-item {
  width: 100%;
}
</style>

示例2: 图片懒加载组件

vue
<!-- src/components/lazy-image/index.vue -->
<template>
  <view class="lazy-image" :style="{ width, height }">
    <image
      v-if="shouldLoad"
      :src="realSrc"
      :mode="mode"
      :lazy-load="true"
      class="image"
      @load="handleLoad"
      @error="handleError"
    />
    <view v-else class="placeholder">
      <text class="placeholder-text">{{ placeholder }}</text>
    </view>

    <view v-if="loading" class="loading">
      <text>加载中...</text>
    </view>

    <view v-if="error" class="error" @tap="retry">
      <text>加载失败,点击重试</text>
    </view>
  </view>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import Taro from '@tarojs/taro'

const props = defineProps({
  src: {
    type: String,
    required: true
  },
  width: {
    type: String,
    default: '100%'
  },
  height: {
    type: String,
    default: '200px'
  },
  mode: {
    type: String,
    default: 'aspectFill'
  },
  placeholder: {
    type: String,
    default: '图片加载中'
  },
  threshold: {
    type: Number,
    default: 100
  }
})

const shouldLoad = ref(false)
const loading = ref(false)
const error = ref(false)
const observer = ref(null)

const realSrc = computed(() => {
  return shouldLoad.value ? props.src : ''
})

onMounted(() => {
  initObserver()
})

onUnmounted(() => {
  if (observer.value) {
    observer.value.disconnect()
  }
})

// 初始化 IntersectionObserver
const initObserver = () => {
  observer.value = Taro.createIntersectionObserver()

  observer.value
    .relativeToViewport({ bottom: props.threshold })
    .observe('.lazy-image', (res) => {
      if (res.intersectionRatio > 0 && !shouldLoad.value) {
        shouldLoad.value = true
        loading.value = true
      }
    })
}

const handleLoad = () => {
  loading.value = false
  error.value = false
}

const handleError = () => {
  loading.value = false
  error.value = true
}

const retry = () => {
  error.value = false
  loading.value = true
  shouldLoad.value = false

  setTimeout(() => {
    shouldLoad.value = true
  }, 100)
}
</script>

<style scoped>
.lazy-image {
  position: relative;
  overflow: hidden;
  background: #f5f5f5;
}

.image {
  width: 100%;
  height: 100%;
}

.placeholder,
.loading,
.error {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f5f5f5;
}

.placeholder-text {
  font-size: 24px;
  color: #999;
}

.loading text,
.error text {
  font-size: 24px;
  color: #666;
}
</style>

示例3: 性能监控工具

javascript
// src/utils/performance.js

/**
 * 性能监控工具
 */
class PerformanceMonitor {
  constructor() {
    this.marks = new Map()
    this.measures = []
  }

  /**
   * 标记时间点
   */
  mark(name) {
    this.marks.set(name, Date.now())
  }

  /**
   * 测量时间差
   */
  measure(name, startMark, endMark) {
    const startTime = this.marks.get(startMark)
    const endTime = this.marks.get(endMark) || Date.now()

    if (!startTime) {
      console.warn(`标记 ${startMark} 不存在`)
      return
    }

    const duration = endTime - startTime

    this.measures.push({
      name,
      startMark,
      endMark,
      duration,
      timestamp: Date.now()
    })

    console.log(`[性能] ${name}: ${duration}ms`)

    return duration
  }

  /**
   * 获取所有测量结果
   */
  getMeasures() {
    return this.measures
  }

  /**
   * 清除标记
   */
  clearMarks() {
    this.marks.clear()
  }

  /**
   * 清除测量
   */
  clearMeasures() {
    this.measures = []
  }

  /**
   * 监控页面性能
   */
  monitorPage(pageName) {
    this.mark(`${pageName}-start`)

    return {
      end: () => {
        this.mark(`${pageName}-end`)
        return this.measure(
          `${pageName}-load`,
          `${pageName}-start`,
          `${pageName}-end`
        )
      }
    }
  }

  /**
   * 监控 API 请求
   */
  monitorRequest(requestName) {
    this.mark(`${requestName}-start`)

    return {
      end: () => {
        this.mark(`${requestName}-end`)
        return this.measure(
          `${requestName}-request`,
          `${requestName}-start`,
          `${requestName}-end`
        )
      }
    }
  }

  /**
   * 生成性能报告
   */
  generateReport() {
    const report = {
      timestamp: Date.now(),
      measures: this.measures,
      summary: {
        totalCount: this.measures.length,
        avgDuration: this.measures.reduce((sum, m) => sum + m.duration, 0) / this.measures.length,
        slowest: this.measures.reduce((max, m) => m.duration > max.duration ? m : max, { duration: 0 }),
        fastest: this.measures.reduce((min, m) => m.duration < min.duration ? m : min, { duration: Infinity })
      }
    }

    console.log('=== 性能报告 ===')
    console.log(`总测量次数: ${report.summary.totalCount}`)
    console.log(`平均耗时: ${report.summary.avgDuration.toFixed(2)}ms`)
    console.log(`最慢操作: ${report.summary.slowest.name} (${report.summary.slowest.duration}ms)`)
    console.log(`最快操作: ${report.summary.fastest.name} (${report.summary.fastest.duration}ms)`)

    return report
  }
}

export default new PerformanceMonitor()

六、简历描述模板

6.1 项目经验描述

项目名称: 企业级 Taro 跨端应用框架 技术栈: Taro 3.x + Vue3 + Pinia + TypeScript 项目周期: 2023.08 - 2024.02 (6个月)

项目描述: 主导开发一套基于 Taro 的跨端应用框架,支持微信/支付宝小程序、H5、React Native 等多个平台。通过深度定制编译流程和运行时优化,实现了高性能的跨端开发方案。

核心职责:

  1. 设计并实现跨端架构方案,通过统一的 API 层抹平平台差异,代码复用率达 90%
  2. 开发自定义编译插件,实现自动路由生成、环境变量注入、代码压缩等功能,提升开发效率 40%
  3. 优化编译配置,实现代码分割和按需加载,首屏加载时间减少 50%,包体积减小 35%
  4. 封装虚拟列表和图片懒加载组件,长列表滚动性能提升 60%,内存占用降低 45%
  5. 建立性能监控体系,实时追踪页面加载和接口请求性能,快速定位性能瓶颈

技术亮点:

  1. 编译优化: 深度定制 Webpack 配置,实现智能代码分割。主包体积控制在 2MB以下,分包按需加载,首屏渲染时间从 3.5s 优化到 1.5s
  2. 自定义插件: 开发 5+ 个编译插件,包括自动路由生成、主题切换、多语言注入等,构建时间缩短 60%
  3. 平台适配: 设计统一的 Platform Adapter,封装 20+ 个平台差异 API,业务代码无需关心平台细节
  4. 性能监控: 实现页面级和接口级性能监控,建立性能数据看板,为优化提供数据支持

项目成果:

  • 支持 5+ 个平台发布,开发成本降低 70%
  • 核心页面性能提升 60%,用户体验显著改善
  • 建立完整的组件库和开发规范,团队开发效率提升 50%

6.2 难点与亮点话术

难点1: 编译性能优化

面试官: 你提到优化了编译性能,具体怎么做的?

回答话术: "我们项目编译一次要 5 分钟,严重影响开发效率。我从三个方面优化:

第一,分析编译耗时。用 webpack-bundle-analyzer 分析打包产物,发现有几个问题:lodash 完整引入了 70kb,moment.js 包含了所有语言包,还有一些重复的依赖。

第二,优化依赖。lodash 改用 lodash-es 按需引入,moment 换成 dayjs,体积从 200kb 降到 20kb。用 DuplicatePackageCheckerPlugin 找出重复依赖,统一版本号。

第三,优化编译配置。开启持久化缓存,第二次编译能复用缓存。配置 thread-loader 多线程编译,充分利用 CPU。exclude node_modules 减少不必要的编译。

优化后,首次编译从 5 分钟降到 2 分钟,二次编译只需 20 秒。开发体验好了很多。"

难点2: 虚拟列表实现

面试官: 虚拟列表的原理是什么?你是怎么实现的?

回答话术: "虚拟列表的核心思想是只渲染可见区域的数据,而不是全部数据。

实现原理: 第一步,计算容器可以显示多少个列表项。比如容器高度 600px,每项 100px,那就能显示 6 个。

第二步,监听滚动事件,计算当前应该显示哪些数据。比如滚动到 500px,那就是第 5-11 项。

第三步,用绝对定位把可见项放到正确位置。通过 transform 或 top 属性控制位置。

第四步,用一个空的占位容器撑开总高度,保证滚动条正常。

我的实现有几个优化:

  1. 加了缓冲区机制,上下各多渲染 5 个,滑动更流畅
  2. 使用 transform 而不是 top,性能更好
  3. 滚动事件用了节流,避免频繁计算

实际效果,1万条数据的列表,优化前卡顿严重,优化后滚动 60fps,内存占用降低 45%。"

难点3: 多端差异处理

面试官: Taro 怎么处理多端差异?你遇到过什么问题?

回答话术: "Taro 的多端差异主要体现在 API 和组件上。我遇到最大的问题是图片选择功能。

问题:

  • H5 只能用 input file,没有相机调用能力
  • 微信小程序用 chooseImage,支付宝用 my.chooseImage,参数还不一样
  • 返回的数据格式也不统一

我的解决方案: 第一步,设计统一的接口规范,定义标准的入参和出参格式。

第二步,创建 Platform Adapter,根据 process.env.TARO_ENV 判断平台,调用对应的实现。

第三步,H5 端特殊处理,用 Promise 封装 input file 的异步操作,模拟小程序的返回格式。

第四步,统一错误处理,所有平台都返回标准的错误对象。

这样业务代码只需调用 platform.chooseImage(),不用关心平台差异。我们封装了 20+ 个这样的 API,覆盖了 90% 的场景。"

难点4: 自定义编译插件

面试官: 你开发过 Taro 插件吗?有什么心得?

回答话术: "开发过,我做了一个自动路由生成插件。

背景:我们有 30+ 个页面,每次新增页面都要手动修改 app.config.js,容易出错,而且不利于团队协作。

实现思路: 第一步,在 onBuildStart 钩子里扫描 pages 目录,找到所有的 index.vue 文件。

第二步,根据目录结构生成路由配置。比如 pages/user/profile/index.vue 就生成 'pages/user/profile/index'。

第三步,自动识别分包。如果目录下有 package.json,就认为是分包,单独配置。

第四步,用 AST 修改 app.config.js,保留用户的自定义配置,只更新 pages 字段。

遇到的坑:

  1. 文件监听要注意性能,用了 chokidar 的节流功能
  2. 路径处理要考虑 Windows 和 Mac 的差异,统一用 posix path
  3. AST 操作要小心,不能破坏原有代码结构

最终效果,新增页面不用手动配置,开发效率提升明显。而且这个插件已经在团队推广,受到好评。"

七、面试常见问题 SOP

Q1: Taro 和 Uni-app 有什么区别?

标准回答: "主要区别:

技术栈:

  • Taro 支持 React 和 Vue,生态更开放
  • Uni-app 主要支持 Vue,更专注

编译方式:

  • Taro 是编译时框架,把 React/Vue 编译成小程序代码
  • Uni-app 是运行时框架,有自己的 runtime

性能:

  • Taro 编译后的代码更接近原生,性能略好
  • Uni-app 的 runtime 有一定性能损耗,但通常可以接受

开发体验:

  • Taro 更接近 Web 开发,学习曲线平缓
  • Uni-app 有自己的一套规范,需要适应

我的选择标准:

  • 如果团队擅长 React,选 Taro
  • 如果需要快速开发,功能相对简单,选 Uni-app
  • 如果对性能要求极高,选 Taro 或直接原生开发"

Q2: Taro 3 相比 Taro 2 有哪些改进?

标准回答: "Taro 3 最大的改进是采用了全新的架构:

第一,运行时方案。Taro 3 不再是完全编译,而是引入了轻量级的运行时,更接近 Web 标准。

第二,支持任意框架。理论上可以支持任何前端框架,不局限于 React 和 Vue。

第三,更好的性能。因为运行时更轻,性能反而比 Taro 2 更好。

第四,开发体验提升。支持 Fast Refresh,改代码不用重新编译,开发效率高。

第五,更完善的生态。插件系统更强大,社区更活跃。

但也有权衡:

  • 包体积会稍微大一点,因为要引入运行时
  • 某些极端优化场景不如纯编译方案

总体来说,Taro 3 是更成熟的方案,我们项目就是用的 Taro 3。"

Q3: 如何优化 Taro 应用的性能?

标准回答: "我总结了一个优化清单:

编译优化:

  1. 代码分割,配置 commonChunks
  2. Tree Shaking,去除无用代码
  3. 开启持久化缓存,提升二次编译速度
  4. 使用 thread-loader 多线程编译

运行时优化:

  1. 长列表用虚拟列表,减少 DOM 数量
  2. 图片懒加载,延迟非首屏图片
  3. 防抖节流,避免频繁操作
  4. 使用 useMemo/useCallback 避免不必要的渲染

包体积优化:

  1. 按需引入第三方库,不要全量引入
  2. 图片压缩,使用 webp 格式
  3. 分包加载,主包只放核心页面
  4. 使用 CSS Modules,避免样式冗余

监控优化:

  1. 建立性能监控,追踪关键指标
  2. 定期分析打包产物,找出优化空间
  3. AB 测试验证优化效果

我们项目按这个思路优化后,首屏时间从 3.5s 降到 1.5s,用户体验明显提升。"


说明: 这是 Taro 深度应用的完整文档,包含了编译原理、自定义插件、多端适配、性能优化等核心技术点,配有详细的代码示例和面试话术。