webpack统治的江湖
在当下的前端工程化工具中,webpack是当之无愧的老大哥,经过多年的发展和大量的生产实践,webpack已然成为最主流的前端模块打包工具且社区全面。
webpack在编译时会从一个入口文件(entry.js)开始,将其依赖的所有js或者其他配置loader的资源全部打包到一个文件(bundle.js)。
当修改文件时,webpack会重新构建js文件。因此随着工程体量的增加,webpack构建的时间也会增加,因此可能导致一次更改需要长达十几秒才能完成编译。
后起之秀vite
vite(法语意为 "快速的")是一种新型前端构建工具,能够显著提升前端开发体验。vite主要由一个开箱即用的开发服务器和构建工具rollup的组成,类似webpack + webpack-dev-server,但是vite的开发服务器基于 原生 ES 模块和esbuild,模块热更新(HMR)速度快到惊人。相比之下,vite更轻更快。
esbuild是一个JavaScript的打包和和压缩工具,最主要的一个特征就是 有极致的性能,那么它到底有多快,参考esbuild官方提供的一张图!
vite工作的方式则不一样,vite只会将当前正在使用的文件或模块转换成原声ES模块,而且这个过程由esbuild完成,其执行速度比webpack快10-100倍,并且由于vite的工作机制,热更新时间不会随工程体量增加儿增加。
基于vite的react工程脚手架
一直以来,我司依赖umijs构建工程模板,umi3内部依赖webpack4。经过几个中大型项目的开发之后,缓慢的应用构建和HMR终于让我们开始寻求新的方案。在体验过vite秒启动之后,我非常想将之用于实践,因此基于vite,我设计了一个脚手架工具neeco(内测中),内部采用许多比较新的依赖,以积累下一代的前端工具链在生产实践中的经验。
尽管现在umi4已经发布,并提供 mfsu以及基于vite或esbuild构建,但经过实测(umi@4.0.11),发现其配置和使用并没有符合预期。另一个方面,相比于umi捆绑的数据流dva,我更倾向基于react hooks的数据流 zustand和 constate。这并非是想表达umi不好,相反,在目前的项目开发中,umi依然是首选的。我想寻求的是在开发阶段能带来更优体验的工具,但在生产环境下,稳定性和兼容性才是首选,这并不冲突。neeco内的很多设计思路都来自于umi,在后续的发展中,会考虑使用umi构建生产包。
设计定位
- 充分的开发约定,开箱即用;
- 全面拥抱
hooks
,逻辑独立且易移植。
开发约定
在基础设施的构建上,neeco采用了以约定为主。
目录结构和约定式路由
├── src
│ ├── layouts
│ │ ├── BasicLayout.tsx
│ │ ├── index.tsx
│ ├── pages
│ │ ├── index.less
│ │ └── index.tsx
│ │ └── useMeta.ts
│ ├── utils // 推荐目录
│ │ └── index.ts
│ ├── useInitialState.ts
│ ├── global.(css|less|sass|scss)
这与umi非常相似,以便在不修改代码的情况下,umi同样可以进行打包。
src目录下主要是layouts目录和pages目录。
layouts/index.tsx
约定式路由时的全局布局文件,实际上是在路由外面套了一层。
pages目录
所有路由组件存放在这里。使用约定式路由时,约定 pages
下所有的 (j|t)sx?
文件即路由。使用约定式路由,意味着不需要维护可怕的路由配置文件。最常用的有基础路由和动态路由(用于详情页等,需要从 url 取参数的情况)。
基础路由
假设 pages
目录结构如下:
+ pages/
+ users/
- index.tsx
- index.tsx
那么,会自动生成路由配置如下:
[
{ path: '/', component: './pages/index.tsx' },
{ path: '/users/', component: './pages/users/index.tsx' }
]
动态路由
约定,带 $
前缀的目录或文件为动态路由。若 $
后不指定参数名,则代表 *
通配,比如以下目录结构:
+ pages/
+ foo/
- $slug.tsx
+ $bar/
- $.tsx
- index.tsx
会生成路由配置如下:
[
{ path: '/', component: './pages/index.tsx' },
{ path: '/foo/:slug', component: './pages/foo/$slug.tsx' },
{ path: '/:bar/*', component: './pages/$bar/$.tsx' }
]
pages/404.tsx
当访问的路由地址不存在时,会自动显示 404 页面,并生成路由 /404
。
useMeta
useMeta
是关于菜单信息的react hook
。见约定式菜单
约定式菜单
相比于繁琐的路由配置,菜单的配置要少许多,因为并非所有的路由都需要在菜单中展示出来。从实际项目经验中可以得出,菜单与路由之间有着较为紧密的联系,在约定式路由的基础上,可以同时生成约定式菜单。
export type MenuItem = Required<MenuProps>['items'][number] & {
children?: MenuItem[];
label?: ReactNode;
title: string;
key: string;
notInMenu?: boolean;
/**
* 菜单顺序,从小到大
*/
index?: number;
};
菜单的层级结构和路径可以从约定式路由中获取,但菜单名称、顺序等需要额外定义。因此在约定式路由的基础上,增加了useMeta.ts
文件,用于定义菜单的相关信息。
type Meta = Omit<MenuItem, 'key' | 'children'>;
例如在/home/useMeta.ts
中设置菜单名称、菜单顺序等:
defineMeta可以提供typescript代码提示,实际效果相当于(params) => ({...params})
import { defineMeta } from 'neeco';
export default defineMeta({
title: '首页',
index: 1
});
notInMenu
可以表明该路由不在菜单中显示,并且其子路径也都不会出现在菜单中。
例如在/login/useMeta.ts
中表明/login
路由不出现在菜单中:
import { defineMeta } from 'neeco';
export default defineMeta({
notInMenu: true
});
useMeta
会以自定义react hooks
的方式调用,因此,你可以在此使用编写一些hooks逻辑,比如动态更改菜单名称:
import type { Meta } from 'neeco';
import { useState, useEffect } from 'react';
export default () => {
const [title, setTitle] = useState('首页');
useEffect(() => {
setTimeout(() => {
setTitle('导航页')
}, 1000)
}, []);
return {
title
} as Meta;
}
不建议在useMeta.ts
中编写过于复杂的逻辑,因为useMeta
并非是按需加载的,neeco
会一次执行完所有的useMeta
来生成菜单。
useInitialState
src/useInitialState.ts
是一个在所有路由逻辑之前执行的hook,可以在这里进行用户鉴权,初始化数据等。
const useInitialState = () => ({
name: 'demo-project',
loaded: true,
});
export default useInitialState;
loaded
是一个默认返回为true
参数,当它为false
时,会展示一个全局的loading状态,并且不会渲染任何页面。
页面文件中可以通过useInitialStateStore
获取useInitialState
返回的数据:
import { useInitialStateStore } from 'neeco';
const App = () => {
const { initialState } = useInitialStateStore()
return <div>{initialState.name}</div>
}
export default App;
内置hooks
neeco
的约定基本都是基于react hooks
,除此之外,还有其他的内置hooks
可用。
useFetch
useFetch
是一个用于发送http请求的hook,核心使用use-http,使用方式基本同use-http
:
import { useFetch } from 'neeco';
const App = () => {
const { data, get } = useFetch('/api/userInfo')
return <div>
<button onClick={get}>点击获取用户名</button>
<p>{data.name}</p>
</div>
}
export default App;
拦截器Interceptor
Interceptor
可以为请求和响应增加一些通用的额外内容,比如为请求添加token
:
<Interceptor options={{ header: { Authorization: token } }}>
<App />
</Interceptor>
统一处理响应数据。例如接口的响应体结构为:
interface Response {
status: string; // 200为成功响应
data: any;
errMsg: string;
}
可以在响应拦截器中统一处理异常数据提示和响应成功判定:
const onResponse = (response, { onSuccess }) => {
const { status, data, errMsg } = response;
if(status === "200") {
onSuccess(data)
} else {
Message.error(errMsg);
}
}
<Interceptor
options={{ header: { Authorization: token } }}
onResponse={onResponse}
>
<App />
</Interceptor>
当响应失败时,在页面上弹出错误消息提示;当响应成功时,调用onSuccess
方法,这在处理一些局部数据更新时非常有用。
import { useFetch } from 'neeco';
const App = () => {
// 第二个参数是配置项;第三个参数是调用依赖,有这个参数时,会自动调用此接口。考详情can
const { data, get } = useFetch('/api/user/detail', {
// interceptor: {} // 也可以单独给这个接口添加拦截器
}, []);
const { post } = useFetch('/api/user/edit', {
onSuccess: get
})
return <div>
<button onClick={() => {
post({ name: 'jack' })
}}>修改用户名为jack</button>
<p>{data.name}</p>
</div>
}
export default App;
在上面的代码中,初始化时会自动调用/api/user/detail
接口获取用户信息,点击按钮则会通过接口/api/user/edit
修改用户名为jack
,修改成功后(接口响应为200
),触发onSuccess
回调,再调用/api/user/detail
更新用户信息。
国际化
neeco
基于zuoy提供了自动国际化配置,进行国际化开发时无需额外配置,直接编写代码即可:
import { useZuoyIntl } from 'neeco'
const App = () => {
const z = useZuoyIntl();
return <div>{z('你好,妮蔻')}</div>
}
可以通过命令行执行npx zy build
,zuoy
会按照配置文件(参加zuoy-使用配置文件)自动生成如下文件:
├── src
│ ├── locales
│ │ ├── zh-CN.json
│ │ ├── en-US.json
国际化配置完成。
开源
一片文章的篇幅有限,neeco
还有很多其他的功能就不一一描述了。按照计划,neeco
将在2023年春节前后发布1.0
版本,并遵循MIT
协议在github上开源,欢迎关注。
neeco@1.0
的主要目标是提效,下一个主要版本的目标是安全,计划有如下内容:
- 取消页面请求
- 停止State更新
- 检查内存泄露
- 监控告罄
- 自动测试
如果在这些方面你有独到的见解,请在评论区留下宝贵的建议,非常感谢。
后语
vite的出现极大地提升了前端开发的效率,没有哪个程序员能拒绝毫秒级响应,可见vite的发展潜力是巨大的。正如其官网所说,vite为下一代的前端工具链提供极速响应,它充分利用了现代浏览器提供的能力,但也因此无法在老旧版本的浏览器中得到支持。但随着社会环境的发展,老旧版本浏览器即将退出历史舞台,前端技术也将迎来一场新的风暴。