最近前端大火的 Vite 2.0 版本終於出來了,在這裏分享一下如何使用 vite 構建一個前端單頁應用
該文章主要面向對 Vite 感興趣,或者作前端項目架構的同窗javascript
源碼地址,歡迎 star 跟蹤最新變動:fe-project-basecss
經過這篇文章,你能瞭解到如下幾點:html
想快速瞭解 Vite 配置構建的,能夠直接跳到 這裏前端
這裏咱們項目名是 fe-project-base
這裏咱們採用的 vite 2.0 來初始化咱們的項目java
npm init @vitejs/app fe-project-base --template react-ts
這個時候,會出現命令行提示,我們按照本身想要的模板,選擇對應初始化類型就 OK 了node
首先,咱們須要安裝依賴,要打造一個基本的前端單頁應用模板,我們須要安裝如下依賴:react
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:git
npm install --save react react-dom react-router @loadable/component classnames react-router-config mobx-react mobx-persist
devDependencies:github
npm install --save-dev eslint lint-staged husky@4.3.8 prettier
在安裝完上面的依賴以後,經過 cat .git/hooks/pre-commit
來判斷 husky 是否正常安裝,若是不存在該文件,則說明安裝失敗,須要從新安裝試試typescript
<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 校驗並修復格式化的能力了
工欲善其事必先利其器,咱們首要解決的是在團隊內部編輯器協做問題,這個時候,就須要開發者的編輯器統一安裝 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", "[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" } }
這個時候,我們的編輯器已經具有了保存並自動格式化的功能了
關於 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 版本 } } }
一個基本的前端單頁應用,須要的大體的目錄架構以下:
這裏以 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"] }
截止做者寫該篇文章時,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
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
做者在使用 mobx
的時候,版本已是 mobx@6.x
,發現這裏相比於舊版本,API 的使用上有了一些差別,特意在這裏分享下踩坑經歷
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()
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"
由於函數組件無法使用註解的方式,因此我們須要使用自定義 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
在函數式組件中的實際應用場景了;
若是有什麼問題,歡迎評論交流 :)