欢迎各位后端程序员来到《Vue3 发懵到发癫》,各位在后端业务中神经百战,现在来试一下前端吧。

# Vue2

一些老项目还在用 vue2,我真的会谢,为什么呢,因为我实习时就要用 vue2

# Vue3

这里的技术栈主要是:Vue3-ts-vite。IDE:VSCode

其实 IDEA2023 版也支持创建 vue3 项目,方便程度都差不多吧,反正都要用命令行初始化项目,嘎嘎爽。

参考链接:

  • 首选官网https://cn.vuejs.org/
  • 快速入门(超级推荐):https://vue3.chengpeiquan.com/

# 搭建项目

选择一个文件夹,管理员权限打开 cmd,然后执行以下命令:

npm install -g create-preset :尽管有很多种创建 vue 项目模式,但是这种更快,将你需要的大部分依赖包都下了,可能会提示你 -g 已经弃用了,叫你使用 --local=global ,改一下即可。 -g 就是全局安装,之后再希望创建项目,就有直接的模板了。

preset -v :检查一下是否安装成功,成功的话会有一个版本号

preset init hello-vue3 --template vue3-ts-viteinit 后的参数就是项目名,最后一个固定参数表示使用的是 vue3、typescript、vite 构建工具

cd hello-vue3npm installnpm run dev :进入项目,安装一些必要的依赖,运行。

停止运行,用 vscode 打开项目,修改一下 vite.config.ts

如果在 Vite 的配置文件 vite.config.ts ,或者是在 Vue CLI(CLI 是 command-line Interface 的缩写) 的配置文件 vue.config.js 里设置了 alias 的话,因为 TypeScript 不认识里面配置的 alias 别名,所以需要再对 tsconfig.json 做一点调整,增加对应的 paths ,否则在 VSCode 里可能会路径报红,提示找不到模块或其相应的类型声明。

假设在 vite.config.ts 里配置了这些 alias :

export default defineConfig({
  //resolve () 函数在 path 包中,不要导错了
  resolve: {
    alias: {
      '@': resolve('src'), // 源码根目录
      '@img': resolve('src/assets/img'), // 图片
      '@less': resolve('src/assets/less'), // 预处理器
      '@libs': resolve('src/libs'), // 本地库
      '@plugins': resolve('src/plugins'), // 本地插件
      '@cp': resolve('src/components'), // 公共组件
      '@views': resolve('src/views'), // 路由组件
    },
  },
  // ...
})

那么在该项目的 tsconfig.json 文件里就需要相应的加上这些 paths :

{
  "compilerOptions": {
    // ...
    "paths": {
      "@/*": ["src/*"],
      "@img/*": ["src/assets/img/*"],
      "@less/*": ["src/assets/less/*"],
      "@libs/*": ["src/libs/*"],
      "@plugins/*": ["src/plugins/*"],
      "@cp/*": ["src/components/*"],
      "@views/*": ["src/views/*"]
    },
    // ...
  },
  // ...
}

# 入门教程

# 虚拟 DOM

如果说只用 js,我们想要动态的修改一个静态标签的内容,比如 h 文本等,或者动态增加一些标签之类的,必须要手撸 DOM 才行,然后重新渲染,如果在一个页面上频繁且大量的操作真实 DOM ,频繁的触发浏览器回流( Reflow )与重绘( Repaint ),会带来很大的性能开销,从而造成页面卡顿,在大型项目的性能上很是致命。

Vue 则是通过操作虚拟 DOM ,每一次数据更新都通过 Diff 算法找出需要更新的节点,只更新对应的虚拟 DOM ,再去映射到真实 DOM 上面渲染,以此避免频繁或大量的操作真实 DOM 。

虚拟 DOM 是一种编程概念,是指将原本应该是真实 DOM 元素的 UI 界面,用数据结构来组织起完整的 DOM 结构,再同步给真实 DOM 渲染,减少浏览器的回流与重绘。在 JavaScript 里,虚拟 DOM 的表现是一个 Object 对象,其中需要包含指定的属性(例如 Vue 的虚拟 DOM 需要用 type 来指定当前标签是一个 <div /> 还是 <span /> ),然后框架会根据对象的属性去转换为 DOM 结构并最终完成内容的显示。

# 文件结构

.vue 被称为单文件组件,也就是 SFC

.vue 文件.html 文件
<template /> 部分HTML 代码
<style /> 部分CSS 代码
<script /> 部分JavaScript 代码

