基于vite搭建一个react移动端脚手架

2023-10-29

基于移动端的配置。

vite官网

优点:

  1. 光速启动
  2. 热模块替换
  3. 按需编译

脚手架功能

  1. antd-mobile移动端组件
  2. axios网络数据交互
  3. hox状态管理
  4. react-router-dom路由管理
  5. postcss-px-to-viewport移动端px转vw/vh
  6. less预编译
  7. autoprefixer自动补全
  8. typescript语法
  9. window.$cancelRequest()取消请求,再初始化即还没有axios请求数据前调用会报错。

安装

vite及react框架安装

npm init vite@latest

选项:

在这里插入图片描述
在这里插入图片描述

安装完成:
在这里插入图片描述

  1. Project name:项目名称,也是文件夹名
  2. Select a framework:选择框架,目录中有vue及react,其他的没见过,相关模板框架结构访问这里
  3. Select a variant:选择js/ts版本

目录结构:

在这里插入图片描述

vite.config.ts:配置文件,相关参数访问这里

没有了public文件夹,index.html与src文件夹同级了。可以自定义一个public文件夹

安装相关依赖

进入 vite-react目录,安装相关依赖

npm install

启动

npm run dev

运行如图:

在这里插入图片描述

查看运行环境:

在这里插入图片描述

发现process红色波浪线,按照提示安装@types/node,此包包含Node.js的类型定义。

在这里插入图片描述

npm i --save-dev @types/node

重新打开vscode,红色波浪线消失,

打包

npm run build

看看目录结构:

在这里插入图片描述

js、css、图片全都在一个assets文件夹下。

看看html文件:

在这里插入图片描述

相关的一些引入全是/assets开头,如果不是放在服务器根目录,这样我们看到的就是空白页面,所以要修改配置文件vite.config.ts

export default defineConfig({
  base:'./'
})

再次打包的时候发现,dist文件夹并没有像vue-cli或umijs那样删除dist文件夹,然后重新打包,而是直接修改对应为文件,大大的增加了打包速度

添加base配置后:

在这里插入图片描述

如果有些静态文件不想被hash,只是想用url,可以自定义一个public文件夹

环境变量

先看看package.json里面的打包命令:

在这里插入图片描述

就这三个,npm run dev(env=development)、npm run serve(本地预览服务env=production)、npm run build(env=production)

现在添加一个test环境:

  1. 与/src同级新增加一个.env.test文件,添加以下内容

    NODE_ENV=test
    
  2. package.json文件添加打包命令

    "scripts": {
        "dev": "vite",
        "build": "tsc && vite build",
        "test": "tsc && vite build --mode test", // 这个命令
        "serve": "vite preview"
      },
    
  3. 执行打包命令npm run testprocess.env.NODE_ENV的值就为test了

路由

安装

npm install react-router-dom --save-dev

react-router-dom英文官网

使用

页面创建

先建几个页面组件:home/home.tsxabout/about.tsxlogin/login.tsxuser/user.tsx404/404.tsx

home.tsx

import { withRouter,Link } from 'react-router-dom'
function Home(props: any) {
  return (
    <div className="Home">
      <p>home</p>
      <Link to='/user'>user</Link><br/>
      <Link to='/user?id=1111'>user(search)</Link><br/> // search传参
      <Link to={{ pathname: '/user', search: 'id=123' }}>user(search)</Link><br/> // search传参
      <Link to={{ pathname: '/user', state: { num: '002' } }}>user(state)</Link><br/> // state传参
      <Link to={{pathname: '/user', query: {num: '003'}}}>user(query)</Link><br/> // query传参
    </div>
  )
}

export default withRouter(Home)

login.tsx

import { withRouter } from 'react-router-dom'
function Login(props:any) {
  const login = () => { 
    let redirect = decodeURIComponent(props.location.search.split('redirect=')[1]).split('&')
    let path = redirect[0] // 登录成功后重定向的路由
    const data = JSON.parse(redirect[1]) // 登录成功后重定向的路由参数,可能是search,可能是state,也许是query,没做params的传参,可查看routerConfig.tsx文件里面重定向登录页的配置
    sessionStorage.setItem('token', '123')
    props.history.replace({pathname:path,...data})
  }
  return (
    <div className="login">
      login
      <button onClick={() => { 
        login()
      }}>登录</button>
    </div>
  )
}

