CrossingMay

CrossingMay 查看完整档案

杭州编辑浙江传媒学院  |  数字媒体技术 编辑杭州宏视科技  |  web前端开发 编辑 crossingmay.com 编辑
编辑

just for the future

个人动态

CrossingMay 赞了文章 · 2020-12-28

有关 npm 包的本地开发和调试

前言

接上一篇 利用AST自动为函数增加错误上报我们开发了一个 Babel plugin(npm 包), 本文我们继续探讨下如何在本地开发和调试 npm 包。

当 npm 包还在开发阶段不可能一边发布一边调试,不然也太繁琐了。不同场景下调试方法有很多,比如现在 npm 包中利用 node 直接运行调试,或者直接在工程的 node_modules 中将引用修改后直接修改源码调试。本文我将介绍其中一种,也是我最常用的一种。

npm link

进入本地的 babel-plugin-function-try-catch 这个 npm 包 的根目录执行:

npm link

上面的命令可以将当前的这个包安装在全局(mac 中的路径是 /usr/local/bin),也就是 npm i -g 安装包的目录。

执行后结果如图:

这样在本地的 global 下就会存在一个这样的 npm 包。

由于这是通过软链接的方式实现的,当我们编辑了本地的 babel-plugin-function-try-catch 模块中的代码,可以直接在安装它的工程中马上看到更新后的效果。

项目中安装本地 npm 包

接下来,我们(随便)找一个业务工程去安装这个 npm 包,本文默认使用这个工程可以点击去下载
在工程根目录执行:

npm link babel-plugin-function-try-catch

上面这一步就是让当前工程去模拟 npm install 命令,包来源于本地 npm link 过的文件。

执行后,业务工程的 node_modules 中就会有这个包。

眼尖的小伙伴会发现,这个 npm 包不同于其它包,它带有一个转折箭头符号。如下图所示:


也就是说这是一个软链接,执行 npm link xxx 后它会在系统中做一个快捷方式映射。所以这样方便我们不断调试本地正在开发的这个 npm 包。

本地项目工程配置这个 loader

打开当前项目的 webpack 配置文件:
webpack.config.js

rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: [
+       "babel-plugin-function-try-catch",
          "babel-loader"
      ]
    },
]

执行 npm start 启动当前前端工程。可以看到原来的代码

var fn = function(){
  console.log(11);
}
fn();

在浏览器中自动会加上 try/catch,错误发生时可以自动捕获和上报。

至此,完成了本地 npm 包和本地业务工程的关联!

本地调试 npm 代码

此时我要修改 catch 中的代码,将其改为 console.log(error),这个时候你只需要修改本地 npm 工程中的代码即可:

// src/index.js

- var catchStatement = template.statement(`ErrorCapture(error)`)();
+ var catchStatement = template.statement(`console.log(error)`)();

然后执行:

npm run build

再切到业务工程重新启动项目 npm start 即可生效。

当本地 npm 包调试完成后,在其根目录执行:

npm unlink babel-plugin-function-try-catch

即可解除 link。

如果要发布到 npm 公网,执行 npm publish 即可。

就到这里,再见吧。

查看原文

赞 7 收藏 6 评论 0

CrossingMay 赞了文章 · 2020-12-26

Vue中jsx不完全应用指南

前言:文章不介绍任务背景知识,没有原理说明,偏向于实践的总结和经验分享。

文章所有的代码是基于Vue CLI 3.x版本,不会涉及到一步步通过Webpack来配置JSX所需要的知识点。

在使用Vue开发项目时绝大多数情况下都是使用模板来写HTML,但是有些时候页面复杂又存在各种条件判断来显示/隐藏和拼凑页面内容,或者页面中很多部分存在部分DOM结构一样的时候就略显捉襟见肘,会写大量重复的代码,会出现单个.vue文件过长的情况,这个时候我们就需要更多的代码控制,这时候可以使用渲染函数

渲染函数想必平时几乎没有人去写,因为写起来很痛苦(本人也没有写过)。更多的是在Vue中使用JSX语法。写法上和在React中差不多,但是功能上还是没有React中那么完善。

在写JSX的过程中不得考虑一个样式的问题,虽然可以直接在.vue文件中不写<tempate>部分,只写<script><style>部分,而不用担心样式作用域问题。但是更多的时候还是推荐直接使用.js的方式来写组件,这个时候就涉及到样式作用域的问题了。

在React的生态中,有很多CSS-IN-JS的解决方案,比如styled-jsxemotionstyled-components等,目前最活跃和用户量最多的是styled-components,目前已经拥有良好的生态圈子。如果需要在样式中作一些像Sass/Less中的颜色计算,可以使用polished来实现,当然不止这么简单的功能。但是在Vue中可使用的方案就太少了,因为Vue使用模板来写HTML本身是开箱即用的样式scoped,在使用JSX写组件的时候就面临着样式问题,一种方案是在组件包裹<div>中取一个特殊的名字,然后样式都嵌套写在这个class下面,但是难免会遇到命名冲突的情况,而且每次还得变着花样取名称。此外,就是引入CSS-IN-JS在Vue对应的实现,但目前来看Styled-components官方提供了一个Vue版本的叫vue-styled-components和emotion的vue-emotion,但是用的人实在太少。像styled-components进行了重大更新和变化,但是Vue版本的还是最初的版本,而且有时候还出现样式不生效的情况。

接下来进入正题,从简单语法到经验分享(大牛请绕行)

基本用法

首先需要约定一下,使用JSX组件命名采用首字母大写的驼峰命名方式,样式可以少的可以直接基于vue-styled-components写在同一个文件中,复杂的建议放在单独的_Styles.js_文件中,当然也可以不采用CSS-IN-JS的方式,使用Less/Sass来写,然后在文件中import进来。

下面是一个通用的骨架:

import styled from 'vue-styled-components'

const Container = styled.div`
    heigth: 100%;
`

const Dashboard = {
  name: 'Dashboard',
  
  render() {
    return (
        <Container>内容</Container>
    )
  }
}

export default Dashboard

插值

在JSX中使用单个括号来绑定文本插值

<span>Message: {this.messsage}</span>
<!-- 类似于v-html -->
<div domPropsInnerHTML={this.dangerHtml}/>
<!-- v-model -->
<el-input v-model={this.vm.name} />

在jsx中不需要把v-model分成事件绑定和赋值二部分分开来写,因为有相应的babel插件来专门处理。

样式

在JSX中可以直接使用class="xx"来指定样式类,内联样式可以直接写成style="xxx"

<div class="btn btn-default" style="font-size: 12px;">Button</div>

<!-- 动态指定 -->
<div class={`btn btn-${this.isDefault ? 'default' : ''}`}></div>
<div class={{'btn-default': this.isDefault, 'btn-primary': this.isPrimary}}></div>
<div style={{color: 'red', fontSize: '14px'}}></div>

遍历

在JSX中没有v-forv-if等指令的存在,这些全部需要采用Js的方式来实现

{/* 类似于v-if */}
{this.withTitle && <Title />}

{/* 类似于v-if 加 v-else */}
{this.isSubTitle ? <SubTitle /> : <Title />}

{/* 类似于v-for */}
{this.options.map(option => {
  <div>{option.title}</div>
})}

事件绑定

事件绑定需要在事件名称前端加上on前缀,原生事件添加nativeOn

<!-- 对应@click -->
<el-buton onClick={this.handleClick}>Click me</el-buton>
<!-- 对应@click.native -->
<el-button nativeOnClick={this.handleClick}>Native click</el-button>
<!-- 传递参数 -->
<el-button onClick={e => this.handleClick(this.id)}>Click and pass data</el-button>

注意:如果需要给事件处理函数传参数,需要使用箭头函数来实现。如果不使用箭头函数那么接收的将会是事件的对象event属性。

高级部分

在Vue中基于jsx也可以把组件拆分成一个个小的函数式组件,但是有一个限制是必需有一个外层的包裹元素,不能直接写类似:

const Demo = () => (
    <li>One</li>
  <li>Two</li>
)

必需写成:

const Demo = () => (
    <div>
      <li>One</li>
    <li>Two</li>
  </div>
)

而在React中可以使用空标签<></><react.Fragment></react.Fragment>来实现包裹元素,这里的空标签其实只是react.Fragment的一个语法糖。同时在React 16中直接支持返回数组的形式:

const Demo = () => [
  <li>One</li>
  <li>Two</li>
]

那么在Vue中就只能通过遍历来实现类似的功能,大体思路就是把数据先定义好数据然后直接一个map生成,当然如果说元素的标签是不同类型的那就需要额外添加标识来判断了。

{
  data() {
    return {
      options: ['one', 'two']
    }
  },
    
    render() {
    const LiItem = () => this.options.map(option => <li>{option}</li>)
                                          
    return (
      <div>
            <ul>
              <LiItem />
          </ul>
         </div>
    )
  }
}

事件修饰符

在基础部分简单介绍了事件的绑定用法,这里主要是补充一下事件修饰符的写法。

在模板语法中Vue提供了很多事件修饰符来快速处理事件的冒泡、捕获、事件触发频率、按键识别等。可以直接查看官方文档的事件&按键修饰符部分,这里把相关内容原样搬运过来:

修饰符前缀
.passive&
.capture!
.once~
.capture.once.once.capture~!

使用方式如下:

<el-button {...{
    '!click': this.doThisInCapturingMode,
  '!keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}}>Click Me!</el-button>

下面给出的事件修饰符是需要在事件处理函数中写出对应的等价操作

修饰符处理函数中的等价操作
.stopevent.stopPropagation()
.preventevent.preventDefault()
.selfif (event.target !== event.currentTarget) return
按键: .enter, .13if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码)
修饰键: .ctrl, .alt, .shift, .metaif (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKeyshiftKey 或者 metaKey)

下面是在事件处理函数中使用修饰符的例子:

methods: {
  keyup(e) {
    // 对应`.self`
    if (e.target !== e.currentTarget) return
    
    // 对应 `.enter`, `.13`
    if (!e.shiftKey || e.keyCode !== 13) return
    
    // 对应 `.stop`
    e.stopPropagation()
    
    // 对应 `.prevent`
    e.preventDefault()
    
    // ...
  }
}

ref和refInFor

在Vue中ref被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件。

注意

  • 因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在
  • $refs不是响应式的,因此你不应该试图用它在模板中做数据绑定。

v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。

假如在jsx中想要引用遍历元素或组件的时候,例如:

const LiArray = () => this.options.map(option => (
  <li ref="li" key={option}>{option}</li>
))

会发现从this.$refs.li中获取的并不是期望的数组值,这个时候就需要使用refInFor属性,并置为true来达到在模板中v-for中使用ref的效果:

const LiArray = () => this.options.map(option => (
  <li ref="li" refInFor={true} key={option}>{option}</li>
))

插槽(v-slot)

在jsx中可以使用this.$slots来访问静态插槽的内容。

注意:在Vue 2.6.x版本后废弃了slotslot-scope,在模板中统一使用新的统一语法v-slot指令。v-slot只能用于Vue组件和template标签。
<div class="page-header__title">
    {this.$slots.title ? this.$slots.title : this.title}
</div>

等价于模板的

<div class="page-header__title">
  <slot name="title">{{ title }}</slot>
</div>

在Vue官方文档中提到:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。因此像下面的示例是无法正常工作的

<current-user>
    {{ user.firstName }}
</current-user>

<current-user>组件中可以访问到user属性,但是提供的内容却是在父组件渲染的。如果想要达到期望的效果,这个时候就需要使用作用域插槽了。下面是改写后的代码,更多知识点可以直接查看官方文档的作用域插槽

<!-- current-user组件定义部分 -->
<span>
    <slot v-bind:user="user">
      {{ user.lastName }}
  </slot>
</span>

<!-- current-user 使用 -->
<current-user>
    <template v-slot:default="slotProps">
      {{ slotProps.user.firstName }}
  </template>
</current-user>

上面的示例其实就是官方的示例,这里需要说明的是,其实在Vue中所谓的作用域插槽功能类似于React中的Render Props的概念,只不过在React中我们更多时候不仅提供了属性,还提供了操作方法。但是在Vue中更多的是提供数据供父作用域渲染展示,当然我们也可以把方法提供出去,例如:

<template>
    <div>
    <slot v-bind:injectedProps="slotProps">
      {{ user.lastName }}
      </slot>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        user: {
          firstName: 'snow',
          lastName: 'wolf'
        }
      }
    },
    
    computed: {
      slotProps() {
        return {
          user: this.user,
          logFullName: this.logFullName
        }
      }
    },
    
    methods: {
      logFullName() {
        console.log(`${this.firstName} ${this.lastName}`)
      }
    }
  }
</script>

在父组件中使用:

<current-user>
    <template v-slot:default="{ injectedProps }">
      <div>{{ injectedProps.user.firstName }}</div>
        <el-button @click="injectedProps.logFullName">Log Full Name</el-button>
  </template>
</current-user>

在上面的代码中我们实际上使用解构的方式来取得injectedProps,基于解构的特性还可以重命名属性名,在propundefined的时候指定初始值。

<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>

如果组件只有一个默认的插槽还可以使用缩写语法,将v-slot:default="slotProps"写成v-slot="slotProps",命名插槽写成v-slot:user="slotProps",如果想要动态插槽名还可以写成v-slot:[dynamicSlotName],此外具名插槽同样也有缩写语法,例如 v-slot:header可以被重写为#header

上面介绍了很多插槽相关的知识点足已说明其在开发过程中的重要性。说了很多在模板中如何定义和使用作用域插槽,现在进入正题如何在jsx中同样使用呢?