# MPA 与 SPA

名词全称中文
MPAMulti-Page Application多页面应用
SPASingle-Page Application单页面应用

MPA 多页面应用是最传统的网站体验,当一个网站有多个页面时,会对应有多个实际存在的 HTML 文件,访问每一个页面都需要经历一次完整的页面请求过程:

从用户点击跳转开始: ---> 浏览器打开新的页面 ---> 请求【所有】资源 ---> 加载 HTML 、CSS 、 JS 、 图片等资源 ---> 完成新页面的渲染。

SPA 单页面应用是现代化的网站体验,与 MPA 相反,不论站点内有多少个页面,在 SPA 项目实际上只有一个 HTML 文件,也就是 index.html 首页文件。

它只有第一次访问的时候才需要经历一次完整的页面请求过程,之后的每个内部跳转或者数据更新操作,都是通过 AJAX 技术来获取需要呈现的内容并只更新指定的网页位置

可以把 SPA 的请求过程简化为如下步骤:

SPA 页面跳转过程,从用户点击跳转开始:---> 浏览器通过 pushState 等方法更新 URL---> 请求接口数据(如果有涉及到前后端交互)---> 通过 JavaScript 处理数据,拼接 HTML 片段 ---> 把 HTML 片段渲染到指定位置,完成页面的 “刷新”。

# CSR 与 SSR

名词全称中文
CSRClient-Side Rendering客户端渲染
SSRServer-Side Rendering服务端渲染

# 工程化

基于 Vue 3 的项目,最主流的工程化组合拳有以下两种:

常用方案Runtime构建工具前端框架
方案一NodeWebpackVue
方案二NodeViteVue

案一是比较传统并且过去项目使用最多的方案组合,但从 2021 年初随着 Vite 2.0 的发布,伴随着更快的开发体验和日渐丰富的社区生态,新项目很多都开始迁移到方案二。

# 路由

在传统的 Web 开发过程中,当需要实现多个站内页面时,以前需要写很多个 HTML 页面,然后通过 <a /> 标签来实现互相跳转。

在如今工程化模式下的前端开发,像 Vue 工程,可以轻松实现只用一个 HTML 文件,却能够完成多个站内页面渲染、跳转的功能,这就是路由。

@viewssrc/views 的路径别名, @cpsrc/components 的路径别名。路径别名可以在 vite.config.ts 等构建工具配置文件里添加 alias ,在搭建项目中我们曾经提到过。

文件结构:

src
# 路由目录
├─router
# 路由入口文件
├───index.ts
# 路由配置,如果路由很多,可以再拆分模块文件
├───routes.ts
# 项目入口文件
└─main.ts

index.ts 是路由的入口文件,如果路由很少,那么可以只维护在这个文件里,如果项目比较复杂,可以像上面这个结构一样拆分成两个文件: index.ts 和 routes.ts ,在 routes.ts 里维护路由树的结构,在 index.ts 导入路由树结构并激活路由,同时可以在该文件里配置路由钩子。

typescript 为例,路由文件的基础格式为三个部分:类型声明、数组结构、模块导出。

// src/router/routes.ts
// 使用 TypeScript 时需要导入路由项目的类型声明
import type { RouteRecordRaw } from 'vue-router'
// 使用路由项目类型声明一个路由数组
const routes: Array<RouteRecordRaw> = [
  // ...
]
// 将路由数组导出给其他模块使用
export default routes

RouteRecordRaw 是路由项目的 TS 类型。

routes 数组中,每一项的组成可以这么写:

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    alias: '/index',
    name: 'home',
    component: () => import('@views/home.vue'),
    children: [
        {
            path: 'login',
            name: 'auth-login',
            component: () => import('@/components/welcome/LoginPage.vue')
        }, {
            path: 'register',
            name: 'auth-register',
            component: () => import('@/components/welcome/RegisterPage.vue')
        }
    ]
  },
]
  • path 是路由的访问路径,如果域名是 https://example.com , 配置为 /home ,那么访问路径就是 https://example.com/home

  • alias 可以为路由指定一个别名,这样用户既可以通过 / 访问首页,也可以通过 /index 访问首页。

  • name 是路由的名称,非必填,但是一般都会配置上去,这样可以很方便的通过 name 来代替 path 实现路由的跳转,因为像有时候的开发环境和生产环境的路径不一致,或者说路径变更,通过 name 无需调整,但如果通过 path ,可能就要修改很多文件里面的链接跳转目标了。

  • component 是路由的模板文件,指向一个 vue 组件,用于指定路由在浏览器端的视图渲染,这里有两种方式来指定使用哪个组件

    • 同步组件:直接接收一个变量,变量的值就是对于的模板组件,在打包的时候,会把组件的所有代码都打包到一个文件里,对于大项目来说,这种方式的首屏加载是个灾难,要面对文件过大带来等待时间变长的问题。

    • 异步组件: component 接收一个函数,在 return 的时候返回模板组件,同时组件里的代码在打包的时候都会生成独立的文件,并在访问到对应路由的时候按需引入。上面的代码就是异步的路由懒加载。

  • 然后每一项其实还有一个参数为 children,每一个 childre 的值又是一个 routes ,从而形成路由树。

