The principle of tailwindcss in weapp

The principle of tailwindcss in weapp

  • The principle of tailwindcss in weapp
    • foreword
    • What does this plugin do?
    • Principles
      • utility-first CSS framework
      • applet compatible
    • The author’s realization
      • Literal escaping in js is easy to accidentally hurt
      • Compatible with multiple frameworks
        • template plugin
        • jsx plugin
        • Compatibility between webpack4/5 and vite
      • css selector replacement
        • Find the tailwindcss node
        • escape atomic class selector
      • Reimplementation of template parsing
      • Dynamic source patches
        • Support for custom length units (rpx)
    • What’s new in 2.0?
    • Epilogue

Foreword

The author has a relatively deep understanding of libraries such as tailwindcss. I have written and released many related packages before, such as tailwindcss-miniprogram-preset, weapp-tailwindcss-webpack-plugin and so on.

Recently I released the 2.0.0 version of weapp-tailwindcss-webpack-plugin, adding some core features. Thinking that now is also the time to review and summarize the principle and history of this plug-in. Looking back at the npm version release history, I saw that the first official version released that year was still on 2022/2/3, and 1 has passed by now It’s been more than a year, think about it, the years are really passing by, waiting for us to grow old.

What does this plugin do?

To briefly summarize, it is to bring the features related to tailwindcss into the development of small programs.

Why is it needed? The core reason is that the applet platform is not as free as h5, and there are many constraints and non-standardized API. Let’s take the WeChat applet as an example:

wxss has less selector support than css and wxml has more selector support than wxml Less character set support, the operation of js and wxs is also separated (wxs syntax is like a lower version of js).

Not to mention that none of the global objects that should exist, such as Blob, File, FormData, WebSocket , fetch, XmlHttpRequest, etc., which caused many browser packages to fail to run after installation.

For example, recently I am doing graphql transformation on the server side. The mainstream graphql client on the market will immediately report an error when it is installed and run directly. Because of the lack of global objects, in order to be compatible with them, I wrote weapp -websocket and weapp-fetch implement subscriptions and query/mutation respectively. It is currently used in the project and works well.

In general, some popular libraries commonly used on h5 may not work in small programs due to the lack of many specific objects and syntax restrictions. tailwindcss is one of them, and this plugin can help you use it in applets.

Principle

utility-first CSS framework

Actually, the tailwindcss/windicss/unocss on the market do the same thing. Metaphorically speaking, they are a funnel of strings with a sieve. They read the content of the code file written by the developer, and divide it into a large number of strings and put them into the funnel, and then filter through the filter, and generate atomic classes if they meet the conditions, and ignore the rest of the “residue” .

Among them, tailwindcss is mostly used as postcss plugin, and its source code implements a file reading mechanism (that is, tailwind.config.js content configuration item) to extract the code we wrote.

And windicss/unocss relies on bundler such as webpack/rollup/vite, during the packaging process Objects such as Source / Asset / Chunk are obtained to extract strings. Although windicss/unocss have corresponding implementations of postcss plugin, most of them are experimental and not very good Copy their experience of packaging plugins.

What makes them different?

This is actually about the advantages of unocss/windicss. Currently tailwindcss postcss plugin actually only has the ability to read, it reads the code we wrote and generates atomic classes. And windicss/unocss are mostly used as a webpack/vite/rollup plugin, so they not only have the ability to read, but also have modify. So they can write code like this:

<button
  bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"
  text="sm white"
  font="mono light"
  p="y-2 x-4"
  border="2 rounded blue-200"
>
  Button
</button>
<div m-2 rounded text-teal-400 />

In fact, it is just overwriting in the packaged plug-in, which is essentially a syntactic sugar.

In addition, when you choose this kind of atomic framework, you should either choose tailwindcss or unocss. At present, windicss seems to be dead.

Mini program compatible

So much has been said above, let’s talk about their compatibility with applets. In fact, there are so many escaping plug-ins on the market, and the presets are all the same thing.