// current-user components
{
  data() {
    return {
      user: {
        firstName: 'snow',
        lastName: 'wolf'
      }
    }
  },
    
  computed: {
    slotProps() {
      return {
        user: this.user,
        logFullName: this.logFullName
      }
    }
  },
    
  methods: {
    logFullName() {
      console.log(`${this.firstName} ${this.lastName}`)
    }
  },
    
  render() {
    return (
        <div>
        {this.$scopedSlots.subTitle({
          injectedProps: this.slotProps
        })}
      </div>
    )
  }
}

然后在父组件中以jsx使用:

<current-user {...{
  scopedSlots: {
    subTitle: ({ injectedProps }) => (
        <div>
          <h3>injectedProps.user</h3>
        <el-button onClick={injectedProps.logFullName}>Log Full Name</el-button>
      </div>
    )
  }
}}></current-user>

指令

这里需要注意的是在jsx中所有Vue内置的指令除了v-show以外都不支持,需要使用一些等价方式来实现,比如v-if使用三目运算表达式、v-for使用array.map()等。

对于自定义的指令可以使用v-name={value}的语法来写,需要注意的是指令的参数、修饰符此种方式并不支持。以官方文档指令部分给出的示例v-focus使用为例,介绍二种解决办法:

1 直接使用对象传递所有指令属性

<input type="text" v-focus={{ value: true }} />

2 使用原始的vnode指令数据格式

{
  directives:{
    focus: {
      inserted: function(el) {
        el.focus()
      }
    }
  },
    
  render() {
    const directives = [
      { name: 'focus', value: true }
    ]
      
    return (
      <div>
          <input type="text" {...{ directives }} />
      </div>
    )
  }
}

过滤器

过滤器其实在开发过程中用得倒是不多,因为更多时候可以通过计算属性来对数据做一些转换和筛选。这里只是简单提及一下并没有什么可以深究的知识点。

在模板中的用法如下:

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

在jsx中使用方法为:

<div>{this.$options.filters('formatDate')('2019-07-01')}</div>
注意:由于Vue全局的过滤器只用于模板中,如果需要用于组件的方法中,可以把过滤器方法单独抽离出一个公共Js文件,然后引入组件中,然后用于方法中。

一些简单经验分享

并不是说我们在开发Vue项目的时候一定要使用jsx的方式来写,但是多掌握一种方式来灵活变通,提高工作效率,扩展思路何尝不值得一试。而且,在有些场景下释放js的完全编程能力会让你更加能够得心应手。其实在使用模板方式的时候我们并没有完全采用组件的思维方式来做,或者说是做得不彻底,不纯粹,拆分的粒度不够。更多 的时候并没有考虑到组件怎么切分和抽象,多人协作的时候如何处理依赖并明确自己的功能点。

关于DOM属性、HTML属性和组件属性

在React中所有数据均挂载在props下,Vue则不然,仅属性就有三种:组件属性props,普通html属性attrs和DOM属性domProps。在Angular的文档中关于插值绑定部分是重点说明了DOM属性HTML属性的区别,在大多数情况下两者都有对应的同名属性,也就是1:1映射关系,但是也有例外的情况,比如HTML中colspan,DOM中的textContent。HTML属性的值指定了初始值,并且不能改变,而DOM属性的值表示当前值,是可以改变的。

然后在Vue的模板语法中是不区分DOM属性和HTML属性的,例如:

<template>
    <div>
    <div>输入的值:{{ title }}</div>
    <input type="text" value="我是DOM属性值" v-model="title" @input="logTitle" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: ''
    }
  },
  
  methods: {
    logTitle(e) {
      // 输出DOM属性
      console.log(e.target.value)
      // 输出HTML属性
      console.log(e.target.getAttribue('value'))
    }
  }
}
</script>

运行示例可以看到input的初始值被设置为了“我是DOM属性值",当我们在输入框中添加或者删除文字时,HTML属性始终没有变化,而绑定的DOM值一值在变动。然后再看一下在jsx中的实现:

<div>输入值:{ this.title }</div>
<input type="text" value="我是DOM属性" v-model={this.title} onInput={this.logTitle} />

同样运行后会发现在jsx写法中并没有直接将HTML属性初始化为DOM属性值,即输入框中当前值为空字符串,这符合预期的行为。

此外在模板语法中是无法区分HTML属性和DOM属性命名一样的场景,但是在jsx中可以很好的区分:

<Demo title="我是组件属性" domPropsTitle="我是DOM属性" />

结果会就是在HMTL中显示title="我是DOM属性,而"我是组件属性”传递给了组件。

在React中CSS的样式写义在jsx中的语法是以className="xx"的形式,而在Vue的jsx中可以直接写成class="xx"。实际上由于class是Js的保留字,因此在DOM中其属性名为className而在HTML属性中为class,我们可以在Vue中这样写,经过Babel转译后得到正确的样式类名:

<div domPropsClassName="mt__xs"></div>
注意:如果同时写了class="xx" domPropsClassName="yy"那么后者的优先级较高,和位置无关。所以尽量还是采用class的写法。

有使用过Bootstrap经验的可能会注意到它里面包含了很多ARIA属性,这些属性并不属于DOM,在jsx中可以通过attrsXX或者直接aria-xx的方式来添加:

<label aria-label="title"></label>
<label attrsAria-label="title"></label>

但是上面的换成domPropsAria-label就没有任何效果。

注意:在jsx中所有DOM属性(Property)语法为domPropsXx, HTML特性(Attribute)语法为attrsXx。更多的时候建议还是少使用,或者说合理使用。

在jsx中还可以使用混用的写法,例如在组件中写了<Demo title="demo" />,还可以定义一个属性对象,然后使用{...props}的方式写在一起,这个时候就会出现属性合并的问题,同样的事件多个地方声明事件处理函数,都会触发。

最后需要提及一点的是,在Vue中当给一个组件传了很多props,但是有的并不是组件声明的,也有可能是一些通用的HTML或者DOM属性,但是在最终编译后的HTML中会直接显示这些props,如果不希望这些属性显示在最终的HTML中,可以在组件中设inheritAttrs: false。虽然不显示了,但是我们依然可以通过vm.$attrs获取所有(除classstyle)绑定的属性,包括不在props中定义的。

关于事件

前面已经把事件相关的知识点都介绍了,这里主要是提及一下关于jsx事件绑定语法onXx和组件属性(主要是函数prop)以on开头的情况如何处理。

虽然在写组件的时候可以避开命名以on开头,但是在使用第三库的时候,如果遇到了该如何处理呢?比如Element组件Upload很多钩子都是以on开头。 下面提供两种解决办法:

1.使用展开

<el-upload {...{
  props: {
    onPreview: this.handlePreview
  }
}} />
  1. 使用propsXx
<el-upload propsOnPreview={this.handlePreview} />

推荐使用第二种方式,写起来要简单些。

复杂逻辑条件判断

在模板语法中可以使用v-ifv-else-ifv-else来做条件判断。在jsx中可以通过?:三元运算符(Ternary operator)运算符来做if-else判断:

const Demo = () => isTrue ? <p>True!</p> : null

然后可以利用&&运算符的特性简写为:

const Demo = () => isTrue && <p>True!</p>

对于复杂的条件判断,例如:

const Demo = () => (
    <div>
      {flag && flag2 && !flag3
        ? flag4
         ? <p>Blash</p>
      : flag5
         ? <p>Meh</p>
      : <p>hErp</p>
        : <p>Derp</p>
    }
  </div>
)

可以采用两种方式来降低判断识别的复杂度

  • 最好的办法:将判断逻辑转移到子组件
  • 可选的hacky方法:使用IIFE(立即执行表达式)
  • 使用第三方库解决:jsx-control-statements

下面是使用IIFE通过内部使用if-else返回值来优化上述问题:

const Demo = () => (
    <div>
    {
      (() => {
        if (flag && flag2 &&!flag3) {
          if (flag4) {
            return <p>Blah</p>
          } else if (flag5) {
            return <p>Meh</p>
          } else {
            return <p>Herp</p>
          }
        } else {
          return <p>Derp</p>
        }
      })()
    }
  </div>
)

还可以使用do表达式,但是需要插件@babel/plugin-proposal-do-expressions的转译来支持,

const Demo = () => (
    <div>
    {
      do {
        if (flag1 && flag2 && !flag3) {
          if (flag4) {
            <p>Blah</p>
          } else if (flag5) {
            <p>Meh</p>
          } else {
            <p>Herp</p>
          }
        } else {
          <p>Derp</p>
        }
      }
    }
  </div>
)

再就是一种比较简单的可选办法,如下:

const Demo = () => {
  const basicCondition = flag && flag1 && !flag3;
  if (!basicCondition) return <p>Derp</p>
  if (flag4) return <p>Blah</p>
  if (flag5) return <p>Meh</p>
  return <p>Herp</p>
}

最后一种使用jsx插件的就不详述和举例了,有兴趣的可以直接查看文档。

组件的传值

在单个jsx文件中可以写很多函数式组件来切分更小的粒度,例如之前的文章Vue后台管理系统开发日常总结__组件PageHeader,组件的形态有两种,一种是普通标题,另一种是带有选项卡的标题,那么在写的时候就可以这样写:

render() {
  // partial html
  const TabHeader = (
      <div class="page-header page-header--tab"></div>
  )
  
  // function partial
  const Header = () => (
      <div class="page-header"></div>
  )
  
  <div class="page-header">
      {this.withTab ? TabHeader : <Header/>}
  </div>
}

注意在拆分的时候,如果不需要做任何判断可以纯粹是HTML片段赋值给变量,如果需要条件判断就使用函数式组件的方式来写。需要注意的是由于render函数会多次被调用,写的时候注意一下对性能的影响,目前能力有限这方面就不作展开了。

既然使用函数式组件,那么同样可以在函数中传递参数了,参数是一个对象中,包含了以下属性

children        # VNode数组,类似于React的children
data          # 绑定的属性
    attrs       # Attribute
    domProps    # DOM property
    on                # 事件
injections  # 注入的对象
listeners:  # 绑定的事件类型
    click         # 点击事件
    ...
parent            # 父组件
props                # 属性
scopedSlots # 对象,作用域插槽,使用中发现作用域插槽也挂在这个下面
slots                # 函数,插槽

虽然可以在函数式组件中传参数、事件、slot但是个人觉得不建议这样做,反而搞复杂了。

render() {
  const Demo = props => {
    return (
        <div>
          <h3>Jsx中的内部组件 { props.data.title }</h3>
        { props.children }
        <br />
        { props.scopedSlots.bar() }
      </div>
    )
  }
  
  return (
      <div>
        <Demo title="test" attrsA="a" domPropsB="b" onClick={this.demo}>
          <h3>我是Children</h3>
        <template slot="bar">
            <p>我是Slot内容</p>
        </template>
      </Demo>
    </div>
  )
}

上面的示例最终生成的HTML中会将<template>的内容转换为#document-fragment

与ElementUI结合使用问题

ElementUI是Vue项目开发过程中最常用的UI组件库,在使用过程中会遇到一会坑,这里简单分享一下

问题:在<el-form>中使用model属性时,会被插件babel-plugin-jsx-v-model当成v-model识别,导致报错Invalid handler for event "input": got undefined

解决办法如下:

<el-form {...{props: {model: this.wechatForm}}} ref='formRef' label-width='40' label-position='right'></el-form>

还有一些注意点,比如在<el-form>model接收的是一个对象值,不能传数组,在动态表单中<el-form-item>prop一写要和关联的<el-input>中的v-model保持一致。

export default {
  name: 'Demo',
  
  data() {
    return {
      wechatForm: {
        customModel: [
          {
            type: 'aReadNum',
            label: '在看数',
            min: '',
            max: ''
          },
          {
            type: 'iExtremeRead',
            label: '阅读数',
            min: '',
            max: ''
          },
          {
            type: 'iExtremeLike',
            label: '点赞数',
            min: '',
            max: ''
          }
        ]
      }
    }
  },
  
  render() {
    return (
      <el-form {...{props: {model: this.wechatForm}}} ref='formRef' label-width='40' label-position='right'>
          {
          this.wechatForm.customModel.map((item, index) => (
            <div class='form-item'>
              <el-form-item label={item.label} prop={`customModel.${index}.min`} rules={{
                required: true,
                message: '不能为空',
                trigger: 'blur'
              }}>
                <el-input v-model={item.min} style='width: 60px;' />
              </el-form-item>
            </div>
          ))
        }

        <el-form-item>
            <el-button type='default'>取消</el-button>
            <el-button type='primary' onClick={this.ok}>确定</el-button>
        </el-form-item>
      </el-form>
    )
  }
}

还有一个问题如果在<el-form-item>中动态指定了key值有可能会出现关联<el-input>输入一个字符立即失去焦点。

由于ElementUI使用的slot语法较老,在使用 el-table 组件时里面的 scoped-slots 需要用以下写法:

<el-table-column
    min-width="240"
    label={this.$t('domain.domain_name')}
    {...{
      scopedSlots: {
        default: scope => (
          <div>
            <div class="domain__name">{scope.row.host}</div>
            {scope.row.enabled && (
              <i class="domain__tag">
                {this.$t('domain.isEnable')}
              </i>
            )}
          </div>
        ),
      },
    }}
/>

总结

接触Vue时间比较早,但是真正的Vue项目开发经验一年不到,平时比较懒,不怎么去深入学习和研究,所以文章在叙述上没有什么条理性,有些知识点可能并没表达清楚,很多东西还是得多实践去检验。如果有问题欢迎留言共同探讨。

