taro-solid小程序插件版本迭代第二期

前情提要

在上一期taro-solid小程序插件版本迭代中,我们解决了h函数带来的问题,忘记了的家人们,传送门回顾下吧。

问题介绍

很高兴,这次又见面了,但也很惭愧,自己写的东西居然漏洞百出,这次也迎来了个影响很大的bug,当时自己自测却没注意到。这次主要介绍的是《button input等标签渲染不出来》bug。

问题分析

上菜:

tsx 复制代码
import { useLoad, useDidShow } from '@tarojs/taro'
import { createSignal } from 'solid-js'
import styles from './index.module.css'

export default function Index() {
  const [color, setColor] = createSignal('red')
  const [cls, setCls] = createSignal('')

  return (
    <view class="index">
      <view>
        <text style={`color: ${color()}`}>Hello world! </text>
        <view>{Math.random()}</view>
      </view>
      <button onClick={() => setCls(styles['bold'])}>set class</button>
      <button onClick={() => setColor('blue')}>set style</button>

      {color() ? <icon type="success"></icon> : null}
      <input type="text" />
    </view>
  )
}

在index页面中,有icon button input等标签都渲染不出来,并且会有一个警告的信息:

该信息其实就是表明找不到该模板标签的占位符,taro3是采用模板占位进行的dsl运行时高度语法编译。

@tarojs/components

本来在jsx的写法中,任何使用到小程序组件,我们都是使用引入@tarojs/components的组件进来使用,这个包能够跨端编译对应的组件,h5采用的是@stencil/core(目前暂不支持solid)编译的web component组件,小程序端只会使用该标签作为编译。但是目前在上一期,我们由于采用jsx import的写法,在solidjs并行不通,还引来很大的问题,所以我们目前的编译方式是跟vue3编译类似的,vue的模板语法,也不需要引入@tarojs/components中的component,直接写的自定义标签编译,但是我们却还未将这种方式迁移过来。

webpack在jsx编译时处理

我们可以直接去到taro-webpack5-runner包查看其在小程序端webpack的特殊处理,这里有个叫做collectComponents的收集器:

ts 复制代码
compilation.hooks.afterOptimizeChunks.tap(PLUGIN_NAME, (chunks: Chunk[]) => {
  const chunksArray = Array.from(chunks)
  /**
   * 收集 common chunks 中使用到 @tarojs/components 中的组件
   */
  commonChunks = chunksArray.filter(chunk => this.commonChunks.includes(chunk.name) && chunkHasJs(chunk, compilation.chunkGraph)).reverse()

  this.isCompDepsFound = false
  for (const chunk of commonChunks) {
    this.collectComponents(compiler, compilation, chunk)
  }
  if (!this.isCompDepsFound) {
    // common chunks 找不到再去别的 chunk 中找
    chunksArray
      .filter(chunk => !this.commonChunks.includes(chunk.name))
      .some(chunk => {
        this.collectComponents(compiler, compilation, chunk)
        return this.isCompDepsFound
      })
  }
})

webpack对于import的语法,都可以用到这个方法去收集chunks,然后对于引用到的chunk利用collectComponents进行小程序component收集。例如:

tsx 复制代码
import { View, Text, Button } from '@tarojs/components'

这样就能收集到View, Text, Button的组件。看看收集器做的操作:

ts 复制代码
collectComponents (compiler: Compiler, compilation: Compilation, chunk: Chunk) {
  const chunkGraph = compilation.chunkGraph
  const moduleGraph = compilation.moduleGraph
  const modulesIterable: Iterable<TaroNormalModule> = chunkGraph.getOrderedChunkModulesIterable(chunk, compiler.webpack.util.comparators.compareModulesByIdentifier) as any
  for (const module of modulesIterable) {
    if (module.rawRequest === taroJsComponents) {
      this.isCompDepsFound = true
      // 在这里进行组件收集
      const includes = componentConfig.includes
      const moduleUsedExports = moduleGraph.getUsedExports(module, chunk.runtime)
      if (moduleUsedExports === null || typeof moduleUsedExports === 'boolean') {
        componentConfig.includeAll = true
      } else {
        for (const item of moduleUsedExports) {
          includes.add(toDashed(item))
        }
      }
      break
    }
  }
}

