跳到主要内容

Elm 开发实践 - 与 Tailwind CSS 的集成

· 阅读需 10 分钟
flytreleft

写在开始

《Elm 开发实践》系列文章为 Elm 工程实践经验分享, 主要目的是针对各类业务场景提供基于 Elm 的可行性方案, 以此让更多人能够认识到 Elm 的优势以及其适用场景, 为前端开发提供不同的问题解决思路。

个人认为,Elm 是一种更为优秀和彻底的前端解决方案,在逐渐跨过其使用门槛后,便会对其爱不释手。 其 强类型系统模式匹配模型/视图更新机制 以及 函数式编程 让应用开发过程更加流畅、思路更加清晰、代码结构也更加简洁明了, 而且,由于数据不可变特性,其还支持 Time Travel 并自带 调试器, 在开发过程中只需要通过其调试器观察和分析数据的变化情况便可以快速定位逻辑错误的位置, 而不需要在一堆混乱的代码中理出头绪(数据可以在任意位置被更新所造成的麻烦真心让人痛苦不堪)。

我甚至开始希望,能够参考 Elm 的设计模式从底层开始重新设计 GUI 的开发语言(展示层与交互层应使用同一套开发语言,以降低各层之间的转换消耗和开发人员的心智负担), 而不是简单地对 JavaScript 进行缝缝补补,也不要照搬面向对象等设计模式的思想去设计新的 GUI 语言, 对于现有的 JS 生态,也仅需提供与其的 互操作 支持, 而不需要提供对其的兼容性支持。

不过,从当下实用的角度出发, 我们也没有必要教条地按照理想的 Elm 模式(比如,消除 CSS、组件无状态等)进行开发, 完全可以在不破坏 Elm 核心原则(模型/视图单向更新、数据不可变、强类型系统、函数式编程等)的前提下, 结合 JS 的优势实现自己的开发需求。

场景描述

Tailwind CSS 将所有 CSS 属性全部封装成了语义化的类,支持响应式设计、暗黑模式, 可定制化程度高,可维护性强,还默认提供一套专业的 UI 属性值(文字、尺寸、边距、颜色等)。 其极大地简化了前端的开发工作,即使没有专业的 UI 设计知识, 也可以快速地开发出现代化的应用页面,实为我等 UI 设计能力薄弱的开发人员的福音。

Elm 开发侧重的是业务逻辑,并不提供专业的 UI 库,要想写出一套美观的页面, 需要像写 HTML 一样编写内联样式(或自定义 CSS)。 而将 Tailwind CSS 与 Elm 进行集成,不仅可以充分利用 Elm 的开发优势, 还可以彻底摆脱页面不美观、无法自适应移动设备等 UI 设计问题, 让一个后端 Boy 也能对前端开发游刃有余。

虽然,像 Elm UI 之类的 Elm 库致力于消除对 CSS 的使用,但其并不能提供一套现成的、专业的、现代化的 UI 库, 而自行从头设计一套 UI,不仅费时费力,还很难达到当下的审美设计要求。 故而,将 Tailwind CSS 与 Elm 集成使用,才是一个后端 Boy 更为务实的选择。

工程搭建

本案例公网演示地址为 https://flytreeleft-elm-tour.netlify.app/tailwindcss-integration

从 Github 克隆 演示项目 到本地:

git clone https://github.com/flytreeleft/elm-tour.git

进入本案例所在的工程目录 tailwindcss-integration 并安装项目依赖:

yarn install

从零开始初始化该 NodeJS/Elm 项目的步骤详见 项目创建

待依赖安装完毕后,启动本地演示服务:

yarn dev

浏览器访问该演示服务地址 http://localhost:4201/, 并点击两个 Logo 中间的图标按钮以查看主题样式切换效果。

方案实现

本方案采用的是在 Elm 视图函数中通过 Html.Attribute.class 属性直接引用 Tailwind CSS 类名的方式进行集成。虽然有 matheus23/elm-default-tailwind-modules 这类 Elm/Tailwind CSS 库,但其使用仍不太灵活,而且对 Tailwind CSS 类名做 Elm 函数化也没有太大必要,此外,在实际的项目中还是会用到其他 JS 组件并引用 Tailwind CSS 的类名,故而,直接引用其类名反而更便于以一致的方式进行组件样式维护。

与其他 JS 项目一样,要使用 Tailwind CSS, 需要在项目中安装、启用并配置其 PostCSS 插件 tailwindcss

安装 tailwindcss 插件
yarn add --dev tailwindcss
postcss.config.js - 启用 tailwindcss 插件
// https://github.com/csaltos/elm-tailwindcss
// https://tailwindcss.com/docs/installation/using-postcss
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
tailwind.config.js - 配置 tailwindcss 插件
// https://tailwindcss.com/docs/installation/using-postcss
module.exports = {
// https://github.com/csaltos/elm-tailwindcss
content: ["./src/**/*.elm"],
// Note: 在任意父节点加上 [theme="dark"],即可对其子节点启用暗黑模式。
// 但 class 是必须指定的,否则,主题模式切换不会起作用
// https://tailwindcss.com/docs/dark-mode#customizing-the-class-name
darkMode: ["class", '[theme="dark"]'],
theme: {
extend: {},
},
plugins: [],
};

