likes
comments
collection
share

基于vite2+react+typescript前端开发工程化(二)

作者站长头像
站长
· 阅读数 26

一、回顾简述

上一篇文章,主要讲述了前端工程初始化、目录结构、以及前端规范体系:

  • 代码格式规范
  • 代码质量规范
  • 代码提交规范
  • 版本管理规范

标准化和统一前端团队的代码风格和代码验证,可以大幅度提高前端团队的生产力。

接下来我们继续探讨,基于vite2+react+typescript前端开发工程化搭建。这一章准备和大家一起探讨样式管理以及组件化方面的问题。

二、样式管理

这一章,我们讨论,前端工程化中的重要模块————样式管理。

基于vite2+react+typescript前端开发工程化(二)

1、为什么先讲样式管理?

讨论前端绕不开视觉,也绕不开前端布局。前端大部分工作的最终结果,都是为了通过客户端,呈现给用户视觉画面和交互操作。

布局重构视觉,是前端开发的第一步。每当前端布开发的前置工作,需求文档+视觉设计+交互设计原型准备就绪,前端工程师首先要做的事情就是页面布局开发。前端布局是css加html,共同作用的结果。为了保证前端布局开发的效率,通常前端团队都会按照视觉和交互规范,制定好基础样式和基础组件。

所以,制定基础样式和基础组件,是前端架构高效和规范的基础保障。高效的完成前端布局工作,才能把更多的精力放在,前端交互逻辑和业务逻辑上面。

2、前端样式布局技术的发展

前端发展的基石是Javascript的发展和统一。尤其是Node.js和ES6以及Babel的出现,大幅度提升了前端工程化的发展速度。与此同时,CSS层叠样式表(英文全称:Cascading Style Sheets),也经历数次革命性的发展:

基于vite2+react+typescript前端开发工程化(二)

  • css3,flex、grid、transform、css vars等
  • 预编译语言sass、less、stylus
  • Postcss,把CSS代码解析成抽象语法树结构(AST),再交由插件处理,比如,浏览器前缀、嵌套语法、css变量等
  • 命名方法论BEM、OOCSS、SMACSS、ITCSS
  • 组件化相伴而生的All In JS,CSS Module、styled-component、emotion、styled-jsx、jss
  • Vue Scope
  • 原子CSS,tailwind.css
  • 样式验证Stylelint

简单整理了一部分,琳琅满目一大堆。那么我们应该如何选择适合自己团队,且效率最优化呢?

3,如何选择?

选择的基础是需求,你需要解决什么问题,就会选择对应的方法和工具。前端布局面临的问题有很多,比如:

  • A、css样式的全局性,造成命名冲突问题和管理困难
  • B、css样式缺乏编程能力,书写繁琐低效
  • C、css和javascript无法共享变量,导致样式管理混乱
  • D、浏览器快速更新迭代,版本层出不穷,兼容性实现愈发困难
  • E、css文件越来越庞大,加载速度越来越慢
  • F、css语法无法验证和风格不统一

这些就是长期以来,前端布局所面临的诸多问题。也正是伴随解决这些问题的过程,出现了很多新的技术方案。下面我们一起基于上述问题,筛选一下工程中需要用到的技术。

  • Stylelint前面已经讲述过,必选项,解决语法验证的问题
  • CSS3是必选项,它是原生的样式语言,也是最终产物
  • 我们需要一门预编译语言,增加编程能力,可以大幅度提高工作效率。如何选择,视团队情况而定
  • Postcss也是必选项,我不想自己去实现浏览器兼容性,效率低,易出错
  • 命名方法论,可以借鉴其思想,毕竟样式名称的可读性是很重要的
  • 原子CSS,tailwind.css是我极力推崇的工具,因为它可以解放90%的样式布局生产力,这一点就够了。之所以没选它,是因为我要在其他工程里面讲它
  • CSS in js 和 CSS Module取舍。两者都能解决命名空间和变量共享的痛点。但是我更愿意选择,接近原生处理模式的工具,因为可以以更少的代价复用css生态,语法高亮、语法提示、语法验证等等。无疑CSS module是更理想的选择。

4、选择结果

基于上述的分析,基于关键痛点和目标,例如,语法验证、命名空间、浏览器兼容、代码压缩、可编程、theme config、CSS Base等,我们筛选出,实现样式管理的目标工具。如下图:

基于vite2+react+typescript前端开发工程化(二)

5、整体思路

三、工程搭建与配置

现在我们已经有了初步的搭建思路和相应的工具。首先,我们需要在vite配置中开启sass和css module。而实际上,vite一直秉承“开箱即用”原则。其默认配置,支持这两种需求。

1、配置sass生效

虽然,内置了.sass / .scss文件的编译功能,但还是需要安装相应的依赖。

yarn add sass -D