路由组件必须在父级组件里带有 <router-view/> 标签中,一级路由的父级组件就是 src/App.vue 这个根组件,直接这么写:

<template>
  <router-view />
</template>

如果站点带有全局公共组件,比如有全站统一的页头、页脚,只有中间区域才是路由,那么可以这样配置:

<template>
  <!-- 全局页头 -->
  <Header />

  <!-- 路由 -->
  <router-view />

  <!-- 全局页脚 -->
  <Footer />
</template>

如果有一部分路由带公共组件,一部分没有,比如大部分页面都需要有侧边栏,但登录页、注册页不需要,就可以这么处理:

<template>
  <!-- 登录 -->
  <Login v-if="route.name === 'login'" />

  <!-- 注册 -->
  <Register v-else-if="route.name === 'register'" />

  <!-- 带有侧边栏的其他路由 -->
  <div v-else>
    <!-- 固定在左侧的侧边栏 -->
    <Sidebar />

    <!-- 路由 -->
    <router-view />
  </div>
</template>

其他页面使用 router 来进行路由跳转:

import { useRouter } from 'vue-router'
const router = useRouter()
// 跳转
router.push({
    name: 'home'
})
// 或者:router.push ('/home'),直接使用 path 进行跳转
// 返回上一页
router.back()

如果路由的 ts 文件已经定义好了 router,也可以直接导入 router,就不需要使用 useRouter

除了使用 router 属性以外,还可以使用 <router-link> 标签:

<template>
  <router-link to="/home">首页</router-link>
</template>

该标签编译后会默认转换为 a 标签,如果想让其变为其他标签,在 vue2 中可以使用 tag 属性:

<template>
  <router-link tag="span" to="/home">首页</router-link>
</template>

在 vue3 中去除了 tag 属性,所以需要通过 customv-slot 的配合将其渲染为其他标签。

<template>
  <router-link to="/home" custom v-slot="{ navigate }">
    <span class="link" @click="navigate"> 首页 </span>
  </router-link>
</template>

解释:

  • custom ,一个布尔值,用于控制是否需要渲染为 a 标签,当不包含 custom 或者把 custom 设置为 false 时,则依然使用 a 标签渲染。
  • v-slot 是一个对象,用来决定标签的行为,它包含了:
字段含义
href解析后的 URL,将会作为一个 a 元素的 href 属性
route解析后的规范化的地址
navigate触发导航的函数,会在必要时自动阻止事件,和 router-link 同理
isActive如果需要应用激活的 class 则为 true ,允许应用一个任意的 class
isExactActive如果需要应用精确激活的 class 则为 true ,允许应用一个任意的 class

对于重定向,在路由数组中使用 redirect 属性即可:

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: () => import('@views/home.vue'),
    meta: {
      title: 'Home',
    },
  },
  
  {
    path: '/error',
    redirect: '/', // 访问这个路由会被重定向到首页
  },
]

我们也可以单独写一个 404 路由页面来替代站内的跳转:

const routes: Array<RouteRecordRaw> = [
  {
    // 只要访问到不存在的路由,就会显示为这个 404 模板
    path: '/:pathMatch(.*)*',
    name: '404',
    component: () => import('@views/404.vue'),
  },
]

这种重定向也可以带一些参数:

{
    path: '/',
    redirect: {
      name: 'home',
      query: {
        from: 'redirect',
      },
    },
  }
// 相当于访问地址为: https://example.com/home?from=redirect

redirect 的值也可以是一个函数:

redirect: () => {
    // `loginInfo` 是当前用户的登录信息
    // 可以从 `localStorage` 或者 `Pinia` 读取
    const { groupId } = loginInfo
    // 根据组别 ID 进行跳转
    switch (groupId) {
            // 管理员跳去仪表盘
        case 1:
            return '/dashboard'
            // 普通用户跳去首页
        case 2:
            return '/home'
            // 其他都认为未登录,跳去登录页
        default:
            return '/login'
    }
}