Most of the ideas are to use these atomic class css frameworks, and generate class to be compatible with the applet environment by renaming, escaping, and overwriting.

To be more specific, it is to modify the packaged product, escape the className written by the developer in wxml, and convert js The className written in dom to be applied to the dom node is also escaped, and the css generated in wxss is also required selector to escape.

And the core of these cores is the escaped class name and selector, which must match each other! ! Otherwise the generated result is completely wrong.

The author’s implementation

Having said so much before, let me talk about my own implementation.

In fact, my initial implementation was also very simple. In the early days of the first version, I chose to write such a webpack plugin:

  1. It internally uses wxml ast to parse all wxml templates to get all className for parsing and replacement
  2. Use postcss to parse all wxss to modify all css selectors
  3. Use babel to parse all js/jsx, so as to dynamically modify the conditions in jsx? Literal (StringLiteral).

However, the ideal is full and the reality is very skinny. In the process of realization, difficulties emerged one by one:

Literal escaping in js is easy to accidentally hurt

At first, I thought about directly matching and replacing the literal value of js that meets the requirements, so I copied the regularity of the parsing extractor from the source code of tailwindcss enthusiastically, and Match and replace after packing.

However, this solution failed because the regular match of tailwindcss may inject some literals of js code into webpack by default. Matching is also included, causing large-scale accidental injuries. This kind of accidental injury will cause js to fail to load, and the application will hang directly. So temporarily remove the dynamic modification of the js literal. At the same time, a method to manually mark the replacement position is exported:

import {<!-- --> replaceJs } from 'weapp-tailwindcss-webpack-plugin/replace'
const cardsColor = reactive([
  replaceJs('bg-[#4268EA] shadow-indigo-100'),
  replaceJs('bg-[#123456] shadow-blue-100')
])

Although this solves the problem of accidental injury, it brings some code intrusion, which leads to poor development experience. But this problem is solved in 2.0.0, please continue to read.

Multi-framework compatibility

Source code framework section

As you can see, my plugin is compatible with almost all mainstream development frameworks that are still alive on the market. In the process of starting to design a compatible solution, I found that the products compiled into small programs by these frameworks are different, one is based on uni-app/mpx/ Native (native) is an example, they convert the writing method of vue template step by step into the writing method of applet template. The other type is the jsx writing method represented by taro, rax, remax, the product of this writing method, The biggest feature is that the wxml of pages and components often has such a structure:

<import src="../../base.wxml"/>
<template is="taro_tmpl" data="{<!-- -->{root:root}}" />

In the base.wxml file, there are a large number of conditional rendering statements, and then an object is generated in js and passed in, and the entire page and logic are rendered. This solution is obviously much more flexible than the template solution. Personal summary, one kind of partial static compilation, one kind of partial dynamic.

So when I started writing, the plug-in was also divided into 2 versions, one is TemplatePlugin specially used to deal with this kind of static framework, and the other is JsxPlugin Responsible for dynamic scenarios. These 2 plug-ins have different emphases, one mainly replaces the template code, and the other mainly escapes jsx, in terms of development difficulty, the jsx plug-in It’s much harder.

Template plugin

Source code base/BaseTemplatePlugin and wxml parts

What the template plugin needs to do is to parse all the class attributes of dom, but there may be js expressions in this attribute ( {{}} wrapped part).

If it does not contain an expression, you can directly escape and override it. The one with an expression is a little more complicated. I need to estimate the syntax used by the developer. For example, developers often use some array binding syntax, or Multiple conditional operators such as x?y:z?a:b to compose class names. This means that we need to parse the expression wrapped inside {{}} like parsing js, and then check the syntax to to replace.

For example, {{ flag ? 'bg-[#123456]' : otherFlag ? 'text- derived from x?y:z?a:b [50px]' : 'text-[#654321]' }} We need to match 3 literals for escaping.

jsx plugin

Source code base/BaseJsxPlugin and jsx parts

