暂时未有相关云产品技术能力~
highlight: vs2015theme: juejin前言原文来自 我的个人博客自上一章我们成功构建了 h 函数创建 VNode 后,这一章的目标就是要在 VNode 的基础上构建 renderer 渲染器。根据上一章的描述,我们知道在 packages/runtime-core/src/renderer.ts 中存放渲染器相关的内容。Vue 提供了一个 baseCreateRenderer 的函数(这个函数很长有 2000 多行代码~),它会返回一个对象,我们把返回的这个对象叫做 renderer 渲染器。对于该对象而言,提供了三个方法:render:渲染函数hydrate:服务端渲染相关createApp:初始化方法因为这里代码实在太长了,所以我们将会以下面两个思想来阅读以及实现:阅读:没有使用的代码就当做不存在实现:用最少的代码来实现接下来就让我们开始吧,Here we go~1. 案例分析我们依然从上一章的测试案例开始讲:<script> const { h, render } = Vue const vnode = h( 'div', { class: 'test' }, 'hello render' ) console.log(vnode) render(vnode, document.querySelector('#app')) </script>上一章中我们跟踪了 h 函数的创建,但是并没有提 render 函数。实际上在 h 函数创建了 VNode 后,就是通过 render 渲染函数将 VNode 渲染成真实 DOM 的。至于其内部究竟是如何工作的,我们从源码中去找答案吧~2. 源码阅读:初见 render 函数,ELEMENT 的挂载操作我们直接到源码 packages/runtime-core/src/renderer.ts 的第 2327 行进行debugger:可以看到 render 函数内部很简单,对 vnode 进行判断是否为 null,此时我们的vnode 是从 h 函数得到的 vnode 肯定不为空,所以会执行 patch 方法,最后将 vnode 赋值到 container._vnode 上。我们进入到 patch 方法。patch 的是贴片、补丁的意思,在这里 patch 表示 更新 节点。这里传递的参数我们主要关注 前三个。container._vnode 表示 旧节点(n1),vnode 表示 新节点(n2),container 表示 容器。我们进入 patch 方法:上图讲得很明白了,我们进入 processElement 方法:因为当前为 挂载操作,所以 没有旧节点,即:n1 === null,进入 mountElement 方法:在 mountElement 方法中,代码首先会进入到 hostCreateElement 方法中,根据上图我们也知道,hostCreateElement 方法实际上就是调用了 document.createElement 方法创建了 Element 并返回,但是有个点可以提的是,这个方法在 packages/runtime-dom/src/nodeOps.ts,我们之前调试的代码都在 packages/runtime-core/src/renderer.ts。这是因为 vue 为了保持兼容性,把所有和浏览器相关的 API 封装到了 runtime-dom 中。此时 el 和 vnode.el 的值为 createElement 生成的 div 实例。我们代码接着往下跑:进入 hostSetElementText,而 hostSetElementText 实际上就是执行 el.textContent = text,hostSetElementText 同样 在 packages/runtime-dom/src/nodeOps.ts 中(和浏览器有关的 API 都在 runtime-dom,下面不再将)。我们接着调试:因为此时我们的 prop 有值, 所以会进入这个 for 循环,看上面的图应该很明白了,就是添加了 class 属性,接着程序跳出 patchClass ,跳出 patchProp ,跳出 for 循环,if 结束。如果此时触发 div 的 outerHTML 方法,就会得到 <div class="test">hello render</div>到现在 dom 已经构建好了,最后就只剩下 挂载 操作了继续执行代码将进入 hostInsert(el, container, anchor) 方法:可以看到 hostInsert 方法就是执行了 insertBefore,而我们知道 insertBefore 可以将 ·dom· 插入到执行节点那么到这里,我们已经成功的把 div 插入到了 dom 树中,执行完成 hostInsert 方法之后,浏览器会出现对应的 div.至此,整个 render 执行完成总结:由以上代码可知:整个挂载 Element | Text_Children 的过程分为以下步骤:触发 patch 方法根据 shapeFlag 的值,判定触发 processElement 方法在 processElement 中,根据 是否存在 旧VNode 来判定触发 挂载 还是 更新 的操作挂载中分成了4大步:生成 div处理 textContent处理 props挂载 dom通过 container._vnode = vnode 赋值 旧 VNode3. 代码实现:构建 renderer 基本架构整个 基本架构 应该分为 三部分 进行处理:renderer 渲染器本身,我们需要构建出 baseCreateRenderer 方法我们知道所有和 dom 的操作都是与 core 分离的,而和 dom 的操作包含了 两部分:Element 操作:比如 insert、createElement 等,这些将被放入到 runtime-dom 中props 操作:比如 设置类名,这些也将被放入到 runtime-dom 中renderer 渲染器本身创建 packages/runtime-core/src/renderer.ts 文件:import { ShapeFlags } from 'packages/shared/src/shapeFlags' import { Fragment } from './vnode' /** * 渲染器配置对象 */ export interface RendererOptions { /** * 为指定 element 的 prop 打补丁 */ patchProp(el: Element, key: string, prevValue: any, nextValue: any): void /** * 为指定的 Element 设置 text */ setElementText(node: Element, text: string): void /** * 插入指定的 el 到 parent 中,anchor 表示插入的位置,即:锚点 */ insert(el, parent: Element, anchor?): void /** * 创建指定的 Element */ createElement(type: string) } /** * 对外暴露的创建渲染器的方法 */ export function createRenderer(options: RendererOptions) { return baseCreateRenderer(options) } /** * 生成 renderer 渲染器 * @param options 兼容性操作配置对象 * @returns */ function baseCreateRenderer(options: RendererOptions): any { /** * 解构 options,获取所有的兼容性方法 */ const { insert: hostInsert, patchProp: hostPatchProp, createElement: hostCreateElement, setElementText: hostSetElementText } = options const patch = (oldVNode, newVNode, container, anchor = null) => { if (oldVNode === newVNode) { return } const { type, shapeFlag } = newVNode switch (type) { case Text: // TODO: Text break case Comment: // TODO: Comment break case Fragment: // TODO: Fragment break default: if (shapeFlag & ShapeFlags.ELEMENT) { // TODO: Element } else if (shapeFlag & ShapeFlags.COMPONENT) { // TODO: 组件 } } } /** * 渲染函数 */ const render = (vnode, container) => { if (vnode == null) { // TODO: 卸载 } else { // 打补丁(包括了挂载和更新) patch(container._vnode || null, vnode, container) } container._vnode = vnode } return { render } } 封装 Element 操作创建 packages/runtime-dom/src/nodeOps.ts 模块,对外暴露 nodeOps 对象:const doc = document export const nodeOps = { /** * 插入指定元素到指定位置 */ insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null) }, /** * 创建指定 Element */ createElement: (tag): Element => { const el = doc.createElement(tag) return el }, /** * 为指定的 element 设置 textContent */ setElementText: (el, text) => { el.textContent = text } } 封装 props 操作创建 packages/runtime-dom/src/patchProp.ts 模块,暴露 patchProp 方法:const doc = document export const nodeOps = { /** * 插入指定元素到指定位置 */ insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null) }, /** * 创建指定 Element */ createElement: (tag): Element => { const el = doc.createElement(tag) return el }, /** * 为指定的 element 设置 textContent */ setElementText: (el, text) => { el.textContent = text } }创建 packages/runtime-dom/src/modules/class.ts 模块,暴露 patchClass 方法:/** * 为 class 打补丁 */ export function patchClass(el: Element, value: string | null) { if (value == null) { el.removeAttribute('class') } else { el.className = value } }在 packages/shared/src/index.ts 中,写入 isOn 方法:const onRE = /^on[^a-z]/ /** * 是否 on 开头 */ export const isOn = (key: string) => onRE.test(key)三大块 全部完成,标记着整个 renderer 架构设计完成。4. 代码实现:基于 renderer 完成 ELEMENT 节点挂载在 packages/runtime-core/src/renderer.ts 中,创建 processElement 方法:/** * Element 的打补丁操作 */ const processElement = (oldVNode, newVNode, container, anchor) => { if (oldVNode == null) { // 挂载操作 mountElement(newVNode, container, anchor) } else { // TODO: 更新操作 } } /** * element 的挂载操作 */ const mountElement = (vnode, container, anchor) => { const { type, props, shapeFlag } = vnode // 创建 element const el = (vnode.el = hostCreateElement(type)) if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 设置 文本子节点 hostSetElementText(el, vnode.children as string) } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // TODO: 设置 Array 子节点 } // 处理 props if (props) { // 遍历 props 对象 for (const key in props) { hostPatchProp(el, key, null, props[key]) } } // 插入 el 到指定的位置 hostInsert(el, container, anchor) } const patch = (oldVNode, newVNode, container, anchor = null) => { if (oldVNode === newVNode) { return } const { type, shapeFlag } = newVNode switch (type) { case Text: // TODO: Text break case Comment: // TODO: Comment break case Fragment: // TODO: Fragment break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement(oldVNode, newVNode, container, anchor) } else if (shapeFlag & ShapeFlags.COMPONENT) { // TODO: 组件 } } }根据源码的逻辑,在这里主要做了五件事情:区分挂载、更新创建 Element设置 text设置 class插入 DOM 树5. 代码实现:合并渲染架构我们知道,在源码中,我们可以直接:const { render } = Vue render(vnode, document.querySelector('#app'))但是在我们现在的代码,发现是 不可以 直接这样导出并使用的。所以这就是本小节要做的 得到可用的 render 函数创建 packages/runtime-dom/src/index.ts:import { createRenderer } from '@vue/runtime-core' import { extend } from '@vue/shared' import { nodeOps } from './nodeOps' import { patchProp } from './patchProp' const rendererOptions = extend({ patchProp }, nodeOps) let renderer function ensureRenderer() { return renderer || (renderer = createRenderer(rendererOptions)) } export const render = (...args) => { ensureRenderer().render(...args) }在 packages/runtime-core/src/index.ts 中导出 createRenderer在 packages/vue/src/index.ts 中导出 render创建测试实例 packages/vue/examples/runtime/render-element.html :`<script> const { h, render } = Vue const vnode = h( 'div', { class: 'test' }, 'hello render' ) console.log(vnode) render(vnode, document.querySelector('#app')) </script>成功渲染出 hello render!
前言终于来到渲染系统啦~在 vue3 渲染系统学习的第一章,我们先来处理 h 函数的构建,关于 h 函数的介绍我这里就不多讲了,具体可以查询文档 h() 以及 创建VNode我们知道 h 函数核心是用来:创建 vnode 的。但是对于 vnode 而言,它存在很多种不同的节点类型。查看 packages/runtime-core/src/renderer.ts 中第 354 行 patch 方法的代码可知,Vue 总共处理了:Text:文本节点Comment:注释节点Static:静态 DOM 节点Fragment:包含多个根节点的模板被表示为一个片段 (fragment)ELEMENT: DOM 节点COMPONENT:组件TELEPORT:新的 内置组件SUSPENSE:新的 内置组件…各种不同类型的节点,而每一种类型的处理都对应着不同的 VNode。所以我们在本章中,就需要把各种类型的 VNode 构建出来(不会全部处理所有类型,只会选择比较有代表性的部分),以便,后面进行 render 渲染。1. 构建 h 函数,处理 ELEMENT + TEXT_CHILDREN老样子,我们从下面这段代码的调试 开始 vue3 的源码阅读<script> const { h } = Vue const vnode = h( 'div', { class: 'test' }, 'hello render' ) console.log(vnode) </script>这段代码很简单,就是使用 h 函数 创建了一个类型为 ELEMENT 子节点为 TEXT 的 vnode。1.1 源码阅读我们直接跳到源码 packages/runtime-core/src/h.ts 中的第 174 行,为 h 函数增加 debugger:通过源码可知,h 函数接收三个参数:type:类型。比如当前的 div 就表示 Element 类型propsOrChildren:props 或者 childrenchildren:子节点而且最终代码将会触发 createVNode 方法,createVNode 方法实际就是调用了 _createVnode 方法 我们进入 _createVNode 方法:3、 这里 _createVNode 对 type 做了一些条件判断,我们的 type 为 div 可以先跳过接着调试:_createVNode 接着对 props 做了 class 和 style 的增强,我们也可以先不管,最终得到 shapeFlag 的值为 1,shapeFlag 为当前的 类型标识: shapeFlag。查看 packages/shared/src/shapeFlags.ts 的代码,根据 enum ShapeFlags 可知:1 代表为 Element即当前 shapeFlag = ShapeFlags.Element,代码继续执行:可以看到 _craeteVNode 最终是调用了 createBaseVNode 方法,我们进入到 createBaseVNode 方法:createBaseVnode 方法首先创建了一个 vnode,此时的 vnode 为上图右侧所示。我们做些简化,剔除对我们无用的属性之后,得到:children: "hello render props: {class: 'test'} shapeFlag: 1 // 表示为 Element type: "div" __v_isVNode: true在 createBaseVnode 中继续执行代码,会进入到 normalizeChildren 的方法中:在 normalizeChildren 的方法中,会执行最后的 else 以及一个 按位或赋值运算 最后得到 shapeFlag 的最终值为 9normalizeChildren 方法 结束, craeteBaseVNode 返回 vnode至此,整个 h 函数执行完成,最终得到的打印有效值为:children: "hello render props: {class: 'test'} shapeFlag: 9 // 表示为 Element | ShapeFlags.TEXT_CHILDREN 的值 type: "div" __v_isVNode: true 总结:h 函数内部本质上只处理了参数的问题createVNode 是生成 vnode 的核心方法在 createVNode 中第一次生成了 shapeFlag = ShapeFlags.ELEMENT,表示为:是一个 Element 类型在 createBaseVNode 中,生成了 vnode 对象,并且对 shapeFlag 的进行 |= 运算,最终得到的 shapeFlag = 9,表示为:元素为 ShapeFlags.ELEMENT,children 为 TEXT1.2 代码实现创建 packages/shared/src/shapeFlags.ts ,写入所有的对应类型:export const enum ShapeFlags { /** * type = Element */ ELEMENT = 1, /** * 函数组件 */ FUNCTIONAL_COMPONENT = 1 << 1, /** * 有状态(响应数据)组件 */ STATEFUL_COMPONENT = 1 << 2, /** * children = Text */ TEXT_CHILDREN = 1 << 3, /** * children = Array */ ARRAY_CHILDREN = 1 << 4, /** * children = slot */ SLOTS_CHILDREN = 1 << 5, /** * 组件:有状态(响应数据)组件 | 函数组件 */ COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT } 创建 packages/runtime-core/src/h.ts ,构建 h 函数:import { isArray, isObject } from '@vue/shared' import { createVNode, isVNode, VNode } from './vnode' export function h(type: any, propsOrChildren?: any, children?: any): VNode { // 获取用户传递的参数数量 const l = arguments.length // 如果用户只传递了两个参数,那么证明第二个参数可能是 props , 也可能是 children if (l === 2) { // 如果 第二个参数是对象,但不是数组。则第二个参数只有两种可能性:1. VNode 2.普通的 props if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { // 如果是 VNode,则 第二个参数代表了 children if (isVNode(propsOrChildren)) { return createVNode(type, null, [propsOrChildren]) } // 如果不是 VNode, 则第二个参数代表了 props return createVNode(type, propsOrChildren, []) } // 如果第二个参数不是单纯的 object,则 第二个参数代表了 props else { return createVNode(type, null, propsOrChildren) } } // 如果用户传递了三个或以上的参数,那么证明第二个参数一定代表了 props else { // 如果参数在三个以上,则从第二个参数开始,把后续所有参数都作为 children if (l > 3) { children = Array.prototype.slice.call(arguments, 2) } // 如果传递的参数只有三个,则 children 是单纯的 children else if (l === 3 && isVNode(children)) { children = [children] } // 触发 createVNode 方法,创建 VNode 实例 return createVNode(type, propsOrChildren, children) } }创建 packages/runtime-core/src/vnode.ts,处理 VNode 类型和 isVNode 函数:export interface VNode { __v_isVNode: true type: any props: any children: any shapeFlag: number } export function isVNode(value: any): value is VNode { return value ? value.__v_isVNode === true : false }在 packages/runtime-core/src/vnode.ts 中,构建 createVNode 函数: /** * 生成一个 VNode 对象,并返回 * @param type vnode.type * @param props 标签属性或自定义属性 * @param children 子节点 * @returns vnode 对象 */ export function createVNode(type, props, children): VNode { // 通过 bit 位处理 shapeFlag 类型 const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0 return createBaseVNode(type, props, children, shapeFlag) } /** * 构建基础 vnode */ function createBaseVNode(type, props, children, shapeFlag) { const vnode = { __v_isVNode: true, type, props, shapeFlag } as VNode normalizeChildren(vnode, children) return vnode } export function normalizeChildren(vnode: VNode, children: unknown) { let type = 0 const { shapeFlag } = vnode if (children == null) { children = null } else if (isArray(children)) { // TODO: array } else if (typeof children === 'object') { // TODO: object } else if (isFunction(children)) { // TODO: function } else { // children 为 string children = String(children) // 为 type 指定 Flags type = ShapeFlags.TEXT_CHILDREN } // 修改 vnode 的 chidlren vnode.children = children // 按位或赋值 vnode.shapeFlag |= type } 在 index 中导出 h 函数下面我们可以创建对应的测试实例,packages/vue/examples/runtime/h-element.html:<script> const { h } = Vue const vnode = h( 'div', { class: 'test' }, 'hello render' ) console.log(vnode) </script>最终打印的结果为:children: "hello render" props: {class: 'test'} shapeFlag: 9 type: "div" __v_isVNode: true至此,我们就已经构建好了:type = Element,children = Text 的 VNode 对象2. 构建 h 函数,处理 ELEMENT + ARRAY_CHILDREN将测试用例改为下面的代码:<script> const { h } = Vue const vnode = h( 'div', { class: 'test' }, [h('p', 'p1'), h('p', 'p2'), h('p', 'p3')] ) console.log(vnode) </script>我们很容易能看出上面的代码执行了四次 h 函数,分别为:h('p', 'p1')h('p', 'p2')h('p', 'p3')以及最外层的 h(...)前三次触发代码的流程和第一个节中相似,我们直接将代码 debugger 到第四次 h 函数2.1 源码阅读此时进入到 _createVNode 时的参数为:代码继续,计算 shapeFlag = 1(与第一节一样)_createVNode 返回一个 createBaseVNode 方法, 进入 createBaseVNodecreateBaseVNode 创建 vnode, 接着执行 normalizeChildren(vnode, children):normalizeChildren 我们之前跟踪过得,这次 vnode.shapeFlag 计算出来是 17。我们最终将不重要的属性剔除,打印出的 vnode 结构为:{ "__v_isVNode": true, "type": "div", "props": { "class": "test" }, "children": [ { "__v_isVNode": true, "type": "p", "children": "p1", "shapeFlag": 9 }, { "__v_isVNode": true, "type": "p", "children": "p2", "shapeFlag": 9 }, { "__v_isVNode": true, "type": "p", "children": "p3", "shapeFlag": 9 } ], "shapeFlag": 17 } 总结处理 ELEMENT + ARRAY_CHILDREN 的过程整体的逻辑并没有变得复杂第一次计算 shapeFlag,依然为 Element第二次计算 shapeFlag,因为 children 为 Array,所以会进入 else if (array) 判断2.2 代码实现根据上一小节的源码阅读可知,ELEMENT + ARRAY_CHILDREN 场景下的处理,我们只需要在 packages/runtime-core/src/vnode.ts 中,处理 isArray 场景即可:在 packages/runtime-core/src/vnode.ts 中,找到 normalizeChildren 方法: else if (isArray(children)) { // TODO: array + type = ShapeFlags.ARRAY_CHILDREN }创建测试实例 packages/vue/examples/runtime/h-element-ArrayChildren.html :<script> const { h } = Vue const vnode = h( 'div', { class: 'test' }, [h('p', 'p1'), h('p', 'p2'), h('p', 'p3')] ) console.log(vnode) </script>2.3 总结到现在我们可以先做一个局部的总结。对于 vnode 而言,我们现在已经知道,它存在一个 shapeFlag 属性,该属性表示了当前 VNode 的 “类型” ,这是一个非常关键的属性,在后面的 render 函数中,还会再次看到它。shapeFlag 分成两部分:createVNode:此处计算 “DOM” 类型,比如 ElementcreateBaseVNode:此处计算 “children” 类型,比如 Text || Array3. 构建 h 函数,处理组件组件是 vue 中非常重要的一个概念,这一小节我们就来分析一下 组件 生成 VNode 的情况。在 vue 中,组件本质上就是 一个对象或一个函数(Function Component)我们这里 不考虑 组件是函数的情况,因为这个比较少见。在 vue3 中,我们可以直接利用 h 函数 + render 函数渲染出一个基本的组件,就像下面这样:<script> const { h, render } = Vue const component = { render() { const vnode1 = h('div', '这是一个 component') console.log(vnode1) return vnode1 } } const vnode2 = h(component) console.log(vnode2) render(vnode2, document.querySelector('#app')) </script>3.1 案例分析在当前代码中共触发了两次 h 函数,第一次是在 component 对象中的 render 函数内,我们可以把 component 对象看成一个组件,实际上在 vue3 中你打印一个组件对象它的内部就有一个 render 函数,下面是我打印的一个 App 组件第二次是在将 component 作为参数生成的 vnode2 时最后将生成的 vnode2 通过 render 渲染函数 渲染到页面上(关于 render 函数我们之后在讲)最终打印的 vnode2 如下图所示:shapeFlag:这个是当前的类型表示,4 表示为一个 组件type:是一个 对象,它的值包含了一个 render 函数,这个就是 component 的 真实渲染 内容__v_isVNode:VNode 标记vnode1:与 ELEMENT + TEXT_CHILDREN 相同{ __v_isVNode: true, type: "div", children: "这是一个 component", shapeFlag: 9 } 总结:那么由此可知,对于 组件 而言,它的一个渲染,与之前不同的地方主要有两个:shapeFlag === 4type:是一个 对象(组件实例),并且包含 render 函数仅此而已,那么依据这样的概念,我们可以通过如下代码,完成同样的渲染:const component = { render() { return { __v_isVNode: true, type: 'div', children: '这是一个 component', shapeFlag: 9 } } } render( { __v_isVNode: true, type: component, shapeFlag: 4 }, document.querySelector('#app') )3.2 代码实现在我们的代码中,处理 shapeFlag 的地方有两个:createVNode:第一次处理,表示 node 类型(比如:Element)createBaseVNode:第二次处理,表示 子节点类型(比如:Text Children)因为我们这里不涉及到子节点,所以我们只需要在 createVNode 中处理即可: // 通过 bit 位处理 shapeFlag 类型 const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : 0此时创建测试实例 packages/vue/examples/runtime/h-component.html:<script> const { h, render } = Vue const component = { render() { const vnode1 = h('div', '这是一个 component') console.log(vnode1) return vnode1 } } const vnode2 = h(component) console.log(vnode2) </script>可以得到相同的打印结果:4. 构建 h 函数,处理 Text / Comment/ Fragment当组件处理完成之后,最后我们来看下 Text 、 Comment、Fragment 这三个场景下的 VNode。 <script> const { h, render, Text, Comment, Fragment } = Vue const vnodeText = h(Text, '这是一个 Text') console.log(vnodeText) // 可以通过 render 进行渲染 render(vnodeText, document.querySelector('#app1')) const vnodeComment = h(Comment, '这是一个 Comment') console.log(vnodeComment) render(vnodeComment, document.querySelector('#app2')) const vnodeFragment = h(Fragment, '这是一个 Fragment') console.log(vnodeFragment) render(vnodeFragment, document.querySelector('#app3')) </script>查看打印:可以看到 Text、Comment、Fragment 三个的 type 分别为 Symbol(Text)、Symbol(Comment)、Symbol(Fragment),还是比较简单的。实现:直接在 packages/runtime-core/src/vnode.ts 中创建三个 Symbol:export const Fragment = Symbol('Fragment') export const Text = Symbol('Text') export const Comment = Symbol('Comment')然后导出即可。创建测试实例 packages/vue/examples/runtime/h-other.html:<script> const { h, Text, Comment, Fragment } = Vue const vnodeText = h(Text, '这是一个 Text') console.log(vnodeText) const vnodeComment = h(Comment, '这是一个 Comment') console.log(vnodeComment) const vnodeFragment = h(Fragment, '这是一个 Fragment') console.log(vnodeFragment) </script>测试打印即可。5. 构建 h 函数,完成虚拟节点下 class 和 style 的增强我们在第一节中有讲过, vue 在 _createVNode 的方法中对 class 和 style 做了专门的增强,使其可以支持 Object 和 Array 。比如说:<script> const { h, render } = Vue const vnode = h( 'div', { class: { red: true } }, '增强的 class' ) render(vnode, document.querySelector('#app')) </script>这样,我们可以得到一个 class: red 的 div。这样的 h 函数,最终得到的 vnode 如下:{ __v_isVNode: true, type: "div", shapeFlag: 9, props: {class: 'red'}, children: "增强的 class" } 由以上的 VNode 可以发现,最终得出的 VNode 与 const vnode = h('div', { class: 'red' }, 'hello render') 是完全相同的。那么 vue 是如何来处理这种增强的呢?我们一起从源码中一探究竟(style 的增强处理与 class 非常相似,所以我们只看 class 即可)5.1 源码阅读我们直接来到在第一节阅读源码有讲过的对 prop 进行处理的地方,也就是 packages/runtime-core/src/vnode.ts 文件中 _createVNode 方法内:执行 props.class = normalizeClass(klass),这里的 normalizeClass 方法就是处理 class 增强的关键,进入 normalizeClass:总结:对于 class 的增强其实还是比较简单的,只是额外对 class 和 style 进行了单独的处理。整体的处理方式也比较简单:针对数组:进行迭代循环针对对象:根据 value 拼接 name5.2 代码实现创建 packages/shared/src/normalizeProp.ts :import { isArray, isObject, isString } from '.' /** * 规范化 class 类,处理 class 的增强 */ export function normalizeClass(value: unknown): string { let res = '' // 判断是否为 string,如果是 string 就不需要专门处理 if (isString(value)) { res = value } // 额外的数组增强。官方案例:https://cn.vuejs.org/guide/essentials/class-and-style.html#binding-to-arrays else if (isArray(value)) { // 循环得到数组中的每个元素,通过 normalizeClass 方法进行迭代处理 for (let i = 0; i < value.length; i++) { const normalized = normalizeClass(value[i]) if (normalized) { res += normalized + ' ' } } } // 额外的对象增强。官方案例:https://cn.vuejs.org/guide/essentials/class-and-style.html#binding-html-classes else if (isObject(value)) { // for in 获取到所有的 key,这里的 key(name) 即为 类名。value 为 boolean 值 for (const name in value as object) { // 把 value 当做 boolean 来看,拼接 name if ((value as object)[name]) { res += name + ' ' } } } // 去左右空格 return res.trim() } 在 packages/runtime-core/src/vnode.ts 的 createVNode 增加判定:if (props) { // 处理 class let { class: klass, style } = props if (klass && !isString(klass)) { props.class = normalizeClass(klass) } }至此代码完成。可以创建 packages/vue/examples/runtime/h-element-class.html 测试用例:<script> const { h, render } = Vue const vnode = h( 'div', { class: { red: true } }, '增强的 class' ) render(vnode, document.querySelector('#app')) </script>打印可以获取到正确的 vnode。6. 总结在本章中,完成了对:ElementComponentTextCommentFragment5 个标签类型的处理。同时处理了:Text ChildrenArray chiLdren两个子节点类型。在这里渲染中,我们可以发现,整个 Vnode 生成,核心的就是几个属性:typechildrenshapeFlag__v_isVNode另外,还完成了 class 的增强逻辑,对于 class 的增强其实是一个额外的 class 和 array 的处理,把复杂数据类型进行解析即可。对于 style 的增强逻辑本质上和 class 的逻辑是一样的所以没有去实现。它的源码是在 packages/shared/src/normalizeProp.ts 中的 normalizeStyle 方法,本身的逻辑也非常简单。
前言对于响应性系统而言,除了前两章接触的 ref 和 reactive 之外,还有另外两个也是我们经常使用到的,那就是:计算属性:computed侦听器:watch本章我们先来实现一下 computed 这个 API1. computed 计算属性计算属性 computed 会 基于其响应式依赖被缓存,并且在依赖的响应式数据发生变化时 重新计算我们来看下面这段代码:<div id="app"></div> <script> const { reactive, computed, effect } = Vue const obj = reactive({ name: '张三' }) const computedObj = computed(() => { return '姓名:' + obj.name }) effect(() => { document.querySelector('#app').innerHTML = computedObj.value }) setTimeout(() => { obj.name = '李四' }, 2000) </script>上面的代码,程序主要执行了 5 个步骤:使用 reactive 创建响应性数据通过 computed 创建计算属性 computedObj,并且触发了 obj 的 getter通过 effect 方法创建 fn 函数在 fn 函数中,触发了 computed 的 getter延迟触发了 obj 的 setter接下来我们将从源码中研究 computed 的实现:2. computed 源码阅读因为研究过了 reactive 的实现,所以我们直接来到 packages/reactivity/src/computed.ts 中的第 84 行,在 computed 函数出打上断点:可以看到 computed 方法其实很简单,主要就是创建并返回了一个 ComputedRefImpl 对象,我们将代码跳转进 ComputedRefImpl 类。在 ComputedRefImpl 的构造函数中 创建了 ReactiveEffect 实例,并且传入了两个参数:getter:触发 computed 函数时,传入的第一个参数匿名函数:当 this._dirty 为 false 时,会触发 triggerRefValue,我们知道 triggerRefValue 会 依次触发依赖 (_dirty 在这里以为 脏 的意思,可以理解为 《依赖的数据发生变化,计算属性就需要重新计算了》)对于 ReactiveEffect 而言,我们之前是有了解过的,生成的实例,我们一般把它叫做 effect,他主要提供两个方法:run 方法:触发 fn,即传入的第一个参数stop 方法:语义上为停止的意思,我这里目前还没有实现至此,我们已经执行完了 computed 函数,我们来总结一下做了什么:定义变量 getter 为我们传入的回调函数生成了 ComputedRefImpl 实例,作为 computed 函数的返回值ComputedRefImpl 内部,利用了 ReactiveEffect 函数,并且传入了 第二个参数当 computed 代码执行完成之后,我们在 effect 中触发了 computed 的 getter:computedObj.value根据我们之前在学习 ref 的时候可知,.value 属性的调用本质上是一个 get value 的函数调用,而 computedObj 作为 computed 的返回值,本质上是 ComputedRefImpl 的实例, 所以此时会触发 ComputedRefImpl 下的 get value 函数。在 get value 中,做了两件事:做了trackRefVale 依赖收集。执行了之前存在 computed 中的函数 () => return '姓名' + obj.name,并返回了结果这里可以提一下第 59 行中的判断条件,_dirty 初始化是 ture(_cacheable 初始化 false),所以会执行这个 if, 在 if 中将 _dirty 改为了 false,也就是说只要不改这个 _dirty,下次再去获取 computedObj.value 值时,不会重新执行 fn 。effect 函数执行完成,页面显示 姓名:张三,延迟两秒之后,会触发 obj.name 即 reactive 的 setter 行为,所以我们可以在 packages/reactivity/src/baseHandlers.ts 中为 set 增加一个断点:可以发现因为之前 oldValue 是张三 ,现在 value 是李四,hasChange 方法为 true,进入到 trigger 方法同样跳过之前相同逻辑,可知,最后会触发:triggerEffects(deps[0], eventInfo) 方法。进入 triggerEffects 方法:这里要注意:因为我们在 ComputedRefImpl 的构造函数中,执行了 this.effect.computed = this,所以此时的 if (effect.computed) 判断将会为 true。此时我们注意看 effects,此时 effect 的值为 ReactiveEffect 的实例,同时 scheduler 存在值;接下来进入 triggerEffect:不知道大家还有没有印象,在 ComputedRefImpl 的构造函数创建 ReactiveEffect 实例时传进去的第二个参数,那个参数就是这里 scheduler。我们进入 scheduler 回调:此时的 _dirty 是 false,所以会执行 triggerRefValue 函数,我们进入 triggerRefValue:triggerRefValue 会再次触发 triggerEffects 依赖触发函数,把当前的 this.dep 作为参数传入。注意此时的 effect 是没有 computed 和 scheduler 属性的。fn 函数的触发,标记着 computedObj.value 触发,而我们知道 computedObj.value 本质上是 get value 函数的触发,所以代码接下来会触发 ComputedRefImpl 的 get value获取到 computedObj.value 后 通过 ocument.querySelector('#app').innerHTML = computedObj.value 修改视图。至此,整个过程结束。梳理一下修改 obj.name 到修改视图的过程:整个事件有 obj.name 开始触发 proxy 实例的 setter执行 trigger,第一次触发依赖注意,此时 effect 包含 scheduler 调度器属性,所以会触发调度器调度器指向 ComputedRefImpl 的构造函数中传入的匿名函数在匿名函数中会:再次触发依赖即:两次触发依赖最后执行 :() => { return '姓名:' + obj.name }得到值作为 computedObj 的值总结:到这里我们基本上了解了 computed 的执行逻辑,里面涉及到了一些我们之前没有了解过的概念,比如 调度器 scheduler ,并且整体的 computed 的流程也相当复杂。对于 computed 而言,整体比较复杂,所以我们将分步进行实现3. 构建 ComputedRefImpl ,读取计算属性的值我们的首先的目标是:构建 ComputedRefImpl 类,创建出 computed 方法,并且能够读取值创建 packages/reactivity/src/computed.ts :import { isFunction } from '@vue/shared' import { Dep } from './dep' import { ReactiveEffect } from './effect' import { trackRefValue } from './ref' /** * 计算属性类 */ export class ComputedRefImpl<T> { public dep?: Dep = undefined private _value!: T public readonly effect: ReactiveEffect<T> public readonly __v_isRef = true constructor(getter) { this.effect = new ReactiveEffect(getter) this.effect.computed = this } get value() { // 触发依赖 trackRefValue(this) // 执行 run 函数 this._value = this.effect.run()! // 返回计算之后的真实值 return this._value } } /** * 计算属性 */ export function computed(getterOrOptions) { let getter // 判断传入的参数是否为一个函数 const onlyGetter = isFunction(getterOrOptions) if (onlyGetter) { // 如果是函数,则赋值给 getter getter = getterOrOptions } const cRef = new ComputedRefImpl(getter) return cRef as any }在 packages/shared/src/index.ts 中,创建工具方法:/** * 是否为一个 function */ export const isFunction = (val: unknown): val is Function => typeof val === 'function'在 packages/reactivity/src/effect.ts 中,为 ReactiveEffect 增加 computed 属性: /** * 存在该属性,则表示当前的 effect 为计算属性的 effect */ computed?: ComputedRefImpl<T>在 packages/reactivity/src/index.ts 和 packages/vue/src/index.ts 导出创建测试实例:packages/vue/examples/reactivity/computed.html: <body> <div id="app"></div> </body> <script> const { reactive, computed, effect } = Vue const obj = reactive({ name: '张三' }) const computedObj = computed(() => { return '姓名:' + obj.name }) effect(() => { document.querySelector('#app').innerHTML = computedObj.value }) setTimeout(() => { obj.name = '李四' }, 2000) </script>此时,我们可以发现,计算属性,可以正常展示。但是: 当 obj.name 发生变化时,我们可以发现浏览器 并不会 跟随变化,即:计算属性并非是响应性的。那么想要完成这一点,我们还需要进行更多的工作才可以。4. 初见调度器,处理脏的状态如果我们想要实现 响应性,那么必须具备两个条件:收集依赖:该操作我们目前已经在 get value 中进行。触发依赖:该操作我们目前尚未完成,而这个也是我们本小节主要需要做的事情。代码实现:在 packages/reactivity/src/computed.ts 中,处理脏状态和 scheduler:export class ComputedRefImpl<T> { ... /** * 脏:为 false 时,表示需要触发依赖。为 true 时表示需要重新执行 run 方法,获取数据。即:数据脏了 */ public _dirty = true constructor(getter) { this.effect = new ReactiveEffect(getter, () => { // 判断当前脏的状态,如果为 false,表示需要《触发依赖》 if (!this._dirty) { // 将脏置为 true,表示 this._dirty = true triggerRefValue(this) } }) this.effect.computed = this } get value() { // 触发依赖 trackRefValue(this) // 判断当前脏的状态,如果为 true ,则表示需要重新执行 run,获取最新数据 if (this._dirty) { this._dirty = false // 执行 run 函数 this._value = this.effect.run()! } // 返回计算之后的真实值 return this._value } } 在 packages/reactivity/src/effect.ts 中,添加 scheduler 的处理:export type EffectScheduler = (...args: any[]) => any /** * 响应性触发依赖时的执行类 */ export class ReactiveEffect<T = any> { /** * 存在该属性,则表示当前的 effect 为计算属性的 effect */ computed?: ComputedRefImpl<T> constructor( public fn: () => T, public scheduler: EffectScheduler | null = null ) {} ... } 最后不要忘记,触发调度器函数/** * 触发指定的依赖 */ export function triggerEffect(effect: ReactiveEffect) { // 存在调度器就执行调度函数 if (effect.scheduler) { effect.scheduler() } // 否则直接执行 run 函数即可 else { effect.run() } } 此时,重新执行测试实例,则发现 computed 已经具备响应性。5. computed 的 缓存问题 和 死循环问题到目前为止,我们的 computed 其实已经具备了响应性,但是还存在一点问题。我们来看下下面的代码5.1 存在的问题我们来看下面的代码:<body> <div id="app"></div> </body> <script> const { reactive, computed, effect } = Vue const obj = reactive({ name: '张三' }) const computedObj = computed(() => { console.log('计算属性执行计算') return '姓名:' + obj.name }) effect(() => { document.querySelector('#app').innerHTML = computedObj.value document.querySelector('#app').innerHTML = computedObj.value }) setTimeout(() => { computedObj.value = '李四' }, 2000) </script>结果报错了:调用了两次 computedObj.value 按理说 computed 只会执行一次才对,但是却提示 超出最大调用堆栈大小。5.2 为什么会出现死循环我们继续从源码中找问题,我们接着从两秒之后的 obj.name = '李四' 开始调试。修改 obj.name = '李四',此时会进行 obj 的依赖处理 trigger 函数中代码继续向下进行,进入 triggerEffects(dep) 方法在 triggerEffects(dep) 方法中,继续进入 triggerEffect(effect)在 triggerEffect 中接收到的 effect,即为刚才查看的 计算属性的 effect此时因为 effect 中存在 scheduler,所以会执行该计算属性的 scheduler 函数在 scheduler 函数中,会触发 triggerRefValue(this)而 triggerRefValue 则会再次触发 triggerEffects。特别注意: 此时 effects 的值为 计算属性实例的 dep:循环 effects,从而再次进入 triggerEffect 中。再次进入 triggerEffect,此时 effect 为 非计算属性的 effect,即 fn 函数(修改 DOM 的函数)因为他 不是 计算属性的 effect ,所以会直接执行 run 方法。而我们知道 run 方法中,其实就是触发了 fn 函数,所以最终会执行:document.querySelector('#app').innerHTML = computedObj.value document.querySelector('#app').innerHTML = computedObj.value但是在这个 fn 函数中,是有触发 computedObj.value 的,而 computedObj.value 其实是触发了 computed 的 get value 方法。那么这次 run 的执行会触发 两次 computed 的 get value第一次进入:进入 computed 的 get value :首先收集依赖接下来检查 dirty 脏的状态,执行 this.effect.run()!获取最新值,返回第二次进入:进入 computed 的 get value :首先收集依赖接下来检查 dirty 脏的状态,因为在上一次中 dirty 已经为 false,所以本次 不会在触发 this.effect.run()!直接返回结束按说代码应该到这里就结束了,但是不要忘记,在刚才我们进入到 triggerEffects 时,effets 是一个数组,内部还存在一个 computed 的 effect,所以代码会 继续 执行,再次来到 triggerEffect 中:此时 effect 为 computed 的 effect:这会导致,再次触发 scheduler,scheduler 中还会再次触发 triggerRefValue,triggerRefValue 又触发 triggerEffects ,再次生成一个新的 effects 包含两个 effect,就像 第五、第六、第七步 一样从而导致 死循环5.3 解决方法想要解决这个死循环的问题,其实比较简单,我们只需要 packages/reactivity/src/effect.ts 中的 triggerEffects 中修改如下代码:export function triggerEffects(dep: Dep) { // 把 dep 构建为一个数组 const effects = isArray(dep) ? dep : [...dep] // 依次触发 // for (const effect of effects) { // triggerEffect(effect) // } // 不在依次触发,而是先触发所有的计算属性依赖,再触发所有的非计算属性依赖 for (const effect of effects) { if (effect.computed) { triggerEffect(effect) } } for (const effect of effects) { if (!effect.computed) { triggerEffect(effect) } } } 查看测试实例的打印,此时 computed 只计算了一次。5.4 解决方法的原理原理就是将具有 computed 属性的 effect 放在前面,先执行有 computed 属性的 effect,再执行没有 computed 属性的 effect第一个执行的有 computed 属性的 effect:第二个执行的没有 computed 属性的 effect:6. 总结计算属性实现的重点:计算属性的实例,本质上是一个 ComputedRefImpl 的实例ComputedRefImpl 中通过 dirty 变量来控制 run 的执行和 triggerRefValue 的触发想要访问计算属性的值,必须通过 .value ,因为它内部和 ref 一样是通过 get value 来进行实现的每次 .value 时都会触发 trackRefValue 即:收集依赖在依赖触发时,需要谨记,先触发 computed 的 effect,再触发非 computed 的 effect
1. SVG 邂逅1.1 什么是 SVG ?什么是SVG?维基百科介绍:SVG 全称为(Scalable Vector Graphics),即可缩放矢量图形。(矢量定义:既有大小又有方向的量。在物理学中称作矢量,如一个带箭头线段:长度表示大小,箭头表示方向;在数学中称作向量。在计算机中,矢量图可无限放大而不变形)SVG 是一种基于XML格式的矢量图,主要用于定义二维图形,支持交互和动画。SVG 规范是万维网联盟(W3C) 自 1998 年以来开发的标准。SVG 图像可在不损失质量的情况下按比例缩放,并支持压缩。基于XML的SVG可轻松的用文本编辑器或矢量图形编辑器创建和编辑,并可以直接在浏览器显示。1.2 SVG 的历史SVG1.x 版本SVG 是 W3C SVG工作组于 1998 年开始开发 ,而 SVG 1.0于 2001 年 9 月 4 日成为W3C 推荐的标准。SVG 1.1 于 2003 年 1 月 14 日成为 W3C 推荐的标准。 该版本增加了模块化规范的内容。除此之外,1.1 和 1.0 几乎没有区别。SVG Tiny 1.2 于 2008 年 12 月 22 日成为 W3C 推荐标准,主要是为性能低的小设备生成图形,但是后来被 SVG 2 所弃用了。SVG 1.1 第二版 于 2011 年 8 月 16 日发布,这次只是更新了勘误表和说明,并没有增加新功能 。SVG 2.0 版本(推荐)SVG 2.0于2016 年 9 月 15 日成为W3C 候选推荐标准,最新草案于2020年5月26日发布。1.3 SVG 的优缺点优点:扩展好:矢量图像在浏览器中放大缩小不会失真,可被许多设备和浏览器中使用。而光栅图像(PNG 、JPG)放大缩小会失真。矢量图像是基于矢量的点、线、形状和数学公式来构建的图形,该图形是没有像素的,放大缩小是不会失真的。光栅图像是由像素点构建的图像——微小的彩色方块,大量像素点可以形成高清图像,比如照片。图像像素越多,质量越高。灵活:SVG是W3C开发的标准,可结合其它的语言和技术一起使用,包括 CSS、JavaScript、 HTML 和 SMIL 。SVG图像可以直接使用JS和CSS进行操作,使用时非常方便和灵活,因为SVG也是可集成到 DOM 中的。3、 可以动画:SVG 图像可以使用 JS 、 CSS 和 SMIL 进行动画处理。对于 Web 开发人员来说非常的友好。轻量级:与其它格式相比,SVG 图像的尺寸非常小。根据图像的不同,PNG 图像质量可能是 SVG 图像的 50 倍。可打印:SVG 图像可以以任何分辨率打印,而不会损失图像质量。利于SEO:SVG 图像被搜索引擎索引。因此,SVG 图像非常适合 SEO(搜索引擎优化)目的。可压缩:与其它图像格式一样,SVG 文件支持压缩。易于编辑:只需一个文本编辑器就可以创建 SVG 图像。设计师通常会使用 Adobe Illustrator (AI)等矢量图形工具创建和编辑。缺点:不适和高清图片制作SVG 格式非常适合用于徽标和图标(ICON)等 2D 图形,但不适用于高清图片,不适合进行像素级操作。SVG 的图像无法显示与标准图像格式一样多的细节,因为它们是使用点和路径而不是像素来渲染的。SVG 图像变得复杂时,加载会比较慢不完全扩平台尽管 SVG 自 1998 年以来就已经存在,并得到了大多数现代浏览器(桌面和移动设备)的支持,但它不适用于 IE8 及更低版本的旧版浏览器。根据caniuse的数据,大约还有 5% 的用户在使用不支持 SVG 的浏览器。1.4 应用场景SVG 非常适合显示矢量徽标(Logo)、图标(ICON)和其他几何设计。SVG 适合应用在需适配多种尺寸的屏幕上展示,因为SVG的扩展性更好。当需要创建简单的动画时,SVG 是一种理想的格式。SVG 可以与 JS 交互来制作线条动画、过渡和其他复杂的动画。SVG 可以与 CSS 动画交互,也可以使用自己内置的 SMIL 动画。SVG 也非常适合制作各种图表(条形图、折线图、饼图、散点图等等),以及大屏可视化页面开发。1.5 SVG 和 Canvas 的区别可扩展性:SVG 是基于矢量的点、线、形状和数学公式来构建的图形,该图形是没有像素的,放大缩小不会失真。Canvas 是由一个个像素点构成的图形,放大会使图形变得颗粒状和像素化(模糊)。SVG可以在任何分辨率下以高质量的打印。Canvas 不适合在任意分辨率下打印。渲染能力:当 SVG 很复杂时,它的渲染就会变得很慢,因为在很大程度上去使用 DOM 时,渲染会变得很慢。Canvas 提供了高性能的渲染和更快的图形处理能力,例如:适合制作H5小游戏。当图像中具有大量元素时,SVG 文件的大小会增长得更快(导致DOM变得复杂),而Canvas并不会增加太多。灵活度:SVG 可以通过JavaScript 和 CSS 进行修改,用SVG来创建动画和制作特效非常方便。Canvas只能通过JavaScript进行修改,创建动画得一帧帧重绘。使用场景:Canvas 主要用于游戏开发、绘制图形、复杂照片的合成,以及对图片进行像素级别的操作,如:取色器、复古照片。SVG 非常适合显示矢量徽标(Logo)、图标(ICON)和其他几何设计。1.6 一份 SVG 代码<?xml version="1.0" standalone="no" ?> <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" > <rect x="0" y="0" width="100" height="100"></rect> </svg>2. SVG 基础2.1 SVG Grid 和 坐标系SVG 使用的 坐标系统(网格系统) 和 Canvas的差不多。坐标系是 以左上角为 (0,0) 坐标原点,坐标以像素为单位,x 轴正方向是向右,y 轴正方向是向下。SVG Grid(坐标系)<svg> 元素默认宽为 300px, 高为 150px。通常来说网格中的一个单元相当于 svg 元素中的一像素。基本上在 SVG 文档中的 1 个像素对应输出设备(比如显示屏)上的 1 个像素(除非缩放)。<svg> 元素和其它元素一样也是有一个坐标空间的,其原点位于元素的左上角,被称为初始视口坐标系<svg>的 transform 属性可以用来移动、旋转、缩放SVG中的某个元素,如<svg>中某个元素用了变形,该元素内部会建立一个新的坐标系统,该元素默认后续所有变化都是基于新创建的坐标系统。2.2 SVG 坐标系单位SVG坐标系统,在没有明确指定单位时,默认以像素为单位。比如:<rect x="0" y="0" width="100" height="100" />定义一个矩形,即从左上角开始,向右延展 100px,向下延展 100px,形成一个 100*100 大的矩形。当然我们也可以手动指明坐标系的单位,比如:2.3 视口-viewport视口(viewport)视口是 SVG 可见的区域(也可以说是SVG画布大小)。可以将视口视为可看到特定场景的窗口。可以使用 <svg> 元素的width和height属性指定视口的大小。一旦设置了最外层 SVG 元素的宽度和高度,浏览器就会建立初始视口坐标系和初始用户坐标系。视口坐标系视口坐标系是在视口上建立的坐标系,原点在视口左上角的点(0, 0),x轴正向向右,y轴正向下。初始视口坐标系中的一个单位等于视口中的一个像素,该坐标系类似于 HTML 元素的坐标系。用户坐标系( 也称为当前坐标系或正在使用的用户空间,后面绘图都是参照该坐标系 )用户坐标系是建立在 SVG 视口上的坐标系。该坐标系最初与视口坐标系相同——它的原点位于视口的左上角。使用viewBox属性,可以修改初始用户坐标系,使其不再与视口坐标系相同。为什么要有两个坐标系?因为SVG是矢量图,支持任意缩放。在用户坐标系统绘制的图形,最终会参照视口坐标系来进行等比例缩放。2.4 视图框-viewBox视图框(viewBox)viewport是 SVG 画布的大小,而 viewBox 是用来定义用户坐标系中的位置和尺寸 (该区域通常会被缩放填充视口)。viewBox 也可理解为是用来指定用户坐标系大小。因为SVG的图形都是绘制到该区域中。用户坐标系可以比视口坐标系更小或更大,也可以在视口内完全或部分可见。一旦创建了视口坐标系(<svg>使用width和height),浏览器就会创建一个与其相同的默认用户坐标系。我们可以使用 viewBox 属性指定用户坐标系的大小。✓ 如果用户坐标系与视口坐标系具有相同的高宽比,它将viewBox区域拉伸以填充视口区域。✓ 如果用户坐标系和视口坐标系没有相同的宽高比,可用 preserveAspectRatio 属性来指定整个用户坐标系统是否在视口内可见。viewBox语法viewBox = <min-x> <min-y> <width> <height>,比如:viewBox =' 0 0 100 100'<min-x> 和 <min-y> 确定视图框的左上角坐标(不是修改用户坐标系的原点,绘图还是从原来的 0, 0 开始)<width> <height>确定该视图框的宽度和高度。➢ 宽度和高度不必与父 <svg> 元素上设置的宽度和高度相同。➢ 宽度和高度负值无效,为 0 是禁用元素的显示。viewport和viewBox有相同的宽高比<svg width="400" height="400" viewBox="0 0 100 100" > <circle cx="50" cy="50" r="50"></circle> </svg>viewport和viewBox有相同的宽高比-指定viewBox最小的x和y<svg width="400" height="400" viewBox="50 50 100 100" > <circle cx="50" cy="50" r="50"></circle> </svg>viewport和viewBox不同的宽高比<svg width="400" height="400" viewBox="0 0 200 100" preserveAspectRatio="xMinYMin" > <circle cx="50" cy="50" r="50"></circle> </svg>关于viewBox属性,可以参考这篇文章,非常容易理解如何理解SVG中的viewport、viewBox和preserveAspectRatio2.5 绘制基本图形矩形 rect<rect> 元素6 个基本属性 x y width height rx ryx :矩形左上角的 x 轴位置y :矩形左上角的 y轴位置width :矩形的宽度height :矩形的高度rx :圆角的 x 轴方位的半径ry :圆角的 y 轴方位的半径 。<rect x="60" y="10" rx="10" ry="10" width="30" height="30"></rect>圆形 circle<circle> 元素3 个基本属性。 r cx cyr:圆的半径cx:圆心的 x 轴位置cy:圆心的 y 轴位置<circle cx="100" cy="100" r="50" fill="red"></circle>椭圆 ellipse<ellipse> 元素4 个基本属性 rx ry cx cyrx:椭圆的 x轴半径ry:椭圆的 y轴半径cx:椭圆中心的 x轴位置cy:椭圆中心的 y轴位置<ellipse cx="100" cy="100" rx="25" ry="50" fill="red"></ellipse>线条 line<line> 元素4 个基本属性x1:起点的 x 轴位置y1:起点的 y轴位置x2:终点的 x轴位置y2:终点的 y轴位置<!-- stroke , 而不是 fill --> <line x1="100" y1="100" x2="200" y2="100" stroke="red" stroke-width="5"></line>折线 polyline<polyline> 元素1 个基本属性points : 点集数列。每个数字用空白、逗号、终止命令符或者换行符分隔开。每个点必须包含 2 个数字,一个是 x 坐标,一个是 y 坐标。所以点列表 (0,0), (1,1) 和 (2,2) 可以写成这样:“0 0, 1 1, 2 2”。✓ 支持格式: “0 0, 1 1, 2 2”或 “0 ,0 , 1, 1, 2, 2”或 “0 0 1 1 2 2”<!-- 第1种写法 --> <!-- <polyline points="20 0, 80 50, 20, 100"></polyline> --> <polyline points="20 0, 80 50, 20, 100" fill="transparent" stroke="red" ></polyline> <!-- 第2种写法 --> <polyline points="20 0 80 50 20 100"></polyline> <!-- 第3种写法 --> <polyline points="20 ,0 ,80 ,50 ,20, 100"></polyline>多边形 polygon<polygon> 元素1 个基本属性points :点集数列。每个数字用空白符、逗号、终止命令或者换行符分隔开。每个点必须包含 2 个数字,一个是 x 坐标,一个是 y 坐标。所以点列表 (0,0), (1,1) 和 (2,2) 推荐写成这样:“0 0, 1 1, 2 2”。路径绘制完后闭合图形,所以最终的直线将从位置 (2,2) 连接到位置 (0,0)。<polygon points="20 0, 80 50, 20 100" fill="transparent" stroke="red"></polygon>路径 path<path> 元素1 个基本属性d :一个点集数列,以及其它关于如何绘制路径的信息,必须M命令开头。✓ 所以点列表 (0,0), (1,1) 和 (2,2) 推荐写成这样:“M 0 0, 1 1, 2 2”。✓ 支持格式: “M 0 0, 1 1, 2 2”或 “M0 0, 1 1, 2 2” 或 “M 0 ,0 , 1, 1, 2, 2”或 “M 0 0 1 1 2 2”<!-- 1.使用path 绘制一个三角形 --> <!-- <path d="M 20 0, 80 50, 20 100" fill="transparent" stroke="red"></path> --> <!-- 1.使用path 绘制一个闭合的三角形 --> <!-- <path d="M 20 0, 80 50, 20 100 Z" fill="transparent" stroke="red"></path> --> <!-- 1.使用 path 绘图的命令: M moveTo Z close path L lineTo --> <path d="M 20 0,L 80 50,L 20 100 Z" fill="transparent" stroke="red"></path>上面的 M Z L 都是一条命令表示 移动到某处 关闭路径 以及 连线到某处,还有很多其他的命令,这里不讲了,遇到去查即可。图片 image在SVG中绘制一张图片,在<image>元素的 href 属性引入图片URL<!-- svg 1.0版本的语法 --> <svg version="1.0" baseProfile="full" width="300" height="300" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <image x="0" y="0" xlink:href="../images/googlelogo_color_92x30dp.png" width="100" height="100" > </image> </svg> <!-- svg 2.0 + 1.0 版本的语法( 为了兼容以前的浏览器的写法 ) --> <svg version="1.0" baseProfile="full" width="300" height="300" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <image x="0" y="0" xlink:href="../images/googlelogo_color_92x30dp.png" href="../images/googlelogo_color_92x30dp.png" width="100" height="100" > </image> </svg>绘制文字 text<text> 元素的基本属性x 和 y 属性决定了文本在用户坐标系中显示的位置。text-anchor 文本流方向属性,可以有 start、middle、end 或 inherit 值,默认值 startdominant-baseline 基线对齐属性 : 有 auto 、middle 或 hanging 值, 默认值:auto<text> 元素的字体属性文本的一个至关重要的部分是它显示的字体。SVG 提供了一些属性,类似于CSS 。下列的属性可以被设置为一个 SVG 属性或一个 CSS 属性:✓ font-family、font-style、font-weight、font-variant、font-stretch、font-size、font-size-adjust、kerning、letter-spacing、word-spacing和textdecoration。其它文本相关的元素:<tspan> 元素用来标记大块文本的子部分,它必须是一个text元素或别的tspan元素的子元素。✓ x 和 y 属性决定了文本在视口坐标系中显示的位置。✓ alignment-baseline 基线对齐属性:auto 、baseline、middle、hanging、top、bottom ... ,默认是 auto<!-- 1.在svg中绘制一个文字 --> <!-- <text x="100" y="100" font-size="50" fill="red">Ay</text> --> <!-- 2.文本的对齐方式 --> <!-- <text x="100" y="100" text-anchor="middle" font-size="50" fill="red">Ay</text> --> <!-- 3.基线对齐方式 : 有 auto 、middle 或 hanging 值, 默认值:auto --> <text x="100" y="100" dominant-baseline="middle" font-size="50" fill="red">Ay</text> <!-- 4.在svg中使用tspan绘制一个文字 --> <text x="40" y="100" font-size="20"> iPhone14 <tspan fill="red">¥100</tspan> </text>2.6 SVG 的组合和复用1. 元素的组合 g<g>元素的属性(该元素只包含全局属性)核心属性:id样式属性:class 、stylePresentation Attributes(也可说是 CSS 属性,这些属性可写在CSS中,也可作为元素的属性用):✓ cursor, display, fill, fill-opacity, opacity,…✓ stroke, stroke-dasharray, stroke-dashoffset, stroke-linecap, stroke-linejoin✓ 更多表示属性:https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation事件属性:onchange, onclick, ondblclick, ondrag…动画属性:transform更多:https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g<!-- <circle cx="50" cy="50" r="25" fill="transparent" stroke="red"></circle> <circle cx="80" cy="50" r="25" fill="transparent" stroke="red"></circle> <circle cx="110" cy="50" r="25" fill="transparent" stroke="red"></circle> <circle cx="140" cy="50" r="25" fill="transparent" stroke="red"></circle> --> <!-- g 元素没有专有的属性,只有全局的属性 全局属性:id class style fill stroke onclick --> <g fill="transparent" stroke="red"> <circle cx="50" cy="50" r="25"></circle> <circle cx="80" cy="50" r="25"></circle> <circle cx="110" cy="50" r="25"></circle> <circle cx="140" cy="50" r="25"></circle> </g>2. 元素的复用和引入 defs 和 use<defs>元素,定义可复用元素。例如:定义基本图形、组合图形、渐变、滤镜、样式等等。在< defs >元素中定义的图形元素是不会直接显示的。可在视口任意地方用<use>来呈现在defs中定义的元素。<defs>元素没有专有属性,使用时通常也不需添加任何属性。<use> 元素的属性href: 需要复制元素/片段的 URL 或 ID(支持跨SVG引用)。默认值:无xlink:href:(SVG2.0已弃用)需要复制的元素/片段的 URL 或 ID 。默认值:无x / y :元素的 x / y 坐标(相对复制元素的位置)。 默认值:0width / height :元素的宽和高(在引入svg或symbol元素才起作用)。 默认值:0<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg"> <defs> <!-- 0.样式 --> <style> rect{ fill: green; } </style> <!-- 1.定义了一个矩形 --> <rect id="rectangle" x="0" y="0" width="100" height="50"></rect> <!-- 2.定义了一个组合图形 --> <g id="logo" fill="transparent" stroke="red"> <circle cx="50" cy="50" r="25"></circle> <circle cx="80" cy="50" r="25"></circle> <circle cx="110" cy="50" r="25"></circle> <circle cx="140" cy="50" r="25"></circle> </g> <!-- 定义渐变 --> <!-- 滤镜 --> </defs> <!-- 在这里进行图形的复用 --> <!-- <use href="#rectangle"></use> --> <!-- <use x="100" y="100" href="#rectangle"></use> --> <!-- <use href="#logo"></use> --> <!-- <use x="100" y="100" href="#logo"></use> --> </svg> <svg width="300" height="300" xmlns="http://www.w3.org/2000/svg" > <!-- 他的宽和高是没有生效的 ???? 只用use引用的图形是 svg 或 symbol 才会起作用 --> <use href="#rectangle" width="200" height="100" ></use> </svg> <svg width="300" height="300" xmlns="http://www.w3.org/2000/svg" > <use href="#logo"></use> </svg>图形元素复用 symbols<symbol> 元素和 <defs> 元素类似,也是用于定义可复用元素,然后通过 <use> 元素来引用显示。在 <symbol> 元素中定义的图形元素默认也是不会显示在界面上。<symbol>元素常见的应用场景是用来定义各种小图标,比如:icon、logo、徽章等<symbol>元素的属性viewBox:定义当前 <symbol> 的视图框。x / y :symbol元素的 x / y坐标。 ;默认值:0width / height:symbol元素的宽度。 默认值:0<symbol>和<defs> 的区别<defs>元素没有专有属性,而<symbol>元素提供了更多的属性✓ 比如: viewBox、 preserveAspectRatio 、x、y、width、height等。<symbol>元素有自己用户坐标系,可以用于制作SVG精灵图。<symbol>元素定义的图形增加了结构和语义性,提高文档的可访问性。SVG ICON文件-合并成SVG精灵图:https://www.zhangxinxu.com/sp/svgo<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg" > <!-- 1.ICON previous--> <symbol id="previous" viewBox="0 0 100 100"> <path fill="currentColor" d="M 80 0,L 20 50, L 80 100 Z"></path> </symbol> <!-- 2.ICON next --> <symbol id="next" viewBox="0 0 100 100"> <polygon points="20 0, 80 50, 20 100"></polygon> </symbol> <!-- 复用 --> <!-- <use href="#previous" width="100" height="100"></use> --> </svg> <!-- 复用 --> <svg width="300" height="300" xmlns="http://www.w3.org/2000/svg" > <!-- 直接在use上指定ICON的 width和 hegiht --> <use href="#previous" width="50" height="50"></use> </svg> <!-- 这个属于缩小 --> <svg width="30" height="30" xmlns="http://www.w3.org/2000/svg" > <use href="#previous" ></use> </svg> <!-- 属于放大 --> <svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" > <use href="#previous" ></use> </svg>3. SVG 高级3.1 填充和描边如果想要给SVG中的元素上色,一般有两种方案可以实现:第一种:直接使用元素的属性,比如:填充(fill)属性、描边(stroke)属性等。<rect x="10" y="10" width="100" height="100" fill="currentColor" fill-opacity="0.4"></rect> <rect x="10" y="10" width="100" height="100" fill="transparent" stroke="red" stroke-width="3" stroke-opacity="1" ></rect>第二种:直接编写CSS样式,因为SVG也是HTML中的元素,也支持用CSS的方式来编写样式。直接编写CSS样式实现填充和描边除了定义元素的属性外,你也可以通过CSS来实现填充和描边(CSS样式可写在defs中,也可写在HTML头部或外部等)。语法和 HTML 里使用 CSS 一样,需要注意的是:需要把 background-color、border 改成 fill 和 stroke不是所有的属性都能用 CSS 来设置,上色和填充的部分是可以用 CSS 来设置。✓ 比如,fill,stroke,stroke-dasharray 等可以用CSS设置;比如,路径的命令则不能用 CSS 设置。哪些属性可以使用CSS设置,哪些不能呢?SVG规范中将属性区分成 Presentation Attributes 和 Attributes 属性。✓ Presentation Attributes 属性( 支持CSS和元素用 ):https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation✓ Attributes 属性(只能在元素用): https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute✓ 提示:这些属性是不需要去记的,用多了就记住了,在忘记时测试一下就知道了。CSS给SVG中的元素填充、描边和上色,支持如下4种编写方式:方式一:内联(行内) CSS 样式,写在元素的style属性上方式二:内嵌(内部) CSS 样式,写在 <defs>中的 <style>标签中方式三:内嵌(内部) CSS 样式,写在<head>中的<style>标签中方式四:外部 CSS 样式文件,写在 .css 文件中CSS样式优先级别:内联的 style > defs中的style > 外部 / head内部 > 属性 fill<!-- 1.行内的样式 --> <rect x="10" y="10" width="100" height="100" style="fill:red;" ></rect> <!-- 2.行内的样式 --> <defs> <style> .rectangle{ fill: green; stroke: red; } </style> </defs> <rect class="rectangle" x="10" y="10" width="100" height="100"></rect> /* 3.行内的样式 */ <style> .rectangle{ fill: blue; } </style> <rect class="rectangle" x="10" y="10" width="100" height="100"></rect> /* 4. 引入css文件*/ <link rel="stylesheet" href="./style.css"> <rect class="rectangle" x="10" y="10" width="100" height="100"></rect>3.2 渐变和滤镜SVG除了可以简单的填充和描边,还支持在填充和描边上应用渐变色。渐变有两种类型:线性渐变 和 径向渐变。编写渐变时,必须给渐变内容指定一个 id 属性,use引用需用到。建议渐变内容定义在<defs>标签内部,渐变通常是可复用的。线性渐变,是沿着直线改变颜色。下面看一下线性渐变的使用步骤:第1步:在 SVG 文件的 defs 元素内部,创建一个 <linearGradient> 节点,并添加 id 属性。第2步:在 <linearGradient> 内编写几个 <stop> 结点。✓ 给 <stop> 结点指定位置 offset属性和 颜色stop-color属性,用来指定渐变在特定的位置上应用什么颜色➢ offset 和 stop-color 这两个属性值,也可以通过 CSS 来指定。✓ 也可通过 stop-opacity 来设置某个位置的半透明度。第3步:在一个元素的 fill 属性或 stroke 属性中通过ID来引用 <linearGradient> 节点。✓ 比如:属性fill属性设置为url( #Gradient2 )即可。第4步(可选):控制渐变方向,通过 ( x1, y1 ) 和 ( x2, y2 ) 两个点控制。✓ (0, 0) (0, 1)从上到下;(0, 0)(1, 0)从左到右。✓ 当然也可以通过 gradientTransform 属性 设置渐变形变。比如: gradientTransform=“rotate(90)” 从上到下。<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg"> <!-- 定义可以复用的元素: 样式, 渐变, 图形, 滤镜... --> <defs> <!-- 默认的渐变色 --> <linearGradient id="gradient1"> <stop offset="0%" stop-color="red"></stop> <stop offset="50%" stop-color="green"></stop> <stop offset="100%" stop-color="blue"></stop> </linearGradient> <!-- 这个是制定渐变的方向 --> <linearGradient id="gradient2" x1="0" y1="0" x2="1" y2="1"> <stop offset="0%" stop-color="red"></stop> <stop offset="50%" stop-color="green"></stop> <stop offset="100%" stop-color="blue"></stop> </linearGradient> <!-- 通过形变 渐变色(了解 ) --> <linearGradient id="gradient3" gradientTransform="rotate(0)"> <stop offset="0%" stop-color="red"></stop> <stop offset="50%" stop-color="green"></stop> <stop offset="100%" stop-color="blue"></stop> </linearGradient> </defs> <rect x="0" y="0" width="100" height="50" fill="url(#gradient3)"></rect> </svg>在前端开发中,毛玻璃效果有几种方案来实现:方案一:使用CSS的 backdrop-filter 或 filter 属性backdrop-filter:可以给一个元素后面区域添加模糊效果。适用于元素背后的所有元素。为了看到效果,必须使元素或其背景至少部分透明。filter:直接将模糊或颜色偏移等模糊效果应用于指定的元素。方案二:使用SVG的 filter 和 feGaussianBlur 元素(建议少用)<filter>:元素作为滤镜操作的容器,该元素定义的滤镜效果需要在SVG元素上的 filter 属性引用。✓ x , y, width, height 定义了在画布上应用此过滤器的矩形区域。x, y 默认值为 -10%(相对自身);width ,height 默认值为 120% (相对自身) 。<feGaussianBlur>:该滤镜专门对输入图像进行高斯模糊✓ stdDeviation 熟悉指定模糊的程度<feOffset> :该滤镜可以对输入图像指定它的偏移量。<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" > <defs> <!-- 高斯模糊的 效果 --> <filter id="blurFilter"> <!-- ...... --> <feGaussianBlur stdDeviation="8"></feGaussianBlur> </filter> </defs> <image href="../images/avatar.jpeg" width="200" height="200" filter="url(#blurFilter)" > </image> </svg>3.3 SVG 形变 transformtransform 属性支持的函数有 translate rotate scale skew matrix1. 平移 translate与 CSS 的 translate 相似但有区别,这里只支持 2D 变换,不需单位。<rect x="0" y="0" width="100" height="50" transform="translate(200, 200)" ></rect>2. 旋转 rotate与CSS的rotate相似但有区别。区别是:支持2D变换,不需单位,可指定旋转原点。<rect transform="translate(100, 0) rotate(45, 50, 25)" x="0" y="0" width="100" height="50" > </rect>3. 缩放 scale与CSS的scale相似但有区别,这只支持2D变换,不需单位。 <rect transform="translate(100, 100) scale(1, 2)" x="0" y="0" width="100" height="50" ></rect>3.4 路径描边动画stroke 是描边属性,专门给图形描边。如果想给各种描边添加动画效果,需用到下面两个属性:stroke-dasharray =“number [, number , ….]”: 将虚线类型应用在描边上。✓ 该值必须是用逗号分割的数字组成的数列,空格会被忽略。比如 3,5 :➢ 第一个表示填色区域的长度为 3➢ 第二个表示非填色区域的长度为 5stroke-dashoffset:指定在dasharray模式下路径的偏移量。✓ 值为number类型,除了可以正值,也可以取负值。描边动画实现步骤:1.先将描边设置为虚线2.接着将描边偏移到不可见处3.通过动画让描边慢慢变为可见,这样就产生了动画效果了。<style> #line1 { /* 指定为虚线 */ stroke-dasharray: 100px; /* 可见 */ stroke-dashoffset: 20px; /* animation: line1Move 2s linear; */ } @keyframes line1Move { 0% { /* 不可见 */ stroke-dashoffset: 100px; } 100% { /* 可见 */ stroke-dashoffset: 0px; } } </style> <svg width="300" height="300" xmlns="http://www.w3.org/2000/svg"> <!-- stroke , 而不是 fill --> <line id="line1" x1="100" y1="70" x2="200" y2="70" stroke="red" stroke-width="10" ></line> </svg>3.5 SMIL 动画SMIL(Synchronized Multimedia Integration Language 同步多媒体集成语言)是W3C推荐的可扩展标记语言,用于描述多媒体演示。SMIL 标记是用 XML 编写的,与HTML有相似之处。SMIL 允许开发多媒体项目,例如:文本、图像、视频、音频等。SMIL 定义了时间、布局、动画、视觉转换和媒体嵌入等标记,比如:<head> <body> <seq> <par> <excl> 等元素SMIL的应用目前最常用的Web浏览器基本都支持 SMIL 语言。SVG 动画元素是基于SMIL实现(SVG中使用SMIL实现元素有:<set>、<animate>、<animateMotion>...)。Adobe Media Player implement SMIL playback。QuickTime Player implement SMIL playback。SVG动画实现方式用JS脚本实现:可以直接通过 JavaScript 在来给 SVG 创建动画和开发交互式的用户界面。用CSS样式实现:自 2008 年以来,CSS动画已成为WebKit中的一项功能,使得我们可以通过CSS动画的方式来给文档对象模型(DOM) 中的 SVG 文件编写动态效果。用SMIL实现:一种基于SMIL语言实现的SVG动画。SMIL动画的优势只需在页面放几个animate元素就可以实现强大的动画效果,无需任何CSS和JS代码。SMIL支持声明式动画。声明式动画不需指定如何做某事的细节,而是指定最终结果应该是什么,将实现细节留给客户端软件在 JavaScript 中,动画通常使用 setTimeout() 或 setInterval() 等方法创建,这些方法需要手动管理动画的时间。而SMIL 声明式动画可以让浏览器自动处理,比如:动画轨迹直接与动画对象相关联、物体和运动路径方向、管理动画时间等等。SMIL 动画还有一个令人愉快的特点是,动画与对象本身是紧密集成的,对于代码的编写和阅读性都非常好。SVG 中支持SMIL动画的元素:<set> <animate> <animateColor> <animateMotion>更多 https://www.w3.org/TR/SVG11/animate.html#AnimationElements1. set 元素set元素是最简单的 SVG 动画元素。它是在经过特定时间间隔后,将属性设置为某个值(不是过度动画效果)。因此,图像不是连续动画,而是改变一次属性值。它支持所有属性类型,包括那些无法合理插值的属性类型,例如:字符串 和 布尔值。而对于可以合理插值的属性通常首选<animate>元素。常用属性有 attributeName to begin<!-- 1. 在3秒后自动将长方形瞬间移到右边 --> <svg width="300" height="300" xmlns="http://www.w3.org/2000/svg" > <rect x="0" y="0" width="100" height="50" fill="red"> <set attributeName ='x' to="200" begin="3s" > </set> </rect> </svg> <!-- 点击长方形后,长方形瞬间移到右边 --> <svg width="300" height="300" xmlns="http://www.w3.org/2000/svg" > <rect id="rectangle" x="0" y="0" width="100" height="50" fill="green"> <set attributeName ='x' to="200" begin="rectangle.click" > </set> </rect> </svg>2. animate 元素常用属性有:attributeName from values begin dur fill repeatCount <svg width="300" height="200" xmlns="http://www.w3.org/2000/svg" > <rect x="0" y="0" width="100" height="50" fill="green"> <!-- form: 0 to: 200 --> <animate attributeName="x" values="0; 170; 200" dur="3s" repeatCount="indefinite" > </animate> <animate attributeName="fill" values="red;green" dur="3s" repeatCount="indefinite" > </animate> </rect> </svg>
1. 什么是 Canvas ?Canvas 最初由 Apple 于 2004 年 引入,用于 Mac OS X Webkit 组件,为仪表盘小组件和 Safari 浏览器等应用程序提供支持。后来,它被 Gecko 内核的浏览器(尤其是 Mozilla Firefox),Opera 和 Chrome 实现,并被网页超文本应用技术工作小组提议为下一代的网络技术的标准元素(HTML5新增元素)。Canvas 提供了非常多的 JavaScript绘图 API (比如:绘制路径、举行、圆、文本和图像等方法),与 <canvas>元素可以绘制各种 2D 图形。Canvas API 主要聚焦于 2D 图形。当然也可以使用 <canvas> 元素对象的 WebGL API 来绘制 2D 和 3D 图形。Canvas 可用于动画、游戏画面、数据可视化、图片编辑以及实现视频处理等方面。浏览器兼容性Canvas 优点:Canvas 提供的功能更原始,适合像素处理,动态渲染和数据量大的绘制,如:图片编辑、热力图、炫光尾迹特效等。Canvas 非常适合图像密集的游戏开发,适合频繁重绘许多的对象。Canvas能够以 .png 或 .jpg 格式保存结果图片,适合对图像进行像素级的处理。Canvas 缺点:在移动端可能因为 Canvas 数量多,而导致内存占用超出了手机的承受能力,导致浏览器崩溃。Canvas 绘图只能通过 JavaScript 脚本操作 all in js。Canvas 是由一个个像素点构成的图形,放大会使图形变得颗粒状和像素化,导致模糊。2. Canvas 绘制图形Canvas 支持两种方式来绘制矩形:矩形方法 和 路径方法。2.1 矩形方法路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。除了矩形,其他的图形都是通过一条或者多条路径组合而成的。通常我们会通过众多的路径来绘制复杂的图形。fillRect(x, y, width, height): 绘制一个填充的矩形strokeRect(x, y, width, height): 绘制一个矩形的边框clearRect(x, y, width, height): 清除指定矩形区域,让清除部分完全透明。Canvas 绘制一个矩形:<canvas id="tutorial" width="300" height="300px"> 你的浏览器不兼容Canvas,请升级您的浏览器! </canvas> <script> window.onload = function() { let canvasEl = document.getElementById('tutorial') if(!canvasEl.getContext){ return } let ctx = canvasEl.getContext('2d') // 2d | webgl ctx.fillRect(0,0, 100, 50) // 单位也是不用写 px } </script>2.2 路径方法使用路径绘制图形的步骤:首先需要创建路径起始点(beginPath)。然后使用画图命令去画出路径( arc绘制圆弧 、lineTo画直线 )。之后把路径闭合( closePath , 不是必须)。一旦路径生成,就能通过 描边(stroke) 或 填充路径区域(fill) 来渲染图形。以下是绘制路径时,所要用到的函数beginPath():新建一条路径,生成之后,图形绘制命令被指向到新的路径上绘图,不会关联到旧的路径。closePath():闭合路径之后图形绘制命令又重新指向到 beginPath之前的上下文中。stroke():通过线条来绘制图形轮廓/描边 (针对当前路径图形)。fill():通过填充路径的内容区域生成实心的图形 (针对当前路径图形)。<canvas id="tutorial" width="300" height="300px"> 你的浏览器不兼容Canvas,请升级您的浏览器! </canvas> <script> window.onload = function () { let canvasEl = document.getElementById("tutorial"); if (!canvasEl.getContext) { return; } let ctx = canvasEl.getContext("2d"); // 2d | webgl // 1.创建一个路径 ctx.beginPath(); // 2.绘图指令 // ctx.moveTo(0, 0) // ctx.rect(100, 100, 100, 50); ctx.moveTo(100, 100); ctx.lineTo(200, 100); ctx.lineTo(200, 150); ctx.lineTo(100, 150); // 3.闭合路径 ctx.closePath(); // 4.填充和描边 ctx.stroke(); }; </script>lineTo 和 arc 两个函数结合既能绘制直线也能绘制圆弧,因此路径方法还可以绘制许多图形,比如三角形、菱形、梯形、椭圆形、圆形等等。。。3. Canvas 样式和颜色3.1 色彩 Colors如果我们想要给图形上色,有两个重要的属性可以做到:fillStyle = color: 设置图形的填充颜色,需在 fill() 函数前调用。strokeStyle = color: 设置图形轮廓的颜色,需在 stroke() 函数前调用。:::warning{title="注意"}一旦设置了 strokeStyle 或者 fillStyle 的值,那么这个新值就会成为新绘制的图形的默认值。如果你要给图形上不同的颜色,你需要重新设置 fillStyle 或 strokeStyle 的:::3.2 透明度 Transparent除了可以绘制实色图形,我们还可以用 canvas 来绘制半透明的图形。方式一:strokeStyle 和 fillStyle属性结合RGBA:// 指定透明颜色,用于描边和填充样式 ctx.strokeStyle = "rgba(255,0,0,0.5)"; ctx.fillStyle = "rgba(255,0,0,0.5)";方式二:globalAlpha 属性// 针对于Canvas中所有的图形生效 ctx.globalAlpha = 0.3 // 2.修改画笔的颜色 // ctx.fillStyle = 'rgba(255, 0, 0, 0.3)' ctx.fillRect(0,0, 100, 50) // 单位也是不用写 px ctx.fillStyle = 'blue' ctx.fillRect(200, 0, 100, 50) ctx.fillStyle = 'green' // 关键字, 十六进制, rbg , rgba ctx.beginPath() ctx.rect(0, 100, 100, 50) ctx.fill():::warning{title="注意"}globalAlpha = 0 ~ 1✓ 这个属性影响到 canvas 里所有图形的透明度✓ 有效的值范围是 0.0(完全透明)到 1.0(完全不透明),默认是 1.0。:::3.3 线型 Line styles调用lineTo()函数绘制的线条,是可以通过一系列属性来设置线的样式。lineWidth = value: 设置线条宽度。lineCap = type: 设置线条末端样式。lineJoin = type: 设定线条与线条间接合处的样式。......lineWidth设置线条宽度的属性值必须为正数。默认值是 1.0px,不需单位。( 零、负数、Infinity和NaN值将被忽略)线宽是指给定路径的中心到两边的粗细。换句话说就是在路径的两边各绘制线宽的一半。如果你想要绘制一条从 (3,1) 到 (3,5),宽度是 1.0 的线条,你会得到像第二幅图一样的结果。✓ 路径的两边个各延伸半个像素填充并渲染出1像素的线条(深蓝色部分)✓ 两边剩下的半个像素又会以实际画笔颜色一半色调来填充(浅蓝部分)✓ 实际画出线条的区域为(浅蓝和深蓝的部分),填充色大于1像素了,这就是为何宽度为 1.0 的线经常并不准确的原因。要解决这个问题,必须对路径精确的控制。如,1px的线条会在路径两边各延伸半像素,那么像第三幅图那样绘制从 (3.5 ,1) 到 (3.5, 5) 的线条,其边缘正好落在像素边界,填充出来就是准确的宽为 1.0 的线条。lineCap: 属性的值决定了线段端点显示的样子。它可以为下面的三种的其中之一:butt 截断,默认是 butt。round 圆形square 正方形lineJoin: 属性的值决定了图形中线段连接处所显示的样子。它可以是这三种之一:round 圆形bevel 斜角miter 斜槽规,默认是 miter。3.4 绘制文本canvas 提供了两种方法来渲染文本:fillText(text, x, y [, maxWidth])✓ 在 (x,y) 位置,填充指定的文本✓ 绘制的最大宽度(可选)。strokeText(text, x, y [, maxWidth])✓ 在 (x,y) 位置,绘制文本边框✓ 绘制的最大宽度(可选)。文本的样式(需在绘制文本前调用)font = value: 当前绘制文本的样式。这个字符串使用和 CSS font 属性相同的语法。默认的字体是:10px sans-serif。textAlign = value:文本对齐选项。可选的值包括:start, end, left, right or center. 默认值是 starttextBaseline = value:基线对齐选项。可选的值包括:top, hanging, middle, alphabetic, ideographic, bottom。✓ 默认值是 alphabetic。<canvas id="tutorial" width="300" height="300px"> 你的浏览器不兼容Canvas,请升级您的浏览器! </canvas> <script> window.onload = function () { let canvasEl = document.getElementById("tutorial"); if (!canvasEl.getContext) { return; } let ctx = canvasEl.getContext("2d"); // 2d | webgl ctx.font = "60px sen-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.strokeStyle = "red"; ctx.fillStyle = "red"; // 将字体绘制在 100, 100 这个坐标点 ctx.fillText("Ay", 100, 100); // ctx.strokeText("Ay", 100, 100); }; </script>3.5 绘制图片绘制图片,可以使用 drawImage 方法将它渲染到 canvas 里。drawImage 方法有三种形态:drawImage(image, x, y)其中 image 是 image 或者 canvas 对象,x 和 y 是其在目标 canvas 里的起始坐标。drawImage(image, x, y, width, height)这个方法多了 2 个参数:width 和 height,这两个参数用来控制 当向 canvas 画入时应该缩放的大小drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)第一个参数和其它的是相同的,都是一个图像或者另一个 canvas 的引用。其它 8 个参数最好是参照右边的图解,前 4 个是定义图像源的切片位置和大小,后 4 个则是定义切片的目标显示位置和大小。:::info{title="图片的来源"}HTMLImageElement:这些图片是由Image()函数构造出来的,或者任何的 <img> 元素。HTMLVideoElement:用一个 HTML 的 <video> 元素作为你的图片源,可以从视频中抓取当前帧作为一个图像。HTMLCanvasElement:可以使用另一个 <canvas> 元素作为你的图片源。等等:::4. Canvas 状态和形变4.1 Canvas 绘画状态-保存和恢复Canvas 绘画状态是当前绘画时所产生的样式和变形的一个快照,Canvas 在绘画时,会产生相应的绘画状态,其实我们是可以将某些绘画的状态存储在栈中来为以后复用,Canvas 绘画状态的可以调用 save 和 restore 方法是用来保存和恢复,这两个方法都没有参数,并且它们是成对存在的。保存和恢复(Canvas)绘画状态save():保存画布 (canvas) 的所有绘画状态restore():恢复画布 (canvas) 的所有绘画状态Canvas绘画状态包括:当前应用的变形(即移动,旋转和缩放)以及这些属性:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, font, textAlign, textBaseline......当前的裁切路径(clipping path)4.2 移动 - translatetranslate方法,它用来移动 canvas 和它的原点到一个不同的位置。translate(x, y)x 是左右偏移量,y 是上下偏移量(无需单位)。移动 canvas 原点的好处如不使用 translate方法,那么所有矩形默认都将被绘制在相同的(0,0)坐标原点。translate方法可让我们任意放置图形,而不需要手工一个个调整坐标值。移动矩形案例第一步:先保存一下canvas当前的状态第二步:在绘制图形前translate移动画布第三步:开始绘制图形,并填充颜色<script> ///1.形变( 没有保存状态) ctx.translate(100, 100); ctx.fillRect(0, 0, 100, 50); // 单位也是不用写 px ctx.translate(100, 100); ctx.strokeRect(0, 0, 100, 50); </script><script> // 2.形变(保存形变之前的状态) ctx.save(); ctx.translate(100, 100); ctx.fillRect(0, 0, 100, 50); // 单位也是不用写 px ctx.restore(); // 恢复了形变之前的状态( 0,0) ctx.save(); // (保存形变之前的状态) ctx.translate(100, 100); ctx.fillStyle = "red"; ctx.fillRect(0, 0, 50, 30); ctx.restore(); </script>4.3 移动 - rotaterotate方法,它用于以原点为中心旋转 canvas,即沿着 z轴 旋转。rotate(angle)只接受一个参数:旋转的角度 (angle),它是顺时针方向,以弧度为单位的值。角度与弧度的 JS 表达式:弧度=( Math.PI / 180 ) * 角度 ,即 1角度 = Math.PI/180 个弧度。比如:旋转90°:Math.PI / 2; 旋转180°:Math.PI ; 旋转360°:Math.PI * 2; 旋转-90°:-Math.PI / 2;旋转的中心点始终是 canvas 的原坐标点,如果要改变它,我们需要用到 translate方法。<SCRIPT> // 保存形变之前的状态 ctx.save() // 1.形变 ctx.translate(100, 100) // 360 -> Math.PI * 2 // 180 -> Math.PI // 1 -> Math.PI / 180 // 45 -> Math.PI / 180 * 45 ctx.rotate(Math.PI / 180 * 45) ctx.fillRect(0, 0, 50, 50) // ctx.translate(100, 0) // ctx.fillRect(0, 0, 50, 50) // 绘图结束(恢复形变之前的状态) ctx.restore() ctx.save() ctx.translate(100, 0) ctx.fillRect(0, 0, 50, 50) ctx.restore() // ....下面在继续写代码的话,坐标轴就是参照的是原点了 <SCRIPT>4.4 移动 - scalescale(x, y) 方法可以缩放画布。可用它来增减图形在 canvas 中的像素数目,对图形进行缩小或者放大。x 为水平缩放因子,y 为垂直缩放因子,也支持负数。<script> // 保存形变之前的状态 ctx.save() // 1.形变 ctx.translate(100, 100) // 平移坐标系统 ctx.scale(2, 2) // 对坐标轴进行了放大(2倍) ctx.translate(10, 0) // 10px -> 20px ctx.fillRect(0, 0, 50, 50) // 绘图结束(恢复形变之前的状态) ctx.restore() // ....下面在继续写代码的话,坐标轴就是参照的是原点了 </script>5. Canvas 动画和案例5.1 Canvas 动画Canvas绘图都是通过JavaScript 去操控的,如要实现一些交互性动画是相当容易的。我们可以使用 setInterval 、 setTimeout 和 requestAnimationFrame 三种方法来定期执行指定函数进行重绘。Canvas 画出一帧动画的基本步骤(如要画出流畅动画,1s 需绘60帧):第一步:用 clearRect 方法清空 canvas ,除非接下来要画的内容会完全充满 canvas(例如背景图),否则你需要清空所有。第二步:保存 canvas 状态,如果加了 canvas 状态的设置(样式,变形之类的),又想在每画一帧之时都是原始状态的话,你需要先保存一下,后面再恢复原始状态。第三步:绘制动画图形(animated shapes) ,即绘制动画中的一帧。第四步:恢复 canvas 状态,如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。5.2 案例:太阳系动画效果代码<script> window.onload = function () { let canvasEl = document.getElementById("tutorial"); if (!canvasEl.getContext) { return; } let ctx = canvasEl.getContext("2d"); // 2d | webgl let sun = new Image(); sun.src = "../../images/canvas_sun.png"; // sun.onload = function() { // // draw // } let earth = new Image(); earth.src = "../../images/canvas_earth.png"; let moon = new Image(); moon.src = "../../images/canvas_moon.png"; requestAnimationFrame(draw); /** 1秒钟会回调 61次 */ function draw() { console.log("draw"); ctx.clearRect(0, 0, 300, 300); ctx.save(); // 1.绘制背景 drawBg(); // 2.地球 drawEarth(); ctx.restore(); requestAnimationFrame(draw); } function drawBg() { ctx.save(); ctx.drawImage(sun, 0, 0); // 背景图 ctx.translate(150, 150); // 移动坐标 ctx.strokeStyle = "rgba(0, 153, 255, 0.4)"; ctx.beginPath(); // 绘制轨道 ctx.arc(0, 0, 105, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } function drawEarth() { let time = new Date(); let second = time.getSeconds(); let milliseconds = time.getMilliseconds(); ctx.save(); // earth start ctx.translate(150, 150); // 中心点坐标系 // 地球的旋转 // Math.PI * 2 一整个圆的弧度 // Math.PI * 2 / 60 分成 60 份 // Math.PI * 2 / 60 1s // Math.PI * 2 / 60 / 1000 1mm // 1s 1mm // Math.PI * 2 / 60 * second + Math.PI * 2 / 60 / 1000 * milliseconds ctx.rotate( ((Math.PI * 2) / 10) * second + ((Math.PI * 2) / 10 / 1000) * milliseconds ); ctx.translate(105, 0); // 圆上的坐标系 ctx.drawImage(earth, -12, -12); // 3.绘制月球 drawMoon(second, milliseconds); // 4.绘制地球的蒙版 drawEarthMask(); ctx.restore(); // earth end } function drawMoon(second, milliseconds) { ctx.save(); // moon start // 月球的旋转 // Math.PI * 2 一圈 360 // Math.PI * 2 / 10 1s(10s一圈) // Math.PI * 2 / 10 * 2 2s(10s一圈) // Math.PI * 2 / 10 / 1000 1mm 的弧度 // 2s + 10mm = 弧度 // Math.PI * 2 / 10 * second + Math.PI * 2 / 10 / 1000 * milliseconds ctx.rotate( ((Math.PI * 2) / 2) * second + ((Math.PI * 2) / 2 / 1000) * milliseconds ); ctx.translate(0, 28); ctx.drawImage(moon, -3.5, -3.5); ctx.restore(); // moon end } function drawEarthMask() { // 这里的坐标系是哪个? 圆上的坐标系 ctx.save(); ctx.fillStyle = "rgba(0, 0, 0, 0.4)"; ctx.fillRect(0, -12, 40, 24); ctx.restore(); } }; </script>
1. 可视化介绍数据可视化(英语:Data visualization),主要旨在借助于图形化手段,清晰有效地传达与沟通信息。 为了清晰有效地传递信息,数据可视化通常使用柱状图、折线图、饼图、玫瑰图、散点图等图形来传递信息,也可以使用点、线、面、地图来对数字数据进行编码展示,以便在视觉上快速传达关键信息,可视化可以帮助用户分析和推理数据,让复杂的数据更容易理解和使用,有利于做出决策。就像你看下图中的表格很难看出什么,但是你看下面的可视化图标,就能很轻易的分辨出各类数据间的比较以及趋势。2. 可视化的历史1. 萌芽阶段早在 17 世纪以前,可视化就开始萌芽了,其中最早的地图在公元前 6200 年于土耳其地区出现。现代考古发现我国最早的地图实物,是出土于甘肃天水放马滩战国墓地一号墓中的《放马滩地图》。17 世纪末随着几何兴起、坐标系、以及人口统计学开端,人类开始了可视化思考的新模式,从此标记可视化的开端。1800-1849年:随着工艺设计的完善,统计图形爆炸性增长,包括柱状图, 饼图, 直方图, 折线图等。1826 年,查尔斯·杜品发明了使用连续黑白底纹来显示法国识字分布,这可能是第一张现代形式主题统计地图。2. 黄金阶段1850-1899:人们开始认识到数字信息对社会计划,工业化,商业和运输的重要性,此时统计理论开始诞生。1869年查尔斯·约瑟夫·米纳德,发布的拿破仑对 1812 年俄罗斯东征事件流图,被誉为有史以来最好的数据可视化。他的流图呈现了拿破仑军队的位置和行军方向、军队汇集、分散和重聚的时间和地点等信息。1879年 Luigi Perozzo 绘制立体图(三维人口金字塔)。标记着可视化开始进入了三维立体图。3. 重生阶段1950-1974 年:引领这次大潮的,首先是一个划时代的事件——计算机的诞生。计算机的出现彻底地改变了数据分析工作,计算机高分辨率和交互式的图形分析,提供了手绘时代无法实现的表现能力。随着统计应用的发展,数理统计把数据可视化变成了一门科学(如:计算机图形学、统计学、分析学),并运用到各行各业。1969年 John W. Tukey 在探索数据分析的图形时,发明箱型图。1982年乔治·罗里克(George Rorick)绘制彩色天气图开创了报纸上的彩色信息图形时代。1996年 Jason Dykes 发明了制图工具:一种地图可视化工具包,可以实时查看数据的图形工具。4. 分析学阶段2004 年至今以前可视化难以应对海量、高维、多源的动态数据的分析,进入21世纪,随着计算机的升级,对于以前难以应对数据,可以借用计算机来综合可视化、图形学、数据挖掘理论与方法来研究新的科学理论模型。通过这种模型来辅助用户从海量、复杂、矛盾的数据中快速挖掘出有用的数据,做出有效决策,这门新兴学科称为可视化分析学。可视化分析现在已大量应用在地图、物流、电力、水利、环保、交通、医学、监控、预警等领域。可视化分析降低了数据理解的难度,突破了常规统计分析的局限性。如下交通拥挤分析图。随着大数据的应用,如今可视化开发也变得越来越重要了。3. 可视化应用场景随着近几年大数据的快速发展,数据可视化技术也迅速被普及。目前数据可视化的应用非常广:如淘宝双十一活动时,借助于数据可视化展示公司实时交易数额,并可以实时动态观察。交管部门可实现对交通形态、卡口数据统计、违章分析、警力部署、出警分析、行车轨迹分析等智能交通大数据分析。企业各层可以借助数据可视化工具,可以直接在手机等设备上远程查看业务运营数据状况和关键指标。医院可以利用数据可视化工具,对医疗卫生数据进行可视化分析和研究应用,进而获取医疗卫生数据隐藏的价值。等等4. 可视化的解决方案前端可视化技术底层图形引擎:Skia 、OpenGL 等。W3C提供:CSS3、Canvas、SVG、WebGL。第三方的可视化库: ZRender、Echarts、 AntV 、Highcharts、D3.js 、three.js 和 百度地图、高德地图 等等。低代码可视化平台:阿里云(DataV)、腾讯云图、网易有数(EasyScreen)、帆软 等。
1. 适配方案1:rem + font-size我们都知道,在 css 中 1rem 等于 html 根元素设定的 font-size 的 px 值,通过动态的修改html 根元素的 font-size 大小就能动态的改变 rem 的大小,从而实现适配。原理动态设置 HTML 根字体大小将 px 转成 rem实现引入 lib-flexible 动态设置 HTML 根字体大小和 body 字体大小。(function flexible(window, document) { var docEl = document.documentElement; var dpr = window.devicePixelRatio || 1; // 调整 body 字体大小 function setBodyFontSize() { if (document.body) { // body 字体大小默认为 16px document.body.style.fontSize = 16 * dpr + "px"; } else { document.addEventListener("DOMContentLoaded", setBodyFontSize); } } setBodyFontSize(); // 移动端默认平均分成 10 等分(适用移动端) // pc端默认平均分成 24 等分(适用 pc 端) function setRemUnit() { var splitNum = /Mobi|Android|iPhone/i.test(navigator.userAgent) ? 10 : 24; var rem = docEl.clientWidth / splitNum; // 1920 / 24 = 80 docEl.style.fontSize = rem + "px"; // 设置 html 字体的大小 80px } setRemUnit(); // 页面调整大小时重置 rem 单位 window.addEventListener("resize", setRemUnit); window.addEventListener("pageshow", function (e) { if (e.persisted) { setRemUnit(); } }); // 检测 0.5px 支持 if (dpr >= 2) { var fakeBody = document.createElement("body"); var testElement = document.createElement("div"); testElement.style.border = ".5px solid transparent"; fakeBody.appendChild(testElement); docEl.appendChild(fakeBody); if (testElement.offsetHeight === 1) { docEl.classList.add("hairlines"); } docEl.removeChild(fakeBody); } })(window, document);将 px 转 rempx 转 rem 的方式有很多种:手动、less/scss 函数、cssrem 插件、webpack 插件、**Vite 插件。cssrem 插件转换vscode `root font-size` 设置为 80px。这个是 `px` 单位转 `rem` 的参考值。  接着就可以按照 1920px * 1080px 的设计稿愉快开发,此时页面已经是响应式,并且宽高比不变  webpack 插件转换安装 ```shell npm i webpack webpack-cli -D npm i style-loader css-loader html-webpack-plugin -D npm i postcss-pxtorem autoprefixer postcss-loader postcss -D ``` 配置 `webpack.config.js` ```js const HtmlWebpackPlugin = require("html-webpack-plugin"); const path = require("path"); module.exports = { entry: "./src/index.js", mode: "development", output: { filename: "[name].[contenthash].bundle.js", path: path.resolve("./dist"), }, module: { rules: [ { test: /\.css$/i, use: ["style-loader", "css-loader", "postcss-loader"], }, ], }, plugins: [ new HtmlWebpackPlugin({ template: "./index.html", }), ], }; ``` 配置 `postcss.config.js` 文件,`postcss-pxtorem 的配置` 可以查询 [文档](https://github.com/cuth/postcss-pxtorem) ```js module.exports = { plugins: { autoprefixer: {}, "postcss-pxtorem": { rootValue: 80, // 根元素的字体大小 unitPrecision: 5, // 小数点后精度 propList: ["*"], // 可以从px改变为rem的属性 exclude: /node_modules/i, // 要忽略并保留为px的文件路径 minPixelValue: 0, // 最小的px转化值(小于这个值的不转化) mediaQuery: false, // 允许在媒体查询中转换px selectorBlackList: [], // 要忽略并保留为px的选择器 replace: true, // 直接在css规则上替换值而不是添加备用 }, }, }; ``` 在 `main.js` 中引入`lib_flexible.js` `index.js` `index.css` ,最后重启项目即可。 :::tip{title="提示"} 这里我为了回顾一下 `webpack` 配置,就从 0 开始配置了。一般通过脚手架创建的项目会有集成webpack以及postcss的,只需要 安装一下 `postcss postcss-pxtorem` 与配置 `postcss.config.js` 即可 ::: :::warning{title="注意"} 由于 `viewport` 单位得到众多浏览器的兼容,`lib-flexible` 这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。下面就讲介绍 `viewport` 的方案。 ::: 2. 适配方案2:vw 单位(推荐)直接使用 vw 单位。屏幕的宽默认为 100vw,那么100vw = 1920px, 1vw = 19.2px 。实现将 px 转 vwcssrem 插件方式转换接着就可以按照 1920px * 1080px 的设计稿愉快开发,此时的页面已经是响应式,并宽高比不变webpack 插件转换安装npm i webpack webpack-cli -D npm i style-loader css-loader html-webpack-plugin -D npm i postcss-px-to-viewport autoprefixer postcss-loader postcss -Dwebpack.config.js 配置不变配置 postcss.config.jsmodule.exports = { plugins: { '@our-patches/postcss-px-to-viewport': { unitToConvert: 'px', // 要转化的单位 viewportWidth: 1920, // UI设计稿的宽度 unitPrecision: 6, // 转换后的精度,即小数点位数 propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换 viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw selectorBlackList: [], // 指定不转换为视窗单位的类名, minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换 mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false replace: true, // 是否转换后直接更换属性值 include: /\/src\/views\/pc\/layoutMapBS\//, exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配 landscape: false // 是否处理横屏情况 } } }:::warning{title="注意"}postcss-pxtoviewport 这个插件在文档中有 include 这个选项,但是作者一直没更新代码,导致这个选项一直无效,而且作者已经很久没改了。可以使用 @our-patches/postcss-px-to-viewport。安装npm i @our-patches/postcss-px-to-viewport -D配置只需要在 postcss.config.js 中将 postcss-px-to-viewport 改为 postcss-px-to-viewport 即可:::3. 适配方案3:scale(推荐)使用CSS3中的scale函数来缩放网页,这里我们将使用两种方案来实现:方案一:直接根据宽度的比率进行缩放。(宽度比率=网页当前宽 / 设计稿宽)<script> window.onload = function () { triggerScale(); window.addEventListener("resize", function () { triggerScale(); }); }; function triggerScale() { var targetX = 1920; var targetY = 1080; // 获取html的宽度和高度(不包含滚动条) var currentX = document.documentElement.clientWidth || document.body.clientWidth; // https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth var currentY = document.documentElement.clientHeight || document.body.clientHeight; // 1.缩放比例 3840 / 2160 => 2 var ratio = currentX / targetX; var bodyEl = document.querySelector("body"); // 2.需要修改缩放的原点 body { transform-origin: left top; } bodyEl.setAttribute("style", `transform:scale(${ratio})`); } </script>方案二:动态计算网页宽高比,决定是是否按照宽度的比率进行缩放。<script> window.onload = function () { triggerScale(); window.addEventListener("resize", function () { triggerScale(); }); }; function triggerScale() { var targetX = 1920; var targetY = 1080; var targetRatio = 16 / 9; var currentX = document.documentElement.clientWidth || document.body.clientWidth; var currentY = document.documentElement.clientHeight || document.body.clientHeight; // 1.缩放比例 3840 / 2160 => 2 var ratio = currentX / targetX; var currentRatio = currentX / currentY; var transformStr = ""; if (currentRatio > targetRatio) { ratio = currentY / targetY; transformStr = `transform:scale(${ratio}) translateX(-${ targetX / 2 }px); left:50%;`; } else { transformStr = `transform:scale(${ratio})`; } var bodyEl = document.querySelector("body"); // 2.需要修改缩放的原点 body { transform-origin: left top; } bodyEl.setAttribute("style", transformStr); } </script>4. 总结vw 相比于 rem 的优势:优势一:不需要去计算 html 的 font-size 大小,不需要给 html 设置 font-size,也不需要设置 body 的 font-size ,防止继承;优势二:因为不依赖 font-size 的尺寸,所以不用担心某些原因 html 的 font-size 尺寸被篡改,页面尺寸混乱;优势三:vw 相比于 rem 更加语义化,1vw 是 1/100 的 viewport 大小(即将屏幕分成 100 份); 并且具备 rem 之前所有的优点;vw 和 rem 存在问题如果使用 rem 或 vw 单位时,在 JS 中添加样式时,单位需要手动设置 rem 或 vw 。第三方库的字体等默认的都是 px 单位,比如:element、echarts,因此通常需要层叠第三方库的样式。当大屏比例更大时,有些字体还需要相应的调整字号。scale 相比 vw 和 rem 的优势优势一:相比于 vw 和 rem,使用起来更加简单,不需要对单位进行转换。优势二:因为不需要对单位进行转换,在使用第三方库时,不需要考虑单位转换问题。优势三:由于浏览器的字体默认最小是不能小于 12px ,导致 rem 或 vw 无法设置小于 12 px的字体,缩放没有这个问题。大屏开发 注意事项字体大小设置问题(非 scale 方案需要考虑)如果使用 rem 或 vw 单位时,在 JS 中添加样式时,单位需要手动设置 rem或 vw。第三方库的字体等默认的都是 px单位,比如:element、echarts,因此通常需要层叠第三方库的样式。当大屏比例更大时,有些字体还需要相应的调整字号。图片模糊问题切图时切 1 倍图、2 倍图,大屏用大图,小屏用小图。建议都使用SVG矢量图,保证放大缩小不会失真。Echarts 渲染引擎的选择使用 SVG 渲染引擎,SVG 图扩展性更好动画卡顿优化创建新的渲染层、启用 GPU 加速、善用 CSS3 形变动画少用渐变和高斯模糊、当不需要动画时,及时关闭动画
1. Git flow 规范Git 作为一个源码管理的工具,不可避免的会涉及到多人的协作,协作必须有一个规范的工作流程,让大家有效的合作使得项目井井有条的发展下去。工作流程在英语中叫 Workflow。原意是水流,比喻项目想水流哪像自然的流动,不会发生冲击、对撞甚至旋涡。Git flow 是最早诞生,并广泛采用的一种工作流,它采用了功能驱动开发(Feature-driven development,简称FDD),它指的是需求是开发的起点,现有需求再有功能分支Git flow 最主要的特点有两个项目始终存在两个长期分支 master(主分支) 和 develop(开发分支)除此之外,还有三个短期分支 feature(功能分支) hotfix(补丁分支) release(预发分支)分支描述master产品分支:只能从其他分支合并内容,不能再这个分支直接修改。合并到 maters 上的 commit 只能来自 release 分支或 hotfix 分支。develop开发主干分支:基于 master 的 tag 建立,主要用来暂时保存开发完成而又未发布的 feature 分支内容,以及 release 和 hotfix 的补充内容feature功能分支:一般一个新功能对应一个功能分支,从而和已经完成的功能隔离开来,而且只有在新功能完成开发的情况下,其对应的 feature 分支才会合并到主开发分支( develop 分支)上release预发分支:当需要发布时,我们从 develop 分支创建一个 release 分支,然后这个release 分支会发布到测试环境进行测试,如果发现问题就在这个分支直接进行修复。发布结束后,这个 release 分支会合并到 develop 和 master 分支,从而保证不会有代码丢失。hotfix补丁分支:主要用于紧急修复一些 Bug。会从 master 分支上的某个 tag 建立,修复结束后再合并到 develop 和 master 分支上2. Git commit 规范写好 Git commit 能提供更多的而历史信息,方便快速浏览,还能过滤某些 commit(比如文档改动),便于快速查找信息。如何优雅写好Git commit规范呢? 现在业界使用比较广泛的是 Angular规范<type>(<scope>):<subject> # 标题行:必填描述主要修改类型和内容。 // 空行 <body> # 主题内容:描述为什么修改,做了什么样的修改,以及开发的思路等等。 // 空行 <footer> 放 Breaking Changes 或 Closed Issued1. type 类型有:Type作用feat新增特性 (feature)fix修复 Bug(bug fix)docs修改文档 (documentation)style代码格式修改(white-space, formatting, missing semi colons, etc)refactor代码重构(refactor)perf改善性能(A code change that improves performance)test测试(when adding missing tests)build变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等)ci更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等chore变更构建流程或辅助工具(比如更改测试环境)revert代码回退scope 影响范围比如L route,component,utils,buildsubject:commit的描述body:commit具体修改内容,可以分为多行footer: 一些备注2. commitizen 工具1. 简介commitizen git commit 的格式化工具,为我们提供标准化的 commit 信息。 帮助我们统一项目commit , 便于信息的回溯或日志的生成。# commit message 格式2. 安装npm install -g commitizen cz-conventional-changelog echo '{"path": "cz-conventional-changelog"}' > ~/.czrc3. 使用在文件夹中修改内容git add * git cz具体操作步骤可以参考 规范(三):从 0 搭建 React+TS 项目 第七章 Git Commit 规范
1. GitHub Actions 是什么?GitHub Actions 是 GitHub 于 2018 年 10 月推出的一个 CI\CD 服务(持续集成和持续部署)。简单明了的说 就是你可以给你的代码仓库部署一系列自动化脚本,在你进行了提交/合并分支等操作后,自动执行脚本。通过 GitHub Actions 可快速搭建 GitHub Pages 静态网站(域名为 http://[username].github.io ),使用它来发布、测试、部署,是非常方便的大家知道,持续集成由很多操作组成,比如抓取代码、运行测试、登录远程服务器,发布到第三方服务等等。GitHub 把这些操作就称为 actions。 很多操作在不同项目里面是类似的,完全可以共享。GitHub 注意到了这一点,想出了一个很妙的点子,允许开发者把每个操作写成独立的脚本文件,存放到代码仓库,使得其他开发者可以引用。 如果你需要某个 action,不必自己写复杂的脚本,直接引用他人写好的 action 即可,整个持续集成过程,就变成了一个 actions 的组合。这就是 GitHub Actions 最特别的地方。2. Github Actions 概念GitHub Actions 有一些自己的术语。workflow (工作流程):持续集成一次运行的过程,就是一个 workflow。job (任务):一个 workflow 由一个或多个 jobs 构成,含义是一次持续集成的运行,可以完成多个任务。step(步骤):每个 job 由多个 step 构成,一步步完成。action (动作):每个 step 可以依次执行一个或多个命令(action)。3. workflow 文件GitHub Actions 的配置文件叫做 workflow 文件,存放在代码仓库的 .github/workflows目录。workflow 文件采用 YAML 格式,文件名可以任意取,但是后缀名统一为 .yml ,比如foo.yml。一个库可以有多个 workflow 文件。GitHub 只要发现 .github/workflows目录里面有 .yml 文件,就会自动运行该文件。workflow 文件的配置字段非常多,详见 官方文档一些基本字段。 (1)namename字段是 workflow 的名称。如果省略该字段,默认为当前 workflow 的文件名。name: GitHub Actions Demo(2) onon字段指定触发 workflow 的条件,通常是某些事件。on: push上面代码指定,push事件触发 workflow。on字段也可以是事件的数组。on: [push, pull_request]上面代码指定,push事件或 pull_request 事件都可以触发 workflow。完整的事件列表,请查看 官方文档 。除了代码库事件,GitHub Actions 也支持外部事件触发,或者定时运行。(3) on.<push|pull_request>.<tags|branches>指定触发事件时,可以限定分支或标签。on: push: branches: - master上面代码指定,只有 master 分支发生 push 事件时,才会触发 workflow。(4) jobs.<job_id>.nameworkflow 文件的主体是 jobs 字段,表示要执行的一项或多项任务。jobs 字段里面,需要写出每一项任务的 job_id,具体名称自定义。job_id里面的name字段是任务的说明。jobs: my_first_job: name: My first job my_second_job: name: My second job上面代码的 jobs 字段包含两项任务,job_id 分别是 my_first_job 和my_second_job。(5) jobs.<job_id>.needsneeds 字段指定当前任务的依赖关系,即运行顺序。jobs: job1: job2: needs: job1 job3: needs: [job1, job2]上面代码中,job1 必须先于 job2 完成,而 job3 等待 job1 和 job2 的完成才能运行。因此,这个 workflow 的运行顺序依次为:job1、job2、job3。(6) jobs.<job_id>.runs-onruns-on字段指定运行所需要的虚拟机环境。它是必填字段。目前可用的虚拟机如下。ubuntu-latest,ubuntu-18.04或ubuntu-16.04 windows-latest,windows-2019或windows-2016 macOS-latest或macOS-10.14下面代码指定虚拟机环境为 ubuntu-18.04。runs-on: ubuntu-18.04(7) jobs.<job_id>.stepssteps 字段指定每个 Job 的运行步骤,可以包含一个或多个步骤。每个步骤都可以指定以下三个字段。jobs.<job_id>.steps.name:步骤名称。jobs.<job_id>.steps.run:该步骤运行的命令或者 action。jobs.<job_id>.steps.env:该步骤所需的环境变量。下面是一个完整的 workflow 文件的范例。name: Greeting from Mona on: push jobs: my-job: name: My Job runs-on: ubuntu-latest steps: - name: Print a greeting env: MY_VAR: Hi there! My name is FIRST_NAME: Mona MIDDLE_NAME: The LAST_NAME: Octocat run: | echo $MY_VAR $FIRST_NAME $MIDDLE_NAME $LAST_NAME.上面代码中,steps字段只包括一个步骤。该步骤先注入四个环境变量,然后执行一条 Bash 命令。4. 实例:React 项目发布到 GitHub Pages(1) 第一件事情是我们需要先创建一个 GitHub 密钥,因为我们需要将示例部署至 Github Page ,需要写权限,创建完成后将这个秘钥保存在当前仓库的 Settings/Secrets 里面。创建秘钥可以参考 官方文档 。点击自己头像,选择 Settings :在左边栏选择 Developer settings:然后在左边栏选择 Personal access tokens 点击头上的 Generate new token 创建一个新的 Token :注意: 创建完成后需要保存好这个 Token ,它只会出现这一次。接下来,在github中创建一个项目,我这里创建的名字叫做 github-actions-demo ,然后点击项目中的 Settings ,在 Secrets 的栏目中的 Actions, 点击右上角的New repository secret,将刚才创建的 Token 填写进去:(2) 接下来是创建一个标准的 React 应用:npx create-react-app github-actions-demo(3) 打开项目中的package.json文件,添加一个homepage字段,如下:"homepage": "https://[username].github.io/github-actions-demo",将[username]替换成你自己的 GitHub 用户名(4) 在个人代码仓库中找到 action,如果你是一个前端项目,可以使用 Node.js 的模板,点击 new workflow ,生产 workflow 文件或者 这个项目中,在 .github/workflows 的目录中手动新增一个 workflow 文件,名字可以随便取,这个我这里的名称是 ci.yml(5) 我们来看看Github Action配置文件的基本构成,配置文件格式是.yml,示例如下:name: GitHub Actions Build and Deploy Demo on: push: branches: - master jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. with: persist-credentials: false - name: Install and Build run: | npm install npm run-script build - name: Deploy uses: JamesIves/github-pages-deploy-action@releases/v3 with: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} BRANCH: gh-pages FOLDER: build上面这个 workflow 文件的要点如下。整个流程在 master 分支发生 push 事件时触发。只有一个 job ,运行在虚拟机环境 ubuntu-latest。第一步是获取源码,使用的 action 是 actions/checkout@v2 。如果用的是这个版本必须得加上 persist-credentials:false第二步是构建,使用npm install npm run-script build命令。第三步是部署,使用的 action 是 JamesIves/github-pages-deploy-action@releases/v3。第三步需要三个环境变量,分别为 GitHub 密钥、发布分支、构建成果所在目录、构建脚本。其中,只有 GitHub 密钥是秘密变量(就是之前配置的变量),需要写在双括号里面,其他三个都可以直接写在文件里。(6) 保存上面的文件后,将整个仓库推送到 GitHub。GitHub 发现了 workflow 文件以后,就会自动运行。你可以在网站上实时查看运行日志,日志默认保存30天。(7) 在setting中设置一下Github Page等到项目部署成功后,访问 GitHub Page,会看到构建成果已经发上网了。然后每次推送到 mater 分支,Github Action 都会自动运行,将构建产物发布至 Github Page
本篇文章目标CI/CD是什么?CI 和 CD 的实现1. CI/CD是什么?CI的全称是 Continuous Integration 也就是持续集成CD其实对应两个概念:CD-持续交付(Continuous Delivery)CD-持续部署(Continuous Deployment)1.1 CI:持续集成(Continuous Integration)“In software engineering,continuous integration (CI) is the practive of merging all developers' working copies to a shared mainline several times a day”开发人员提交新代码之后,立即进行构建、(单元)测试。根据测试结果骂我们可以确定新代码和原有代码能否正确地集成在一起。CI就是高频地、自动化地将个人开发的代码集成到开发主线中高频:高频才能保证不会出现巨大变更引起的严重问题自动化:自动化才可以保证高频主线:不一定是master分支1.2 CD-持续交付(Continuous Delivery)“Continuous delivery(CD) is a software engineering approach in which teams produce software in short cycles,ensuring that the software can be reliably released at any time and, when releasing the sofeware,doing so manually”在持续继承的基础上,将集成后的代码部署到更贴近真实运行环境的类生产环境中尽快的交付给 QA 人员在类生产环境中测试保证有一个“随时可发布”的版本部署可以是手动的1.3 CD-持续部署(Continuous Deployment)“Continuous Deployment(CD) is softwrae engineering approach in which software functionalities are delivered frequently through automated deployments”在持续交付的基础上,能够自动化地讲软件部署在真实生产环境尽快地交付给用户敏捷开发思想的体现实际上持续交付和持续部署这两个概念不用分的特别清楚,在实践中,我们往往认为持续交付和持续部署这两者是一体的1.4 CI/CD的意义和价值敏捷开发思想的体现频繁、自动化、可重复。流程像管道,代码像水快速失败,尽早出现问题,尽早解决不能保证BugFree,所以提高发布频率,降低单次发布风险快速交付用户价值,拥抱市场变化2. CI和CD的实现2.1 CI的实现CI重在个人对团队的交付,所以他关注的点有两个:保证协作质量代码风格检查版本规范Git分支规范自动化单元测试、端到端测试...保证失败可回溯测试结果通知Changelog记录Code Review机制...2.2 CD的实现持续交付和持续部署的过程是对QA和最终用户的交付的过程持续交付多种级别的测试环境QA团队对功能测试的快速响应自动化测试覆盖率的检查发布流程的标准化...持续部署完善的项目迭代机制渐进式的发布策略线上监控告警快速回滚能力...
1. TS在工程项目中的模块使用及配置1.1 声明文件什么是声明文件?声明文件就是给 js 代码补充类型标注. 这样在 ts 编译环境下就不会提示 js 文件"缺少类型".声明变量使用关键字 declare 来表示声明其后面的全局变量的类型, 比如:// packages/global.d.ts declare var __DEV__: boolean declare var __TEST__: boolean declare var __BROWSER__: boolean declare var __RUNTIME_COMPILE__: boolean declare var __COMMIT__: string declare var __VERSION__: string上面代码表示 __DEV__ 等变量是全局, 并且标注了他们的类型. 这样无论在项目中的哪个 ts 文件中使用 __DEV__, 变量 ts 编译器都会知道他是 boolean 类型.声明文件在哪里?声明文件的文件名是有规范要求的, 必须以 .d.ts 结尾。声明文件放在项目里的任意路径/文件名都可以被 ts 编译器识别, 但实际开发中发现, 为了规避一些奇怪的问题, 推荐放在根目录下.声明文件对纯js项目有什么帮助?即便你只写 js 代码, 也可以安装声明文件, 因为如果你用的是 vscode , 那么他会自动分析 js 代码, 如果存在对应的声明文件, vscode 会把声明文件的内容作为代码提示。1.2. @types和DefinitelyTyped仓库DefinitelyTyped 是一个高质量的 TypeScript 类型定义的仓库。通过 @types 方式来安装常见的第三方JavaScript库的声明适配模块1.3. lib.d.ts当你安装 TypeScript 时,会顺带安装 lib.d.ts 等声明文件。此文件包含了 JavaScript 运行时以及 DOM 中存在各种常见的环境声明。它自动包含在 TypeScript 项目的编译上下文中;它能让你快速开始书写经过类型检查的 JavaScript 代码。你可以通过指定 —noLib 的编译器命令行标志(或者在 tsconfig.json 中指定选项 noLib: true)从上下文中排除此文件。看如下例子const foo = 123; const bar = foo.toString();这段代码的类型检查正常,因为 lib.d.ts 为所有 JavaScript 对象定义了 toString 方法。如果你在 noLib 选项下,使用相同的代码,这将会出现类型检查错误:const foo = 123; const bar = foo.toString(); // Error: 属性 toString 不存在类型 number 上1.4. tsconfig.json配置文件在 TS 的项目中,TS 最终都会被编译 JS 文件执行,TS 编译器在编译 TS 文件的时候都会先在项目根目录的 tsconfig.json 文件,根据该文件的配置进行编译,默认情况下,如果该文件没有任何配置,TS 编译器会默认编译项目目录下所有的 .ts、.tsx、.d.ts文件。实际项目中,会根据自己的需求进行自定义的配置,下面就来详细了解下tsconfig.json的文件配置。文件选项配置files : 表示编译需要编译的单个文件列表"files": [ // 指定编译文件是src目录下的a.ts文件 "scr/a.ts" ]include: 表示编译需要编译的文件或目录"include": [ // "scr" // 会编译src目录下的所有文件,包括子目录 // "scr/*" // 只会编译scr一级目录下的文件 "scr/*/*" // 只会编译scr二级目录下的文件 ]exclude:表示编译器需要排除的文件或文件夹默认排除node_modules文件夹下文件"exclude": [ // 排除src目录下的lib文件夹下的文件不会编译 "src/lib" ]extends: 引入其他配置文件,继承配置// 把基础配置抽离成tsconfig.base.json文件,然后引入 "extends": "./tsconfig.base.json"compileOnSave:设置保存文件的时候自动编译vscode暂不支持该功能,可以使用'Atom'编辑器"compileOnSave": true执行 tsc --init 生成的ts.config.js会有六个初始设置{ "compilerOptions":{ "target":"es2016", // 指定编译成的是哪个版本的js "module":"commonjs", // 指定要使用的模块化的规范 "esModuleInterop":true, // 兼容JS模块无default的导入 "forceConsistentCasingInFileNames":true, // 兼容JS模块无default的导入 "strict":true, // 所有严格检查的总开关 "skipLibCheck":true // 跳过所有.d.ts文件的类型检查 } }Vue3中使用TS在Vue组合式API中它会有默认的自动类型注解,另外我们可以使用泛型进行复杂类型注解。自动类型注解<script setup lang="ts"> import { ref } from "vue"; let count = ref(0); count.value = "123"; // 不能将类型“string”分配给类型“number”。 </script>手动类型注解<script setup lang="ts"> import { ref } from "vue"; let count = ref<string|number>(0); count.value = "123"; // √ </script>复杂类型注解<script setup lang="ts"> import { ref } from "vue"; interface List { } let count = ref<string|number>(0); count.value = "123"; // √ </script>Vue3 + TS 组件通信父子通信// parent.vue <my-child :count="count"></my-child> // my-child // 1. vue自带的定义方式 defineProps({ count: [Number] }) // 2. ts的方式 interface Props({ count: number }) defineProps<Props>()子父通信// parent.vue <my-child @say-hello="sayHello"></my-child> const sayHello = (message: string) => { console.log({ message }); }; // my-child interface Emits { (e: "say-hello", message: string): void; } let emit = defineEmits<Emits>(); emit("say-hello", "hello-world");VueRouter + TSRouteRecordRaw -> 路由表选项类型const routes: Array<RouteRecordRaw> = [ { path: "/", name: "home", component: HomeView, } ];RouteMeta -> 扩展meta的类型declare module "vue-router" { interface RouteMeta { // 是可选的 isAdmin?: boolean; // 每个路由都必须声明 requiresAuth: boolean; } }RouterOptions -> createRouter的配置类型RouteLocationNormalized -> 标准化的路由地址Router -> router的实例类型调用路由的方式import { userRouter, useRoute } from 'vue-router' const router = useRouter() // 类似 this.$router const route = useRoute() // 类似 this.$routeVuex +TS导出key导出key重写useStore使用store// store.ts import { createStore, Store, useStore as baseUseStore } from "vuex"; import { InjectionKey } from "vue"; export interface State { count: number; } // step 3 重写useStore export function useStore() { return baseUseStore(key); } // step 1 导入key export const key: InjectionKey<Store<State>> = Symbol(); export default createStore<State>({ state: { count: 1, }, getters: {}, mutations: {}, actions: {}, modules: {}, }); // main.ts import store, { key } from "./store"; // step 2 导出key app.use(store,key)// App.vue import { useStore } from '@/store' // step 4 使用store const store = useStore() console.log(store.state.count)Pinia如何使用TS首先在main.ts中注册Pinaimport { createPina } from 'pinia' const pinia = createPinia() createApp(App).use(pinia).mount('#app')创建 /stores/counter.ts文件import { defineStore } from 'pinia' interface Counter { counter: number } export const useCounterStore = defineStore('counterStore', { state: (): Counter => ({ counter: 0 }), actions: { add(n : number) { this,counter += n } } })在App.vue中使用import { storeToRefs } from 'pinia import { useCounterStore } from './stores/counter' let counterStore = useCounter() let { counter } = storeToRefs(counterStore) let handleClick = () => { counterStore.add(2) }Pinia除了选项式写法外,也支持组合式写法,主要利用的就是Vue组合式API来实现的import { defineStore } from 'pinia' impoer { ref } from 'vue' export const useCounterStore = defineStore('counterStore', () => { conter = ref<number>(0) return { counter } })Element Plus中如何使用TS如果使用Volar,在ts.config.json中通过comiplerOptions.types指定全局组件类型"types": ["element-plus/global"],会有更好的提示。
1. 前言最近自己学习写了一个基于Vue3的组件库,感觉有点意思,这篇文章来记录一下我是怎么从0快速打造一个UI组件库的附上访问地址jw-ui上面网址打不开的话可以用这个jw-ui2. 使用Vite搭建官网Vite是尤雨溪开发的一种新型前端构建工具,具体介绍可以查看官方文档2.1 创建项目2.1.1. 全局安装vite(这里我装的时候是2.7.2)$ yarn create vite@2.7.22.1.2. 构建一个vue模板(项目名可以改成自己的名字)yarn create vite jw-ui --template vue2.1.3. 装好之后按照提示逐步执行命令cd jw-ui yarn yarn dev可以看到界面ps: 推荐的IDE和设置:VSCode + Volar2.2 基本完成官网的搭建2.2.1. 下载vue-routeryarn add vue-router@42.2.2. 创建home首页与doc文档页 以及顶部导航栏/* /views/home/index.vue 首页*/ <template> <div> Home </div> </template>/* /views/doc/index.vue 文档页面 */ <template> <div> Doc </div> </template>/* /components/Topnav.vue 顶部导航栏组件 */ <template> <div class="topnav"> <router-link to="/home">首页</router-link> <router-link to="/doc">文档</router-link> </div> </template>2.2.3. 配置路由创建路由配置文件// router/index.ts import { createRouter, createWebHashHistory } from "vue-router"; const history = createWebHashHistory(); const router = createRouter({ history, routes: [ { path: "/", redirect: "" }, ], }); export default router;在main.ts里导入,使得整个应用支持路由。import { createApp } from "vue"; import App from "./App.vue"; import router from "./router"; const app = createApp(App); app.use(router); app.mount("#app"); 修改App.vue<template> <Topnav /> <router-view /> </template> <script setup> import Topnav from "./components/Topnav.vue"; </script>到目前为止的效果装饰一下顶部导航栏后的效果这里首页按照自己喜欢的来写CSS就好了,接下来讲一下文档页面。文档页需要一个侧边栏来切换不同组件的文档,这里我就举例做一个Button组件// doc/index.vue <template> <div class="layout"> <div class="content"> <aside> <router-link class="menu-item text-overflow" to="/doc/button" >Button 组件</router-link > </aside> <main style="padding-left: 302px"> <router-view /> </main> </div> </template>// router/index.ts 添加一个展示的button页面 import { createRouter, createWebHashHistory } from "vue-router"; import Home from "../views/home/index.vue"; import Doc from "../views/doc/index.vue"; import ButtonDoc from "../views/doc/button/index.vue"; const history = createWebHashHistory(); const router = createRouter({ history, routes: [ { path: "/", redirect: "/home" }, { path: "/home", component: Home }, { path: "/doc", component: Doc, children: [{ path: "button", component: ButtonDoc }], }, ], }); export default router;// /views/doc/button/index <template> <Button /> </template> <script setup> import Button from '../../../lib/button/index.vue' </script> <style lang="scss" scoped> </style> 展示效果好了到这里官网总算是基本搭建完了,我们终于就可以愉快的在src/lib/button/index.vue文件里封装组件啦。(封装的组件都放在lib文件夹里,以后打包用)3. 封装一个Button组件下面附上我写的一个Button组件以及使用效果PS: 需要注意的一点是封装的样式一定要加自己独特的前缀我这里是 jw 以避免在项目中产生样式重叠<template> <button class="jw-button" :class="classes"> <span v-if="loading" class="jw-loadingIndicator"></span> <slot> {{ theme }} </slot> </button> </template> <script setup lang="ts"> import { computed } from "vue"; const props = defineProps({ theme: { type: String, default: "default", }, dashed: { type: Boolean, default: false, }, size: { type: String, default: "default", }, round: { type: Boolean, default: false, }, disabled: { type: Boolean, default: false, }, loading: { type: Boolean, default: false, }, }); const { theme, dashed, size, round, disabled } = props; const classes = computed(() => { return { [`jw-theme-${theme}`]: theme, [`jw-theme-dashed`]: dashed, [`jw-size-${size}`]: size, [`is-round`]: round, [`is-disabled`]: disabled, }; }); </script> <script lang="ts"> export default { name: "JwButton", }; </script> <style lang="scss" scoped> $h-default: 32px; $h-small: 20px; $h-large: 48px; $white: #fff; $default-color: #333; $primary-color: #36ad6a; $info-color: #4098fc; $success-color: #85ce61; $warning-color: #f0a020; $error-color: #d03050; $grey: grey; $default-border-color: #d9d9d9; $radius: 3px; $green: #18a058; .jw-button { box-sizing: border-box; height: $h-default; background-color: #fff; padding: 0 12px; cursor: pointer; display: inline-flex; justify-content: center; align-items: center; white-space: nowrap; border-radius: $radius; box-shadow: 0 1px 0 fade-out(black, 0.95); transition: all 250ms; color: $default-color; border: 1px solid $default-border-color; user-select: none; &:focus { outline: none; } &::-moz-focus-inner { border: 0; } &.jw-size-large { font-size: 24px; height: $h-large; padding: 0 16px; } &.jw-size-small { font-size: 12px; height: $h-small; padding: 0 8px; } &.is-round.jw-size-default { border-radius: calc($h-default / 2); } &.is-round.jw-size-large { border-radius: calc($h-large / 2); } &.is-round.jw-size-small { border-radius: calc($h-small / 2); } &.jw-theme-default { &:hover { color: $green; border-color: $green; > .jw-loadingIndicator { border-style: dashed; border-color: $green $green $green transparent; } } &:active { color: darken($green, 20%); border-color: darken($green, 20%); > .jw-loadingIndicator { border-style: dashed; border-color: darken($green, 20%) darken($green, 20%) darken($green, 20%) transparent; } } &.jw-theme-dashed { border-style: dashed; } > .jw-loadingIndicator { border-style: dashed; border-color: $default-color $default-color $default-color transparent; } } &.jw-theme-primary { background-color: $primary-color; border-color: $primary-color; color: $white; &:hover { background: lighten($primary-color, 20%); border-color: lighten($primary-color, 20%); } &:active { background-color: darken($primary-color, 20%); border-color: darken($primary-color, 20%); } &.is-disabled { cursor: not-allowed; background: lighten($primary-color, 20%); border-color: lighten($primary-color, 20%); &:hover { background: lighten($primary-color, 20%); border-color: lighten($primary-color, 20%); } } &.jw-theme-dashed { border-style: dashed; background-color: $white !important; color: $primary-color; > .jw-loadingIndicator { border-style: dashed; border-color: $primary-color $primary-color $primary-color transparent; } } } &.jw-theme-info { background-color: $info-color; border-color: $info-color; color: $white; &:hover { background: lighten($info-color, 20%); border-color: lighten($info-color, 20%); } &:active { background-color: darken($info-color, 20%); border-color: darken($info-color, 20%); } &.is-disabled { cursor: not-allowed; background: lighten($info-color, 20%); border-color: lighten($info-color, 20%); &:hover { background: lighten($info-color, 20%); border-color: lighten($info-color, 20%); } } &.jw-theme-dashed { border-style: dashed; background-color: $white !important; color: $info-color; > .jw-loadingIndicator { border-style: dashed; border-color: $info-color $info-color $info-color transparent; } } } &.jw-theme-success { background-color: $success-color; border-color: $success-color; color: $white; &:hover { background: lighten($success-color, 20%); border-color: lighten($success-color, 20%); } &:active { background-color: darken($success-color, 20%); border-color: darken($success-color, 20%); } &.is-disabled { cursor: not-allowed; background: lighten($success-color, 20%); border-color: lighten($success-color, 20%); &:hover { background: lighten($success-color, 20%); border-color: lighten($success-color, 20%); } } &.jw-theme-dashed { border-style: dashed; background-color: $white !important; color: $success-color; > .jw-loadingIndicator { border-style: dashed; border-color: $success-color $success-color $success-color transparent; } } } &.jw-theme-warning { background-color: $warning-color; border-color: $warning-color; color: $white; &:hover { background: lighten($warning-color, 20%); border-color: lighten($warning-color, 20%); } &:active { background-color: darken($warning-color, 20%); border-color: darken($warning-color, 20%); } &.is-disabled { cursor: not-allowed; background: lighten($warning-color, 20%); border-color: lighten($warning-color, 20%); &:hover { background: lighten($warning-color, 20%); border-color: lighten($warning-color, 20%); } } &.jw-theme-dashed { border-style: dashed; background-color: $white !important; color: $warning-color; > .jw-loadingIndicator { border-style: dashed; border-color: $warning-color $warning-color $warning-color transparent; } } } &.jw-theme-error { background-color: $error-color; border-color: $error-color; color: $white; &:hover { background: lighten($error-color, 20%); border-color: lighten($error-color, 20%); } &:active { background-color: darken($error-color, 20%); border-color: darken($error-color, 20%); } &.is-disabled { cursor: not-allowed; background: lighten($error-color, 20%); border-color: lighten($error-color, 20%); &:hover { background: lighten($error-color, 20%); border-color: lighten($error-color, 20%); } } &.jw-theme-dashed { border-style: dashed; background-color: $white !important; color: $error-color; > .jw-loadingIndicator { border-style: dashed; border-color: $error-color $error-color $error-color transparent; } } } > .jw-loadingIndicator { width: 14px; height: 14px; display: inline-block; margin-right: 4px; border-radius: 8px; border-color: $white $white $white transparent; border-style: solid; border-width: 2px; animation: jw-spin 1s infinite linear; } } @keyframes jw-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> 虽然有不完美,但差不多就这个意思吧4. 封装Markdown组件介绍文档4.1. 下载vite-plugin-markdown:一个插件可以让你导入Markdown文件作为各种格式的vite项目。github-markdown-css:复制GitHub Markdown风格yarn add github-markdown-css vite-plugin-markdown4.2. main.ts中引入import "github-markdown-css";4.3. vite.config.js中配置vite-plugin-markdown插件import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' const md = require("vite-plugin-markdown"); export default defineConfig({ plugins: [vue(), md.plugin({ mode: ["html", "vue"], }),] }) 4.4. 封装Markdown组件// /components/Markdown.vue <template> <article class="markdown-body" v-html="content"></article> </template> <script setup lang="ts"> // 传入的md文件 const props = defineProps({ content: { type: String, required: true, }, }); </script> 4.5. 创建介绍页面路由import { h } from "vue"; import { createRouter, createWebHashHistory } from "vue-router"; import Home from "../views/home/index.vue"; import Doc from "../views/doc/index.vue"; import ButtonDoc from "../views/doc/button/index.vue"; const history = createWebHashHistory(); import Markdown from "../components/Markdown.vue"; const md = (string) => h(Markdown, { content: string, key: string }); import { html as Intro } from "../../markdown/intro.md"; const IntroDoc = md(Intro); const router = createRouter({ history, routes: [ { path: "/", redirect: "/home" }, { path: "/home", component: Home }, { path: "/doc", component: Doc, children: [ { path: "intro", component: IntroDoc }, { path: "button", component: ButtonDoc }, ], }, ], }); export default router; 可以看到,最终md就能导入,并且生成了github上md的样式了5. 自定义代码块获取组件展示源代码5.1. 自定义插件vue-custom-blocks-pluginimport path from "path"; import fs from "fs"; import { baseParse } from "@vue/compiler-core"; const vitePluginVue = { name: "preview", transform(code, id) { if ( !/\/src\/views\/doc\/.*\.preview\.vue/.test(id) || !/vue&type=preview/.test(id) ) { return; } let path = `.${id.match(/\/src\/views\/doc\/.*\.preview\.vue/)[0]}`; const file = fs.readFileSync(path).toString(); const parsed = baseParse(file).children.find((n) => n.tag === "preview"); const title = parsed.children[0].content; const main = file.split(parsed.loc.source).join("").trim(); return `export default function (Component) { Component.__sourceCode = ${JSON.stringify(main)} Component.__sourceCodeTitle = ${JSON.stringify(title)} }`.trim(); }, }; export default vitePluginVue; 5.2. 在vite.config.ts中配置import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' const md = require("vite-plugin-markdown"); import vitePluginVue from "./plugins/vue-custom-blocks-plugin"; export default defineConfig({ plugins: [vue(), md.plugin({ mode: ["html", "vue"], }), vitePluginVue] }) 5.3. 封装Preview组件展示<template> <div class="pre"> <h2> {{ component.__sourceCodeTitle }} <Button @click="hideCode" v-if="codeVisible">隐藏代码</Button> <Button @click="showCode" v-else>查看代码</Button> </h2> <div class="pre-component"> <component :is="component" /> </div> <div class="pre-code" v-if="codeVisible"> <pre class="language-html">{{ component__sourceCOde }}</pre> </div> </div> </template> <script setup lang="ts"> import Button from "../lib/button/index.vue"; import { computed, ref } from "vue"; const props = defineProps({ component: Object, }); const showCode = () => (codeVisible.value = true); const hideCode = () => (codeVisible.value = false); const codeVisible = ref(false); </script> <style lang="scss" scoped> $border-color: #d9d9d9; .pre { border: 1px solid $border-color; margin: 16px 0px 32px; max-width: 700px; min-width: 300px; > h2 { font-size: 20px; padding: 8px 16px; border-bottom: 1px solid $border-color; display: flex; justify-content: space-between; } &-component { padding: 16px; } &-actions { padding: 8px 16px; border-top: 1px dashed $border-color; } &-code { padding: 8px 16px; border-top: 1px dashed $border-color; > pre { line-height: 1.1; font-family: Consolas, "Courier New", Courier, monospace; margin: 0; background-color: #fff; } } } </style> 5.4. 使用Preview组件views/doc/button/index.vue<template> <div> <Preview :component="Button1" /> </div> </template> <script setup> import Button1 from "./Button1.preview.vue"; import Preview from "../../../components/Preview.vue"; </script> <style lang="scss"> .jw-button + .jw-button { margin-left: 20px; } </style> /views/doc/button/Button1.preview.vue<preview>基础示例</preview> <template> <Button /> </template> <script setup lang="ts"> import Button from "../../../lib/button/index.vue"; </script> 现在,只要编写上面的以.preview.vue后缀的文件就行了。效果:5.5. 高亮源代码下载prismjsyarn add prismjs对Preview组件做修改<template> <div class="pre"> <h2> {{ component.__sourceCodeTitle }} <Button @click="hideCode" v-if="codeVisible">隐藏代码</Button> <Button @click="showCode" v-else>查看代码</Button> </h2> <div class="pre-component"> <component :is="component" /> </div> <div class="pre-code" v-if="codeVisible"> <pre class="language-html" v-html="html" /> </div> </div> </template> <script setup lang="ts"> import Button from "../lib/button/index.vue"; import { computed, ref } from "vue"; import "prismjs"; import "prismjs/themes/prism.css"; const Prism = (window as any).Prism; const props = defineProps({ component: Object, }); console.log(props.component.__sourceCode); const html = computed(() => { return Prism.highlight( props.component.__sourceCode, Prism.languages.html, "html" ); }); const showCode = () => (codeVisible.value = true); const hideCode = () => (codeVisible.value = false); const codeVisible = ref(false); </script>效果6. 去掉示例中的文件导入6.1. 在lib目录下创建main.ts 这个也是作为之后打包上传至npm的入口import { App } from "vue"; import JwButton from "./button/index.vue"; export { JwButton }; const components = [JwButton]; // 全局注册主键 export function registerJwUi(app: App): void { for (const component of components) { app.component(component.name, component); } } export default registerJwUi; 6.2. main.ts中导入注册import JwUi from "./lib/index"; app.use(JwUi);6.3. 这样在示例中就可以直接用了/src/views/doc/button/Button1.preview<preview>基础示例</preview> <template> <jw-button /> </template>6.4. 效果7. 部署到github官网7.1. 打包yarn build7.2. 上传至githubgithub创建一个新的仓库将dist上传只仓库7.3. 进入仓库Settings最底层7.4. 找到GitHub Pages7.5. 选择master分支 点击保存 链接就生成了7.6 一键部署创建deploy.sh文件rm -rf dist && yarn build && cd dist && git init && git add . && git commit -m "update" && git branch -M master && git remote add origin git@github.com:coderyjw/jw-ui-website.git && git push -f -u origin master && cd - echo https://coderyjw.github.io/jw-ui-website/执行命令sh deploy.sh8. 上传至npm8.1. 创建rollup.config.js配置文件// 为了保证版本一致,请复制我的 package.json 到你的项目,并把 name 改成你的库名 import esbuild from 'rollup-plugin-esbuild' import vue from 'rollup-plugin-vue' import scss from 'rollup-plugin-scss' import dartSass from 'sass'; import { terser } from "rollup-plugin-terser" import alias from '@rollup/plugin-alias' import path from "path"; import resolve from 'rollup-plugin-node-resolve' export default { input: 'src/lib/index.ts', output: [{ globals: { vue: 'Vue' }, name: 'Yjw-ui', file: 'dist/lib/yjw-ui.js', format: 'umd', plugins: [terser()] }, { name: 'Yjw-ui', file: 'dist/lib/yjw-ui.esm.js', format: 'es', plugins: [terser()] }], plugins: [ scss({ include: /\.scss$/, sass: dartSass }), esbuild({ include: /\.[jt]s$/, minify: process.env.NODE_ENV === 'production', target: 'es2015' }), vue({ include: /\.vue$/, }), alias({ entries: [ { find: '@', // 别名名称,作为依赖项目需要使用项目名 replacement: path.resolve(__dirname, 'src'), customResolver: resolve({ extensions: ['.js', '.jsx', '.vue', '.sass', '.scss'] }) } ] }), ], }8.2. 执行命令打包rollup -c8.3. 效果可以看到dist文件下有lib文件,就是打包后的文件8.4. 上传至npm需要先注册npm账号npm login // 先登录 npm publish // 发布9. 最后ok,终于写完了,如果你觉得我写的不错,麻烦点个赞再走吧~如果觉得我写的有错的,麻烦指出再点个赞鼓励一下吧。原文地址
2023年02月
2023年01月