TypeScript Vite

初始化项目

这里项目名是 fe-project-base,这里采用的 vite 2.0 来初始化项目

  1. npm init @vitejs/app fe-project-base --template react-ts

这个时候,会出现命令行提示,咱们按照自己想要的模板,选择对应初始化类型就 OK 了

安装项目依赖

首先,需要安装依赖,要打造一个基本的前端单页应用模板,咱们需要安装以下依赖:

  1. react & react-dom:基础核心
  2. react-router:路由配置
  3. @loadable/component:动态路由加载
  4. classnames:更好的 className 写法
  5. react-router-config:更好的 react-router 路由配置包
  6. mobx-react & mobx-persist:mobx 状态管理
  7. eslint & lint-staged & husky & prettier:代码校验配置
  8. eslint-config-alloy:ESLint 配置插件

dependencies:

  1. npm install --save react react-dom react-router @loadable/component classnames react-router-config mobx-react mobx-persist

devDependencies:

  1. npm install --save-dev eslint lint-staged husky@4.3.8 prettier

pre-commit 配置

在安装完上面的依赖之后,通过 cat .git/hooks/pre-commit 来判断 husky 是否正常安装,如果不存在该文件,则说明安装失败,需要重新安装试试
这里的 husky 使用 4.x 版本,5.x 版本已经不是免费协议了 测试发现 node/14.15.1 版本会导致 husky 自动创建 .git/hooks/pre-commit 配置失败,升级 node/14.16.0 修复该问题
在完成了以上安装配置之后,还需要对 package.json 添加相关配置

  1. {
  2. "husky": {
  3. "hooks": {
  4. "pre-commit": "lint-staged"
  5. }
  6. },
  7. "lint-staged": {
  8. "src/**/*.{ts,tsx}": [
  9. "eslint --cache --fix",
  10. "git add"
  11. ],
  12. "src/**/*.{js,jsx}": [
  13. "eslint --cache --fix",
  14. "git add"
  15. ]
  16. },
  17. }

到这里,整个项目就具备了针对提交的文件做 ESLint 校验并修复格式化的能力了
Vite   React   TypeScript 构建实战 - 图1ESLintError

编辑器配置

工欲善其事必先利其器,首要解决的是在团队内部编辑器协作问题,这个时候,就需要开发者的编辑器统一安装 EditorConfig 插件(这里以 vscode 插件为例)
首先,在项目根目录新建一个配置文件:.editorconfig 参考配置:

  1. root = true
  2. [*]
  3. indent_style = space
  4. indent_size = 2
  5. end_of_line = lf
  6. charset = utf-8
  7. trim_trailing_whitespace = true
  8. insert_final_newline = true

配置自动格式化与代码校验 在 vscode 编辑器中,Mac 快捷键 command+, 来快速打开配置项,切换到 workspace 模块,并点击右上角的 open settings json 按钮,配置如下信息:

  1. {
  2. "editor.formatOnSave": true,
  3. "editor.codeActionsOnSave": {
  4. "source.fixAll.tslint": true
  5. },
  6. "editor.defaultFormatter": "esbenp.prettier-vscode",
  7. "[javascript]": {
  8. "editor.formatOnSave": true,
  9. "editor.defaultFormatter": "esbenp.prettier-vscode"
  10. },
  11. "[typescript]": {
  12. "editor.defaultFormatter": "esbenp.prettier-vscode"
  13. },
  14. "typescript.tsdk": "node_modules/typescript/lib",
  15. "[typescriptreact]": {
  16. "editor.defaultFormatter": "esbenp.prettier-vscode"
  17. }
  18. }

这个时候,咱们的编辑器已经具备了保存并自动格式化的功能了

ESLint + Prettier

关于 ESLint 与 Prettier 的关系,可以移步这里:彻底搞懂 ESLint 和 Prettier
1、.eslintignore:配置 ESLint 忽略文件
2、.eslintrc:ESLint 编码规则配置,这里推荐使用业界统一标准,这里推荐 AlloyTeam 的 eslint-config-alloy,按照文档安装对应的 ESLint 配置:

  1. npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy

