Coredns 源码解析:启动流程 - 卡瓦邦噶

以下原文:https://www.kawabangga.com/posts/4728

Coredns 的启动流程不是很好阅读,因为它本质上是一个 caddy server, 是基于 caddy 开发的。也就是说,它是将自己的逻辑注入到 caddy 里面去,相当于把 caddy 当做一个框架来用,实际的启动流程其实是 caddy 的启动流程,Coredns 里面不会看到很明确的准备启动,Start server 之类的代码,而大部分都是注册逻辑到 caddy 代码。类似于 Openresty 和 Nginx 的关系。这就导致一开始阅读源码不是很好上手,需要搞清楚哪些东西是 Coredns 的,哪些接口是 caddy 的。而 caddy 的文档又不是很多,而且 Coredns 所使用的 caddy 已经不是官方的了,而是自己维护的一个版本,也加大了阅读的难度。所以我将启动流程梳理了这篇博客,希望能理清它的逻辑。

在开始分析源代码之前,读者需要具备的准备工作是:

  1. Golang 的基本语法,但是我发现 Golang 很简单,看完 https://gobyexample.com/ 足矣;
  2. 知道 DNS 的基本工作原理,知道 DNS Resolver, DNS Root Server, DNS TLS Server 的区别,能区分 Cloudflare 1.1.1.1 服务和 AWS 的 Route53 分别是什么角色,知道 Coredns 是哪一种 Server(其实 Coredns 哪一个都能做);
  3. 需要先看完 Coredns 的使用文档,知道它怎么配置;

在完成了这些之后,就基本可以知道 Coredns 是怎么工作的了。在开始进入源码之前,可以通过已知的文档来思考一下:如果 Coredns 需要完成已知的这些功能,需要做哪些事情呢?

猜想的实现模块

因为在之前的博客中已经介绍过,Coredns 其实是基于一个 caddy 的 server,所以我们可以猜想 Coredns 必须要完成以下事情:

  1. 需要能够解析 Corefile,这是 Coredns 的配置文件;
  2. 需要维护一个 Plugin Chain,因为 Coredns 工作方式的本质是一个一个的 Plugin 调用;
  3. Plugin 需要初始化;
  4. Coredns 是基于 caddy 的,那么 Coredns 必须有地方告诉 caddy 自己处理请求的逻辑,监听的端口等等;

你可能还想到了其他的功能,这样,在接下来的源码阅读中,就可以尝试在源代码中找到这些逻辑,看看 Coredns 是怎么实现的。

Coredns 的入口程序是 coredns.go,里面只有两部分,第一部分是 import,第二部分是 main

import (
	_ "github.com/coredns/coredns/core/plugin" // Plug in CoreDNS.
	"github.com/coredns/coredns/coremain"
)

func main() {
	coremain.Run()
}

Golang 的 import 并不是简单地引入了 package 的名字,如果 package 里面有 init() 的话,会执行这个 init()。所以这里的 import 其实做了很多事情。

注册

import package 的时候,imported package 还会 import 其他的 package,最终,这些 package 的 init() 都会被执行。

import 的链路和最终执行过的 init 如下,其中一些不重要的,比如 caddy.init() 在图中忽略了。

img

重要的注册主要有两部分,第一部分是将所有的 Plugins 都 import 了一遍,并且执行了这些 Plugin 里面的 init()。这个 Plugin 的 import 列表其实是生成的,在之前的博客中提到过。

因为 Golang 是静态编译的语言,所以要修改支持的 Plugin 列表,必须要重新编译 Coredns:

  1. 修改 plugin.cfg 文件
  2. 使用仓库中的 directives_generate.go 生成 zplugin.go 源代码中文,这时 import 列表更新
  3. 重新编译 Coredns

Plugin 中的 init() 做的事情很简单,就是调用 coredns.plugin.Register 函数,将 Plugin 注册到 caddy 中去,告诉 caddy 两个事情:

  1. 这个 Plugin 所支持的 ServerType 是 DNS
  2. Plugin 的 Action,即初始化函数。这里只是注册,并没有运行过。

第二部分是 register 中的 init 函数,主要的动作是使用 caddy 的接口 caddy.RegisterServerType 注册上了一个新的 Server 类型。

注册的时候,要按照 caddy 的接口告诉 caddy:

  1. Directives: 新的 ServerType 支持的 Directives 是什么;
  2. DefaultInput: 在没有配置文件输入的时候,默认的配置文件是什么,这个选项其实不重要;
  3. NewContext: 这个是最重要的,如何生成对应这个 ServerType 的 Context,Context 是后面管理 Config 实例的主要入口;

coremain 里面也有一个 init() 主要是处理了 Coredns 启动时候的命令行参数,然后注册了 caddyFileLoader,即读取(注意还没有解析)配置文件的函数。

到这里,import 阶段就结束了,目前为止所做的工作大部分都是将函数注册到 caddy,告诉 caddy 应该做什么,函数并没有运行。

然后回到 coredns.go 文件的第二部分:coremain.Run()

启动

数据结构