路由中也有一些全局钩子用于在跳转(前、后)时处理一些工作:

可用钩子含义触发时机
beforeEach全局前置守卫在路由跳转前触发
beforeResolve全局解析守卫在导航被确认前,同时在组件内守卫和异步路由组件被解析后
afterEach全局后置守卫在路由跳转完成后触发
// 创建路由
const router = createRouter({ ... })
// 在这里调用导航守卫的钩子函数
router.beforeEach((to, from) => {
  // ...
})

有一些参数:

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

在 vue2 中第三个参数是 next,用于操作路由接下来的跳转,但是新版本路由里,已经通过 RFC 将其删除,虽然目前还是作为可选参数使用,但以后不确定是否会移除,不建议继续使用。新版本路由可以通过 return 来代替 next

router.beforeEach((to, from) => {
  const { isNoLogin } = to.meta
  if (!isNoLogin) return '/login'
})

isNoLogin{} 是因为它是一个对象属性,需要使用解构语法来获取它的值

当然也有独享的路由钩子, beforeEnterbeforeResolve

# 组件通信

父子组件之间的通信方式包括:

  • props/emits
  • v-model/emits
  • ref/emits

第一种,子组件使用 props 入参来获取父组件传过来的值:

// Child.vue
export default defineComponent({
  props: {
    title: String,
    index: Number,
    userName: String,
    uid: Number,
  },
  // 在这里需要添加一个入参
  setup(props) {
    // 该入参包含了当前组件定义的所有 props
    console.log(props)
  },
})

如果父组件 Father.vue 传进来的数据在 Child.vue 里未定义,不仅不会拿到,并且在控制台会有警告信息。

v-model 更为简单,操作上简化了,但是副作用就是功能上没有 props 那么多。

# Node.js 入门

Node.js (简称 Node ) 是一个基于 Chrome V8 引擎构建的 JS 运行时( JavaScript Runtime )。它让 JavaScript 代码不再局限于网页上,还可以跑在客户端、服务端等场景,极大的推动了前端开发的发展,现代的前端开发几乎都离不开 Node 。

Runtime ,可以叫它 “运行时” 或者 “运行时环境” ,这个概念是指,项目的代码在哪里运行,哪里就是运行时。

传统的 JavaScript 只能跑在浏览器上,每个浏览器都为 JS 提供了一个运行时环境,可以简单的把浏览器当成一个 Runtime ,明白了这一点,相信就能明白什么是 Node 。

Node 就是一个让 JS 可以脱离浏览器运行的环境,当然,这里并不是说 Node 就是浏览器。

虽然 Node 也是基于 Chrome V8 引擎构建,但它并不是一个浏览器,它提供了一个完全不一样的运行时环境,没有 Window 、没有 Document 、没有 DOM 、没有 Web API ,没有 UI 界面…

但它提供了很多浏览器做不到的能力,比如和操作系统的交互,例如 “文件读写” 这样的操作在浏览器有诸多的限制,而在 Node 则轻轻松松。

对于前端开发者来说, Node 的巨大优势在于,使用一种语言就可以编写所有东西(前端和后端),不再花费很多精力去学习各种各样的开发语言。

哪怕仅仅只做 Web 开发,也不再需要顾虑新的语言特性在浏览器上的兼容性( e.g. ES6 、 ES7 、 ES8 、 ES9 …), Node 配合构建工具,以及诸如 Babel 这样的代码编译器,可以帮转换为浏览器兼容性最高的 ES5 。

查看你的 node.js 版本: node -v

# 镜像源

可以先在命令行输入以下命令查看当前的 npm 配置:

bash

npm config get registry
# https://registry.npmjs.org/

默认情况下,会输出 npm 官方的资源注册表地址,接下来在命令行上输入以下命令,进行镜像源的绑定:

npm config set registry https://registry.npmmirror.com

可以再次运行查询命令来查看是否设置成功:

npm config get registry
# https://registry.npmmirror.com/

可以看到已经成功更换为中国镜像站的地址了,之后在安装 npm 包的时候,速度会有很大的提升!

如果需要删除自己配置的镜像源,可以输入以下命令进行移除,移除后会恢复默认设置:

npm config rm registry