export default withRouter(Login)

about.tsx及user.tsx、404.tsx基本差不多

import { withRouter } from 'react-router-dom'
function User(props:any) {
  return (
    <div className="User">
      user
    </div>
  )
}

export default withRouter(User)

因为采用的是函数式组件,如果组件中需要history方法的话:

  1. 需借助react-router-dom的高阶组件中的withRouter,作用是将一个组件包裹进Route里面, 然后react-router的三个对象history, location, match就会被放进这个组件的props属性中。

  2. 使用react-router-dom的hooks方法useHistory,这个方法只返回history,其他location等有相关的hooks方法,react-router-dom英文官网

 import { useHistory } from "react-router-dom";
 function HomeButton() {
   let history = useHistory();
   function handleClick() {
     history.push("/home");
   }
   return (
     <button type="button" onClick={handleClick}>
       Go home
     </button>
   );
 }

路由配置文件:router/router.tsrouterConfig.tsx

我还是参考vue的路由格式来,配置router.tsx,引入react-router-dom,红色波浪线提示:

在这里插入图片描述

此时需要在vite-react根目录新建一个typing.d.ts文件作为全局的声明文件,并做如下配置:

declare module 'react-router-dom'

然后在tsconfig.json文件中include添加:

{      // 其他配置  
"include": [      
	// 其他配置    
	"./typing.d.ts"    
	]
}

波浪线消失。

为了方便组件的引入,我们配置一个别名,像vue一样:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
const path = require("path");

// https://vitejs.dev/config/
export default defineConfig({
    // ...
  resolve: {
    alias: {
      '@':path.resolve(__dirname, "src")
    }
  }
})

在router.tsx中引入组件:

在这里插入图片描述

此时又有了红色波浪线,处理方式同react-router-dom,在typing.d.ts文件内:

declare module 'react-router-dom'declare module '@/*'

路由配置router.ts

完整的router.ts,react的Suspense+lazy组合实现路由懒加载

import React from 'react'
// 组件
const Home = React.lazy(() => import('@/pages/home/home')) // 路由懒加载,配合App.tsx的Suspense
const About = React.lazy(() => import('@/pages/about/about'))
const Login = React.lazy(() => import('@/pages/login/login'))
const User = React.lazy(() => import('@/pages/user/user'))
const Miss = React.lazy(() => import('@/pages/404/404'))

const routerMap:any[] = [
  {
    path: '/',
    redirect: '/home',
    auth: false,
    footerShow: true
  },
  {
    path: '/home',
    component: Home,
    auth: false,
    footerShow: true
  },
  {
    path: '/about',
    component: About,
    auth: false,
    footerShow: true
  },
  {
    path: '/login',
    component: Login,
    auth: false,
    footerShow: false
  },
  {
    path: '/user',
    component: User,
    auth: true,
    footerShow: false
  },
  {
    path: '/404',
    component: Miss,
    auth: false,
    footerShow: false
  },
]

export default routerMap

路由权限校验配置routerConfig.tsx

routerConfig.tsx

import { Route,Redirect,withRouter } from 'react-router-dom';
import routerMap from './router'

const BasicRoute = (props:any) => {
  const pathname = props.location.pathname
  const targetRouter = routerMap.find((item: any) => item.path === pathname);
  const isLogin = sessionStorage.getItem('token')
  if (!targetRouter) { // 页面不存在
    return <Redirect to="/404" />
  }
  if (targetRouter && targetRouter.redirect) { // 重定向
    return <Redirect to={targetRouter.redirect} />
  }
  if (targetRouter && !targetRouter.auth) { // 无需登录
    return  <Route exact path={targetRouter.path} component={targetRouter.component}/>
  }
  if (targetRouter.auth) { // 要登录授权
    if (isLogin) {
      return <Route exact path={targetRouter.path} component={targetRouter.component} />
    } else { 
      let redirect = pathname
      const query = JSON.stringify(props.location?.query)
      const state = JSON.stringify(props.location?.state)
      const search = props.location?.search
      if (query) { // query传参
        redirect += `&{"query":${query}}`
      }
      if (state) { // state传参
        redirect += `&{"state":${state}}`
      }
      if (search) { // search传参
        redirect += `&{"search":"${search}"}`
      }
      redirect = encodeURIComponent(redirect)
      return <Redirect to={`/login?redirect=${redirect}`} />
    }
  }
};

