vue3.0CompositionAPIs及3.0原理 todoList使用

vue3.0CompositionAPIs及3.0原理

一、3.0介绍

  • 源码组织方式的改变
  • Composition API
  • 性能提升
  • Vite

1、源码组织方式

  • 源码采用TypeScript重写
    提高了代码的可维护性。大型项目的开发都推荐使用类型化的语言,在编码的过程中检查类型的问题。
  • 使用Monorepo管理项目结构
    使用一个项目管理多个包,把不同功能的代码放到不同的package中管理,每个功能模块都可以单独发布,单独测试,单独使用

2、不同构建版本

Vue3中不再构建UMD模块化的方式,因为UMD会让代码有更多的冗余,它要支持多种模块化的方式。Vue3中将CJS、ESModule和自执行函数的方式分别打包到了不同的文件中。在packages/vue/dist中有Vue3的不同构建版本。

  • cjs(两个版本都是完整版,包含编译器)
    • vue.cjs.js
    • vue.cjs.prod.js(生产版,代码进行了压缩)
  • global(这四个版本都可以在浏览器中直接通过scripts标签导入,导入js之后会增加一个全局的Vue对象)
    • vue.global.js(完整版,包含编译器和运行时,开发版本)
    • vue.global.prod.js(完整版,包含编译器和运行时,代码进行了压缩)
    • vue.runtime.global.js(只包含运行时的版本)
    • vue.runtime.global.prod.js
  • browser(四个版本都包含esm,浏览器的原生模块化方式,可以直接通过

    3、Composition API

    • RFC (Request For Coments)
      • https://github.com/vuejs/rfcs
    • Composition API RFC
      • https://composition-api.vuejs.org

    设计动机

    • Options API
      • 包含一个描述组件选项(data、methods、props等)的对象
      • Options API 开发复杂组件,同一个功能逻辑的代码被拆分到不同选项
    • Composition API
      • Vue.js 3.0 新增的一组API
      • 一组基于函数的API
      • 可以更灵活的组织组件的逻辑

    4、性能提升

    1、响应式系统升级

    Vue3使用Proxy对象重写了响应式系统。

    • Vue.js 2.x中响应式系统的核心 defineProperty,初始化的时候递归遍历所有的属性,转化为getter、setter,若data中属性又是对象的话,需要递归处理子对象属性(初始化时)
    • Vue.js 3.0中使用Proxy对象重写响应式系统
      • 可监听动态新增的属性
      • 可以监听删除的属性
      • 可以监听数组的索引和length属性

    2. 编译优化

    重写了DOM提高渲染的性能。

    • Vue.js 2.x中通过标记静态根节点,优化diff的过程
    • Vue.js 3.0 中标记和提升所有的静态根节点,diff的时候只需 要对比动态节点内容
      • Fragments(升级vetur插件)
      • 静态提升
      • Patch flag
      • 缓存事件处理函数

    3.源码体积的优化

    通过优化源码的体积和更好的TreeShaking的支持,减少大打包的体积

    • Vue.js 3.0中移除了一些不常用的API
      • 例如:inline-template、filter等
    • Tree-shaking
      • 例如:Vue3中的没用到的模块不会被打包,但是核心模块会打包。Keep-Alive、transition等都是按需引入的内置指令v-model都是按需引入的。

    5 Vite

    Vue的打包工具。Vite是法语中的"快"的意思

    1.ES Module

    • 现代浏览器都支持ES Module(IE不支持)
    • 通过下面的方式加载模块
    • 支持模块的script默认延迟加载
    • 有了type="module"的模块是延迟加载的,类似于script标签设置defer
      • 在文档解析完成后,也就是DOM树生成之后,触发DOMContentLoaded事件前执行
        相关代码查看gtee

    2.Vite as Vue-CLI

    Vite 快就是使用浏览器支持的ES Modules的方式,避免开发环境打包,从而提升开发速度

    • Vite 在开发模式下不需要打包可以直接运行
    • Vue-Cli 开发模式下必须对项目打包才可以运行

    3.Vite特点

    • 快速冷启动(因为不需要打包)
    • 按需编译(当前代码需要加载时才编译)
    • 模块热更新(更新性能和模块总数无关)
    • Vite 在生产环境下使用Rollup 打包(打包体积小)
      • 基于ES Module 的方式打包
    • Vue-Cli 使用 Webpack 打包

    4.Vite创建项目

    • Vite 创建项目
      • npm init vite-app
      • cd
      • npm install
      • npm run dev
    • 基于模版创建项目
      • npm init vite-app --template react
      • npm init vite-app --template preact

    二、Composition API

    1.Composition API

    启动本地http打开html,需要安装Live Server
    安装Vue3.0 ,体验 createApp 的使用

    npm install vue@3.0.0-rc.9
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        x: {{ position.x }}
        y: {{ position.y }}
      </div>
      <script type="module">
        import { createApp } from './node_modules/vue/dist/vue.esm-browser.js'
        const app = createApp({
          data () {
            return {
              position: {
                x: 0,
                y: 0
              }
            }
          }
        })
        console.log(app)
        app.mount('#app')
      </script>
    </body>
    </html>
    

    Vue 3.0 和 Vue2.0 的区别,成员要少很多,没有$开头, 使用方式和以前一样
    在这里插入图片描述
    setUp 函数:Composition api的入口
    createApp:創建vue对象
    reactive:创建响应式对象

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    
    <body>
      <div id="app">
        x: {{ position.x }}
        <br>
        y: {{ position.y }}
      </div>
      <script type="module">
      import { createApp, reactive} from './node_modules/vue/dist/vue.esm-browser.js'
      const app = createApp({
      setup () {
        // 第一个参数 props
        // 第二个参数 context, attrs, emit, slots
        const position = reactive({
          x: 0,
          y: 0
        })
        return {
          position
        }
        }
    })
    console.log(app)
    app.mount('#app')
    </script>
    
    </body>
    
    </html>
    

    2.生命周期钩子函数

    setup中使用钩子函数
    在这里插入图片描述
    当鼠标移动的时候显示鼠标移动的位置,当组件卸载时,鼠标移动的事件也要移除

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    
    <body>
      <div id="app">
        x: {{ position.x }}
        <br>
        y: {{ position.y }}
      </div>
      <script type="module">
    import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
    const app = createApp({
      setup () {
        // 第一个参数 props
        // 第二个参数 context, attrs, emit, slots
        const position = reactive({
          x: 0,
          y: 0
        })
        const update = e => {
          position.x = e.pageX;
          position.y = e.pageY;
        }
        
        onMounted (() => {
          window.addEventListener('mousemove', update)
        })
    
        onUnmounted (() => {
          window.removeEventListener('mousemove', update)
        })
        return {
          position
        }
      }
    })
    console.log(app)
    app.mount('#app')
    </script>
    </body>
    </html>
    

    将获取鼠标位置的方法封装到一个函数中,优化代码

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    
    <body>
      <div id="app">
        x: {{ position.x }}
        <br>
        y: {{ position.y }}
      </div>
      <script type="module">
      import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
    
      function useMousePosition(){
      // 第一个参数 props
        // 第二个参数 context, attrs, emit, slots
        const position = reactive({
          x: 0,
          y: 0
        })
        const update = e => {
          position.x = e.pageX;
          position.y = e.pageY;
        }
        
        onMounted (() => {
          window.addEventListener('mousemove', update)
        })
    
        onUnmounted (() => {
          window.removeEventListener('mousemove', update)
        })
    
        return position
      }
      const app = createApp({
        setup () {
          const position = useMousePosition();
          return {
            position
          }
        }
      })
    console.log(app)
    app.mount('#app')
    </script>
    </body>
    </html>
    

    3.reactive-toRefs-ref

    先看上一节出现的小问题

    import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
    function useMousePosition () {
      // 第一个参数 props
      // 第二个参数 context, attrs, emit, slots
      const position = reactive({
        x: 0,
        y: 0
      })
      const update = e => {
        position.x = e.pageX;
        position.y = e.pageY;
      }
      
      onMounted (() => {
        window.addEventListener('mousemove', update)
      })
    
      onUnmounted (() => {
        window.removeEventListener('mousemove', update)
      })
      return position
    }
    const app = createApp({
      setup () {
        // const position = useMousePosition()
        const { x, y} = useMousePosition()
        return {
          x,
          y
        }
      }
    })
    console.log(app)
    app.mount('#app')
    
    

    useMousePosition进行解构后,数据不是响应式的了

    这里的 position 是响应式对象,因为在useMousePosition中调用了reactive 函数,把传入的对象包装成了 Proxy 对象,也就是说 position 就是 proxy对象,当 position 访问 x, y的时候会调用代理中的 getter 拦截收集依赖,变化的时候会调用 setter
    当把代理对象解构之后,x,y就是两个基本类型的变量。当重新复制时,无法调用代理对象的gettersetter,故无法进行更新操作
    解决方法:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    
    <body>
      <div id="app">
        x: {{ x }}
        <br>
        y: {{ y }}
      </div>
      <script type="module">
      import { createApp, reactive, onMounted, onUnmounted ,toRefs} from './node_modules/vue/dist/vue.esm-browser.js'
    
      function useMousePosition(){
      // 第一个参数 props
        // 第二个参数 context, attrs, emit, slots
        const position = reactive({
          x: 0,
          y: 0
        })
        const update = e => {
          position.x = e.pageX;
          position.y = e.pageY;
        }
        
        onMounted (() => {
          window.addEventListener('mousemove', update)
        })
    
        onUnmounted (() => {
          window.removeEventListener('mousemove', update)
        })
    
        console.log(position);
        return toRefs(position)
      }
      const app = createApp({
      setup () {
        // const position = useMousePosition()
        const { x, y} = useMousePosition()
        return {
          x,
          y
        }
      }
    })
    console.log(app)
    app.mount('#app')
    </script>
    
    </body>
    
    </html>
    

    toRefs原理
    toRefs 要求传入的参数 必须为代理对象,当前的 position 就是 reactive返回的代理对象,如果不是的话,会发出警告,提示传递代理对象,
    内部会创建一个新的对象,然后遍历传入代理对象的所有属性,把所有属性的值都转换成响应式对象,相当于将postion 的x, y属性转换成响应式对象,
    挂在到新创建的对象上,最后把新创建的对象返回。内部为代理对象的每一个属性创建一个具有 value 属性,value属性具有 getter,setter, getter 中返回对象属性的值,
    setter中给代理对象赋值。所以返回的每一个属性都是响应式的

    ref 把普通数据转转换成响应式数据,ref 可以把基本类型包装成响应式数据

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    
    <body>
      <div id="app">
        <button @click="increase">按钮</button>
        <span>{{count}}</span>
      </div>
      <script type="module">
      import { createApp, ref} from './node_modules/vue/dist/vue.esm-browser.js'
    
      function useCount(){
        const count = ref(0)
        return {
          count,
          increase:()=>{
            count.value++;
          }
        }
      }
      const app = createApp({
      setup () {
       return {
         ...useCount()
       }
      }
    }).mount('#app')
    </script>
    
    </body>
    
    </html>
    

    4.computed

    作用:简化模板中的代码,可以缓存计算的结果,当数据变化后,才会重新计算

    • 第一种用法:
      • wacth(() => { count.value + 1})
    • 第二种用法
    const count = ref(1)
    
    const plusOne = computed({
      get: () => {
        count.value + 1
      },
      set: val => {
        conut.value = val - 1
      }
    })
    

    案例展示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <button @click="push">按钮</button>
        未完成:{{ activeCount }}
      </div>
      <script type="module">
        import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js'
        const data = [
          { text: '看书', completed: false },
          { text: '敲代码', completed: false },
          { text: '约会', completed: true }
        ]
        createApp({
          setup(){
            const todos = reactive(data);//将data转化为响应式对象
            const activeCount = computed(()=>{
              return todos.filter(item=>!item.completed).length
            })
            return {
              activeCount,
              push:()=>{
                todos.push({
                  text:'开会',
                  completed:false
                })
              }
            }
          }
        }).mount("#app")
      </script>
    </body>
    </html>
    

    5.watch

    • Watch 的三个参数
      • 第一个参数: 要监听的数据(可谓获取值得函数,ref ,reactive返回的数据,或者是数组)
      • 第二个参数: 监听到数据变化后执行的函数,这个函数有两个参数分别是新- 值和旧值
      • 第三个参数: 选项对象, deep 和 immediate,
    • Watch 的返回值
      • 取消监听的函数
        使用案例
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <p>
          请问一个 yes/no 的问题:
          <input v-model="question">
        </p>
        <p>{{ answer }}</p>
      </div>
    
      <script type="module">
        // https://www.yesno.wtf/api
        import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js'
    
        createApp({
          setup () {
            const question = ref('')
            const answer = ref('')
    
            watch(question, async (newValue, oldValue) => {
              const response = await fetch('https://www.yesno.wtf/api')
              const data = await response.json()
              answer.value = data.answer
            })
    
            return {
              question,
              answer
            }
          }
        }).mount('#app')
      </script>
    </body>
    </html>
    

    6.WatchEffect

    • 是watch 函数的简化版本,也用来监视数据的变化
      • 内部实现和watch调用的同一个函数dowatch,不同的是watchEffect没有第二个回调函数的参数
    • 接收一个函数作为参数,监听函数内响应式数据的变化,会立即执行这个函数,当数据变化会重新运行此函数
      演示:
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <button @click="increase">increase</button>
        <button @click="stop">stop</button>
        <br>
        {{ count }}
      </div>
    
      <script type="module">
        import { createApp, ref, watchEffect } from './node_modules/vue/dist/vue.esm-browser.js'
    
        createApp({
          setup () {
            const count = ref(0)
            const stop = watchEffect(() => {
              console.log(count.value)
            })
    
            return {
              count,
              stop,
              increase: () => {
                count.value++
              }
            }
          }
        }).mount('#app')
      </script>
    </body>
    </html>
    

    三、TodoList

    代办事情清单:使用Composition API实现此案例
    ToDoList 案例:

    • 添加代办事项
    • 删除待办事项
    • 编辑待办事项
    • 切换待办事项
    • 存储待办事项

    1.TodoList-项目结构

    升级 Vue-cli 需要升级到4.5 版本以上,可以选择Vue3.0 进行安装

    • 使用 vue -V查看目前的版本
    • 卸载之前安装的npm uninstall vue-cli -g
    • 重新安装npm install -g @vue/cli

    main.js 导入了 vue 的模块,从 Vue 模块又导入了createApp 函数,导入 app组件,将app 组件挂在到div 上

    2. 添加待办事项

    给输入框绑定事件

    <header class="header">
      <h1>todos</h1>
      <input
        class="new-todo"
        placeholder="What needs to be done?"
        autocomplete="off"
        autofocus
        v-model="input"
        @keyup.enter="addTodo"
        >
    </header>
    

    js文件内容

    import './assets/index.css'
    // 添加待办事项
    import { ref } from 'vue'
    const useAdd = todos => {
      const input = ref('')
      const addTodo = () => {
        const text = input.value && input.value.trim()
        if (text.length === 0) return 
        todos.value.unshift({
          text,
          completed: false
        })
        input.value = ''
      }
      return {
        addTodo,
        input
      }
    }
    export default {
      setup () {
        const todos = ref([])
        return {
          ...useAdd(todos),
          todos
        }
      }
    }
    
    

    循环遍历,展示添加的todo事件

    <section class="main">
      <input id="toggle-all" class="toggle-all" v-model="allDone" type="checkbox">
      <label for="toggle-all">Mark all as complete</label>
      <ul class="todo-list">
        <li
          v-for="todo in todos"
          :key="todo.text"
        >
          <div class="view">
            <input class="toggle" type="checkbox">
            <label>{{ todo.text }}</label>
            <button class="destroy"></button>
          </div>
          <input
            class="edit"
            type="text"
            >
        </li>
      </ul>
    </section>
    
    

    3. 删除代办事项

    <div class="view">
      <input class="toggle" type="checkbox">
      <label>{{ todo.text }}</label>
      <!-- 删除当前的待办事项 -->
      <button class="destroy" @click="remove(todo)"></button>
    </div>
    
    const useRemove = todos => {
      const remove = todo => {
        const index = todos.value.indexOf(todo)
        todos.value.splice(index, 1)
      }
      return {
        remove
      }
    }
    
    export default {
      setup () {
        const todos = ref([])
        return {
          todos,
          ...useAdd(todos),
          ...useRemove(todos)
        }
      }
    }
    

    4. 编辑代办事项

    • 双击待办事项,展示编辑文本框
    • 按回车或者编辑文本框失去焦点修改数据
    • 按ESC 取消编辑
    • 把编辑文本框清空按回车,删除这一项
    • 显示编辑文本框的时候获取焦点
    // 编辑待办事项
    const useEdit = remove => {
      let beforeEditingText = ''
      const editingTodo = ref(null)
      const editTodo = todo => {
        beforeEditingText = todo.text
        editingTodo.value = todo
      }
    
      const doneEdit = todo => {
        if (!editingTodo.value) return
        todo.text = todo.text.trim()
        todo.text || remove(todo)
        editingTodo.value = null
      }
    
      const cancelEdit = todo => {
        editingTodo.value = null
        todo.text = beforeEditingText
      }
      return {
        editingTodo,
        editTodo,
        doneEdit,
        cancelEdit
      }
    }
    
    export default{
      name:'App',
      setup(){
        const todos = ref([])
        const {remove} = useRemove(todos)//因useEdit使用remove函数,故需要先调用useRemove函数,解构remove函数
        return {
          todos,
          remove,
          ...useAdd(todos),
          ...useEdit(remove)
        }
      }
    }
    
    <ul class="todo-list">
      <li
        v-for="todo in todos"
        :key="todo.text"
        :class="{ editing: todo === editingTodo}"
      >
      <!-- <li
              v-for="todo in todos"
              :key="todo.text"  
              :class="{ editing: todo === editingTodo}"
         > -->
        <div class="view">
          <input class="toggle" type="checkbox">
          <label @dblclick="editTodo(todo)">{{ todo.text }}</label>
          <button class="destroy" @click="remove(todo)"></button>
        </div>
        <input
          class="edit"
          type="text"
          v-model="todo.text"
          @keyup.enter="doneEdit(todo)"
          @blur="doneEdit(todo)"
          @keyup.esc="cancelEdit(todo)"
          >
      </li>
    </ul>
    

    5. 编辑文本框获取焦点

    自定义指令
    在这里插入图片描述
    实际使用:
    在这里插入图片描述
    通过判断编辑文本框是否是当前要编辑的文本框

    <input
      class="edit"
      type="text"
      v-editing-focus="todo === editingTodo"
      v-model="todo.text"
      @keyup.enter="doneEdit(todo)"
      @blur="doneEdit(todo)"
      @keyup.esc="cancelEdit(todo)"
      >
    

    自定义指令的编写

    export default {
      setup () {
        const todos = ref([])
        const { remove } = useRemove(todos)
        return {
          todos,
          remove,
          ...useAdd(todos),
          ...useEdit(remove)
        }
      },
      directives: {
        editingFocus: (el, binding) => {
          binding.value && el.focus()
        }
      }
    }
    

    6. 切换待办事项的状态

    • 点击checkboxd,改变所有待办项状态
    • All/Active(w未完成的代办项)/Completed(已完成的待办项)
      其它
    • 显示未完成待办项个数
    • 移除所有完成的项目
    • 如果没有待办项,隐藏main和footer

    6.1 点击check,改变状态

    // 4 切换代办项,完成状态
    const useFilter = todos=>{
      const allDone = computed({
        get(){
          return !todos.value.filter(todo=>!todo.completed).length //有未完成的代办项返回false
        },
        set(value){
          todos.value.forEach(todo=>{
            todo.completed = value
          })
        }
      })
      return {
        allDone
      }
    }
    

    自定义指令

    export default{
      name:'App',
      setup(){
        const todos = ref([])
        const {remove} = useRemove(todos)
        return {
          todos,
          remove,
          ...useAdd(todos),
          ...useEdit(remove),
          ...useFilter(todos)
        }
      },
       directives:{
        editingFocus:(el,binding)=>{
          binding.value && el.focus()
        }
      }
    }
    

    html代码

        <section class="main" >
          <input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
          <label for="toggle-all">Mark all as complete</label>
          <ul class="todo-list">
            <li 
            v-for="todo in todos"
            :key="todo" 
            :class="{ editing: todo === editingTodo , completed:todo.completed}"
            >
              <div class="view">
                <input class="toggle" type="checkbox" v-model="todo.completed">
                <label @dblclick="editTodo(todo)">{{todo.text}}</label>
                <button class="destroy" @click="remove(todo)"></button>  <!-- 删除按钮 -->
              </div>
              <input  
                class="edit"
                type="text"
                v-editing-focus ="todo=== editingTodo"
                v-model="todo.text"
                @keyup.enter ="doneEdit(todo)"
                @blur="doneEdit(todo)"
                @keyup.esc ="cancleEdit(todo)"
                ><!-- 编辑代办事项的按钮 -->
            </li>
            
          </ul>
        </section>
    

    实现效果图:
    在这里插入图片描述

    6.2 all active completed三个按钮切换

    const useFilter = todos => {
      const allDone = computed({
        get () {
          return !todos.value.filter(todo => !todo.completed).length
        },
        set (value) {
          todos.value.forEach(todo => {
            todo.completed = value
          })
        }
      })
    
      const filter = {
        all: list => list,
        active: list => list.filter(todo => !todo.completed),
        completed: list => list.filter(todo => todo.completed)
      }
      const type = ref('all')
      const filteredTodos = computed(() => filter[type.value](todos.value))
    
      const onHashChange = () => {
        const hash = window.location.hash.replace('#/', '')
        if (filter[hash]) {
          type.value = hash
        } else {
          type.value = 'all'
          window.location.hash = ''
        }
      }
    
      onMounted(() => {
        window.addEventListener('hashchange', onHashChange)
        onHashChange()
      })
    
      onUnmounted(() => {
        window.removeEventListener('hashchange', onHashChange)
      })
    
      return  {
        allDone,
        filteredTodos
      }
    }
    
    
     <li 
            v-for="todo in todos"
            :key="todo" 
            :class="{ editing: todo === editingTodo , completed:todo.completed}"
            >
    

    修改为:

     <li 
            v-for="todo in filteredTodos"
            :key="todo" 
            :class="{ editing: todo === editingTodo , completed:todo.completed}"
            >
    
    <footer class="footer">
          <span class="todo-count">
            item left
          </span>
          <ul class="filters">
            <!-- 不使用路由
              实现:监控地址中hash的变化
              当组件挂载完毕,注册hashchanges事件
              当组件卸载时,将hashchange事件移除
              在hashchange事件中,获取当点锚点的值
             -->
            <li><a href="#/all">All</a></li>
            <li><a href="#/active">Active</a></li>
            <li><a href="#/completed">Completed</a></li>
          </ul>
          <button class="clear-completed">
            Clear completed
          </button>
        </footer>
    

    active:
    在这里插入图片描述
    completed
    在这里插入图片描述
    all
    在这里插入图片描述

    6.3 处理其他问题:个数显示,删除已完成的代办事项,

    • 个数显示:
    // 在useFilter 中加入
    const remainingCount = computed(() => filter.active(todos.value).length)
    // 使用
    return  {
      allDone,
      filteredTodos,
      remainingCount
    }
    

    html中使用:

    <span class="todo-count">
      <strong>{{ remainingCount }}</strong> {{ remainingCount > 1 ? 'items' : 'item' }} left
    </span>
    
    • 删除已经完成的代办事项
    onst useRemove = todos => {
      const remove = todo => {
        const index = todos.value.indexOf(todo)
        todos.value.splice(index, 1)
      }
      const removeCompleted = () => {
        todos.value = todos.value.filter(todo => !todo.completed)
      }
      return {
        remove,
        removeCompleted
      }
    }
    

    html:

     <button class="clear-completed" @click="removeCompleted" >
            Clear completed
          </button>
    

    在这里插入图片描述

    • 没有数据的情况下隐藏 main 和footer
      js
    //在useFilter函数中
    const count  = computed(() => todos.value.length)
    return  {
      allDone,
      count,
      filteredTodos,
      remainingCount
    }
    

    html:
    在这里插入图片描述

    6.4 存储待办事项

    utils/useLocalStorage.js

    function parse (str) {
      let value
      try {
        value = JSON.parse(str)
      } catch {
        value = null
      }
      return value
    }
    
    function stringify (obj) {
      let value
      try {
        value = JSON.stringify(obj)
      } catch {
        value = null
      }
      return value
    }
    
    export default function useLocalStorage () {
      function setItem (key, value) {
        value = stringify(value)
        window.localStorage.setItem(key, value)
      }
    
      function getItem (key) {
        let value = window.localStorage.getItem(key)
        if (value) {
          value = parse(value)
        }
        return value
      }
    
      return {
        setItem,
        getItem
      }
    }
    

    App.vue导入

    import useLocalStorage from './utils/useLocalStorage'
    const storage = useLocalStorage()
    
    //5.存储本地数据
    const useStorage = () => {
      const KEY = 'TODOKEYS'
      const todos = ref(storage.getItem(KEY) || [])  //先从本地数据中加载
      watchEffect(() => {  //监视todos的变化
        storage.setItem(KEY, todos.value)
      })
      return todos
    }
    export default{
      name:'App',
      setup(){
        const todos = useStorage()
        const {remove,removeCompleted} = useRemove(todos)
        return {
          todos,
          remove,
          removeCompleted,
          ...useAdd(todos),
          ...useEdit(remove),
          ...useFilter(todos)
        }
      },
       directives:{
        editingFocus:(el,binding)=>{
          binding.value && el.focus()
        }
      }
    }
    

    四 响应式原理

    1.vuejs响应式回顾

    • Proxy对象实现属性监听
    • 多层属性嵌套,在访问属性过程中处理下一级属性
    • 默认监听动态添加的属性
    • 默认监听属性的删除操作
    • 默认监听数组索引和length属性
    • 可以作为单独的模块使用
      核心方法
    • reactive / ref / toRefs / computed
    • effect
    • track
    • trigger

    2. proxy对象回顾

    'use strict'
        // 问题1: set 和 deleteProperty 中需要返回布尔类型的值
        //        在严格模式下,如果返回 false 的话会出现 Type Error 的异常
        const target = {
          foo: 'xxx',
          bar: 'yyy'
        }
        // Reflect.getPrototypeOf()
        // Object.getPrototypeOf()
        const proxy = new Proxy(target, {
          get (target, key, receiver) {
            // return target[key]
            return Reflect.get(target, key, receiver)
          },
          set (target, key, value, receiver) {
            // target[key] = value
            Reflect.set(target, key, value, receiver)
          },
          deleteProperty (target, key) {
            // delete target[key]
            Reflect.deleteProperty(target, key)
          }
        })
    
        proxy.foo = 'zzz'
        // delete proxy.foo
    
    //若不出现错误则修改为
    	set (target, key, value, receiver) {
            // target[key] = value
            return Reflect.set(target, key, value, receiver)
          },
          deleteProperty (target, key) {
            // delete target[key]
            return Reflect.deleteProperty(target, key)
          }
    

    问题1::错误
    在这里插入图片描述
    Proxy 和 Reflect 中使用的 receiver

    • Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
    • Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver
    const obj = {
          get foo() {
            console.log(this)
            return this.bar
          }
        }
    
        const proxy = new Proxy(obj, {
          get (target, key, receiver) {
            if (key === 'bar') { 
              return 'value - bar' 
            }
            return Reflect.get(target, key)
          }
        })
        console.log(proxy.foo)
    

    打印结果:
    在这里插入图片描述

    js

    const obj = {
          get foo() {
            console.log(this)
            return this.bar
          }
        }
    
        const proxy = new Proxy(obj, {
          get (target, key, receiver) {
            if (key === 'bar') { 
              return 'value - bar' 
            }
            return Reflect.get(target, key, receiver)
          }
        })
        console.log(proxy.foo)
    

    在这里插入图片描述

    3. 响应式原理 reactive

    • reactive 接受一个参数,判断这个参数是否是响应式对象
    • 创建拦截器对象 handler, 设置 get / set / deleteProperty
    • 返回Proxy 对象
      步骤:
    1. 在页面上加载ES Module 的方式来实现,在 index.js 中, 先创建 reactive 的形式,直接导出一个 reactive 函数,它接收一个参数 target,
    2. 在函数中首先要判断是否是对象,如果不是的话直接返回,否则将target 对象转化为代理对象,定义辅助函数isObject用来判断变量是否是对象。
    3. 定义一个handler 对象 里面有 get / set / deleteProperty 方法, 首先在 get 方法中要去收集依赖,然后返回target 对应key 的值,通过 Reflect.get 来获取
    4. 注意: 如果当前key 属性对应的值还是对象,r若是对象,则将它转化为响应式对象, 如果有嵌套对象,会在get 中递归
    5. 在 set 中,首先要去获取get 函数的值,判断值是否相等,不相等则调用Reflect更新值,在 deleteProperty 判断是否有自己的key 属性
    //reactivity/index.js
    const isObject = val => val !== null && typeof val === 'object'
    const convert = target => isObject(target) ? reactive(target) : target
    const hasOwnProperty = Object.prototype.hasOwnProperty
    const hasOwn = (target, key) => hasOwnProperty.call(target, key)
    
    export function reactive (target) {
      if (!isObject(target)) return target
    
      const handler = {
        get (target, key, receiver) {
          // 收集依赖
          console.log('get ',key)
          const result = Reflect.get(target, key, receiver)
          return convert(result)
        },
        set (target, key, value, receiver) {
          const oldValue = Reflect.get(target, key, receiver)
          let result = true
          if (oldValue !== value) {
            result = Reflect.set(target, key, value, receiver)
            // 触发更新
            console.log('set ',key,value)
          }
          return result
        },
        deleteProperty (target, key) {
          const hadKey = hasOwn(target, key)
          const result = Reflect.deleteProperty(target, key)
          if (hadKey && result) {
            // 触发更新
            console.log('deleteProperty',target,key)
          }
          return result
        }
      }
      return new Proxy(target, handler)
    }
    

    测试代码:
    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        <script type="module">
            import { reactive } from './reactivity/index.js'
                const obj = reactive({
                    name: 'zs',
                    age: 18
                })
            obj.name = 'lisi'
            delete obj.age
            console.log(obj)
        </script>
    </body>
    </html>
    

    在这里插入图片描述

    4. 响应式系统原理 - 收集依赖

    通过 reactive 创建了一个响应式对象, effect 接收一个函数,和 watchEffect 用发一样,effect 内部首先会执行一次,当内部响应式数据变化,会再次执行

    在收集依赖过程中会创建三个集合,targetMap、depMaps、dep, targetMap的作用是用来记录目标对象和一个字典,也就是depMaps, targetMap中key也就是目标对象
    在这里插入图片描述

    5.响应式原理 effect / track

    effect 接收一个函数作为参数, 在effect 中首先要执行一次effect,在callback 中会访问响应式对象的属性,在这个过程中去收集依赖在收集依赖中,需要将callback 存储起来,定义一个activeEffect来存储callback,收集完毕,需要将 activeEffect `值设为初始值,因为在收集依赖的过程,如果有嵌套属性,是一个递归

    let activeEffect = null
    export function effect (callback) {
      activeEffect = callback
      callback() // 访问响应式对象属性,去收集依赖
      activeEffect = null
    }
    
    let targetMap = new WeakMap()
    export function track (target, key) {
      if (!activeEffect) return
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
      }
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, (dep = new Set()))
      }
      dep.add(activeEffect)
    }
    

    在代理对象中调用这个函数, reactive get

    get (target, key, receiver) {
          // 收集依赖
          track(target, key)
          console.log('get ',key)
          const result = Reflect.get(target, key, receiver)
          return convert(result)
       }
    

    6.响应式原理 触发更新 trigger

    export function trigger (target, key) {
      const depsMap = targetMap.get(target)
      if (!depsMap) return
      const dep = depsMap.get(key)
      if (dep) {
        dep.forEach(effect => {
          effect()
        })
      }
    }
    

    在 reactive set 中触发更新 调用trigger, deleteProperty 调用

    set (target, key, value, receiver) {
          const oldValue = Reflect.get(target, key, receiver)
          let result = true
          if (oldValue !== value) {
            result = Reflect.set(target, key, value, receiver)
            // 触发更新
            trigger(target,key)
            console.log('set ',key,value)
          }
          return result
        },
        deleteProperty (target, key) {
          const hadKey = hasOwn(target, key)
          const result = Reflect.deleteProperty(target, key)
          if (hadKey && result) {
            // 触发更新
            trigger(target,key)
            console.log('deleteProperty',target,key)
          }
          return result
        }
    

    html 测试:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <script type="module">
        import { reactive, effect } from './reactivity/index.js'
    
        const product = reactive({
          name: 'iPhone',
          price: 5000,
          count: 3
        })
        let total = 0 
        effect(() => {
          total = product.price * product.count
        })
        console.log(total)
    
        product.price = 4000
        console.log(total)
    
        product.count = 1
        console.log(total)
    
      </script>
    </body>
    </html>
    

    在这里插入图片描述

    7.响应式原理- Ref

    接收一个参数,可以是原始值,也可以是对象,
    如果传入的是对象,并且是ref创建的对象,则直接返回
    如果是普通对象,内部会调用reactive 创建响应式对象,
    否则创建一个只有value 属性对象返回

    export function ref (raw) {
      // 判断 raw 是否是ref 创建的对象,如果是的话直接返回
      if (isObject(raw) && raw.__v_isRef) {
        return
      }
      let value = convert(raw)
      const r = {
        __v_isRef: true,
        get value () {
          track(r, 'value')
          return value
        },
        set value (newValue) {
          if (newValue !== value) {
            raw = newValue
            value = convert(raw)
            trigger(r, 'value')
          }
        }
      }
      return r
    }
    

    测试:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <script type="module">
        import { reactive, effect, ref } from './reactivity/index.js'
    
        const price = ref(5000)
        const count = ref(3)
       
        let total = 0 
        effect(() => {
          total = price.value * count.value
        })
        console.log(total)
    
        price.value = 4000
        console.log(total)
        count.value = 1
        console.log(total)
    
      </script>
    </body>
    </html>
    

    结果:
    在这里插入图片描述
    总结:
    reactive 和 ref

    • ref 可以把基本数据类型数据,转成响应式对象
    • ref 返回的对象,重新赋值成对象也是响应式的
    • reactive 返回的对象,重新赋值丢失响应式
    • reactive 返回的对象不可以解构

    如果一个对象成员非常多的时候,使用ref 并不方便,因为总要带着value 属性,如果一个函数内部只有一个响应式数据,这个时候使用ref 会比较方便,因为可以解构返回
    在这里插入图片描述

    8.响应式系统原理- toRefs

    函数的作用:函数接收一个reactive返回的响应式对象,也就是一个Proxy对象,如果传入的参数不是reactive创建的响应式对象,则直接返回,然后把传入对象的所有属性转化为一个类似于ref返回的对象,把转化后的属性挂载到一个新的对象上返回

    export function toRefs (proxy) {
      const ret = proxy instanceof Array ? new Array(proxy.length) : {}
      for (const key in proxy) {
        ret[key] = toProxyRef(proxy, key)
      }
      return ret
    }
    function toProxyRef (proxy, key) {
      const r = {
        __v_isRef: true,
        get value () {
          return proxy[key]
        },
        set value (newValue) {
          proxy[key] = newValue
        }
      }
      return r
    }
    

    测试:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <script type="module">
        import { reactive, effect, toRefs } from './reactivity/index.js'
    
        function useProduct () {
          const product = reactive({
            name: 'iPhone',
            price: 5000,
            count: 3
          })
          
          return toRefs(product)
        }
    
        const { price, count } = useProduct()
    
    
        let total = 0 
        effect(() => {
          total = price.value * count.value
        })
        console.log(total)
    
        price.value = 4000
        console.log(total)
    
        count.value = 1
        console.log(total)
    
      </script>
    </body>
    </html>
    

    在这里插入图片描述

    9.响应式系统原理- computed

    export function computed (getter) {
      const result = ref()
      effect(() => (result.value = getter()))
      return result
    }
    

    测试数据

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <script type="module">
        import { reactive, effect, computed } from './reactivity/index.js'
    
        const product = reactive({
          name: 'iPhone',
          price: 5000,
          count: 3
        })
        let total = computed(() => {
          return product.price * product.count
        })
       
        console.log(total.value)
    
        product.price = 4000
        console.log(total.value)
    
        product.count = 1
        console.log(total.value)
    
      </script>
    </body>
    </html>
    

    五 Vite

    1. Vite的概念

    • Vite 是一个面向现代化浏览器的一个更轻、更快的 web 应用应用开发工具
    • 它基于 ECMAScript 标准原生模块系统(ES Module)实现的

    它的出现是为了解决 Webpack 在开发阶段,使用 webpack-dev-server 冷启动时间过长和 Webpack MHR 热更新反应慢的问题。

    使用 Vite 创建的项目,默认就是一个普通的 Vue3应用,相比于 Vue CLI 创建的项目,会少了很多文件和依赖。

    2. Vite 项目依赖

    Vite 创建的默认项目,开发依赖很少也很简单,只包含了:

    • Vite
    • @vue/compiler-sfc(用来编译.vue 结尾的单文件文件)

    需要注意的是,Vite 目前创建的 Vue 项目只支持 3.0 的版本。在创建项目的时候,通过指定不同的模板,也可以创建其他框架的项目。

    3. Vite 提供的命令

    Vite 提供的命令

    • vite serve

    工作原理
    用于启动一个开发的 web 服务器,在启动服务器的时候不需要编译所有的模块启动速度非常的快。
    图示
    在这里插入图片描述
    在运行 vite serve 的时候,不需要打包,直接开启了一个 web 服务器。当浏览器请求服务器时,例如是一个 css,或者是一个单文件组件,这个时候在服务器会把这个浏览器请求的文件先编译,然后直接把编译后的结果返回给浏览器。
    这里的编译是在服务器端,并且,模块的处理是在请求到服务器端处理的。

    我们来回顾一下,Vue CLI 创建的应用
    在这里插入图片描述
    Vue CLI 创建的项目启动 web 服务器用的是 vue-cli-service,当运行它的时候,它内部会使用 Webpack 去打包所有的模块(如果模块很多的情况下,编译的速度会很慢),打包完成后会将编译好的模块存储到内存中,然后启动一个 web 服务器,浏览器请求 web 服务器,最后才会从内存中把编译好的内容,返回到浏览器。

    Webpack 这样的工具,它的做法是将所有的模块提前都编译打包进内存里,不管模块是否被执行是否被调用,它会都打包编译,随着项目越来越大,打包后的内容也会越来越大,打包的速度也会越来越慢。

    Vite 使用现代化浏览器原生支持的 ES Module 模块化的特性,省略了模块的打包环节。对于需要编译的文件,例如样式模块和单文件组件等,vite 采用了即时编译,也就是说当加载到这个文件的时候,才会去服务端编译好这个文件。

    所有,这种即时编译的好处体现在按需编译,速度会更快。
    HMR

    • Vite HMR
      立即编译当前所修改的文件
    • Webpack HMR
      会自动以这个文件为入口重新编译一次,所有的涉及到的依赖也会被加载一次

    Vite 默认也支持 HMR 模块热更新,相对于 Webpack 中的 HMR 效果会更好,因为 Webpack 的 HMR 模块热跟新会从你修改的文件开始全部在编译一遍

    • vite build

    • Rollup
    • Dynamic import
      • Polyfill
        Vite 创建的项目使用 Vite build 进行生产模式的打包,这个命令内部使用过的是 Rollup 打包,最终也是把文件都打包编译在一起。对于代码切割的需求,Vite 内部采用的是原生的动态导入的方式实现的,所以打包的结果只能支持现代化的浏览器(不支持 ie)。不过相对应的 Polyfill 可以解决

    是否还需要打包?

    随着 Vite 的出现,我们需要考虑一个问题,是否还必要打包应用。之前我们使用 Webpack 进行打包,会把所有的模块都打包进 bundle.js 中,主要有两个原因:

    • 浏览器环境对原生 ES Module 的支持
    • 零零散散的模块文件会产生大量的 HTTP 请求

    但是,现在目前大部分的浏览器都已经支持了 ES Module。并且我们也可以使用 HTTP2 长链接去解决大量的 HTTP 请求。那是否还需要对应用进行打包,取决于你的团队和项目应用的运行环境。

    浏览器对ES Module的支持

    在这里插入图片描述
    IE不支持esModule

    • TypeScript - 内置支持
    • less/sass/stylus/postcss - 内置支持(需要单独安装)
    • JSX
    • Web Assemby

    Vite 的特性

    • 快速冷启动
    • 模块热更新
    • 按需编译
    • 开箱即用

    4. 实现Vite

    4.1 vite 的工作原理,分为以下五个步骤

    • 静态 web 服务器
    • 编译单文件组件
      • 拦截浏览器不识别的模块,并处理
    • HMR(通过 WebSocket 实现,跳过)

    5. 静态 web 服务器

    vite 内部使用过的是 koa 来开启静态服务器的,这里我们也使用 koa 来开启一个静态服务器,把当前运行的目录作为静态服务器的根目录

    创建一个名为 vite-cli 的空文件夹,进入该文件夹初始化 package.json,并且安装 koakoa-send

    mkdir vite-cli
    cd vite-cli
    npm init --yes
    npm i koa koa-send
    

    在 package.json 来配置 bin 字段:
    在这里插入图片描述
    新建 index.js 文件,并且在第一行配置 node 的运行环境(因为我们要开发的是一个基于 node 的命令行工具,所以要指定运行 node 的位置)

    #!/usr/bin/env node
    

    基于 koa 启动一个 web 静态服务器:

    #!/usr/bin/env node
    const Koa = require('koa')
    const send = require('koa-send')
    
    const app = new Koa()
    
    // 1.开启静态文件服务器
    app.use(async (ctx, next) => {
      await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
     await next()
    })
    
    app.listen(3000)
    console.log('Serve running @ http://localhost:3000')
    

    接着,使用 npm link 到全局,然后打开一个使用 vue3 写的项目(可以用 vite 创建一个默认项目),进入命令行终端,输入 vite-cli。如果没有报错的话,会打印出"Serve running @ http://localhost:3000"这句话,我们打开浏览器,打开这个网址。

    不过是一片空白的,接着我们打开 F12,会看到一个报错,报错的信息的意思是,解析 vue 模块的时候失败了,使用 import 导入模块的时候,模块的开头必须是 “/”, “./”, or “…/” 这三种其中的一个。
    在这里插入图片描述
    我们来做一个对比,我们把使用 vite 创建的项目启动后, vite-cli 创建的项目启动后的 main.js 在浏览器响应中的区别:
    vite:
    在这里插入图片描述
    vite-cli:
    在这里插入图片描述
    通过上面两幅图的对比,你会发现,vite 它会处理这个模块引入的路径,它会加载一个不存在的路径 @modules,并且请求这个路径的 js 文件也是可以请求成功的。

    这是 vite 创建的项目启动后的 vue.js 的请求,观察响应头中的 Content-Type 字段,他是 application/javascript;所以我们可以通过这个类型,在返回的时候去处理这个 js 中的第三方路劲问题。
    在这里插入图片描述

    6. 修改第三方模块的路径

    "/", "./", or "../" 开头的引用,全部替换成 “/@modules/”

    const streamToString = stream => new Promise((resolve, reject) => {
      const chunks = []
      stream.on('data', chunk => chunks.push(chunk))
      stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
      stream.on('error', reject)
    })
    
    const stringToStream = text => {
      const stream = new Readable()
      stream.push(text)
      stream.push(null)
      return stream
    }
    // 2. 修改第三方模块的路径
    app.use(async (ctx, next) => {
        if (ctx.type === 'application/javascript') {
          const contents = await streamToString(ctx.body)
          // import vue from 'vue'
          // import App from './App.vue'
        /**
         * 这里进行分组的全局匹配
         * 第一个分组匹配以下内容:
         *  from 匹配 from
         *  \s+ 匹配空格
         *  ['"]匹配单引号或者是双引号
         * 第二个分组匹配以下内容:
         *  ?! 不匹配这个分组的结果
         *  \.\/ 匹配 ./
         * $1表示第一个分组的结果
         */
    
          ctx.body = contents
            .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
        }
      })
    

    7. 加载第三方模块

    现在我们要做的是,将 /@modules/ 开头的引用,去 node_modules 中找到并且替换它的返回内容。我们需要在创建一个中间件,这个中间件需要在创建静态服务器之前被调用

    const path = require('path')
    
    // 3.加载第三方模块
    app.use(async (ctx, next) => {
      // ctx.path --> /@modules/vue
      // 判断第三方模块的路径是否有/@modules/开头
      if (ctx.path.startsWith('/@modules/')) {
        // 对字符串进行截取,获取到模块名称
        const moduleName = ctx.path.substr(10)
        // 找到该模块名称在node_moduls中的package.json路径
        const pkgPath = path.join(
          process.cwd(),
          'node_modules',
          moduleName,
          'package.json'
        )
        // 通过require加载当前package.json
        const pkg = require(pkgPath)
        // 将内容替换成node_modules中的内容
        ctx.path = path.join('/node_modules', moduleName, pkg.module)
      }
      // 返回执行下一个中间件
      await next()
    })
    
    

    编写完后,我们需要重新启动一下服务器,启动完成后,我们重新打开 network 网络面板,看看 vue 这个模块是否被加载了进来。
    在这里插入图片描述
    我们看到,vue 这个模块已经被加载进来了。

    但是,我们发现 /@modules/@vue/runtime-dom 和 /@modules/@vue/shared 却没有被加载进来,并且控制台却报了两个错误:加载模块 App.vue 和 index.css 失败

    8. 编译单文件组件

    原本的 vite 启动后,查看 单文件夹组件的请求是如何处理的
    在这里插入图片描述
    我们来看 app.vue 的响应内容,它引入了一些组件,然后把它编译成一个选项对象,然后它又去加载了 app.vue 并且在后面加上了一个参数 type=template,并且解构出了一个 render 函数,然后把 render 函数挂载到选项对象上,然后又设置了两个属性(这两个属性不模拟),最后导出这个选项对象。

    从这段代码我们可以观察到,当请求到单文件组件的时候,服务器会来编译这个单文件组件,并把相对应的结果返回给浏览器。

    我们在来编写一个中间件,在编写中间件的时候,我们需要安装一个模块 @vue/compiler-sfc 并且导入,这个模块的作用主要是编译单文件组件的。

    // 4. 处理单文件组件
    const { Readable } = require('stream')
    const compilerSFC = require('@vue/compiler-sfc')
    
    // 4. 处理单文件组件
    app.use(async (ctx, next) => {
      if (ctx.path.endsWith('.vue')) {
        const contents = await streamToString(ctx.body)
        const { descriptor } = compilerSFC.parse(contents)
        let code
        if (!ctx.query.type) {
          code = descriptor.script.content
          // console.log(code)
          code = code.replace(/export\s+default\s+/g, 'const __script = ')
          code += `
          import { render as __render } from "${ctx.path}?type=template"
          __script.render = __render
          export default __script
          `
        } else if (ctx.query.type === 'template') {
          const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
          code = templateRender.code
        }
        ctx.type = 'application/javascript'
        ctx.body = stringToStream(code)
      }
      await next()
    })
    

    然后,重启一下服务,需要注意的是,需要把图片和其他和 js 或者 vue 无关的文件都注释掉,因为我们这里只处理了 vue 文件。

    9. 完成版代码

    #!/usr/bin/env node
    const path = require('path')
    const { Readable } = require('stream')
    const Koa = require('koa')
    const send = require('koa-send')
    const compilerSFC = require('@vue/compiler-sfc')
    
    const app = new Koa()
    
    const streamToString = stream => new Promise((resolve, reject) => {
      const chunks = []
      stream.on('data', chunk => chunks.push(chunk))
      stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
      stream.on('error', reject)
    })
    
    const stringToStream = text => {
      const stream = new Readable()
      stream.push(text)
      stream.push(null)
      return stream
    }
    
    // 3. 加载第三方模块
    app.use(async (ctx, next) => {
      // ctx.path --> /@modules/vue
      if (ctx.path.startsWith('/@modules/')) {
        const moduleName = ctx.path.substr(10)
        const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
        const pkg = require(pkgPath)
        ctx.path = path.join('/node_modules', moduleName, pkg.module)
      }
      await next()
    })
    
    // 1. 静态文件服务器
    app.use(async (ctx, next) => {
      await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
      await next()
    })
    
    // 4. 处理单文件组件
    app.use(async (ctx, next) => {
      if (ctx.path.endsWith('.vue')) {
        const contents = await streamToString(ctx.body)
        const { descriptor } = compilerSFC.parse(contents)
        let code
        if (!ctx.query.type) {
          code = descriptor.script.content
          // console.log(code)
          code = code.replace(/export\s+default\s+/g, 'const __script = ')
          code += `
          import { render as __render } from "${ctx.path}?type=template"
          __script.render = __render
          export default __script
          `
        } else if (ctx.query.type === 'template') {
          const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
          code = templateRender.code
        }
        ctx.type = 'application/javascript'
        ctx.body = stringToStream(code)
      }
      await next()
    })
    
    // 2. 修改第三方模块的路径
    app.use(async (ctx, next) => {
      if (ctx.type === 'application/javascript') {
        const contents = await streamToString(ctx.body)
        // import vue from 'vue'
        // import App from './App.vue'
        ctx.body = contents
          .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
          .replace(/process\.env\.NODE_ENV/g, '"development"')
      }
    })
    
    app.listen(3000)
    console.log('Server running @ http://localhost:3000')
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值