前端框架:vite2+vue3+typescript+axios+vant移动端 框架实战项目详解(四)

5,809 阅读3分钟

本系列文章过长会分开消化理解:

资源处理

引用静态资源

这块官网说的很清楚,可以查看:vitejs.cn/guide/asset…

以相对和绝对路径方式引用

我们可以在*.vue 文件的template, style和纯.css文件中以相对和绝对路径方式引用静态资源。

<!-- 相对路径 -->
<img src="./assets/logo.png">
<!-- 绝对路径 -->
<img src="/src/assets/logo.png">


<style scoped>
#app {
  background-image: url('./assets/logo.png');
}
</style>

图片动态引入

1、import

<img :src="imgUrlVal"><script lang="ts" setup>
    import { ref } from 'vue'
    import imgUrl from './img.png'
    const imgUrlVal = ref(imgUrl)
</script><style scoped>
#app {
  background-image: url('./assets/logo.png');
}
</style>

2、new URL

import.meta.url 是一个 ESM 的原生功能,会暴露当前模块的 URL。将它与原生的 URL 构造器 组合使用,在一个 JavaScript 模块中,通过相对路径我们就能得到一个被完整解析的静态资源 URL:

创建一个新 URL 对象的语法:

new URL(url, [base])
  • url —— 完整的 URL,或者仅路径(如果设置了 base),
  • base —— 可选的 base URL:如果设置了此参数,且参数 url 只有路径,则会根据这个 base 生成 URL。

🍕例子:

const imgUrl = new URL('./img.png', import.meta.url).hrefdocument.getElementById('hero-img').src = imgUrl

./img.png是相对路径,而import.meta.url 是base url (根链接)。

这在现代浏览器中能够原生使用 - 实际上,Vite 并不需要在开发阶段处理这些代码!

这个模式同样还可以通过字符串模板支持动态 URL:

function getImageUrl(name) {
  return new URL(`./dir/${name}.png`, import.meta.url).href
}

在生产构建时,Vite 才会进行必要的转换保证 URL 在打包和资源哈希后仍指向正确的地址。

注意:无法在 SSR 中使用

如果你正在以服务端渲染模式使用 Vite 则此模式不支持,因为 import.meta.url 在浏览器和 Node.js 中有不同的语义。服务端的产物也无法预先确定客户端主机 URL。

new Url中无法使用别名,但是可以拼接路径

public目录

public 目录下可以存放在源码中引用的资源,它们会被留下且文件名不会有哈希处理。

这些文件会被原封不动拷贝到发布目录的根目录下。

<img src="/logo.png">

