Vite 2.0 is finally out. Here is how to build a front-end single-page application using Vite
This article is intended for students who are interested in Vite or doing front-end project architecture
Source address, welcome star, will be updated later maintenance: fe-project-base
Here are a few things you can learn from the article:
- Vscode editor configuration
- How to configure git pre-commit
- ESLint + Pritter configuration
- Standard front-end single-page application directory planning
- Learn vite build optimization from 0 to 1
- Mobx /6.x + React + TypeScript best practices
For a quick look at Vite configuration builds, you can jump right here
Initialize the project
Here our project name is fe-project-base and here we use Vite 2.0 to initialize our project
npm init @vitejs/app fe-project-base --template react-ts
Copy the code
At this time, the command line prompt will appear, we follow the template we want, select the corresponding initialization type OK
Installation project dependencies
First, we need to install dependencies. To build a basic front-end single-page application template, we need to install the following dependencies:
react
&react-dom
: Basic corereact-router
: Route configuration@loadable/component
: Dynamic route loadingclassnames
: a better way to write classNamereact-router-config
: Better React-Router routing configuration packagemobx-react
&mobx-persist
: Mobx status managementeslint
&lint-staged
&husky
&prettier
: code verification configurationeslint-config-alloy
: ESLint configures plugins
dependencies:
npm install --save react react-dom react-router @loadable/component classnames react-router-config mobx-react mobx-persist
Copy the code
DevDependencies:
NPM install --save-dev eslint Lint-staged [email protected] prettierCopy the code
The pre – commit configuration
After installing the above dependencies, check to see if husky is successfully installed using cat.git /hooks/pre-commit. If this file is not found, husky failed to be installed and needs to be re-installed
Husky here uses the 4.x version, which is no longer a free agreement
Git /hooks/pre-commit configuration failed. Update node/14.16.0 to fix this
After the above installation configuration is complete, we also need to add related configuration to 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"]}}Copy the code
At this point, our entire project has the ability to do ESLint validation and fix formatting for submitted files
Editor configuration
We first solve the problem of editor collaboration within the team. At this time, we need to install the EditorConfig plug-in in the developer’s editor (here we take 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
Copy the code
Configure automatic formatting and code validation
In the VSCode editor, the Mac shortcut command + is used 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"}}Copy the code
At this point, our editor already has the ability to save and automatically format
ESLint + Prettier
Prettier, ESLint, Prettier the next step in ESLint and Prettier can be: Know ESLint and Prettier completely
-
.eslintignore: configure ESLint to ignore files
-
Eslintrc: ESLint code rule configuration, here we recommend using the industry standard, here I recommend AlloyTeam eslint-config-alloy, according to the documentation to install the corresponding ESLint configuration:
-
npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy Copy the code
-
. Prettierignore: Configure Prettier to ignore a file
-
. Prettierrc: Formatting a user-defined configuration
{ "singleQuote": true."tabWidth": 2."bracketSpacing": true."trailingComma": "none"."printWidth": 100."semi": false."overrides": [{"files": ".prettierrc"."options": { "parser": "typescript"}}}]Copy the code
There are several reasons to choose eslint-config-alloy:
- Clearer ESLint hints: for example, special characters need to be escaped, etc
error `'` can be escaped with `' `, `‘ `, ` & # 39; `, `’ ` react/no-unescaped-entitiesCopy the code
- Stricter ESLint configuration prompts: for example, ESLint will be alerted if a version specifying React is not configured
Warning: React version not specified in eslint-plugin-react settings. See https://github.com/yannickcr/eslint-plugin-react#configuration Copy the code
Let’s make up pairs here
react
Version Configuration// .eslintrc { "settings": { "react": { "version": "detect" // Probes the react version of the current node_modules installation}}}Copy the code
Overall Catalog Planning
For a basic front-end single-page application, the general directory architecture is as follows:
The directory partition under SRC is used as an example
.├ ─ app.tsx ├─ Assets, │ ├─ favicon. SVG │ ├─ common // │ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ Business components or UI components │ ├ ─ ─ Toast ├ ─ ─ the config / / configuration file directory │ ├ ─ ─ but ts ├ ─ ─ hooks / / custom hook │ └ ─ ─ index. The ts ├ ─ ─ layouts / / template, Different routing, can configure different templates │ └ ─ ─ index. The TSX ├ ─ ─ lib / / here usually prevent third-party libraries, ├─ ├─ exercises - ├─ exercises - ├─ exercises - exercises - exercises - ├─ exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - Exercises - Exercises - Exercises - Exercises - Exercises - Exercises - Exercises - Exercises Ofessional page level component │ ├ ─ ─ home ├ ─ ─ routes / / routing configuration │ └ ─ ─ index. The ts ├ ─ ─ store / / global state management │ ├ ─ ─ common. Ts │ ├ ─ ─ index. Ts │ └ ─ ─ ├─ ├─ lessons.├ ─ lessons.├ ─ lessons.├ ─ lessons.└Copy the code
OK, now that we have a rough directory structure for the front-end project, we need to configure aliases to optimize the code, such as: import XXX from ‘@/utils’ path experience
There is usually a public directory that is the same as the SRC directory, and files in that directory are copied directly to the build directory
The alias configuration
To configure aliases, we need to focus on two places: vite.config.ts & tsconfig.json
Vite. Config. ts is used for compilation and identification; Tsconfig. json is used for Typescript recognition;
@/ is recommended. Why not use @? This is to avoid conflicts with some NPM package names in the industry (e.g. @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')}}}Copy the code
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"]}Copy the code
Build configurations from 0 to 1 Vite
By the time the author wrote the article,vite
Version forVite / 2.1.2
, all the following configurations are only applicable to this version
The configuration file
The default vite initialization project does not create.env,.env.production, and.env.devlopment configuration files. The official template provides package.json files that will be used by each script. So we need to manually create first, here provides the official documentation:.env configuration
# package.json
{
"scripts": {
"dev": "vite".Vite -m development ='serve',mode='development'
"build": "tsc && vite build".Vite -m production command='build', mode='production'
"serve": "vite preview"."start:qa": "vite -m qa" // Custom commands will look for.env.qa configuration files; Command ='serve', mode='qa'}}Copy the code
At the same time here the command corresponds to the configuration file: mode distinction
import { ConfigEnv } from 'vite'
export default ({ command, mode }: ConfigEnv) => {
// Command defaults to === 'serve'
// Command === 'build' when executing vite build
// The configuration of the corresponding environment can be exported according to the condition judgment of command and mode
}
Copy the code
For details about the configuration file, see fe-project-vite/vite.config.ts
Routing planning
First, the most important part of a project is routing configuration. So we need a configuration file as the entry point to configure all the page routes. Here we use the react-router as an example:
Route configuration file configuration
SRC /routes/index.ts, we introduced the @loadable/ Component library to load routes dynamically. Vite supports dynamic loading by default to improve program packaging efficiency
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 routing
{
path: '/hybird'.exact: true.component: Layout,
routes: [{path: '/'.exact: false.component: loadable(() = > import('@/pages/hybird'))}},// H5 related routes
{
path: '/h5'.exact: false.component: H5Layout,
routes: [{path: '/'.exact: false.component: loadable(() = > import('@/pages/h5')}]}]export default routesConfig
Copy the code
The entry main. TSX file configures the routed intersections
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'))Copy the code
React-router-config: renderRoutes: react-router-config: renderRoutes: react-router-config: renderRoutes: react-router-config: renderRoutes: react-router-config:
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;
Copy the code
Through the above two configurations, we can basically run the project, but also have the lazy loading capability of the route;
Execute NPM run build and view the file output to see that our dynamic route loading has been configured successfully
$ tsc && vite buildVite v2.1.2 Building for Production... ✓ 53 modules transformed. Dist/index. HTML 0.41 KB dist/assets/index c034ae3d. Js 0.11 KB/brotli: 0.09 KB dist/assets/index. C034ae3d. Js. Map 0.30 KB dist/assets/index f0d0ea4f. Js 0.10 KB/brotli: 0.09 KB dist/assets/index. F0d0ea4f. Js. Map 0.29 KB dist/assets/index. 8105412 a. js 2.25 KB/brotli: 0.89 KB dist/assets/index. 8105412 a. js. Map 8.52 KB dist/assets/index. 7 be450e7. CSS 1.25 KB/brotli: 0.57 KB dist/assets/vendor. 7573543 b. js 151.44 KB/brotli: 43.17 KB dist/assets/vendor. 7573543 b. js. Map 422.16 KB ✨ Done in 9.34 s.Copy the code
Careful students may find that in the routing configuration above, we deliberately split two Layout & H5Layout, the purpose of doing so here is to distinguish the difference between wechat H5 and Hybird set up template entrance, you can decide whether to need the Layout layer according to their own business
Style to deal with
Speaking of styling, our example here uses.less files, so we need to install the corresponding parsing library in the project
npm install --save-dev less postcss
Copy the code
To support CSS modules, you need to enable the corresponding configuration items in the viet.config. ts file:
// vite.config.ts
{
css: {
preprocessorOptions: {
less: {
// Support inline JavaScript
javascriptEnabled: true}},modules: {
// Style small hump transform,
//css: goods-list => tsx: goodsList
localsConvention: 'camelCase'}}},Copy the code
Compile build
In fact, this is basically the entire vite build, 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('Config file does not exist, ignore')}}const isBuild = command === 'build'
// const base = isBuild ? process.env.VITE_STATIC_CDN : '//localhost:3000/'
config.base = process.env.VITE_STATIC_CDN
if (isBuild) {
// Compress the Html plug-in
config.plugins = [...plugins, minifyHtml()]
}
if (process.env.VISUALIZER) {
const { plugins = [] } = rollupOptions
rollupOptions.plugins = [
...plugins,
visualizer({
open: true.gzipSize: true.brotliSize: true}})]// The import.meta. Env variable cannot be used here
if (command === 'serve') {
config.server = {
// Reverse proxy
proxy: {
api: {
target: process.env.VITE_API_HOST,
changeOrigin: true.rewrite: (path: any) = > path.replace(/^\/api/.' ')}}}}return config
}
Copy the code
In this case, we use a dotenv library to help us bind the configuration content to process.env for our configuration file
For details, see Demo
Build optimization
-
In order to better and more intuitively know the dependency problem after the project packaging, we can use the rollup-plugin-Visualizer package to achieve visual packaging dependency
-
To build the configuration file using a custom environment, in.env.custom, configure
# .env.custom NODE_ENV=production Copy the code
NODE_ENV=production does not work in custom configuration files. It is compatible with the following methods
// vite.config.ts const config = { ... define: { 'process.env.NODE_ENV': '"production"'}... }Copy the code
-
Antd-mobile is loaded on demand and configured 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'}]}]}Copy the code
The above configuration ensures the normal operation of ANTD in local development mode, however, before execution
build
Command, an error is reported on the server access, similar toissueYou can refer toNPM install Indexof. NPM install Indexof
Mobx6. x + React + typescript practice
When I use mobx, the version is already [email protected], and I find that there are some differences in the use of API compared to the old version, so I would like to share my experience here
Store division
It is important to note that when the store is initialized, if you want the data to be responsive, you need to set the default value to be undefined or null during initialization. In this case, the data cannot be responsive
// store.ts
import { makeAutoObservable, observable } from 'mobx'
class CommonStore {
// An initializer must be given, otherwise reactive data will not take effect
title = ' '
theme = 'default'
constructor() {
// This is the key to implementing responsiveness
makeAutoObservable(this)}setTheme(theme: string) {
this.theme = theme
}
setTitle(title: string) {
this.title = title
}
}
export default new CommonStore()
Copy the code
Store injection
mobx@6x data injection, using the React Context feature; It is divided into the following three steps
Root node change
Use the Provider component to inject global stores
// Import file 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'))Copy the code
The Provider is provided by mox-React. Provier implements the react Context internally:
// Mobx-React Provider
import React from "react"
import { shallowEqual } from "./utils/utils"
import { IValueMap } from "./types/IValueMap"
// Create a Context
export const MobXProviderContext = React.createContext<IValueMap>({})
export interface ProviderProps extends IValueMap {
children: React.ReactNode
}
export function Provider(props: ProviderProps) {
// All except the children attribute are store values
const{ children, ... stores } = propsconst parentValue = React.useContext(MobXProviderContext)
// store references the latest value
constmutableProviderRef = React.useRef({ ... parentValue, ... stores })const value = mutableProviderRef.current
if (__DEV__) {
constnewValue = { ... 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>
}
// Displays the Provider name for the debugging tool
Provider.displayName = "MobXProvider"
Copy the code
Store use
Since function components can’t use annotations, we need to use custom hooks:
/ / useStore implementation
import { MobXProviderContext } from 'mobx-react'
import counterStore from './counter'
import commonStore from './common'
const _store = {
counterStore,
commonStore
}
export type StoreType = typeof _store
// Declare the store type
interface ContextType {
stores: StoreType
}
// These are function declarations, overloaded
function useStores() :StoreType
function useStores<T extends keyof StoreType> (storeName: T) :StoreType[T] /** * get the rootstoreOr specifystoreName data * @param storeNameSpecify the childstoreName * @returns typeof StoreType[storeName] * /function useStores<T extends keyof StoreType> (storeName? : T) {// hereMobXProviderContextIs the abovemobx-reactTo provide theconst rootStore = React.useContext(MobXProviderContext)
const { stores } = rootStore as ContextType
return storeName ? stores[storeName] : stores
}
export { useStores }
Copy the code
Component references refer to stores by customizing components
import React from 'react'
import { useStores } from '@/hooks'
import { observer } from 'mobx-react'
// Implemented through the Observer high-level component
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
Copy the code
You can see the custom hooks we designed earlier that provide friendly code hints through Typescript features
That’s how mobx + typescript works in a functional component; If you have any questions, please feel free to comment 🙂
The resources
- React Hook useContext
- Mobx official documentation
- Vite build case Vite-Concent-Pro