启动的大致流程是,初始化好各种 Instance, Context, 和 Config, 然后启动 Server. 不同的阶段所初始化的数据结构不同,要理解这个过程,最好先明白这些数据结构之间的关系。主要的数据结构以及它们之间的互相应用如下图。黄色的表示 caddy 中的数据结构,绿色的表示 Coredns 中的数据结构。

img

其中,caddy.Controllercaddy.Instance 是 caddy 中定义的结构,主要是 caddy server 在使用,Coredns 中并没有看到很多用到的地方。

dnsContext 是实现了 caddy.context,内部保存了和 Config 之间的关系,实现了 caddy 中定义的 InspectServerBlocks 和 MakeServers 接口,是一个主要的数据结构。对应 caddy.Instanse 全局只有一个,由 caddy 创建。

Config 就完全是 Coredns 内部的结构了,是最重要的一个结构,里面保存了 Plugin 列表,在处理 DNS 请求的时候,主要通过 Config 去调用 Plugin. 对于每一个 Corefile 配置文件中的 ServerBlock 和 Zone 都会有一个 Config 实例。

启动流程

Coredns 的启动流程之所以复杂,一个原因是真正的流程在 caddy 中而不是在 Coredns 中,另一个原因是随之而来的各种逻辑,本质上是 Coredns 定义的,然后注册到 caddy 中,caddy 执行的代码实际上是 Coredns 写的。

所以为了说明白这个启动的流程,我先画了一个图。启动流程的本职是初始化好上文中描述的各种数据结构。下图中,上面是数据结构,下面是代码的执行流程。在下图中,实线表示实际调用关系,虚线表示这段代码初始化了数据机构实例。

img

从 coremain.Run 开始,这里逻辑很简单,先是执行了上文提到过的注册的 caddyFileLoader 。然后调用了 caddy.Start,由 caddy 负责主要的启动流程。

下面我们找深度优先描述这个启动过程。

Caddy 先创建了一个 Instnace ,然后调用 ValidateAndExecuteDirectives。

ValidateAndExecuteDirectives 中,根据我们之前 load 出来的 caddy file 中的 ServerType string,拿到 DNS Server Type,就是上文提到的我们在 init 的过程中注册进去的。

然后执行 loadServerBlocks,这是 caddy 内置的函数,根据我们之前注册的 ServerType.Directives 返回的 Directives 去解析 caddy file,这时候是作为一个普通的 caddy 文件解析的,没有 coredns 的解析逻辑。这时候原来的 caddy file 被解析成 Tokens。由此也可以看出,Coredns 的配置文件和 Corefile 和 caddy 的格式必须是一样的,遵循一样的语法规范,因为解析器都是用的 caddy 中的(如果 Coredns fork 的版本没有修改的话)。

下一步是调用 ServerType 注册的第二个重要方法:NewContext 创建出来一个 context,实质的类型是 dnsContext

然后调用 context.InspectServerBlocks,这个逻辑也是 Coredns 中实现的,是 dnsContext 实现的接口。主要做了两件事,一是检查 Corefile 是否合法,有无重复定义等。然后是创建 dnsserver.Config,Config 主要是和 dnsContext 关联,我们后面拿到这个 Config 主要也是通过 Context。比如在 config.go 中通过 Controller 拿到对应 Config 的方法实现

// GetConfig gets the Config that corresponds to c.
// If none exist nil is returned.
func GetConfig(c *caddy.Controller) *Config {
	ctx := c.Context().(*dnsContext)
	key := keyForConfig(c.ServerBlockIndex, c.ServerBlockKeyIndex)
	if cfg, ok := ctx.keysToConfigs[key]; ok {
		return cfg
	}
	// we should only get here during tests because directive
	// actions typically skip the server blocks where we make
	// the configs.
	ctx.saveConfig(key, &Config{ListenHosts: []string{""}})
	return GetConfig(c)
}

实际上是通过 Controller 找到 Instance, 然后找到 Instance 上的 Context (c.Context() 逻辑)。然后通过一个 Utils 去从 Context 上找到对应的 Config。这个实现和上图也是完全符合的。

上文提到过,每一个 Server Block 中的每一个 Key 都会有一个对应的 Config,那么这么多的 Config,我们怎么找到对应的呢?

其实就是 keyForConfig 的逻辑,context 中记录了 Server Block Index + Server Block Key Index 组合,和 Config 的一一对应关系。

回到 ValidateAndExecuteDirectives 的逻辑,调用完 InspectServerBlocks 之后,就是针对每一个 Directive 去拿到 Action 然后去执行。即,初始化每一个 Plugin。

完成之后,逻辑回到 caddy.Start中,会调用 dnsContext 中实现的第二个重要的接口:MakeServers. 初始化好 Coredns 中的 Server.

最后一步,就是 startServers 了,这里实际的逻辑又回到了 Coredns 实现的接口上。感觉比较清晰,没有什么难以理解的地方。主要是实现了两个接口,一个是 Listen, 一个是 Serve

然后就可以开始处理请求了。

开始处理请求

最后 Server 实现了 caddy.Server 的 Serve 方法, 里面做的事情主要就是根据 DNS 查询请求里面的 zone 匹配到对应的 Config,然后 PluginChain 保存在 Config 里面,通过调用 h.pluginChain.ServeDNS 来完成请求的处理

