V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
GrapeCityChina
V2EX  ›  推广

Vue 3 组件开发:搭建基于 SpreadJS 的表格编辑系统(环境搭建)

  •  
  •   GrapeCityChina · 2021-01-05 15:47:50 +08:00 · 1840 次点击
    这是一个创建于 1178 天前的主题,其中的信息可能已经有所发展或是发生改变。

    image.png

    Vue 是一套用于构建用户界面的渐进式框架,与其它大型 JS 框架不同,Vue 被设计为可以自底向上逐层应用,更易上手,还便于与第三方库或既有项目整合,因此,Vue 完全能够为复杂的单页应用提供驱动。

    2020 年 09 月 18 日,Vue.js 3.0 正式发布,作者尤雨溪将其描述为:更快、更小、更易于维护。

    Vue 3 都加入了哪些新功能?

    本次发布,Vue 框架本身迎来了多项更新,如 Vue 此前的反应系统是使用 Object.defineProperty 的 getter 和 setter 。 但是,在 Vue 3 中,将使用 ES2015 Proxy 作为其观察者机制,这样做的好处是消除了以前存在的警告,使速度加倍,并节省了一半的内存开销。

    除了基于 Proxy 的观察者机制,Vue 3 的其他新特性还包括:

    1. Performance (性能提升)

    在 Vue 2 中,当某个 DOM 需要更新时,需要遍历整个虚拟 DOM 树才能判断更新点。而在 Vue 3 中,无需此项操作,仅需通过静态标记,对比虚拟节点上带有 patch flag 的节点,即可定位更新位置。

    对比 Vue 2 和 Vue 3 的性能差异,官方文档中给出了具体数据说明:

    · SSR 速度提高了 2~3 倍

    · Update 性能提高 1.3~2 倍

    2. Composition API (组合 API )

    Vue 2 中有 data 、methods 、mounted 等存储数据和方法的对象,我们对此应该不陌生了。比如说要实现一个轮播图的功能,首先需要在 data 里定义与此功能相关的数据,在 methods 里定义该功能的方法,在 mounted 里定义进入页面自动开启轮播的代码…… 有一个显而易见的问题,就是同一个功能的代码却要分散在页面的不同地方,维护起来会相当麻烦。

    为了解决上述问题,Vue 3 推出了具备清晰的代码结构,并可消除重复逻辑的 Composition API,以及两个全新的函数 setup 和 ref 。

    Setup 函数可将属性和方法返回到模板,在组件初始化的时候执行,其效果类似于 Vue 2 中的 beforeCreate 和 created 。如果想使用 setup 里的数据,需要将值 return 出来,没有从 setup 函数返回的内容在模板中不可用。

    Ref 函数的作用是创建一个引用值,主要是对 String 、Number 、Boolean 的数据响应做引用。

    相对于 Vue 2,Vue 3 的生命周期函数也发生了变更,如下所示:

    · beforeCreate -> 请使用 setup()

    · created -> 请使用 setup()

    · beforeMount -> onBeforeMount

    · mounted -> onMounted

    · beforeUpdate -> onBeforeUpdate

    · updated -> onUpdated

    · beforeDestroy -> onBeforeUnmount

    · destroyed -> onUnmounted

    · errorCaptured -> onErrorCaptured

    需要注意的是,Vue 2 使用生命周期函数时是直接在页面中写入生命周期函数,而在 Vue 3 则直接引用即可:

    import {reactive, ref, onMounted} from 'vue'

    3. Tree shaking support (按需打包模块)

    有人将“Tree shaking”  称之为“摇树优化”,其实就是把无用的模块进行“剪枝”,剪去没有用到的 API,因此“Tree shaking”之后,打包的体积将大幅度减少。

    官方将 Vue 2 和 Vue 3 进行了对比,Vue 2 若只写了 Hello World,且没有用到任何的模块 API,打包后的大小约为 32kb,而 Vue 3 打包后仅有 13.5kb 。         

    4. 全新的脚手架工具:Vite

    Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。

    和 Webpack 相比,具有以下特点:

    · 快速的冷启动,不需要等待打包

    · 即时的热模块更新

    · 真正的按需编译,不用等待整个项目编译完成

    由于完全跳过了打包这个概念,Vite 的出现大大的撼动了 Webpack 的地位,且真正做到了服务器随起随用。看来,连尤大神都难逃“真香”理论。

    image.png

    Vite 究竟有什么魔力?不妨让我们通过实际搭建一款基于 Vue 3 组件的表格编辑系统,亲自体验一把。

    一、环境搭建

    使用 Vite 初始化一个 Vue 3 项目

    1. 执行代码:

    
    $ npm init vite-app <project-name>
    
    $ cd <project-name> //进入项目目录
    
    $ npm install //安装项目所需依赖
    
    $ npm run dev //启动项目
    
    

    我们来看下生成的代码, 因为 vite 会尽可能多地镜像 vue-cli 中的默认配置, 所以,这段代码看上去和 vue-cli 生成的代码没有太大区别。

    ├── index.html
    
    ├── package.json
    
    ├── public
    
    │ └── favicon.ico
    
    └── src
    
     ├── App.vue
    
     ├── assets
    
     │ └── logo.png
    
     ├── components
    
     │ └── HelloWorld.vue
    
     ├── index.css
    
     └── main.js
    

    2. 执行下列命令:

    image.png

    此时如果不通过 npm run dev 来启动项目,而是直接通过浏览器打开 index.html, 会看到下面的报错:

    image.png

    报错的原因:浏览器的 ES module 是通过 http 请求拿到模块的,所以 vite 的一个任务就是启动一个 web server 去代理这些模块,在 vite 里是借用了 koa 来启动的服务。

    export function createServer(config: ServerConfig): Server {
      // ...
      const app = new Koa<State, Context>()
      const server = resolveServer(config, app.callback())
      
      // ...
      const listen = server.listen.bind(server)
      server.listen = (async (...args: any[]) => {
        if (optimizeDeps.auto !== false) {
          await require('../optimizer').optimizeDeps(config)
        }
        return listen(...args)
      }) as any
      
      return server
    }
    

    由于浏览器中的 ESM 是获取不到导入的模块内容的,需要借助 Webpack 等工具,如果我们没有引用相对路径的模块,而是引用 node_modules,并直接 import xxx from 'xxx',浏览器便无法得知你项目里有 node_modules,只能通过相对路径或者绝对路径去寻找模块。

    这便是 vite 的实现核心:拦截浏览器对模块的请求并返回处理后的结果(关于 vite 的实现机制,文末会深入讲解)。

    3. 生成项目结构:

    image.png

    入口 index.html 和 main.js 代码结构为:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <link rel="icon" href="/favicon.ico" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Vite App</title>
    </head>
    <body>
      <div id="app"></div>
      <script type="module" src="/src/main.js"></script>
    </body>
    </html>
    
    // main.js
    // 只是引用的是最新的 vue3 语法,其余相同
    import { createApp } from 'vue'
    import App from './App.vue'
    import './index.css'
    
    createApp(App).mount('#app')
    
    

    4. 进入项目目录:cd myVue3 

    5. 安装相关模块:npm install

    image.png

    6. 下载模块:

    image.png

    7. 启动项目:npm run dev

    image.png

    8. 进入地址,当我们看到这个页面时,说明项目已经成功启动了。

    image.png

    Vite 的实现机制

    1. /@module/ 前缀

    对比工程下的 main.js 和开发环境下实际加载的 main.js ,可以发现代码发生了变化。

    工程下的 main.js:

    import { createApp } from 'vue'
    import App from './App.vue'
    import './index.css'
    
    createApp(App).mount('#app')
    
    

    实际加载的 main.js:

    import { createApp } from '/@modules/vue.js'
    import App from '/src/App.vue'
    import '/src/index.css?import'
    
    createApp(App).mount('#app')
    
    

    为了解决 import xxx from 'xxx' 报错的问题,vite 对这种资源路径做了统一处理,即添加一个 /@module/前缀。

    在 src/node/server/serverPluginModuleRewrite.ts 源码的 koa 中间件里可以看到 vite 对 import 做了一层处理,其过程如下:

    · 在 koa 中间件里获取请求 body

    · 通过 es-module-lexer 解析资源 ast 拿到 import 的内容

    · 判断 import 的资源是否是绝对路径,绝对视为 npm 模块

    · 返回处理后的资源路径:"vue" => "/@modules/vue"

    2. 支持 /@module/

    在 /src/node/server/serverPluginModuleResolve.ts 里可以看到大概的处理逻辑:

    · 在 koa 中间件里获取请求 body

    · 判断路径是否以 /@module/ 开头,如果是取出包名

    · 去 node_module 里找到这个库,基于 package.json 返回对应的内容

    3. 文件编译

    通过前文,我们知道了 js module 的处理过程,对于 vue 、css 、ts 等文件,其又是如何处理的呢?

    以 vue 文件为例,在 webpack 里使用 vue-loader 对单文件组件进行编译,在这里 vite 同样拦截了对模块的请求并执行了一个实时编译。

    通过工程下的 App.vue 和实际加载的 App.vue ,便发现改变。

    工程下的 App.vue:

    <template>
      <img alt="Vue logo" src="./assets/logo.png" />
      <HelloWorld msg="Hello Vue 3.0 + Vite" />
    </template>
    
    <script>
    import HelloWorld from './components/HelloWorld.vue';
    
    export default {
      name: 'App',
      components: {
        HelloWorld,
      },
    };
    </script>
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>
    
    

    实际加载的 App.vue:

    import HelloWorld from '/src/components/HelloWorld.vue';
    
    const __script = {
        name: 'App',
        components: {
            HelloWorld,
        },
    };
    
    import "/src/App.vue?type=style&index=0&t=1592811240845"
    import {render as __render} from "/src/App.vue?type=template&t=1592811240845"
    __script.render = __render
    __script.__hmrId = "/src/App.vue"
    __script.__file = "/Users/wang/qdcares/test/vite-demo/src/App.vue"
    export default __script
    
    

    可见,一个 .vue 文件被拆成了三个请求(分别对应 script 、style 和 template ) ,浏览器会先收到包含 script 逻辑的 App.vue 的响应,然后解析到 template 和 style 的路径后,再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。

    // App.vue?type=style
    import { updateStyle } from "/vite/hmr"
    const css = "\n#app {\n  font-family: Avenir, Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  text-align: center;\n  color: #2c3e50;\n  margin-top: 60px;\n}\n"
    updateStyle("7ac74a55-0", css)
    export default css
    
    // App.vue?type=template
    import {createVNode as _createVNode, resolveComponent as _resolveComponent, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock} from "/@modules/vue.js"
    
    const _hoisted_1 = /*#__PURE__*/
    _createVNode("img", {
        alt: "Vue logo",
        src: "/src/assets/logo.png"
    }, null, -1 /* HOISTED */
    )
    
    export function render(_ctx, _cache) {
        const _component_HelloWorld = _resolveComponent("HelloWorld")
    
        return (_openBlock(),
        _createBlock(_Fragment, null, [_hoisted_1, _createVNode(_component_HelloWorld, {
            msg: "Hello Vue 3.0 + Vite"
        })], 64 /* STABLE_FRAGMENT */
        ))
    }
    

    vite 对于其他的类型文件的处理几乎都是类似的逻辑,即根据请求的不同文件类型,做出不同的编译处理结果。

    扩展阅读

    · Vue 3 组件开发实战:搭建基于 SpreadJS 的表格编辑系统(组件集成篇)

    · Vue 3 组件开发实战:搭建基于 SpreadJS 的表格编辑系统(功能拓展篇)

    · SpreadJS Vue 框架支持

    3 条回复    2021-01-26 15:06:08 +08:00
    dengshen
        1
    dengshen  
       2021-01-05 16:38:01 +08:00 via iPhone
    优秀
    anson2017
        2
    anson2017  
       2021-01-26 09:29:47 +08:00
    不错.. vite 确实很快
    robinlovemaggie
        3
    robinlovemaggie  
       2021-01-26 15:06:08 +08:00
    Vite 轻量快是必须的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1202 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 23:13 · PVG 07:13 · LAX 16:13 · JFK 19:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.