taro-solid我做了什么事

前情提要

在上一期版本迭代过程中,主要解决了收集项目@tarojs/components使用到的组件,忘记了的家人们,传送门回顾下吧。

近况

有一天,我看到有篇taro官方文章,主要介绍了4.x版本要发布的内容,发现他们居然准备要发布vite的特性了,在之前我就挺期待能够在taro使用vite运行,所以就打算去实现taro-solid插件的vite支持,第一件事,肯定是去看看官方仓库是怎么把各个编译器插件集成进去的,好家伙,一看不要紧,我看到了他们已经在迁移solid编译器的相关实现了,看看时间好像也就2周前,仔细看了他们solid中的实现,发现他们也跟我一开始一样,也是参考一代目的代码,自行实现了h函数。 这不就是我踩过的坑吗?一期记录,于是我就给他们提了个issue,说明了下自实行h函数在其中会存在的bug,并自告奋勇来解决这个bug,以上就是最近在做的事情。在解决这个bug的时候,也遇到了很多问题,期间也多亏了taro技术委员会@ZakaryCode大佬孜孜不倦为我答疑,以至于能够顺利完成这个pr🫡。下面是主要遇到的问题的记录。

小程序端的解决

简简单单把h函数删除咯,清清爽爽,关机~。由于我们之前的插件,书写形式是采用小程序原生的标签:

tsx 复制代码
<view class="h-xl flex flex-col">
  <view class="w-full h-90 flex flex-nowrap items-center border border-gray-4 border-solid rounded-lg my-5 px-2 box-border">
    <text class="mr-2 pr-2 border-r-style-solid border border-gray-2">
      +86
    </text>
    <input
      placeholder="请输入手机号"
      type="number"
    />
  </view>
  <button>
    登录
  </button>
</view>

而这也只能在小程序端使用,就不太符合taro跨端的理念,所以说我们还是得走引入@tarojs/components包的组件进行使用,但是究竟如何让他能够顺利编译,我想到了2个方案。

套娃

在小程序端,我们引用@tarojs/components包的组件其实都是string类型的标签,在solid编译的时候,由于babel已经解析这里是使用component编译,导致报错,那么只要将这份导出多套一层,使其成为组件,不就好了?

ts 复制代码
export const View = (props) => <view {...props}>{props.children}</view>

export const Text = (props) => <text {...props}>{props.children}</text>

从结论上来说,这是能成功编译的,不过他存在一个问题,在solid经babel转换后的源码,他会多加get children,这无论在编译体积,还是运行时性能,都会存在一定的损耗,特别是组件层级嵌套过深的时候,这个带来的影响就比较明显,而且咱们还是运行在小程序端。所以这个方案也是最低选择项。

babel

又迎来我们常驻嘉宾babel,最近老跟他打交道了属于是,在之前我们的解决策略是,在babel进行小程序组件收集,让他能够顺利编译使用原生的小程序标签。 那么我们是否可以思路反转一下,我还是使用jsx组件的引入写法,但是在babel的transformElement的时候,识别当前的节点是否是@tarojs/components里的,然后转换为小程序的原生标签,让其走createElement的分支,那么编译后的结果,从理论上来说,是跟写原生小程序标签是一致的,性能上也是最优解。但是有个问题,就是如果用户写的组件跟@tarojs/components里的一样,那岂不是编译错乱了? babel转换组件处理后编译结果:

处理流程图:

graph LR A(tsx单文件入口) --> B[收集组件] B --transformElement--> C{当前节点node是否是小程序组件} C --是--> D[改变tagName为原生标签] C --否--> E[继续往下执行] E --> Z[文件transform完成
收集组件缓存清空] D --> F[使用createElement分支创建元素] F --> Z

解决组件重名

为了解决用户使用的组件命名是跟@tarojs/components里的导出命名一致:

js 复制代码
import { Button } from '@tarojs/components'
import View from '@/components/MyView'

我们可以对import的语法进行分析,只要收集'@tarojs/components'里import进来的组件就好,在babel里也是能做到这个的,具体代码如下:

js 复制代码
visitor: {
  JSXElement: transformJSX,
  JSXFragment: transformJSX,
  Program: {
    enter: (path, { opts }) => {
      const taroComponentsMap = (path.hub.file.metadata.taroComponentsMap ||= new Set())
      for (const stmt of path.get('body')) {
        if (t.isImportDeclaration(stmt.node)) {
          // 是否是@tarojs/components的包
          if (isTaroComponent(stmt.node.source.value)) {
            stmt.node.specifiers.forEach((specifier) => {
              // 包体导出的变量名
              const importedName = specifier.imported.name
              taroComponentsMap.add(importedName)
            })
          }
        }
      }
      if (merged.validate) path.traverse(JSXValidator)
    },
    exit: postprocess
  }
}