其实早一点实践jsx的写法,对于后面的Vue 3.0出现后可以更快的融入其中,就像React对函数式组件中新增了钩子(Hooks)函数,以后Vue也是主推函数式组件,以后模板语法方式的占比会稍有下降。

文章并没有包含所有Vue中jsx写法的全部知识点,→_→所以叫不完全指南^_^"

最后,感谢各位的支持!!!…(⊙_⊙;)… ○圭~○

查看原文

赞 27 收藏 12 评论 0

CrossingMay 收藏了文章 · 2020-12-01

vue新特性provide/inject深入学习

provide/inject深入学习

阅读本文帮你你可以在使用provide/inject传递响应式数据时记住一个特性: provide传递的每一个响应式数据都需要值是一个引用不变的可监听对象。

在开发vue项目时,不可避免的需要进行组件之间的相互通信。如果是在一个实际的业务项目中, 组件间的通信可以采用采用像vuex, EventBus等机制实现跨组件通信。但如果在开发基础组件库时,需要跟业务项目外部环境(vuexEventBus)解耦,不可以使用这些机制。一般的解决方案是模仿事件冒泡和广播来实现在基础库中组合组件的通信,比如element-uiemitter

vue的2.2.0版本中, 添加了provide/injectapi, 该api参照实现了reactcontext的功能。也为跨组件通信提供了一种新的方式。

从官网文档介绍来看, 这是一种比emitter优雅的多的机制, 但是有一个特殊的提示.

提示:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

意味着provide/inject机制对响应式传递有很大的限制, 那它是否可以完全替换掉emitter机制呢?

但在实际开发中, 组合父子组件都是配套使用,在实现时一般不会使用事件冒泡或者事件广播的方式进行通信, 一般通过递归或者循环获取$parent的方式的得到祖先组件的数据, 因为这些数据是可响应的,不需要事件传递那么繁琐。如在子组件中有代码:


computed: {

    count() {

      return (this.$parent || {}).count;

    },

    user() {

      return (this.$parent || {}).user;

    },

  },

当父组件的count(基本类型)或者user(引用类型)发生改变时, 会自动更新子组件的computed。这在高级组件中非常有用, 可以把一些子组件共有的特性抽取到父组件中去, 父组件为这些子组件的一些特性提供一个默认值, 改变父组件的props来改变所有的使用默认值的子组件的行为。 如el-form中的label-width就可以为所有的目标子组件el-form-item设置一个默认的label-width

实际使用时,不会直接对this.$parent获取目标组件, 因为这样耦合性太高, 一般通过循环或者递归查找某种特征的组件,具体可以查看element-uiemitter中冒泡中是如何查找目标组件触发事件.

$children是不支持响应式的.

下面使用provide在组件树中数据自上向下传递,在父组件中通过provide中暴露一些数据, 子组件接受这些数据,然后通过子组件渲染在页面上。在父组件中改变这些值, 查看数据流动。

如果父组件中提供:


  props: {

    count: Number,

    user: Object,

  },

  data() {

    return {

      grade: {

        number: 12,

      },

    };

  },

  provide() {

    return {

      count: this.count,

      user: this.user,

      grade: this.grade,

    };

  },

其中, user是外部组件通过props传递给父组件的数据, 结构为:


{

    name: ''.

    age: 0,

    address: {

        number: 0,

    }

}

这些数据user, grade都是响应式数据结构,count不是。

分别改变这些数据结构,依次改变grade.number, user.address.number, user.age, user.address , 会发现子组件能接受到的响应式数据,但当改变了grade, user, count时, 子组件不能响应式渲染页面, 且改变grade, user的属性也会发现不会响应式渲染。

其中count由于是一个不可监听的对象, 没有在子组件中动态渲染, 符合期望。其后在改变grade.number, user.address.number, user.age, user.address时, 子组件可以动态渲染改变值,这是由于这些属性都在一个可监听对象中。改变grade, user的后,发现子组件没有动态渲染, 证明grade, user是不可响应的,这是由于grade, user不在一个可监听的对象里面。 grade, user在哪呢? 在provide的声明中, 返回了一个对象:


 provide() {

    return {

      count: this.count,

      user: this.user,

      grade: this.grade,

    };

  }

grade, user就在这个对象里面, 该对象不是可监听对象, 所以导致grade, user不是响应式的. 首先的解决方案就是, 将这个对象作为一个响应式的,可以在data中声明一个容器字段, 用于包装需要传递的数据, 如:


  data() {

    return {

      context: {

        user: '',

        count: 0,

        grade: '',

      },

      grade: {

        number: 12,

      },

    };

  },

  watch: {

    user(val) {

      this.context.user = val;

    },

    count(val) {

      this.context.count = val;

    },

    grade(val) {

      this.context.grade = val;

    },

  },

  created() {

    this.context.user = this.user;

    this.context.count = this.count;

    this.context.grade = this.grade;

  },

为什么这样写, 是由于provide/inject机制不支持响应式编程的,后续对provide返回的对象直接修改不会重新刷新provide/inject机制, 也就是provide返回的对象的最顶层的响应机制会失效,且无法对对象顶层属性进行操作。这个机制会导致以下三种方式不能实现响应式传递:

  • 上文中的context不能在computed中声明。因为每次computed都会返回一个新的值(引用),而provide只会记录一开始的context的那个引用, 后续数据发生变更, 新的context不会被刷新到provide中去。
  • 上文中的context就算data中声明的, 但如果在某个地方执行了this.context = {...}, 新的context也不会被更新在provideprovide中的context永远是在初始化时复制给他的那个引用。这会导致在父组件中context可以动态刷新, 但是子组件中的context不会动态刷新。
  • 直接在provide函数中返回上文中的context,那么user, grade就会成为顶层属性,在created中进行的重新赋值操作和后续的重新赋值操作都不会响应到provide中, 将会失去响应式。

按照上面的写法, 发现grade, user已经能够动态在子组件中渲染了。由上得到结论:要想provide传递的数据一直是可响应的, 需要provide传递的每一个属性的值都是一个引用不变的可监听对象。

每次维护context太麻烦了, 有没有简便方法? 可以这样写:


 provide() {

    return {

      group: this,

    };

  }

直接将父组件的引用放在provide返回对象的一个属性中, this代表当前实例, 引用实不会发生变化的, 且this中大多数属性都是响应式的。但需要注意带$前缀的属性大多数都不是响应式属性,如$el,子组件在使用这些属性时, 不会动态渲染。如果父组件有更大的作用域是,比如同时为多种类型子组件服务,或者允许第三方子组件inject使用时, 建议不要直接传递this, 而是在provide中暴露特定的api。但是按照上文中维护一个特定的context对象太繁琐了,可以使用函数来保证引用不变(推荐使用该方式):


provide() {

    return {

      getContext: () => ({

          user: this.user,

          grade: this.grade,

      })

    };

  }

在子组件中:


inject: ['getContext'],

computed: {

    context() {

        return this.getContext();

    }

}

总体来说, provide/inject是可以完全代替emitter机制的, 包括事件冒泡和事件广播。且provide/inject还提供了一种更安全的父组件对子组件暴露api的方式。

通过阅读vueprovide/inject源码), 发现实现provide/inject的机制也是通过$parent实现的。provide会在初始化时放到一个_provided的属性中,子组件在访问inject的值时, 会通过$parent属性向上查找所有祖先节点的_provided获取第一个有目标属性的值。为什么provide返回的对象的最顶层的响应机制会失效? 在查找inject的值时, 会将目标_provide的单个属性拿出来), 放在一个不是响应式的对象中。从设计上也是合理的,provide的是一个数据集, 而不是一个数据,子组件inject到的数据可能从多个provide中挑选的集合, 故父组件的provide没有必要维持这个数据集的响应式 。 

通过阅读inject的源码, 可以发现, 其实获取inject的值也是通过$parent向上查找祖先中的值, 直接取得就是目标组件得_provided属性得值,子组件跟父组件是共享一个变量,在子组件中改变这个值, 父组件中该值也会发生了变化。

查看原文

CrossingMay 赞了文章 · 2020-11-25

聊聊 ESM、Bundle 、Bundleless 、Vite 、Snowpack

前言

一切要都要从打包构建说起。

当下我们很多项目都是基于 webpack 构建的, 主要用于:

  • 本地开发
  • 打包上线

首先,webpack 是一个伟大的工具。

经过不断的完善,webpack 以及周边的各种轮子已经能很好的满足我们的日常开发需求。

我们都知道,webpack 具备将各类资源打包整合在一起,形成 bundle 的能力。

可是,当资源越来越多时,打包的时间也将越来越长。

一个中大型的项目, 启动构建的时间能达到数分钟之久。

拿我的项目为例, 初次构建大概需要三分钟, 而且这个时间会随着系统的迭代越来越长。

相信不少同学也都遇到过类似的问题。 打包时间太久,这是一个让人很难受的事情。

那有没有什么办法来解决呢?

当然是有的。

这就是今天的主角 ESM, 以及以它为基础的各类构建工具, 比如:

  1. Snowpack
  2. Vite
  3. Parcel

等等。

今天,我们就这个话题展开讨论, 希望能给大家一些其发和帮助。

文章较长,提供一个传送门:

  1. 什么是 ESM
  2. ESM 是如何工作的
  3. Bundle & Bundleless
  4. 实现一个乞丐版 Vite
  5. Snowpack & 实践
  6. bundleless 模式在实际开发中存在的一些问题
  7. 结论

正文

什么是 ESM

ESM 是理论基础, 我们都需要了解。

「 ESM 」 全称 ECMAScript modules,基本主流的浏览器版本都以已经支持。

image.png

ESM 是如何工作的

image.png

当使用ESM 模式时, 浏览器会构建一个依赖关系图。不同依赖项之间的连接来自你使用的导入语句。

通过这些导入语句, 浏览器 或 Node 就能确定加载代码的方式。

通过指定一个入口文件,然后从这个文件开始,通过其中的import语句,查找其他代码。

image.png

通过指定的文件路径, 浏览器就找到了目标代码文件。 但是浏览器并不能直接使用这些文件,它需要解析所有这些文件,以将它们转换为称为模块记录的数据结构。

image.png

然后,需要将 模块记录 转换为 模块实例

image.png

模块实例, 实际上是 「 代码 」(指令列表)与「 状态」(所有变量的值)的组合。

对于整个系统而言, 我们需要的是每个模块的模块实例。

模块加载的过程将从入口文件变为具有完整的模块实例图。

对于ES模块,这分为 三个步骤

  1. 构造—查找,下载所有文件并将其解析为模块记录。
  2. 实例化—查找内存中的框以放置所有导出的值(但尚未用值填充它们)。然后使导出和导入都指向内存中的那些框,这称为链接。
  3. 运行—运行代码以将变量的实际值填充到框中。

image.png

在构建阶段时, 发生三件事情:

  1. 找出从何处下载包含模块的文件
  2. 提取文件(通过从URL下载文件或从文件系统加载文件)
  3. 将文件解析为模块记录

1. 查找

首先,需要找到入口点文件。

在HTML中,可以通过脚本标记告诉加载程序在哪里找到它。

image.png

但是,如何找到下一组模块, 也就是 main.js 直接依赖的模块呢?

这就是导入语句的来源。

导入语句的一部分称为模块说明符, 它告诉加载程序可以在哪里找到每个下一个模块。

image.png

在解析文件之前,我们不知道模块需要获取哪些依赖项,并且在提取文件之前,也无法解析文件。

这意味着我们必须逐层遍历树,解析一个文件,然后找出其依赖项,然后查找并加载这些依赖项。

image.png

如果主线程要等待这些文件中的每个文件下载,则许多其他任务将堆积在其队列中。

那是因为当浏览器中工作时,下载部分会花费很长时间。

image.png

这样阻塞主线程会使使用模块的应用程序使用起来太慢。

这是ES模块规范将算法分为多个阶段的原因之一。

将构造分为自己的阶段,使浏览器可以在开始实例化的同步工作之前获取文件并建立对模块图的理解。

这种方法(算法分为多个阶段)是 ESMCommonJS模块 之间的主要区别之一。

CommonJS可以做不同的事情,因为从文件系统加载文件比通过Internet下载花费的时间少得多。

这意味着Node可以在加载文件时阻止主线程。

并且由于文件已经加载,因此仅实例化和求值(在CommonJS中不是单独的阶段)是有意义的。

这也意味着在返回模块实例之前,需要遍历整棵树,加载,实例化和评估任何依赖项。

该图显示了一个Node模块评估一个require语句,然后Node将同步加载和评估该模块及其任何依赖项

在具有CommonJS模块的Node中,可以在模块说明符中使用变量。

require在寻找下一个模块之前,正在执行该模块中的所有代码。这意味着当进行模块解析时,变量将具有一个值。

但是,使用ES模块时,需要在进行任何评估之前预先建立整个模块图。

这意味着不能在模块说明符中包含变量,因为这些变量还没有值。

使用变量的require语句很好。 使用变量的导入语句不是。

但是,有时将变量用于模块路径确实很有用。

例如,你可能要根据代码在做什么,或者在不同环境中运行来记载不同的模块。

为了使ES模块成为可能,有一个建议叫做动态导入。有了它,您可以使用类似的导入语句:

import(`${path}/foo.js`)

这种工作方式是将使用加载的任何文件import()作为单独图的入口点进行处理。

动态导入的模块将启动一个新图,该图将被单独处理。

两个模块图之间具有依赖性,并用动态导入语句标记

但是要注意一件事–这两个图中的任何模块都将共享一个模块实例。

这是因为加载程序会缓存模块实例。对于特定全局范围内的每个模块,将只有一个模块实例。

