目录
前言
一、本地存储
1.1.5 源代码
1.1.6 提交代码
1.2 localStorage
1.2.1 新建 storage.ts
1.2.2 实现存取函数
1.2.3 使用一下
1.2.4 提交代码
二、日期相关
2.1 格式化
2.1.1 新建 date.ts
2.1.2 完成格式化函数
2.1.3 测试一下
2.1.4 提交代码
三、设备区分
3.1 使用 userAgent
3.1.1 新建 device.ts
3.1.2 源代码
3.1.3 提交代码
3.2 h5 唤起 App
3.2.1 安装 callapp-lib
3.2.2 实现唤起函数
3.2.3 源代码
3.2.4 提交代码
四、判断 js 类型
4.1 实现各种 is 函数
4.1.1 新建 is.ts
4.1.2 源代码
4.1.3 提交代码
4.2 应用 is 方法
4.2.1 修改 env.ts
4.2.2 修改 storage.ts
4.2.3 提交代码
五、上传文件
5.1 自定义上传单个方法
5.1.1 新建 file.ts
5.1.2 实现选择文件函数
5.1.3 应用
5.1.4 提交代码
5.2 自定义上传多个文件
5.2.1 增加 webMulChooseFile 方法
5.2.2 提交代码
5.3 使用组件库上传
六、下载文件
6.1 浏览器能预览的文件
6.1.1 通过链接下载
6.2 浏览器不能预览的文件
6.2.1 下载txt
6.2.2 下载 exe、zip
6.2.3 提交代码
七、节流和防抖
7.1 安装 lodash-es
7.2 安装 @types/lodash-es
7.3 应用
7.4 提交代码
八、部署
8.1 本地打包
8.1.1 运行 npm run build
8.1.2 解决问题
8.1.3 重新打包
8.1.4 预览
8.1.5 提交代码
8.2 使用vercel
8.2.1 创建 github 仓库
8.2.2 vercel 导入 github 仓库
8.2.3 预览
小结
总结
在看这篇文章之前,请先看之前两章的内容,分别是项目搭建和请求封装,本篇文章是在前两篇文章基础上进行的,这篇文章是整个 vite + ts + vue3 项目的完结篇。
在第二篇文章中我们提到还剩两部分的内容没有写,一个是公共工具方法,一个是项目部署,这两部分也是这篇文章的主要内容。
在开始之前,请去仓库克隆代码,本篇文章是在这个提交的基础上进行的,建议在这个提交记录新建新分支然后开始一步一步的学习。
本篇文章的全部代码相关的流程图如下:
对于 cookie 建议是用 js-cookie 模块,方便我们设置过期时间。
pnpm i js-cookie
pnpm i @types/js-cookie -D
在 src/global 下新建 cookie.ts
import Cookies from 'js-cookie'
export const setCookie = Cookies.set.bind(Cookies)
export const removeCookie = Cookies.remove.bind(Cookies)
export const getCookie = Cookies.get.bind(Cookies)
在 src/global 下新建文件 storage.ts
// 所有 key 用常量存储起来
export enum StorageKeys {
TOKEN = 'TOKEN',
NAME_LIST = 'NAME_LIST',
}
export function setStorage(key: string, val: string): void {
localStorage.setItem(key, val)
}
export function getStorage(key: string): string | null {
return localStorage.getItem(key) || ''
}
export function removeStorage(key: string): void {
localStorage.removeItem(key)
}
// 判断是否存在
export function hasStorage(key: string): boolean {
return getStorage(key) !== null
}
// 取数组类型的,直接返回解析后的数组
export function getStorageArray(key: string): Array | null {
const data = localStorage.getItem(key)
if (data === null) {
return data
}
try {
const arr = JSON.parse(data)
if (Array.isArray(arr)) {
return arr
}
localStorage.removeItem(key)
} catch (err) {
console.log('json parse error getStorageArray', err)
}
return null
}
// 取对象类型的,直接返回解析后的对象
export function getStorageObject(key: string): Object | null {
const data = localStorage.getItem(key)
if (data === null) {
return data
}
try {
const obj = JSON.parse(data)
if (Object.prototype.toString.call(obj) === '[object Object]') {
return obj
}
localStorage.removeItem(key)
} catch (err) {
console.log('json parse error getStorageObject', err)
}
return null
}
在 login.vue 中使用一下
格式化日期比较简单,如果仅仅是这个功能就没必要引入 moment 包,自己写就行。
在 src/global 下面新建 date.ts
// ie 基本上已经不用了
export function isIE(UA: string): number {
const isIE = UA.indexOf('compatible') > -1 && UA.indexOf('MSIE') > -1 //判断是否IE<11浏览器
const isIE11 = UA.indexOf('Trident') > -1 && UA.indexOf('rv:11.0') > -1
if (isIE) {
const reIE = new RegExp('MSIE (\\d+\\.\\d+);')
reIE.test(UA)
const fIEVersion = parseFloat(RegExp['$1'])
if (fIEVersion == 7) {
return 7
} else if (fIEVersion == 8) {
return 8
} else if (fIEVersion == 9) {
return 9
} else if (fIEVersion == 10) {
return 10
} else {
return 6 //IE版本<=7
}
} else if (isIE11) {
return 11
} else {
return -1 //不是ie浏览器
}
}
function isMac(UA: string): boolean {
return /macintosh|mac os x/i.test(UA) ? true : false
}
function isWin32(UA: string): boolean {
return /win32|wow32/i.test(UA) ? true : false
}
function isWechat(UA: string): boolean {
return /MicroMessenger/i.test(UA) ? true : false
}
function isQQ(UA: string): boolean {
return /QQ/i.test(UA) ? true : false
}
function isMoible(UA: string): boolean {
return /(Android|webOS|iPhone|iPod|BlackBerry|Mobile)/i.test(UA) ? true : false
}
export function isIOS(UA: string): boolean {
return /iPhone|iPad|iPod/i.test(UA) ? true : false
}
function isAndroid(UA: string): boolean {
return /Android/i.test(UA) ? true : false
}
// 返回类型按需设置
export function deviceType(UA: string): { type: string; env?: string } {
if (isMoible(UA)) {
if (isIOS(UA)) {
if (isWechat(UA)) {
return {
type: 'ios',
env: 'wechat',
}
}
if (isQQ(UA)) {
return {
type: 'ios',
env: 'qq',
}
}
return {
type: 'ios',
}
}
if (isAndroid(UA)) {
if (isWechat(UA)) {
return {
type: 'android',
env: 'wechat',
}
}
if (isQQ(UA)) {
return {
type: 'android',
env: 'qq',
}
}
return {
type: 'android',
}
}
return {
type: 'mobile',
}
} else {
if (isMac(UA)) {
return {
type: 'mac',
}
}
let env = ''
if (isIE(UA) > 0) {
env = 'ie'
}
// 注意区分 window 的 32位/64位
if (isWin32(UA)) {
return {
type: 'win32',
env: env,
}
}
return {
type: 'win64',
env: env,
}
}
}
export function isPC(UA = window.navigator?.userAgent) {
const agent = deviceType(UA)
return ['pc', 'mac', 'win32', 'win64'].includes(agent.type)
}
pnpm i callapp-lib
import CallApp from 'callapp-lib'
// 移动端唤醒应用
export function callApp(isIOS: boolean) {
let packageName = ''
// 一般来说 ios 和 android 的包名不一样
if (isIOS) {
packageName = 'com.mobile.android.vite.name' // ios 移动端提供
} else {
packageName = 'com.mobile.ios.vite.name' // android 移动端提供
}
const appStoreUrl = 'https://itunes.apple.com/cn/app/xxxx' // 填写appstore的下载地址
const androidUrl = 'https://yingyongbao' // 软件应用宝地址
const options = {
scheme: {
protocol: 'vite', // 移动端设置的
},
intent: {
package: packageName,
scheme: 'vite', // 移动端设置的
},
timeout: 2000,
appstore: appStoreUrl,
yingyongbao: androidUrl,
fallback: 'https://www.xxx.com/',
}
const callLib = new CallApp(options)
callLib.open({
path: 'xxx.app/openwith', // 移动端提供
callback: () => {
if (isIOS) {
window.location.href = appStoreUrl // ios 直接跳转到appStore 地址即可
} else {
// android 跳转
const link = document.createElement('a')
link.target = '_blank'
link.href = androidUrl
link.click()
}
},
})
}
在 src/global 下新建 is.ts,可以用 is 这个第三方模块,但是没必要,因为逻辑很简单
const { isInteger } = Number
const { toString } = Object.prototype
export function isNumber(v: unknown): v is number {
return typeof v === 'number' && !Number.isNaN(v) && Number.isFinite(v)
}
export function isInt(v: unknown): boolean {
return isNumber(v) && isInteger(v)
}
export function isFloat(v: unknown): boolean {
return isNumber(v) && !isInteger(v)
}
export function isString(v: unknown): v is string {
return typeof v === 'string'
}
export function isBoolean(v: unknown): v is boolean {
return v === true || v === false
}
export function isFunction(v: unknown): v is (...args: unknown[]) => unknown {
return typeof v === 'function'
}
export const { isArray } = Array
export function isPlainObject(v: unknown): v is Record {
return toString.call(v) === '[object Object]'
}
export function isRegExp(v: unknown): v is RegExp {
return v instanceof RegExp
}
export function isDate(v: unknown): v is Date {
return v instanceof Date
}
export function isNull(v: unknown): v is null {
return v === null
}
export function isDef(v: unknown): v is Exclude {
return v !== undefined && v !== null
}
export function isUndef(v: unknown): v is undefined {
return v === undefined
}
export function isUndefOrNull(v: unknown): v is null | undefined {
return v === undefined || v === null
}
在 src/global 下新建文件 file.ts
import { isNull } from '@/global/is'
export function webChooseFile(cb: (file: File) => unknown, accept?: string): void {
const id = 'file-selector-99999999'
let input = document.getElementById(id) as HTMLInputElement
if (isNull(input)) {
input = document.createElement('input')
input.id = id
input.type = 'file'
input.style.position = 'fixed'
input.style.left = '-10000px'
document.body.appendChild(input)
} else {
input.value = ''
}
if (accept) {
input.accept = accept
} else {
input.removeAttribute('accept')
}
input.onchange = (): void => {
if (input.files?.length) {
cb(input.files[0])
}
}
input.classList.add('selectFile')
const e = document.createEvent('MouseEvent') as unknown as Event
e.initEvent('click', false, true)
input.dispatchEvent(e)
}
和 5.1 中的单个文件差不多,只是多了个参数
可以使用组件库自带的上传组件
如下载 pdf
export function webDownloadFileByUrl(url: string, name: string): void {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.responseType = 'blob'
xhr.onload = (): void => {
if (xhr.status === 200) {
const a = document.createElement('a')
a.download = name
a.href = URL.createObjectURL(xhr.response)
a.click()
}
}
xhr.send()
}
export function downloadToTxt(text: string, filename: string) {
const element = document.createElement('a')
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
element.setAttribute('download', filename)
element.style.display = 'none'
document.body.appendChild(element)
element.click()
}
直接使用 location.href 跳转即可
window.location.href = 'https://xxx.exe'
更多下载场景,可以参考这篇文章、这篇文章
pnpm i lodash-es
lodash 是一个强大的工具库,lodash-es 是lodash库按照 es 模块导出的,所以我们的项目中需要安装 loadsh-ts 更加方便。
lodash 的功能模块也可以单独安装,比如 lodash.throttle (节流)和 lodash.debounce ( 防抖)都可以单独安装,可以根据需要。
pnpm i @types/lodash-es -D
Lodash Documentation lodash 库的应用不做详细解释,可以参考官方文档
在部署之前我们要保证自己的代码能够在本地打包,运行 npm run build
发现报了一个错误
在 tsconfig.json 中加入 allowJs: true即可
发现可以正常打包了
运行 npm run preview,可以运行成功
每个公司部署代码的方式不尽相同,有的公司是用gitlab 的 ci/cd 流程 + docker + k8s 等一套流程,但是这些工作很多都是架构组的流程实现的,我们前端的小白就学会怎么用就行,后续当然还是要学会更好。
作为一个学习的项目,弄一套流程还是挺麻烦的,但是我们可以使用 vercel 这个工具,简单易学。
不幸的是,vercel 暂时不支持 gitee 仓库,所以我要先把仓库弄到 github 上面。在github 中 导入仓库即可,我的github仓库地址是这个
跟着步骤一步一步来就行
导入之后他会自动打包,而且如果你github 仓库有内容更新,vercel 这边也会重新打包,很厉害吧!
他会自动给你分配一个域名
好了,就是这么简单,你可以开始画你的页面了。
部署这一块有一个问题,就是我整个教程用的都是 gitee 仓库,开始是因为有些时候没有工具不能访问 github。但是如果要使用 vercel 部署就不克避免的要把仓库迁移到 github 了。所以如果你有科学上网的工具,也完全可以最开始就新建一个github 仓库,只是一个管理工具罢了,不必纠结。怎么方便怎么来。
至此,完整的vite + ts + vue3 项目的教程已经全部完成,现在可以说是克隆就能用了。
第一篇教程
第二篇教程
gitee 仓库地址
github 仓库地址
之后可能会有优化的内容会在后续更新。
内容很多,难免疏漏,有不对的地方,欢迎评论区指正。
好了,现在你领导来了,说:“小王啊,我们有个新项目,你先把项目搭建起来吧。”
这个时候你要首先判断是不是官网?需不需要服务端渲染?服务端渲染的项目需要用nuxt 等框架。我之后会出一个类似的 nuxt3 + vue +ts 教程,欢迎大家持续关注。
如果是普通的管理系统的项目,那就没问题了,你自信的说,好嘞,然后顺手把这个教程中的仓库克隆下来,改改名字(一定要改名字啊),摸会鱼,就可以交差了~~