如果之前已经绑定过 npm.taobao 系列域名,也请记得更换成 npmmirror 这个新的域名!随着新的域名已经正式启用,老 npm.taobao.orgregistry.npm.taobao.org 域名在 2022 年 05 月 31 日零时后不再提供服务。

# 初始化项目

在项目所在目录打开命令行:

npm init

之后命令行会输出一些提示,比如:

package name: (demo) hello-node

package name 是问题的题干,会询问要输入什么内容,直接回车就是默认值,等所有都填完后,会输出以下信息:

{
  "name": "hello-node",
  "version": "1.0.0",
  "description": "A demo about Node.js",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "cyan",
  "license": "MIT"
}
Is this OK? (yes)

也可以直接加上 -y 参数,这样会以 Node 推荐的答案帮快速生成项目信息。而这些信息就成为 json 文件,保存在项目的 package.json 文件中

除了手动维护这些信息之外,在安装 npm 包等操作时, Node 也会帮写入数据到这个文件里,来了解一些常用字段的含义:

字段名含义
name项目名称,如果打算发布成 npm 包,它将作为包的名称
version项目版本号,如果打算发布成 npm 包,这个字段是必须的,遵循 语义化版本号 的要求
description项目的描述
keywords关键词,用于在 npm 网站上进行搜索
homepage项目的官网 URL
main项目的入口文件
scripts指定运行脚本的命令缩写,常见的如 npm run build 等命令就在这里配置,详见 脚本命令的配置
author作者信息
license许可证信息,可以选择适当的许可证进行开源
dependencies记录当前项目的生产依赖,安装 npm 包时会自动生成,详见:依赖包和插件
devDependencies记录当前项目的开发依赖,安装 npm 包时会自动生成,详见:依赖包和插件
type配置 Node 对 CJS 和 ESM 的支持

# 项目命名规则

如果打算发布成 npm 包,它将作为包的名称,可以是普通包名,也可以是范围包的包名。

类型释义例子
范围包具备 @scope/project-name 格式,一般有一系列相关的开发依赖之间会以相同的 scope 进行命名@vue/cli@vue/cli-service 就是一系列相关的范围包
普通包其他命名都属于普通包vuevue-router

包名有一定的书写规则:

  • 名称必须保持在 1 ~ 214 个字符之间(包括范围包的 @scope/ 部分)
  • 只允许使用小写字母、下划线、短横线、数字、小数点(并且只有范围包可以以点或下划线开头)
  • 包名最终成为 URL 、命令行参数或者文件夹名称的一部分,所以名称不能包含任何非 URL 安全字符

想要查询包名是否存在: npm view <package-name>

vue@3.3.4 | MIT | deps: 5 | versions: 445
The progressive JavaScript framework for building modern web UI.
https://github.com/vuejs/core/tree/main/packages/vue#readme
keywords: vue

如果该包名不存在,就会返回 404 信息

# 版本号格式与升级规则

超级重要的常识

版本号的格式为: Major.Minor.Patch (简称 X.Y.Z ),它们的含义和升级规则如下:

英文中文含义
Major主版本号当项目作了大量的变更,与旧版本存在一定的不兼容问题
Minor次版本号做了向下兼容的功能改动或者少量功能更新
Patch修订号修复上一个版本的少量 BUG

一般情况下,三者均为正整数,并且从 0 开始,遵循这三条注意事项:

  • 当主版本号升级时,次版本号和修订号归零
  • 当次版本号升级时,修订号归零,主版本号保持不变
  • 当修订号升级时,主版本号和次版本号保持不变

下面以一些常见的例子帮助快速理解版本号的升级规则:

  • 如果不打算发布,可以默认为 0.0.0 ,代表它不是一个进入发布状态的包
  • 在正式发布之前,可以将其设置为 0.1.0 发布第一个测试版本,自此,代表已进入发布状态,但还处于初期开发阶段,这个阶段可能经常改变 API ,但不需要频繁的更新主版本号
  • 0.1.0 发布后,修复了 BUG ,下一个版本号将设置为 0.1.1 ,即更新了一个修订号
  • 0.1.1 发布后,有新的功能发布,下一个版本号可以升级为 0.2.0 ,即更新了一个次版本号
  • 当觉得这个项目已经功能稳定、没有什么 BUG 了,决定正式发布并给用户使用时,那么就可以进入了 1.0.0 正式版了

npm run devnpm run build 是什么?其实就是运行 package.json 文件中的 scripts 里的内容:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "node index"
  }