这意味着发动机的工作量更少。

例如,这意味着即使多个模块依赖该模块文件,它也只会被提取一次。(这是缓存模块的一个原因。我们将在评估部分中看到另一个原因。)

加载程序使用称为模块映射的内容来管理此缓存。每个全局变量在单独的模块图中跟踪其模块。

当加载程序获取一个URL时,它将把该URL放入模块映射中,并记下它当前正在获取文件。然后它将发出请求并继续以开始获取下一个文件。

加载程序图填充在“模块映射表”中,主模块的URL在左侧,而“获取”一词在右侧

如果另一个模块依赖于同一文件会怎样?加载程序将在模块映射中查找每个URL。如果在其中看到fetching,它将继续前进到下一个URL。

但是模块图不仅跟踪正在获取的文件。模块映射还充当模块的缓存,如下所示。

2. 解析

现在我们已经获取了该文件,我们需要将其解析为模块记录。这有助于浏览器了解模块的不同部分。

该图显示了被解析成模块记录的main.js文件

创建模块记录后,它将被放置在模块图中。这意味着无论何时从此处请求,加载程序都可以将其从该映射中拉出。

模块映射图中的“获取”占位符被模块记录填充

解析中有一个细节看似微不足道,但实际上有很大的含义。

解析所有模块,就像它们"use strict"位于顶部一样。还存在其他细微差异。

例如,关键字await是在模块的顶级代码保留,的值this就是undefined

这种不同的解析方式称为“解析目标”。如果解析相同的文件但使用不同的目标,那么最终将得到不同的结果。
因此,需要在开始解析之前就知道要解析的文件类型是否是模块。

在浏览器中,这非常简单。只需放入type="module"的script标签。
这告诉浏览器应将此文件解析为模块。并且由于只能导入模块,因此浏览器知道任何导入也是模块。

加载程序确定main.js是一个模块,因为script标签上的type属性表明是这样,而counter.js必须是一个模块,因为它已导入

但是在Node中,您不使用HTML标记,因此无法选择使用type属性。社区尝试解决此问题的一种方法是使用 .mjs扩展。使用该扩展名告诉Node,“此文件是一个模块”。您会看到人们在谈论这是解析目标的信号。目前讨论仍在进行中,因此尚不清楚Node社区最终决定使用什么信号。

无论哪种方式,加载程序都将确定是否将文件解析为模块。如果它是一个模块并且有导入,则它将重新开始该过程,直到提取并解析了所有文件。

我们完成了!在加载过程结束时,您已经从只有入口点文件变为拥有大量模块记录。

建设阶段的结果,左侧为JS文件,右侧为3个已解析的模块记录

下一步是实例化此模块并将所有实例链接在一起。

3. 实例化

就像我之前提到的,实例将代码与状态结合在一起。

该状态存在于内存中,因此实例化步骤就是将所有事物连接到内存。

首先,JS引擎创建一个模块环境记录。这将管理模块记录的变量。然后,它将在内存中找到所有导出的框。模块环境记录将跟踪与每个导出关联的内存中的哪个框。

内存中的这些框尚无法获取其值。只有在评估之后,它们的实际值才会被填写。该规则有一个警告:在此阶段中初始化所有导出的函数声明。这使评估工作变得更加容易。

为了实例化模块图,引擎将进行深度优先的后顺序遍历。这意味着它将下降到图表的底部-底部的不依赖其他任何内容的依赖项-并设置其导出。

中间的一列空内存。 计数和显示模块的模块环境记录已连接到内存中的框。

引擎完成了模块下面所有出口的接线-模块所依赖的所有出口。然后,它返回一个级别,以连接来自该模块的导入。

请注意,导出和导入均指向内存中的同一位置。首先连接出口,可以确保所有进口都可以连接到匹配的出口。

与上图相同,但具有main.js的模块环境记录,现在其导入链接到其他两个模块的导出。

这不同于CommonJS模块。在CommonJS中,整个导出对象在导出时被复制。这意味着导出的任何值(例如数字)都是副本。

这意味着,如果导出模块以后更改了该值,则导入模块将看不到该更改。

中间的内存中,有一个导出的通用JS模块指向一个内存位置,然后将值复制到另一个内存位置,而导入的JS模块则指向新位置

相反,ES模块使用称为实时绑定的东西。两个模块都指向内存中的相同位置。这意味着,当导出模块更改值时,该更改将显示在导入模块中。

导出值的模块可以随时更改这些值,但是导入模块不能更改其导入的值。话虽如此,如果模块导入了一个对象,则它可以更改该对象上的属性值。

导出模块更改内存中的值。 导入模块也尝试但失败。

之所以拥有这样的实时绑定,是因为您可以在不运行任何代码的情况下连接所有模块。当您具有循环依赖性时,这将有助于评估,如下所述。

因此,在此步骤结束时,我们已连接了所有实例以及导出/导入变量的存储位置。

现在我们可以开始评估代码,并用它们的值填充这些内存位置。

4. 执行

最后一步是将这些框填充到内存中。JS引擎通过执行顶级代码(函数外部的代码)来实现此目的。

除了仅在内存中填充这些框外,评估代码还可能触发副作用。例如,模块可能会调用服务器。

模块将在功能之外进行编码,标记为顶级代码

由于存在潜在的副作用,您只需要评估模块一次。与实例化中发生的链接可以完全相同的结果执行多次相反,评估可以根据您执行多少次而得出不同的结果。

这是拥有模块映射的原因之一。模块映射通过规范的URL缓存模块,因此每个模块只有一个模块记录。这样可以确保每个模块仅执行一次。与实例化一样,这是深度优先的后遍历。

那我们之前谈到的那些周期呢?

在循环依赖关系中,您最终在图中有一个循环。通常,这是一个漫长的循环。但是为了解释这个问题,我将使用一个简短的循环的人为例子。

左侧为4个模块循环的复杂模块图。 右侧有一个简单的2个模块循环。

让我们看一下如何将其与CommonJS模块一起使用。首先,主模块将执行直到require语句。然后它将去加载计数器模块。

一个commonJS模块,其变量是在require语句之后从main.js导出到counter.js的,具体取决于该导入

然后,计数器模块将尝试message从导出对象进行访问。但是由于尚未在主模块中对此进行评估,因此它将返回undefined。JS引擎将在内存中为局部变量分配空间,并将其值设置为undefined。

中间的内存,main.js和内存之间没有连接,但是从counter.js到未定义的内存位置的导入链接

评估一直持续到计数器模块顶级代码的末尾。我们想看看我们是否最终将获得正确的消息值(在评估main.js之后),因此我们设置了超时时间。然后评估在上恢复main.js

counter.js将控制权返回给main.js,从而完成评估

消息变量将被初始化并添加到内存中。但是由于两者之间没有连接,因此在所需模块中它将保持未定义状态。

main.js获取到内存的导出连接并填写正确的值,但是counter.js仍指向其中未定义的其他内存位置

如果使用实时绑定处理导出,则计数器模块最终将看到正确的值。到超时运行时,main.js的评估将完成并填写值。

支持这些循环是ES模块设计背后的重要理由。正是这种设计使它们成为可能。


(以上是关于 ESM 的理论介绍, 原文链接在文末)。

Bundle & Bundleless

谈及 Bundleless 的优势,首先是启动快

因为不需要过多的打包,只需要处理修改后的单个文件,所以响应速度是 O(1) 级别,刷新即可即时生效,速度很快。

image.png

所以, 在开发模式下,相比于Bundle,Bundleless 有着巨大的优势。

基于 Webpack 的 bundle 开发模式

image.png
上面的图具体的模块加载机制可以简化为下图:
image.png
在项目启动和有文件变化时重新进行打包,这使得项目的启动和二次构建都需要做较多的事情,相应的耗时也会增长。

基于 ESModule 的 Bundleless 模式

image.png
从上图可以看到,已经不再有一个构建好的 bundle、chunk 之类的文件,而是直接加载本地对应的文件。
image.png
从上图可以看到,在 Bundleless 的机制下,项目的启动只需要启动一个服务器承接浏览器的请求即可,同时在文件变更时,也只需要额外处理变更的文件即可,其他文件可直接在缓存中读取。

对比总结

image.png

Bundleless 模式可以充分利用浏览器自主加载的特性,跳过打包的过程,使得我们能在项目启动时获取到极快的启动速度,在本地更新时只需要重新编译单个文件。

实现一个乞丐版 Vite

Vite 也是基于 ESM 的, 文件处理速度 O(1)级别, 非常快。

作为探索, 我就简单实现了一个乞丐版Vite:

GitHub 地址: Vite-mini

image.png

简要分析一下。

<body>
  <div id="app"></div>
  <script type="module" data-original="/src/main.js"></script>
</body>

html 文件中直接使用了浏览器原生的 ESM(type="module") 能力。

所有的 js 文件经过 vite 处理后,其 import 的模块路径都会被修改,在前面加上 /@modules/。当浏览器请求 import 模块的时候,vite 会在 node_modules 中找到对应的文件进行返回。

image.png

其中最关键的步骤就是模块的记载和解析, 这里我简单用koa简单实现了一下, 整体结构:

const fs = require('fs');
const path = require('path');
const Koa = require('koa');
const compilerSfc = require('@vue/compiler-sfc');
const compileDom = require('@vue/compiler-dom');
const app = new Koa();

// 处理引入路径
function rewriteImport(content) {
  // ...
}

// 处理文件类型等, 比如支持ts, less 等类似webpack的loader的功能
app.use(async (ctx) => {
  // ...
}

app.listen(3001, () => {
  console.log('3001');
});

我们先看路径相关的处理:

function rewriteImport(content) {
    return content.replace(/from ['"]([^'"]+)['"]/g, function (s0, s1) {
        // import a from './c.js' 这种格式的不需要改写
        // 只改写需要去node_module找的
        if (s1[0] !== '.' && s1[0] !== '/') {
          return `from '/@modules/${s1}'`;
        }
        return s0;
    });
}

处理文件内容: 源码地址

image.png

后续的都是类似的:

image.png

这个代码只是解释实现原理, 不同的文件类型处理逻辑其实可以抽离出去, 以中间件的形式去处理。

代码实现的比较简单, 就不额解释了。

Snowpack

image.png

和 webpack 的对比:

image.png

我使用 Snowpack 做了个 demo , 支持打包, 输出 bundle。

github: Snowpack-React-Demo

image.png

能够清晰的看到, 控制台产生了大量的文件请求(也叫瀑布网络请求),

不过因为都是加载的本地文件, 所以速度很快。

配合HMR, 实现编辑完成立刻生效, 几乎不用等待:

image.png

但是如果是在生产中,这些请求对于生产中的页面加载时间而言, 就不太好了。

尤其是HTTP1.1,浏览器都会有并行下载的上限,大部分是5个左右,所以如果你有60个依赖性要下载,就需要等好长一点。

虽然说HTTP2多少可以改善这问题,但若是东西太多,依然没办法。

关于这个项目的打包, 直接执行build:

image.png

打包完成后的文件目录,和传统的 webpack 基本一致:

image.png

在 build 目录下启动一个静态文件服务:

image.png

build 模式下,还是借助了 webpack 的打包能力:

image.png

做了资源合并:

image.png

就这点而言, 我认为未来一段时间内, 生产环境还是不可避免的要走bundle模式。

bundleless 模式在实际开发中的一些问题

开门见山吧, 开发体验不是很友好,几点比较突出的问题:

  • 部分模块没有提供 ESModule 的包。(这一点尤为致命)
  • 生态不够健全,工具链不够完善;

当然还有其他方方面面的问题, 就不一一列举。

我简单改造了一个页面, 就遇到很多奇奇怪怪的问题, 开发起来十分难受, 尽管代码的修改能立刻生效。

结论

bundleless 能在开发模式下带了很大的便利。 但就目前来说,要运用到生产的话, 还是有一段路要走的。

就目当下而言, 如果真的要用的话,可能还是 bundleless(dev) + bundle(production) 的组合。

至于未来能不能全面铺开 bundleless,我认为还是有可能的, 交给时间吧。

结尾

本文主要介绍了 esm 的原理, 以及介绍了以此为基础的Vite, Snowpack 等工具, 提供了两个可运行的 demo:

  1. Vite-mini
  2. Snowpack-React-Demo

并探索了 bundleless 在生产中的可行性。

Bundleless, 本质上是将原先 Webpack 中模块依赖解析的工作交给浏览器去执行,使得在开发过程中代码的转换变少,极大地提升了开发过程中的构建速度,同时也可以更好地利用浏览器的相关开发工具。

最后,也非常感谢 ESModule、Vite、Snowpack 等标准和工具的出现,为前端开发提效。

才疏学浅, 文中若有错误,还能各位大佬指正, 谢谢。

参考资料

  1. https://hacks.mozilla.org/201...
  2. https://developer.aliyun.com/...

如果你觉得这篇内容对你挺有启发,可以:

  1. 点个「在看」,让更多的人也能看到这篇内容。
  2. 关注公众号「前端e进阶」,掌握前端面试重难点,公众号后台回复「加群」和小伙伴们畅聊技术。

图片

查看原文

赞 47 收藏 21 评论 5

CrossingMay 赞了文章 · 2020-07-23

vue的jsx写法记录

[toc]

通过本文, 你可以学到一些vuejsx的语法。

vue更加推荐使用模板开发组件,但是在一些基础组件开发中,为了获取js的完全编程能力,不可避免需要使用一些jsx语法,而官方vue jsxvue特有的语法糖这方面的文档特别少, 本文记录一些本人在开发过程中使用jsx的经验和思考。

整体上来看,由于vuecreateElementreactcreateElement有区别,导致jsx的写法也有一些区别.但整体上还是符合react的jsx,react jsx进阶的语法。

如果需要了解vue jsx特殊写法的原理,可以阅读babel-plugin-transform-vue-jsx , 本文不做探讨。

data写法

jsx本质上是createElement的语法糖,最终会被编译器转为createElement函数.当在jsx的标签中使用{ ...obj }时, obj将会编译为createElement的第二个参数.

vuecreateElement跟react的createElement函数第二个参数意义是不一样的.在vue中,第二个参数是 data对象, 而react第二个参数是props。所以本人将这种方式称为data写法

如在vue中需要设置动态属性时:

const props={
  name: 'joyer',
},

<my-button {...{
  props:props,
}}></my-button>

当不知道模板中某个vue语法怎么用jsx中实现时,可以先转换为createElementdata对象,然后使用{...data}写在jsx标签上(本文重点).

如官方推荐原生dom属性的jsx写法:

<button domPropsType="submit"><button>

采用data写法为:

<button { ...{
  domProps: {
    type: 'submit',
  }, 
}}><button>

该方式不够优雅,如果官方有更加优雅的语法糖,推荐使用官方推荐。如果某个语法,官方没有案例,该方式就可以作为最终选择。 并且通过这种方式,createElement中所有的特性都可以用于jsx的开发了。

下文中所有的写法, 都可以使用data写法。

v-model的写法

官网中v-model写法不可行。

模板中写法为:

<el-input v-model.trim="inputValue"/>

jsx写法需要为:

<el-input vModel_trim={inputValue}/>
// 或者使用
<el-input 
 value={this.inputValue}
 on-input={val => this.inputValue = val.trim()}/>

v-for

模板中的写法:

<el-tag
 v-for="(item, index) in content"
 :key="index"
 type="success"
 >
    {{item.name}}
</el-tag>

jsx的写法:

{
  this.content.map((item, index) = >{
    return (<el-tag
      key={ index }
      type="success">{ item.name }</el-tag>);
  })
}

事件 & 按键修饰符

官方的写法

<input vOn:click_stop_prevent="newTodoText" />

一些编辑器会提示语法错误(因为reactjsx不允许这样写),推荐使用下面的写法

<input
 nativeOn-keyup={arg => arg.keyCode === 13 && this.fetch()}
 on-click={event => {event.stopPropagation();event.preventDefault();this.click()} }/>

修饰符需要自己在代码中实现。或者可以使用修饰符简写,对照官网的语法, jsx写法为:

<input {...{
    on: {
     '!click': () => this.doThisInCapturingMode,
    '~keyup': () => this.doThisOnce,
    '~!mouseover': () => this.doThisOnceInCapturingMode
    }
}} />

事件处理都采用了箭头函数, 跟react一样, 需要处理this绑定问题,可以使用bind绑定,

`on-click={this.click.bind(this, args) }`

不能直接赋值函数on-click={this.click}

高阶组件中的v-on="$listeners"v-bind="$attrs"

在高阶组件中, 一般都会使用v-on="$listeners"v-bind="$attrs",但在官方文档没有说明jsx如何实现,只有createElement中说明了实现:

return createElement('div', {
    props: {
      ...$attrs,
      otherProp: value,
    },
    on: {
      ...$listeners,
      click() {
      },
    }
},this.$slots.default])