其核心就是在componentConfig.includes 中添加了组件,然后就会在小程序的模板中生成这些组件的占位符。

vue3在webpack编译时处理

简简单单分析下vue3在webpack编译时,是如何收集组件的,进入到taro-vue3-plugin包中,看看webpack.mini的处理:

ts 复制代码
function setVueLoader (ctx: IPluginContext, chain, data, config: IConfig) {
  const vueLoaderPath = getVueLoaderPath()
  vueLoaderOption.compilerOptions.nodeTransforms.unshift((node: RootNode | TemplateChildNode) => {
    if (node.type === 1 /* ELEMENT */) {
      node = node as ElementNode
      const nodeName = node.tag
      if (capitalize(toCamelCase(nodeName)) in internalComponents) {
        // 收集小程序使用到的组件
        data.componentConfig.includes.add(nodeName)
      }
    }
  })
}

精简了一下代码,因为太长了,其实就是通过vue-loader的插件去收集小程序用到的组件,那么vue文件有vue-loader编译器去收集,而在solidjs中,有编译器吗?类似vue-loader的solid-js-loader,但是solid-js-loader目前并没有。

暴力收集

ts 复制代码
// componentConfig内容
export const componentConfig: IComponentConfig = {
  includes: new Set(['view', 'catch-view', 'static-view', 'pure-view', 'scroll-view', 'image', 'static-image', 'text', 'static-text']),
  exclude: new Set(),
  thirdPartyComponents: new Map(),
  includeAll: false
}

本身在componentConfig的includes里就有包含了一些小程序的组件标签,那么只要在webpack编译后的文件中,在去收集里面有用到小程序组件,也未尝不可,毕竟不能做到像vue-loader一样,可以在编译过程中就收集,只能在编译后再收集了。我的想法是在plugin-solid的loader-meta中,有个modifyConfig自定义方法,这里可以拿到编译后的文件source。

