vue3+webpack5+ts 搭建生产级移动端项目

·  阅读 710

项目搭建

局部安装vue/lci,这里使用局部安装是因为维护项目较多vue/cli版本不统一,根据个人需求全局还是局部安装

npm init -y
npm install @vue/cli

局部vue/cli创建项目: npx vue create project

F5461DE9-EB48-460f-847D-CA7A38A6617F.png

224E7045-6DB9-42ff-AB05-5BFEB9A1DC53.png

规范

editorconfig

解决ide、操作系统不同,代码风格一致性(写代码时)

根目录下创建.editorconfig

# Editor configuration, see http://editorconfig.org

# 表示是最顶层的 EditorConfig 配置文件
root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

如果使用vscode,需要安装EditorConfig插件才能自动读取.editorconfig

prettier

prettier代码格式化工具,如果使用vscode需要安装prettier插件,保存时自动格式化

根目录下创建.prettierrc

{
  "useTabs": false, //使用tab还是空格
  "tabWidth": 2,    //空格情况下,选择几个空格
  "printWidth": 100,//行字符超过100换行,也可以80/120
  "singleQuote": true,//true单引号,false双引号
  "trailingComma": "none",//多行输入末尾是否添加逗号
  "bracketSpacing": true,//在{}前后输出空格
  "semi": false    //语句末尾是否带分号
}

根目录下创建忽略文件.prettierignore