参照data写法, jsx实现为:

const data = {
  props: {
     ...$attrs,
     otherProp: value,
  },
  on: {
     ...$listeners,
     click() {
     },
  }
}

<button { ...data }><button>

对于$attrs$listeners可以有更加灵活的用法。如要实现elemet-ui中一个可以快速布局el-form-item高阶组件,将el-form-itemel-col的结合:

render(h) {
 // 拆分出作用于col和form-item上的属性
 const colAttrs = {};
 const itemAtts = {};
 this.$attrs.forEach((attrName) => {
    // 这里使用了lodash
    if (_.startsWith(attrName, `col-`)) {
         colAttrs[attrName.replace(`col-`, '')] = this.$attrs[attrName];
         return;
    }
    itemAtts[attrName] = this.$attrs[attrName];
 });
 return (<el-col {
    ...{
       props: this.colAttrs,
    }
 }> 
    <el-form-item {
        ...{
            props: this.itemAtts,
        }
    }></el-form-item>
 </el-col>);
}

该高阶组件可以传递两种类型的属性, 带col-开头的属性,作用于el-col上面, 其他属性作用于el-form-item组件上。

如果需要标签属性透传,将当前组件的所有attrs传递给内部组件(内部组件也用v-bind="$attrs"),还需设置attrs值。
如高阶组件my-button:

<el-button v-bind="$attrs" type="primary">
 <slot></slot>
</el-button>

高阶组件high-button:

render(h) {
 return (<my-button { ...{
  props: this.attrs,
  attrs: this.$attrs,
 } }><my-button>);
}

通过设置attrshigh-button接收到的所有标签设置传递给my-button中。如果不这样做, 那么my-button中将接收不到任何属性设置,因为只传递props,在high-button组件中对于没有匹配high-buttonprops声明的标签属性都会被丢弃。

slot写法

默认插槽模板写法:

<button>
    <slot></slot>
</button>

jsx的写法:

<button>
    {this.$scopedSlots.default()}
</button>

具名插槽模板写法:

<button>
    <slot name="before"></slot>
    <slot ></slot>
</button>

jsx写法:

let before = '';
if (this.$scopedSlots.before) {
    before = this.$scopedSlots.before(props => props.text);
}
return (<button>
    { before }
    {this.$scopedSlots.default()}
</button>)

作用域插槽模板写法:

<slot :isAdvancedPanelShow="isAdvancedPanelShow"></slot>

jsx写法:

{this.$scopedSlots.default({
    isAdvancedPanelShow: this.isAdvancedPanelShow
})}

动态组件名字

还记得官网怎么介绍 createElement 获取js的完全编程能力吗? 举了一个根据不同的等级使用不同标题的案例:

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level, // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

这个案例通过jsx的写法为:

Vue.component('anchored-heading', {
  render: function (h) {
   const TagName = 'h' + this.level;
    return <TagName>{ this.$slots.default(this.props) }</TagName>
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

要知道,vuecreateElement函数的第一个参数可以是一个 HTML 标签名、组件选项对象,或者resolve了上述任何一种的一个async函数。

动态组件编排

在实际开发中, 碰到一种场景, 有组件CompA, CompB, CompC 。如果是a情况,需要在当前组件按照CompA, CompB, CompC顺序展示, 如果是不是a情况, 则通过CompC, CompB, CompA来展示。jsx写法为:

render(h) {
 // 假设组件A是需要特殊设置一些属性的
 const compA = (<CompA name="joyer">hellow word</CompA>)
 const content = this.status === 'a' ? [compA, CompB, CompC] : [CompC, CompB, compA];
 return (<div>{content}</div>)
}

这里末尾也可以这样写:

return (<div>{...content}</div>)

但在有些编辑器会报错,因为在react不允许这样写:

Spread children are not supported in React.

在babel官方编辑器中尝试

可以在babel官方提供的编辑器jsx尝试代码,不过需要记得在左边最下角添加babel=plugin-transform-vue-jsx插件:

添加vue-jsx插件

查看原文

赞 11 收藏 5 评论 0

CrossingMay 赞了回答 · 2020-07-03

解决less没有自定义函数吗~~

关注 7 回答 3

CrossingMay 关注了用户 · 2020-06-23

下一门课 @mofiter

通过“下一门课”公众号前去购买知识付费课程可获得奖励

关注 2

CrossingMay 赞了文章 · 2020-06-12

如何对多行文本进行截断?

前言

单行文字的截断非常简单,但是多行文字的截断可谓是非常头疼,刚学css时候就为了这个写了很多乱七八糟的代码,今天工作又遇到了这个需求...比较巧的是某公众号今天正好也推送了一篇如何设置截断的文章,心血来潮就自己将见到的各种方法总结下,也算加深记忆与理解。

各种奇淫巧技

先预设一个html结构,分别用不同的方法对其进行尝试,html结构:

<div class="wrap">
  <div>中国足球就像是一列疾驰而过的地铁,当再一次与世界杯擦肩而过的时候,我们才会更清楚地意识到,这列地铁的前方也许依旧是冬天,但是里皮的到来或许可以让那个冬天不再像以往那样的寒冷和漫长,因为至少他在用他的睿智和对足球的热情点亮了我们前方的路,只愿这终将是一列开往春天的地下铁。</div>
</div>

初始css代码,主要设置了容器的宽度,行高和overflow:hidden:

.wrap {
  width: 300px;
  height: 3.6em;
  border: 1px solid #eee;
  overflow: hidden;
  font: 700 16px/1.2 "微软雅黑";
}

下面的方法都是在样式名wrap的基础上添加各自的多重选择器。

-webkit-line-camp实现

这个放在第一个说,因为这个可能是最简单的方法了,代码:

.line-camp {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical; 
}

直接用-webkit-line-clamp控制行数,就可以达到目的,当文字超过三行的时候就会截断加上省略号。但是看到这个-webkit-就会知道这个只针对webkit内核浏览器,火狐都是不支持的,这可是很严重的问题,目前也没有很好的hack方法。并且注意这里文字我用了div包裹,加入用p等有默认内边距或外边距的元素包裹,这个样式就会出很大的问题无法使用。因此这个属性看起来美但是使用起来局限性太大了...不得不吐槽为啥不把这个属性规范统一了,也就没下面那么多事了。

浮动特性实现

这个方法我看到真得是被惊艳到了,网上有很多文章介绍了这个,搜了下找到了原文。这里就复述下。
从这个方法的思想源头来分析,假设一个固定大小(宽300px)的容器内部有三个块,一个左浮动(宽100px)高度撑满,右边两个右浮动根据文字多少自适应撑开。
图片描述

  • 当蓝色块中的文字逐渐增多时候,橙色块的位置也会发生变化,图中容器高度正好是三倍的行高,因此当蓝色块文字少于或等于三行的时候,橙色块一直在蓝色块的右下方移动:

    图片描述

    当蓝色块文字大于三行的时候,橙色块会移动到蓝色块右侧,粉色块下面:
    图片描述

  • 由于浮动的特性,橙色块的移动给了我们灵感,新增一个黄色块相对于橙色块定位:

    .realend {
        position: absolute;
        width: 100%;
        top: -20px;
        left: 200px;
        background-color: yellow;
     }

    那么随着蓝色块文字增多黄色块移动的效果如下:
    图片描述
    图片描述
    图片描述

    很惊喜的发现这样做的话,当蓝色块文字溢出时候,黄色块正好在右下角可以作为省略号的位置,而未溢出时候黄色块是溢出容器的,正好符合要求。

  • 但是这样多一个标签绝对定位明显是很啰嗦的,我们采用相对定位就可以了,只保留黄色块:

    .realend {
      float: right;
      width: 80px;
      background-color: yellow;
      position: relative;
      left: 200px;
      top: -20px;
    }

    效果一样的:
    图片描述

  • 整个思路已经有了,下面就轮到处理现在存在的问题了,红色块的宽度把蓝色块推到右边100px的距离,显然是不合理的,我们利用负margin处理蓝色块,将蓝色块设置为100%宽度,然后margin-left设置为-100px,这样蓝色块就能充满整个容器覆盖了粉色块,这时候黄色块也设置自身宽度的负margin-left设置为-80px,然后用盒模型的原理再设置padding-right为粉色块的宽度100px(设置margin-right也可以,都是利用盒模型让黄色块实际盒模型宽度为紫色块的宽度),这里的负边距的用法很巧妙,可以多理解下,然后再left百分百定位就可以了。

    .main {
      float: right;
      width: 100%;
      margin-left: -100px;
      background-color: #00ffff;
    }
    .realend {
      float: right;
      width: 80px;
      background-color: yellow;
      position: relative;
      left: 100%;
      top: -20px;
      margin-left: -80px;
      padding-right: 100px;
    }

    效果如下:
    图片描述

    给容易加上overflow:hidden就达到效果了。

  • 最后我们就根据上面的原理实现第二种文字截断:

    .ellipsis:before {
        content:"";
        float: left;
        width: 5px; height: 3.6em; }
    
    .ellipsis > *:first-child {
        float: right;
        width: 100%;
        margin-left: -5px; }        
    
    .ellipsis:after {
        content: "\02026";  
    
        box-sizing: content-box;
        -webkit-box-sizing: content-box;
        -moz-box-sizing: content-box;
    
        float: right; position: relative;
        top: -1.2em; left: 100%; 
        width: 3em; margin-left: -3em;
        padding-right: 5px;
        
        text-align: right;
    
      
      background-size: 100% 100%;
      /* 512x1 image, gradient for IE9. Transparent at 0% -> white at 50% -> white at 100%.*/
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAABCAMAAACfZeZEAAAABGdBTUEAALGPC/xhBQAAAwBQTFRF////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wDWRdwAAAP90Uk5TgsRjMZXhS30YrvDUP3Emow1YibnM9+ggOZxrBtpRRo94gxItwLOoX/vsHdA2yGgL8+TdKUK8VFufmHSGgAQWJNc9tk+rb5KMCA8aM0iwpWV6dwP9+fXuFerm3yMs0jDOysY8wr5FTldeoWKabgEJ8RATG+IeIdsn2NUqLjQ3OgBDumC3SbRMsVKsValZplydZpZpbJOQco2KdYeEe36BDAL8/vgHBfr2CvTyDu8R7esU6RcZ5ecc4+Af3iLcJSjZ1ivT0S/PMs3LNck4x8U7wz7Bv0G9RLtHuEq1TbJQr1OtVqqnWqRdoqBhnmSbZ5mXapRtcJGOc4t2eYiFfH9AS7qYlgAAARlJREFUKM9jqK9fEGS7VNrDI2+F/nyB1Z4Fa5UKN4TbbeLY7FW0Tatkp3jp7mj7vXzl+4yrDsYoVx+JYz7mXXNSp/a0RN25JMcLPP8umzRcTZW77tNyk63tdprzXdmO+2ZdD9MFe56Y9z3LUG96mcX02n/CW71JH6Qmf8px/cw77ZvVzB+BCj8D5vxhn/vXZh6D4uzf1rN+Cc347j79q/zUL25TPrJMfG/5LvuNZP8rixeZz/mf+vU+Vut+5NL5gPOeb/sd1dZbTs03hBuvmV5JuaRyMfk849nEM7qnEk6IHI8/qn049hB35QGHiv0yZXuMdkXtYC3ebrglcqvYxoj1muvC1nDlrzJYGbpcdHHIMo2FwYv+j3QAAOBSfkZYITwUAAAAAElFTkSuQmCC);
      
        background: -webkit-gradient(linear, left top, right top,
            from(rgba(255, 255, 255, 0)), to(white), color-stop(50%, white));
        background: -moz-linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);            
        background: -o-linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);
        background: -ms-linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);
        background: linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);

