Use Vite + React + Typescript to create a front-end single-page application template
Recently, the front-end Vite 2.0 version has finally come out, here is how to use vite to build a front-end single-page application
This article is mainly for students who are interested in Vite or who are doing front-end project architecture
Source address, welcome to star to track the latest changes: fe-project-base
Through this article, you can learn the following:
- vscode editor configuration
- how git pre-commit configuration
- ESLint + Pritter configuration
- standard front-end single-page application directory planning
- from 0 to 1 to build and optimize
- mobx/6.x + react + TypeScript best practices
If you want to quickly understand the Vite configuration and construction, you can jump directly to here
Initialize the project
Here our project name is fe-project-base
Here we use vite 2.0 to initialize our project
npm init @vitejs/app fe-project-base --template react-ts
At this time, a command line prompt will appear, let's select the corresponding initialization type according to the template we want, and it will be OK
Install project dependencies
First, we need to install dependencies. To create a basic front-end single-page application template, we need to install the following dependencies:
react
&react-dom
: basic corereact-router
: routing configuration@loadable/component
: dynamic route loadingclassnames
: Better writing of classNamereact-router-config
: Better react-router routing configuration packagemobx-react
&mobx-persist
: mobx state managementeslint
&lint-staged
&husky
&prettier
: code verification configurationeslint-config-alloy
: ESLint configuration plugin
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 configuration
After installing the above dependencies, use cat .git/hooks/pre-commit
to determine whether husky is installed normally. If the file does not exist, the installation has failed and you need to reinstall it.
<span style="color:red;font-weight:bold;">
Husky here uses 4.x version, 5.x version is no longer a free agreement<br/>The test found that node/14.15.1 version will cause Husky to automatically create .git/hooks/pre-commit configuration failure, upgrade node/14.16 .0 fix the problem
</span>
After completing the above installation and configuration, we also need to add related configuration 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"
]
},
}
At this point, our entire project has the ability to do ESLint verification and repair the formatting of the submitted files.
<span id="editor">Editor configuration</span>
your . The first thing we solve is the problem of editor collaboration within the team. At this time, you need to install the 160819d8abbc65 EditorConfig 160819d8abbc67 plug-in in the editor of the developer (here, take the vscode plug-in as an example)
First, we create a new configuration file in the project root directory: .editorconfig
Reference configuration:
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
configure automatic formatting and code verification
In the vscode editor, Mac shortcut key command + ,
to quickly open the configuration items, switch to the workspace
module, and click the open settings json
button in the upper right corner to configure the following information:
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.tslint": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"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"
}
}
At this time, our editor already has the function of saving and formatting automatically
<span id="eslint">ESLint + Prettier</span>
Regarding the relationship between ESLint and Prettier, you can move here: thoroughly understand ESLint and Prettier
.eslintignore
: Configure ESLint to ignore files.eslintrc
: ESLint coding rule configuration, here it is recommended to use the industry unified standard, here I recommend AlloyTeam's eslint-config-alloy , install the corresponding ESLint configuration according to the document:npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy
.prettierignore
: Configure Prettier to ignore files.prettierrc
: Format custom configuration{ "singleQuote": true, "tabWidth": 2, "bracketSpacing": true, "trailingComma": "none", "printWidth": 100, "semi": false, "overrides": [ { "files": ".prettierrc", "options": { "parser": "typescript" } } ] }
The eslint-config-alloy
are as follows:
Clearer ESLint prompts: such as prompts that special characters need to be escaped, etc.
error `'` can be escaped with `'`, `‘`, `'`, `’` react/no-unescaped-entities
Strict ESLint configuration prompts: for example, it will prompt that ESLint is not configured and specify the version of React and an alarm will be issued
Warning: React version not specified in eslint-plugin-react settings. See https://github.com/yannickcr/eslint-plugin-react#configuration
Here we fill in the configuration
react
// .eslintrc { "settings": { "react": { "version": "detect" // 表示探测当前 node_modules 安装的 react 版本 } } }
<span id="dir">Overall directory planning</span>
A basic front-end single-page application requires a rough directory structure as follows:
Here is an example of the directory division 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, at this point, we have planned a rough front-end project directory structure. Next, we need to configure aliases to optimize the code, such as: import xxx from '@/utils'
path experience
Usually there will be a public directory at the same level as the src directory, and the files in this directory will be copied directly to the build directory
Alias configuration
For alias configuration, we need to pay attention to two places: vite.config.ts
& tsconfig.json
Among them, vite.config.ts
used to compile and recognize; tsconfig.json
is used to recognize Typescript;
It is recommended to @/
, why not @
, this is to avoid conflicts with some npm package names in the industry (for example, @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">Vite build configuration from 0 to 1</span>
As of the time the author writes this article, the vite
version is vite/2.1.2
, and all the following configurations are only responsible for this version
Configuration file
The default vite
initialization project will not create three configuration files for us, .env.production
, .env.devlopment
.env
package.json
files provided by the official template by default, the three script
will use these files respectively, so we need Manually create first, here is the official document: .env configuration
# 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'
}
}
At the same time, the command here, the corresponding configuration file: mode distinguish
import { ConfigEnv } from 'vite'
export default ({ command, mode }: ConfigEnv) => {
// 这里的 command 默认 === 'serve'
// 当执行 vite build 时,command === 'build'
// 所以这里可以根据 command 与 mode 做条件判断来导出对应环境的配置
}
Specific configuration file reference: fe-project-vite/vite.config.ts
Routing planning
First of all, the most important part of a project is the routing configuration; then we need a configuration file as the entry point to configure all page routing, here is react-router
as an example:
Routing profile configuration
src/routes/index.ts
, here we have introduced the @loadable/component
library for dynamic routing loading, vite supports dynamic loading feature default to improve the efficiency of program packaging
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
Entrance main.tsx file configuration routing junction
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')
)
The method provided by renderRoutes
used by the react-router-config
here is react-router
. By viewing the source code as follows:
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;
Through the above two configurations, we can basically run the project, and at the same time have the lazy loading ability of routing;
Execute npm run build
and check the file output, you can find that our dynamic route loading has been configured successfully
$ 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.
Attentive students may find that in our routing configuration above, two Layout
& H5Layout
deliberately split. The purpose of doing this here is to distinguish the template entry set for the difference between WeChat h5 and hybrid. You can Decide whether you need the Layout
layer according to your own business
Style processing
Speaking of style processing, our example here uses the .less
file, so the corresponding parsing library needs to be installed in the project
npm install --save-dev less postcss
If you want to support the css modules
feature, you need to enable the corresponding configuration item vite.config.ts
// vite.config.ts
{
css: {
preprocessorOptions: {
less: {
// 支持内联 JavaScript
javascriptEnabled: true
}
},
modules: {
// 样式小驼峰转化,
//css: goods-list => tsx: goodsList
localsConvention: 'camelCase'
}
},
}
Compile and build
In fact, at this point, I have basically finished the entire construction of vite. Refer to the configuration file mentioned earlier:
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
}
Here, we use a dotenv
library to help us bind the configuration content to process.env
for our configuration file to use
For detailed configuration, please refer to demo
Build optimization
- In order to better and more intuitively know the dependency problem after the project is packaged, we can realize the visual packaging dependency
rollup-plugin-visualizer
In the use of a customized environment to build the configuration file, in
.env.custom
, configure# .env.custom NODE_ENV=production
As of version
vite@2.1.5
, there is an official BUG. The aboveNODE_ENV=production
does not take effect in the custom configuration file. It can be compatible in the following ways// vite.config.ts const config = { ... define: { 'process.env.NODE_ENV': '"production"' } ... }
antd-mobile
loaded on demand, and the configuration is as follows: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' } ] }) ] }
The above configuration can ensure the normal operation of antd in the local development mode, but after executing the
build
command, an error will be reported in the server access
, similar to issue can refer tosolution
Manual installation separately installindexof
npm package:npm install indexof
<span id="mobx">mobx6.x + react + typescript practice</span>
When the author used mobx
, the version was already mobx@6.x
, and found that compared with the old version, there are some differences in the use of the API. I hereby share the experience of stepping on the pit
Store division
The division of store, mainly refer to the example
It should be noted that when the store is initialized, if the data needs to be responsively bound, the default value must be given at the time of initialization, and it cannot be set to undefined or null. In this case, the data cannot be responsive.
// 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 injection
mobx@6x
data injection, the use of react
of context
characteristics; mainly divided into the following three steps
Root node change
Through the Provider
component, inject the global 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')
)
The Provider here is provided by mobx-react
We find that by looking at the source code, Provier
internal implementation is also 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 uses
Because function components cannot use annotations, we need to use custom Hook
to achieve:
// 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 }
Component reference through custom component reference 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
You can see that the custom Hook we designed earlier can provide friendly code hints Typescript
The above is the actual application scenario of the mobx + typescript
If you have any questions, please comment and exchange :)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。