本文的代码解析就到这里,至此启动流程就完成了。本文尽量少贴代码,试图缕清启动的流程,具体的代码如果对照本文的图片和解释应该都找到的,并且剩下的部分应该不难看懂。如果发现错误或者有疑问,欢迎在评论区交流。

本文基于 Coredns 源码 Commit:3288b111b24fbe01094d5e380bbd2214427e00a4

对应最近的 tag 是:v1.8.6-35-gf35ab931

相关文章


Win7 设置登录/锁屏桌面背景

天晓得,我竟然还得用 win7。这篇文章记录下怎么改登陆壁纸/锁屏壁纸。

  1. 修改注册表

    Win+R快捷键,运行regedit

    打开注册表编辑器,定位到

    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\Background

    右边窗口会看到名为:OEMBackground的双字节值。

    • 如果没有,可以在右窗口空白处鼠标右击,选择”新建”–“DWORD (32-位)值”,命名为:OEMBackground。
    • 双击这个键值,默认应该为0,改为1,单击确定按钮,关闭注册表编辑器。
  2. 进入 C:\Windows\System32\oobe ,下面有一张 background.bmp的图片,是系统默认的背景图片

  3. 创建/进入 info/backgrounds 文件夹,把自定义的背景图片挪进来,起名 backgroundDefault.jpg 即可

所以自定义壁纸图片的存放路径是:C:\Windows\System32\oobe\info\backgrounds\backgroundDefault.jpg

通过快捷键Win+L,可以看锁屏效果了。


Nuxtjs3 备忘

作为一个本职工作是基础设施开发的后端人员,我将js比作linux系统,将vue比作docker/containerd,而nuxt就相当于kubernetes了。当然这个类比并不是非常准确。前端比容器大战要柔和的多,感觉大家都在底层上重复建设。对比容器技术,swarm/k8s/mesos也就5年左右就分出了胜负。前端目前主流还是react/vue/Angular这三家,基于这三家之上又有各自的服务端框架(更底层typescript倒是已经统一了),导致前端学习门槛较高,框架概念、设计模式其实也走了一遍后端框架的老路,很多概念后端开发可以平移过来。我比较相信Taylor Otwell(Laravel的创造者),他说vue好,所以我就跟着他一起用了。

前两篇记录了点vue2vue3的笔记,这一篇记录下nuxtjs3,这是官网,2022 年 4 月 20 日正式发布 nuxt3,代号为“Mount Hope”

Nuxt3是基于 Vue3 发布的 SSR 框架:

  • SPA应用:也就是单页应用,这些多是在客户端的应用,不能进行SEO优化(搜索引擎优化)。
  • SSR应用:在服务端进行渲染,渲染完成后返回给客户端,每个页面有独立的URL,对SEO友好,对应企业网站、商品展示 、博客这类型的展示型网站。

这是官网显示的一些优势说明:

  • 更轻量:以现代浏览器为基础的情况下,服务器部署和客户端产物最多减小75倍。
  • 更快:用动态服务端代码来优化冷启动。
  • Hybird:增量动态生成和其他高级模式现在都成为可能。
  • Suspense: 导航前后可在任何组件中获取数据。
  • Composition API : 使用Composition API 和 Nuxt3的composables 实现真正的可复用性。
  • Nuxt CLI : 权限的零依赖体验,助你轻松搭建项目和集成模块。
  • Nuxt Devtools :专属调试工具,更多的信息和快速修复,在浏览器中高效工作。
  • Nuxt Kit :全新的基于 TypeScript 和跨版本兼容的模块开发。
  • Webpack5 : 更快的构建速度和更小的构建包,并且零配置。
  • Vite:用Vite作为你的打包器,体验轻量级的快速HMR。
  • Vue3 : 完全支持Vue3语法,这一点特别关键。
  • TypeScript:由原生TypeScript和ESM构成,没有额外配置步骤。

安装

npx是npm从5.2版开始增加的命令

官方链接

npx nuxi init nuxt3-test  # 快速创建

image-20220517113727498

npm install
npm run dev

image-20220517113912229

生成文件如下:

image-20220517114211393

这里要重点提的是 nuxt.config.js 文件,所有配置都在里面。

一些常用的目录如下:

- pages               // 页面目录,Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置。
- components          // 组件目录
- assets              // 静态资源目录
- public              // 不需要 vue 处理的静态资源,此类文件不会被 Nuxt.js 调用 Webpack 进行构建编译处理。 服务器启动的时候,该目录下的文件会映射至应用的根路径 / 下。
- layouts             // 项目布局目录
- common              // 存储js文件
- plugins             // nuxt扩展的js文件
- store               // vuex的配置文件,Nuxt.js 框架集成了 Vuex 状态树 的相关功能配置,在 store 目录下创建一个 index.js 文件可激活这些配置。

配置参考链接:

删除根目录下的app.vue,然后pages目录下新建index.vue文件,就会自动关联vue-router,支持动态路由。

