最近前端大火的 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
在函数式组件中的实际应用场景了;
若是有什么问题,欢迎评论交流 :)