// exact :精确匹配
export default withRouter(BasicRoute);

因为没有像vue-router一样的导航守卫,所以权限验证也得自己配置,包括404页面。基础的路由及权限差不多完成了。

App.tsx:有那么点像vue的样子了

import { Switch ,NavLink,withRouter} from 'react-router-dom';
import RouterView from '@/router/routerConfig'
import {useState,useEffect,Suspense} from 'react'
import routerMap from './router/router'
import './App.css'

function App(props:any) {
  const [footerShow, setFooterShow] = useState(false)
  const routerChange = () => {
    const targetRouter = routerMap.find((item: any) => item.path === props.location.pathname);
    setFooterShow(targetRouter?.footerShow)
  }
  useEffect(() => {
    routerChange()
  }, [props.location])
  return (
    <div className="page">
      <div className="content">
        <Suspense fallback={<div>Loading...</div>}> {/*配合router.ts的lazy懒加载*/}
          <Switch>
            <RouterView />
          </Switch>
        </Suspense>
      </div>
      {footerShow ? <div className="footer">
        <NavLink to="/home" className="item">首页</NavLink>
        <NavLink to="/about" className="item">关于</NavLink>
      </div> : ''}
    </div>
  )
}

export default withRouter(App)

main.tsx:添加了BrowserRouter

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter} from 'react-router-dom';
import './index.css'
import App from './App'

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
)

路由传参方式

import { withRouter,Link } from 'react-router-dom'
function Home(props: any) {
  return (
    <div className="Home">
      <p>home</p>
      <Link to='/user'>user</Link><br/>
      <Link to='/user?id=1111'>user(search)</Link><br/> // search传参
      <Link to={{ pathname: '/user', search: 'id=123' }}>user(search)</Link><br/> // search传参 {/** /user?id=123 **/}
      <Link to={{ pathname: '/user', state: { num: '002' } }}>user(state)</Link><br/> // state传参
      <Link to={{pathname: '/user', query: {num: '003'}}}>user(query)</Link><br/> // query传参
      // 函数传参
      <button onClick={() => props.history.push({pathname:"/user",search:'123456'})}>通过函数跳转</button> {/** /user?123456 **/}
  	  <button onClick={() => this.props.history.push({pathname:"/user",state: { num : '002' }})}>通过函数跳转detail组件</button>
      <button onClick={() => props.history.push({pathname:"/user",query: { num : '002' }})}>通过函数跳转detail组件</button>
    </div>
  )
}

export default withRouter(Home)
  1. query传参
    优点:传递参数可传对象; 缺点:刷新地址栏,参数丢失
  2. state传参
    优点:传递参数可传对象; 缺点:刷新地址栏,参数丢失
  3. params传参
    优点:刷新地址栏,参数依然存在 缺点:只能传字符串,并且,如果传的值太多的话,url会变得长而丑陋。(对象可以转字符串传递)
  4. search传参
    优势:刷新地址栏,参数依然存在 缺点:只能传字符串,并且,如果传的值太多的话,url会变得长而丑陋。(对象可以转字符串传递)

导航守卫- 离开前确认

import { useState } from 'react'
import { withRouter,Prompt } from 'react-router-dom'
function User(props: any) {
  const [leave, setLeave] = useState(true)
  return (
    <div className="User">
      user
      <Prompt message={() => { 
        if (!leave) { 
          return true
        }
        const r = confirm('确定离开?')
        return r
      }} when={leave}></Prompt>
    </div>
  )
}

export default withRouter(User)
  1. message:string/function,function默认都返回false,表示继续停留在当前页面。
  2. when:boolean,true要提示,false不提示

大概这样子:

在这里插入图片描述

axios通信

安装

npm install axios

使用

