preface

Hello everyone, I’m WebFansplz. First of all, I’d like to share with you a piece of good news, I joined VueUse team. Thank you @ANTfu for inviting me and I’m very happy to be a member of the team. Today I want to talk to you about the design and implementation of VueUse.

introduce

As you all know, Vue3 introduces a composite API that greatly improves logic reuse. VueUse implements many easy-to-use, practical, and fun features based on a composite API. Such as:

useMagicKeys

UseMagicKeys monitors key status and provides the function of combining hotkeys, which is very magical and interesting. Using it, we can easily monitor the number of times we use the CV method 🙂

useScroll

UseScroll provides responsive states and values, such as scroll state, arrival state, scroll direction, and current scroll position.

useElementByPoint

UseElementByPoint is used to retrieve the topmost element of the current coordinate position in real time. With useMouse, we can do some interesting interactions and effects.

The user experience

User experience

VueUse is a great user experience for both users and developers. Let’s start with the user experience:

Strong typing support

VueUse is written in TypeScript with full TS documentation and good TS support.

SSR support

We have friendly support for SSR, which works well on the server rendering scene.

Ease of use

For some functions that support passing in configuration options, we provide a set of common default options for users, so that users do not need to pay too much attention to the implementation and details of your functions in most application scenarios. Take useScroll as an example:

<script setup lang="ts">
import { useScroll } from '@vueuse/core'

const el = ref<HTMLElement | null> ()// Just pass in the scroll element to work
const { x, y } = useScroll(el)
// Throttling support options
const { x, y } = useScroll(el, { throttle: 200 })
</script>
Copy the code

UseScroll provides throttling options for performance-demanding developers. However, we want users to focus on this configuration only when they need it, because it can be a mental burden to understand the meaning and configuration of multiple parameters. In addition, the universal default configuration is also a manifestation of the ability to open the box!

Using document

Using the document, we provide interactive Demo and simplified Usage. Users can learn more about the function by playing the Demo, or easily use the function by copying the Usage by CV method. How sweet!

compatibility

As mentioned earlier, Vue3 introduces the concept of composite apis, but thanks to the implementation of the composition-API plug-in, we can also use composite apis in Vue2 projects. In order to make VueUse accessible to more users,Anthony Fu implemented Vue-Demi, which determines the user’s installation environment (Vue2 project reference composition-API plugin,Vue3 project reference official package) so that Vue2 users can use VueUse, Nays!

Developer Experience

The directory structure

Based on Monorepo, the project uses a flat directory structure to make it easy for developers to find corresponding functions.

We created a separate folder for each function implementation, so that when fixing bugs and adding new features, developers only need to pay attention to the implementation of specific functions in this folder, and do not need to pay attention to the details of the implementation of the project itself, which greatly reduces the cost of getting started. Demo and documentation are also written in this folder, avoiding the bad development experience of repeatedly jumping up and down to find directory structure files.

Contribution to guide

We provided a very detailed contribution guide to help developers get started quickly and wrote some automated scripts to help developers avoid manual work.

Atomization CSS

The project uses atomized CSS as the programming scheme of CSS. Personally, I think atomized CSS can help us write Demo quickly, and the Demo of each function is independent and uncoupled, without the mental burden of abstract reuse.

Design idea

Combinable functions

Combinable functions simply mean that combinatorial relations can be established between functions, for example:

The realization of useScroll combines three functions, which combine functions of a single responsibility into another function to achieve the ability of logic reuse. I think this is the charm of the combined function. Of course, each function can also be used independently, and users can choose according to their own needs.

Developers can achieve a better separation of concerns when dealing with functions. For example, when dealing with useScroll, we only need to focus on the implementation of the scroll function, and do not need to focus on the logic and implementation inside the anti-shake throttling and event binding.

Establish “connections”

Anthony Fu shares this pattern in Vue Conf 2021:

  • Establish an input -> output link
  • The output automatically changes as the input changes

When we write combinable functions, we link the data to the logic so that we don’t have to worry about how and when to update the data. Here’s an example:

<script setup lang="ts">
import { ref } from 'vue'
import { useDateFormat, useNow } from '@vueuse/core'

const now = useNow() // return a ref value
const formatted = useDateFormat(now) // Pass the data to the logic to establish a link

</script>

/ / useDateFormat implementation
function useDateFormat(date, formatStr = 'HH:mm:ss') {
  return computed(() = > formatDate(normalizeDate(unref(date)), unref(formatStr)))
}
Copy the code

As you can see from the example above, useDateFormat uses computed attributes to wrap the input in its internal logic, so that the output automatically changes according to the input, and the user only needs to pass in a reactive value without worrying about the specific update logic.

Use whenever possiblerefalternativereactive

Ref and Reactive have their own advantages and disadvantages. Here are my personal views from the perspective of users:

// reactive