/dist/*
.local
.output.js
/node_modules/**

**/*.svg
**/*.sh

/public/*

创建命令用于全部文件全部格式化:

安装 npm install prettier -D

package.json中创建命令prettier

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "prettier": "prettier --write ."
  }

eslint代码检查

eslint用于检查代码规不规范,如果使用vscode需要安装eslint插件

针对ts中使用any类型报错,可以在.eslintrc.js中添加

rules: {
    '@typescript-eslint/no-var-requires': 'off',
    '@typescript-eslint/no-explicit-any': 'off', //解决ts any 报错
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-non-null-assertion': 'off'
}

解决eslint和prettier冲突

在create项目时选择eslint + prettier会自动安装以下两个插件,如果没有需手动安装

npm i eslint-plugin-prettier eslint-config-prettier -D

还需要在.eslintrc.js中添加"plugin:prettier/recommended"

extends: [
    "plugin:vue/vue3-essential",
    "eslint:recommended",
    "@vue/typescript/recommended",
    "@vue/prettier",
    "@vue/prettier/@typescript-eslint",
    "plugin:prettier/recommended"
  ],

husky

用于git hooks拦截

  • 提交前 pre-commit
  • 提交信息 commit-msg
  • push前 pre-push

主要作用:提交前将代码格式化,我们希望保证代码仓库中的代码都是符合eslint规范的

安装并自动配置命令:

windows执行npx husky-init ; npm install

mac执行npx husky-init && npm install

上述命令主要帮我们做了三件事:

  1. 安装husky相关依赖
  2. 根目录下创建.husky文件夹
  3. package.json中添加一个脚本"prepare": "husky install"
"scripts": {
    "prepare": "husky install"
  },

接下来,我们需要去完成一个操作:在进行commit时,执行lint脚本

image.png 这个时候我们执行git commit的时候会自动对代码进行lint校验。

扩展:npx代表执行node_modules下的bin下的命令

lint-staged

lint-staged 这个工具一般结合 husky 来使用,它可以让 husky 的 hook 触发的命令只作用于 git add那些文件(即 git 暂存区的文件),而不会影响到其他文件。对暂存区的文件进行eslint校验

  1. 安装 lint-staged
npm i lint-staged -D
  1. 在 package.json里增加 lint-staged 配置项
"*.{vue,js,jsx,ts,tsx}": [
      "npm run prettier", //prettier全局格式化
      "npm run lint",     //执行eslint检测
      "git add -n"        //继续git 流程
    ]

commit提交信息规范

Commitizen 是一个帮助我们编写规范 commit message 的工具

1.安装Commitizen

npm install commitizen -D

2.安装cz-conventional-changelog,并且初始化cz-conventional-changelog

npx commitizen init cz-conventional-changelog --save-dev --save-exact

这个命令帮我们干了两件事:

  1. 安装cz-conventional-changelog
  2. 并且在package.json中配置
"config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }

这个时候我们提交代码需要使用 npx cz 或者在package.jsonscripts中构建一个命令来执行 cz

"scripts": {
    "commit": "cz"
  },

利用npm run commit代替git commit; 如果全局安装了commitizen,可以使用git cz

cz规范分格详解

  • 第一步是选择type,本次更新的类型
Type作用
feat新增特性 (feature)
fix修复 Bug(bug fix)
docs修改文档 (documentation)
style代码格式修改(white-space, formatting, missing semi colons, etc)
refactor代码重构(refactor)
perf改善性能(A code change that improves performance)
test测试(when adding missing tests)
build变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等)
ci更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等
chore变更构建流程或辅助工具(比如更改测试环境)
revert代码回退
  • 第二步选择本次修改的范围(作用域)

image-20210723150147510

  • 第三步选择提交的信息

image-20210723150204780

  • 第四步提交详细的描述信息

image-20210723150223287

  • 第五步是否是一次重大的更改

image-20210723150322122

  • 第六步是否影响某个open issue

image-20210723150407822

代码提交验证

如果我们按照cz来规范了提交风格,但是依然有同事通过 git commit 按照不规范的格式提交应该怎么办呢?

  • 我们可以通过commitlint来限制提交;

1.安装 @commitlint/config-conventional@commitlint/cli

npm i @commitlint/config-conventional @commitlint/cli -D

2.在根目录创建commitlint.config.js文件,配置commitlint

module.exports = {
  extends: ['@commitlint/config-conventional']
}

3.使用husky生成commit-msg文件,验证提交信息

npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

webpack配置

根目录下创建 vue.config.js

  1. 配置方式一:CLI提供的属性
  2. 配置方式二:

(1) 和webpack属性完全一致,最后会进行合并

configureWebpack:{
    relove:{
        alias:{
            components:'@/components'
        }
    }
}

(2) 和webpack属性完全一致,覆盖操作

configureWebpack:(config) => {
    config.resolve.alias = {
        '@':ptah.resolve(__dirname,'src'),
        components:'@/components'
    }
}

3.配置方式三:链式操作

chainWebpack:(config) => {
    config.resolve.alias
    .set('@',path.resolve(__dirname,'src'))
    .set('components','@/components')
}

集成vant

  1. 安装vant3
npm i vant@3
  1. 按需引入 安装插件npm i babel-plugin-import -D 配置插件
{ 
   "plugins": [ 
     [ 
       "import", 
       { 
           "libraryName": "vant", 
           "libraryDirectory": "es", 
           "style": true 
       } 
    ] 
   ] 
}
  1. 引入组件

import { Button } from 'vant'

  1. 按需引入全局注册
import {
  Button,
  Icon
} from 'vant'

const app = createApp(App)

const components = [
  Button,
  Icon
]

for (const cpn of components) {
  app.component(cpn.name, cpn)
}

移动端适配

移动端单位使用vw/rem,这里我使用的vw,安装postcss-px-to-viewport插件,可以直接根据设计图写真实px

  • 安装

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

  • vant 适配 vw

根目录下创建postcss.config.js

module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      viewportWidth: 375
    }
  }
}

环境变量

  • 方式一:

基于vue/cli

.env.development

.env.production

.env.test

变量名已VUE_APP_固定格式开头

使用process.env.VUE_APP_ xxx获取

  • 方式二:
let BASE_URL = ''
const TIME_OUT = 10000

if (process.env.NODE_ENV === 'development') {
  BASE_URL = '/api'
} else if (process.env.NODE_ENV === 'production') {
  BASE_URL = 'http://coderwhy.org/prod'
} else {
  BASE_URL = 'http://coderwhy.org/test'
}

export { BASE_URL, TIME_OUT }

axios集成

  1. 安装axios
npm install axios@0.21.1
  1. 封装axios

封装以下功能:

  • 创建HYRequest类用于生成不同配置的axios实例

  • 配置可以实例配置或根据单个请求配置

  • 拦截器

    拦截器执行顺序:请求拦截后拦截的先执行,响应拦截先拦截的先执行

    拦截器粒度化,可扩展;分为全局拦截器,实例拦截器,请求拦截器

    请求

    service.interceptors.request.use(fn1,fn2)
    
    fn1: 请求成功走这里
    
    (config) => {
        //请求头
        //loading
    }
    
    fn2: 请求失败走这里
    (err) => {
        console.log('请求发送错误') //message.error 组件
        return err
    }
      
    

    响应

    service.interceptors.response.use(fn1,fn2)
    
    fn1: 服务器返回数据成功  响应成功走这里
    
    (res) => {
    }
    
    fn2: 服务器返回失败 请求失败走这里
    
    (err) => {
    
    }
    
  • 前缀地址

  • 超时

完整代码:

//-------request.ts----------
import axios from 'axios'
// 实例类型
import type { AxiosInstance } from 'axios'
// 自定义接口
import type { HYRequestInterceptors, HYRequestConfig } from './type'

import { Toast, Dialog } from 'vant'

// 创建不同的axios实例
class HYRequest {
  instance: AxiosInstance
  interceptors?: HYRequestInterceptors

  constructor(config: HYRequestConfig) {
    // 创建axios实例
    this.instance = axios.create(config)
    // 保存信息
    this.interceptors = config.interceptors

    // 创建拦截器
    // 1.从config中取出的拦截器是对应的实例的拦截器(传参才有)
    this.instance.interceptors.request.use(
      this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )

    // 2.全局拦截器,添加所有实例拦截
    this.instance.interceptors.request.use(
      (config) => {
        // @todo添加loading
        return config
      },
      (err) => {
        // return err
        return Promise.reject(err)
      }
    )

    this.instance.interceptors.response.use(
      (config) => {
        // @todo移除loadinging
        const data = config.data
        if (data.success) {
          return data
        } else {
          //2. 返回200,服务器没有正常返回数据
          Toast.fail(`服务器错误 ${data.message}`)
        }
      },
      (err) => {
        // @todo移除loading
        let message = ''
        //1. 响应失败返回错误码
        switch (err.response.status) {
          case 400:
            message = '参数不正确'
            break
          case 401:
            message = '您未登录,或者登录已经超时,请先登录!'
            break
          case 403:
            message = '拒绝访问'
            break
          case 404:
            message = '很抱歉资源未找到'
            break
          case 500:
            message = '服务器错误'
            break
          case 502:
            message = '网关错误'
            break
          case 503:
            message = '服务不可用'
            break
          case 504:
            message = '网络超时,请稍后再试!'
            break
          default:
            message = `系统提示${err.response.message}` //异常问题,请联系管理员!
            break
        }
        Toast.fail(message)
        // return err
        return Promise.reject(err)
      }
    )
  }

  request<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      // 单个请求对请求config的处理
      if (config.interceptors?.requestInterceptor) {
        config = config.interceptors.requestInterceptor(config)
      }
      // 判断是否需要显示loading
      // if (config.showLoading === false) {
      //   this.showLoading = false
      // }
      this.instance
        .request<any, T>(config)
        .then((res) => {
          // 单个请求对数据的处理
          if (config.interceptors?.responseInterceptor) {
            res = config.interceptors.responseInterceptor(res)
          }
          //这样不会影响下一个请求
          // this.showLoading = true

          //将结果resolve
          resolve(res)
        })
        .catch((err) => {
          //这样不会影响下一个请求
          // this.showLoading = true
          reject(err)
        })
    })
  }

  get<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  post<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  delete<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }

  put<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'PUT' })
  }
}
export default HYRequest

//------------type.ts--------------
//axios 配置类型,响应类型
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
// 定义拦截器接口
export interface HYRequestInterceptors<T = AxiosResponse> {
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  responseInterceptor?: (res: T) => T
  responseInterceptorCatch?: (error: any) => any
}

export interface HYRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: HYRequestInterceptors<T>
}

//-------------index.ts----------
import HYRequest from './request'
import localCache from '@/utils/cache'
const hyRequest = new HYRequest({
  baseURL: process.env.VUE_APP_BASE_URL,
  timeout: 9000,
  interceptors: {
    requestInterceptor: (config) => {
      //从localStorage获取token
      const token = localCache.getCache('token')
      if (token) {
        config.headers['X-Access-Token'] = token // 让每个请求携带自定义 token
        // config.headers.Authorization = `Bearer ${token}`
      }
      return config
    },
    requestInterceptorCatch: (err) => {
      return err
    },
    responseInterceptor: (config) => {
      return config
    },
    responseInterceptorCatch: (err) => {
      return err
    }
  }
})
export default hyRequest
  1. 封装api
//------------api.ts--------------
import hyRequest from '@/utils/request'
import qs from 'qs'

interface DataType<T = any> {
  result: T
  code: string | number
  success: boolean
}

// post
export function postAction(url: string, parameter: any): Promise<DataType> {
  return hyRequest.post<DataType>({
    url: url,
    data: parameter
  })
}

// put
export function putAction(url: string, parameter: any): Promise<DataType> {
  return hyRequest.put<DataType>({
    url: url,
    data: parameter
  })
}

// get
export function getAction(url: string, parameter: any): Promise<DataType> {
  return hyRequest.get<DataType>({
    url: url,
    params: parameter
  })
}

// delete
export function deleteAction(url: string, parameter: any): Promise<DataType> {
  return hyRequest.delete<DataType>({
    url: url,
    params: parameter
  })
}

// 下载文件,用于excel导出 get
export function downFileGet(url: string, parameter: any): Promise<DataType> {
  return hyRequest.get<DataType>({
    url: url,
    params: parameter,
    responseType: 'blob'
  })
}

// 下载文件,用于excel导出 post
export function downFilePost(url: string, parameter: any): Promise<DataType> {
  return hyRequest.post<DataType>({
    url: url,
    params: parameter,
    responseType: 'blob'
  })
}

// 文件上传
export function uploadAction(url: string, parameter: any): Promise<DataType> {
  return hyRequest.post<DataType>({
    url: url,
    data: parameter,
    headers: {
      'Content-Type': 'multipart/form-data' // 文件上传
    }
  })
}

//post 序列化参数
export function loginAPI(url: string, parameter: any): Promise<DataType> {
  return hyRequest.post({
    url: url,
    data: parameter,
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    transformRequest: [
      (data) => {
        return qs.stringify(data)
      }
    ]
  })
}

  1. 补充

(1)同时发送异步请求

axios.all([]) => promise.all([])

所有请求都拿到数据才返回

(2)发送第一个请求拿到结果再发送第二个请求

请求1.then(请求2.then(请求3))

await 请求1; await 请求2

css预处理器

  • 安装less
npm install less
  • 公共样式文件
@import './variables.less';

//文本最多两行展示
.text-line-2 {
  overflow: hidden;
  text-overflow: ellipsis;
  word-break: break-word;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  display: -webkit-box;
  text-align: left;
  line-height: 1.5;
}

//单行文本超出...表示
.text-line-1 {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: inline-block;
}

.z-index-1 {
  z-index: 1;
}

.z-index-2 {
  z-index: 2;
}

.z-index-4 {
  z-index: 4;
}

.z-index-max {
  z-index: 999;
}

//iPhoneX顶部危险区
.iphonex-top {
  padding-top: constant(safe-area-inset-top);
  padding-top: env(safe-area-inset-top);
}

//iphonex底部危险区
.iphonex-bottom {
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}

//混入 scroll-view溢出滚动,未设置定位
.scroll-view {
  overflow: hidden;
  overflow-y: auto;
  z-index: 2;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* firefox */
  -ms-overflow-style: none;

  /* IE 10+ */
  &::-webkit-scrollbar {
    display: none;
    /* Chrome Safari */
  }
}

//混入,非iphonex 定位在navbar和toolbar中间,iphonex需要重新设置top
.scroll-view-content {
  position: absolute;
  top: @statusBarHeight + 92px;
  left: 0;
  bottom: calc(100px + constant(safe-area-inset-bottom));
  bottom: calc(100px + env(safe-area-inset-bottom));
  overflow: hidden;
  overflow-y: auto;
  z-index: 2;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* firefox */
  -ms-overflow-style: none;

  /* IE 10+ */
  &::-webkit-scrollbar {
    display: none;
    /* Chrome Safari */
  }
}
  • 变量文件
@infoSize: 28px;
@minInfoSize: 24px;
@minSize: 16px;
@titleSize: 40px;
@signSize: 20px;
@radiusSize: 24px;
@marginSize: 20px;
@paddingSize: 20px;
@navBarHeight: 44PX;
@statusBarHeight: 44px;
@iphoneXTop: 44PX;
@iphonexBottom: 34PX;

@mainColor: #d81e06;
@textHintColor: #999999;
@bgColor: #f6f6f6;
@lineColor: #e5e5e5;
@fontColor: #333;
@lineColor: #f6f7f8;
@intervalLineColor: #ebedf0;

项目地址

github 代码地址链接

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改