The development of this plugin is more difficult, because I found that only taro is a framework, it supports react/ preact/ vue2 / vue3 These frameworks, the terrible thing is, react/vue2/vue3 The products they build ,there is a big difference.

At this point, you can check the difference between the source code in my src/jsx directory and the corresponding jest unit test snapshot.

So in the configuration item, in addition to the original appType (framework), I added a framework specially provided for taro, by passing in react/vue2/vue3 Let them follow their own jsx? replacement strategy.

Of course, this is far from solving the problem. After all, the core logic of the original plug-in is executed in the hook of processAssets, which is relatively late and may cause accidental damage. The problem exists. I did some effort for this:

  1. When replacing literals, only match part of the return template code of react/vue2/vue3, and discard the matching and replacement of literals within the function scope.
  2. In order to make the scope of this escape replacement precise enough, it must be executed as early as possible. So based on this idea, I thought of dynamically inserting my own webpack-loader:jsx-rename-loader in the plug-in (see source code src/loader )

This loader will be dynamically inserted into the loading and reading sequence of the jsx? file, and ensure that it is executed first in the queue, that is, babel-loader or ts-loader. In this way, when escaping is performed inside the loader, the original content obtained is very close to the code written by the user himself.

compatibility between webpack4/5 and vite

Source code base -> v4/v5 part

This is also caused by the different versions of webpack / postcss used by each framework.

But it is relatively simple to achieve this goal, just look at the webpack document, and rollup/vite, their own API It is simpler than webpack, and it is very simple to implement.

Some specific examples are:

// webpack5 dynamically inserts loader
import {<!-- --> NormalModule } from 'webpack'
NormalModule.getCompilationHooks(compilation).loader.(pluginName, (loaderContext, module) => {<!-- -->})
// webpack4
compilation.hooks.normalModuleLoader.tap(pluginName, (loaderContext, module) => {<!-- -->})

// webpack5
compilation.hooks.processAssets
// webpack4
compilation.hooks.optimizeChunkAssets

// webpack5
const Compilation = compiler. webpack. Compilation
const {<!-- --> ConcatSource } = compiler.webpack.sources
// webpack4
import {<!-- --> ConcatSource, Source } from 'webpack-sources'
// There is also a version issue with `loader-utils`
import {<!-- --> getOptions } from 'loader-utils'
// There is also the problem that the assets object cannot be modified after Compilation is closed, etc.

This kind of problem can be solved as long as you are willing to debug more.

css selector replacement

Source code postcss section

This part is the home of postcss, we know that postcss is essentially a css ast tool. With ast there are various conversion plugins to generate css.

Now our core goal of using it is to find all tailwindcss generating blocks and escape the selector.

Let’s solve the first problem first: how to find it?

Find the tailwindcss node

Source code postcss/mp section

Different frameworks have different paths to their common style files. For example, the path of uni-app is common/main.wxss, taro is app.wxss, and a Some frameworks are even more wonderful, common/miniprogram-app.wxss and so on, this is simply simple.

So we can accurately locate the position of the public style through the appType framework type passed in by the user. However, this solution has undergone many optimizations in the follow-up. The core is to use postcss analysis to guess the file location where tailwindcss is located.

This guessed solution is achieved due to the feature of tailwindcss. Because before tailwindcss generates atomic classes, it will inject a large number of css variables to control the presentation of all atomic classes.

So we will see a css node like this

*,:after,:before{<!-- -->
  --tw-border-spacing-x: 0;
  --tw-border-spacing-y: 0;
  --tw-translate-x: 0;
  --tw-translate-y: 0;
  --tw-rotate: 0;
  /* ..... */
}

Then we can think that if we find it, this file is where the public style is located. However, pay attention to the selector condition and -tw- variable condition when addressing, so as to achieve an exact match.

Escape atomic class selector

Source code postcss/selectorParser and shared parts

The first problem was solved, but the second problem came one after another, how to change it?

Is it possible to escape all selectors directly? Obviously not feasible, this will only lead to large-scale accidental injury of css.