假设内容如上,执行 npm run dev 就会执行 node index 命令,执行 npm run test 就会执行 echo 。这里我们在项目下创建一个 index.js ,并写入:

console.log("====")

然后执行 npm run dev ,就会在控制台打印 "====",所以 npm run 后面接的参数是什么不重要,它只是 scripts 里对应 keyvalue 的映射。

# Module

软工里有个原则:单一职责,一个完整业务应该是各种高内聚的模块组合到一起,每个单一职责的代码块就叫做模块(Module)。

  • ESM ( ES Module ) 是 JavaScript 在 ES6( ECMAScript 2015 )版本推出的模块化标准,旨在成为浏览器和服务端通用的模块解决方案。
  • CJS ( CommonJS ) 原本是服务端的模块化标准(设计之初也叫 ServerJS ),是为 JavaScript 设计的用于浏览器之外的一个模块化方案, Node 默认支持了该规范,在 Node 12 之前也只支持 CJS ,但从 Node 12 开始,已经同时支持 ES Module 的使用。(老项目还是可能会遇到)

一个独立的文件就是一个模块,该文件内部的变量必须通过导出才能被外部访问到,而外部文件想访问这些变量,需要导入对应的模块才能生效。

# CJS 设计模块

先调整一下目录结构:

  1. 删掉 index.js 文件
  2. 创建一个 src 文件夹,在里面再创建一个 cjs 文件夹
  3. cjs 文件夹里面创建两个文件: index.cjsmodule.cjs

使用了 .cjs 文件扩展名,其实它也是 JS 文件,但这个扩展名是 Node 专门为 CommonJS 规范设计的

再改一下 scripts 里的内容:

"scripts": {
    "dev:cjs": "node src/cjs/index.cjs"
  }

cjs 使用 module.exports 导出,使用 require 导入:

  • module.cjs 写入代码:
// src/cjs/module.cjs
function foo() {
  console.log(bar)
}
const bar = 'Hello World from bar.'
// 导出
module.exports = {
  foo,
  bar
}
  • index.js 写入代码:
// src/cjs/index.cjs
const m = require('./module.cjs')
console.log(m)

运行之后会发现 m 其实是一个对象,也就是 module.cjs 模块抽象出来的对象,需要通过 m.foo()m.bar 的形式才可以拿到值。

const m = require('./module.cjs')
m.foo()
m.bar = 'after change bar'
m.foo()
// 结果都是 This is bar.

也可以这样:

// src/cjs/index.cjs
const { foo: foo2, bar } = require('./module.cjs')
foo2()
console.log(bar)

这里注意 foo: foo2 ,可以直接写成 foo ,前者是为了重命名,避免模块多了命名冲突

# ESM 设计模块

不过因为历史原因,如果要直接在浏览器里使用该方案,在不同的浏览器里会有一定的兼容问题,因此一般情况下都需要借助构建工具进行开发,工具通常会提供开箱即用的本地服务器用于开发调试,并且最终打包的时候还可以抹平不同浏览器之间的差异。

esm 的默认导出为: export default ;命名导出为 export 。默认导出的意思是,一个模块只包含一个值;而导入默认值则意味着,导入时声明的变量名就是对应模块的值。

使用 import ... from ... 导入模块,在导入的时候,如果文件扩展名是 .js 则可以省略文件名后缀,否则需要把扩展名也完整写出来。

# 默认导出和导入

module.mjs 写入:

// src/esm/module.mjs
export default 'Hello World'
// 如果是 cjs,其实就是 module.exports = 'Hello World'

然后在 index.mjs 中写入:

// src/esm/index.mjs
import m from './module.mjs'
console.log(m)

# 命名导出和导入

命名导出: export const bar = 'xx' ,不论是变量还是函数,直接加上 export 即可。

命名导入:

// src/esm/index.mjs
import {
  foo as foo2,  // 这里进行了重命名
  bar
} from './module.mjs'

# 依赖包和插件

项目的依赖建议优先选择本地安装(项目中的 node_module 目录),这是因为本地安装可以把依赖列表记录到 package.json 里,多人协作的时候可以减少很多问题出现,特别是当本地依赖与全局依赖版本号不一致的时候。

npm install --save <package-name>@<version | tag>

需要提前在命令行 cd 到的项目目录下再执行安装。另外, --save 或者 -S 选项在实际使用的时候可以省略,因为它是默认选项。

可以在项目的 package.json 文件里的 dependencies 字段查看是否已安装成功,例如:

// package.json
// 生产依赖包会被安装到项目根目录下的 node_modules 目录里。
{
  // 生产依赖包: --save
  "dependencies": {
    // 以 "包名":"版本号" 的格式写入
    "vue-router": "^4.0.14"
  },
  // 开发依赖包: --save-dev/-D
  "devDependencies": {
    // 以 "包名":"版本号" 的格式写入
    "eslint": "^8.6.0"
  }
}

开发依赖包和生产依赖包不同的点在于,只在开发环境生效,构建部署到生产环境时可能会被抛弃,一些只在开发环境下使用的包,就可以安装到开发依赖里,比如检查代码是否正确的 ESLint 就可以用这个方式安装。

至于全局安装,也就类似于 @vue/clicreate-preset 之类的脚手架会提供全局安装的服务,安装后,就可以使用 vue create xxx 等命令直接创建 Vue 项目了。

# 一些命令

  • 版本升级: npm update

  • 本地卸载: npm uninstall <package-name>

  • 全局卸载: npm uninstall --global <package-name>

# Babel

Babel 是一个 JavaScript 编译器,它可以让开发者仅需维护一份简单的 JSON 配置文件,即可调动一系列工具链将源代码编译为目标浏览器指定版本所支持的语法

详细参考:Babel 的使用和配置

# TypeScript

既是一门新语言,有是 JS 的一个超集,它是在 JavaScript 的基础上增加了一套类型系统,它支持所有的 JS 语句,为工程化开发而生,最终在编译的时候去掉类型和特有的语法,生成 JS 代码。

强类型语言,我焯,爽。

# 新建项目

src 文件夹下创建一个 ts 文件夹,并在 ts 下创建 index.ts

// src/ts/index.ts
function getFirstWord(msg: string) {
  console.log(msg.split(' ')[0])
}
getFirstWord('Hello World')

然后安装两个依赖包:

npm install -D typescript ts-node
  • typescript 这个包是用 TypeScript 编程的语言依赖包
  • ts-node 是让 Node 可以运行 TypeScript 的执行环境

这次添加了一个 -D 参数,因为 TypeScript 和 TS-Node 是开发过程中使用的依赖,所以将其添加到 package.json 的 devDependencies 字段里。

修改 scripts 字段,增加一个 dev:ts 的 script :

"scripts": {
    "dev:cjs": "node src/cjs/index.cjs",
    "dev:esm": "node src/esm/index.mjs",
    "dev:ts": "ts-node src/ts/index.ts",
    "compile": "babel src/babel --out-dir compiled",
    "serve": "node server/index.js"
  }

# 原始数据类型

原始数据类型JavaScriptTypeScript
字符串Stringstring
数值Numbernumber
布尔值Booleanboolean
大整数BigIntbigint
符号Symbolsymbol
不存在Nullnull
未定义Undefinedundefined

在 ts 定义变量的类型,不是放在变量名前面,而是后面:

// 字符串
const str: string = 'Hello World'
// 数值
const num: number = 1
// 布尔值
const bool: boolean = true

至于数组的定义:

数组里的数据类型写法 1类型写法 2
字符串string[]Array<string>
数值number[]Array<number>
布尔值boolean[]Array<boolean>
大整数bigint[]Array<bigint>
符号symbol[]Array<symbol>
不存在null[]Array<null>
未定义undefined[]Array<undefined>

一开始如果没有知名数据类型,或者编译器无法通过元素推断出数组类型,比如:

// 这个时候会认为是 any [] 或者 never [] 类型
const nums = []
// 这个时候再 push 一个 number 数据进去,也不会使其成为 number []
nums.push(1)

# 对象(接口)

感觉就是类型的定义

对象的类型定义有两个语法支持: typeinterface

// 定义用户对象的类型
interface UserItem {
  name: string
  // 这个属性变成了可选
  age?: number
}
// 在声明变量的时候将其关联到类型上
const petter: UserItem = {
  name: 'Petter',
  age: 20
}

在实际的业务中,有可能会出现一些属性并不是必须的,就像这个年龄,可以将其设置为可选属性,通过添加 ? 来定义。

# 继承

直接用 extends 即可:

// 这里继承了 UserItem 的所有属性类型,并追加了一个权限等级属性
interface Admin extends UserItem {
  permissionLevel: number
}

如果父类有些类型用不上,可以借助 Omit 在继承的时候选择删除:

type Omit<T, K extends string | number | symbol>