js实现

夜深人静了...下次再来写吧 TODO...

总结

推荐第二种利用浮动和负边距等特性的方案,可以兼容到所有浏览器,对理解css也有很大帮助,js方法其实很繁琐效率也很低不到迫不得己还是不要使用。

参考

本站某个很好的方案
还是本站的一个总结

查看原文

赞 4 收藏 3 评论 1

CrossingMay 赞了文章 · 2020-06-12

如何对多行文本进行截断?

前言

单行文字的截断非常简单,但是多行文字的截断可谓是非常头疼,刚学css时候就为了这个写了很多乱七八糟的代码,今天工作又遇到了这个需求...比较巧的是某公众号今天正好也推送了一篇如何设置截断的文章,心血来潮就自己将见到的各种方法总结下,也算加深记忆与理解。

各种奇淫巧技

先预设一个html结构,分别用不同的方法对其进行尝试,html结构:

<div class="wrap">
  <div>中国足球就像是一列疾驰而过的地铁,当再一次与世界杯擦肩而过的时候,我们才会更清楚地意识到,这列地铁的前方也许依旧是冬天,但是里皮的到来或许可以让那个冬天不再像以往那样的寒冷和漫长,因为至少他在用他的睿智和对足球的热情点亮了我们前方的路,只愿这终将是一列开往春天的地下铁。</div>
</div>

初始css代码,主要设置了容器的宽度,行高和overflow:hidden:

.wrap {
  width: 300px;
  height: 3.6em;
  border: 1px solid #eee;
  overflow: hidden;
  font: 700 16px/1.2 "微软雅黑";
}

下面的方法都是在样式名wrap的基础上添加各自的多重选择器。

-webkit-line-camp实现

这个放在第一个说,因为这个可能是最简单的方法了,代码:

.line-camp {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical; 
}

直接用-webkit-line-clamp控制行数,就可以达到目的,当文字超过三行的时候就会截断加上省略号。但是看到这个-webkit-就会知道这个只针对webkit内核浏览器,火狐都是不支持的,这可是很严重的问题,目前也没有很好的hack方法。并且注意这里文字我用了div包裹,加入用p等有默认内边距或外边距的元素包裹,这个样式就会出很大的问题无法使用。因此这个属性看起来美但是使用起来局限性太大了...不得不吐槽为啥不把这个属性规范统一了,也就没下面那么多事了。

浮动特性实现

这个方法我看到真得是被惊艳到了,网上有很多文章介绍了这个,搜了下找到了原文。这里就复述下。
从这个方法的思想源头来分析,假设一个固定大小(宽300px)的容器内部有三个块,一个左浮动(宽100px)高度撑满,右边两个右浮动根据文字多少自适应撑开。
图片描述

  • 当蓝色块中的文字逐渐增多时候,橙色块的位置也会发生变化,图中容器高度正好是三倍的行高,因此当蓝色块文字少于或等于三行的时候,橙色块一直在蓝色块的右下方移动:

    图片描述

    当蓝色块文字大于三行的时候,橙色块会移动到蓝色块右侧,粉色块下面:
    图片描述

  • 由于浮动的特性,橙色块的移动给了我们灵感,新增一个黄色块相对于橙色块定位:

    .realend {
        position: absolute;
        width: 100%;
        top: -20px;
        left: 200px;
        background-color: yellow;
     }

    那么随着蓝色块文字增多黄色块移动的效果如下:
    图片描述
    图片描述
    图片描述

    很惊喜的发现这样做的话,当蓝色块文字溢出时候,黄色块正好在右下角可以作为省略号的位置,而未溢出时候黄色块是溢出容器的,正好符合要求。

  • 但是这样多一个标签绝对定位明显是很啰嗦的,我们采用相对定位就可以了,只保留黄色块:

    .realend {
      float: right;
      width: 80px;
      background-color: yellow;
      position: relative;
      left: 200px;
      top: -20px;
    }

    效果一样的:
    图片描述

  • 整个思路已经有了,下面就轮到处理现在存在的问题了,红色块的宽度把蓝色块推到右边100px的距离,显然是不合理的,我们利用负margin处理蓝色块,将蓝色块设置为100%宽度,然后margin-left设置为-100px,这样蓝色块就能充满整个容器覆盖了粉色块,这时候黄色块也设置自身宽度的负margin-left设置为-80px,然后用盒模型的原理再设置padding-right为粉色块的宽度100px(设置margin-right也可以,都是利用盒模型让黄色块实际盒模型宽度为紫色块的宽度),这里的负边距的用法很巧妙,可以多理解下,然后再left百分百定位就可以了。

    .main {
      float: right;
      width: 100%;
      margin-left: -100px;
      background-color: #00ffff;
    }
    .realend {
      float: right;
      width: 80px;
      background-color: yellow;
      position: relative;
      left: 100%;
      top: -20px;
      margin-left: -80px;
      padding-right: 100px;
    }

    效果如下:
    图片描述

    给容易加上overflow:hidden就达到效果了。

  • 最后我们就根据上面的原理实现第二种文字截断:

    .ellipsis:before {
        content:"";
        float: left;
        width: 5px; height: 3.6em; }
    
    .ellipsis > *:first-child {
        float: right;
        width: 100%;
        margin-left: -5px; }        
    
    .ellipsis:after {
        content: "\02026";  
    
        box-sizing: content-box;
        -webkit-box-sizing: content-box;
        -moz-box-sizing: content-box;
    
        float: right; position: relative;
        top: -1.2em; left: 100%; 
        width: 3em; margin-left: -3em;
        padding-right: 5px;
        
        text-align: right;
    
      
      background-size: 100% 100%;
      /* 512x1 image, gradient for IE9. Transparent at 0% -> white at 50% -> white at 100%.*/
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAABCAMAAACfZeZEAAAABGdBTUEAALGPC/xhBQAAAwBQTFRF////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wDWRdwAAAP90Uk5TgsRjMZXhS30YrvDUP3Emow1YibnM9+ggOZxrBtpRRo94gxItwLOoX/vsHdA2yGgL8+TdKUK8VFufmHSGgAQWJNc9tk+rb5KMCA8aM0iwpWV6dwP9+fXuFerm3yMs0jDOysY8wr5FTldeoWKabgEJ8RATG+IeIdsn2NUqLjQ3OgBDumC3SbRMsVKsValZplydZpZpbJOQco2KdYeEe36BDAL8/vgHBfr2CvTyDu8R7esU6RcZ5ecc4+Af3iLcJSjZ1ivT0S/PMs3LNck4x8U7wz7Bv0G9RLtHuEq1TbJQr1OtVqqnWqRdoqBhnmSbZ5mXapRtcJGOc4t2eYiFfH9AS7qYlgAAARlJREFUKM9jqK9fEGS7VNrDI2+F/nyB1Z4Fa5UKN4TbbeLY7FW0Tatkp3jp7mj7vXzl+4yrDsYoVx+JYz7mXXNSp/a0RN25JMcLPP8umzRcTZW77tNyk63tdprzXdmO+2ZdD9MFe56Y9z3LUG96mcX02n/CW71JH6Qmf8px/cw77ZvVzB+BCj8D5vxhn/vXZh6D4uzf1rN+Cc347j79q/zUL25TPrJMfG/5LvuNZP8rixeZz/mf+vU+Vut+5NL5gPOeb/sd1dZbTs03hBuvmV5JuaRyMfk849nEM7qnEk6IHI8/qn049hB35QGHiv0yZXuMdkXtYC3ebrglcqvYxoj1muvC1nDlrzJYGbpcdHHIMo2FwYv+j3QAAOBSfkZYITwUAAAAAElFTkSuQmCC);
      
        background: -webkit-gradient(linear, left top, right top,
            from(rgba(255, 255, 255, 0)), to(white), color-stop(50%, white));
        background: -moz-linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);            
        background: -o-linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);
        background: -ms-linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);
        background: linear-gradient(to right, rgba(255, 255, 255, 0), white 50%, white);

js实现

夜深人静了...下次再来写吧 TODO...

总结

推荐第二种利用浮动和负边距等特性的方案,可以兼容到所有浏览器,对理解css也有很大帮助,js方法其实很繁琐效率也很低不到迫不得己还是不要使用。

参考

本站某个很好的方案
还是本站的一个总结

查看原文

赞 4 收藏 3 评论 1

CrossingMay 赞了文章 · 2020-06-03

SVG 动画精髓

TL;DR

本文主要是讲解关于 SVG 的一些高级动画特效,比如 SVG 动画标签,图形渐变,路径动画,线条动画,SVG 裁剪等。

例如:路径动画

gif

图形渐变:

fig

线条动画:

test.gif-388.2kB

以及,相关的动画的矩阵知识,这个也是现在 CSS 动画里面最重要,同时也是最为欠缺的知识点:

image.png-7.1kB

文章会先从基本语法入手,然后,慢慢深入。介绍一些动画基本原理和对应的数学原理知识点。并且文章后面,还附有相关语法的介绍,当你在遇到不熟悉语法的时候可以参考参考。

前面一篇文章,主要介绍了一些 SVG 的基本概念和基本图形。接下来我们需要了解一下,SVG 处理矢量这个特性之外,还有啥内容吸引我们,能让 SVG 现在普及度这么高?

原文参考:前端小吉米

SVG Animation

在 SVG 中,如果我们想实现一个动画效果,可以使用 CSS,JS,或者直接使用 SVG 中自带的 animate 元素添加动画。

使用 CSS 的话,有两种选择一种是通过 style 直接内联在里面,另外是直接使用相关的动画属性-- transform

 <use id="star" class="starStyle" xlink:href="#starDef"
       transform="translate(100, 100)"
       style="fill: #008000; stroke: #008000"/>

而使用 SVG 中自定的 animate 主要还是 SVG 自己的东西,比较好用。如果想用 CSS 的动画,这都无所谓。

先看一个 SVG animate DEMO:

<rect x="10" y="10" width="200" height="20" stroke="black" fill="none">
  <animate
    attributeName="width"
    attributeType="XML"
    from="200" to="20"
    begin="0s" dur="5s"
    fill="freeze" />
</rect>

通过将 animate 标签嵌套在指定的图形里面,即可实现变换的效果。另外,还有 animateTransform,它主要是用来做变形动画的。

<rect x="-10" y="-10" width="20" height="20"
    style="fill: #ff9; stroke: black;">
    <animateTransform attributeType="XML"
      attributeName="transform" type="scale"
      from="1" to="4 2"
      begin="0s" dur="4s" fill="freeze"/>
</rect>

简单来说:

  • animate: 相当于 CSS 中的 transition

  • animateTransform: 相当于 CSS 中的 transform

里面一些技术细节我们这里就不过多讲解了。这里,主要想介绍一下 animate 中的 morph 的效果。

animate morph

该效果主要做的就是图形内部的渐变。如图:

fig

这种动画是怎么实现呢?

直接看代码吧:

<path fill="#1EB287">
    <animate 
             attributeName="d" 
             dur="1440ms" 
             repeatCount="indefinite"
             keyTimes="0;
                       .0625;
                       .208333333;
                       .3125;
                       .395833333;
                       .645833333;
                       .833333333;
                       1"
             calcMode="spline" 
             keySplines="0,0,1,1;
                         .42,0,.58,1;
                         .42,0,1,1;
                         0,0,.58,1;
                         .42,0,.58,1;
                         .42,0,.58,1;
                         .42,0,.58,1"
             values="M 0,0 
                     C 50,0 50,0 100,0
                     100,50 100,50 100,100
                     50,100 50,100 0,100
                     0,50 0,50 0,0
                     Z;

                     M 0,0 
                     C 50,0 50,0 100,0
                     100,50 100,50 100,100
                     50,100 50,100 0,100
                     0,50 0,50 0,0
                     Z;

                     M 50,0 
                     C 75,25 75,25 100,50 
                     75,75 75,75 50,100
                     25,75 25,75 0,50
                     25,25 25,25 50,0
                     Z;

                     M 25,50 
                     C 37.5,25 37.5,25 50,0 
                     75,50 75,50 100,100
                     50,100 50,100 0,100
                     12.5,75 12.5,75 25,50
                     Z;

                     M 25,50 
                     C 37.5,25 37.5,25 50,0 
                     75,50 75,50 100,100
                     50,100 50,100 0,100
                     12.5,75 12.5,75 25,50
                     Z;

                     M 50,0
                     C 77.6,0 100,22.4 100,50 
                     100,77.6 77.6,100 50,100
                     22.4,100, 0,77.6, 0,50
                     0,22.4, 22.4,0, 50,0
                     Z;
                     
                     M 50,0
                     C 77.6,0 100,22.4 100,50 
                     100,77.6 77.6,100 50,100
                     22.4,100, 0,77.6, 0,50
                     0,22.4, 22.4,0, 50,0
                     Z;
                     
                     M 100,0 
                     C 100,50 100,50 100,100
                     50,100 50,100 0,100
                     0,50 0,50 0,0
                     50,0 50,0 100,0
                     Z;"/>
  </path>

