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为下一代的前端工具链提供极速响应,它充分利用了现代浏览器提供的能力,但也因此无法在老旧版本的浏览器中得到支持。但随着社会环境的发展,老旧版本浏览器即将退出历史舞台,前端技术也将迎来一场新的风暴。
 
                            