应用 Vite + React + Typescript 打造一个前端单页利用模板
最近前端大火的 Vite 2.0 版本终于进去了,在这里分享一下如何应用 vite 构建一个前端单页利用
该文章次要面向对 Vite 感兴趣,或者做前端我的项目架构的同学
源码地址,欢送 star 跟踪最新变更:fe-project-base
通过这篇文章,你能理解到以下几点:
- vscode 编辑器配置
- git pre-commit 如何配置
- ESLint + Pritter 配置
- 规范前端单页利用目录布局
- 从 0 到 1 学习 vite 构建优化
- mobx/6.x + react + TypeScript 最佳实际
想疾速理解 Vite 配置构建的,能够间接跳到 这里
初始化我的项目
这里咱们我的项目名是 fe-project-base
这里咱们采纳的 vite 2.0 来初始化咱们的我的项目
npm init @vitejs/app fe-project-base --template react-ts
这个时候,会呈现命令行提醒,咱们依照本人想要的模板,抉择对应初始化类型就 OK 了
装置我的项目依赖
首先,咱们须要装置依赖,要打造一个根本的前端单页利用模板,咱们须要装置以下依赖:
react
&react-dom
:根底外围react-router
:路由配置@loadable/component
:动静路由加载classnames
:更好的 className 写法react-router-config
:更好的 react-router 路由配置包mobx-react
&mobx-persist
:mobx 状态治理eslint
&lint-staged
&husky
&prettier
:代码校验配置eslint-config-alloy
:ESLint 配置插件
dependencies:
npm install --save react react-dom react-router @loadable/component classnames react-router-config mobx-react mobx-persist
devDependencies:
npm install --save-dev eslint lint-staged husky@4.3.8 prettier
pre-commit 配置
在装置完下面的依赖之后,通过 cat .git/hooks/pre-commit
来判断 husky 是否失常装置,如果不存在该文件,则阐明装置失败,须要重新安装试试
<span style=”color:red;font-weight:bold;”>
这里的 husky 应用 4.x 版本,5.x 版本曾经不是收费协定了 <br/> 测试发现 node/14.15.1 版本会导致 husky 主动创立 .git/hooks/pre-commit 配置失败,降级 node/14.16.0 修复该问题
</span>
在实现了以上装置配置之后,咱们还须要对 package.json
增加相干配置
{
"husky": {
"hooks": {"pre-commit": "lint-staged"}
},
"lint-staged": {"src/**/*.{ts,tsx}": [
"eslint --cache --fix",
"git add"
],
"src/**/*.{js,jsx}": [
"eslint --cache --fix",
"git add"
]
},
}
到这里,咱们的整个我的项目就具备了针对提交的文件做 ESLint 校验并修复格式化的能力了
<span id=”editor”> 编辑器配置 </span>
工欲善其事必先利其器,咱们首要解决的是在团队外部编辑器合作问题,这个时候,就须要开发者的编辑器对立装置 EditorConfig 插件(这里以 vscode 插件为例)
首先,咱们在我的项目根目录新建一个配置文件:.editorconfig
参考配置:
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
配置主动格式化与代码校验
在 vscode 编辑器中,Mac 快捷键 command + ,
来疾速关上配置项,切换到 workspace
模块,并点击右上角的 open settings json
按钮,配置如下信息:
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {"source.fixAll.tslint": true},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"": {"editor.formatOnSave": true,"editor.defaultFormatter":"esbenp.prettier-vscode"},"[typescript]": {"editor.defaultFormatter":"esbenp.prettier-vscode"},"typescript.tsdk":"node_modules/typescript/lib","[typescriptreact]": {"editor.defaultFormatter":"esbenp.prettier-vscode"}
}
这个时候,咱们的编辑器曾经具备了保留并主动格式化的性能了
<span id=”eslint”>ESLint + Prettier</span>
对于 ESLint 与 Prettier 的关系,能够移步这里:彻底搞懂 ESLint 和 Prettier
.eslintignore
:配置 ESLint 疏忽文件.eslintrc
:ESLint 编码规定配置,这里举荐应用业界统一标准,这里我举荐 AlloyTeam 的 eslint-config-alloy,依照文档装置对应的 ESLint 配置:-
npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy
.prettierignore
:配置 Prettier 疏忽文件-
.prettierrc
:格式化自定义配置{ "singleQuote": true, "tabWidth": 2, "bracketSpacing": true, "trailingComma": "none", "printWidth": 100, "semi": false, "overrides": [ { "files": ".prettierrc", "options": {"parser": "typescript"} } ] }
抉择
eslint-config-alloy
的几大理由如下: -
更清晰的 ESLint 提醒:比方特殊字符须要本义的提醒等等
error `'` can be escaped with `'`, `‘`, `'`, `’` react/no-unescaped-entities
-
更加严格的 ESLint 配置提醒:比方会提醒 ESLint 没有配置指明 React 的 version 就会告警
Warning: React version not specified in eslint-plugin-react settings. See https://github.com/yannickcr/eslint-plugin-react#configuration
这里咱们补上对
react
版本的配置// .eslintrc { "settings": { "react": {"version": "detect" // 示意探测以后 node_modules 装置的 react 版本} } }
<span id=”dir”> 整体目录布局 </span>
一个根本的前端单页利用,须要的大抵的目录架构如下:
这里以 src
上面的目录划分为例
.
├── app.tsx
├── assets // 动态资源,会被打包优化
│ ├── favicon.svg
│ └── logo.svg
├── common // 公共配置,比方对立申请封装,session 封装
│ ├── http-client
│ └── session
├── components // 全局组件,分业务组件或 UI 组件
│ ├── Toast
├── config // 配置文件目录
│ ├── index.ts
├── hooks // 自定义 hook
│ └── index.ts
├── layouts // 模板,不同的路由,能够配置不同的模板
│ └── index.tsx
├── lib // 通常这里避免第三方库,比方 jweixin.js、jsBridge.js
│ ├── README.md
│ ├── jsBridge.js
│ └── jweixin.js
├── pages // 页面寄存地位
│ ├── components // 就近准则页面级别的组件
│ ├── home
├── routes // 路由配置
│ └── index.ts
├── store // 全局状态治理
│ ├── common.ts
│ ├── index.ts
│ └── session.ts
├── styles // 全局款式
│ ├── global.less
│ └── reset.less
└── utils // 工具办法
└── index.ts
OK,到这里,咱们布局好了一个大抵的前端我的项目目录构造,接下来咱们要配置一下别名,来优化代码中的,比方:import xxx from '@/utils'
门路体验
通常这里还会有一个 public 目录与 src 目录同级,该目录下的文件会间接拷贝到构建目录
别名配置
别名的配置,咱们须要关注的是两个中央:vite.config.ts
& tsconfig.json
其中 vite.config.ts
用来编译辨认用的;tsconfig.json
是用来给 Typescript 辨认用的;
这里倡议采纳的是 @/
结尾,为什么不必 @
结尾,这是为了防止跟业界某些 npm 包名抵触(例如 @vitejs)
vite.config.ts
// vite.config.ts
{
resolve: {
alias: {'@/': path.resolve(__dirname, './src'),
'@/config': path.resolve(__dirname, './src/config'),
'@/components': path.resolve(__dirname, './src/components'),
'@/styles': path.resolve(__dirname, './src/styles'),
'@/utils': path.resolve(__dirname, './src/utils'),
'@/common': path.resolve(__dirname, './src/common'),
'@/assets': path.resolve(__dirname, './src/assets'),
'@/pages': path.resolve(__dirname, './src/pages'),
'@/routes': path.resolve(__dirname, './src/routes'),
'@/layouts': path.resolve(__dirname, './src/layouts'),
'@/hooks': path.resolve(__dirname, './src/hooks'),
'@/store': path.resolve(__dirname, './src/store')
}
},
}
tsconfig.json
{
"compilerOptions": {
"paths": {"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/styles/*": ["./src/styles/*"],
"@/config/*": ["./src/config/*"],
"@/utils/*": ["./src/utils/*"],
"@/common/*": ["./src/common/*"],
"@/assets/*": ["./src/assets/*"],
"@/pages/*": ["./src/pages/*"],
"@/routes/*": ["./src/routes/*"],
"@/hooks/*": ["./src/hooks/*"],
"@/store/*": ["./src/store/*"]
},
"typeRoots": ["./typings/"]
},
"include": ["./src", "./typings", "./vite.config.ts"],
"exclude": ["node_modules"]
}
<span id=”vite”> 从 0 到 1 Vite 构建配置 </span>
截止作者写该篇文章时,vite
版本为 vite/2.1.2
,以下所有配置仅针对该版本负责
配置文件
默认的 vite
初始化我的项目,是不会给咱们创立 .env
,.env.production
,.env.devlopment
三个配置文件的,而后官网模板默认提供的 package.json
文件中,三个 script
别离会要用到这几个文件,所以须要咱们 手动先创立,这里提供官网文档:.env 配置
# package.json
{
"scripts": {
"dev": "vite", // 等于 vite -m development,此时 command='serve',mode='development'
"build": "tsc && vite build", // 等于 vite -m production,此时 command='build', mode='production'
"serve": "vite preview",
"start:qa": "vite -m qa" // 自定义命令,会寻找 .env.qa 的配置文件; 此时 command='serve',mode='qa'
}
}
同时这里的命令,对应的配置文件:mode 辨别
import {ConfigEnv} from 'vite'
export default ({command, mode}: ConfigEnv) => {
// 这里的 command 默认 === 'serve'
// 当执行 vite build 时,command === 'build'
// 所以这里能够依据 command 与 mode 做条件判断来导出对应环境的配置
}
具体配置文件参考:fe-project-vite/vite.config.ts
路由布局
首先,一个我的项目最重要的局部,就是路由配置;那么咱们须要一个配置文件作为入口来配置所有的页面路由,这里以 react-router
为例:
路由配置文件配置
src/routes/index.ts
,这里咱们引入的了 @loadable/component
库来做路由动静加载,vite 默认反对动静加载个性,以此进步程序打包效率
import loadable from '@loadable/component'
import Layout, {H5Layout} from '@/layouts'
import {RouteConfig} from 'react-router-config'
import Home from '@/pages/home'
const routesConfig: RouteConfig[] = [
{
path: '/',
exact: true,
component: Home
},
// hybird 路由
{
path: '/hybird',
exact: true,
component: Layout,
routes: [
{
path: '/',
exact: false,
component: loadable(() => import('@/pages/hybird'))
}
]
},
// H5 相干路由
{
path: '/h5',
exact: false,
component: H5Layout,
routes: [
{
path: '/',
exact: false,
component: loadable(() => import('@/pages/h5'))
}
]
}
]
export default routesConfig
入口 main.tsx 文件配置路由路口
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter} from 'react-router-dom'
import '@/styles/global.less'
import {renderRoutes} from 'react-router-config'
import routes from './routes'
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>{renderRoutes(routes)}</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
)
这里的面的 renderRoutes
采纳的 react-router-config
提供的办法,其实就是咱们 react-router
的配置写法,通过查看 源码 如下:
import React from "react";
import {Switch, Route} from "react-router";
function renderRoutes(routes, extraProps = {}, switchProps = {}) {
return routes ? (<Switch {...switchProps}>
{routes.map((route, i) => (
<Route
key={route.key || i}
path={route.path}
exact={route.exact}
strict={route.strict}
render={props =>
route.render ? (route.render({ ...props, ...extraProps, route: route})
) : (<route.component {...props} {...extraProps} route={route} />
)
}
/>
))}
</Switch>
) : null;
}
export default renderRoutes;
通过以上两个配置,咱们就根本能把我的项目跑起来了,同时也具备了路由的懒加载能力;
执行 npm run build
,查看文件输入,就能发现咱们的动静路由加载曾经配置胜利了
$ tsc && vite build
vite v2.1.2 building for production...
✓ 53 modules transformed.
dist/index.html 0.41kb
dist/assets/index.c034ae3d.js 0.11kb / brotli: 0.09kb
dist/assets/index.c034ae3d.js.map 0.30kb
dist/assets/index.f0d0ea4f.js 0.10kb / brotli: 0.09kb
dist/assets/index.f0d0ea4f.js.map 0.29kb
dist/assets/index.8105412a.js 2.25kb / brotli: 0.89kb
dist/assets/index.8105412a.js.map 8.52kb
dist/assets/index.7be450e7.css 1.25kb / brotli: 0.57kb
dist/assets/vendor.7573543b.js 151.44kb / brotli: 43.17kb
dist/assets/vendor.7573543b.js.map 422.16kb
✨ Done in 9.34s.
仔细的同学可能会发现,下面咱们的路由配置外面,特意拆分了两个 Layout
& H5Layout
,这里这么做的目标是为了辨别在微信 h5 与 hybird 之间的差异化而设置的模板入口,大家能够依据本人的业务来决定是否须要 Layout
层
款式解决
说到款式解决,这里咱们的示例采纳的是 .less
文件,所以在我的项目外面须要装置对应的解析库
npm install --save-dev less postcss
如果要反对 css modules
个性,须要在 vite.config.ts
文件中开启对应的配置项:
// vite.config.ts
{
css: {
preprocessorOptions: {
less: {
// 反对内联 JavaScript
javascriptEnabled: true
}
},
modules: {
// 款式小驼峰转化,
//css: goods-list => tsx: goodsList
localsConvention: 'camelCase'
}
},
}
编译构建
其实到这里,根本就讲完了 vite 的整个构建,参考后面提到的配置文件:
export default ({command, mode}: ConfigEnv) => {
const envFiles = [/** mode local file */ `.env.${mode}.local`,
/** mode file */ `.env.${mode}`,
/** local file */ `.env.local`,
/** default file */ `.env`
]
const {plugins = [], build = {}} = config
const {rollupOptions = {} } = build
for (const file of envFiles) {
try {fs.accessSync(file, fs.constants.F_OK)
const envConfig = dotenv.parse(fs.readFileSync(file))
for (const k in envConfig) {if (Object.prototype.hasOwnProperty.call(envConfig, k)) {process.env[k] = envConfig[k]
}
}
} catch (error) {console.log('配置文件不存在,疏忽')
}
}
const isBuild = command === 'build'
// const base = isBuild ? process.env.VITE_STATIC_CDN : '//localhost:3000/'
config.base = process.env.VITE_STATIC_CDN
if (isBuild) {
// 压缩 Html 插件
config.plugins = [...plugins, minifyHtml()]
}
if (process.env.VISUALIZER) {const { plugins = [] } = rollupOptions
rollupOptions.plugins = [
...plugins,
visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
]
}
// 在这里无奈应用 import.meta.env 变量
if (command === 'serve') {
config.server = {
// 反向代理
proxy: {
api: {
target: process.env.VITE_API_HOST,
changeOrigin: true,
rewrite: (path: any) => path.replace(/^\/api/, '')
}
}
}
}
return config
}
在这里,咱们利用了一个 dotenv
的库,来帮咱们将配置的内容绑定到 process.env
下面供咱们配置文件应用
具体配置请参考 demo
构建优化
- 为了更好地、更直观的晓得我的项目打包之后的依赖问题,咱们,咱们能够通过
rollup-plugin-visualizer
包来实现可视化打包依赖 -
在应用自定义的环境构建配置文件,在
.env.custom
中,配置# .env.custom NODE_ENV=production
截止版本
vite@2.1.5
,官网存在一个 BUG,下面的NODE_ENV=production
在自定义配置文件中不失效,能够通过以下形式兼容// vite.config.ts const config = { ... define: {'process.env.NODE_ENV': '"production"'} ... }
-
antd-mobile
按需加载,配置如下:import vitePluginImp from 'vite-plugin-imp' // vite.config.ts const config = { plugins: [ vitePluginImp({ libList: [ { libName: 'antd-mobile', style: (name) => `antd-mobile/es/${name}/style`, libDirectory: 'es' } ] }) ] }
以上配置,在本地开发模式下能保障 antd 失常运行,然而,在执行
build
命令之后,在服务器拜访会报一个谬误
,相似 issue 能够参考解决方案
手动装置独自装置indexof
npm 包:npm install indexof
<span id=”mobx”>mobx6.x + react + typescript 实际 </span>
作者在应用 mobx
的时候,版本曾经是 mobx@6.x
,发现这里相比于旧版本,API 的应用上有了一些差别,顺便在这里分享下踩坑经验
Store 划分
store 的划分,次要参考本文的示例
须要留神的是,在 store 初始化的时候,如果须要数据可能响应式绑定,须要在初始化的时候,给默认值,不能设置为 undefined 或者 null,这样子的话,数据是无奈实现响应式的
// store.ts
import {makeAutoObservable, observable} from 'mobx'
class CommonStore {
// 这里必须给定一个初始化的只,否则响应式数据不失效
title = ''theme ='default'
constructor() {
// 这里是实现响应式的要害
makeAutoObservable(this)
}
setTheme(theme: string) {this.theme = theme}
setTitle(title: string) {this.title = title}
}
export default new CommonStore()
Store 注入
mobx@6x
的数据注入,采纳的 react
的 context
个性;次要分成以下三个步骤
根节点变更
通过 Provider
组件,注入全局 store
// 入口文件 app.tsx
import {Provider} from 'mobx-react'
import counterStore from './counter'
import commonStore from './common'
const stores = {
counterStore,
commonStore
}
ReactDOM.render(
<React.StrictMode>
<Provider stores={stores}>
<BrowserRouter>{renderRoutes(routes)}</BrowserRouter>
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
这里的 Provider 是由 mobx-react 提供的
通过查看源码咱们会发现,Provier
外部实现也是 React Context:
// mobx-react Provider 源码实现
import React from "react"
import {shallowEqual} from "./utils/utils"
import {IValueMap} from "./types/IValueMap"
// 创立一个 Context
export const MobXProviderContext = React.createContext<IValueMap>({})
export interface ProviderProps extends IValueMap {children: React.ReactNode}
export function Provider(props: ProviderProps) {
// 除开 children 属性,其余的都作为 store 值
const {children, ...stores} = props
const parentValue = React.useContext(MobXProviderContext)
// store 援用最新值
const mutableProviderRef = React.useRef({...parentValue, ...stores})
const value = mutableProviderRef.current
if (__DEV__) {const newValue = { ...value, ...stores} // spread in previous state for the context based stores
if (!shallowEqual(value, newValue)) {
throw new Error("MobX Provider: The set of provided stores has changed. See: https://github.com/mobxjs/mobx-react#the-set-of-provided-stores-has-changed-error.")
}
}
return <MobXProviderContext.Provider value={value}>{children}</MobXProviderContext.Provider>
}
// 供调试工具显示 Provider 名称
Provider.displayName = "MobXProvider"
Store 应用
因为函数组件没法应用注解的形式,所以咱们须要应用自定义 Hook
的形式来实现:
// useStore 实现
import {MobXProviderContext} from 'mobx-react'
import counterStore from './counter'
import commonStore from './common'
const _store = {
counterStore,
commonStore
}
export type StoreType = typeof _store
// 申明 store 类型
interface ContextType {stores: StoreType}
// 这两个是函数申明,重载
function useStores(): StoreType
function useStores<T extends keyof StoreType>(storeName: T): StoreType[T]
/**
* 获取根 store 或者指定 store 名称数据
* @param storeName 指定子 store 名称
* @returns typeof StoreType[storeName]
*/
function useStores<T extends keyof StoreType>(storeName?: T) {
// 这里的 MobXProviderContext 就是下面 mobx-react 提供的
const rootStore = React.useContext(MobXProviderContext)
const {stores} = rootStore as ContextType
return storeName ? stores[storeName] : stores
}
export {useStores}
组件援用通过自定义组件援用 store
import React from 'react'
import {useStores} from '@/hooks'
import {observer} from 'mobx-react'
// 通过 Observer 高阶组件来实现
const HybirdHome: React.FC = observer((props) => {const commonStore = useStores('commonStore')
return (
<>
<div>Welcome Hybird Home</div>
<div>current theme: {commonStore.theme}</div>
<button type="button" onClick={() => commonStore.setTheme('black')}>
set theme to black
</button>
<button type="button" onClick={() => commonStore.setTheme('red')}>
set theme to red
</button>
</>
)
})
export default HybirdHome
能够看到后面咱们设计的自定义 Hook,通过 Typescript
的个性,可能提供敌对的代码提醒
以上就是整个 mobx + typescript
在函数式组件中的理论利用场景了;
如果有什么问题,欢送评论交换 :)
参考资料
- React Hook useContext
- Mobx 官网文档
- vite 构建案例 vite-concent-pro