We know a css selector, which may be simple or complex, it can contain adjacent sibling selectors, child selectors, descendant selectors, general sibling selectors, pseudo class selectors, pseudo Element selector, you can also use , to add selectors. We have to further parse the selector to achieve exact matching and escaping.

At this time, postcss-selector-parser is needed.

After we use postcss to perform root.walk, we will then selectors.walk the qualified css nodes, In this way, the effect of local precise escape of the selector is achieved.

In addition, since the default preflight of tailwindcss is for h5, we also need to inject our own small program preflight, It is also worth noting that the escape method of js/wxml is different from the escape method of wxss, because in css, often An extra \ character will be added.

Reimplementation of template parsing

Source code reg and wxml/utils sections

The previously used wxml ast is a third-party, and it has been completely unmaintained, and there have been bug (such as wxml inline wxs ), so I wrote a template attribute extractor based on regex.

This is due to the growing demand. Originally, you may think that in the template, it is enough to support attribute escapes such as class/ hover-class. However, it was later discovered that when users define and use components, they often pass className into the component as an attribute. At this time, we need to customize the generation rules to convert the attributes that meet the conditions. Righteous. for example:

<my-com class="bg-[#123456]" hover-class="bg-[#654321]" custom-class="text-[#ff00ff]" happy-attr ="text-[green]" sad-attr="text-[blue]"></my-com>

By default, it only matches escaped 2, how to do it? Therefore, the customAttributesEntities configuration item is opened. This configuration item will be matched with the original class/ hover-class related attributes to transform the above example All Arbitrary values in.

Dynamic source patch

Source code tailwindcss section

Many times, it is due to the limitations of tailwindcss itself, technical conditions and other reasons. Our pr is often not accepted by them. At this time, we need to modify their source code by ourselves, so as to encapsulate a tailwindcss that suits China’s national conditions. This can be done by fork releasing a new version, or by patching the source code.

Note: To patch the source code, this operation must be idempotent, otherwise repeated execution may destroy the structure of the source code

Support custom length unit (rpx)

Source code tailwindcss/supportCustomUnit part

Since tailwindcss 3.2.x, due to the addition of unit classification verification issue#110, some atomic classes that directly write rpx in the class name were misrecognized and then removed converted to color.

What does that mean?

For example, there are three atomic classes, text-[#123], text-[30rpx], text-[30px], in 3.2.x, the former one is the font color, and the latter two are the font size. After this version, the first 2 are colors and only the last one is font size anymore!

This is because tailwindcss will check the legal length unit on mdn. If it is in the legal length unit list, it is considered as font size, otherwise it is font color. The rpx unit is an original creation of WeChat, not a standard css unit, so it is naturally outside the set of legal length units.

So how about repair support? By reading the source code of tailwindcss, you will find that its method of verifying units is in the file utils/dataType.js.

Then use the three-piece set of babel: parse, traverse, generate to precisely lock the position of the exposed legal variable list, Then push the rpx unit into ast node.elements, and regenerate the overwrite.

What did

2.0 add?

This version adds UnifiedWebpackPluginV5 and UnifiedViteWeappTailwindcssPlugin which start with Unified.

They can automatically recognize and accurately handle all tailwindcss tool classes, which means it can handle wxss, wxml and js< Static and dynamic class in /code> (v1 version only has the ability to handle wxss, wxml static class) . So you don’t need to introduce and call the replaceJs method in js anymore! Since the 2.x plug-in has the ability to accurately convert js/jsx, the problem of accidental injury has been effectively solved, and the The development experience of taro this kind of dynamic template framework.

Welcome to experience, star/fork.

Introduction

As Mark Twain famously said: To a man with a hammer, everything looks like a nail.

The author’s plan will definitely have many limitations, and there will inevitably be many errata in this article.

You are welcome to point out, and everyone’s suggestions and pointers are welcome _

Author: ice breaker

syntaxbug.com © 2021 All Rights Reserved.