下面创建一个样式文件,试一下是否成功支持sass

cd src && mkdir styles

cd styles && touch base.scss

echo '.color-green { color: green; }' >> base.scss

编译结果:

.color-green { color: green; }

引入sass,是为了解决css编写低效率问题。基于sass的语法特性和可编程能力,一方面可以延续css的语法风格,开发者可以无障碍顺利上手编写sass;另一方面sass带来诸多的便利工具,例如:变量、逻辑判断、类型、语法嵌套、继承、混入、函数、内置工具等等。这些工具,大幅度减少了代码总量,大幅度提高了代码的复用性,减轻了代码管理负担的同时,大幅度提高了开发者的生产效率。

2、 配置css module生效

vite默认,任何以 .module.{css,sass,scss,less,stylus,styl} 为后缀名的 CSS 文件都被认为是一个 CSS modules 文件

看一下,vite的css module配置选项,设置scopeBehaviour='local',开启css module:

interface CSSModulesOptions {
  scopeBehaviour?: 'global' | 'local'
  globalModulePaths?: RegExp[]
  generateScopedName?:
    | string
    | ((name: string, filename: string, css: string) => string)
  hashPrefix?: string
  /**
   * 默认:null
   */
  localsConvention?:
    | 'camelCase'
    | 'camelCaseOnly'
    | 'dashes'
    | 'dashesOnly'
    | null
}

a、vite css module机制

导入符合 vite css module的文件会返回一个相应的模块对象。

重命名base.scss为base.module.scss

/* base.module.scss*/

.colorGrreen {
    color: green;
}

导入使用:

import styles from './base.module.scss'

console.log(styles)

结果得到一个映射对象:

{colorGreen: '_colorGreen_2x4kt_1'}

应用到jsx

<div className={styles.colorGreen}> hello world </div>

最终编译成了

<div class='_colorGreen_2x4kt_1'> hello world </div>

看一下header,你会发现,多出了如下代码:

<style type="text/css">
._colorGreen_2x4kt_1 { color: green; } 
</style>

这段代码是从哪里来的呢?其实并不难猜测,这是vite css module机制作用的结果。

为了证明这一点,我们一起看一下network中vite css module加载机制。你会发现,base.module.scss文件是通过js的方式加载的。代码如下:

import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/styles/base.module.scss");
import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from "/@vite/client"

const __vite__id = "/Users/yangfei/workspace/react-ts-shop/src/styles/base.module.scss"
const __vite__css = "._colorGreen_2x4kt_1 {\n  color: green;\n}\n"
__vite__updateStyle(__vite__id, __vite__css)
export const colorGreen = "_colorGreen_2x4kt_1";

export default {
    colorGreen: colorGreen,
};

import.meta.hot.prune(() => __vite__removeStyle(__vite__id))

上面的代码不难理解,整体来说做了以下几个事情:

  • 将css module文件编译成了es module,这样css文件就有实现模块化管理
  • 把每个css name编译成带hash的唯一值,避免样式冲突
  • 将最终的样式,inject到header里面,以供后面引用使用
  • 以es module的形式导出css name的映射文件,方便引用指定的样式
  • 增加hot reload,实时编译更新

b、自定义class name

开发环境,生成这种带hash的class name,可读性相对较差,不利于代码调试。那么如何配置生成,可读性比较强的css代码呢?

添加配置generateScopedName选项:

css: {
    modules: {
        scopeBehaviour: 'local',
        generateScopedName: `[name]_[local]_[hash:5]`,
    }
}

上面的例子,生成的映射对象会变成:

{colorGreen: 'base-module_colorGreen_00cc7'}

generateScopedName选项支持回调函数,其返回值被用作相应的class name:

/**
 * @param name - class name
 * @param filename - 源文件全路径
 * @param css - 编译结果 css string
 **/
generateScopedName: ((name: string, filename: string, css: string) => string)

除此之外,还需要配置生成class name的命名风格、全局class name(不转译)、配置stylelint冲突等,都很简单,这里就不细讲了。重要的是,css module带来了模块化管理能力;解决css样式全局属性带来的命名冲突问题;以及相应程度上的css和js通信能力,例如共享变量问题。

3、theme配置管理

theme配置管理,也就是配置变量的管理。变量可以sass变量,css变量,也可以是js变量。无论采用哪种变量,声明和维护都有成熟的管理体系。theme配置管理的障碍在组件的构成包括css、js、和html。html暂且不做讨论,因为大部分的html模板,都和js整合成了一个整体,不存在通信障碍问题。下面我们讨论一下,基于sass和基于js管理theme的问题。

a、基于sass变量的theme配置

CSS In JS模式,是All In JS理念的一部分。天然具备CSS 和 JS的通信能力。但在CSS Module理念下面,实现和JS共享变量,却需要费一番功夫。可能你会很自然的想到,这样做:

/* vars.module.scss */
$colorPrimary: orange;

在jsx文件里面使用:

import vars from './vars.module.scss'

const Text = ()=> <section style={{color: vars.colorPrimary}}>hello world</section>

但很遗憾,css module只会转译并导出class name,但不会导出sass变量。

不过,令人兴奋的是,css module提供了:export伪类,用于导出sass元素。我们可以改造一下上面的例子:

/* vars.module.scss */
$colorPrimary: orange;

:export {
    colorPrimary: $colorPrimary
}

我们顺利的拿到了导出的变量:

console.log(Vars)
// {colorPrimary:"orange"}

但这并不意味着,万事大吉。:export导出value只能是string类型。这意味着,导出数据必须经过stringify处理,并且只有一个层级。如果是复杂数据类型,Map、List等,就不得不经过多次转换处理。

虽然,Map,List的stringify,以及数据格式化,会带来一定的挑战。但是基于sass管理theme的思路是行得通的。虽然,这种模式,需要我们对sass的语法掌握比较精通。

b、基于js变量或者json配置管理theme

虽然,sass的stringify会给我们带来一定的挑战,但是js的stringify对我们来说却很简单。

我们可以换个思路,我们把配置放进theme.config.js。然后,再把config里的配置转化成sass变量,这样做比sass转化js要简单的多。

而且vite的sass、less、stylus都提供了,注入变量的方法:

css: { 
    preprocessorOptions: { 
        scss: { 
            additionalData: `$injectedColor: orange;`, 
        }, 
        less: { 
            math: 'parens-division', 
        }, 
        styl: { 
            define: { 
                $specialColor: new stylus.nodes.RGBA(51, 197, 255, 1), 
            }, 
       }, 
  }, 
},

假设我们的theme.config.js文件,像下面这样:

export default {
    colors: {
        primary: 'blue',
        warning: 'orange',
        error: 'red',
        success:'green'
    },
    // ...
}

最终,我们需要转化成的sass Map应该是这样:

$colors: (
    primary:blue,
    warning: orange,
    error: red,
    success: green
);

基于additionalData我们只需要,把js配置转化成对应格式的sass string即可:

const mapJSToSass = (origin):string => {
    return // origin to string
}

export default {
    css:{
        preprocessorOptions: {
            scss: {
              additionalData: `
                @use "sass:map";\n
                @use "sass:list";\n
                @use "sass:math";\n
                @use "sass:color";\n
                @use "sass:string" as string;\n\n
                
                $colors: ${mapJSToSass(themes.colors)};
                // ...
              `
            }
      }
    }
}

这样就可以在每个被加载的scss文件的头部注入 $colors 变量。需要注意的是只有经过@import 或者 import语法加载的scss文件,才会被注入变量。而@use语法导入的scss文件,并不会注入变量。

总之,无论采用哪种思路,都可以解决js和css共享变量的问题,规范样式配置的统一管理。themes.config.js将是前端对接UI主通道。只要UI设计是规范且统一的,随之,前端布局也将实现规范统一。

4、原子化css

虽然,我们拥有了规范统一的配置,但是配置必须生成对应的样式,才能最终体现到界面上面。 虽然,我们可以借助组件化,实现配置变现。但是步子迈的太大,就会忽略细节。尽管组件化以及前端成熟的组件库,已经解决了大部分布局问题。但是真正的实践中,总是有很多细粒度的问题,比如,颜色、边距、字体大小、尺寸、行高等,困扰着我们。涉及到细粒度的元素的调整,既分散又不规范,且不易管理。因此,我们需要更细粒度的css样式,专门处理诸如此的问题。

我们可以,在sass变量的基础上,进一步生成相对应的css样式,用于细粒度的样式调整:

@each $key, $val in $colors {
  $name: capitalize($key);

  .fc#{$name} {
    color: $val;
  }

  // ...
}

生成以下代码:

.base__fcPrimary {
  color: #ff4b6d;
}
.base__fcSecondary {
  color: #fe8eaf;
}
// ...

如果进一步考虑到可能会需要动态定制主题。可以多生成一层runntime 变量:css vars

