文章大纲:
- react hooks在业务中的应用
- react hooks与react class的使用区别
- 使用tsx编写自己的组件
- 用webpack+node将组件编译成es5
- 使用lerna将项目改造为monorepo
- 将组件发布到npm
- 使用github actions实现gh-pages页面的自动化构建
react hooks在业务中的应用
react hooks出了有一段时间,hooks的各种用法也渐渐被大家所了解,在体验过hooks写法之后,对于react,又有了新的感受,那就是大道至简,react又一次从写法层面上革新了我们对于面向UI编程的理解。
于是我选择在保证业务稳定的情况下,将后台项目中用到的各个组件以及页面从class组件迁移到hooks组件。
hooks的用法我就不赘述了,网上有很多文章可以入门hooks,这里仅仅做一点拓展,分享一些解决方案,让大家能高效率地进行业务开发,并写出优雅的代码。
- immutablejs 这是配合hooks编写高性能react代码的必备法宝,语雀 使用的就是react+immutable.
- umi/hooks umi虽然是类nextjs的偏脚手架的“框架”,但这并不妨碍阿里的react developer在此基础上进行创新,相比于ant design,我更加偏爱umi/hooks这个开源项目,因为它从hooks的角度出发,写了大量hooks逻辑复用的范例,是一个具有开创性的项目,配合antd,它能快速构建应用,节省了许多写相关逻辑的时间,这个开源项目的官网用的是pwa,没有使用ssr,所以目前在百度搜索不到,点击链接访问或者自行谷歌。
react hooks与react class的使用区别
之前写angular以及react的时候,经常有一个疑惑飘荡在我的脑海里——为什么要使用class进行开发,而不是更加简洁的function。
hooks给出了答案,angular却相信class依然可以沿用下去。
事实上,对于java、C#这类语言,class是其逻辑复用的核心,后端代码,某种意义上并没有前端这么复杂,前端代码里面混杂着视图和数据相关的逻辑,而数据往往是异步的,这使得数据相关逻辑更加难以复用,所以前端代码复用一直是一个难题。
既然前端页面无法通过class进行复用,那class还有什么用呢?答案可能是,能更方便地实现生命周期。(虽然react提供了mixin的功能,但react并不推荐通过mixin进行逻辑复用,详情请看这里)
组件/页面有了生命周期之后,就可以精准控制其在什么时候加载,什么时候销毁,这对于spa应用而言,十分重要,虽然现在的硬件设备的性能都足够强,但开发者始终考虑的是,在保证功能的前提下,提升开发效率的同时,最大限度地节省硬件资源。
不过,对于绝大部分组件而言,其实并不需要生命周期,它们的作用仅仅用于展示,最常见的场景可能是等待数据请求完毕之后将数据填充到指定位置,像这样的功能,使用handlebars就能很简单地实现,但handlebars这内裤无法做到双向数据绑定,于是就有了emberjs,不过问题依然存在——组件化,随着浏览器的更新换代,在抛弃了ie这个大包袱之后,react应运而生,谷歌也在angularjs的基础上重造出了angular,两年之后,尤雨溪吸收了两者的思想,并融入了对于web开发的思考,开源了vue。
技术始终是服务于商业的。“互联网的下半场”,其实说的是,现在早已不是多学多用的阶段,如今知而不精等于没用,因为现在开发者要做到的不仅仅是在工期内把一个产品开发出来,还需要进行多个维度的构建和优化,交互层面、设计层面、代码层面、运营层面、用户心理层面、市场层面、技术层面...
现在需要的是综合实力强者,但这无法投机取巧,需要开发者在各个领域的天赋以及后期长时间的学习积累。
从某种意义上讲,框架即开发者,开发者即框架。所以“互联网的下半场”,框架同样得是综合实力强者。
于是开发者和框架开发者们开始思考,如何防止劣币驱逐良币的情况出现,面对抄袭者、跟风者,最优解就是降维打击——从创造力这个角度入手,不断地创造新的解决方案去解决当下的各种问题,不仅仅是造轮子,而且还要给给过去制造的马车装上发动机,让它变成汽车。
言归正传,react的“进化”其实代表了一种趋势——变者生,留者逝。
从代码层面上讲,class组件其实是有冗余的,不要小瞧这种冗余,当页面达到成百上千之后,它产生的影响可能会让人难以想象。从逻辑复用层面上讲,class组件很难进行逻辑复用,只能进行组件化。
虽然react提供了pureComponent这样的组件,但还是没有解决根本问题,无状态组件上面依然挂载了一堆没用的属性和方法。对于有代码洁癖的人来说,这是无法让人忍受的。
在使用了hooks之后,react代码能够做到最大程度的分割,过去可能一个复杂的页面或者组件需要700行,而且由于class的缘故,无法进行原子级别的代码分离,现在hooks将useState这类逻辑从组件中抽象出来,把代码粒度做到了最细,不仅让代码更加易读,更加容易追踪代码逻辑,而且在抛弃了生命周期之后,开发者只需要关注数据逻辑,hooks用一种非常functional的方式将视图逻辑“压缩”成了数据逻辑,这种变化,让开发者面对复杂度更低的代码,某种程度上讲,降低了react的门槛。
使用tsx编写自己的组件
npx create-react-app my-app --typescript
在编程领域,有一个概念叫做“防御型编程”:
防御性编程是一种细致、谨慎的编程方法。为了开发可靠的软件,我们要设计系统中的每个组件,以使其尽可能地“保护”自己。我们通过明确地在代码中对设想进行检查,击碎了未记录下来的设想。这是一种努力,防止(或至少是观察)我们的代码以将会展现错误行为的方式被调用。
而typescript提供给我们的,是一种被动形式的防御性编程,我将之成为“收敛模式”。它从语法层面规范编码人员需要按照它的规范来进行编码,不仅如此,它还提供更为高级的语法特性供开发者使用,这种在语言之上通过编译器构建另一种语言的模式近年来被kotlin以及typescript所采用。
与创造一门新的编程语言不同的是,依赖编译器的高级语言可以直接对接目标语言的生态和社区,并通过吸收社区的内容来壮大自身,这种模式远比创造一种新的语言来的有效。
如何在react中使用typescript,这里以我写的image组件为例:
Image.tsx
import React, { useState, useEffect, useRef } from 'react'
import styles from './Image.less'
interface IProps {
width?: string,
height?: string,
borderRadius?: string,
src: string,
alt?: string,
mode?: "scaleToFill" | "aspectFit" | "aspectFill" | "aspectFillWidth" | "aspectFillHeight" | "widthFix" | "top" | "bottom" | "center" | "left" | "right" | "topLeft" | "topRight" | "bottomLeft" | "bottomRight",
lazy?: boolean,
}
const Index: React.FC<IProps> = (props) => {
const {
width = '300px',
height = '225px',
borderRadius = '0px',
src = '',
alt = 'image',
mode = 'aspectFill',
lazy = false
} = props
const [state_img_style, setStateImgStyle] = useState({})
const [state_img_src, setStateImgSrc] = useState('')
const container_ref = useRef<HTMLDivElement>(null)
const img_ref = useRef<HTMLImageElement>(null)
useEffect(() => {
let _width: number
let _height: number
const { current } = img_ref
if (width && height && container_ref.current) {
if (width === '100%') {
_width = container_ref.current.offsetWidth
} else {
_width = Number(width.split('').filter(item => Number.isSafeInteger(Number(item))).join(''))
}
if (height === '100%') {
throw Error('height can`t be 100%!')
} else {
_height = Number(height.split('').filter(item => Number.isSafeInteger(Number(item))).join(''))
}
if (current) {
if (lazy) {
let io = new IntersectionObserver((e) => {
const _e = e[e.length - 1]
if (_e.isIntersecting) {
setStateImgSrc(src)
io.unobserve(current)
}
})
io.observe(current)
} else {
setStateImgSrc(src)
}
if (mode === 'aspectFill') {
current.onload = () => {
const { naturalWidth, naturalHeight } = current
const ratio_natural = naturalWidth / naturalHeight
const ratio_preset = _width / _height
if (ratio_natural > ratio_preset) {
setStateImgStyle({
maxHeight: '100%'
})
} else {
setStateImgStyle({
maxWidth: '100%'
})
}
}
}
}
}
}, [height, lazy, mode, src, width])
const style: object = {
width: width,
height: mode === 'widthFix' ? 'auto' : height,
borderRadius: borderRadius,
overflow: 'hidden'
}
return (
<div
ref={container_ref}
className={`${styles._local}`}
style={{
width: '100%'
}}
>
<div
className={`${mode}`}
style={style}
>
<img
ref={img_ref}
className='img'
src={state_img_src}
alt={alt}
style={state_img_style}
/>
</div>
</div>
)
}
export default React.memo(Index)
使用typescript,让我最满意的一点就是它会根据类型判断当前值是否有可能为空,然后让你必须写判断,这里就避免出现存在undefine的值导致代码报错的情况。
还需要给react组件写类型定义文件:
index.d.ts
import React from "react";
interface IProps {
width?: string,
height?: string,
borderRadius?: string,
src: string,
alt?: string,
mode?: "scaleToFill" | "aspectFit" | "aspectFill" | "aspectFillWidth" | "aspectFillHeight" | "widthFix" | "top" | "bottom" | "center" | "left" | "right" | "topLeft" | "topRight" | "bottomLeft" | "bottomRight",
lazy?: boolean
}
declare const Image: React.FC<IProps>;
export default Image;
并通过package.json的types字段指定路径:
_package.json
{
"name": "rokit-image",
"version": "1.2.0",
"description": "image component of rokit,inspire by miniapp image component.",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/MatrixAge/rokit.git"
},
"keywords": [
"react",
"component",
"js",
"rokit",
"miniapp",
"wxapp"
],
"author": "matrixage",
"license": "MIT",
"bugs": {
"url": "https://github.com/MatrixAge/rokit/issues"
},
"homepage": "https://github.com/MatrixAge/rokit#readme"
}
用webpack+node将组件编译成es5
组件写好了,还需要将其编译成es5,这样就能发布给其他react开发者使用了,我使用的是monorepo来管理组件,所以组件打包之后也得组件与组件之间进行分离。这里需要自行用node操作文件并执行webpack配置:
lib_build.js
const path = require('path')
const glob = require('glob')
const fs = require('fs-extra')
const webpack = require('webpack')
const config = require('../config/lib_webpack.config')
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages')
const { appComponents, appComponentsSource } = require('../config/paths')
const build = () => {
fs.emptyDirSync(appComponents)
console.log('building components...')
const compiler = webpack(config)
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages
if (err) {
if (!err.message) {
return reject(err)
}
messages = formatWebpackMessages({
errors: [ err.message ],
warnings: []
})
}
else {
messages = formatWebpackMessages(stats.toJson({ all: false, warnings: true, errors: true }))
}
if (messages.errors.length) {
if (messages.errors.length > 1) {
messages.errors.length = 1
}
return reject(new Error(messages.errors.join('\n\n')))
}
return resolve({
stats,
warnings: messages.warnings
})
})
})
}
const copy = () => {
const copyFiles = (name, target_name) => {
const files = glob.sync(path.join(appComponentsSource, `/**/${name}`))
files.map((item) => {
const array_package_path = item.split('/')
const package_name = array_package_path[array_package_path.length - 2]
fs.copyFileSync(item, `${appComponents}/${package_name}/${target_name}`)
})
}
copyFiles('index.d.ts', 'index.d.ts')
copyFiles('_package.json', 'package.json')
copyFiles('readme.md', 'readme.md')
}
build()
.then((res) => {
if (res) {
copy()
console.log('components building success!')
}
})
.catch((err) => {
console.log(err)
})
这里用了一个glob库,用来批量操作文件,很好用,强烈推荐。然后是webpack配置:
lib_webpack.config.js
const path = require('path')
const glob = require('glob')
const isWsl = require('is-wsl')
const TerserPlugin = require('terser-webpack-plugin')
const safePostCssParser = require('postcss-safe-parser')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const { appComponents } = require('./paths')
process.env.BABEL_ENV = 'production'
process.env.NODE_ENV = 'production'
const getComponents = (path) => {
const files = glob.sync(path)
let entry = {}
files.map((item) => {
const array_package_path = item.split('/')
const package_name = array_package_path[array_package_path.length - 2]
entry[package_name] = item
})
return entry
}
module.exports = [
{
mode: 'production',
entry: getComponents(path.resolve(__dirname, '../src/components/**/index.tsx')),
target: 'web',
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: [
{
loader: 'babel-loader'
}
],
exclude: /node_modules/
},
{
test: /\.(css|less)$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
},
{
loader: 'less-loader',
options: {
modules: true
}
}
]
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
externals: {
react: 'react'
},
output: {
filename: '[name]/index.js',
path: appComponents,
libraryTarget: 'commonjs2'
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
ecma: 8
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2
},
mangle: {
safari10: true
},
keep_classnames: false,
keep_fnames: false,
output: {
ecma: 5,
comments: false,
ascii_only: true
}
},
parallel: !isWsl,
cache: true,
sourceMap: false
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: false
}
})
]
}
}
]
这里有两个容易出问题的点:
其一就是output.target配置:目前模块化标准有commonjs、amd、cmd以及es6 modules,而webpack对上述模块化标准的导出方式有所不同,详情请看这里,我这里使用的是commonjs2。
其二就是externals配置:如果在组件中已经引入react而在打包时没有将react给排除,那么有可能会因为多次引入react而报错。
使用lerna将项目改造为monorepo
rokit组件库的目标是提供单个好用的组件,想用哪个就安装哪个,不需要像一些UI框架那样上一个全家桶,虽然webpack有tree-shaking,但全家桶还是心智负担太重了些,所以我使用lerna来管理组件的发布,与此同时,修改了之间webpack配置,将编译之后的组件导入到了packages文件夹。
无论是新项目,还是现有项目,执行lerna init,然后修改目录结构为lerna指定的结构即可享受lerna publish带来的爽快体验了。下面是lerna.json的配置:
{
"packages": [
"packages/*"
],
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"ignoreChanges": [
"ignored-file",
"*.md"
]
},
"bootstrap": {
"npmClientArgs": [
"--no-package-lock"
]
}
}
}
这是rokit的文件目录结构:
将组件发布到npm
百度搜索npm,到npm官网使用邮箱注册一个账号,账号名称要international一点,最好单词本身带有语义,比如matrixage,这样能够加强认知,不要取一些自以为是狗屁不通的英文名字,我之前就取过vertical(直男)、verts(vert.x)这些,后来自己都不记得为什么叫这个了,于是就弃用了。
注册完成之后,到项目目录,npm login,输入账号密码,然后使用lerna publish选择版号即可发布到npm了,或是到你要发布的组件的目录执行npm publish --access=publish.
哦,对了,最好写一个readme,介绍一下项目的目标以及用法。
使用github actions实现gh-pages页面的自动化构建
今年github开源了github actions,可以通过github直接进行构建部署,通过编写yml文件(类似于dockerfile),可以进行自动化构建和部署,同时还可以直接引用别人的action,并在此基础上进行扩展,也算是自动化的一种延伸吧。
下面说一下用法,在项目目录新建一个.github文件夹,在.github文件夹中新建一个workflows文件夹,在workflows文件夹下新建一个ci.yml文件,内容如下:
name: Deploy gh-pages
on:
push:
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Build
run: npm install && npm run build:site
- name: Deploy
uses: peaceiris/actions-gh-pages@v2
env:
ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
PUBLISH_BRANCH: gh-pages
PUBLISH_DIR: ./build
里面有一个ACTIONS_DEPLOY_KEY,如何使用请看这里。另外,阮一峰老师的Github Actions入门 里面的脚本有点问题,生成文件之后无法触发gh-pages的自动构建流程,于是一番google之后找到了解决方案,就是上述代码咯。
至此,一个组件的开发到构建发布以及网站部署的流程就走完了,后续有什么不懂的请看这里。