T 代表已有的一个对象类型, K 代表要删除的属性名,如果只有一个属性就直接是一个字符串,如果有多个属性,用 | 来分隔开,下面的例子就是删除了两个不需要的属性:

interface UserItem {
  name: string
  age: number
  enjoyFoods: string[]
  friendList?: UserItem[]
}
// 这里在继承 UserItem 类型的时候,删除了两个多余的属性
interface Admin extends Omit<UserItem, 'enjoyFoods' | 'friendList'> {
  permissionLevel: number
}
// 现在的 admin 就非常精简了
const admin: Admin = {
  name: 'Petter',
  age: 18,
  permissionLevel: 1,
}

#

使用 class 关键字:

// 定义一个类
class User {
  //constructor 上的数据需要先这样定好类型
  name: string
  // 入参也要定义类型
  constructor(userName: string) {
    this.name = userName
  }
  getName() {
    console.log(this.name)
  }
}
// 通过 new 这个类得到的变量,它的类型就是这个类
const petter: User = new User('Petter')
petter.getName() // Petter

# 联合类型

其实就是通过 | 来联合多种类型:

function counter(count: number | string) {
  console.log(`The current count is: ${count}.`)
}
// 不论传数值还是字符串,都可以达到的目的
counter(1)  // The current count is: 1.
counter('2')  // The current count is: 2.

# 异步函数返回值

对于异步函数,需要用 Promise<T> 类型来定义它的返回值,这里的 T 是泛型,取决于的函数最终返回一个什么样的值( async / await 也适用这个类型)。

// 注意这里的返回值类型
function queryData(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Hello World')
    }, 3000)
  })
}
queryData().then((data) => console.log(data))

# @Type 包

有些包并不支持 ts,在 ts 文件中使用会报错:

// src/ts/index.ts
import md5 from 'md5'
console.log(md5('Hello World'))

这是因为缺少 md5 这个包的类型定义,根据命令行的提示,安装 @types/md5 这个包。这是因为这些包是很早期用 JavaScript 编写的,因为功能够用作者也没有进行维护更新,所以缺少相应的 TS 类型,因此开源社区推出了一套 @types 类型包,专门处理这样的情况。

@types 类型包的命名格式为 @types/<package-name> ,也就是在原有的包名前面拼接 @types ,日常开发要用到的知名 npm 包都会有相应的类型包,只需要将其安装到 package.json 的 devDependencies 里即可解决该问题。

以 md5 为例:

npm install -D @types/md5

# 编译

但最终可能需要的是一个 JS 文件,比如要通过 <script src> 来放到 HTML 页面里,这就涉及到对 TypeScript 的编译。

来看看如何把一个 TS 文件编译成 JS 文件,让其从 TypeScript 变成 JavaScript 代码。

在 package.json 里增加一个 build script,然后执行 npm run build

"scripts": {
    "dev:cjs": "node src/cjs/index.cjs",
    "dev:esm": "node src/esm/index.mjs",
    "dev:ts": "ts-node src/ts/index.ts",
    "build": "tsc src/ts/index.ts --outDir dist",
    "compile": "babel src/babel --out-dir compiled",
    "serve": "node server/index.js"
  },

# 构建工具

为什么以前的前端页面直接编写代码就可以在浏览器访问,现在却还要进行构建编译,是否 “多此一举” ?

构建工具主要的功能有:

  • 项目中代码可以复用,抽离成模块、组件,交给构建工具合并打包
  • npm 包开箱即用,剩下的工作交给构建工具去按需抽离与合并
  • CSS 写起来很慢,可以使用 Sass、Less 等 CSS 预处理器,利用它们的变量支持、混合继承等功能提高开发效率,最终交给构建工具去按需抽离与合并。

最常见的就是 webpack 和 vite 两种构建器,前者是在项目启动时就打包所有的模块,再启动开发服务器,所以随着项目的模块变多,项目启动也会越慢;Vite 则是按需打包,将打包的部分工作交给了浏览器(支持 ES Module 的浏览器),不需要预先打包,而是直接启动开发服务器,请求(http 请求)到对应的模块的时候在进行编译。所以项目启动更快。

一般来说,像这种 BS 项目,上线时启动时间并不会影响生产,只是开发者很难受,Vite 是增加了开发者的幸福感。但由于 Vite 是面向现代浏览器,所以如果的项目有兼容低版本浏览器的需求的话,建议还是用 Webpack 来打包,否则,Vite 是目前的更优解。