3、.prettierignore:配置 Prettier 忽略文件
4、.prettierrc:格式化自定义配置

  1. {
  2. "singleQuote": true,
  3. "tabWidth": 2,
  4. "bracketSpacing": true,
  5. "trailingComma": "none",
  6. "printWidth": 100,
  7. "semi": false,
  8. "overrides": [
  9. {
  10. "files": ".prettierrc",
  11. "options": { "parser": "typescript" }
  12. }
  13. ]
  14. }

选择 eslint-config-alloy 的几大理由如下:

  1. 更清晰的 ESLint 提示:比如特殊字符需要转义的提示等等

    1. error `'` can be escaped with `'`, `‘`, `'`, `’` react/no-unescaped-entities
  2. 更加严格的 ESLint 配置提示:比如会提示 ESLint 没有配置指明 React 的 version 就会告警

    1. Warning: React version not specified in eslint-plugin-react settings. See https://github.com/yannickcr/eslint-plugin-react#configuration

    这里补上对 react 版本的配置

    1. // .eslintrc
    2. {
    3. "settings": {
    4. "react": {
    5. "version": "detect" // 表示探测当前 node_modules 安装的 react 版本
    6. }
    7. }
    8. }

    整体目录规划

    一个基本的前端单页应用,需要的大致的目录架构如下:
    这里以 src 下面的目录划分为例

    1. .
    2. ├── app.tsx
    3. ├── assets // 静态资源,会被打包优化
    4. ├── favicon.svg
    5. └── logo.svg
    6. ├── common // 公共配置,比如统一请求封装,session 封装
    7. ├── http-client
    8. └── session
    9. ├── components // 全局组件,分业务组件或 UI 组件
    10. ├── Toast
    11. ├── config // 配置文件目录
    12. ├── index.ts
    13. ├── hooks // 自定义 hook
    14. └── index.ts
    15. ├── layouts // 模板,不同的路由,可以配置不同的模板
    16. └── index.tsx
    17. ├── lib // 通常这里防止第三方库,比如 jweixin.js、jsBridge.js
    18. ├── README.md
    19. ├── jsBridge.js
    20. └── jweixin.js
    21. ├── pages // 页面存放位置
    22. ├── components // 就近原则页面级别的组件
    23. ├── home
    24. ├── routes // 路由配置
    25. └── index.ts
    26. ├── store // 全局状态管理
    27. ├── common.ts
    28. ├── index.ts
    29. └── session.ts
    30. ├── styles // 全局样式
    31. ├── global.less
    32. └── reset.less
    33. └── utils // 工具方法
    34. └── index.ts

    到这里规划好了一个大致的前端项目目录结构,接下来要配置一下别名,来优化代码中的,比如:import xxx from'@/utils' 路径体验
    通常这里还会有一个 public 目录与 src 目录同级,该目录下的文件会直接拷贝到构建目录
    别名配置
    别名的配置,需要关注的是两个地方:vite.config.ts & tsconfig.json
    其中 vite.config.ts 用来编译识别用的;tsconfig.json 是用来给 Typescript 识别用的;
    这里建议采用的是 @/ 开头,为什么不用 @ 开头,这是为了避免跟业界某些 npm 包名冲突(例如 @vitejs)

  • vite.config.ts

    1. // vite.config.ts
    2. {
    3. resolve: {
    4. alias: {
    5. '@/': path.resolve(__dirname, './src'),
    6. '@/config': path.resolve(__dirname, './src/config'),
    7. '@/components': path.resolve(__dirname, './src/components'),
    8. '@/styles': path.resolve(__dirname, './src/styles'),
    9. '@/utils': path.resolve(__dirname, './src/utils'),
    10. '@/common': path.resolve(__dirname, './src/common'),
    11. '@/assets': path.resolve(__dirname, './src/assets'),
    12. '@/pages': path.resolve(__dirname, './src/pages'),
    13. '@/routes': path.resolve(__dirname, './src/routes'),
    14. '@/layouts': path.resolve(__dirname, './src/layouts'),
    15. '@/hooks': path.resolve(__dirname, './src/hooks'),
    16. '@/store': path.resolve(__dirname, './src/store')
    17. }
    18. },
    19. }
  • tsconfig.json

    1. {
    2. "compilerOptions": {
    3. "paths": {
    4. "@/*": ["./src/*"],
    5. "@/components/*": ["./src/components/*"],
    6. "@/styles/*": ["./src/styles/*"],
    7. "@/config/*": ["./src/config/*"],
    8. "@/utils/*": ["./src/utils/*"],
    9. "@/common/*": ["./src/common/*"],
    10. "@/assets/*": ["./src/assets/*"],
    11. "@/pages/*": ["./src/pages/*"],
    12. "@/routes/*": ["./src/routes/*"],
    13. "@/hooks/*": ["./src/hooks/*"],
    14. "@/store/*": ["./src/store/*"]
    15. },
    16. "typeRoots": ["./typings/"]
    17. },
    18. "include": ["./src", "./typings", "./vite.config.ts"],
    19. "exclude": ["node_modules"]
    20. }

    从 0 到 1 Vite 构建配置

    当前使用的 vite 版本为 vite/2.1.2

    配置文件

    默认的 vite 初始化项目,是不会给创建 .env.env.production.env.devlopment 三个配置文件的,然后官方模板默认提供的 package.json 文件中,三个 script 分别会要用到这几个文件,所以需要手动先创建,这里提供官方文档:.env 配置

    1. # package.json
    2. {
    3. "scripts": {
    4. "dev": "vite", // 等于 vite -m development,此时 command='serve',mode='development'
    5. "build": "tsc && vite build", // 等于 vite -m production,此时 command='build', mode='production'
    6. "serve": "vite preview",
    7. "start:qa": "vite -m qa" // 自定义命令,会寻找 .env.qa 的配置文件;此时 command='serve'mode='qa'
    8. }
    9. }

    同时这里的命令,对应的配置文件:mode 区分

    1. import { ConfigEnv } from 'vite'
    2. export default ({ command, mode }: ConfigEnv) => {
    3. // 这里的 command 默认 === 'serve'
    4. // 当执行 vite build 时,command === 'build'
    5. // 所以这里可以根据 command 与 mode 做条件判断来导出对应环境的配置
    6. }

    具体配置文件参考:fe-project-vite/vite.config.ts

    路由规划

    首先,一个项目最重要的部分,就是路由配置;那么需要一个配置文件作为入口来配置所有的页面路由,这里以 react-router 为例:

    路由配置文件配置

    src/routes/index.ts,这里引入的了 @loadable/component 库来做路由动态加载,vite 默认支持动态加载特性,以此提高程序打包效率

    1. import loadable from '@loadable/component'
    2. import Layout, { H5Layout } from '@/layouts'
    3. import { RouteConfig } from 'react-router-config'
    4. import Home from '@/pages/home'
    5. const routesConfig: RouteConfig[] = [
    6. {
    7. path: '/',
    8. exact: true,
    9. component: Home
    10. },
    11. // hybird 路由
    12. {
    13. path: '/hybird',
    14. exact: true,
    15. component: Layout,
    16. routes: [
    17. {
    18. path: '/',
    19. exact: false,
    20. component: loadable(() => import('@/pages/hybird'))
    21. }
    22. ]
    23. },
    24. // H5 相关路由
    25. {
    26. path: '/h5',
    27. exact: false,
    28. component: H5Layout,
    29. routes: [
    30. {
    31. path: '/',
    32. exact: false,
    33. component: loadable(() => import('@/pages/h5'))
    34. }
    35. ]
    36. }
    37. ]
    38. export default routesConfig

    入口 main.tsx 文件配置路由路口

    1. import React from 'react'
    2. import ReactDOM from 'react-dom'
    3. import { BrowserRouter } from 'react-router-dom'
    4. import '@/styles/global.less'
    5. import { renderRoutes } from 'react-router-config'
    6. import routes from './routes'
    7. ReactDOM.render(
    8. {renderRoutes(routes)},
    9. document.getElementById('root')
    10. )

    这里面的 renderRoutes 采用的 react-router-config 提供的方法,其实就是咱们 react-router 的配置写法,通过查看 源码 如下:

    1. import React from "react";
    2. import { Switch, Route } from "react-router";
    3. function renderRoutes(routes, extraProps = {}, switchProps = {}) {
    4. return routes ? (
    5. {routes.map((route, i) => ( route.render ? ( route.render({ ...props, ...extraProps, route: route }) ) : ( ) } /> ))}
    6. ) : null; } export default renderRoutes;

    通过以上两个配置,咱们就基本能把项目跑起来了,同时也具备了路由的懒加载能力;
    执行 npm run build,查看文件输出,就能发现动态路由加载已经配置成功了

    1. $ tsc && vite build
    2. vite v2.1.2 building for production...
    3. 53 modules transformed.
    4. dist/index.html 0.41kb
    5. dist/assets/index.c034ae3d.js 0.11kb / brotli: 0.09kb
    6. dist/assets/index.c034ae3d.js.map 0.30kb
    7. dist/assets/index.f0d0ea4f.js 0.10kb / brotli: 0.09kb
    8. dist/assets/index.f0d0ea4f.js.map 0.29kb
    9. dist/assets/index.8105412a.js 2.25kb / brotli: 0.89kb
    10. dist/assets/index.8105412a.js.map 8.52kb
    11. dist/assets/index.7be450e7.css 1.25kb / brotli: 0.57kb
    12. dist/assets/vendor.7573543b.js 151.44kb / brotli: 43.17kb
    13. dist/assets/vendor.7573543b.js.map 422.16kb
    14. Done in 9.34s.

    细心的同学可能会发现,上面咱们的路由配置里面,特意拆分了两个 Layout & H5Layout,这里这么做的目的是为了区分在微信 h5 与 hybird 之间的差异化而设置的模板入口,大家可以根据自己的业务来决定是否需要 Layout 层

    样式处理

    说到样式处理,这里咱们的示例采用的是 .less 文件,所以在项目里面需要安装对应的解析库
    npm install —save-dev less postcss
    如果要支持 css modules 特性,需要在 vite.config.ts 文件中开启对应的配置项:

    1. // vite.config.ts
    2. {
    3. css: {
    4. preprocessorOptions: {
    5. less: {
    6. // 支持内联 JavaScript
    7. javascriptEnabled: true
    8. }
    9. },
    10. modules: {
    11. // 样式小驼峰转化,
    12. //css: goods-list => tsx: goodsList
    13. localsConvention: 'camelCase'
    14. }
    15. },
    16. }

    编译构建

    其实到这里,基本就讲完了 vite 的整个构建,参考前面提到的配置文件:

    1. export default ({ command, mode }: ConfigEnv) => {
    2. const envFiles = [
    3. /** mode local file */ `.env.${mode}.local`,
    4. /** mode file */ `.env.${mode}`,
    5. /** local file */ `.env.local`,
    6. /** default file */ `.env`
    7. ]
    8. const { plugins = [], build = {} } = config
    9. const { rollupOptions = {} } = build
    10. for (const file of envFiles) {
    11. try {
    12. fs.accessSync(file, fs.constants.F_OK)
    13. const envConfig = dotenv.parse(fs.readFileSync(file))
    14. for (const k in envConfig) {
    15. if (Object.prototype.hasOwnProperty.call(envConfig, k)) {
    16. process.env[k] = envConfig[k]
    17. }
    18. }
    19. } catch (error) {
    20. console.log('配置文件不存在,忽略')
    21. }
    22. }
    23. const isBuild = command === 'build'
    24. // const base = isBuild ? process.env.VITE_STATIC_CDN : '//localhost:3000/'
    25. config.base = process.env.VITE_STATIC_CDN
    26. if (isBuild) {
    27. // 压缩 Html 插件
    28. config.plugins = [...plugins, minifyHtml()]
    29. }
    30. if (process.env.VISUALIZER) {
    31. const { plugins = [] } = rollupOptions
    32. rollupOptions.plugins = [
    33. ...plugins,
    34. visualizer({
    35. open: true,
    36. gzipSize: true,
    37. brotliSize: true
    38. })
    39. ]
    40. }
    41. // 在这里无法使用 import.meta.env 变量
    42. if (command === 'serve') {
    43. config.server = {
    44. // 反向代理
    45. proxy: {
    46. api: {
    47. target: process.env.VITE_API_HOST,
    48. changeOrigin: true,
    49. rewrite: (path: any) => path.replace(/^\/api/, '')
    50. }
    51. }
    52. }
    53. }
    54. return config
    55. }

    在这里,利用了一个 dotenv 的库,来将配置的内容绑定到 process.env 上面供配置文件使用
    详细配置请参考 demo:https://github.com/lichenbuliren/fe-project-base

    构建优化

  1. 为了更好地、更直观的知道项目打包之后的依赖问题,可以通过 rollup-plugin-visualizer 包来实现可视化打包依赖
  2. 在使用自定义的环境构建配置文件,在 .env.custom 中,配置

    1. # .env.custom
    2. NODE_ENV=production

    截止版本 vite@2.1.5,官方存在一个 BUG,上面的 NODE_ENV=production 在自定义配置文件中不生效,可以通过以下方式兼容

    1. // vite.config.ts
    2. const config = {
    3. ...
    4. define: {
    5. 'process.env.NODE_ENV': '"production"'
    6. }
    7. ...
    8. }
  3. antd-mobile 按需加载,配置如下:

    1. import vitePluginImp from 'vite-plugin-imp'
    2. // vite.config.ts
    3. const config = {
    4. plugins: [
    5. vitePluginImp({
    6. libList: [
    7. {
    8. libName: 'antd-mobile',
    9. style: (name) => `antd-mobile/es/${name}/style`,
    10. libDirectory: 'es'
    11. }
    12. ]
    13. })
    14. ]
    15. }

    以上配置,在本地开发模式下能保证 antd 正常运行,但是,在执行 build 命令之后,在服务器访问会报一个错误Vite   React   TypeScript 构建实战 - 图2,类似 issue 可以参考解决方案 手动安装单独安装 indexof npm 包:npm install indexof

    mobx6.x + react + typescript 实践

    在使用 mobx 的时候,版本已经是 mobx@6.x,发现这里相比于旧版本,API 的使用上有了一些差异,特地在这里分享下踩坑经历

    Store 划分

    store 的划分,主要参考本文的示例 需要注意的是,在 store 初始化的时候,如果需要数据能够响应式绑定,需要在初始化的时候,给默认值,不能设置为 undefined 或者 null,这样子的话,数据是无法实现响应式的

    1. // store.ts
    2. import { makeAutoObservable, observable } from 'mobx'
    3. class CommonStore {
    4. // 这里必须给定一个初始化的只,否则响应式数据不生效
    5. title = ''
    6. theme = 'default'
    7. constructor() {
    8. // 这里是实现响应式的关键
    9. makeAutoObservable(this)
    10. }
    11. setTheme(theme: string) {
    12. this.theme = theme
    13. }
    14. setTitle(title: string) {
    15. this.title = title
    16. }
    17. }
    18. export default new CommonStore()

    Store 注入

    mobx@6x的数据注入,采用的 reactcontext 特性;主要分成以下三个步骤

    根节点变更

    通过 Provider 组件,注入全局 store

    1. // 入口文件 app.tsx
    2. import { Provider } from 'mobx-react'
    3. import counterStore from './counter'
    4. import commonStore from './common'
    5. const stores = {
    6. counterStore,
    7. commonStore
    8. }
    9. ReactDOM.render(
    10. {renderRoutes(routes)},
    11. document.getElementById('root')
    12. )

    这里的 Provider 是由 mobx-react 提供的 通过查看源码可以发现, Provier内部实现也是 React Context:

    1. // mobx-react Provider 源码实现
    2. import React from "react"
    3. import { shallowEqual } from "./utils/utils"
    4. import { IValueMap } from "./types/IValueMap"
    5. // 创建一个 Context
    6. export const MobXProviderContext = React.createContext({})
    7. export interface ProviderProps extends IValueMap {
    8. children: React.ReactNode
    9. }
    10. export function Provider(props: ProviderProps) {
    11. // 除开 children 属性,其他的都作为 store 值
    12. const { children, ...stores } = props
    13. const parentValue = React.useContext(MobXProviderContext)
    14. // store 引用最新值
    15. const mutableProviderRef = React.useRef({ ...parentValue, ...stores })
    16. const value = mutableProviderRef.current
    17. if (__DEV__) {
    18. const newValue = { ...value, ...stores } // spread in previous state for the context based stores
    19. if (!shallowEqual(value, newValue)) {
    20. throw new Error(
    21. "MobX Provider: The set of provided stores has changed. See: https://github.com/mobxjs/mobx-react#the-set-of-provided-stores-has-changed-error."
    22. )
    23. }
    24. }
    25. return {children}
    26. }
    27. // 供调试工具显示 Provider 名称
    28. Provider.displayName = "MobXProvider"

    Store 使用

    因为函数组件没法使用注解的方式,所以咱们需要使用自定义 Hook 的方式来实现:

    1. // useStore 实现
    2. import { MobXProviderContext } from 'mobx-react'
    3. import counterStore from './counter'
    4. import commonStore from './common'
    5. const _store = {
    6. counterStore,
    7. commonStore
    8. }
    9. export type StoreType = typeof _store
    10. // 声明 store 类型
    11. interface ContextType {
    12. stores: StoreType
    13. }
    14. // 这两个是函数声明,重载
    15. function useStores(): StoreType
    16. function useStores<T extends keyof StoreType>(storeName: T): StoreType[T]
    17. /**
    18. * 获取根 store 或者指定 store 名称数据
    19. * @param storeName 指定子 store 名称
    20. * @returns typeof StoreType[storeName]
    21. */
    22. function useStores<T extends keyof StoreType>(storeName?: T) {
    23. // 这里的 MobXProviderContext 就是上面 mobx-react 提供的
    24. const rootStore = React.useContext(MobXProviderContext)
    25. const { stores } = rootStore as ContextType
    26. return storeName ? stores[storeName] : stores
    27. }
    28. export { useStores }

    组件引用通过自定义组件引用 store

    1. import React from 'react'
    2. import { useStores } from '@/hooks'
    3. import { observer } from 'mobx-react'
    4. // 通过 Observer 高阶组件来实现
    5. const HybirdHome: React.FC = observer((props) => {
    6. const commonStore = useStores('commonStore')
    7. return (
    8. <>
    9. <div>Welcome Hybird Homediv>
    10. <div>current theme: {commonStore.theme}div>
    11. <button type="button" onClick={() => commonStore.setTheme('black')}>
    12. set theme to black
    13. button>
    14. <button type="button" onClick={() => commonStore.setTheme('red')}>
    15. set theme to red
    16. button>
    17. )
    18. })
    19. export default HybirdHome

    可以看到前面设计的自定义 Hook,通过 Typescript 的特性,能够提供友好的代码提示
    Vite   React   TypeScript 构建实战 - 图3
    以上就是整个 mobx+typescript 在函数式组件中的实际应用场景了