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能变得更好🎉🎉🎉

相关推荐
落798.2 小时前
【植物大战僵尸杂交版】获取+存档插件
python·游戏·github
剑海风云2 小时前
GraalVM简介及使用
java·jvm·graalvm·编译器·本地镜像
FutureUniant8 小时前
GitHub每日最火火火项目(7.7)
python·计算机视觉·ai·github·视频
疯人院长sir13 小时前
linux监控服务器磁盘、内存空间使用率到达90%发送邮件脚本
linux·服务器·github
Justin3go14 小时前
独立开发总会有同类产品 - FAV0周刊006期
前端·程序员·github
粥里有勺糖15 小时前
视野修炼-技术周刊第91期 | 惊讶线条
前端·程序员·github
LinXunFeng20 小时前
Flutter - 支持观察NestedScrollView,兼容性更强 😈
前端·flutter·github
金金金__20 小时前
githup开了代理push不上去
git·github
Bentley Xvier1 天前
Python 获取tiktok视频评论回复数据 api接口
大数据·人工智能·爬虫·github·运维开发
前端达人1 天前
GitHub开源推荐:AI加持的Notion风格编辑器Novel
人工智能·编辑器·github·notion