在enter中,可以获取所有import进来的语句,基于此,对于别的import路径组件,即使重名,也不会对其进行收集。

解决组件别名

除了需要解决组件重名外,还存在另一个隐患:

js 复制代码
import { Button as MyButton } from '@tarojs/components'

组件别名,使用上述方法收集到的组件是MyButton,在transformElement处理分支的时候,MyButton并不在可用列表中,那这个Button的编译就会被遗漏掉了,所以我们还得改写上述的方法:

js 复制代码
visitor: {
  JSXElement: transformJSX,
  JSXFragment: transformJSX,
  Program: {
    enter: (path, { opts }) => {
      const taroComponentsMap = (path.hub.file.metadata.taroComponentsMap ||= new Map())
      for (const stmt of path.get('body')) {
        if (t.isImportDeclaration(stmt.node)) {
          // 是否是@tarojs/components的包
          if (isTaroComponent(stmt.node.source.value)) {
            stmt.node.specifiers.forEach((specifier) => {
              // 包体导出的变量名
              const importedName = specifier.imported.name
              // 当前使用的变量名 防止别名
              // import { Button as MyButton } from '@tarojs/components'
              const localName = specifier.local.name
              taroComponentsMap.set(localName, importedName)
            })
          }
        }
      }
      if (merged.validate) path.traverse(JSXValidator)
    },
    exit: postprocess
  }
}

将Set数据结构改为Map,采用localName(对应的MyButton): importedName(对应的Button),这样能够方便transformElement直接获取到源组件名。

其他问题

在使用babel处理后,由于我们把源文件'@tarojs/components'的组件在正文中并没有在使用了(变为原生标签使用),导致这个包变为了一个无引用import chunk,这在taro的收集小程序组件chunks流程中,会收集不到。所以还得对这个在taro的流程中特殊处理:

ts 复制代码
!(nameOfCallee && nameOfCallee.includes('_$createElement')) && // solidjs创建元素

大概就是在TaroComponentsExportsPlugin,再进行一次收集,solid转换后的源码中,都会用_$createElement方法进行元素创建,所以就去检索这个方法里的标签作为小程序使用到的组件即可,这在taro中也有比较多类似其他第三方组件库收集不到的处理方法,这里也是大佬给我指明在这里处理,所以就不在继续深入研究。

H5端的解决

由于taro在h5中统一都是使用stencil做的web-component,而stencil的output适配器并没有solid的实现,在solid实现web-component的话,他自己有个solid-element的库,写solid的代码,能够将其define为customElement,这2者最终导出的实现都会走defineCustomElement的原生方法,从结果上来说是一致的,要么就把各个taro的组件用solid-element实现一遍,但是这工作量也很大,并且维护成本也变高,所以这个方案也被驳回了。

stencil-solid-output

实现一个stencil-solid-output的适配器,其实taro的output导出的component,都需要自定义,那么只要去参考别的output适配器即可。

ts 复制代码
export const generateProxies = (config: Config, components: ComponentCompilerMeta[], pkgData: PackageJSON, outputTarget: OutputTargetSolid, rootDir: string): string => {
  const distTypesDir = path.dirname(pkgData.types)
  const dtsFilePath = path.join(rootDir, distTypesDir, GENERATED_DTS)
  const componentsTypeFile = relativeImport(outputTarget.proxiesFile, dtsFilePath, '.d.ts')
  const pathToCorePackageLoader = getPathToCorePackageLoader(config, outputTarget)

  const imports = `/* eslint-disable */
  /* tslint:disable */
  /* auto-generated solid proxies */
  import { createSolidComponent } from './solid-component-lib';\n`

  const generateTypeImports = () => {
    if (outputTarget.componentCorePackage !== undefined) {
      const dirPath = outputTarget.includeImportCustomElements ? `/${outputTarget.customElementsDir || 'components'}` : ''
      return `import type { ${IMPORT_TYPES} } from '${normalizePath(outputTarget.componentCorePackage)}${dirPath}';\n`
    }

    return `import type { ${IMPORT_TYPES} } from '${normalizePath(componentsTypeFile)}';\n`
  }

  const typeImports = generateTypeImports()

  let sourceImports = ''
  let registerCustomElements = ''

  return [
    imports,
    typeImports,
    sourceImports,
    registerCustomElements,
    components.map(cmpMeta => createComponentDefinition(cmpMeta, outputTarget.includeImportCustomElements)).join('\n'),
  ].join('\n') + '\n'
}

这部分代码就不需要细看了,因为我删了一部分,而且这个适配器的实现并不是重点。

createSolidComponent

createSolidComponent这个方法是用来沟通stencil与solid的连接器,由于stencil导出的组件都已经变为web-component,类似以下:

tsx 复制代码
<View class="index">
  <View style={{color: ${color()}}}>
    <Text>Hello world! </Text>
  </View>
  <Button onClick={() => setCls('bold')}>set class</Button>
  <Button onClick={() => setColor('red')}>set style</Button>
  <View>{Math.random()}</View>