因为我是容器化环境开发的,所以改了一下运行端口到80端口,访问方便一些。如果发现配置不生效,可以尝试重启一卡容器(多个容器在同一个目录下启动npm run时感觉会冲突,就遇到有些配置不生效了)。

image-20220519113053640

一些官网备忘

setup函数中,可以useHead使用与元标记对应的键的元属性对象进行调用:title, titleTemplate, base, script, style, metaand link, 以及htmlAttrsand bodyAttrs。还有两个速记属性charsetviewport

data-fetching

Nuxt 提供useFetchuseLazyFetchuseAsyncDatauseLazyAsyncData处理应用程序中的数据获取。

state-management

useState是一个对 SSR 友好的ref 替代品。它的值将在服务器端渲染后(在客户端水化期间)被保留,并使用唯一键在所有组件之间共享。

server-routes

Nuxt 会自动扫描 、 和 目录中的文件~/server/api~/server/routes~/server/middleware注册具有 HMR 支持的 API 和服务器处理程序。

每个文件都应该导出一个用defineEventHandler().

路由

Nuxt使用了自动加载路由的方案,他会根据你项目的目录来自动创建router对象;

一个简单的示例,pages下的目录示例及其对应的路由如下所示:

test:
  a.vue     -- 对应路径:/test/a
  index.vue -- 对应路由:/test
index:
  _id:
    a.vue      -- 对应路由:/{id}/a
    index.vue  -- 对应路由:/{id}
  index.vue    -- 对应路由:/,会被包含到上一级的index.vue的<nuxt-child>中
index.vue      -- 对应路由:/,其中如果包含有nuxt-child元素,那么会将index目录下的index.vue加载到nuxt-child元素位置  
  • 在 Nuxt.js 里面定义带参数的动态路由,需要创建对应的以下划线作为前缀的 Vue 文件 或 目录。
  • 如果你想将_id设置为必选的路由,需要在 users/_id 目录内创建一个 index.vue 文件。

路由参数校验

Nuxt.js 可以让你在动态路由组件中定义参数校验方法。

举个例子: pages/users/_id.vue

export default {
  validate ({ params }) {
    // 必须是number类型
    return /^\d+$/.test(params.id)
  }
}

如果校验方法返回的值不为 true或Promise中resolve 解析为false或抛出Error , Nuxt.js 将自动加载显示 404 错误页面或 500 错误页面。

未知嵌套深度的动态嵌套路由

如果您不知道URL结构的深度,您可以使用_.vue动态匹配嵌套路径。这将处理与更具体请求不匹配的情况。

文件结构:

pages/
--| people/
-----| _id.vue
-----| index.vue
--| _.vue
--| index.vue

知识点

一些代码只是为了做代码的结构备忘,不代表逻辑正确。

前端的语法糖太多了,我觉得这些语法糖是前端晦涩的一大原因。

不过,“无他,但手熟尔”,在前端也是一样。

  1. 命令行nuxi

    npx nuxi init nuxt3-test  # 快速创建
    
  2. layout 布局 html(静态)

    layout文件夹
    page文件夹
    
  3. 引入 css

    <style scope>
    .abc {}
    </style>
    
  4. 应用状态state

    <script setup>
      const options = reactive({
          xxx: "xxx"
      });
    </script>
       
    <template>
      <button class="abc" :class="options.xxx === 'xxx' && 'option-active'">abc</button>
    </template>
    
  5. a标签替换为 NuxtLink 标签

    <NuxtLink to="/">Home</NuxtLink>
    
  6. 简单的typescript

    我觉得typescript的语法有点像golang。 看着也挺舒服的~

    先定义个interface类型,一些选项/常量用enum定义,再const一个实例。

    <script setup lang="ts">
      interface OptionState {
        xxx: string,
        yyy: string
      }
         
      enum Xxx {
        AAA = "aaa",
        BBB = "bbb"
      }
       
      const obj := OptionState{
          xxx: Xxx.AAA,
          yyy: Xxx.BBB
      }
         
      const options = reactive<OptionState>({
          xxx: Xxx.AAA,
          yyy: Xxx.BBB
      });
    </script>
    
  7. import 其他文件里定义的内容

    import {OptionState} from "@/data"  // 其他文件要 export特定的定义
    

    教程有的为了简化内容,直接准备一个json文件,然后import数据进来:

    <script setup lang="ts">
    import {xxx} from "@/data.json"
         
    const rest = {
      first: [...xxx].splice(0,25),
      second: [...xxx].spice(25,25),
    };
    </script>
         
    <template>
      <Row v-for="r in rest.first" :key="r.id" />
      <Row v-for="r in rest.second" :key="r.id" />
    </template>
         
    
  8. 更新state

    <script setup lang="ts">
      const computeSelectedNames = () => {
        const filterNames = names
        	.filter((name) => name.xxx === options.xxx )
        	.filter((name) => name.yyy === options.yyy )
        	.filter((name) => {
            return ...
          })
             
          selectdNames.value = filterdNames.map((name) => name.xxx )
           
      };
      const selectdNames = ref<string[]>{[]}
    </script>
       
    <template>
      <button class="abc" @click="computeSelectName">Find</button>
      {{ selectedNames }}
    </template>
    
  9. 注意拆分component,以及通过props在父子组件中传值

    <script setup lang="ts">
      interface OptionProps{
        option: {
            id: string,
          	title: string,
            catagory: string,
            index: number
        };
      }
         
      const props = defindProps<OptionProps>()
    </script>
    

    在父组件中传入:

       
    
   
   
   
   大写开头的标签就是component,有一些是 `nuxt` 保留的,用来作为头部header内容的,类似于 `metadata`

   