🙋‍问:什么样的文件适合放到public 目录下?

  • 不会被源码引用(例如 robots.txt
  • 必须保持原有文件名(没有经过 hash)
  • …或者你只是不想为了获取 URL 而首先导入该资源

目录默认是 <root>/public,但可以通过 publicDir 选项 来配置。

😈注意避坑:

  • 引入 public 中的资源永远应该使用根绝对路径 - 举个例子,public/icon.png 应该在源码中被引用为 /icon.png
  • public 中的资源不应该被 JavaScript /ts文件引用。
  • 可以将该资源放在一个特别的 public 目录中,它应位于你的项目根目录。该目录中的资源应该在开发时能直接通过 / 根路径访问到,并且打包时会被完整复制到目标目录的根目录下。

利用jest@vue/test-utils测试组件

安装依赖

"jest": "^24.0.0",
"vue-jest": "^5.0.0-alpha.3",
"babel-jest": "^26.1.0",
"@babel/preset-env": "^7.10.4",
"@vue/test-utils": "^2.0.0-beta.9"

配置babel.config.js

module.exports = {
  presets: [
    [
      "@babel/preset-env", { 
        targets: { 
          node: "current" 
        } 
      }
    ]
  ],
};

配置jest.config.js

module.exports = {
  testEnvironment: "jsdom",
  transform: {
    "^.+\.vue$": "vue-jest",
    "^.+\js$": "babel-jest",
  },
  moduleFileExtensions: ["vue", "js", "json", "jsx", "ts", "tsx", "node"],
  testMatch: ["**/tests/**/*.spec.js", "**/__tests__/**/*.spec.js"],
  moduleNameMapper: {
    "^main(.*)$": "<rootDir>/src$1",
  },
};
​

启动脚本

"test": "jest --runInBand"

测试代码,tests/example.spec.js

import HelloWorld from "main/components/HelloWorld.vue";
import { shallowMount } from "@vue/test-utils";
​
describe("aaa", () => {
  test("should ", () => {
    const wrapper = shallowMount(HelloWorld, {
      props: {
        msg: "hello,vue3",
      },
    });
    expect(wrapper.text()).toMatch("hello,vue3");
  });
});

lint配置添加jest环境,要不然会有错误提示:

module.exports = {
  env: {
    jest: true
  },
}

linttestgit挂钩

npm i lint-staged yorkie -D

"gitHooks": {
  "pre-commit": "lint-staged",
  "pre-push": "npm run test"
},
"lint-staged": {
  "*.{js,vue}": "eslint"
},

正常情况下安装 yorkie 后会自动安装提交钩子 如果提交钩子未生效可以手动运行 node node_modules/yorkie/bin/install.js 来安装。 当然,你也可以运行 node node_modules/yorkie/bin/uninstall.js 来卸载提交钩子。

模式和环境变量

模式和环境变量

使用模式做多环境配置,vite serve时模式默认是developmentvite build时是production

当需要将应用部署到生产环境时,只需运行 vite build 命令。默认情况下,它使用 <root>/index.html 作为构建入口点,并生成一个适合通过静态部署的应用包。查看 部署静态站点 获取常见服务的部署指引。

多环境配置,在之前的系列内容中我们其实创建过

.env.development //开发环境配置文件
.env.production  //生产环境配置文件

放在根目录,方便vite.config.ts读取

可以使用

export default ({ command, mode }) => {
  return defineConfig({
    });
};

代替

export default defineConfig({
 });
​

✔创建配置文件.env.development

VITE_BASE_API=/api
VITE_BASE_URL=./
VITE_APP_OUT_DIR = dist

✔创建配置文件.env.production

VITE_BASE_API=/api
VITE_BASE_URL=./
VITE_APP_OUT_DIR = dist

✔在env.d.ts中声明(便于有智能提示和使用时无需再次断言)

// 环境变量提示
interface ImportMetaEnv{
  VITE_BASE_API:string
  VITE_BASE_URL:string
  VITE_APP_OUT_DIR:string
}

✔代码中读取

import.meta.env.VITE_BASE_API
import.meta.env.VITE_BASE_URL

😈注意避坑:

🙋‍1、import.meta.env.XXx 在.env[mode]中定义的变量不能访问

解决方式:要用 loadEnv函数代替

✔修改vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import { resolve } from "path";
import styleImport, { VantResolve } from "vite-plugin-style-import";
import { viteMockServe } from "vite-plugin-mock";
import { loadEnv } from "vite";

export default ({ command, mode }) => {
  return defineConfig({
    resolve: {
      alias: {
        "@": resolve(__dirname, "src"),
        comps: resolve(__dirname, "src/components"),
        apis: resolve(__dirname, "src/apis"),
        views: resolve(__dirname, "src/views"),
        utils: resolve(__dirname, "src/utils"),
        routes: resolve(__dirname, "src/routes"),
        styles: resolve(__dirname, "src/styles"),
      },
    },

    plugins: [
      vue(),
      vueJsx(),
      styleImport({
        resolves: [VantResolve()],
      }),
      viteMockServe({
        mockPath: "mock", // ↓解析根目录下的mock文件夹
        supportTs: false, // 打开后,可以读取 ts 文件模块。 请注意,打开后将无法监视.js 文件。
      }),
    ],
    server: {
      host: "0.0.0.0", // 解决  Network: use --host to expose
      // port: 4000, //启动端口
      open: true,
      // proxy: {
      //   "^/api": {
      //     target: "https://baidu.com",
      //     changeOrigin: true,
      //     ws: true,
      //     rewrite: pathStr => pathStr.replace(/^\/api/, ""),
      //   },
      // },
      // cors: true,
    },

    /**
     * 在生产中服务时的基本公共路径。
     * @default '/'
     */
    base: "./",
    build: {
    //target: 'modules', // 构建目标格式,默认是modules,也可以是esbuild配置项,https://esbuild.github.io/api/#target
      outDir: loadEnv(mode, process.cwd()).VITE_APP_OUT_DIR, // 构建输出路径
      assetsDir:"static", //静态资源文件夹,和outDir同级 默认assets
      sourcemap: false, // map文件
       assetsInlineLimit: 4096, // kb, 小于此值将内联base64格式
      // rollupOptions: {
      //   output: {
      //     chunkFileNames: 'static/js1/[name]-[hash].js',
      //     entryFileNames: 'static/js2/[name]-[hash].js',
      //     assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
      //   },
      // },
      // brotliSize: false, // 不统计
      // minify: 'esbuild' // 混淆器,terser构建后文件体积更小
      // terserOptions: {
      //   compress: {
      //     drop_console: true,
      //   },
      // },
    },
  });
};

### 🛢打包

使用npm run build执行打包

自定义构建

构建过程可以通过多种 构建配置选项 来自定义。特别地,你可以通过 build.rollupOptions 直接调整底层的 Rollup 选==项

vite.config.ts

build: {
    rollupOptions: {
      // https://rollupjs.org/guide/en/#big-list-of-options
    }
  }

🍕示例:1、多页面应用模式

假设你有下面这样的项目文件结构

├── package.json
├── vite.config.js
├── index.html
├── main.js
└── nested
    ├── index.html
    └── nested.js

在开发中,简单地导航或链接到 /nested/ - 将会按预期工作,就如同一个正常的静态文件服务器。

在构建中,你要做的只有指定多个 .html 文件作为入口点:

vite.config.ts

 build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        nested: resolve(__dirname, 'nested/index.html')
      }
    }
  }