这么多,是不是感觉有点懵逼。不过,我们细分来看一下其实很简单。里面主要是利用 animate 中的 keyTimescalcModekeySplines,以及 values 这几个属性。不急,我们一个一个来解释一下。

  • keyTimes: 这其实和 CSS 中定义的 @keyframes 一样。通过 0-1 之间的值,定义每段动画完成的时间。格式为:value;value...。例如 0;.0625;.208333333;.3125;.395833333;.645833333;.833333333;1。从第一个动画,到第二个动画经历的时间比例为 6.25%。并且,keyTimes 需要和 values 里面定义的帧数一致。

  • calcMode: 用来定义动画具体的插值模型。取值有: discrete | linear[default] | paced | spline。具体可以参考 MDN。这里我们主要介绍一下 spline。该值表示每个动画间使用自定的贝塞尔变换曲线。如果没有特殊要求,使用 linear 其实已经足够了,这样就不用麻烦去定义下面的 keySplines 属性。

  • keySplines:该值用来具体定义动画执行时的 贝塞尔曲线。使用格式是通过 ; 来分隔每一个值。即,cubic-bezier(.31,.57,.93,.46) 为一组。使用 keySplines 表达,则为:keySplines = ".31,.57,.93,.46;"。当然,里面的贝塞尔曲线组数为 整个动画帧数 - 1

而 values 就很简单了。它是直接结合 attributeName 属性,来设置具体的值,每个值之间使用 ; 进行分隔。

像上面那样,可以在指定元素里面嵌套多个 animate,既实现了形状的改变,又实现了颜色的改变。Morph 比较常用于数字的更迭,比如,倒数 10s 的相关动画。到这里,Morpah 相关的知识点就结束了。

接着,让我们来看一下 SVG 中,另外一非常重要的标签 -- animateMotion

该标签可以让指定的元素,绕着指定的路径进行运动。所以这对于复杂的路径来说非常有用,因为我们很难使用 transform 去模拟复杂的变换路径。看一个 DEMO

gif

animateMotion

animateMotion 大致的属性和 animate 差不多,不过,它还拥有自己特有的属性,比如 keyPointsrotatepath 等。不过,calcMode 在 AM(animateMotion) 中的默认属性由,linear 变为 paced

这些属性,我们慢慢介绍,先从最简单的开始吧。首先,我们来看一个 DEMO:

<g>
  <rect x="0" y="0" width="30" height="30" style="fill: #ccc;"/>
  <circle cx="30" cy="30" r="15" style="fill: #cfc; stroke: green;"/>
  <animateMotion from="0,0" to="60,30" dur="4s" fill="freeze"/>
</g>
  • from,to:指定两点的位置,位置参数是以元素的坐标为原点的。

  • dur:执行渲染时间

  • fill:指定动画结束后停留的装填。有 freezeremove 效果。remove 表示回到动画开始的位置,freeze 表示停留在动画结束的位置。

如果,你想要更复杂的路径,可以直接使用 path 属性来指定路径。用法和 path 标签中 d 属性是一样的。

<rect x="0" y="0" width="30" height="30" style="fill: #ccc;">
    <animateMotion
    path="M50,125 C 100,25 150,225, 200, 125"
    dur="6s" fill="freeze"/>
</rect>

或者使用 mpath 标签,引用外部的 path

  <path d="M10,110 A120,120 -45 0,1 110 10 A120,120 -45 0,1 10,110"
      stroke="lightgrey" stroke-width="2" 
      fill="none" id="theMotionPath"/>
  <circle cx="10" cy="110" r="3" fill="lightgrey"  />
  <circle cx="110" cy="10" r="3" fill="lightgrey"  />

  <!-- Red circle which will be moved along the motion path. -->
  <circle cx="" cy="" r="5" fill="red">

    <!-- Define the motion path animation -->
    <animateMotion dur="6s" repeatCount="indefinite">
      <mpath xlink:href="#theMotionPath"/>
    </animateMotion>
  </circle>

动画效果为:

tuiche.gif-17.8kB

所以,一般而言我们在定义 AM 的路径的时候,只用一种方式定义即可,否则会发生相应的覆盖:mpath>path>values>from/to

在 AM 运动中,还有一个很重要的概念就是旋转角。默认情况下,运动物体的角度是按照它和坐标轴的初始角度确定的。例如:

test.gif-23.7kB

这样看起来确实有些别扭,那能不能让物体垂直于路径进行运动呢?

有的,根据 rotate 属性值,一共有 3 个值可供选择。

  • auto:让物体垂直于路径的切线方向运动。不过,如果你的路径是闭合曲线的话,需要注意起始点的位置。

例如:

test.gif-22.4kB

  • auto-reverse:让物体垂直于路径的切线方向并 + 180°。也就是和 auto 运动关于切线对称。

test.gif-27.8kB

  • Number:让物体以固定的旋转角度运动。这个就相当于使用 transform:rotate(deg) 进行控制。

在动画设置标签中,还有一个更简单的--set

set

该标签也是用来模拟 transition 效果的。它和 animate 的主要区别是,它仅仅需要 to 的指定属性,而不需要其他的参考属性,比如 fromby 等。那它有啥特别的存在意义吗?

有的,因为 set 针对于所有属性,甚至包括 style 里面的相关 CSS 属性。所以,可以靠它来很好描述一些非 number 的属性值。

<text text-anchor="middle" x="60" y="60" style="visibility: hidden;">
  <set attributeName="visibility" attributeType="CSS"
    to="visible" begin="4.5s" dur="1s" fill="freeze"/>
  All gone!
</text>

矩阵动画

上面差不多简单阐述了关于 SVG 一些比较有特点的动画。当然,还有比较重要的线条动画,这个我们放到后面进行讲解。这里先来看一下所有动画中,非常重要的矩阵原理。线性代数应该是大学里面来说,最容易学的一门科目,MD。。。还记得,大学线代期末考试的时候,100 分的同学应该说是如韭菜地般,一抓一大片(对不起,我没能和他们同流合污。)

那矩阵是如何在动画中使用的呢?

简单的说,矩阵中的每个元素其实可以等价代换为每个因式里面的系数:

image.png-6kB

上面也叫作 三维矩阵。即,它涉及到 x,y,z 轴的计算。那对于我们平面 2D 变换来说,那么此时矩阵又是哪种形式呢?

很简单,只要将 z 轴永远置为一个常数就 OK。这里,惯例上是直接取 0 0 1 来设置。

image.png-1.7kB

不信的话,大家只要代进去乘以乘,应该就可以得到结果了。所以,在二维中,具体变换方式为:

image.png-7.1kB

后面,我们也会依据这个公式进行相关的变形操作。那矩阵变换是怎么运用到 CSS/SVG 当中呢?

在 CSS 中,是直接使用 transform 中的属性:

transform: matrix(a,b,c,d,e,f);

当然,在 SVG 中也是一样的:

<g transform="matrix(1,2,3,4,5,6)">
    <line x1="10" y1="20" x2="30" y2="40" style="stroke-width: 10px; stroke: blue;"/>
  </g>

所以,我们主要的重点就是讲解一下 matrix 这个属性。它的格式为:

matrix(a,b,c,d,e,f);

对应于我们上面的公式有:

image.png-2.5kB

在接触 transform 的时候,大家应该了解到 transform 里面有很多固定的动画属性:

  • translate()

  • rotate()

  • scale()

  • skew()

实际上,在底层还是使用 matrix 实现的变换。就拿 translate 举个例子吧。

translate 的格式为:

translate(dx,dy)

相当于参考当前原点,在 x/y 轴上移动 dx/dy 的距离。那么映射到矩阵,应该如何表示呢?

很简单,它等同于:

matrix(1 0 0 1 dx dy);

使用代数证明一下:

假设有 matrix(1 0 0 1 20 30)

变为矩阵为:

image.png-3.2kB

根据,上面的表达式有:

X = x'*1 + y'*0 + 20 = x' + 20
Y = x'*0 + y'*0 + 30 = y' + 30

所以,就是 X 在原有 X 轴坐标上向右移动 20 的距离,Y 相对于原有移动 30 的距离。

那么其他几个属性呢?也是怎么变化的吗?

恩,类似。只是里面取值不一样:

  • scale(x,y): 放大 X/Y 轴,矩阵的表达为 matrix(x 0 0 y 0 0)。

  • rotate(θ): 坐标旋转,矩阵的表达为 matrix(cosθ sinθ -sinθ cosθ 0 0)。

  • skew(θx,θy): X/Y 轴拉伸,矩阵的表达为 matrix(1 tanθx tanθy 1 0 0)。

注意,上面三个都会改变原有物体的坐标系!!! 这点很重要,换句话说,后面每次变换都是基于前面一个的变换结果的。

详情看图:

image.png-49.6kB

详情可以参考:MDN matrix

不过,这并不是我们使用 matrix 的重点,也不是它的优势。它的优势在于可计算,即,能够将复杂的动画集合到一个表达式中,并且,后续的变换可以直接基于当前的 matrix。

我们先来了解一下,如果多个变换动画一起使用,matrix 应该如何表达呢?

只需要找到我们变换动画对应的矩阵,然后相乘即可。例如,先旋转 45°,然后放大 1.5 倍,则有变换动画为:

transform: rotate(45deg) scale(1.5,1.5);

注意,虽然,你定义动画是分开的,但此时的动画是同时进行的。为啥?因为,这两个动画实际上可以整合成为一个变换矩阵:

image.png-7.4kB

并且,位置是不可以调换的。比如,transform: scale(2,2) translate(20px,30px)。即,你先放大两倍,然后移动 20,30 的距离。注意,这里移动的 20,30 相对的是已经放大过后的坐标,相对于原坐标而言就是 40,60 了。 如果,你调换位置,即 transform: translate(20px,30px) scale(2,2)。就变成现在原坐标移动 20,30,然后再放大两倍。

而上面强调的顺序关系,实际上就可以理解为矩阵不满足交换律的原则。因为一旦交换,结果很可能不一样。

矩阵高级用法

上面的内容只是简单的描述了关于矩阵的概念。在实际中,矩阵可以说是真正利器。

假设现在有一个动画,要求你将一个物体从一个点通过抛物线的方式移动到另外一个点,那么此时要求 JS/CSS 随你挑。此时,你会不会感觉,呼吸急促,头脑发热呢?

恩,matrix 可以治,而且包治百病。不过,matrix 有一个限制点,它只能用于一次线性动画表达式。即,针对于抛物线,椭圆曲线这类复杂曲线来说,不太合适。那么有什么办法吗?

有的,微分思想。每一段动画其实都可以通过一定范围内的直线拼接而成,那么这样,我们就可以将一段抛物线拆分为由几段线段构成的曲线。当然,如果你分的越细,拟合度就越高。这里我们不打算过度你和,我们简单的将一段抛物线分为 5段。

如图:

image.png-12.9kB

那么接下来就是抠细节。这里,依次取倾角为 45°,30°,0°,-45°,-30° 这 5 段直线。每段分配的时间比例为 20%、25%、10%、25%、20% 这主要是用于 keyframe 的设定。现在,用数学来分析一下,这个动画到底该怎么弄。

现在,已知两点之间的距离为 100px。那么我们同样根据上述比例分,则有 20px, 25px, 10px, 25px, 20px。

这里我们以 45° 倾角为参考点,则终点坐标为 (20,20); 。那么,该段的矩阵为:

// 注意 Y 轴需要取负值!

 1 0 20
 0 1 -20
 0 0 1

CSS 中的变形动画为:

transform: matrix(1,0,0,1,20,-20);

然后,第二段就为:

1 0 25
0 1 -14.4
0 0 1

使用矩阵的乘法法,则有:

 1 0 45
 0 1 -34.4
 0 0 1

变形动画为:

transform: matrix(1,0,0,1,45,-34.4);

剩余几段也是这样的做法。最终,整个 keyframe 就应该表示为:

@keyframe Parabola{
    20%{
        transform: matrix(1,0,0,1,20,-20);
    }
    45%{
        transform: matrix(1,0,0,1,45,-34.4);
    }
    ...
}

整个动画过程差不多都是这样。当然,矩阵也不仅仅局限于这几个动画,凭借着高度定制化和灵活性的特点,这它还常常用于进行回弹,弹跳等动画中。如果大家有兴趣,后期也可以对这类动画进行简单的讲解。

后面,我们最后来了解一下 SVG 中很重要的线条动画。

线条动画

SVG 中的线条动画常常用作过渡屏(splash screen)中。例如:

test.gif-35.7kB

或者,一些比较炫酷的 LOGO 中,比如 AllowTeam 的:

AT

看到这些炫酷的效果,大家有没有动心想学一学,看看自己到底能否做的这么好呢?