10. 通过事件触发父子组件传值。

    ```vue
    // 父:
    <script setup lang="ts">
      const removeName = (index: number) => {
        const filteredNames = [...selectdNames.value]
        filterNames.splice(index,1)
        selectedName.value = filterNames 
      }
    </script>
    
    <template>
      <CardName v-for="(name, index) in selectedNames" :key="name" :name="name" @remove="()=> removeName(index)" :index="index" />
    </template>
    
    // 子CardName:
    <script setup lang="ts">
      interface NameProps{
        name: string;
        index: number
      }
      
      const props = defindProps<NameProps>();
      const emit = defindEmits(["remove"])
      
      const removeName = () => {
        emit("remove", props.index)
      }
    </script>
    
    <template>
      <p @click="removeName">xxx </p>
    </template>

  1. 常用的循环语句

    v-for
    
  2. Pages, 基于文件的路由route

    <script setup lang="ts">
    // import {}  from "#app"; // 用来让编辑器不报错的,其实没什么用。
    import xxx from "@/data.json"
          
    const route = useRoute();
    const name = route.params.name;
          
    const x = xxx.find(r => r.name === name)
    </script>
        
    <template>
    {{ name }}
    </template>
    
  3. 404页面

  4. Layouts

    创建 layouts 文件夹,并创建default.vue文件。

    在page中声明使用特定的layout

    <template>
      <div>
        <NuxtLayout name="custom">
          <Xxx />
        </NuxtLayout>
      </div>
    </template>
    

    在layout中插入slot(假设layout名error)

    <template>
      <div>
        <slot name="xxx" />
        <div>...</div>
      </div>
    </template>
    

    在page中:

    <template>
      <div>
        <NuxtLayout name="error">
          <template #xxx>
            <h1></h1>
          </template>
        </NuxtLayout>
        <div>...</div>
      </div>
    </template>
    
  5. metadata

    image-20220523155833406

    为了方便控制,也可以写在script里,

    image-20220523160139589

    也可以使用 outside stylesheets 方式:

    在nuxt.config.js里配置。

    export default defineNuxtConfig({
        charset: "utf-8",
        viewport: "width=device-width, initial-scale=1, maximum-scale=1",
        meta: {
            link: [
                { rel: "stylesheet", href: "/assets/css/style.css" },
                { rel: "stylesheet", href: "/assets/css/font-awesome.min.css" },
            ],
            script: [
                { src: '/assets/js/jquery.min.js' },
              ],
        }
    })
    
  6. 网页数据状态设计

  7. composable,useState

    用来保存状态。不能用 ref 做跟踪,因为这里没有render,会造成内存泄漏。

    image-20220523162132417

    image-20220523162252693

  8. Global State

    image-20220523162655826

    image-20220523162728965

  9. 生成静态网站

    目前还是测试阶段,官方不建议用于生产。

参考资料


vuejs3 备忘

上一篇记录了vue2的一些备忘,这一篇记录vue3和2的不同的地方。

2020.09.18发布vuejs 3.0.0,代号 one piece。官网:https://v3.cn.vuejs.org/

官方针对版本3的说明:https://github.com/vuejs/core/releases/tag/v3.0.0

一、更新内容

  1. 性能提升
  2. 源码升级
    1. tree-shaking
  3. 更好支持TS
  4. 新特性
    1. composition API

二、安装

配置下代理,备忘

npm init

npm config set proxy http://10.101.1.1:8118
npm config set https-proxy http://10.101.1.1:8118

npm i -S vue
  1. vue-cli

    4.5.0版本以上

    npm install -g @vue/cli # 安装/升级
       
    vue create xxx
    cd xxx
    npm run serve
    
  2. vite

    想代替 webpack,由vue团队打造的打包工具。

    HMR

    npm init vite@latest
    npm init vite-app my-vue-app
    npm init vite@latest my-vue-app -- --template vue
       
    cd my-vue-app
    npm install 
    npm run dev
    
  3. 代码结构对比

    Vue2:

    image-20220516105708052

    vue3:

    image-20220516115404231

    createApp 工厂函数,返回的app对象比vm更轻

    image-20220516115831732

三、入门

  1. setup方法

    image-20220516135431724

  2. reactive、ref、watch

    组件中的数据、方法都要配置到setup中(不需要往props和data、func里塞了)。

    创建独立的响应式值作为 refs

    watch只能监听ref的基本类型,要监听对象就用reactive。watchEffect也不错,但是感觉会影响性能?

    ref类似vue2中get和set来实现响应式。

    reactive是vue3中新增的proxy实例对象来实现响应式,搭配toRefs实现。

    <template>
      <div>
        <span>8</span>
        <button @click="count ++">Increment count</button>
        <button @click="nested.count.value ++">Nested Increment count</button>
      </div>
    </template>
       
    <script>
      import { ref } from 'vue'
      export default {
        setup() {
          const count = ref(0)
          return {
            count,
       
            nested: {
              count
            }
          }
        }
      }
    </script>
    

    image-20220516121609701

    ref 作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动解包内部值.

    使用解构的两个 property 的响应性都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联。

    import { reactive, toRefs } from 'vue'
       
    const book = reactive({
      author: 'Vue Team',
      year: '2020',
      title: 'Vue 3 Guide',
      description: 'You are reading this book right now ;)',
      price: 'free'
    })
       
    let { author, title } = toRefs(book)
       
    title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
    console.log(book.title) // 'Vue 3 Detailed Guide'
    

    image-20220516165948633

    image-20220516122757072

  3. compute

    vue3里可以按照vue2的compute使用。不过我们现在搞到setup里了。

    只读computed,接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。

    const count = ref(1)
    const plusOne = computed(() => count.value + 1)
       
    console.log(plusOne.value) // 2
    

    读写computed

    const count = ref(1)
    const plusOne = computed({
      get: () => count.value + 1,
      set: val => {
        count.value = val - 1
      }
    })
       
    plusOne.value = 1
    console.log(count.value) // 0
    
  4. 生命周期

    可以通过直接导入 onX 函数来注册生命周期钩子:

    import { onMounted, onUpdated, onUnmounted } from 'vue'
       
    const MyComponent = {
      setup() {
        onMounted(() => {
          console.log('mounted!')
        })
        onUpdated(() => {
          console.log('updated!')
        })
        onUnmounted(() => {
          console.log('unmounted!')
        })
      }
    }
    
  5. 自定义hook

    一般放在hook文件夹里,感觉很不错啊,这种引用方式。

    image-20220516163730910

    image-20220516163539285

    image-20220516163933726

    image-20220516163849888

  6. 其它composition api

    • readonly
    • shallowReadonly
    • toRaw
    • markRaw
    • customRef,注意防抖~
    • provide和inject,祖孙组件通信。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。
    • isRef,isReacrive,isReadonly,isProxy
  7. composition api的优势

    就我个人感觉,更像后端的一个class,定义变量和方法都在setup里,改起来舒服。

    配合hook使用就起飞🛫️。

  8. fragment

  9. Teleport

    用来UI定位的,舒服舒服,直接把内容传送到任意一个元素去。

  10. suspense

四、原理精进

  1. vue3如何做到交互性?

    参考vue2的内容,原理是一样的,只不过换了一个方法实现——ES6的Proxy Reflect对象。

    它们的区别仅在于:

    • Proxy返回的是bool值,defineProperty返回的是原始对象。
    • ES6语法更严格。

    通过ES6的 new Proxy(),可以创建一个代理用来代替另一个对象,实现基本操作的拦截和自定义。这个代理与目标对象表面上可以被当作同一个对象来看待。

    image-20220530180855430

    image-20220530181123151


一些简单的 Vue.js 笔记

前端不是我的主业,不过太久不用,捡起来每次都觉得头疼,还是稍微做点笔记吧。

首先是官方文档:https://cn.vuejs.org/v2/guide/,虽然目前到3版本了,还是先了解下v2版本的知识,并不复杂。

一、基本方法

基本就是官网内容。

  1. 格式

     <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
        
     <div id="app">
          
     </div>
        
     var app = new Vue({
       el: '#app',
       data: {
         message: 'Hello Vue!'
       }
     })
    

    image-20220510110633818

  2. 数据绑定、css绑定、事件绑定和语法糖

    • v-bind:value=<expression> 单项绑定,语法糖 :value=<expression>
    • v-model:value=<expression> 双向绑定(只用表单),语法糖 v-model=`
    • v-bind:class=xxx ,语法糖 :class=<str>
    • v-on:click.stop=<expression> 事件绑定,语法糖 @click.stop=<expression>
      • .stop
      • .prevent
  3. 计算属性

    func 里this指针指向的是vm

    computed不吃性能,尽量用它

     var vm = new Vue({
       el: '#example',
       data: {
         message: 'Hello'
       },
          
       computed: {
         // 计算属性的 getter
         reversedMessage: function () {
           // `this` 指向 vm 实例
           return this.message.split('').reverse().join('')
         }
       },
          
       watch: {
         // 如果 `question` 发生改变,这个函数就会运行
         question: function (newQuestion, oldQuestion) {
           this.answer = 'Waiting for you to stop typing...'
           this.debouncedGetAnswer()
         }
       },
          
       created: function () {
         this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
       },
          
       methods: {
         getAnswer: function () {
         }
      },
         
      mounted(){
         
      }
     })
    

    由vue管理的method,要以普通方式定义。

    否则应该用箭头函数,例如:

    getAnswer: function (()=>{})
    

    此时它的this为window。

    另外,指令directive相关的 this 都是 window

  4. 循环语句和条件语句

    循环:

     <ul id="example-1">
       <li v-for="item in items" :key="item.message">
            
       </li>
     </ul>
        
     <ul id="example-2">
       <li v-for="(item, index) in items">
          -  - 
       </li>
     </ul>
        
     <div v-for="(value, name, index) in object">
       . : 
     </div>
    

    条件:

     <div v-if="type === 'A'"></div>
     <div v-else-if="type === 'B'"></div>
     <div v-else-if="type === 'C'"></div>
     <div v-else></div>
    
  5. Vue 无法监控数组内容的改变

    https://cn.vuejs.org/v2/guide/list.html#数组更新检测

    对象因为有setter和getter,修改model可以监控到,view也会变动。

    但直接操作数组不奏效,因为数组没有这两个方法,引发view变动则需要调用如下方法:

    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()
  6. 过滤器,用于一些常见的文本格式化

  7. 生命周期

    一旦 data 数据变化就会重新解析模版。

    • create

    • mounted做初始化,把初始DOM放入页面时,仅调用一次。

    • update
    • destroy
  8. 组件

    • 创建

      <template>
      </template>
            
      <script>
      import ComponentA from './ComponentA'
      import ComponentC from './ComponentC'
            
      export default {
        name: ''
        data(){
        },
        components: {
          ComponentA,
          ComponentC
        },
        // ...
      }
      </script>
            
      <style>
      </style>
            
      或者
            
      const vc = Vue.extend({ })
      
    • 注册

      new Vue({
        el: '#app',
        components: {
          'component-a': ComponentA,
          'component-b': ComponentB
        }
      })
      
    • 使用

      <div id="app">
        <component-a></component-a>
        <component-b></component-b>
        <component-c></component-c>
      </div> 
      
  9. 原型对象prototype

    实例的隐式原型属性 __prototype__ 永远指向类(父类)的原型对象

    image-20220508152632174