🍕示例:2、库模式

当你开发面向浏览器的库时,你可能会将大部分时间花在该库的测试/演示页面上。使用 Vite,你可以使用 index.html 。

当需要构建你的库用于发布时,请使用 build.lib 配置项,请确保将你不想打包进你库中的依赖进行外部化,例如 vue 或 react:

vite.config.ts

 build: {
    lib: {
      entry: resolve(__dirname, 'lib/main.js'),
      name: 'MyLib'
    },
    rollupOptions: {
      // 请确保外部化那些你的库中不需要的依赖
      external: ['vue'],
      output: {
        // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
        globals: {
          vue: 'Vue'
        }
      }
    }
  }

问答时间

🙋‍问:1、vite无法使用require,require is not defined

答:原因:

  • node.js不是内置对象的一部分,如果想用typescript写Node.js,则需要引入第三方声明文件
  • vue无法识别require,浏览器不支持cjs

处理方式:如果使用的是 typeScript2.x 那么只需要安装以下包即可

npm install @types/node --save-dev


处理方式:1、使用import引用替换require

🍕例如: 我们想引入一个图片:require('./logo.png') 是会报错的

<template>
    <img :src="imgUrl" alt="">
    <img :src="imgUrl2" alt="">
</template>
<script setup lang="ts">
    import {ref, onMounted} from "vue";
    import imgUrl from './logo.png'
    const imgUrl2 = ref('');
    const handleImgSrc = async()=>{
        let m = await import('./logo.png');
        imgUrl2.value = m.default;
     };
    handleImgSrc()
</script>

2、new URL

require('./assets/home.png')
// 等价于
new URL('./assets/home.png', import.meta.url).href

require在vite中是不可用的

而import()在vite环境下可用,但是在css in js不可用.

2、使用import.meta.globEager() 或者import.meta.glob