:root {
    @each $key, $val in $colors {
       @include decleare-css-var(('color', #{$key}), $val);
    }
}

@each $key, $value in $colors {
  $name: capitalize($key);
  $val: use-css-var(('color', #{$key}));

  .fc#{$name} {
    color: $val;
  }

  // ...
}

相应生成的css

:root{
  --color-primary: #ff4b6d;
  --color-secondary: #fe8eaf;
  // ...
}

.base__fcPrimary {
  color: var(--color-primary);
}
.base__fcSecondary {
  color: var(--color-secondary);
}
// ...

有了原子css,我们就拥有了快速布局的解决方案。虽然这并不是布局的最佳实践,但是基于theme配置基础,生成的基础样式,是规范有序且、可管理的。我们将散乱、随意的细粒度元素,全部收归到同一个基础源,从而实现统一管理。

import Base from '@/styles/base.module.scss'

export default const Page = () => (<div className={[Base.fcPrimary, Base.p12, Base.w120, Base.h120].join(' ')}>page info</div>)

5、theme hooks

前面我们css链路讨论了样式管理,下面我们从js链路继续讨论样式管理的思路。themes配置管理,具有全局特性。React Context非常适合管理全局性的元素。结合React Context和hooks,可以构建最佳的主题样式管理方案。

创建AppContext

import config from '@config/index.js'
import Styles from '@/styles/base.module.scss'

const AppContext = createContext<AppContextProps>({ baseCss: {}, themes: {} })

const App = () => (<AppContext.Provider value={{ baseCss: Styles, themes: config.themes }}>
// ...
</AppContext.Provider>)

声明相应的 hooks

export const useAppContext = () => useContext(AppContext)

export const useThemes = () => {
  const { themes } = useAppContext()
  return themes
}

export const useBaseCss = () => {
  const { baseCss } = useAppContext()
  return baseCss
}

组件里应用:

const Test = ({color, width, height})=>{
    const { sizes, colors } = useThemes()
    const Base = useBaseCss()
    
    // padding 12
    return (<div className={Base.p12}>
        <Icon color={colors[color]} width={sizes[width]} height={sizes[height]}  />
    </div>)
}

6、base Component

虽然,我们有最佳的主题管理方案。但仅仅靠原子css布局,效率虽然很高,但是写起来很繁琐、冗余。还需要在此基础构建,通用且可复用的组件。比如,基础的layouts组件,文本组件,Flex组件等

以文本组件为例:

type TextProps = {
  size?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17
  line?: 'under' | 'over' | 'through' | 'blink' | 'none' | 'inherit'
  weight?: 'lightest' | 'lighter' | 'light' | 'normal' | 'medium' | 'bold' | 'bolder' | 'boldest'
  align?: 'center' | 'left' | 'right' | 'justify' | 'none' | 'inherit'
  italic?: boolean
} & HTMLAttributes<HTMLElement>

/**
 * 文本组件 props {@link TextProps}
 * @param param0
 * @returns
 */
export const Text: FC<TextProps> = ({
  children,
  size,
  color = '',
  className = '',
  italic = false,
  weight,
  style,
  align
}) => {
  const Base = useBaseCss()
  const classes = useMemo(() => {
    let c = [
      size ? Base[`text${size}`] : '',
      color ? Base[`fc${capitalize(color)}`] : '',
      weight ? Base[`fw${capitalize(weight)}`] : '',
      italic ? Base.italic : '',
      align ? Base[`al${capitalize(align)}`] : ''
    ]

    return c
  }, [size, color, italic,weight,align])

  return (
    <span className={cssx(classes, className)} style={style}>
      {children}
    </span>
  )
}

Icon组件为例:

import { useColors, useSizes } from '@/hooks/theme'

export interface IconProps {
  name: string
  color?: string
  size?: number | string
  prefix?: string
  style?: React.CSSProperties
  className?: string
}

const { prefix: appPrefix } = __CONFIG__

const Icon: FC<IconProps> = (props) => {
  let { name = '', color = 'grey3', size = 4, prefix = appPrefix, className, style } = props

  let { classes: colorClasses } = useColors({ fill: color })

  let { classes } = useSizes({
    width: size,
    height: size
  })

  return (
    <div className={cssx(IconCss.icon, classes, className)} style={style}>
      <svg className={cssx(classes, colorClasses)}>
        <use xlinkHref={`#${prefix}-${name}`}></use>
      </svg>
    </div>
  )
}

除了Text组件和Icon组件,布局常用的基础组件,还包括:

  • Box组件:基本容器,尺寸、边距、边框、圆角、背景等
  • FlexBox:Flex布局组件,主轴对齐、辅助轴对齐、主轴方向(行/列)、wrap,等
  • Image:图片组件
  • Table组件:行、列、分页、表头、排序
  • 等等

当然,最理想的方式,制定团队内部的UI规范,并在此基础开发一套UI组件库。如果,时间和精力都不允许,市场上也有很多成熟的组件库。而且,大多数组件库,都有自己的基础配置,跟theme.config.js适配并不是很难。

四、总结

这篇文章,我们讨论样式管理最佳实践。基本的思路,也就是本篇文章的写作顺序和思路,其实并不复杂。下面画了一个思维导图,算是一个总结吧。

基于vite2+react+typescript前端开发工程化(二)
转载自:https://juejin.cn/post/7242876385734262841
评论
请登录