</View>

输出在页面上的ui都会编译为web-component:

tsx 复制代码
 <taro-view-core class="index">
  <taro-view-core style={`color: ${color()}`} >
    <taro-text-core>Hello world!</taro-text-core>
  </taro-view-core>
  <taro-button-core  onClick={() => setCls('bold')}>set class</taro-button-core >
  <taro-button-core onClick={() => setColor('red')}>set style</taro-button-core>
  <taro-view-core>
    {Math.random()}
  </taro-view-core>
</taro-view-core>

将这些taro的component转换为web-component就是createSolidComponent做的事情,那么到底如何做的呢?

ts 复制代码
export const createSolidComponent = <
  PropType,
  ElementType extends HTMLStencilElement,
  ExpandedPropsTypes = any
>(
  tagName: string,
  manipulatePropsFunction?: (
    originalProps: StencilSolidInternalProps<ElementType>,
    newProps: any
  ) => ExpandedPropsTypes,
  defineCustomElement?: () => void,
): Component<PropType & JSX.DOMAttributes<ElementType> & ComponentSupplementaryTypes> => {

  if (defineCustomElement !== undefined) {
    defineCustomElement()
  }

  function SolidComponentWrapper(props: { children: JSX.Element } & any) {
    const [local, other] = splitProps(props, ['children', 'ref'])
    const eventsMap = new Map()
    const reactiveKeys = []
    const getUnTrackProps = (_props: Record<string, any>) => {
      let propsToPass: typeof props = {}
      for (const key in _props) {
        if (!_props.hasOwnProperty(key)) {
          continue
        }
        if (isPropNameAnEvent(key)) {
          eventsMap.set(key, _props[key])
          continue
        }
        if (isReactiveKey(_props, key)) {
          reactiveKeys.push(key)
          continue
        }
        const propValue = _props[key]
        propsToPass[camelToDashCase(key)] = propValue
      }
      if (manipulatePropsFunction !== undefined) {
        propsToPass = manipulatePropsFunction(_props, propsToPass)
      }
      return propsToPass
    }

    const unTrackProps = getUnTrackProps(other)
    const [reactiveProps] = splitProps(other, reactiveKeys)

    const _mergeProps = mergeProps(unTrackProps, { ref: (element: HTMLElement) => {
      if (local.ref && isFunction(local.ref)) local.ref(element)
      syncEvents(element, eventsMap)
      setReactiveProps(element, reactiveProps)
    } })

    return memo(() => h(tagName, _mergeProps, local.children), true)
  }

  return SolidComponentWrapper as any
}

这里主要解决的问题如下:

  • 使用solid中的h函数创建web-components,其实就是自定义element,因为注册web-component都已经在内部注册完成
  • props的响应式属性需关联并能依赖更新ui

在利用h函数的过程中,遇到许许多多的问题,各种形态我都尝试过,还自己对children进行循环遍历,并递归调用h函数。到最后,自己看了h函数的源码后,发现他内部已经会对children进行递归处理了,我是多此一举的行为,当把h函数的正确形态书写完毕后,后面的props更新,耗费的时间就少很多,就跟之前写solid的编译器setProps很类似。

总结

这一次的taro-solid的响应式处理,虽然只是一个bug,但是这涉及到的改动也很多,很大程度上都已经不算是一个bug的处理,需要深入到了编译层面,对babel工具的使用又加深了其理解,并且在处理大型框架上,需要考虑的东西也会很多,尽量做到满足用户需求的同时,也要考虑到后期的维护拓展问题。希望taro4版本能够顺利发布,也祝taro能变得更好🎉🎉🎉

相关推荐
Morpheon5 小时前
Cursor 1.0 版本 GitHub MCP 全面指南:从安装到工作流增强
ide·github·cursor·mcp
LinXunFeng7 小时前
Flutter - GetX Helper 助你规范应用 tag
flutter·github·visual studio code
草梅友仁8 小时前
AI 图片文字翻译与视频字幕翻译工具推荐 | 2025 年第 23 周草梅周报
开源·github·aigc
Jooolin10 小时前
【编程史】IDE 是谁发明的?从 punch cards 到 VS Code
ai编程·visual studio code·编译器
qianmoQ13 小时前
GitHub 趋势日报 (2025年06月04日)
github
abcnull14 小时前
github中main与master,master无法合并到main
git·github
星哥说事15 小时前
使用VuePress2.X构建个人知识博客,并且用个人域名部署到GitHub Pages中
开源·github
勤劳打代码16 小时前
步步为营 —— Github Connection refused 分层诊断
github
寻月隐君16 小时前
深入解析 Rust 的面向对象编程:特性、实现与设计模式
后端·rust·github
qianmoQ1 天前
GitHub 趋势日报 (2025年05月31日)
github