注意,路径需为以 ./ 开头)或绝对路径(以 / 开头,相对于项目根目录解析

  • 这只是一个 Vite 独有的功能而不是一个 Web 或 ES 标准
  • 该 Glob 模式会被当成导入标识符:必须是相对路径(以 ./ 开头)或绝对路径(以 / 开头,相对于项目根目录解析)。
  • Glob 匹配是使用 fast-glob 来实现的 —— 阅读它的文档来查阅 支持的 Glob 模式
  • 你还需注意,glob 的导入不接受变量,你应直接传递字符串模式。
  • glob 模式不能包含与包裹引号相同的引号字符串(其中包括 '"``),例如,如果你想实现'/Tom's files/ '的效果,请使用"/Tom's files/ "` 代替。

import.meta.glob 为过动态导入,构建时,会分离为独立的 chunk

import.meta.globEager 为直接引入

🍕例子:

export function getAssetsImagesUrl(path:string) {
    return import.meta.globEager("/src/assets/images/*/*")[`/src/assets/images/${path}`].default
}

🍕或者某个文件夹下的文件

export function loadLottieApidataFile(path: string, meta: IdefaultObject) {
  return new Promise((resolve, reject) => {
    const modules = import.meta.glob(`/src/scripts/lottie/*/*.ts`);
    meta = meta || {};
    // 加载
    modules[`/src/${path}`]()
      .then(
        (data) => {
          meta = { ...meta, ...data.default };
          meta.reload = !meta.reload; // 添加重新绑定的开关
          resolve(data.default);
        },
        (err) => {
          reject(err);
        }
      )
      .catch((err) => {
        reject(err);
      });
  });
}

🍕动态导入多个vue页面

import { App, Component } from 'vue'interface FileType {
    [key: string]: Component
}
​
// 导入 globComponents 下面的 所有 .vue文件
const files: Record<string, FileType> = import.meta.globEager("/src/components/golbComponents/*.vue")
​
export default (app: App): void => {
    // 因为通过 import.meta.globEager 返回的列表不能迭代所以直接使用 Object.keys 拿到 key 遍历
    Object.keys(files).forEach((c: string) => {
        const component = files[c]?.default
        // 挂载全局控件
        app.component(component.name as string, component)
    })
}
​

如果直接使用import.meta.globvscode会报类型ImportMeta上不存在属性“glob”的错误,需要在tsconfig文件下添加类型定义vite/client

{
  "compilerOptions": {
    "types": ["vite/client"]
  }
}

ps: module.exports = { 这种的也最好改为 export default {

🙋‍问:2、启动页空白,怎么办?

答:有可能的原因:

  • 检查兼容性、
  • 注意入口文件index.html,需要放置项目根目录、
  • 在服务器中还是空白,在vite.config.ts文件添加base:'./' (因为 vue 打包后的路径默认是根路径,而在 vite 里面的配置文件是 vite.config.ts)

🙋‍问:3、vite环境下默认没有process.env,怎么办?

答:可通过define定义全局变量,在vite.config.ts中使用

自定义全局变量process.env

vite.config.ts中使用

define: {
  'process.env': {}
}

🙋‍问:4、子组件中defineProps如何设置特殊的类型

答:PropType使用

假如我有一个TitleScore组件,需要scoreData信息属性,需要准守 ITitleScore interface。

1、 引入

import { PropType } from 'vue'

2、 定义接口

// 评分组件
export interface ITitleScore {
    title: string
    score: number
}

3、 属性验证

const { scoreData } = defineProps({
    scoreData: {
        type: Object as PropType<ITitleScore>
    }
}) 

🙋‍问:5、子组件中defineProps如何设置默认值

答:使用withDefaults

withDefaults作用是给defineProps绑定默认值的api

如在问题4的基础上在加上默认值

父组件

<template>
   <TitleScore :scoreData="{ title: `评分`, score: 98 }" />
</template>

子组件

<template>
    <!-- 发质评分组件 -->
    <div class="titlescore-container display-center-align-items">
        <span class="titlescore-text">{{scoreData.title}}:</span>
        <div class="score-bg all-score" v-for="item in totalScore" :key="item">
            <div v-if="scoreData.score >= item" class="score-bg current-score"></div>
        </div>
    </div>
</template><script setup lang="ts">
import { PropType } from 'vue';
​
 interface ITitleScore {
    title: string
    score: number
}
​
withDefaults(defineProps<ITitleScore>(),{
    title:'发质评分',
    score:100
 })
​
​
​
</script><style lang="scss" scoped>
​
    .titlescore-container{
        width: 100%;
        height: 15px;
        margin-bottom: 15px;
        .titlescore-text{
            font-size: 15px;
            font-family: PingFangSC-Semibold, PingFang SC;
            font-weight: 600;
            color: #000000;
        }
        .score-bg{
            background-image: url('../../assets/images/common/iconBg.png');
            background-repeat: no-repeat;
            background-size: 180px auto;
        }
        .all-score{
            width: 16px;
            height: 15px;
            background-position: -46px -163px;
            margin-right: 6px;
            position: relative;
        }
        .current-score{
            width: 100%;
            height: 100%;
            background-position: -15px -163px;
            position: absolute;
            left: 0;
            top: 0;
        }
    }
​
</style>

注意:默认值为引用类型的,需要包装一个函数 return 出去。

<script lang='ts' setup>
interface Props {
  child: string|number,
  title?: string, // 未设置默认值,为 undefined
  strData: string,
  msg?: string
  labels?: string[],
  obj?:{a:number}
}
const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two'],
  obj: () => { return {a:2} }
})
</script>

🙋‍问:6、打包生成的index.html直接在浏览器打开会报跨域的错误

答:Vite 默认输出 <script type=module>,也就是 ES Modules,它是不支持文件系统访问的,我们可以使用 VScode 的 open with live server 打开html文件

image.png

ps:其他问题正在总结中。。。

可查看仓库:github.com/lixiaoyanle…