tailwind.config.js 中需要首先 配置 Content, 也就是指定 Tailwind CSS 类名的引用文件位置。 其插件通过扫描 content 列表中的文件,识别出被引用的类名, 进而生成最小化的 CSS 文件,以避免在前端页面中包含无关的 CSS 样式,影响资源加载。

CSS 文件将使用 postcss-loader 加载并处理, 所以,在 content 中不放置 CSS 文件路径。

由于本方案是在 Elm 源码文件中直接引用其类名, 故而,直接将 src 下任意层级的 elm 文件(./src/**/*.elm)加入到 content 列表中。

darkMode 用于设置页面暗黑模式的启用方式。 默认是在任意节点的 class 属性中添加 dark 名称(一般在 <html/><body/> 节点上设置), 其子节点便会应用其所设置的暗黑样式。 但本方案使用 [theme="dark"] 选择器来启用页面的暗黑模式, 也就是在父节点上添加 theme 属性并设置其值为 dark 或为空,便可以控制其子节点暗黑模式的启/禁。 该方式在代码层面更加清晰,也更便于通过代码控制暗黑主题的切换。

完成插件配置后,还必须在项目的 CSS 主文件(页面中第一个引入的 CSS 文件,如,public/index.css)中通过 @tailwind 指令指定 PostCSS 插件 tailwindcss 生成的各类样式在 CSS 文件中的位置:

public/index.css
/* 指定 tailwindcss 各类样式在 CSS 文件中的插入位置
* https://tailwindcss.com/docs/functions-and-directives#tailwind
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;

由于在 CSS 文件中的 @tailwind@applyTailwind CSS 指令 是通过 PostCSS 插件 tailwindcss 识别并处理的, 所以,需要在 Webpack 的配置文件中配置对 CSS 文件优先使用 postcss-loader 加载(use 中的加载器列表为倒序执行,故,postcss-loader 需放在最后):

webpack.config.js
const common = {
// ...
};

// 开发模式下的配置
if (MODE === "development") {
module.exports = merge(common, {
module: {
// ...
rules: [
{
test: /\.css$/,
exclude: [/elm-stuff/],
use: [
"style-loader",
{
loader: "css-loader",
options: {
// 启用对 css 中资源的引用路径处理
url: true,
},
},
"postcss-loader",
],
},
// ...
],
},
});
}

// 生产模式下的配置
if (MODE === "production") {
module.exports = merge(common, {
// ...
module: {
rules: [
{
test: /\.css$/,
exclude: [/elm-stuff/],
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
// 启用对 css 中资源的引用路径处理
url: true,
},
},
"postcss-loader",
],
},
// ...
],
},
});
}

至此,Elm 与 Tailwind CSS 的集成便配置完成了。 接下来,便可以愉快地在 Elm 源码文件中引用 Tailwind CSS 的样式了:

src/Main.elm
type alias Model =
{ theme : Theme
-- 其他状态 ...
}


type Msg
= NoOp
| ChangeTheme Theme


type Theme
= Light
| Dark

view : Model -> Html Msg
view ({ theme } as model) =
div
-- 在最外层 div 节点控制启禁视图的暗黑模式
[ attribute "theme"
(case theme of
Dark ->
"dark"

Light ->
""
)
, class "w-full h-full"
]
[ welcome model
-- 其他视图函数 ...
]


welcome : Model -> Html Msg
welcome { theme } =
div
[ class "text-slate-500 dark:text-slate-400 bg-white dark:bg-slate-900"
, class "w-full h-full px-8"
]
[ -- 视图内容 ...
]

关系紧密的 Tailwind CSS 类可以作为一组,通过 class 集中在一起, 多个 class 就是多组不同的控制样式。 按这种方式组织的代码,在视觉上更加整齐一致,也更便于进行样式维护。

实践总结

Elm 负责业务逻辑,Tailwind CSS 负责美观,二者各有侧重点,可以互取所长, 对于降低前端开发难度,提升前端开发效率,能够起到十分积极的作用。

注意事项

若同时与 Elm UI 集成,会因为其生成的 CSS 内边距类名 p-N 与 Tailwind CSS 的重名, 而出现 Tailwind CSS 的内边距设置不生效的问题。 此时,可以使用 Tailwind CSS 中的 px-Npy-Npt-N 等指定了内边距方向的类名来规避命名冲突问题。

更为彻底的方案(也是与现有项目的兼容方案)是为 Tailwind CSS 的类名都加上 前缀

tailwind.config.js
module.exports = {
prefix: 'tw-',
}

这样,内边距等类的名称都需要加上 tw- 前缀(如,tw-p-1), 从而有效避免 CSS 类重名问题。

扩展阅读

版权声明