新建文件:

  1. apiNames.ts:接口名称文件

    export default { 
         entrustStorageExport: '/exportDetails', //下载  
         commonUpload: '/upload', // 上传图片  
         PageCustomService: '/support', // 客服信息get  
         loginUserResetPassword: '/resetPassword', // 重置密码post
     }
    
  2. axios.ts:axios请求封装文件

     import request from './axiosConfig';
     import { sessions } from '@/utils/utils'
     
     interface api { 
       url: string
       data?: any
       header?:any
     }
     
     const httpConfig = (method:string,params?:any) => { 
       let token = sessions.get(`token`)
       let data: any = null
       if (method !== 'FILE') { // 非文件上传
         if (method === 'POST' || method === 'PUT') {
           data = {
             data: params.data,
           }
         } else if (method === 'GET' || method === 'DELETE') {
           data = {
             params: params.data,
           }
         }
         return new Promise((resolve, reject) => {
           request(params.url, {
             method,
             ...data,
             headers: {
               'Content-Type': 'application/json;charset=UTF-8',
               'Authorization': token ? token : 'Basic aHc6aHc=',
               ...params.header
             }
           }).then((res:any) => { 
             resolve(res)
           }).catch((err:any)=>{
             console.log(err,'异常')
           })
         })
     
       } else { // 文件上传
         return new Promise((resolve, reject) => {
           request(params.url, {
             method: 'post',
             data: params.data,
             requestType: 'form',
             headers: {
               'Authorization': token ? token : 'Basic aHc6aHc='
             }
           }).then((res:any) => { 
             resolve(res)
           }).catch((err:any)=>{
             console.log(err,'异常')
           })
         })
       }
     
     }
     
     export default {
       post: (params: api) => {
         return httpConfig('POST', params)
       },
       get: (params:api) => {
         return httpConfig('GET', params)
       },
       delete: (params:api) => {
         return httpConfig('DELETE', params)
       },
       put: (params:api) => {
         return httpConfig('PUT', params)
       },
       file: (params:api) => {
         return httpConfig('FILE', params)
       },
     }
        ```
        
    
  3. axiosConfig.ts:axios配置文件

       /**
       * axios 网络请求工具
       */
      import axios from 'axios';
      import api from './apiNames'
      import url from './url'
      
      // 服务器状态码
      const codeMessage:any = {
        200: '服务器成功返回请求的数据。',
        201: '新建或修改数据成功。',
        202: '一个请求已经进入后台排队(异步任务)。',
        204: '删除数据成功。',
        400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
        401: '用户没有权限(令牌、用户名、密码错误)。',
        403: '用户得到授权,但是访问是被禁止的。',
        404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
        406: '请求的格式不可得。',
        410: '请求的资源被永久删除,且不会再得到的。',
        422: '当创建一个对象时,发生一个验证错误。',
        500: '服务器发生错误,请检查服务器。',
        502: '网关错误。',
        503: '服务不可用,服务器暂时过载或维护。',
        504: '网关超时。',
      };
      // 接口返回状态码
      const apiCode: any = {
        toast: 101, // 错误信息,需要toast提示
        loginFail: 10086, // 登录失效
      }
      
      let request = axios.create({
        baseURL: url,
        timeout: 2e4,
        responseType: 'json',
      });
      /**
       * 异常处理程序
       */
      const errorHandler = (response: any) => {
        if (response && response?.status) {
          const errorText = codeMessage[response.status] || response.statusText;
          const { status, url } = response;
          if (response?.status === 401) { // 登录失效
            setTimeout(() => {
              window.sessionStorage.clear();
              window.location.href = `${window.location.origin}/login`;
            }, 1e3);
          } else {
            console.log(`请求错误 ${status}: ${url},${errorText}`);
          }
        }
        return response;
      };
      
      // 取消请求
      const cancelAxios:any = [];
      request.interceptors.request.use((config:any) => {
        const c = config;
        c.cancelToken = new axios.CancelToken((cancel:any) => {
          cancelAxios.push(cancel);
        });
        return c;
      }, () => {
        // console.log(error);
      });
      // 触发axios取消事件,挂载到window
      window.$cancelRequest = () => { 
        cancelAxios.forEach((element:any, index:number) => {
          element('cancel');
          delete cancelAxios[index];
        });
      };
      
      		// 过滤导出excel错误提示,文件流下载接口声明列表
      const list = [
        { url: api.entrustStorageExport, type: 'export', export: 1},
      ];
      
      // 添加请求拦截器
      request.interceptors.request.use((config:any) => {
        const finds = list.find(item => config.url.includes(item.url));
        const num = config[config.method.toUpperCase() ===  'GET' ? 'params' : 'data']?.export || 0;
        if (finds && num === 1) { // 下载
          config.responseType = 'blob'
        }
        
        return config
      }, (error: any) => {
          console.log(error)
      })
      
      // 添加响应拦截器
      request.interceptors.response.use(async (response: any) => {
        const options = response.config
        const finds = list.find(item => options.url.includes(item.url));
        const num = options[options.method.toUpperCase() === 'GET' ? 'params' : 'data']?.export || 0;
        if (finds && num === 1 && response.status === 200) { // 文件流下载,请求中必须含:export:1,可选fileName,默认时间
          const blob = new Blob([response.data], {type: 'application/vnd.ms-excel'});
          let filename = options[options.method.toUpperCase() === 'GET' ? 'params' : 'data']?.fileName || new Date().Format('YYYY-MM-DD hh:mm:ss');
          if(window.navigator.msSaveOrOpenBlob) {// 兼容IE10
            navigator.msSaveBlob(blob,filename);
          } else {
            // 创建一个超链接,将文件流赋进去,然后实现这个超链接的单击事件
            const elink = document.createElement('a');
            elink.download = filename;
            elink.style.display = 'none';
            elink.href = URL.createObjectURL(blob);
            document.body.appendChild(elink);
            elink.click();
            URL.revokeObjectURL(elink.href); // 释放URL 对象
            document.body.removeChild(elink);
          }    
          return blob
        }
        if (response.status === 200) { // 一般性接口请求
          try {
            if (response.data.code) {
              if (response.data.code !== 200) {
                if (response.data.code === apiCode.loginFail) { // 登录失效
                  setTimeout(() => {
                    window.sessionStorage.clear();
                    window.location.href = `${window.location.origin}/login`;
                  }, 1e3);
                }
                if (response.data.code === apiCode.toast) {
                  alert(response.data.errorMsg);
                } else { 
                  console.log(response.data.errorMsg);
                }
              }
            }
      
          } catch (err) {
            console.log('接口请求失败');
          }
        } else {
          errorHandler(response)
        }
        return response.data;
      });
      
      export default request
    
  4. url.ts:接口路径配置文件,分环境接口地址

     let url = ''
     switch (process.env.NODE_ENV) { 
       case 'development': url = "http://127.0.0.1:9999"; break; // 开发
       case 'test': url = "http://127.0.0.1:9999"; break; // 测试
       case 'production': url = "http://10.21.1.104:9999"; break; // 生产
       default:  url = "http://127.0.0.1:9999"; break;// 其他
     }
     export default url
    
  5. user文件夹下新建services.ts

     import request from '@/axios/axios'
     import api from '@/axios/apiNames'
     
     
     // 获取服务信息
     export const getPageCustomService = () => { 
         let params = {
           url: api.PageCustomService
         }
       return request.get(params)
     }
     // 重置密码
     export const resetPassword = (name:string) => { 
       let params = {
         url: api.loginUserResetPassword,
         data: {
           name
         }
       }
       return request.post(params)
     }
     // 上传图片
     export const uploadImage = (formData:any) => { 
       let params = {
         url: api.commonUpload,
         data: formData
       }
       return request.file(params)
     }
     
     // 导出申请清单
     export const exportUrl = () => { 
       let params = {
         url: api.entrustStorageExport,
         data: {
           id: 4,
           fileName: '申请商品明细.xls',
           export: 1,
         }
       }
       return request.get(params)
     }
    
  6. 修改user.tsx

      import { useState,useEffect } from 'react'
      import { withRouter, Prompt, useHistory } from 'react-router-dom'
      import {
        getPageCustomService,
        resetPassword,
        uploadImage,
        exportUrl
      } from './services'
      
      function User(props: any) {
        const [leave, setLeave] = useState(true)
        let history = useHistory()
        console.log(history)
      
        useEffect(() => {
          getData()
        }, [])
      
        // get请求
        const getData = async () => { 
          let { data, code } = await getPageCustomService()
          console.log(data,code)
        }
        // post请求
        const postData = async () => {
          let { code, data } = await resetPassword("gdtest002")
          console.log(code,data)
        }
        // 图片上传
        const [file, setFile] = useState(null) as any
        const imageUpload = async () => {
          if (!file) { 
            return
          }
          if (file?.size > 2 * 1024 * 1024) {
            alert('大了')
            return
          }
          let formData = new FormData();
          const fileName = props.name || 'file';
          formData.append(fileName, file);
          let {code,data} = await uploadImage(formData)
          console.log(code,data)
        }
        // 导出/下载文件流
        const exportData = async () => {
          let data = await exportUrl()
          console.log(data)
        }
      
        return (
          <div className="User">
            user
            <p>
              <button onClick={() => { postData() }}>post请求</button>
            </p>
            <p>
              <button onClick={() => { history.replace('/about') }}>点我去about</button>
            </p>
            <p>
              <button onClick={() => {exportData()}}>点我下载</button>
            </p>
            <p>
              <input type="file" onChange={(e:any) => {setFile(e.target.files[0])}}/>
              <button onClick={() => {imageUpload()}}>点我上传</button>
            </p>
            <Prompt message={() => { 
              if (!leave) { 
                return true
              }
              const r = confirm('确定离开?')
              return r
            }} when={leave}></Prompt>
          </div>
        )
      }
      
      export default withRouter(User)
    

在这里插入图片描述

CSS及LESS

css

在about文件夹内新建index.css

.App{  color: #ff0000;}.text{  font-size: 30px;}

并引入about.tsx文件:

import './index.css'
function About() {
  return (
    <div className="App">
      <p className="text">about</p>
    </div>
  )
}

export default About

效果如图:

在这里插入图片描述

这样有个问题,如果我在其他组件内也有相同的class类名,那么样式将会相互影响。

改进:

  1. index.css改为:index.module.css:任何以 .module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 文件。导入这样的文件会返回一个相应的模块对象:官方
  2. 使用:
    在这里插入图片描述

less

安装less

npm install -D less

官方文档

使用

将上面的index.module.css更改后缀为index.module.less即可使用less语法

配置全局less变量

在/src目录下新建global.less文件:

/**
less全局变量 
*/

@mainColor: #ff0000;
@textColor: #666;

vite.config.ts中配置:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
const path = require("path");

export default defineConfig({
  plugins: [react()],
  // ... 其他配置
  css: {
    preprocessorOptions: {
      less: {
        javascriptEnabled: true,
        additionalData: `@import "${path.resolve(__dirname, 'src/global.less')}";`
      }
    }
  }
})

浏览器前缀

安装

npm install autoprefixer postcss -D

使用

vite.config.ts中配置:

export default defineConfig({
  // ...其他配置
  css: {
      // ...其他配置
    postcss: {
      plugins: [
        require("autoprefixer")
      ]
    }
  }
})

移动端单位换算

这里使用的插件是:postcss-px-to-viewport

安装

npm install postcss-px-to-viewport --save-dev

使用

vite.config.ts中配置:

export default defineConfig({
    //...其他配置
  css: {
    //...其他配置
    postcss: {
      plugins: [
        require("autoprefixer"),
        require("postcss-px-to-viewport")({
          viewportWidth: 750,  //视窗的宽度,对应的是我们设计稿的宽度,一般是750
          viewportHeight: 1334, // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置
          unitPrecision: 3,       // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
          viewportUnit: 'vw',     // 指定需要转换成的视窗单位,建议使用vw
          selectorBlackList: ['.ignore', '.hairlines'],  // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
          minPixelValue: 1,       // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
          mediaQuery: false       // 允许在媒体查询中转换`px`
        })
      ]
    }
  }
})

Ant Design Mobile

官网
安装

npm install --save antd-mobile@next

使用

import { Button,Input } from 'antd-mobile'
function About() {
  return (
    <div>
      <Button color="primary">123</Button>
      <Input placeholder='请输入内容'/>
    </div>
  )
}

export default About

状态管理hox

蚂蚁金服的react状态管理器文档

  • 只有一个 API,简单高效,几乎无需学习成本
  • 使用 custom Hooks 来定义 model,完美拥抱 React Hooks
  • 完美的 TypeScript 支持
  • 支持多数据源,随用随取

安装

npm install --save hox

使用

  1. 新建store/store.ts文件:

    import { useState } from "react";
     import { createModel } from "hox";
     
     function useCounter() {
       const [count, setCount] = useState(0);
       const decrement = (num?:number) => setCount(typeof num !== 'number' ? count - 1 : count - num);
       const increment = (num?:number) => setCount(typeof num !== 'number' ? count + 1 : count + num);
       return {
         count,
         decrement,
         increment
       };
     }
     
     export default createModel(useCounter);
    
  2. home.tsx

     import {useEffect} from 'react'
     import useCounterModel from '@/store/store'
     import { withRouter, Link } from 'react-router-dom'
     import { Button } from 'antd-mobile'
     function Home(props: any) {
       const model = useCounterModel()
     
       useEffect(() => {
         console.log('值变了')
       }, [model.count])
     
       return (
         <div className="Home">
           {model.count}
           <Button color="danger" onClick={() => { 
             model.increment()
           }}>加个数</Button>
           <Button color="danger" onClick={() => { 
             model.decrement()
           }}>减一下</Button>
           <Button color="danger" onClick={() => { 
             model.increment(20)
           }}>20</Button>
         </div>
       )
     }
     
     export default withRouter(Home)
    

更多文献参考文档

typing.d.ts

全局的声明文件

格式:

declare module '***'

vite.config.ts

官方文档

其他

  1. utils/regexp.ts:常用几个正则表达式;
  2. utils/utils.ts:sessionStorage封装,数据*号替换,数字三位加逗号

git地址

https://gitee.com/mosowe/react-vite-app

npm

npm install -g yo

npm install -g generator-vite-react-app

yo vite-react-app

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于vite搭建一个react移动端脚手架 的相关文章

  • Intellij Idea golang插件开发

    1 安装Intellij idea 的golang插件 2 建立目录 D SystemFile GoWorkspace 在系统里面配置GOPATH D SystemFile GoWorkspace 然后在GOPATH目录下面建立 src b

随机推荐

  • 2019-6-18 车牌识别尝试-图像抗扭处理和SVM学习(opencv)

    抗扭曲函数deskew 利用opencv中svm算法学习图片和识别图片 抽取特征向量函数hot分析 车牌识别中涉及字符的识别 识别方法可以用opencv自带的机器学习算法svm 支持向量机 来实现 参见https docs opencv o
  • pandas的定义以及pandas的Series的初步使用(一)

    一 什么是pandas pandas是一种Python数据分析的利器 是一个开源的数据分析包 最初是应用于金融数据分析工具而开发出来的 因此pandas为时间序列分析提供了很好的支持 pandas是PyData项目的一部分 官网 http
  • 大数据实训kaggle比赛-房价预测(下)

    接着开始表演 下面介绍模型 Sequential 序贯模型 它是函数式模型的简略版 为最简单的线性 从头到尾的结构顺序 不分叉 是多个网络层的线性堆叠 实现方法 模型需要知道它所期待的输入的尺寸 出于这个原因 序贯模型中的第一层需要接收关于
  • 计算机视觉论文-2021-06-29

    本专栏是计算机视觉方向论文收集积累 时间 2021年6月29日 来源 paper digest 欢迎关注原创公众号 计算机视觉联盟 回复 西瓜书手推笔记 可获取我的机器学习纯手推笔记 直达笔记地址 机器学习手推笔记 GitHub地址 1 T
  • Python机器视觉--OpenCV进阶(核心)--图像直方图与掩膜直方图与直方图均衡化

    1 图像直方图 1 1 图像直方图的基本概念 在统计学中 直方图是一种对数据分布情况的图形表示 是一种二维统计图表 图像直方图是用一表示数字图像中亮度分布的直方图 标绘了图像中每个亮度值的像素数 可以借助观察该直方图了解需要如何调整亮度分布
  • Python教你一招,爬取链家二手房并做数据可视化分析

    前言 数据采集的步骤是固定 发送请求 模拟浏览器对于url地址发送请求 获取数据 获取网页数据内容 gt 请求那个链接地址 返回服务器响应数据 解析数据 提取我们需要的数据内容 保存数据 保存本地文件 所需模块 win R 输入cmd 输入
  • mybatis 配置打印sql

    1 在mybatis config xml中配置加一个setting 2 如果是spring集成mybatis的话 在sqlSessionFactory配置好configLocation属性 3 在日志文件中配置如下 DEBUG
  • 强化对信息安全事件的预防

    声明 本文是学习信息技术 安全技术 信息安全事件管理 第1部分 事件管理原理 而整理的学习笔记 分享出来希望更多人受益 如果存在侵权请及时联系我们 范围 本部分提出了信息安全事件管理的基本概念和过程阶段 并将这些概念与结构化方法的原理相结合
  • spring常用注解

    1 Controller 在SpringMVC 中 控制器Controller 负责处理由DispatcherServlet 分发的请求 它把用户请求的数据经过业务处理层处理之后封装成一个Model 然后再把该Model 返回给对应的Vie
  • mysql-5.6.16-win32免安装配置方法

    转载自 http blog csdn net fzhmoive article details 20042437 http blog csdn net leili0806 article details 8573636 1 下载MySQL
  • 口袋进化服务器维护,《口袋进化》新手指引.新手指导

    第一只精灵 你做好成为一名训练家的准备了么 作为一枚纯纯的萌新 在刚接触时 建议根据游戏提供的引导 来逐步熟悉基本玩法和养成体系 除此之外 别忘了阅读新手FAQ 在这里 为大家总结了一些新手常见的问题 方便大家享受游戏乐趣 Q 怎么升级 A
  • 编译protobuf静态库依赖顺序问题

    在项目中要加入protobuf协议支持 在编译成功生成程序包动态库后 发现启动运行报错 未定义的protobuf符号xxx等等 用ldd查看so文件 发现确实提示了未找到 大致的makefile文件如下 CXX g CFLAGS g Wal
  • 前后端分离命名规范:JSON下划线,对象小驼峰,以及Jackson常用配置

    在application yml中添加配置即可 spring jackson配置 jackson json和对象的命名转换 property naming strategy SNAKE CASE date format yyyy MM dd
  • QML下如何实现邻近区域无遮挡文字滚动

    在一些嵌入式显示设备中需要实现文字滚动功能 而利用QML是实现文字的滚动大家经常使用的功能 也非常简单 本文主要讲解一下如何避免2个邻近区域的文字滚动重叠问题 大家有没有按照如下的方式来设置文本的滚动 Window visible true
  • 机器学习流程是什么?简述机器学习流程!

    1 抽象成数学问题 明确问题是进行机器学习的第一步 机器学习的训练过程通常都是一件非常耗时的事情 胡乱尝试时间成本是非常高的 这里的抽象成数学问题 指的明确我们可以获得什么样的数据 抽象出的问题 是一个分类还是回归或者是聚类的问题 2 获取
  • 【Linux】深入理解文件缓冲区

    文章目录 问题引入 如何理解缓冲区 缓冲区刷新策略 问题解释 模拟一个文件缓冲区 问题引入 首先看一段代码 include
  • Unity3D -- Hit UFO adapter模式

    目录 一 游戏内容 二 UML图设计 三 游戏的实现 1 DiskFactory 2 FlyAction 3 SSActionManager 4 PhyUFOFlyAction 5 PhyFlyActionManager 6 ISceneC
  • 2017-百度-安全岗笔试

    2017 baidu spring 1 请回答如下端口默认对应的服务 以及在渗透测试过程中我们可以从哪些角度考虑其安全问题 端口 21 22 873 1433 3306 6379 11211 端口 服务 说明 21 FTP 匿名访问 弱口令
  • STM32内部FLASH读写-通用

    转https m baidu com from 844b bd page type 1 ssid 0 uid 0 pu usm 401 2Csz 401320 2001 2Cta 40iphone 1 10 1 3 602 baiduid
  • 基于vite搭建一个react移动端脚手架

    基于移动端的配置 vite官网 优点 光速启动 热模块替换 按需编译 脚手架功能 antd mobile移动端组件 axios网络数据交互 hox状态管理 react router dom路由管理 postcss px to viewpor