ts 复制代码
function extractCreateElementTags (code: string) {
  const regex = /_\$createElement\s*\(\s*(['"])?(.*?)\1\s*\)/g
  const matches = []
  let match

  while ((match = regex.exec(code)) !== null) {
    if (match[2]) { // 检查是否有内容
      matches.push(match[2].replace(/"/g, '')) // 移除双引号
    }
  }

  return matches
}

function modifyComponentConfig (source) {
  const res = extractCreateElementTags(source)

  res.forEach((name) => {
    if (capitalize(toCamelCase(name)) in internalComponents) {
      // 收集小程序模板中需要渲染的组件
      componentConfig.includes.add(name)
    }
  })
  return {}
}

采用正则去匹配_createElement括号里面的内容,这里面就是view,text之类的标签,因为_createElement是solidjs编译后的创建标签的函数,所以可以如此取,但是如果页面有打印_createElement('view'),这样也会将view标签收集,明显这种暴力方法虽然很方便,但并不准确

solidjs的编译器babel-preset-solid

由于采用暴力法去收集小程序用到的组件并不准确,所以还是得从编译过程入手,让他能够语法分析获得tagName是什么。对于jsx的编译器,其实就是babel做的,毕竟本身就是js超集语法,而solidjs用到的babel就是babel-preset-solid,里面有个babel-plugin-jsx-dom-expressions,这个插件就是用来解析jsx dom的。

js 复制代码
import SyntaxJSX from "@babel/plugin-syntax-jsx";
import { transformJSX } from "./shared/transform";
import postprocess from "./shared/postprocess";
import preprocess from "./shared/preprocess";

export default () => {
  return {
    name: "JSX DOM Expressions",
    inherits: SyntaxJSX.default,
    visitor: {
      JSXElement: transformJSX,
      JSXFragment: transformJSX,
      Program: {
        enter: preprocess,
        exit: postprocess
      }
    }
  };
};

其入口的配置如下,分析了这个结构,我发现可以在preprocess这个文件,只要重写,加入一个tagCollector函数,这样就很方便,并且不会对源代码造成什么影响,不过自己得重新发布一个babel的插件,用户得从babel-preset-solid迁移到我这个babel插件。这也是一个缺点,要是能给babel-plugin-jsx-dom-expressions这个库提个pr拓展这个方法肯定是最好了,但是估计能不能同意合进去又另外说。

最终解决方法

在上面是采用引入babel-plugin-jsx-dom-expressions库,然后重写preprocess,然而这有一个新的问题,对于一些动态的标签,他并不能识别到:

tsx 复制代码
// 1.
<Component renderHeader={() => <view>header</view>} />

// 2.
<view>
  {show() ? <view>show</view> : <text>hide</text>}
</view>

对于上述2种情况,preprocess里面的JSXValidator并不会识别到这个标签,看来一开始的方向错误,这个preprocess只是静态的解析语法,如果这个方法行不通的话,其他的类似transformJSX应该是准确的,但是这个文件里面结构很复杂,想要重写这个文件,需要改动点太大了,不得已,采用了一个简易的方法,直接拿到这个babel-plugin-jsx-dom-expressions库打包后的文件,在这个文件进行代码修改,所幸的是,这个打包后的文件,只有一份index.js,那就直接进行修改吧。

js 复制代码
function transformElement(config, path, info = {}) {
  const node = path.node
  const tagName = getTagName(node)

  // <Component ...></Component>
  if (isComponent(tagName)) return transformComponent(path)
  // new Add tagCollector
  if (typeof config.tagCollector === 'function') {
    config.tagCollector(tagName)
  }
  // <div ...></div>
  // const element = getTransformElemet(config, path, tagName);

  const tagRenderer = (config.renderers ?? []).find(renderer => renderer.elements.includes(tagName))

  if (tagRenderer?.name === 'dom' || getConfig(path).generate === 'dom') {
    return transformElement$3(path, info)
  }

  if (getConfig(path).generate === 'ssr') {
    return transformElement$2(path, info)
  }

  return transformElement$1(path)
}

这个方法就是最准确获取tagName的位置了,无论是动态的还是静态的。

总结

在衡量一个标准时,我个人还是以准确性为第一要义。像使用暴力法,虽然很方便,而且也不需要用户额外安装另外的库,但是它的准确性存在一定的问题。不知道如果是你,你会怎么选择这2种方案?

这期我们主要讲了如何处理收集小程序用到的组件问题,其实还有其他问题没有说,像用不了solid-js/web下的Portal、Dynamic组件等,自定义指令ts类型不生效,打包发包该用changest管理等等,后面如果有时间就继续写下一期,敬请期待。

相关推荐
uhakadotcom5 小时前
云计算与开源工具:基础知识与实践
后端·面试·github
uhakadotcom6 小时前
BPF编程入门:使用Rust监控CPU占用
后端·面试·github
uhakadotcom7 小时前
GHSL-2024-252: Cloudflare Workers SDK 环境变量注入漏洞解析
后端·面试·github
uhakadotcom7 小时前
GHSL-2024-264_GHSL-2024-265: 了解 AWS CLI 中的正则表达式拒绝服务漏洞 (ReDoS)
后端·面试·github
uhakadotcom7 小时前
了解Chainlit:简化AI应用开发的Python库
后端·面试·github
zoahxmy09297 小时前
微信小程序 request 流式数据处理
微信小程序
小华同学ai7 小时前
1K star!这个开源项目让短信集成简单到离谱,开发效率直接翻倍!
后端·程序员·github
ON.LIN8 小时前
Git提交本地项目到Github
git·github
人人题8 小时前
汽车加气站操作工考试答题模板
笔记·职场和发展·微信小程序·汽车·创业创新·学习方法·业界资讯
uhakadotcom8 小时前
使用 Model Context Protocol (MCP) 构建 GitHub PR 审查服务器
后端·面试·github