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 wroteweapp -websocket
andweapp-fetch
implementsubscriptions
andquery/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
<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
selector to escape.wxss
is also required
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
:
- It internally uses
wxml ast
to parse allwxml
templates to get allclassName
for parsing and replacement - Use
postcss
to parse allwxss
to modify allcss
selectors - Use
babel
to parse alljs
/jsx
, so as to dynamically modify the conditions injsx?
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
andwxml
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
We need to match x?y:z?a:b
[50px]' : 'text-[#654321]' }}3
literals for escaping.
jsx plugin
Source code
base/BaseJsxPlugin
andjsx
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 correspondingjest
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:
- When replacing literals, only match part of the
return
template code ofreact/vue2/vue3
, and discard the matching and replacement of literals within the function scope. - 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 codesrc/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
andshared
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
andwxml/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