function useScroll(element){
  const position = reactive({ x: 0.y: 0 });
  // impl...
  return { position,...}
}
// Deconstruction loses responsiveness
const { position } = useScroll(element)
// The user needs to manually toRefs to remain responsive
const { x, y } = toRefs(position)

// ref

function useScroll(element){
  const x = ref(0);
  const y = ref(0);
  // impl...
  return {x,y,...}
}
// No loss of responsiveness, users can render directly,watch..
const { x, y } = useScroll(element)
Copy the code

As we can see from the example above, if we use Reactive, the user needs to worry about the loss of responsiveness of deconstruction, which limits the user’s freedom to use deconstruction and reduces the usability of the function to a certain extent.

Some people may make fun of the ref.value usage, but in most cases, there are a few tricks you can use to reduce its use:

  • unref API
const x = ref(0)
console.log(unref(x)) / / 0
Copy the code
  • usereactiveUnpack theref
const x = ref(0)
const y = ref(0)
const position = reactive({x, y})
console.log(position.x, position.y) / / 0 0
Copy the code
  • It’s still experimentalReactivity Transform
<script setup>
let count = $ref(0)
count++
</script>
Copy the code

Use the Options object as a parameter

When implementing a function with optional arguments, we usually recommend that developers use objects as input arguments, for example:

// good

function useScroll(element, { throttle, onScroll, ... }){... }// bad

function useScroll(element, throttle, onScroll, ....){... }Copy the code

You can clearly see the difference between the two. There is no doubt that the first version is more extensible and less likely to make disruptive changes to the functionality itself in later iterations.

Document implementation

The specific implementation of the function will not be detailed, after all, we have 200 so many 😝. Here are some interesting implementations of the VueUse build documentation section, which I thought was a great job.

The document of

Let’s first look at the components of the next function document:

The build process

VueUse uses VitePress as a document builder, so let’s take a look at the interesting parts:

  • Start the VitePress service with the Packages folder as the entry point

VitePress USES stereotypes about routing (file routing), so visit http://xxx.com/core/onClickOutside will parse we actually corresponds to the index. The md file. The index. Md file only contains usage. Where does the other information come from? Here’s the fun part

  • Write a Vite plug-inMarkdownTransformTo process Markdown files:
export function MarkdownTransform() :Plugin {
 
  return {
    name: 'vueuse-md-transform'.enforce: 'pre'.async transform(code, id) {
      if(! id.endsWith('.md'))
        return null

      const [pkg, name, i] = id.split('/').slice(-3)

      if (functionNames.includes(name) && i === 'index.md') {
        // handle index.md
        // Concatenate the Demo, type declarations, contributor information, and update logs using concatenated strings
        const { footer, header } = await getFunctionMarkdown(pkg, name)

        if (hasTypes)
          code = replacer(code, footer, 'FOOTER'.'tail')
        if (header)
          code = code.slice(0, sliceIndex) + header + code.slice(sliceIndex)
      }

      return code
    },
  }
}
Copy the code

With the processing of this Vite plug-in, our documentation section is complete. Here’s another question: Where do the contributor data and update log data come from? Both data are handled in the same way, so I’ll take one of them to illustrate the implementation:

  • Get git committer information
import Git from 'simple-git'

export async function getContributorsAt(path: string) {
    const list = (await git.raw(['log'.'--pretty=format:"%an|%ae"'.The '-', path]))
      .split('\n')
      .map(i= > i.slice(1, -1).split('|') as [string.string])
    return list
}
Copy the code

We read the information about the file submitter through the simple-Git plug-in. Once we have the data, how do we render it to the page? Use the Vite plugin again, but this time we need to register the virtual module.

  • Register a Virtual module
const ID = '/virtual-contributors'

export function Contributors(data: Record<string, ContributorInfo[]>) :Plugin {
  return {
    name: 'vueuse-contributors'.resolveId(id) {
      return id === ID ? ID : null
    },
    load(id) {
      if(id ! == ID)return null
      return `export default The ${JSON.stringify(data)}`}}},Copy the code

We just pass in the data we just obtained when we register the virtual module, and then we can introduce the virtual module in the component to access the data.

  • Using the data
<script setup lang="ts">
import _contributors from '/virtual-contributors'
import { computed } from 'vue'

const props = defineProps<{ fn: string} > ()const contributors = computed(() = > _contributors[props.fn] || [])
</script>
Copy the code

Once we have the data, we can render the page. That’s how the Ficolin-3 and Changelog sections in the documentation work. Let’s take a look at the effect:

The Vite plugin can be used for a lot of things.

V8.0 come 🎉

We officially released V8.0 in the last couple of days, bringing with it:

  • Some function names are normalized and backward compatible with aliases
  • Several new functions have been added and the number of functions now reaches 200 +
  • @vueuse/core/nuxt= >@vueuse/nuxt
  • Some functions to do instruction support, welcome to use

conclusion

Finally, thanks to Anthony Fu for his corrections and suggestions on this article, Risby! If my article is helpful to you, welcome to follow me to study together.