OK,我们现在来正式介绍一下线条动画。在 SVG 中,最长用到的线条标签就是 Path。这里我前面一篇文章已经做了介绍,我这里就不赘述了。

而在具体变化当中用到的是关于 stroke 的相关属性:(下面的属性都可以直接用在 CSS 当中!)

  • stroke*:定义笔触的颜色。例如:stroke="green"

  • stroke-dasharray*:定义 dash 和 gap 的长度。它主要是通过使用 , 来分隔 实线间隔 的值。例如:stroke-dasharray="5, 5" 表示,按照 实线为 5,间隔为 5 的排布重复下去。如下图:

image.png-0.2kB

放大看有:

image.png-5.3kB

另外,stroke-dasharray 并不局限于只能设置两个值,要知道,它本身的含义是设置最小重复单元,即,dash,gap,dash,gap...。比如,我定义 stroke-dasharray="15, 10, 5" 则相当于,[15,10,5] 为一段。则有:

image.png-0.3kB

放大看则有:

image.png-7.7kB

  • stroke-dashoffset*: 用来设置 dasharray 定义其实 dash 线条开始的位置。值可以为 number || percentage。百分数是相对于 SVG 的 viewport。通常结合 dasharray 可以实现线条的运动。

  • stroke-linecap: 线条的端点样式。

  • stroke-linejoin: 线条连接的样式

  • stroke-miterlimit: 一个比较复杂的概念,如果我们只是画一些一般的线段,使用上面 linejoin 即可。如果涉及对边角要求比较高的,则可以使用该属性进行定义。它的值,其实就是角长度比上线宽:

image.png-5.4kB

而实际理解的话,就是假设当 width 为 1。此时比例为 2。那么 miter = 2。那么超过 2 的 miter 部分则会被 cut 掉。可以参照:

image.png-15.6kB

他主要是配合 linejoin 一起使用。因为 linejoin 默认取值就是 miter。所以,默认情况下就可以使用该标签属性。它默认值为 4。其余的大家下去实践一下即可。详细可以参考: miter

  • stroke-opacity:线段的透明度

  • stroke-width:线的粗细。

OK,介绍完关于 path 的所有 stroke 属性之后,我们就要开始动手写一下让线条动起来的代码。简单来说,就是通过 stroke-dashoffsetstroke-dasharray 来做。整个动画可以分为两个过程:

  • 通过 dasharray 将实线部分隐藏,空余为全线段长。然后,将实线部分增加至全长。比如:dasharray: 0,1000 变为 dasharray: 1000,1000

  • 同时,通过 dashoffset 来移动新增的实线部分,造成线段移动的效果。有: dashoffset:0,变为 dashoffset:1000

不过,这里我们不打算使用 Path 来做啥复杂的动画,这主要考虑到手头没有一些 SVG 生成工具。所以,这里我们就以 Text 来做吧(因为做起来真的简单)。

这里,先以 IV-WEB 这段文字来做动画。

先给大家看一下最终结果:

test.gif-61.9kB

那么这种动画是怎么做的呢?

这里,我主要介绍一下关于 CSS 相关,SVG 就一个 Text 我直接贴代码了:

<svg viewBox="0 0 1320 300">

  <!-- Symbol -->
  <symbol id="s-text">
    <text text-anchor="middle"
          x="50%" y="50%" dy=".35em">
      IV-WEB
    </text>
  </symbol>  

  <!-- Duplicate symbols -->
  <use xlink:href="#s-text" class="text"
       ></use>
  <use xlink:href="#s-text" class="text"
       ></use>
  <use xlink:href="#s-text" class="text"
       ></use>
 

</svg>

上面是通过创建一个居中定位的字体,然后使用 3 个 text 重叠。具体 CSS 我们下面来说一下。首先,我们营造的效果是从无到有,就需要使用 dasharray 将 gap 设置的足够大。这里我取 300 即可。

stroke-dasharray: 0 300;

然后,通过 nth-child 选择器,给每一个文字使用不同的颜色值:

.text:nth-child(3n + 1) {
  stroke: #F60A0A;
}
.text:nth-child(3n + 2) {
  stroke: #F2FF14;
}

.text:nth-child(3n + 3) {
  stroke: #FB9505;
}

下面才是重点内容。此时,这 3 个 text 的起始点重合。我现在既要他们在运行时不完全重合,又要他们的线条能进行滚动。不啰嗦了,直接看代码吧:

@keyframes stroke {
  100% {
    stroke-dashoffset: 1000;
    stroke-dasharray: 80 160;
  }
}

@keyframes stroke1 {
  100% {
    stroke-dashoffset: 1080;
    stroke-dasharray: 80 160;
  }
}


@keyframes stroke2 {
  100% {
    stroke-dashoffset: 1160;
    stroke-dasharray: 80 160;
  }
}

这就是上面 3 个不同的 text 运用的动画。dashoffet 由 0 到 1000。这完成了滚动的目的。同时,为了让字体不重合,我还需要在对应字体的 dashoffset 上,加上不同的间隔距离。比如,第一个字体 offset 为 1000。那么第二个字体,我需要加上前一个字体 dash 的长度,即,80。所以,第二个字体就变为 1080。那么第三个就是加上前两个的 dash 长度,即 1160

大致过程就是这样,详情可以查看: IVWEB 线条动画

这里再给大家布置一个练习作业,如何实现无线连续的分段动画呢?

具体效果如图:

test.gif-388.2kB

给点提示:

将多个文字重叠,取不同的 offset 和 array 即可。动画的终止位置一般取一个 gap + dash 的周期长即可。

后面看看这篇文章反响如何,到时候再决定是否再写一篇续集,介绍该作业的原理。

SVG 文字

在 SVG 中定义文字直接使用 text 标签即可。关于文字来说,一般而言需要注意的点就那么即可,文字的排列,间距等等。这些都可以直接使用 CSS 进行控制。不过,有几个属性比较特殊,这里需要额外提一下。

text-anchor

用来定义参考点和实际字符之间的定位关系。格式为:

  • text-anchor: start | middle | end | inherit

直接看代码解释吧:

<!-- Anchors in action -->
    <text text-anchor="start"
          x="60" y="40">A</text>

    <text text-anchor="middle"
          x="60" y="75">A</text>

    <text text-anchor="end"
          x="60" y="110">A</text>

第一个 A,参考的是 (60,40) 的点,定义为 start ,那么参考点应该在字符的前面。

image.png-1kB

而剩下两个也是同样的道理:

image.png-1.9kB

tspan

现在,假如我们想在 text 里面添加一些特殊的字符效果,比如斜体,加粗等。由于,text 标签不能实现嵌套,所以,为了解决这个痛点,提出了 tspan 的标签。它其实就是一个可以嵌套的 text 标签。

<text x="10" y="30" style="font-size:12pt;">
  Switch among
  <tspan style="font-style:italic">italic</tspan>, normal,
  and <tspan style="font-weight:bold">bold</tspan> text.
</text>

tspan 里面同样可以自定义相关的自身属性。详细的可以参考 tspan 我这里就不详述了。

在 Path 展示 text

Text 一般可以横放,竖放。那有没有啥办法让文字可以按照一定的路径任意排放呢?

有的,这里可以使用 textPath 标签,来定义具体参考路径。

<path id="sharp-corner"
    d="M 30 110 100 110 100 160"
    style="stroke: gray; fill: none;"/>

<text>
    <textPath xlink:href="#sharp-corner">
    Making a quick turn
    </textPath>
</text>

如图:

image.png-11kB

具体细节我这里就不多说了。

Clip

在 DOM 中如果想展示一个图片的部分,或者以某种形状展示图片的部分,一般是通过一个 cover div 来实现的。不过,如果涉及到不规则图形的话,那么 DOM 就有天生缺陷了(当然使用 CSS 里的 clip-path 可以完成,不过兼容性不太好)。而在 SVG 中,提供了 clipPath 标签,能够让我们自定义裁剪图片的范围和形状。

clipPath 里面可以接任何图形,比如,path,rect 甚至是 text。使用的时候,直接在 style 中,指定 clip-path 即可,或者直接使用 clip-path 属性指定。

<defs>
  <clipPath id="textClip">
    <text id="text1" x="20" y="20" transform="rotate(60)"
      style="font-family: 'Liberation Sans';
        font-size: 48pt; stroke: black; fill: none;">
CLIP
    </text>
  </clipPath>
 </defs>
 
 <use transform="translate(100, 0)"
  xlink:href="#shapes" style="clip-path: url(#textClip);"/>
  
   <use transform="translate(100, 0)"
  xlink:href="#shapes" clip-path="url(#textClip);"/>

image.png-66.6kB

或者说,如果我们想画一个圆的裁剪区域的话:

<defs>
     <clipPath id="circularPath" clipPathUnits="objectBoundingBox">
     <circle cx="0.5" cy="0.5" r="0.5"/>
    </clipPath>
</defs>

<use xlink:href="#shapes" style="clip-path: url(#circularPath);" />

Appendix 参考标签

g

分组标签应该毫无意外排第一,因为其实作为绘制图形中最常和最基本的标签。前面一篇文章也主要介绍过了,这里做点补充。

每一个分组标签都带有 id 属性,唯一标识该分组,为什么呢?

因为,后面我们可以使用该 id 标签添加动画,重用该分组等。

<g id="demo" stroke="green" fill="white" stroke-width="5">
     <circle cx="25" cy="25" r="15"/>
     <circle cx="40" cy="25" r="15"/>
     <circle cx="55" cy="25" r="15"/>
     <circle cx="70" cy="25" r="15"/>
   </g>

每个分组里面可以含有一些描述标签,比如 desc。 这些描述内容是不会被渲染的。

<g id="demo" stroke="green" fill="white" stroke-width="5">
    <desc>Just Demo</desc>
     <circle cx="25" cy="25" r="15"/>
   </g>

use

该标签就是结合 g 标签一起使用,作用是可以复用 g 分组的样式。

<g id="Port">
      <circle style="fill: inherit;" r="10"/>
</g>
<use x="50" y="30" xlink:href="#Port" class="classA"/>

里面使用 xlink:href 加上指定 group 的 id,然后通过 xy 属性指定副本放置的位置。不过,有一个限制,use 标签的 style 属性,并不能覆盖点原始的 group style 样式。而且,有时候,我们只是想使用一些模板,即,图形并未被解析,只有代码存在。这时候,就需要使用 defs 来包裹了。

defs

用来保存一些代码,使其不会被浏览器解析。并且里面的分组可以被 use 属性的 style 样式所覆盖。

<defs>
    <g id="Port">
      <circle style="fill: inherit;" r="10"/>
    </g>
  </defs>

 <use x="50" y="50" xlink:href="#Port" style="fill: blue;"/>

symbol

该标签和 g 标签类似,也是用来进行分组。不过,它有个特点,即,不会被浏览器所渲染。那它不和 defs 差不多吗?

恩,确实。不过,defs 是官方推荐,用来包裹一些模板 svg 代码而创造出来,用来增加可读性的标签。而 symbol 是存粹的作为一个模板。它可以独立于 svg 的 viewbox 来自定义子 viewbox 和 preserveAspectRatio。

<symbol id="sym01" viewBox="0 0 150 110">
  <circle cx="50" cy="50" r="40" stroke-width="8"
      stroke="red" fill="red"/>
  <circle cx="90" cy="60" r="40" stroke-width="8"
      stroke="green" fill="white"/>
</symbol>

<use href="#sym01"
     x="0" y="0" width="100" height="50"/>

同样使用该模板,也是使用 use 标签来完成。

image

既然 use 可以重用 SVG 代码,那么 SVG 里面能不能重用已经画好的 png/jpg 的图片呢?

这时候,就需要用到 image 标签。其可以用来加载外部的 PNG, JPEG 图片,注意,官方规定是前两种,其它图片支持不支持官方没做答复。即,如果你使用 GIF 图片,并不能保证所有的浏览器都能正常显示。

<image xlink:href="kwanghwamun.jpg"
  x="72" y="92"
  width="160" height="120"/>
</svg>

同样,该 image 标签也具有自定义 preserveAspectRatio 的效果。

  • x: 定义水平位置

  • y: 定义垂直位置

  • width: 图片渲染的宽度,必须有。

  • height: 图片渲染的高度,必须有。

  • preserveAspectRatio: 控制图片的缩放

marker

marker 一般是用来画箭头或者线段始末的标识图形。

<defs>
    <marker id="Triangle" viewBox="0 0 10 10" refX="1" refY="5"
        markerWidth="6" markerHeight="6" orient="auto">
      <path d="M 0 0 L 10 5 L 0 10 z" />
    </marker>
  </defs>

  <polyline points="10,90 50,80 90,20" fill="none" stroke="black" 
      stroke-width="2" marker-end="url(#Triangle)" />

如图:

image.png-1.5kB

这里我们只需要里了解即可,因为在实际画的时候,直接使用相关工具生成更加方便。

a

这里的 a 标签和我们直接在 HTML 使用的超链接 a 标签类似。也是用来定义一个外链的。

<a xlink:href="https://developer.mozilla.org/en-US/docs/SVG"
      target="_blank">
    <rect height="30" width="120" y="0" x="0" rx="15"/>
    <text fill="white" text-anchor="middle" 
          y="21" x="60">SVG on MDN</text>
  </a>

个人公众号

更多内容,可以关注我的公众号:前端小吉米。

分享吉米的前端路,后面也会定期推出当前热门的前端技术~ 比如,直播,VR

image.png-43.2kB

查看原文

赞 37 收藏 52 评论 1

认证与成就

  • 获得 9 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-06-04
个人主页被 444 人浏览