二、脚手架

国内代理备忘

npm init

npm config set proxy http://10.101.1.1:8118
npm config set https-proxy http://10.101.1.1:8118

npm i -S vue
  1. 命令备忘

    npm install -g @vue/cli
    vue --version
    vue create hello-world
    

    image-20220516103015750

    修改配置,在vue.config.js中添加(参考配置

    module.exports = {
        devServer: {
          host: '0.0.0.0',
          port: 80
        }
    }
    

    image-20220516105420100

    image-20220516105517784

    文件预览

    image-20220516105708052

  2. render

    image-20220509105413880

    vue.runtime.xxx.js 是运行版的vue,没有模版解析器,不能使用template配置项。

  3. vue ref,vue用于获取元素,用来代替以前通过id获取元素的办法。

  4. 数据传递prop

    相当于后端中一个类class实例的只读属性,。

    props: {
      title: String,
      likes: Number,
      isPublished: Boolean,
      commentIds: Array,
      author: Object,
      callback: Function,
      contactsPromise: Promise // or any other constructor
    }
    
  5. 混入 (mixin)

    有点类似于后端的基类和子类,mixin属于基类的方法,多个子类都可以使用基类的方法。

    var vm = new Vue({
      mixins: [mixin],
      methods: {
         ...
      }
    })
    

    可以全局混入,也可以局部混入。

    Vue.mixin({
      xxx: xxx
    })
    
  6. 插件

    // 调用 `MyPlugin.install(Vue)`
    Vue.use(MyPlugin)
    
  7. scope

    解决style类名相同,引入顺序不同,导致冲突的问题。

    <style scoped>
    </style>
    

    不要在App里使用scoped

三、开发步骤(入门)

官网的一些内容和后端思维是互通的,比如状态提升之类的,不要害怕,见人说人话,见鬼说鬼话。

  1. 拆分静态组件,按功能点拆分

  2. 静态组件,不考虑交互

    1. 把html整个塞到app里
    2. 把一个组件的html剪切后立马写占位符,然后塞到组件里。
    3. 把样式也拆了,塞到组件,style加个scoped。
  3. 数据交互设计

    类似于后端,设计数据库和model。

    组件通信的几种方式:

    1. 通过 props 存储数据,数据存在App上。

      1. 父组件定义data,通过props传给子组件(子要在props里接收)
      2. 父组件定义method,通过props传给另一个子组件
      3. 注意子组件的v-model不要改props

      相当于内存存变量。

    2. 自定义事件

      子组件给父组件传数据用。

      在父组件中给子组件绑定自定义事件,事件回调在父组件。(父给子传函数,要求子返回数据)

    3. 全局事件总线

      定义一个所有组件的子组件vc(也可以直接用vm)挂在prototype上,然后使用事件触发。相当于后端的redis。多读单写

    4. 消息订阅&发布

      全局事件总线和消息订阅其实差不多的效果。多读单写

    5. 浏览器localstorage,相当于后端的数据库DB。

    6. vuex

      多读多写

  4. 动态组件

    不同功能的增删查改+统计信息变动

    1. 增删,事件event。

四、高级

  1. 自定义事件

    this.$emit('myEvent')
       
    <my-component v-on:my-event="doSomething"></my-component>
       
    this.$off('myEvent')
    this.$off(['myEvent','xxx'])
    this.$off()
    

    也适用于子给父组件传输数据。

    image-20220509163051261

    自定义事件由父组件运行,props方式由子组件运行。

    还可以通过ref绑定自定义事件。

    image-20220509163610694

  2. 全局事件总线

    定义一个所有组件的子组件vc,然后赋值给prototype。

    image-20220510115509466

    改良版:

    把vm当成所有组件的子组件,这样子?真是奇妙。

    image-20220510120130524

    image-20220511165656535

  3. 消息订阅&发布

    $ npm -i pubsub.js
       
    import pubsub from 'pubsub-js'
       
       
    pubsub.publish('hello',666)
    pubsub.subscribe('hello',function(msgName,data){ })
    pubsub.subscribe('hello',(msgName,data)=>{ })
    
  4. nextTick

  5. 动画/过渡

    其实就是封装了一个 transition 标签。

    <transition name"xxx" appear></transition>
       
    .xxx-enter-active{
    animation: kelu 1s linear;
    }
    .xxx-leave-active{}
       
    @keyframs kelu {
    	from{
    		transform: xxx
    	}
    	to{}
    }
    

    image-20220510163037015

    第三方动画:Animate.css:http://animate.style

  6. 跨域

    axios请求,

    cors(后端解决),代理服务器(前端解决)

    module.exports = {
      devServer: {
        proxy: 'http://localhost:4000'
      }
    }
       
    module.exports = {
      devServer: {
        proxy: {
          '/api': {
            target: '<url>',
            ws: true,
            changeOrigin: true
          },
          '/foo': {
            target: '<other_url>'
          }
        }
      }
    }
       
    # https://github.com/chimurai/http-proxy-middleware#proxycontext-config
    

    image-20220511144557437

    image-20220511145140405

  7. 插槽

    <div class="container">
      <header>
        <slot name="header"></slot>
      </header>
      <main>
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>
       
    <base-layout>
      <template v-slot:header>
        <h1>Here might be a page title</h1>
      </template>
       
      <p>A paragraph for the main content.</p>
      <p>And another one.</p>
       
      <template v-slot:footer>
        <p>Here's some contact info</p>
      </template>
    </base-layout>
    
  8. vuex

    npm i vuex
       
    import Vuex from 'vuex'
    Vue.use(Vuex)
    

    vuex

    import { createApp } from 'vue'
    import { createStore } from 'vuex'
       
    // 创建一个新的 store 实例
    const store = createStore({
      state () {
        return {
          count: 0
        }
      },
      mutations: {
        increment (state) {
          state.count++
        }
      }
    })
       
    const app = createApp({ /* 根组件 */ })
       
    // 将 store 实例作为插件安装
    app.use(store)
    

    dispatch -> commit ->

    vc -> Action(ctx,value) -> mutation(state,value)

    dispatch可以跳过,直接commit就行了。

    mapState

    // 在单独构建的版本中辅助函数为 Vuex.mapState
    import { mapState } from 'vuex'
       
    export default {
      // ...
      computed: mapState({
        // 箭头函数可使代码更简练
        count: state => state.count,
       
        // 传字符串参数 'count' 等同于 `state => state.count`
        countAlias: 'count',
       
        // 为了能够使用 `this` 获取局部状态,必须使用常规函数
        countPlusLocalState (state) {
          return state.count + this.localCount
        }
      })
    }
    

    image-20220512135239999

  9. 路由

    npm i vue-router@3
       
    import Vue from 'vue'
    import VueRouter from 'vue-router'
       
    Vue.use(VueRouter)
    
    • 一般组件

    • 路由组件

    <router-view></router-view>
    # 一级路由
    
    • 多级路由
    const routes = [
      {
        path: '/user/:id',
        component: User,
        children: [
          {
            path: 'profile',
            component: UserProfile,
          },
          {
            path: 'posts',
            component: UserPosts,
          },
        ],
      },
    ]
    
    • 路由传参

      • query参数

      • params参数

      • props(对象/函数/布尔)

        image-20220512160434852

        解构赋值,连续解构赋值:

        image-20220512160650919

        image-20220512160544087

    • 路由命名

    • 路由历史记录

      声明式 编程式
      <router-link :to="..." replace> router.replace(...)
      <router-link :to="..."> router.push(...)

      路由缓存:

        <keep-alive :include="['xx','yy']">
          <router-view></router-view>
        </keep-alive>
      

      路由组件新增了两个生命周期

      • activated
      • deactivated
    • 路由权限

      数据存在meta里,

      前置路由守卫,主要做鉴权

      // GOOD
      router.beforeEach((to, from, next) => {
        if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
        else next()
      })
      

      后置路由守卫作用,一般就是修改页面内容之类的。

五、原理精进

  1. 为什么 vuejs 可以做到reactive交互式

    主要是通过 Object.defineProperty() 方法实现,参考https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty,直接在一个对象上定义一个新属性。

    自定义一个setter(所以必须用object类型)

    image-20220530172233546

    image-20220530172617448

六、其他

  1. 字面量赋值

    image-20220511154311040