Thinker

Thinker 查看完整档案

广州编辑华南师范大学  |  通信工程 编辑食议兽科技  |  前端工程师 编辑 shiyuanjieyi.cn 编辑
编辑

Make A Thinker!

个人动态

Thinker 赞了回答 · 10月22日

解决如何优雅的删除对象中的指定属性?

要优雅的话,使用 Lodash 的 omit 方法移除不要的属性:

const object = { 'a': 1, 'b': '2', 'c': 3 };
 
const result = _.omit(object, ['a', 'c']);
// => { 'b': '2' }

或者用 pick 方法只留下需要的属性:

const object = { 'a': 1, 'b': '2', 'c': 3 };
 
const result = _.pick(object, ['a', 'c']);
// => { 'a': 1, 'c': 3 }

当然如果你不想用库或者喜欢动手的话,自己实现一个 omit 也是可以的,实现方法有很多:

// 中规中矩式
const omit = (obj, uselessKeys) =>
   Object.keys(obj).reduce((acc, key) =>
      return uselessKeys.includes(key) ?
        acc : 
        {...acc, [key]: acc[key]}
   }, {});


// 投机取巧式
const omit = (obj, uselessKeys) =>
  uselessKeys.reduce((acc, key) => {
    return {...acc, [key]: undefined}
  }, obj)


// 粗暴式
const omit = (obj, uselessKeys) => {
  uselessKeys.forEach(key => {
    delete obj[key]
  })
  return obj
}

最后是特别粗暴的方法:

delete obj.created_at
delete obj.deleted_at
delete obj.updated_at

希望对你有帮助

关注 7 回答 6

Thinker 收藏了文章 · 9月7日

vue组件:canvas实现图片涂鸦功能

方案背景

需求

  1. 需要对图片进行标注,导出图片。
  2. 需要标注N多图片最后同时保存。
  3. 需要根据多边形区域数据(区域、颜色、名称)标注。

对应方案

  1. 用canvas实现涂鸦、圆形、矩形的绘制,最终生成图片base64编码用于上传
  2. 大量图片批量上传很耗时间,为了提高用户体验,改为只实现圆形、矩形绘制,最终保存成坐标,下次显示时根据坐标再绘制。
  3. 多边形区域的显示是根据坐标点绘制,名称显示的位置为多边形质心。

代码

<template>
  <div>
    <canvas
      :id="radom"
      :class="{canDraw: 'canvas'}"
      :width="width"
      :height="height"
      :style="{'width':`${width}px`,'height':`${height}px`}"
      @mousedown="canvasDown($event)"
      @mouseup="canvasUp($event)"
      @mousemove="canvasMove($event)"
      @touchstart="canvasDown($event)"
      @touchend="canvasUp($event)"
      @touchmove="canvasMove($event)">
    </canvas>
  </div>
</template>
<script>
  // import proxy from './proxy.js'
  const uuid = require('node-uuid')
  export default {
    props: {
      canDraw: { // 图片路径
        type: Boolean,
        default: true
      },
      url: { // 图片路径
        type: String
      },
      info: { // 位置点信息
        type: Array
      },
      width: { // 绘图区域宽度
        type: String
      },
      height: { // 绘图区域高度
        type: String
      },
      lineColor: { // 画笔颜色
        type: String,
        default: 'red'
      },
      lineWidth: { // 画笔宽度
        type: Number,
        default: 2
      },
      lineType: { // 画笔类型
        type: String,
        default: 'circle'
      }
    },
    watch: {
      info (val) {
        if (val) {
          this.initDraw()
        }
      }
    },
    data () {
      return {
        // 同一页面多次渲染时,用于区分元素的id
        radom: uuid.v4(),
        // canvas对象
        context: {},
        // 是否处于绘制状态
        canvasMoveUse: false,
        // 绘制矩形和椭圆时用来保存起始点信息
        beginRec: {
          x: '',
          y: '',
          imageData: ''
        },
        // 储存坐标信息
        drawInfo: [],
        // 背景图片缓存
        img: new Image()
      }
    },
    mounted () {
      this.initDraw()
    },
    methods: {
      // 初始化绘制信息
      initDraw () {
        // 初始化画布
        const canvas = document.getElementById(this.radom)
        this.context = canvas.getContext('2d')
        // 初始化背景图片
        this.img.setAttribute('crossOrigin', 'Anonymous')
        this.img.src = this.url
        this.img.onerror = () => {
          var timeStamp = +new Date()
          this.img.src = this.url + '?' + timeStamp
        }
        this.img.onload = () => {
          this.clean()
        }
        // proxy.getBase64({imgUrl: this.url}).then((res) => {
        //   if (res.code * 1 === 0) {
        //     this.img.src = 'data:image/jpeg;base64,'+res.data
        //     this.img.onload = () => {
        //       this.clean()
        //     }
        //   }
        // })
        // 初始化画笔
        this.context.lineWidth = this.lineWidth
        this.context.strokeStyle = this.lineColor
      },
      // 鼠标按下
      canvasDown (e) {
        if (this.canDraw) {
          this.canvasMoveUse = true
          // client是基于整个页面的坐标,offset是cavas距离pictureDetail顶部以及左边的距离
          const canvasX = e.clientX - e.target.parentNode.offsetLeft
          const canvasY = e.clientY - e.target.parentNode.offsetTop
          // 记录起始点和起始状态
          this.beginRec.x = canvasX
          this.beginRec.y = canvasY
          this.beginRec.imageData = this.context.getImageData(0, 0, this.width, this.height)
          // 存储本次绘制坐标信息
          this.drawInfo.push({
            x: canvasX / this.width,
            y: canvasY / this.height,
            type: this.lineType
          })
        }
      },
      Area (p0,p1,p2) {
        let area = 0.0 ;
        area = p0.x * p1.y + p1.x * p2.y + p2.x * p0.y - p1.x * p0.y - p2.x * p1.y - p0.x * p2.y;
        return area / 2 ;
      },
      // 计算多边形质心
      getPolygonAreaCenter (points) {
        let sum_x = 0;
        let sum_y = 0;
        let sum_area = 0;
        let p1 = points[1];
        for (var i = 2; i < points.length; i++) {
          let p2 = points[i];
          let area = this.Area(points[0],p1,p2) ;
          sum_area += area ;
          sum_x += (points[0].x + p1.x + p2.x) * area;
          sum_y += (points[0].y + p1.y + p2.y) * area;
          p1 = p2 ;
        }
        return {
          x: sum_x / sum_area / 3,
          y: sum_y / sum_area / 3
        }
      },
      // 根据坐标信息绘制图形
      drawWithInfo () {
        this.info.forEach(item => {
          this.context.beginPath()
          if (!item.type) {
            // 设置颜色
            this.context.strokeStyle = item.regionColor
            this.context.fillStyle = item.regionColor
            // 绘制多边形的边
            if (typeof item.region === 'string') {
              item.region = JSON.parse(item.region)
            }
            item.region.forEach(point => {
              this.context.lineTo(point.x * this.width, point.y * this.height)
            })
            this.context.closePath()
            // 在多边形质心标注文字
            let point = this.getPolygonAreaCenter(item.region)
            this.context.fillText(item.areaName, point.x * this.width, point.y * this.height)
          } else if (item.type === 'rec') {
            this.context.rect(item.x * this.width, item.y * this.height, item.w * this.width, item.h * this.height)
          } else if (item.type === 'circle') {
            this.drawEllipse(this.context, (item.x + item.a) * this.width, (item.y + item.b) * this.height, item.a > 0 ? item.a * this.width : -item.a * this.width, item.b > 0 ? item.b * this.height : -item.b * this.height)
          }
          this.context.stroke()
        })
      },
      // 鼠标移动时绘制
      canvasMove (e) {
        if (this.canvasMoveUse && this.canDraw) {
          // client是基于整个页面的坐标,offset是cavas距离pictureDetail顶部以及左边的距离
          let canvasX = e.clientX - e.target.parentNode.offsetLeft
          let canvasY = e.clientY - e.target.parentNode.offsetTop
          if (this.lineType === 'rec') { // 绘制矩形时恢复起始点状态再重新绘制
            this.context.putImageData(this.beginRec.imageData, 0, 0)
            this.context.beginPath()
            this.context.rect(this.beginRec.x, this.beginRec.y, canvasX - this.beginRec.x, canvasY - this.beginRec.y)
            let info = this.drawInfo[this.drawInfo.length - 1]
            info.w = canvasX / this.width - info.x
            info.h = canvasY / this.height - info.y
          } else if (this.lineType === 'circle') { // 绘制椭圆时恢复起始点状态再重新绘制
            this.context.putImageData(this.beginRec.imageData, 0, 0)
            this.context.beginPath()
            let a = (canvasX - this.beginRec.x) / 2
            let b = (canvasY - this.beginRec.y) / 2
            this.drawEllipse(this.context, this.beginRec.x + a, this.beginRec.y + b, a > 0 ? a : -a, b > 0 ? b : -b)
            let info = this.drawInfo[this.drawInfo.length - 1]
            info.a = a / this.width
            info.b = b / this.height
          }
          this.context.stroke()
        }
      },
      // 绘制椭圆
      drawEllipse (context, x, y, a, b) {
        context.save()
        var r = (a > b) ? a : b
        var ratioX = a / r
        var ratioY = b / r
        context.scale(ratioX, ratioY)
        context.beginPath()
        context.arc(x / ratioX, y / ratioY, r, 0, 2 * Math.PI, false)
        context.closePath()
        context.restore()
      },
      // 鼠标抬起
      canvasUp (e) {
        if (this.canDraw) {
          this.canvasMoveUse = false
        }
      },
      // 获取坐标信息
      getInfo () {
        return this.drawInfo
      },
      // 清空画布
      clean () {
        this.context.drawImage(this.img, 0, 0, this.width, this.height)
        this.drawInfo = []
        if (this.info && this.info.length !== 0) this.drawWithInfo()
      }
    }
  }
</script>
<style lang="scss" scoped>
  .canvas{
    cursor: crosshair;
  }
</style>

必须传入的参数

  • 图片路径
url: string
  • 绘图区域宽度
width: string
  • 绘图区域高度
height: string

选择传入的参数

  • 是否可以绘制,默认true
canDraw: boolean
  • 坐标点信息,不传入则不绘制
info: string
  • 是否可绘制,默认true
canDraw: boolean
  • 绘图颜色,默认red
lineColor: string
  • 绘图笔宽度,默认2
lineWidth: number
  • 绘图笔类型,rec、circle,默认rec
lineType: string

可以调用的方法

  • 清空画布
clean()
  • 返回坐标点信息
getInfo()

特殊说明

  • canvas对象不能获得坐标,是通过父元素坐标获取的,所以该组件的父元素以上的层级不能有太多的定位、嵌套,否则绘制坐标会偏移。
  • 域名不同的图片可能存在跨域问题,看过很多资料没有太好的办法,最后项目中是用node服务做了一个图片转为base64的接口,再给canvas绘制解决的。并不一定适用于其他项目,如果有更好的办法解决欢迎分享。
  • 导出坐标点数据只能导出规则图案的坐标点,因为随意涂鸦的坐标点太多时会崩溃的(虽然没试过具体到什么程度会崩溃),如果有高性能的实现方式欢迎分享。
  • 如果涂鸦后保存再请求图片url出现请求不到的情况,是因为CDN缓存的问题,在图片路径后面拼个随机码就可以解决。
查看原文

Thinker 收藏了文章 · 8月1日

vue项目如何监听窗口变化,达到页面自适应?

欢迎关注前端小讴的github,阅读更多原创技术文章

【自适应】向来是前端工程师需要解决的一大问题——即便作为当今非常火热的vue框架,也无法摆脱——虽然elementui、iview等开源UI组件库层出不穷,但官方库毕竟不可能满足全部需求,因此我们可以通过【监听窗口变化】达到想要的绝大部分自适应效果。

获取窗口宽度:document.body.clientWidth
监听窗口变化:window.onresize

同时回顾一下JS里这些方法:
网页可见区域宽:document.body.clientWidth
网页可见区域高:document.body.clientHeight
网页可见区域宽:document.body.offsetWidth (包括边线的宽)
网页可见区域高:document.body.offsetHeight (包括边线的宽)

我们将document.body.clientWidth赋值给data中自定义的变量:

data:{
    screenWidth: document.body.clientWidth
}

在页面mounted时,挂载window.onresize方法:

mounted () {
    const that = this
    window.onresize = () => {
        return (() => {
            window.screenWidth = document.body.clientWidth
            that.screenWidth = window.screenWidth
        })()
    }
}

监听screenWidth属性值的变化,打印并观察screenWidth发生变化的值:

watch:{
    screenWidth(val){
        // 为了避免频繁触发resize函数导致页面卡顿,使用定时器
        if(!this.timer){
            // 一旦监听到的screenWidth值改变,就将其重新赋给data里的screenWidth
            this.screenWidth = val
            this.timer = true
            let that = this
            setTimeout(function(){
                // 打印screenWidth变化的值
                console.log(that.screenWidth)
                that.timer = false
            },400)
        }
    }
}

好!既然可以监听到窗口screenWidth值的变化,就可以根据这个值设定不同的自适应方案了!

【举个例子:div或img图片高度随宽度自适应】

div或img的宽度自适应很简单——设定css里width属性为百分比即可——但是高度呢?父级元素的高度并不是固定的(通常都是子级元素撑开的)

图片描述

如上图,一个类似【图片库】的功能,当屏幕放大缩小时,我们可以设置外层边框(也就是灰色框框)的宽度为100%,以达到自适应——但高度无法设置,因此我们需要:
1.数据加载完后,获取图片(或外层框)的宽度
2.根据这个宽度,设置外层框的高度(如:宽度的60%)
3.监听窗口screenWidth值的变化,每次变化都重新获取宽度,并重新设置高度

所以,我们只需在前文代码的基础上,添加以下代码,以确保屏幕缩放时,每次监听宽度变化:

mounted() {
    // 1、数据首次加载完后 → 获取图片(或外层框)宽度,并设置其高度
    this.$nextTick(() => {
        // 获取图片(或外层框)
        let imgBox = this.$refs.imgBox
        // 获取其宽度
        let wImgbox = imgBox[0].getBoundingClientRect().width
        // 设置其高度(以宽度的60%为例)
        this.imgBox.height = 0.6 * wImgbox + 'px'
    })
    // 2、挂载 reisze 事件 → 屏幕缩放时监听宽度变化
    const that = this
    window.onresize = () => {
        return (() => {
            // window.screenWidth = document.body.clientWidth
            // that.screenWidth = window.screenWidth
            // console.log(that.screenWidth)
            this.$nextTick(() => {
                let imgBox = this.$refs.imgBox
                let wImgbox = imgBox[0].getBoundingClientRect().width
                this.imgBox.height = 0.6 * wImgbox + 'px'
            })
        })()
    }
},

最终实现效果如下:

图片描述

查看原文

Thinker 赞了文章 · 8月1日

vue项目如何监听窗口变化,达到页面自适应?

欢迎关注前端小讴的github,阅读更多原创技术文章

【自适应】向来是前端工程师需要解决的一大问题——即便作为当今非常火热的vue框架,也无法摆脱——虽然elementui、iview等开源UI组件库层出不穷,但官方库毕竟不可能满足全部需求,因此我们可以通过【监听窗口变化】达到想要的绝大部分自适应效果。

获取窗口宽度:document.body.clientWidth
监听窗口变化:window.onresize

同时回顾一下JS里这些方法:
网页可见区域宽:document.body.clientWidth
网页可见区域高:document.body.clientHeight
网页可见区域宽:document.body.offsetWidth (包括边线的宽)
网页可见区域高:document.body.offsetHeight (包括边线的宽)

我们将document.body.clientWidth赋值给data中自定义的变量:

data:{
    screenWidth: document.body.clientWidth
}

在页面mounted时,挂载window.onresize方法:

mounted () {
    const that = this
    window.onresize = () => {
        return (() => {
            window.screenWidth = document.body.clientWidth
            that.screenWidth = window.screenWidth
        })()
    }
}

监听screenWidth属性值的变化,打印并观察screenWidth发生变化的值:

watch:{
    screenWidth(val){
        // 为了避免频繁触发resize函数导致页面卡顿,使用定时器
        if(!this.timer){
            // 一旦监听到的screenWidth值改变,就将其重新赋给data里的screenWidth
            this.screenWidth = val
            this.timer = true
            let that = this
            setTimeout(function(){
                // 打印screenWidth变化的值
                console.log(that.screenWidth)
                that.timer = false
            },400)
        }
    }
}

好!既然可以监听到窗口screenWidth值的变化,就可以根据这个值设定不同的自适应方案了!

【举个例子:div或img图片高度随宽度自适应】

div或img的宽度自适应很简单——设定css里width属性为百分比即可——但是高度呢?父级元素的高度并不是固定的(通常都是子级元素撑开的)

图片描述

如上图,一个类似【图片库】的功能,当屏幕放大缩小时,我们可以设置外层边框(也就是灰色框框)的宽度为100%,以达到自适应——但高度无法设置,因此我们需要:
1.数据加载完后,获取图片(或外层框)的宽度
2.根据这个宽度,设置外层框的高度(如:宽度的60%)
3.监听窗口screenWidth值的变化,每次变化都重新获取宽度,并重新设置高度

所以,我们只需在前文代码的基础上,添加以下代码,以确保屏幕缩放时,每次监听宽度变化:

mounted() {
    // 1、数据首次加载完后 → 获取图片(或外层框)宽度,并设置其高度
    this.$nextTick(() => {
        // 获取图片(或外层框)
        let imgBox = this.$refs.imgBox
        // 获取其宽度
        let wImgbox = imgBox[0].getBoundingClientRect().width
        // 设置其高度(以宽度的60%为例)
        this.imgBox.height = 0.6 * wImgbox + 'px'
    })
    // 2、挂载 reisze 事件 → 屏幕缩放时监听宽度变化
    const that = this
    window.onresize = () => {
        return (() => {
            // window.screenWidth = document.body.clientWidth
            // that.screenWidth = window.screenWidth
            // console.log(that.screenWidth)
            this.$nextTick(() => {
                let imgBox = this.$refs.imgBox
                let wImgbox = imgBox[0].getBoundingClientRect().width
                this.imgBox.height = 0.6 * wImgbox + 'px'
            })
        })()
    }
},

最终实现效果如下:

图片描述

查看原文

赞 24 收藏 15 评论 10

Thinker 关注了问题 · 6月4日

解决git-bash执行npm命令提示'"node"' 不是内部或外部命令,也不是可运行的程序 或批处理文件。

环境变量已经配置过了,我使用cmd进行操作就可以,使用bash就会提示这个,npm install是可以正常运行的,错误日志提示如下
项目1:

0 info it worked if it ends with ok
1 verbose cli [
1 verbose cli   'D:\\nodejs\\node.exe',
1 verbose cli   'D:\\nodejs\\node_modules\\npm\\bin\\npm-cli.js',
1 verbose cli   'run',
1 verbose cli   'upall'
1 verbose cli ]
2 info using npm@6.13.4
3 info using node@v12.14.1
4 verbose run-script [ 'preupall', 'upall', 'postupall' ]
5 info lifecycle hexo-site@0.0.0~preupall: hexo-site@0.0.0
6 info lifecycle hexo-site@0.0.0~upall: hexo-site@0.0.0
7 verbose lifecycle hexo-site@0.0.0~upall: unsafe-perm in lifecycle true
8 verbose lifecycle hexo-site@0.0.0~upall: PATH: D:\nodejs\node_modules\npm\node_modules\npm-lifecycle\node-gyp-bin;F:\whiteS18.github.io\node_modules\.bin;D:\Git\mingw64\bin;D:\Git\usr\bin;C:\Users\SCW\bin;F:\whiteS18.github.io\node_modules\.bin;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0;C:\Windows\System32\OpenSSH;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\Intel\WiFi\bin;C:\Program Files\Common Files\Intel\WirelessCommon;D:\Java\jdk1.8.0_241\bin;D:\Java\jdk1.8.0_241\jre\bin;.;D:\apache-maven-3.6.2\bin";D:\nodejs;D:\Git\mingw64\libexec\git-core;D:\Git\usr\bin;C:\Windows\system3;.;C:\Windows;C:\Windows\System32\Wbem;C:\Users\SCW\AppData\Local\Microsoft\WindowsApps;C:\Program Files\Bandizip;D:\Microsoft VS Code\bin;D:\JetBrains\IntelliJ IDEA 2019.3.1\bin;C:\Program Files\Intel\WiFi\bin;C:\Program Files\Common Files\Intel\WirelessCommon;C:\Users\SCW\AppData\Local\BypassRuntm;C:\Users\SCW\AppData\Roaming\npm;D:\nodejs
9 verbose lifecycle hexo-site@0.0.0~upall: CWD: F:\whiteS18.github.io
10 silly lifecycle hexo-site@0.0.0~upall: Args: [
10 silly lifecycle   '/d /s /c',
10 silly lifecycle   'cross-env hexo cl && hexo g && gulp && hexo d && git add . && git commit -m " quick update" && git push origin source '
10 silly lifecycle ]
11 silly lifecycle hexo-site@0.0.0~upall: Returned: code: 9009  signal: null
12 info lifecycle hexo-site@0.0.0~upall: Failed to exec upall script
13 verbose stack Error: hexo-site@0.0.0 upall: `cross-env hexo cl && hexo g && gulp && hexo d && git add . && git commit -m " quick update" && git push origin source `
13 verbose stack Exit status 9009
13 verbose stack     at EventEmitter.<anonymous> (D:\nodejs\node_modules\npm\node_modules\npm-lifecycle\index.js:332:16)
13 verbose stack     at EventEmitter.emit (events.js:223:5)
13 verbose stack     at ChildProcess.<anonymous> (D:\nodejs\node_modules\npm\node_modules\npm-lifecycle\lib\spawn.js:55:14)
13 verbose stack     at ChildProcess.emit (events.js:223:5)
13 verbose stack     at maybeClose (internal/child_process.js:1021:16)
13 verbose stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:283:5)
14 verbose pkgid hexo-site@0.0.0
15 verbose cwd F:\whiteS18.github.io
16 verbose Windows_NT 10.0.18362
17 verbose argv "D:\\nodejs\\node.exe" "D:\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "run" "upall"
18 verbose node v12.14.1
19 verbose npm  v6.13.4
20 error code ELIFECYCLE
21 error errno 9009
22 error hexo-site@0.0.0 upall: `cross-env hexo cl && hexo g && gulp && hexo d && git add . && git commit -m " quick update" && git push origin source `
22 error Exit status 9009
23 error Failed at the hexo-site@0.0.0 upall script.
23 error This is probably not a problem with npm. There is likely additional logging output above.
24 verbose exit [ 9009, true ]

项目2:

0 info it worked if it ends with ok
1 verbose cli [
1 verbose cli   'D:\\nodejs\\node.exe',
1 verbose cli   'D:\\nodejs\\node_modules\\npm\\bin\\npm-cli.js',
1 verbose cli   'run',
1 verbose cli   'serve'
1 verbose cli ]
2 info using npm@6.13.4
3 info using node@v12.14.1
4 verbose run-script [ 'preserve', 'serve', 'postserve' ]
5 info lifecycle cwsong@0.1.0~preserve: cwsong@0.1.0
6 info lifecycle cwsong@0.1.0~serve: cwsong@0.1.0
7 verbose lifecycle cwsong@0.1.0~serve: unsafe-perm in lifecycle true
8 verbose lifecycle cwsong@0.1.0~serve: PATH: D:\nodejs\node_modules\npm\node_modules\npm-lifecycle\node-gyp-bin;F:\cwsong\node_modules\.bin;D:\Git\mingw64\bin;D:\Git\usr\bin;C:\Users\SCW\bin;F:\cwsong\node_modules\.bin;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0;C:\Windows\System32\OpenSSH;C:\Program Files (x86)\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files\Intel\Intel(R) Management Engine Components\DAL;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\Intel\WiFi\bin;C:\Program Files\Common Files\Intel\WirelessCommon;D:\Java\jdk1.8.0_241\bin;D:\Java\jdk1.8.0_241\jre\bin;.;D:\apache-maven-3.6.2\bin";D:\nodejs;D:\Git\mingw64\libexec\git-core;D:\Git\usr\bin;C:\Windows\system3;.;C:\Windows;C:\Windows\System32\Wbem;C:\Users\SCW\AppData\Local\Microsoft\WindowsApps;C:\Program Files\Bandizip;D:\Microsoft VS Code\bin;D:\JetBrains\IntelliJ IDEA 2019.3.1\bin;C:\Program Files\Intel\WiFi\bin;C:\Program Files\Common Files\Intel\WirelessCommon;C:\Users\SCW\AppData\Local\BypassRuntm;C:\Users\SCW\AppData\Roaming\npm;D:\nodejs
9 verbose lifecycle cwsong@0.1.0~serve: CWD: F:\cwsong
10 silly lifecycle cwsong@0.1.0~serve: Args: [ '/d /s /c', 'vue-cli-service serve' ]
11 silly lifecycle cwsong@0.1.0~serve: Returned: code: 9009  signal: null
12 info lifecycle cwsong@0.1.0~serve: Failed to exec serve script
13 verbose stack Error: cwsong@0.1.0 serve: `vue-cli-service serve`
13 verbose stack Exit status 9009
13 verbose stack     at EventEmitter.<anonymous> (D:\nodejs\node_modules\npm\node_modules\npm-lifecycle\index.js:332:16)
13 verbose stack     at EventEmitter.emit (events.js:223:5)
13 verbose stack     at ChildProcess.<anonymous> (D:\nodejs\node_modules\npm\node_modules\npm-lifecycle\lib\spawn.js:55:14)
13 verbose stack     at ChildProcess.emit (events.js:223:5)
13 verbose stack     at maybeClose (internal/child_process.js:1021:16)
13 verbose stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:283:5)
14 verbose pkgid cwsong@0.1.0
15 verbose cwd F:\cwsong
16 verbose Windows_NT 10.0.18362
17 verbose argv "D:\\nodejs\\node.exe" "D:\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "run" "serve"
18 verbose node v12.14.1
19 verbose npm  v6.13.4
20 error code ELIFECYCLE
21 error errno 9009
22 error cwsong@0.1.0 serve: `vue-cli-service serve`
22 error Exit status 9009
23 error Failed at the cwsong@0.1.0 serve script.
23 error This is probably not a problem with npm. There is likely additional logging output above.
24 verbose exit [ 9009, true ]

这两个命令我通过cmd去执行是可以正常执行的
我的node,npm版本如下

SCW@DESKTOP-73RCKME MINGW64 ~/Desktop
$ node -v
v12.14.1

SCW@DESKTOP-73RCKME MINGW64 ~/Desktop
$ npm -v
6.13.4

SCW@DESKTOP-73RCKME MINGW64 ~/Desktop
$

请问该怎么解决呢???

关注 5 回答 4

Thinker 收藏了文章 · 3月28日

页面可视化搭建工具前生今世

原文地址: https://github.com/CntChen/cn...

背景

引子

页面可视化搭建, 是一个历久弥新的话题. 更广义上讲, 页面是 GUI 的一部分, GUI 的拖拉生成在各种开发工具上很常见, 如 Android Studio, Xcode, Visual Studio 等. 前端页面早在十几年前就能用 Dreamweaver, Frontpage 等工具可视化搭建出来.

Dreamweaver 操作页面示例:

dreamweaver_demo

但是现在已经很少人使用 Dreamweaver 了, 其主要原因是页面承载的内容已经和页面源码分离, 由后端接口返回再渲染到页面, 静态页面网站无法承载大量的动态内容.

Dreamweaver 死了, 但是页面可视化搭建工具依然广泛需要和使用, 所以这个话题依然值得探讨.

文章内容

  • 页面构成和页面组件化.
  • 页面可视化搭建工具的必要性.
  • 页面可视化搭建工具的区分维度.
  • 业界的实践实例.

页面

页面是 HTML / DOM

页面可视化搭建的操作对象是页面. 页面是一份 HTML 文档, 不管是静态页面还是动态渲染出来的页面, 在页面上看到的内容, 都是 HTML 文档的一部分.

对 HTML 文档的实例化和操作, 通过文档对象模型(DOM)来实现, 也可以说页面是一个 DOM. 本文没有严格区分 HTML 和 DOM 这两个概念, 以下行文都用 HTML 这个概念.

HTML 使用一种树形结构来表示页面, 树的每个节点为一个页面元素或文本节点, 一个页面元素可以包含多个页面元素节点或文本节点. 页面元素通常称为标签, 页面元素类型由 HTML 规范定义.

HTML 结构示例:

pic_htmltree

https://www.w3schools.com/js/...

页面是 HTML Tree + Data

从前端开发的角度, 可以认为页面是由 HTML TreeData 组成, HTML Tree 是页面元素的树形结构, Data 是页面元素的属性或文本节点. 下图中蓝色框所示的节点可以认为是数据.

pic_htmltree_data

为什么从前端开发角度会说页面是 HTML Tree + Data? 举一个常见场景来说明: 在开发新页面时, 我们是可以复制已有页面(好吧, 我就是这样的前端工程师), 然后只修改页面 HTML, 或者只修改数据, 或同时修改 HTML 和数据, 从而完成新页面的开发.

静态页面和动态逻辑页面

上一节说页面的由 HTML TreeData 组成, 讨论的是静态页面.

what_is_static_page

浏览器请求静态页面, 网络返回的 HTML 源码就是页面渲染完成后的 HTML. 静态页面的源码和页面渲染结果一致:

static_page_render_result

当下, 前端页面更多的是有动态逻辑的页面, 在页面中引入和使用动态脚本(Javascript)对页面进行修改和控制.

what_is_dynamic_page

浏览器请求动态逻辑页面, 网络返回的 HTML 源码与页面渲染完成后的 HTML 有差异. 动态逻辑页面的源码和渲染结果有差异:

dynamic_logic_page_render_result

页面组件化

页面渲染后是一棵 HTML 元素构成的树, 页面的可编辑粒度为 HTML 规范定义的 HTML 元素.
使用 Web Components 组合 HTML 元素, 实现了功能封装和可复用的页面组件. 在流行的前端框架中, 都提供了组件化的功能, 从前端框架的视角看, 页面是由组件树组成. 这些组件内部维护自身的 HTML 元素结构、样式和功能逻辑, 并通过组件的 props 获取外部传入的数据, 实现了功能封装和复用.

Vue 组件树示例:

vue_components

https://vuejs.org/v2/guide/#C...

并没有讨论 CSS

在以上的章节中, 我们并没有讨论决定页面样式的 CSS. 因为借助 Javascript 的动态逻辑, CSS 可以归入到 Data 的范围: 通过对页面元素 style attribute 的修改, 或将 CSS 属性动态添加到 <style> 标签中, 可以实现对页面元素样式的修改.

页面可视化搭建

有了对页面组成的认知基础,可以对页面可视化搭建有更多的讨论: 页面可视化搭建是什么? 为什么需要?

是什么

如前文所阐述, 动态逻辑页面分解为 HTML Tree, DataDynamic Logic. 前端开发工程师开发前端页面的过程, 本质上是用编程工具(IDE)对页面的 HTML Tree, DataDynamic Logic 进行增删和修改.

页面可视化搭建, 是用可视化交互的方式对页面的 HTML Tree, DataDynamic Logic 进行增删和修改, 从而实现页面的生成. 页面可视化搭建工具是实现页面可视化编辑的软件工具.

用页面可视化搭建工具来搭建页面与前端工程师在页面上搬砖, 都是搭建页面, 区别在于实现页面搭建的方式. 做个简单对比:

差异点编程开发页面可视化搭建页面
技能要求需要编程基础可以没有编程基础
操作方式在代码编辑器中编写代码在可视化搭建工具中拖拉/填表/编写代码

为什么需要

任何工具的存在都是更高效地解决问题. 页面可视化搭建工具, 用于解决页面生成的效率问题.
可能前端工程师会觉得最有效率的页面生成方式是打代码, 但有搭建页面需求的不只是前端工程师. 而可视化页面搭建工具, 恰恰是面向"就缺一个前端工程师"的人员, 用于提升他们生成页面的效率.
我们可以从一些使用场景来窥探页面可视化搭建工具的应用场合.

页面小白做 H5

页面小白不需要任何页面相关的知识, 不需要了解 HTML/JS/CSS 这些概念, 只要像使用 Word 一样在 H5 制作工具上操作, 就可以做出一个挺漂亮的页面. H5 制作工具很多, 其中 百度H5 做很好不错.

如: 小陈女票要生日了, 小陈为女票做了一个有创意的生日祝福页面:

baidu_h5

营销活动页面搭建

大多数互联网公司需要做许多的活动页面来承载运营业务. 运营活动页面的特点是: 页面功能大同小异、需求急、时间紧、下线快、研发性很比低. 前端工程师无法持续开发无穷无尽的活动页面, 需要采用活动页面可视化搭建工具, 由运营人员/产品人员直接生成活动页面. 研发人员的工作转变为提供满足活动页面业务需要的活动模板.

如: 抽奖活动页面的可视化搭建:

activity_demo

中后台系统开发

在公司内部, 需要做许多的中后台支持系统, 这些系统的管理端一般用 web 页面承载. 那么问题来了, 中后台系统的前端工程, 怎么保障可用性、可维护性和页面呈现一致性? 这些系统与后台逻辑强关联, 一般由后台开发人员开发; 后台开发人员写代码逻辑是没有问题的, 但是其前端开发能力相对较弱. 所以需要增强他们开发前端页面的能力, 前端开发能力由前端服务化提供.

前端服务化的第一种方式是提供一套组件库, 如 饿了么的 Element.
组件库一般由前端开发人员封装成模板工程, 模板工程提供公共样式和函数库, 并对编写的代码做校验和约束, 一定程度上降低了前端开发难度, 统一后台人员代码风格. 此时后台开发人员的开发方式为: 在代码中用组件拼凑页面, 然后写代码逻辑.

前端服务化的第二种方式, 是提供页面可视化组装系统, 这个系统输出组装后的前端工程源码. 这样的系统比提供组件库和模板工程的方式走得更远: 通过可视化生成模板工程, 后台开发人员不需要在代码中拼凑前端页面, 不需要关注前端组件, 只需要编写代码逻辑.
这种方式可以参考阿里的 ice.

阿里 ice 示例:

iceworks_demo

前端服务化的终极方式, 是直接提供一个开发的 IDE, 将动态逻辑的书写也在 IDE 中完成.
美团外卖前端可视化界面组装平台 —— 乐高, 前端服务化——页面搭建工具的死与生.

美团乐高示例:

lego_demo

前端服务化

更加广泛来说, 为页面小白/运营人员/产品人员提供的页面可视化生成工具, 也是赋予以上人员前端开发的能力. 所以页面可视化搭建, 本质上是前端服务化的一部分. 前端服务化总结, 可以看百度的 前端即服务-通向零成本开发之路.

页面可视化搭建工具区分维度

有了前文对页面的基础认知, 终于进入了本文的正题 -- 页面可视化搭建工具.
前面已经零星讨论过页面可视化搭建工具的定义, 再总结一下: 页面可视化搭建, 是指用可视化交互的方式(对比编写代码的方式), 实现页面的修改或生成; 页面可视化搭建工具, 增强了使用者的前端开发能力, 提升了使用者修改或生成页面的效率.

思考一个更具体的问题: 当我们讨论页面可视化搭建工具时, 怎么进行描述和讨论? 换个角度提问题: 可以从什么维度对页面可视化搭建工具进行描述和区分?

页面可视化搭建工具的区分维度包括:

  • 系统功能
  • 面向客群
  • 编辑自由度

下文会对页面可视化搭建工具的区分维度做介绍, 并会对每个区分维度提供示例(这些示例不会展开讨论, 且在不同维度下会多次使用同个示例).

系统功能

页面可视化搭建工具的系统功能是指该工具在解决特定页面可视化搭建问题上提供的核心能力.
页面是由HTML Tree, DataDynamic Logic 三部分组成, 一个页面可视化搭建工具提供的能力是编辑页面组成部分之一或多部分. 对基于组件的页面, 其可编辑单元为组件, 此时采用 Component Tree 概念取代 HTML Tree.

system_function_category

HTML Tree 编辑

这类页面搭建工具专注于可视化地编辑页面 HTML Tree 部分, 一般可以对页面做自由度较高的编辑.
其关键功能在于高自由度: 几乎可以编辑页面可见的所有元素, 能自由修改页面结构、页面元素样式和页面数据, 采用类似 Word, Photoshop 的可视化编辑方式.
这类工具一般只适用于生成逻辑比较简单的页面, 其中原因后续会讲.
常说的 H5 制作工具就是指这类工具.

如: 百度H5iH5

Component Tree 编辑

这类页面搭建工具针对组件化的页面, 主要实现 Component Tree 的可视化编辑. 其核心功能在于页面布局设计: 在 UI 组件列表中选择合适的组件, 通过拖拉的方式将组件嵌入到页面中, 生成带布局和样式的页面.

如: ice 阿里飞冰、vue-layout

vue-layout 示例:

vue_layout_demo

https://jaweii.github.io/Vue-...

页面 Data 编辑

这类页面搭建工具专注于可视化地编辑页面的 Data 部分, 如图片URL、按钮文本、按钮跳转链接等.
这类搭建工具主要针对 HTML Tree 比较固定、能承载复杂业务逻辑的页面. HTML Tree 固定的常见方式是页面组件化, 只需修改页面组件的 Data 就能快速地生成页面.
其核心功能在于快速搭建承载业务逻辑的页面.
通常营销活动页面就采用这种方式来可视化搭建.

如: 阿里云凤蝶、开源的 pipeline

阿里云凤蝶示例:

yunfengdie_demo

Dynamic Logic 编辑

这类页面搭建工具支持在界面上输入逻辑代码, 实现页面 Dynamic Logic 编辑, 如后台接口请求逻辑, 业务判断逻辑等.
这些逻辑代码需要有合适的插入点, 一般在事件钩子中提供插入点, 如页面 onload、网络请求状态变更、按钮事件、数据变更等.
做到可以支持编辑 Dynamic Logic 是超牛逼的事情, 这类工具对页面的理解最深入, 对开发者的技术能力、前端架构能力和开发能力都要求很高.

如: 前端服务化——页面搭建工具的死与生

系统功能组合

还有其他系统功能的组合, 可以综合上面的典型类别来做讨论.

面向客群

页面可视化搭建工具的面向客群是指工具的使用客群. 不同的使用客群, 其对页面技术的认知程度、搭建页面的诉求有所不同, 所以可以从工具的面向客群来区分不同工具.

custom_category

前端小白

前端小白是不具有前端知识的人群, 他们对页面可视化搭建工具的诉求是交互性越高越好. 最适合他们的工具是像 Word, Powerpoint, Photoshop 等具有丰富交互功能, 且所见即所得的页面搭建工具.
同时他们也不关心页面最后用什么方式托管到互联网上, 页面编辑完成后要帮他们在公网上托管页面, 并提供页面链接, 方便前端小白将页面发给自己的女朋友.

如页面界的 Photoshop:

ih5_demo

https://www.ih5.cn

运营/产品

运营、产品人员没有开发人员页面开发、逻辑编程的能力, 他们的诉求是可以快速搭建活动、产品页面. 活动、产品页面是承载着业务逻辑的: 如包含领取优惠券功能、背景音乐播放功能、产品购买功能等. 运营、产品对页面可视化搭建的另一个诉求是“快速”: 一天好几个活动, 怎么快怎么来.
面向运营、产品的可视化搭建工具, 需要将页面的逻辑功能封装在页面区块内, 支持通过点击来选择区块, 然后在表单中编辑区块所需数据, 只对页面进行少量编辑就完成业务页面搭建.
如领取优惠券的页面, 运营、产品只要在表单中填入优惠券的 ID, 然后就快速生成领取该优惠券的页面, 不需要关心优惠券在页面上如何展示和被领取的具体逻辑.

如, 开源项目 pipeline:

demo

中后台开发人员

中后台开发人员具有逻辑编程能力, 但其前端开发能力比较弱. 中后台开发人员的诉求是, 在开发中后台系统的 Web 管理端时, 不需要进行重度的前端页面结构和样式开发, 可以专注在逻辑和数据处理上.
这要求页面可视化搭建工具提供页面搭建的区块, 对区块进行可视化组合来输出一个基本的前端页面; 并在页面搭建工具上提供业务逻辑编写的输入点, 或将基本前端页面源码导出到 IDE 中供中后台开发人员进行业务逻辑的开发.

如: ice 阿里飞冰

前端工程师

要啥页面可视化搭建工具, 抓起键盘就开始干.

just_do_it

编辑自由度

页面可视化搭建工具的编辑自由度, 是指页面可编辑单元的粒度. 前端页面的可编辑单元为 HTML 元素; 从前端页面组件化的角度, 页面可编辑单元为组件.
不同的编辑自由度的选择, 是可视化搭建工具在不同业务场景下编辑自由度与编辑效率的平衡.

degree_of_freedom_category

编辑自由度为 HTML 元素(左)与自由度为组件(右)的示例:

edit_free_degree

编辑自由度为 HTML 元素

编辑自由度为 HTML 元素的页面搭建工具有以下特点: 可编辑的元素丰富、页面结构灵活、可视化编辑效率较低、业务逻辑封装度较低.
这类工具的可编辑单元为 HTML 元素, 可以编辑元素的文本、样式和行为, 可编辑的元素较丰富; 并且可以组合各种 HTML 元素到页面中, 生成的页面结构灵活; 从生成页面的角度, 编辑出一个页面需要从基本的 HTML 元素开始搭建, 可视化编辑的工作量较大; 一个业务功能的实现, 通常需要渲染多个 HTML 元素, 而这类工具可以自由增删业务所需的 HTML 元素, 这导致无法固定地承载业务功能, 所以这类编辑工具生成的页面, 业务逻辑封装程度较低.

如: iH5vvveb

vvveb 示例:
vvveb_demo

http://www.vvveb.com/vvvebjs/...

编辑自由度为前端框架组件

编辑自由度为前端框架组件的页面搭建工具有以下特点: 可编辑的元素依赖搭建工具提供的组件, 可视化编辑效率较高、业务逻辑封装度较高.
这类工具的最小可编辑单元为前端框架的组件, 这些组件需要开发并导入到页面可视化搭建工具中; 组件的渲染结果包含了多个 HTML 元素, 所以从生成页面的角度, 编辑出一个页面只需要组合组件, 可以较快速完成页面的生成; 组件本身承载了特定的业务功能, 所以这类编辑器生成的页面, 业务逻辑封装程度较高.

如: Vue-Layout

vue-layout 示例:

vue_layout_demo

不嵌套的前端框架组件

移动端的页面, 常用的布局策略是: 宽度铺满, 高度滚动. 如果前端框架组件都设置为铺满宽度, 页面展示时组件只需在浏览器垂直方向上顺序排列, 则组件组合时候不需要嵌套, 所有组件互为兄弟节点. 这种铺满宽度的组件, 非常适合搭建移动端页面的场景: 在承载页面逻辑的同时, 使得页面的编辑更加简单, 使用者只需要处理组件的顺序, 不需要处理组件的嵌套. 组件的嵌套需要重点解决组件数据流和组件布局适配.

如: 阿里云凤蝶pipeline

pipeline 示例:
pipeline_demo

https://page-pipepline.github...

理想的页面可视化搭建框架

页面可视化搭建工具, 需要对页面做一些定义和约定, 在可视化搭建时遵循工具定义和约定来编辑页面. 更全面讨论页面可视化搭建工具时, 不只是关注工具本身的功能, 还需要关注工具的依赖和约束, 如页面可视化搭建工具的组件化方式、模板组织方式、编辑功能实现方式等. 从工具开发的角度说, 页面可视化搭建工具是需要架构设计的, 不同工具的区分, 其实是不同的页面可视化搭建框架间的差异.

在互联网公司中, 广泛运用页面可视化搭建工具来支持运营活动页面的生成, 本章我们只探讨运营页面搭建工具的理想框架.

页面可视化搭建框架的核心是实现页面的可视化编辑. 运营页面搭建工具声明配置数据并提供配置表单, 通过对配置表单的数据填充, 实现基于模板的页面生成. 如图所示:

visual_edit

可视化编辑

配置数据

对页面的可编辑部分, 需要准确描述可编辑部分所需的配置数据; 配置数据是异构的, 不同页面、不同区块的配置数据各不相同. 所以需要对不同页面、不同区块定义各自配置数据的数据结构和字段类型.
最理想的配置数据格式为 JSON, 因为其格式灵活, 前端友好; 最理想的配置数据描述是 JSON Schema, 因为其支持表单动态生成和数据校验.

编辑表单生成

采用 JSON Schema, 容易生成配置表单, 只要按照 JSON Schema 对 JSON 数据的描述, 可以动态渲染出配置表单. 并且可以采用 JSON Schema 对编辑后的数据做格式校验, 避免编辑错误.

如配置表单自动生成工具 json-editor:

json_editor_demo

组件化

组件是对 HTML 元素、元素布局和样式、业务逻辑的封装, 通过组件化的方式, 将页面的搭建转化为对组件的组合, 大大减低了运营页面生成的编辑工作量, 实现承载业务逻辑的运营页面的快速搭建.

如 pipeline 的页面组件化:

component_demo

模板

模板是带有默认数据的页面; 对于组件化的页面, 模板是从组件库中选取部分组件, 并带有各个组件的默认数据.
采用模板生成页面, 只需对模板进行少量编辑即可实现页面快速生成.

与编辑系统解偶

编辑系统和组件解偶,组件只需要遵循编辑系统的组织约定, 其具体开发过程和承载的逻辑与编辑系统无关, 支持自由拓展页面组件.
编辑系统与模板采用的前端框架解偶, 在遵循编辑系统约定下, 可以选择不同的前端框架.

理想的运营页面可视化搭建框架

  • 采用 JSON Schema 声明配置数据, 配置表单自动生成.
  • 采用组件化和页面模板实现页面生成效率的提升.
  • 编辑系统与组件、前端框架、模板解耦.
  • 在遵循编辑系统约定下, 前端框架可以自由选择, 组件可以自由拓展.

页面可视化搭建工具举例

列举一些页面可视化搭工具, 并附带少量点评.

阿里云凤蝶

移动建站平台
  • 支持页面 Data 编辑, 面向运营/产品人员, 编辑自由度为无嵌套的组件.
  • 目前制作运营、活动页面功能上最好的工具.
  • 提供页面搭建的模板, 并支持自定义模板.
  • 配置表单基于 Schema 生成, 配置表单操作功能完善.

ice 阿里飞冰

飞冰 - 让前端开发简单而友好
  • 支持 Component Tree 编辑, 面向中后台开发人员, 编辑自由度为无嵌套的组件.
  • 使用"物料-区块", 非前端开发人员可以快速搭建出可用、符合规范的页面.
  • 页面以源码方式输出.
  • 前端服务化的一种方式.

百度H5

创意,绝不雷同
  • 支持 HTML Tree 编辑, 面向前端小白, 编辑自由度为 HTML 元素.
  • 做 H5 的好工具, 功能上很强大, 对动画的编辑功能做到细致.

美团外卖前端可视化界面组装平台 —— 乐高

  • 支持 Dynamic Logic 编辑, 面向中后台开发人员, 编辑自由度为可嵌套的组件.
  • 前端服务化的一种方式.
  • 在美团内部支持了许多业务页面, 没有公网服务, 了解该系统只能通过其介绍文章.

esview

Drag vue dynamic components to build your page,generate vue code.

开源项目, 模仿美团点评的乐高.

  • 完整的可视化页面搭建框架, 面向中后台开发人员.
  • 页面布局结果看起来比较乱, 自定义组件写法比较诡异; 没有融合业务逻辑, 不支持在框架中写页面的代码逻辑.

gaea-editor

Design websites in your browser

开源项目.

  • 支持 Component Tree 编辑, 面向中后台开发人员, 编辑自由度为可嵌套的组件.
  • 页面的拖拉生成, 实现得很完整.
  • 用于页面设计, 所以偏向页面元素的样式控制.
  • 技术文章对可视化搭建工具的理解深刻: 可视化在线编辑器架构设计.

Vue-Layout

基于UI组件的Vue可视化布局、生成.vue代码的工具。

开源项目.

  • 支持 Component Tree 编辑, 面向中后台开发人员, 编辑自由度为可嵌套的组件.
  • 工具的使用体验效果不错.

gen

根据接口生成页面,减少重复性工作
  • 开源项目, 用起来感觉不错.
  • 系统中有好几个概念, 开始比较难上手.

其他

业界实践

列举一些业界在页面可视化搭工具上的实践, 并附带少量点评.

前端服务化——页面搭建工具的死与生

  • 支持 Dynamic Logic 的页面可视化搭建 IDE.
  • 讲解了页面可视化搭建框架支持 Dynamic Logic 的可行性和设计架构.
  • 作者在前端框架和 IDE 方面写了好几篇文章, 很深刻.

腾讯IMWeb: 积木系统,将运营系统做到极致

2015年的文章! 完全说到点上.

  • 简单易用的、可视化的可编辑页面.
  • 通用的、简便地组件接入机制.
  • 组件:开发过程和系统无关, 逻辑和系统无关.

美团外卖前端可视化界面组装平台 —— 乐高

  • 把系统架构将得很清楚, 有借鉴意义.
  • 对页面组成做了分析, 阐述了可视化配置的原理.

前端即服务-通向零成本开发之路

百度的前端服务化实践, 都在这一篇.

可视化在线编辑器架构设计

  • 可视化在线编辑器属于前端开发引擎, 前端进入了前端工业时代.
  • 深入讨论了组件数据流.

百度外卖如何做到前端开发配置化

  • PPT 将原理和架构讲得很清楚.
  • 使用流程图很清晰.
  • 项目开源了 -- block, 试用起来功能比较简陋.

转转运营活动高效开发有哪些秘诀

基于组件的页面生成系统-魔方, 采用 npm 管理组件.

QQ会员: 如何保证H5页面高质量低成本快速生成

内部 ET 平台, 包含活动管理的其他功能.

esview -- 这可能是目前最好的vue代码生成工具

总结

  • 页面由 HTML Tree, Data, Dynamic Login 组成.
  • 页面可视化搭建工具用于提升各类人员的页面搭建效率.
  • 页面可视化搭建其实是前端服务化的方式.
  • 页面可视化搭建工具需要平衡自由度和效率.
  • 组件和模板是页面可视化搭建框架的核心.

全文结束, 本文对页面可视化搭建思考和讨论可能还不够完整, 欢迎讨论和补充.

后记: 终于写完了, 历时估计一个月! 写这篇文章的初衷是给我造的页面可视化搭建框架 -- pipeline 写背景, 但思考的点比较多, 所以就独立写了一篇文章. Pipeline 基本对标阿里的云凤蝶, 已经开源, 相关文章还在撰写中. 赶紧点击 Demo 体验吧.

EOF

查看原文

Thinker 收藏了文章 · 2月25日

15 行代码实现并发控制(javascript)

前言

首发于 github blog

做过爬虫的都知道,要控制爬虫的请求并发量,其实也就是控制其爬取频率,以免被封IP,还有的就是以此来控制爬虫应用运行内存,否则一下子处理N个请求,内存分分钟会爆。

python爬虫一般用多线程来控制并发,

然而如果是node.js爬虫,由于其单线程无阻塞性质以及事件循环机制,一般不用多线程来控制并发(当然node.js也可以实现多线程,此处非重点不再多讲),而是更加简便地直接在代码层级上实现并发。

为图方便,开发者在开发node爬虫一般会找一个并发控制的npm包,然而第三方的模块有时候也并不能完全满足我们的特殊需求,这时候我们可能就需要一个自己定制版的并发控制函数。

下面我们用15行代码实现一个并发控制的函数。

具体实现

参数

首先,一个基本的并发控制函数,基本要有以下3个参数:

  • list {Array} - 要迭代的数组
  • limit {number} - 控制的并发数量
  • asyncHandle {function} - 对list的每一个项的处理函数

设计

以下以爬虫为实例进行讲解

设计思路其实很简单,假如并发量控制是 5

  1. 首先,瞬发 5 个异步请求,我们就得到了并发的 5 个异步请求

    // limit = 5
    while(limit--) {
        handleFunction(list)
    }
  2. 然后,这 5 个异步请求中无论哪一个先执行完,都会继续执行下一个list

    let recursion = (arr) => {
        return asyncHandle(arr.shift())
            .then(()=>{
                // 迭代数组长度不为0, 递归执行自身
                if (arr.length!==0) return recursion(arr) 
                // 迭代数组长度为0,结束 
                else return 'finish';
            })
    }
  3. list所有的项迭代完之后的回调

    return Promise.all(allHandle)

代码

上述步骤组合起来,就是

/**
 * @params list {Array} - 要迭代的数组
 * @params limit {Number} - 并发数量控制数
 * @params asyncHandle {Function} - 对`list`的每一个项的处理函数,参数为当前处理项,必须 return 一个Promise来确定是否继续进行迭代
 * @return {Promise} - 返回一个 Promise 值来确认所有数据是否迭代完成
 */
let mapLimit = (list, limit, asyncHandle) => {
    let recursion = (arr) => {
        return asyncHandle(arr.shift())
            .then(()=>{
                if (arr.length!==0) return recursion(arr)   // 数组还未迭代完,递归继续进行迭代
                else return 'finish';
            })
    };
    
    let listCopy = [].concat(list);
    let asyncList = []; // 正在进行的所有并发异步操作
    while(limit--) {
        asyncList.push( recursion(listCopy) ); 
    }
    return Promise.all(asyncList);  // 所有并发异步操作都完成后,本次并发控制迭代完成
}

测试demo

模拟一下异步的并发情况

var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];
var count = 0;
mapLimit(dataLists, 3, (curItem)=>{
    return new Promise(resolve => {
        count++
        setTimeout(()=>{
            console.log(curItem, '当前并发量:', count--)
            resolve();
        }, Math.random() * 5000)  
    });
}).then(response => {
    console.log('finish', response)
})

结果如下:

clipboard.png




手动抛出异常中断并发函数测试:

var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];
var count = 0;
mapLimit(dataLists, 3, (curItem)=>{
    return new Promise((resolve, reject) => {
        count++
        setTimeout(()=>{
            console.log(curItem, '当前并发量:', count--)
            if(curItem > 4) reject('error happen')
            resolve();
        }, Math.random() * 5000)  
    });
}).then(response => {
    console.log('finish', response)
})

并发控制情况下,迭代到5,6,7 手动抛出异常,停止后续迭代:
clipboard.png

查看原文

Thinker 赞了文章 · 2019-09-19

【Day 1】云开发0基础训练营

云开发0基础训练营首日课程发布!

图片描述

今日份课程内容主题:

小程序开发入门

目标:了解小程序开发者工具、文件结构、json语法、全局配置、页面配置

WXML与WXSS

目标:了解小程序文本、颜色、盒子模型

链接与图片

目标:学会在小程序里添加链接和图片


完整课程链接:

关注【腾讯云云开发】公众号,回复【第一日课程】获取完整课程链接!


打卡送书活动规则:

(1)掌握课程内容。(后续会要求大家提交根据课程动手做出的小程序进行验证,所以要认真完成每日课程的学习哦)

(2)打卡分享。完成上述学习任务后在下方评论区打卡(考虑到学员基础不同,每日课程在五日内打卡均有效),并将活动海报分享至朋友圈。

坚持学习完成验证+打卡分享达到课程发布天数,即可获得云开发技术图书~
图片描述


报名方式:

图片描述

图片描述

查看原文

赞 6 收藏 2 评论 81

Thinker 关注了专栏 · 2019-09-19

小程序云开发技术专栏

云开发CloudBase官方技术专栏!微信 x 腾讯云联合提供的小程序·云开发服务,快速构建小程序、Web和移动应用。

关注 286

Thinker 收藏了文章 · 2019-07-09

JavaScript禁止页面回退的方法

1.可以消除 后退的所有动作。包括 键盘、鼠标手势等产生的后退动作。

<script language="javascript">
    //防止页面后退
    history.pushState(null, null, document.URL);
    window.addEventListener('popstate', function () {
            history.pushState(null, null, document.URL);
    });
</script>

2.系统登录退出后,跳转到登录页面,登录页面点击浏览器后退按钮,页面后退到之前登录的页面。在退出登录后,禁止浏览器点击后退按钮,进行页面回退;

window.location.hash="no-back";
window.location.hash="Again-No-back-button";
window.onhashchange=function(){window.location.hash="no-back";}

3.系统中嵌入iframe,映入其他页面。当session失效时,点击导航后,由于后台做了登录拦截,此时iframe中就会嵌入登录页面;当session失效时,希望系统可以跳转到登录页面中;

/**
 * [loadTopWindow 判断是否有顶层窗口,登录超时跳转]
 * @return {[type]} [description]
 */
 function loadTopWindow() {
     if (window.top != null && window.top.document.URL != document.URL){
         window.top.location = document.URL; 
     }
 }

<body onload="loadTopWindow()"></body>

整理了一些项目实战的学习视频,好友都会在里面交流,分享一些学习的方法和需要注意的小细节,每天也会准时的讲一些前端的炫酷特效,及前端直播课程学习。【 731771211 】

查看原文

Thinker 收藏了文章 · 2019-06-18

Nuxt.js 基础入门教程

原文链接

Vue 开发一个单页面应用,相信很多前端工程师都已经学会了,但是单页面应用有一个致命的缺点,就是 SEO 极不友好。除非,vue 能在服务端渲染(ssr)并直接返回已经渲染好的页面,而并非只是一个单纯的 <div id="app"></div>

Nuxt.js 就是一个极简的 vue 版的 ssr 框架。基于它,我们可以快速开发一个基于 vue 的 ssr 单页面应用。

安装

Nuxt.js 官方提供了一个模板,可以使用 vue-cli 直接安装。

$ vue init nuxt-community/starter-template <project-name>

目录结构

.
├── README.md
├── assets
├── components
├── layouts
├── middleware
├── node_modules
├── nuxt.config.js
├── package.json
├── pages
├── plugins
├── static
├── store
└── yarn.lock

其中:

  1. assets: 资源文件。放置需要经过 webpack 打包处理的资源文件,如 scss,图片,字体等。
  2. components: 组件。这里存放在页面中,可以复用的组件。
  3. layouts: 布局。页面都需要有一个布局,默认为 default。它规定了一个页面如何布局页面。所有页面都会加载在布局页面中的 <nuxt /> 标签中。如果需要在普通页面中使用下级路由,则需要在页面中添加 <nuxt-child />该目录名为Nuxt.js保留的,不可更改。
  4. middleware: 中间件。存放中间件。可以在页面中调用: middleware: 'middlewareName'
  5. pages: 页面。一个 vue 文件即为一个页面。index.vue 为根页面。

    1. 若需要二级页面,则添加文件夹即可。
    2. 如果页面的名称类似于 _id.vue (以 _ 开头),则为动态路由页面,_ 后为匹配的变量(params)。
    3. 若变量是必须的,则在文件夹下建立空文件 index.vue。更多的配置请移步至 官网
  6. plugin: 插件。用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。需要注意的是,在任何 Vue 组件的生命周期内, 只有 beforeCreatecreated 这两个钩子方法会在 客户端和服务端均被调用。其他钩子方法仅在客户端被调用。
  7. static: 静态文件。放置不需要经过 webpack 打包的静态资源。如一些 js, css 库。
  8. store: 状态管理。具体使用请移步至 官网
  9. nuxt.config.js: nuxt.config.js 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。具体配置请移步至 官网

Nuxt 特有函数

首先,了解一下在 nuxt 的页面中独有的函数/变量:

asyncData(context)

asyncData方法使得你能够在渲染组件之前异步获取数据。该方法在服务端中执行的,所以,请求数据时,不存在跨域问题。返回的数据将与 data() 返回的数据进行合并。由于asyncData方法是在组件 初始化 前被调用的,所以在方法内是没有办法通过 this 来引用组件的实例对象。

context 变量的可用属性一览:

属性字段类型可用描述
isClientBoolean客户端 & 服务端是否来自客户端渲染
isServerBoolean客户端 & 服务端是否来自服务端渲染
isDevBoolean客户端 & 服务端是否是开发(dev) 模式,在生产环境的数据缓存中用到
routevue-router 路由客户端 & 服务端vue-router 路由实例。
storevuex 数据流客户端 & 服务端Vuex.Store 实例。只有vuex 数据流存在相关配置时可用。
envObject客户端 & 服务端nuxt.config.js 中配置的环境变量, 见 环境变量 api
paramsObject客户端 & 服务端route.params 的别名
queryObject客户端 & 服务端route.query 的别名
reqhttp.Request服务端Node.js API 的 Request 对象。如果 nuxt 以中间件形式使用的话,这个对象就根据你所使用的框架而定。nuxt generate 不可用
reshttp.Response服务端Node.js API 的 Response 对象。如果 nuxt 以中间件形式使用的话,这个对象就根据你所使用的框架而定。nuxt generate 不可用
redirectFunction客户端 & 服务端用这个方法重定向用户请求到另一个路由。状态码在服务端被使用,默认 302。redirect([status,] path [, query])
errorFunction客户端 & 服务端用这个方法展示错误页:error(params)params 参数应该包含 statusCodemessage 字段。

fetch(context)

fetch 方法用于在渲染页面前填充应用的状态树(store)数据, 与 asyncData 方法类似,不同的是它不会设置组件的数据。为了让获取过程可以异步,你需要返回一个 Promise,Nuxt.js 会等这个 promise 完成后再渲染组件。

fetch 会在组件每次加载前被调用(在服务端或切换至目标路由之前)。

head

Nuxt.js 使用了 vue-meta 更新应用的 头部标签(Head)html 属性

用于更新 头部信息。如 title,descripe 等。head 方法里可通过 this 关键字来获取组件的数据。

layout

指定该页面使用哪个布局文件。默认值为 default

middleware

需要执行的中间件,如鉴权的 auth等。

transition

指定页面切换时的动画效果。支持传入 String, Object, Function。具体配置请移步至 官网

validate

Nuxt.js 可以让你在动态路由对应的页面组件中配置一个校验方法用于校验动态路由参数的有效性。

返回 true 说明路由有效,则进入路由页面。返回不是 true 则显示 404 页面。

Begin Coding

前置工作

API

在这里,我们使用 CNode API 进行开发 Demo.

axios

请求数据,我们使用 Nuxt 官方提供的 @nuxtjs/axios 安装后,在 nuxt.config.js 中加上:

export default {
  ...
  modules: [
    '@nuxtjs/axios'
  ],
  axios: {
    baseURL: 'https://cnodejs.org/api/v1',
    // or other axios configs.
  }
  ...
}

就可以在页面中通过 this.$axios.$get 来获取数据,不需要在每个页面都单独引入 axios.

scss

需要先安装 sass-loader 和 node-sass

$    yarn add sass-loader node-sass --dev

如果需要在项目中全局使用某个 scss 文件(如 mixins, vars 等),需要借助 sass-resources-loader : yarn add sass-resources-loader —dev, 还需要在 nuxt.config.js 的 build 配置中调整导出的 loader 配置:

export default {
  ...
  build: {
    extend(config, { isDev, isClient }) {
      const sassResourcesLoader = {  
        loader: 'sass-resources-loader',  
        options: {  
          resources: [
            // 填写需要全局注入 scss 的文件。引入后,所有页面均有效。
            'assets/styles/mixins.scss'  
          ]
        }  
      }
      // 修改 scss sass 引用的 loader。
      config.module.rules.forEach((rule) => {  
        if (rule.test.toString() === '/\\.vue$/') {  
          rule.options.loaders.sass.push(sassResourcesLoader)  
          rule.options.loaders.scss.push(sassResourcesLoader)  
        }  
        if (['/\\.sass$/', '/\\.scss$/'].indexOf(rule.test.toString()) !== -1) {  
          rule.use.push(sassResourcesLoader)  
        }  
      })  
    }
  }
  ...
}

首页

首页一般只需要简单的获取首页数据并渲染即可。

主要 代码:

asyncData({app, query}) {
  console.log(query)
  // 根据不用的标签获取不同的数据,最后返回话题列表。
  return app.$axios.$get(`topics?tab=${query.tab || ''}`).then(res => {
    // console.log(res)
    // console.log(JSON.parse(res))
    return {list: res.data}
  })
}

当进入首页时,该函数会被执行, nuxt 会等到获取数据后再和组件的 data 合并,进而渲染数据。在模板中,可以直接使用 list 变量获取数据。

<div class="card fluid topic" v-for="topic in list" :key="topic.id" >
  <div class="section">
    <h3><nuxt-link :to="{name: 'topic-id', params: {id: topic.id}}" class="topic-title">{{topic.title}}</nuxt-link></h3>
    <p class="topic-info">
      <mark v-if="topic.top" class="tertiary">精华</mark>
      <mark v-else>{{tabsObj[topic.tab]}}</mark>
      <span class="avatar">
        <img :data-original="topic.author.avatar_url" alt="">
      </span>
      <span class="username">
        {{topic.author.loginname}}
      </span>
    </p>
  </div>
</div>

在这里提及一下, <nuxt-link /><a /> 的区别是: nuxt-link 走的是 vue-router 的路由,即网页已为单页面,并且浏览器不会重定向。而 a 标签走的是 window.location.href,每一次点击 a 标签后的页面,都会进行一次服务端渲染,和普通的 PHP 混合开发没有太大的区别。

在这里使用了 nuxt-link 是因为 CNode 的 API 不存在跨域问题,因此可以作为一个单页面应用,体验更好。

因为列表页数据类型有多种,该页面可能会被复用,所以当路由对象发生变化时,需要重新获取数据,这时可以监听路由的变化以做出响应:

watch: {
  '$route': function() {
    console.log('$route has changed.')
    this.getData()
  }
}

配置 seo 优化(这里只是单纯的复制罢了,demo 使用,侵删):

head() {
  return {
    title: '首页' + (this.$route.query.tab ? `- ${this.tabsObj[this.$route.query.tab]}` : ''),
    meta: [{
      hid: 'description',
      name: 'description',
      content: 'CNode:Node.js专业中文社区'
    }]
  }
}

话题详情

同样的,使用 asyncData 函数进行获取数据,再渲染页面。

asyncData({app, params}) {
  console.log(params)
  return app.$axios.$get('topic/' + params.id).then(res => {
    // let data = res.data instanceof String ? JSON.parse(res.data) : res.data
    let data = res.data
    // console.log(res)
    // let div = document.createElement('div')
    // div.innerHTML = res.data.data.content
    // res.data.summary = div.innerText.substr(0, 120)
    data.summary = data.content.replace(/<[^>]+>/g,"").substr(0, 120).replace(/\s+/g, '')
    return {detail: data}
  }).catch(err => {
    console.log('axios.get failed.')
    console.error(err)
  })
}

在这里,踩过坑。想使用 div 的 innerText 来过滤掉正文中的 HTML 标签,但是,如果用户是直接进入这个页面的时候,执行 asyncData 时,document 对象是不存在的,从而会报错。也就是说,asyncData 在服务端执行时,是没有 documentwindow 对象的,请大家注意一下。

作为一个社区,seo 尤为重要,倘若每个页面都需要写一大堆的 head 对象,就会显得尤其的繁琐。所以可以借助 nuxt 的 plugin 机制,将其封装成一个函数,并注入到每一个页面当中:

// plugins/global.js
import Vue from 'vue'

Vue.mixin({
  methods: {
    // 必传 标题,描述。其他的 meta 标签通过 payload 注入,其中,每个 meta 的 hid 需要是唯一的。
    $seo(title, content, payload = []) {
      return {
        title,
        meta: [{
          hid: 'description',
          name: 'description',
          content
        }].concat(payload)
      }
    }
  }
})

在 nuxt.config.js 中加上:

export default {
  plugins: [
    '~plugins/global.js'
  ]
}

这样,只需要在页面的 head 的函数中,返回该函数即可:

head() {
    return this.$seo(this.detail.title, this.detail.summary)
}

详情页 seo

可见,详情页已经成功的设置了部分 seo 的标签。

以上是 Nuxt 的一些基础配置及应用。

我再去研究一下, fetch 和 store 的结合,将该 demo 继续完善。

Demo 线上地址
GitHub 地址

查看原文

Thinker 收藏了文章 · 2019-04-24

基于apicloud原生模块实现的凌乱的单点音频播放凌乱功能总结

关于作者

程序开发人员,不拘泥于语言与技术,目前主要从事PHP和前端开发,使用Laravel和VueJs,App端使用Apicloud混合式开发。合适和够用是永不停息的追求。

个人网站:https://www.linganmin.cn

最近刚写了一个手机在线播放的H5电影站:http://www.ifilm.ltd


缘由:公司项目使用APIcloud开发混合式app,用到了APIcloud官方提供的audio原生音频播放模块来做单点音频播放功能。单点的意思是所有播放状态交由首页处理,播放页面更改对应播放的item状态即可。因个人感觉凌乱,在老大指引下使用事件和事件监听完成之,遂总结记录之。

附上完成后的效果图:

  • 备注:因需要改变播放页的状态,在播放页的监听器中需要传递当前页面的数据,并在监听器中进行遍历,判断,修改。不然无法完成修改当前播放状态操作。

  1. 给当前页面展示的post和相关post使用Vue增加播放所需的状态,进度等属性

  2. 点击播放,判断当前post是否正在播放,若是则执行暂停事件,若不是则执行播放事件

  3. 首页(index.html)始终进行播放事件监听。
    当捕获到播放事件,开始判断当前将要播放的和当前播放的是不是同一个post,如果不是则将执行停止播放事件去停掉当前正在播放的post,然后开始播放将要播放的post。

播放语音的回调函数中执行设置播放状态事件,并将播放过程获取到的音频长度。当前播放位置等参数传递给该事件,供该事件监听器使用。

  1. 首页和播放页始终进行暂停事件监听 。【两个监听器】
    当首页捕获到暂停事件,则进行音频暂停操作(audio.pause);

当播放页捕获到暂停事件,则将当前正在播放的post的状态改为暂停

  1. 首页和播放页始终进行停止播放事件监听。【两个监听器】
    当首页捕获到停止播放事件,则进行音频停止播放操作(audio.stop);

当播放页捕获到停止播放事件,则将当前正在播放的post状态改为未播放,播放进度等置空

  1. 播放页始终进行设置播放状态事件监听。
    当播放页捕获到设置播放状态事件,则获取事件传递的数据,实时改变播放页面的状态

  2. 设置播放进度(通过拖动播放进度条)事件由当前播放页触发,出发后将progress的值传递到首页的设置播放进度监听器使用,通过该值设置播放位置

查看原文

Thinker 赞了文章 · 2019-04-24

基于apicloud原生模块实现的凌乱的单点音频播放凌乱功能总结

关于作者

程序开发人员,不拘泥于语言与技术,目前主要从事PHP和前端开发,使用Laravel和VueJs,App端使用Apicloud混合式开发。合适和够用是永不停息的追求。

个人网站:https://www.linganmin.cn

最近刚写了一个手机在线播放的H5电影站:http://www.ifilm.ltd


缘由:公司项目使用APIcloud开发混合式app,用到了APIcloud官方提供的audio原生音频播放模块来做单点音频播放功能。单点的意思是所有播放状态交由首页处理,播放页面更改对应播放的item状态即可。因个人感觉凌乱,在老大指引下使用事件和事件监听完成之,遂总结记录之。

附上完成后的效果图:

  • 备注:因需要改变播放页的状态,在播放页的监听器中需要传递当前页面的数据,并在监听器中进行遍历,判断,修改。不然无法完成修改当前播放状态操作。

  1. 给当前页面展示的post和相关post使用Vue增加播放所需的状态,进度等属性

  2. 点击播放,判断当前post是否正在播放,若是则执行暂停事件,若不是则执行播放事件

  3. 首页(index.html)始终进行播放事件监听。
    当捕获到播放事件,开始判断当前将要播放的和当前播放的是不是同一个post,如果不是则将执行停止播放事件去停掉当前正在播放的post,然后开始播放将要播放的post。

播放语音的回调函数中执行设置播放状态事件,并将播放过程获取到的音频长度。当前播放位置等参数传递给该事件,供该事件监听器使用。

  1. 首页和播放页始终进行暂停事件监听 。【两个监听器】
    当首页捕获到暂停事件,则进行音频暂停操作(audio.pause);

当播放页捕获到暂停事件,则将当前正在播放的post的状态改为暂停

  1. 首页和播放页始终进行停止播放事件监听。【两个监听器】
    当首页捕获到停止播放事件,则进行音频停止播放操作(audio.stop);

当播放页捕获到停止播放事件,则将当前正在播放的post状态改为未播放,播放进度等置空

  1. 播放页始终进行设置播放状态事件监听。
    当播放页捕获到设置播放状态事件,则获取事件传递的数据,实时改变播放页面的状态

  2. 设置播放进度(通过拖动播放进度条)事件由当前播放页触发,出发后将progress的值传递到首页的设置播放进度监听器使用,通过该值设置播放位置

查看原文

赞 1 收藏 1 评论 0

Thinker 收藏了文章 · 2019-03-04

学习Less-看这篇就够了

原文链接

前言

CSS的短板

    作为前端学习者的我们 或多或少都要学些 CSS ,它作为前端开发的三大基石之一,时刻引领着 Web 的发展潮向。 而 CSS 作为一门标记性语言,可能 给初学者第一印象 就是简单易懂,毫无逻辑,不像编程该有的样子。在语法更新时,每当新属性提出,浏览器的兼容又会马上变成绊脚石,可以说 CSS 短板不容忽视。

    问题的诞生往往伴随着技术的兴起, 在 Web 发展的这几年, 为了让 CSS 富有逻辑性,短板不那么严重,涌现出了 一些神奇的预处理语言。 它们让 CSS 彻底变成一门 可以使用 变量 、循环 、继承 、自定义方法等多种特性的标记语言,逻辑性得以大大增强。

预处理语言的诞生

其中 就我所知的有三门语言:Sass、Less 、Stylus 。

  1. Sass 诞生于 2007 年,Ruby 编写,其语法功能都十分全面,可以说 它完全把 CSS 变成了一门编程语言。另外 在国内外都很受欢迎,并且它的项目团队很是强大 ,是一款十分优秀的预处理语言。
  2. Stylus 诞生于 2010 年,来自 Node.js 社区,语法功能也和 Sass 不相伯仲,是一门十分独特的创新型语言。
  3. Less 诞生于 2009 年,受Sass的影响创建的一个开源项目。 它扩充了 CSS 语言,增加了诸如变量、混合(mixin)、函数等功能,让 CSS 更易维护、方便制作主题、扩充(引用于官网)。

选择预处理语言

这是一个十分纠结的问题。

在我看来,这就好比 找女朋友,有人喜欢 贤惠安静的,就有人喜欢 活泼爱闹的,各有各的爱好,可晚上闭灯后 其实都差不多,所以你不用太过纠结。当然了 ,首先 你要有女朋友。

在网上讨论看来,Sass 与 Stylus 相比于 Less 功能更为丰富,但对于学习成本以及适应时间 ,Less 稍胜一筹,这也是我选择 Less 的原因。

Less 没有去掉任何 CSS 的功能,而是在现有的语法上,增添了许多额外的功能特性,所以学习 Less 是一件非常舒服的事情。

如果你之前没有接触过预处理语言,纠结应该学哪一个,不如先看看 下面 Less 的介绍,我相信你会爱上它的。

使用 Less 的前奏

使用 Less 有两种方式

  1. 在页面中 引入 Less.js
    可在官网下载
    或使用CDN
    <script data-original="//cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js"></script>

需要注意的是,link 标签一定要在 Less.js 之前引入,并且 link 标签的 rel 属性要设置为stylesheet/less。

       <link rel="stylesheet/less" href="style.less">
       <script data-original="less.min.js"></script>
  1. 在命令行 使用npm安装
      npm install -g less

具体使用命令

      $ lessc styles.less > styles.css

假如还有问题,官网已经有了明确的步骤。

如果你也是 Webpack 的使用者,还需要配合 less-loader 进行处理,具体可见我的这篇文章:Webpack飞行手册,里面详细说明了 less 的处理方式。

如果你在本地环境,可以使用第一种方式,非常简单;但在生产环境中,性能非常重要,最好使用第二种方式。

正文

下面我将简介 Less 的功能特性。

变量

我们常常在 CSS 中 看到同一个值重复多次,这样难易于代码维护。
理想状态,应是下面这样:

const bgColor="skyblue";
$(".post-content").css("background-color",bgColor);
$("#wrap").css("background-color",bgColor);
$(".arctive").css("background-color",bgColor);

只要我们修改 bgColor这一个变量, 整个页面的背景颜色都会随之改变。

而 Less 中的变量十分强大,可化万物,值得一提的是,其变量是常量 ,所以只能定义一次,不能重复使用。
值变量

      /* Less */
      @color: #999;
      @bgColor: skyblue;//不要添加引号
      @width: 50%;
      #wrap {
        color: @color;
        width: @width;
      }
    
      /* 生成后的 CSS */
      #wrap {
        color: #999;
        width: 50%;
      }

@ 开头 定义变量,并且使用时 直接 键入 @名称。

在平时工作中,我们就可以把 常用的变量 封装到一个文件中,这样利于代码组织维护。

      @lightPrimaryColor: #c5cae9;
      @textPrimaryColor: #fff;
      @accentColor: rgb(99, 137, 185);
      @primaryTextColor: #646464;
      @secondaryTextColor: #000;
      @dividerColor: #b6b6b6;
      @borderColor: #dadada;

选择器变量

让 选择器 变成 动态

      /* Less */
      @mySelector: #wrap;
      @Wrap: wrap;
      @{mySelector}{ //变量名 必须使用大括号包裹
        color: #999;
        width: 50%;
      }
      .@{Wrap}{
        color:#ccc;
      }
      #@{Wrap}{
        color:#666;
      }
    
      /* 生成的 CSS */
      #wrap{
        color: #999;
        width: 50%;
      }
      .wrap{
        color:#ccc;
      }
      #wrap{
        color:#666;
      }

属性变量

可减少代码书写量

      /* Less */
      @borderStyle: border-style;
      @Soild:solid;
      #wrap{
        @{borderStyle}: @Soild;//变量名 必须使用大括号包裹
      }
    
      /* 生成的 CSS */
      #wrap{
        border-style:solid;
      }
    

url 变量

项目结构改变时,修改其变量即可。

      /* Less */
      @images: "../img";//需要加引号
      body {
        background: url("@{images}/dog.png");//变量名 必须使用大括号包裹
      }
    
      /* 生成的 CSS */
      body {
        background: url("../img/dog.png");
      }
    

声明变量

有点类似于 下面的 混合方法

      - 结构: @name: { 属性: 值 ;};
      - 使用:@name();
      /* Less */
      @background: {background:red;};
      #main{
          @background();
      }
      @Rules:{
          width: 200px;
          height: 200px;
          border: solid 1px red;
      };
      #con{
        @Rules();
      }
    
      /* 生成的 CSS */
      #main{
        background:red;
      }
      #con{
        width: 200px;
        height: 200px;
        border: solid 1px red;
      }

变量运算

不得不提的是,Less 的变量运算完全超出我的期望,十分强大。

  - 加减法时 以第一个数据的单位为基准
  - 乘除法时 注意单位一定要统一
      /* Less */
      @width:300px;
      @color:#222;
      #wrap{
        width:@width-20;
        height:@width-20*5;
        margin:(@width-20)*5;
        color:@color*2;
        background-color:@color + #111;
      }
    
      /* 生成的 CSS */
      #wrap{
        width:280px;
        height:200px;
        margin:1400px;
        color:#444;
        background-color:#333;
      }
    

变量作用域

一句话理解就是:就近原则,不要跟我提闭包。

借助官网的Demo

      /* Less */
      @var: @a;
      @a: 100%;
      #wrap {
        width: @var;
        @a: 9%;
      }
    
      /* 生成的 CSS */
      #wrap {
        width: 9%;
      }

用变量去定义变量

      /* Less */
      @fnord:  "I am fnord.";
      @var:    "fnord";
      #wrap::after{
        content: @@var; //将@var替换为其值 content:@fnord;
      }
      /* 生成的 CSS */
      #wrap::after{
        content: "I am fnord.";
      }

嵌套

& 的妙用

& :代表的上一层选择器的名字,此例便是header

      /* Less */
      #header{
        &:after{
          content:"Less is more!";
        }
        .title{
          font-weight:bold;
        }
        &_content{//理解方式:直接把 & 替换成 #header
          margin:20px;
        }
      }
      /* 生成的 CSS */
      #header::after{
        content:"Less is more!";
      }
      #header .title{ //嵌套了
        font-weight:bold;
      }
      #header_content{//没有嵌套!
          margin:20px;
      }

媒体查询

在以往的工作中,我们使用 媒体查询,都要把一个元素 分开写

      #wrap{
        width:500px;
      }
      @media screen and (max-width:768px){
        #wrap{
          width:100px;
        }
      }

Less 提供了一个十分便捷的方式

      /* Less */
      #main{
          //something...
    
          @media screen{
              @media (max-width:768px){
                width:100px;
              }
          }
          @media tv {
            width:2000px;
          }
      }
      /* 生成的 CSS */
      @media screen and (maxwidth:768px){
        #main{
            width:100px; 
        }
      }
      @media tv{
        #main{
          width:2000px;
        }
      }

唯一的缺点就是 每一个元素都会编译出自己 @media 声明,并不会合并。

实战技巧

可以借助 Less 在元素中,去定义自己的私有样式。

      /* Less */
      #main{
        // something..
        &.show{
          display:block;
        }
      }
      .show{
        display:none;
      }
      const main = document.getElementById("main");
      main.classList.add("show");

结果:

      #main.show{
        display:block;
      }
      .show{
        display:none; //会被覆盖。
      }

混合方法

无参数方法

方法犹如 声明的集合,使用时 直接键入名称即可。

      /* Less */
      .card { // 等价于 .card()
          background: #f6f6f6;
          -webkit-box-shadow: 0 1px 2px rgba(151, 151, 151, .58);
          box-shadow: 0 1px 2px rgba(151, 151, 151, .58);
      }
      #wrap{
        .card;//等价于.card();
      }
      /* 生成的 CSS */
      #wrap{
        background: #f6f6f6;
        -webkit-box-shadow: 0 1px 2px rgba(151, 151, 151, .58);
        box-shadow: 0 1px 2px rgba(151, 151, 151, .58);
      }

其中 .card.card() 是等价的。
个人建议,为了避免 代码混淆,应写成 :

      .card(){
        //something...
      }
      #wrap{
        .card();
      }
要点:
  `.` 与 `#` 皆可作为 方法前缀。
  方法后写不写 `()` 看个人习惯。

默认参数方法

Less 可以使用默认参数,如果 没有传参数,那么将使用默认参数。

@arguments 犹如 JS 中的 arguments 指代的是 全部参数。

传的参数中 必须带着单位。

      /* Less */
      .border(@a:10px,@b:50px,@c:30px,@color:#000){
          border:solid 1px @color;
          box-shadow: @arguments;//指代的是 全部参数
      }
      #main{
          .border(0px,5px,30px,red);//必须带着单位
      }
      #wrap{
          .border(0px);
      }
      #content{
        .border;//等价于 .border()
      }
    
      /* 生成的 CSS */
      #main{
          border:solid 1px red;
          box-shadow:0px,5px,30px,red;
      }
      #wrap{
          border:solid 1px #000;
          box-shadow: 0px 50px 30px #000;
      }
      #content{
          border:solid 1px #000;
          box-shadow: 10px 50px 30px #000;
      }
    

方法的匹配模式

与 面向对象中的多态 很相似

      /* Less */
      .triangle(top,@width:20px,@color:#000){
          border-color:transparent  transparent @color transparent ;
      }
      .triangle(right,@width:20px,@color:#000){
          border-color:transparent @color transparent  transparent ;
      }
    
      .triangle(bottom,@width:20px,@color:#000){
          border-color:@color transparent  transparent  transparent ;
      }
      .triangle(left,@width:20px,@color:#000){
          border-color:transparent  transparent  transparent @color;
      }
      .triangle(@_,@width:20px,@color:#000){
          border-style: solid;
          border-width: @width;
      }
      #main{
          .triangle(left, 50px, #999)
      }
      /* 生成的 CSS */
      #main{
        border-color:transparent  transparent  transparent #999;
        border-style: solid;
        border-width: 50px;
      }

要点

  - 第一个参数 `left` 要会找到方法中匹配程度最高的,如果匹配程度相同,将全部选择,并存在着样式覆盖替换。

  - 如果匹配的参数 是变量,则将会匹配,如 `@_` 。

方法的命名空间

让方法更加规范

      /* Less */
      #card(){
          background: #723232;
          .d(@w:300px){
              width: @w;
              
              #a(@h:300px){
                  height: @h;//可以使用上一层传进来的方法
                  width: @w;
              }
          }
      }
      #wrap{
          #card > .d > #a(100px); // 父元素不能加 括号
      }
      #main{
          #card .d();
      }
      #con{
          //不得单独使用命名空间的方法
          //.d() 如果前面没有引入命名空间 #card ,将会报错
          
          #card; // 等价于 #card();
          .d(20px); //必须先引入 #card
      }
      /* 生成的 CSS */
      #wrap{
        height:100px;
        width:300px;
      }
      #main{
        width:300px;
      }
      #con{
        width:20px;
      }
    

要点

  - 在 CSS 中`>` 选择器,选择的是 儿子元素,就是 必须与父元素 有直接血源的元素。
  - 在引入命令空间时,如使用 `>` 选择器,父元素不能加 括号。
  - 不得单独使用命名空间的方法 必须先引入命名空间,才能使用 其中方法。
  - 子方法 可以使用上一层传进来的方法

方法的条件筛选

Less 没有 if else,可是它有 when

    /* Less */
    #card{
        
        // and 运算符 ,相当于 与运算 &&,必须条件全部符合才会执行
        .border(@width,@color,@style) when (@width>100px) and(@color=#999){
            border:@style @color @width;
        }
    
        // not 运算符,相当于 非运算 !,条件为 不符合才会执行
        .background(@color) when not (@color>=#222){
            background:@color;
        }
    
        // , 逗号分隔符:相当于 或运算 ||,只要有一个符合条件就会执行
        .font(@size:20px) when (@size>50px) , (@size<100px){
            font-size: @size;
        }
    }
    #main{
        #card>.border(200px,#999,solid);
        #card .background(#111);
        #card > .font(40px);
    }
    /* 生成后的 CSS */
    #main{
      border:solid #999 200px;
      background:#111;
      font-size:40px;
    }

要点

  - 比较运算有: > >= = =< <。
  - = 代表的是等于
  - 除去关键字 true 以外的值都被视为 false:

数量不定的参数

如果你希望你的方法接受数量不定的参数,你可以使用... ,犹如 ES6 的扩展运算符。

      /* Less */
      .boxShadow(...){
          box-shadow: @arguments;
      }
      .textShadow(@a,...){
          text-shadow: @arguments;
      }
      #main{
          .boxShadow(1px,4px,30px,red);
          .textShadow(1px,4px,30px,red);
      }
    
      /* 生成后的 CSS */
      #main{
        box-shadow: 1px 4px 30px red;
        text-shadow: 1px 4px 30px red;
      }

方法使用important!

使用方法 非常简单,在方法名后 加上关键字即可。

      /* Less */
      .border{
          border: solid 1px red;
          margin: 50px;
      }
      #main{
          .border() !important;
      }
      /* 生成后的 CSS */
      #main {
          border: solid 1px red !important;
          margin: 50px !important;
      }

循环方法

Less 并没有提供 for 循环功能,但这也难不倒 聪明的程序员,使用递归去实现。

下面是官网中的一个 Demo,模拟了生成栅格系统。

      /* Less */
      .generate-columns(4);
    
      .generate-columns(@n, @i: 1) when (@i =< @n) {
        .column-@{i} {
          width: (@i * 100% / @n);
        }
        .generate-columns(@n, (@i + 1));
      }
      /* 生成后的 CSS */
      .column-1 {
        width: 25%;
      }
      .column-2 {
        width: 50%;
      }
      .column-3 {
        width: 75%;
      }
      .column-4 {
        width: 100%;
      }
  1. 属性拼接方法

+_ 代表的是 空格;+ 代表的是 逗号。


  - 逗号
      /* Less */
      .boxShadow() {
          box-shadow+: inset 0 0 10px #555;
      }
      .main {
        .boxShadow();
        box-shadow+: 0 0 20px black;
      }
      /* 生成后的 CSS */
      .main {
        box-shadow: inset 0 0 10px #555, 0 0 20px black;
      }
  - 空格
      /* Less */
      .Animation() {
        transform+_: scale(2);
      }
      .main {
        .Animation();
        transform+_: rotate(15deg);
      }
    
      /* 生成的 CSS */
      .main {
        transform: scale(2) rotate(15deg);
      }
  1. 实战技巧

    下面是官网中的一个非常赞的 Demo

      /* Less */
      .average(@x, @y) {
        @average: ((@x + @y) / 2);
      }
    
      div {
        .average(16px, 50px); // 调用 方法
        padding: @average;    // 使用返回值
      }
    
      /* 生成的 CSS */
      div {
        padding: 33px;
      }

可以说 Less 是一门优雅编程语言。

继承

extend 是 Less 的一个伪类。它可继承 所匹配声明中的全部样式。
extend 关键字的使用

      /* Less */
      .animation{
          transition: all .3s ease-out;
          .hide{
            transform:scale(0);
          }
      }
      #main{
          &:extend(.animation);
      }
      #con{
          &:extend(.animation .hide);
      }
    
      /* 生成后的 CSS */
      .animation,#main{
        transition: all .3s ease-out;
      }
      .animation .hide , #con{
          transform:scale(0);
      }

all 全局搜索替换

  使用选择器匹配到的 全部声明。
      /* Less */
      #main{
        width: 200px;
      }
      #main {
        &:after {
          content:"Less is good!";
        }
      }
      #wrap:extend(#main all) {}
    
      /* 生成的 CSS */
      #main,#wrap{
        width: 200px;
      }
      #main:after, #wrap:after {
          content: "Less is good!";
      }

减少代码的重复性

从表面 看来,extend 与 方法 最大的差别,就是 extend 是同个选择器共用同一个声明,而 方法 是使用自己的声明,这无疑 增加了代码的重复性。

方法示例 与上面的 extend 进行对比:

      /* Less */
      .Method{
        width: 200px;
        &:after {
            content:"Less is good!";
        }
      }
      #main{
        .Method;
      }
      #wrap{
        .Method;
      }
    
      /* 生成的 CSS */
      #main{
        width: 200px;
        &:after{
          content:"Less is good!";
        }  
      }
      #wrap{
        width: 200px;
        &:after{
          content:"Less is good!";
        }  
      }
    

要点

翻译官网

    • 选择器和扩展之间 是允许有空格的:pre:hover :extend(div pre).
    • 可以有多个扩展: pre:hover:extend(div pre):extend(.bucket tr) - 注意这与 pre:hover:extend(div pre, .bucket tr)一样。
    • 这是不可以的,扩展必须在最后 : pre:hover:extend(div pre).nth-child(odd)。
    • 如果一个规则集包含多个选择器,所有选择器都可以使用extend关键字。

      导入

      1. 导入 less 文件 可省略后缀

        import "main"; 
        //等价于
        import "main.less";
      2. @import 的位置可随意放置

        #main{
          font-size:15px;
        }
        @import "style";
    1. reference

      Less 中 最强大的特性
      使用 引入的 Less 文件,但不会 编译它。

      /* Less */
      @import (reference) "bootstrap.less"; 
      
      #wrap:extend(.navbar all){}
       翻译官网:
       > 使用@import (reference)导入外部文件,但不会添加 把导入的文件 编译到最终输出中,只引用。
      
    2. once

      @import语句的默认行为。这表明相同的文件只会被导入一次,而随后的导入文件的重复代码都不会解析。
      @import (once) "foo.less";
      @import (once) "foo.less"; // this statement will be ignored
    3. multiple

      使用@import (multiple)允许导入多个同名文件。
      /* Less */
         
      // file: foo.less
      .a {
        color: green;
      }
      // file: main.less
      @import (multiple) "foo.less";
      @import (multiple) "foo.less";
         
      /* 生成后的 CSS */
      .a {
        color: green;
      }
      .a {
        color: green;
      }

    函数

    1. 判断类型

      • isnumber

        判断给定的值 是否 是一个数字。
        
        ```less
        isnumber(#ff0);     // false
        isnumber(blue);     // false
        isnumber("string"); // false
        isnumber(1234);     // true
        isnumber(56px);     // true
        isnumber(7.8%);     // true
        isnumber(keyword);  // false
        isnumber(url(...)); // false
        ```
        
      • iscolor

        > 判断给定的值 是否 是一个颜色。
        
      • isurl

        > 判断给定的值 是否 是一个 url 。
        
    2. 颜色操作

      • saturate

        > 增加一定数值的颜色饱和度。
        
      • lighten

        > 增加一定数值的颜色亮度。
        
      • darken

        > 降低一定数值的颜色亮度。
        
      • fade

        > 给颜色设定一定数值的透明度。
        
      • mix

        > 根据比例混合两种颜色。
        
    3. 数学函数

      • ceil

        > 向上取整。
        
      • floor

        > 向下取整。
        
      • percentage

        > 将浮点数转换为百分比字符串。
        
      • round

        > 四舍五入。
        
      • sqrt

        > 计算一个数的平方根。
        
      • abs

        > 计算数字的绝对值,原样保持单位。
        
      • pow

        > 计算一个数的乘方。
        

    由于 文章 篇幅有限,所以 只能介绍一些 使用效率高的函数。

    如果你想了解更多,可以去官网的函数链接

    其他

    1. 注释

      • /* */ CSS原生注释,会被编译在 CSS 文件中。
      • /   / Less提供的一种注释,不会被编译在 CSS 文件中。
    2. 避免编译
          /* Less */
          #main{
            width:~'calc(300px-30px)';
          }
        
          /* 生成后的 CSS */
          #main{
            width:calc(300px-30px);
          }
      结构: `~' 值 '`
    
    1. 使用 JS

      因为 Less 是由 JS 编写,所以 Less 有一得天独厚的特性:代码中使用 Javascript 。

          /* Less */
          @content:`"aaa".toUpperCase()`;
          #randomColor{
            @randomColor: ~"rgb(`Math.round(Math.random() * 256)`,`Math.round(Math.random() * 256)`,`Math.round(Math.random() * 256)`)";
          }
          #wrap{
            width: ~"`Math.round(Math.random() * 100)`px";
            &:after{
                content:@content;
            }
            height: ~"`window.innerHeight`px";
            alert:~"`alert(1)`";
            #randomColor();
            background-color: @randomColor;
          }
          /* 生成后的 CSS */
        
          // 弹出 1
          #wrap{
            width: 随机值(0~100)px;
            height: 743px;//由电脑而异
            background: 随机颜色;
          }
          #wrap::after{
            content:"AAA";
          }

        前几个月 , 有个 CSS in JS 的概念非常火,现在 看来 JS in CSS 也未曾不可。
    我觉得完全可以根据 Less 这个特性来造个轮子,JS来控制 CSS ,形成 动态属性,如果成功 很可能会改变 现在前端的打开姿势。

    结束语

    原文链接

    (完)

    查看原文

    Thinker 赞了文章 · 2019-03-04

    学习Less-看这篇就够了

    原文链接

    前言

    CSS的短板

        作为前端学习者的我们 或多或少都要学些 CSS ,它作为前端开发的三大基石之一,时刻引领着 Web 的发展潮向。 而 CSS 作为一门标记性语言,可能 给初学者第一印象 就是简单易懂,毫无逻辑,不像编程该有的样子。在语法更新时,每当新属性提出,浏览器的兼容又会马上变成绊脚石,可以说 CSS 短板不容忽视。

        问题的诞生往往伴随着技术的兴起, 在 Web 发展的这几年, 为了让 CSS 富有逻辑性,短板不那么严重,涌现出了 一些神奇的预处理语言。 它们让 CSS 彻底变成一门 可以使用 变量 、循环 、继承 、自定义方法等多种特性的标记语言,逻辑性得以大大增强。

    预处理语言的诞生

    其中 就我所知的有三门语言:Sass、Less 、Stylus 。

    1. Sass 诞生于 2007 年,Ruby 编写,其语法功能都十分全面,可以说 它完全把 CSS 变成了一门编程语言。另外 在国内外都很受欢迎,并且它的项目团队很是强大 ,是一款十分优秀的预处理语言。
    2. Stylus 诞生于 2010 年,来自 Node.js 社区,语法功能也和 Sass 不相伯仲,是一门十分独特的创新型语言。
    3. Less 诞生于 2009 年,受Sass的影响创建的一个开源项目。 它扩充了 CSS 语言,增加了诸如变量、混合(mixin)、函数等功能,让 CSS 更易维护、方便制作主题、扩充(引用于官网)。

    选择预处理语言

    这是一个十分纠结的问题。

    在我看来,这就好比 找女朋友,有人喜欢 贤惠安静的,就有人喜欢 活泼爱闹的,各有各的爱好,可晚上闭灯后 其实都差不多,所以你不用太过纠结。当然了 ,首先 你要有女朋友。

    在网上讨论看来,Sass 与 Stylus 相比于 Less 功能更为丰富,但对于学习成本以及适应时间 ,Less 稍胜一筹,这也是我选择 Less 的原因。

    Less 没有去掉任何 CSS 的功能,而是在现有的语法上,增添了许多额外的功能特性,所以学习 Less 是一件非常舒服的事情。

    如果你之前没有接触过预处理语言,纠结应该学哪一个,不如先看看 下面 Less 的介绍,我相信你会爱上它的。

    使用 Less 的前奏

    使用 Less 有两种方式

    1. 在页面中 引入 Less.js
      可在官网下载
      或使用CDN
        <script data-original="//cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js"></script>

    需要注意的是,link 标签一定要在 Less.js 之前引入,并且 link 标签的 rel 属性要设置为stylesheet/less。

           <link rel="stylesheet/less" href="style.less">
           <script data-original="less.min.js"></script>
    1. 在命令行 使用npm安装
          npm install -g less

    具体使用命令

          $ lessc styles.less > styles.css

    假如还有问题,官网已经有了明确的步骤。

    如果你也是 Webpack 的使用者,还需要配合 less-loader 进行处理,具体可见我的这篇文章:Webpack飞行手册,里面详细说明了 less 的处理方式。

    如果你在本地环境,可以使用第一种方式,非常简单;但在生产环境中,性能非常重要,最好使用第二种方式。

    正文

    下面我将简介 Less 的功能特性。

    变量

    我们常常在 CSS 中 看到同一个值重复多次,这样难易于代码维护。
    理想状态,应是下面这样:

    const bgColor="skyblue";
    $(".post-content").css("background-color",bgColor);
    $("#wrap").css("background-color",bgColor);
    $(".arctive").css("background-color",bgColor);

    只要我们修改 bgColor这一个变量, 整个页面的背景颜色都会随之改变。

    而 Less 中的变量十分强大,可化万物,值得一提的是,其变量是常量 ,所以只能定义一次,不能重复使用。
    值变量

          /* Less */
          @color: #999;
          @bgColor: skyblue;//不要添加引号
          @width: 50%;
          #wrap {
            color: @color;
            width: @width;
          }
        
          /* 生成后的 CSS */
          #wrap {
            color: #999;
            width: 50%;
          }

    @ 开头 定义变量,并且使用时 直接 键入 @名称。

    在平时工作中,我们就可以把 常用的变量 封装到一个文件中,这样利于代码组织维护。

          @lightPrimaryColor: #c5cae9;
          @textPrimaryColor: #fff;
          @accentColor: rgb(99, 137, 185);
          @primaryTextColor: #646464;
          @secondaryTextColor: #000;
          @dividerColor: #b6b6b6;
          @borderColor: #dadada;

    选择器变量

    让 选择器 变成 动态

          /* Less */
          @mySelector: #wrap;
          @Wrap: wrap;
          @{mySelector}{ //变量名 必须使用大括号包裹
            color: #999;
            width: 50%;
          }
          .@{Wrap}{
            color:#ccc;
          }
          #@{Wrap}{
            color:#666;
          }
        
          /* 生成的 CSS */
          #wrap{
            color: #999;
            width: 50%;
          }
          .wrap{
            color:#ccc;
          }
          #wrap{
            color:#666;
          }

    属性变量

    可减少代码书写量

          /* Less */
          @borderStyle: border-style;
          @Soild:solid;
          #wrap{
            @{borderStyle}: @Soild;//变量名 必须使用大括号包裹
          }
        
          /* 生成的 CSS */
          #wrap{
            border-style:solid;
          }
        

    url 变量

    项目结构改变时,修改其变量即可。

          /* Less */
          @images: "../img";//需要加引号
          body {
            background: url("@{images}/dog.png");//变量名 必须使用大括号包裹
          }
        
          /* 生成的 CSS */
          body {
            background: url("../img/dog.png");
          }
        

    声明变量

    有点类似于 下面的 混合方法

          - 结构: @name: { 属性: 值 ;};
          - 使用:@name();
    
          /* Less */
          @background: {background:red;};
          #main{
              @background();
          }
          @Rules:{
              width: 200px;
              height: 200px;
              border: solid 1px red;
          };
          #con{
            @Rules();
          }
        
          /* 生成的 CSS */
          #main{
            background:red;
          }
          #con{
            width: 200px;
            height: 200px;
            border: solid 1px red;
          }

    变量运算

    不得不提的是,Less 的变量运算完全超出我的期望,十分强大。

      - 加减法时 以第一个数据的单位为基准
      - 乘除法时 注意单位一定要统一
    
          /* Less */
          @width:300px;
          @color:#222;
          #wrap{
            width:@width-20;
            height:@width-20*5;
            margin:(@width-20)*5;
            color:@color*2;
            background-color:@color + #111;
          }
        
          /* 生成的 CSS */
          #wrap{
            width:280px;
            height:200px;
            margin:1400px;
            color:#444;
            background-color:#333;
          }
        

    变量作用域

    一句话理解就是:就近原则,不要跟我提闭包。

    借助官网的Demo

          /* Less */
          @var: @a;
          @a: 100%;
          #wrap {
            width: @var;
            @a: 9%;
          }
        
          /* 生成的 CSS */
          #wrap {
            width: 9%;
          }

    用变量去定义变量

          /* Less */
          @fnord:  "I am fnord.";
          @var:    "fnord";
          #wrap::after{
            content: @@var; //将@var替换为其值 content:@fnord;
          }
          /* 生成的 CSS */
          #wrap::after{
            content: "I am fnord.";
          }

    嵌套

    & 的妙用

    & :代表的上一层选择器的名字,此例便是header

          /* Less */
          #header{
            &:after{
              content:"Less is more!";
            }
            .title{
              font-weight:bold;
            }
            &_content{//理解方式:直接把 & 替换成 #header
              margin:20px;
            }
          }
          /* 生成的 CSS */
          #header::after{
            content:"Less is more!";
          }
          #header .title{ //嵌套了
            font-weight:bold;
          }
          #header_content{//没有嵌套!
              margin:20px;
          }

    媒体查询

    在以往的工作中,我们使用 媒体查询,都要把一个元素 分开写

          #wrap{
            width:500px;
          }
          @media screen and (max-width:768px){
            #wrap{
              width:100px;
            }
          }

    Less 提供了一个十分便捷的方式

          /* Less */
          #main{
              //something...
        
              @media screen{
                  @media (max-width:768px){
                    width:100px;
                  }
              }
              @media tv {
                width:2000px;
              }
          }
          /* 生成的 CSS */
          @media screen and (maxwidth:768px){
            #main{
                width:100px; 
            }
          }
          @media tv{
            #main{
              width:2000px;
            }
          }

    唯一的缺点就是 每一个元素都会编译出自己 @media 声明,并不会合并。

    实战技巧

    可以借助 Less 在元素中,去定义自己的私有样式。

          /* Less */
          #main{
            // something..
            &.show{
              display:block;
            }
          }
          .show{
            display:none;
          }
          const main = document.getElementById("main");
          main.classList.add("show");

    结果:

          #main.show{
            display:block;
          }
          .show{
            display:none; //会被覆盖。
          }

    混合方法

    无参数方法

    方法犹如 声明的集合,使用时 直接键入名称即可。

          /* Less */
          .card { // 等价于 .card()
              background: #f6f6f6;
              -webkit-box-shadow: 0 1px 2px rgba(151, 151, 151, .58);
              box-shadow: 0 1px 2px rgba(151, 151, 151, .58);
          }
          #wrap{
            .card;//等价于.card();
          }
          /* 生成的 CSS */
          #wrap{
            background: #f6f6f6;
            -webkit-box-shadow: 0 1px 2px rgba(151, 151, 151, .58);
            box-shadow: 0 1px 2px rgba(151, 151, 151, .58);
          }

    其中 .card.card() 是等价的。
    个人建议,为了避免 代码混淆,应写成 :

          .card(){
            //something...
          }
          #wrap{
            .card();
          }
    要点:
      `.` 与 `#` 皆可作为 方法前缀。
      方法后写不写 `()` 看个人习惯。
    

    默认参数方法

    Less 可以使用默认参数,如果 没有传参数,那么将使用默认参数。

    @arguments 犹如 JS 中的 arguments 指代的是 全部参数。

    传的参数中 必须带着单位。

          /* Less */
          .border(@a:10px,@b:50px,@c:30px,@color:#000){
              border:solid 1px @color;
              box-shadow: @arguments;//指代的是 全部参数
          }
          #main{
              .border(0px,5px,30px,red);//必须带着单位
          }
          #wrap{
              .border(0px);
          }
          #content{
            .border;//等价于 .border()
          }
        
          /* 生成的 CSS */
          #main{
              border:solid 1px red;
              box-shadow:0px,5px,30px,red;
          }
          #wrap{
              border:solid 1px #000;
              box-shadow: 0px 50px 30px #000;
          }
          #content{
              border:solid 1px #000;
              box-shadow: 10px 50px 30px #000;
          }
        

    方法的匹配模式

    与 面向对象中的多态 很相似

          /* Less */
          .triangle(top,@width:20px,@color:#000){
              border-color:transparent  transparent @color transparent ;
          }
          .triangle(right,@width:20px,@color:#000){
              border-color:transparent @color transparent  transparent ;
          }
        
          .triangle(bottom,@width:20px,@color:#000){
              border-color:@color transparent  transparent  transparent ;
          }
          .triangle(left,@width:20px,@color:#000){
              border-color:transparent  transparent  transparent @color;
          }
          .triangle(@_,@width:20px,@color:#000){
              border-style: solid;
              border-width: @width;
          }
          #main{
              .triangle(left, 50px, #999)
          }
          /* 生成的 CSS */
          #main{
            border-color:transparent  transparent  transparent #999;
            border-style: solid;
            border-width: 50px;
          }

    要点

      - 第一个参数 `left` 要会找到方法中匹配程度最高的,如果匹配程度相同,将全部选择,并存在着样式覆盖替换。
    
      - 如果匹配的参数 是变量,则将会匹配,如 `@_` 。
    

    方法的命名空间

    让方法更加规范

          /* Less */
          #card(){
              background: #723232;
              .d(@w:300px){
                  width: @w;
                  
                  #a(@h:300px){
                      height: @h;//可以使用上一层传进来的方法
                      width: @w;
                  }
              }
          }
          #wrap{
              #card > .d > #a(100px); // 父元素不能加 括号
          }
          #main{
              #card .d();
          }
          #con{
              //不得单独使用命名空间的方法
              //.d() 如果前面没有引入命名空间 #card ,将会报错
              
              #card; // 等价于 #card();
              .d(20px); //必须先引入 #card
          }
          /* 生成的 CSS */
          #wrap{
            height:100px;
            width:300px;
          }
          #main{
            width:300px;
          }
          #con{
            width:20px;
          }
        

    要点

      - 在 CSS 中`>` 选择器,选择的是 儿子元素,就是 必须与父元素 有直接血源的元素。
      - 在引入命令空间时,如使用 `>` 选择器,父元素不能加 括号。
      - 不得单独使用命名空间的方法 必须先引入命名空间,才能使用 其中方法。
      - 子方法 可以使用上一层传进来的方法
    

    方法的条件筛选

    Less 没有 if else,可是它有 when

        /* Less */
        #card{
            
            // and 运算符 ,相当于 与运算 &&,必须条件全部符合才会执行
            .border(@width,@color,@style) when (@width>100px) and(@color=#999){
                border:@style @color @width;
            }
        
            // not 运算符,相当于 非运算 !,条件为 不符合才会执行
            .background(@color) when not (@color>=#222){
                background:@color;
            }
        
            // , 逗号分隔符:相当于 或运算 ||,只要有一个符合条件就会执行
            .font(@size:20px) when (@size>50px) , (@size<100px){
                font-size: @size;
            }
        }
        #main{
            #card>.border(200px,#999,solid);
            #card .background(#111);
            #card > .font(40px);
        }
        /* 生成后的 CSS */
        #main{
          border:solid #999 200px;
          background:#111;
          font-size:40px;
        }

    要点

      - 比较运算有: > >= = =< <。
      - = 代表的是等于
      - 除去关键字 true 以外的值都被视为 false:
    

    数量不定的参数

    如果你希望你的方法接受数量不定的参数,你可以使用... ,犹如 ES6 的扩展运算符。

          /* Less */
          .boxShadow(...){
              box-shadow: @arguments;
          }
          .textShadow(@a,...){
              text-shadow: @arguments;
          }
          #main{
              .boxShadow(1px,4px,30px,red);
              .textShadow(1px,4px,30px,red);
          }
        
          /* 生成后的 CSS */
          #main{
            box-shadow: 1px 4px 30px red;
            text-shadow: 1px 4px 30px red;
          }

    方法使用important!

    使用方法 非常简单,在方法名后 加上关键字即可。

          /* Less */
          .border{
              border: solid 1px red;
              margin: 50px;
          }
          #main{
              .border() !important;
          }
          /* 生成后的 CSS */
          #main {
              border: solid 1px red !important;
              margin: 50px !important;
          }

    循环方法

    Less 并没有提供 for 循环功能,但这也难不倒 聪明的程序员,使用递归去实现。

    下面是官网中的一个 Demo,模拟了生成栅格系统。

          /* Less */
          .generate-columns(4);
        
          .generate-columns(@n, @i: 1) when (@i =< @n) {
            .column-@{i} {
              width: (@i * 100% / @n);
            }
            .generate-columns(@n, (@i + 1));
          }
          /* 生成后的 CSS */
          .column-1 {
            width: 25%;
          }
          .column-2 {
            width: 50%;
          }
          .column-3 {
            width: 75%;
          }
          .column-4 {
            width: 100%;
          }
    1. 属性拼接方法

    +_ 代表的是 空格;+ 代表的是 逗号。

    
      - 逗号
          /* Less */
          .boxShadow() {
              box-shadow+: inset 0 0 10px #555;
          }
          .main {
            .boxShadow();
            box-shadow+: 0 0 20px black;
          }
          /* 生成后的 CSS */
          .main {
            box-shadow: inset 0 0 10px #555, 0 0 20px black;
          }
      - 空格
          /* Less */
          .Animation() {
            transform+_: scale(2);
          }
          .main {
            .Animation();
            transform+_: rotate(15deg);
          }
        
          /* 生成的 CSS */
          .main {
            transform: scale(2) rotate(15deg);
          }
    1. 实战技巧

      下面是官网中的一个非常赞的 Demo

          /* Less */
          .average(@x, @y) {
            @average: ((@x + @y) / 2);
          }
        
          div {
            .average(16px, 50px); // 调用 方法
            padding: @average;    // 使用返回值
          }
        
          /* 生成的 CSS */
          div {
            padding: 33px;
          }

    可以说 Less 是一门优雅编程语言。

    继承

    extend 是 Less 的一个伪类。它可继承 所匹配声明中的全部样式。
    extend 关键字的使用

          /* Less */
          .animation{
              transition: all .3s ease-out;
              .hide{
                transform:scale(0);
              }
          }
          #main{
              &:extend(.animation);
          }
          #con{
              &:extend(.animation .hide);
          }
        
          /* 生成后的 CSS */
          .animation,#main{
            transition: all .3s ease-out;
          }
          .animation .hide , #con{
              transform:scale(0);
          }

    all 全局搜索替换

      使用选择器匹配到的 全部声明。
          /* Less */
          #main{
            width: 200px;
          }
          #main {
            &:after {
              content:"Less is good!";
            }
          }
          #wrap:extend(#main all) {}
        
          /* 生成的 CSS */
          #main,#wrap{
            width: 200px;
          }
          #main:after, #wrap:after {
              content: "Less is good!";
          }

    减少代码的重复性

    从表面 看来,extend 与 方法 最大的差别,就是 extend 是同个选择器共用同一个声明,而 方法 是使用自己的声明,这无疑 增加了代码的重复性。

    方法示例 与上面的 extend 进行对比:

          /* Less */
          .Method{
            width: 200px;
            &:after {
                content:"Less is good!";
            }
          }
          #main{
            .Method;
          }
          #wrap{
            .Method;
          }
        
          /* 生成的 CSS */
          #main{
            width: 200px;
            &:after{
              content:"Less is good!";
            }  
          }
          #wrap{
            width: 200px;
            &:after{
              content:"Less is good!";
            }  
          }
        

    要点

    翻译官网

    • 选择器和扩展之间 是允许有空格的:pre:hover :extend(div pre).
    • 可以有多个扩展: pre:hover:extend(div pre):extend(.bucket tr) - 注意这与 pre:hover:extend(div pre, .bucket tr)一样。
    • 这是不可以的,扩展必须在最后 : pre:hover:extend(div pre).nth-child(odd)。
    • 如果一个规则集包含多个选择器,所有选择器都可以使用extend关键字。

      导入

      1. 导入 less 文件 可省略后缀

        import "main"; 
        //等价于
        import "main.less";
      2. @import 的位置可随意放置

        #main{
          font-size:15px;
        }
        @import "style";
    1. reference

      Less 中 最强大的特性
      使用 引入的 Less 文件,但不会 编译它。

      /* Less */
      @import (reference) "bootstrap.less"; 
      
      #wrap:extend(.navbar all){}
       翻译官网:
       > 使用@import (reference)导入外部文件,但不会添加 把导入的文件 编译到最终输出中,只引用。
      
    2. once

      @import语句的默认行为。这表明相同的文件只会被导入一次,而随后的导入文件的重复代码都不会解析。
      @import (once) "foo.less";
      @import (once) "foo.less"; // this statement will be ignored
    3. multiple

      使用@import (multiple)允许导入多个同名文件。
      /* Less */
         
      // file: foo.less
      .a {
        color: green;
      }
      // file: main.less
      @import (multiple) "foo.less";
      @import (multiple) "foo.less";
         
      /* 生成后的 CSS */
      .a {
        color: green;
      }
      .a {
        color: green;
      }

    函数

    1. 判断类型

      • isnumber

        判断给定的值 是否 是一个数字。
        
        ```less
        isnumber(#ff0);     // false
        isnumber(blue);     // false
        isnumber("string"); // false
        isnumber(1234);     // true
        isnumber(56px);     // true
        isnumber(7.8%);     // true
        isnumber(keyword);  // false
        isnumber(url(...)); // false
        ```
        
      • iscolor

        > 判断给定的值 是否 是一个颜色。
        
      • isurl

        > 判断给定的值 是否 是一个 url 。
        
    2. 颜色操作

      • saturate

        > 增加一定数值的颜色饱和度。
        
      • lighten

        > 增加一定数值的颜色亮度。
        
      • darken

        > 降低一定数值的颜色亮度。
        
      • fade

        > 给颜色设定一定数值的透明度。
        
      • mix

        > 根据比例混合两种颜色。
        
    3. 数学函数

      • ceil

        > 向上取整。
        
      • floor

        > 向下取整。
        
      • percentage

        > 将浮点数转换为百分比字符串。
        
      • round

        > 四舍五入。
        
      • sqrt

        > 计算一个数的平方根。
        
      • abs

        > 计算数字的绝对值,原样保持单位。
        
      • pow

        > 计算一个数的乘方。
        

    由于 文章 篇幅有限,所以 只能介绍一些 使用效率高的函数。

    如果你想了解更多,可以去官网的函数链接

    其他

    1. 注释

      • /* */ CSS原生注释,会被编译在 CSS 文件中。
      • /   / Less提供的一种注释,不会被编译在 CSS 文件中。
    2. 避免编译
          /* Less */
          #main{
            width:~'calc(300px-30px)';
          }
        
          /* 生成后的 CSS */
          #main{
            width:calc(300px-30px);
          }
      结构: `~' 值 '`
    
    1. 使用 JS

      因为 Less 是由 JS 编写,所以 Less 有一得天独厚的特性:代码中使用 Javascript 。

          /* Less */
          @content:`"aaa".toUpperCase()`;
          #randomColor{
            @randomColor: ~"rgb(`Math.round(Math.random() * 256)`,`Math.round(Math.random() * 256)`,`Math.round(Math.random() * 256)`)";
          }
          #wrap{
            width: ~"`Math.round(Math.random() * 100)`px";
            &:after{
                content:@content;
            }
            height: ~"`window.innerHeight`px";
            alert:~"`alert(1)`";
            #randomColor();
            background-color: @randomColor;
          }
          /* 生成后的 CSS */
        
          // 弹出 1
          #wrap{
            width: 随机值(0~100)px;
            height: 743px;//由电脑而异
            background: 随机颜色;
          }
          #wrap::after{
            content:"AAA";
          }

        前几个月 , 有个 CSS in JS 的概念非常火,现在 看来 JS in CSS 也未曾不可。
    我觉得完全可以根据 Less 这个特性来造个轮子,JS来控制 CSS ,形成 动态属性,如果成功 很可能会改变 现在前端的打开姿势。

    结束语

    原文链接

    (完)

    查看原文

    赞 227 收藏 286 评论 27

    Thinker 赞了回答 · 2019-01-25

    微信小程序,怎么图片渲染不出来

    data-original="{{detail.user_info.avtor}}"

    关注 4 回答 2

    Thinker 发布了文章 · 2019-01-23

    列表渲染 wx:key 的作用、条件渲染 wx:if 与 hidden 的区别

    这是微信小程序踩坑系列的第三篇,想要了解更多关于微信小程序开发的那些事,欢迎关注我的《微信小程序》专栏。


    前言

    开发微信小程序离不开“页面渲染”,对于初学者来说很难理解小程序里的“页面渲染”是什么、怎么用?
    而学过 vue 的同学来说,这个就比较熟悉了,实际上就是数据绑定页面渲染。
    那么关于页面渲染最重要的是列表渲染和条件渲染这两块,先来看看几个简单的例子。

    下面是个“列表渲染”的例子:

    <view wx:for="{{array}}">
      {{index}}: {{item.message}}
    </view>
    Page({
      data: {
        array: [{
          message: 'foo',
        }, {
          message: 'bar'
        }]
      }
    })

    上面的例子可以看出,默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item。当然,使用 wx:for-item 可以指定数组当前元素的变量名,使用 wx:for-index 可以指定数组当前下标的变量名,如下:

    <view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">
      {{idx}}: {{itemName.message}}
    </view>

    下面是个“条件渲染”的例子:

    <view wx:if="{{condition}}">True</view>
    Page({
      data: {
        condition: true
      }
    })

    上面的例子说明,当 condition 为真时,页面渲染上面的 view 标签。当然也可以用 wx:elif 和 wx:else 来添加一个 else 块,如下:

    <view wx:if="{{length > 5}}">1</view>
    <view wx:elif="{{length > 2}}">2</view>
    <view wx:else>3</view>

    下面接入正题,探索文章题目的疑问

    一、 列表渲染中的 wx:key 有什么作用

    其实初次看 官方文档 可能会对 wx:key 有点懵,官方解释是这样的:

    clipboard.png

    根据我多年看文档经验,一般我看不懂的可以忽略不重要的文字,只需关注重点,例如上图的文字加粗部分,因此,一开始我选择不写 wx:key 这个属性。然而在开发过程中写多了列表渲染(而没有加 wx:key)之后,控制台会报很多的 wx:key 的警告,对于有点代码洁癖的我看起来很不爽,但又苦于不清楚 wx:key 的真正作用,于是自创了一个解决办法,那就是在每个列表渲染后面加上 wx:key="{{index}}",类似下面这样:

    <view wx:for="{{array}}" wx:key="{{index}}">
      {{item}}
    </view>

    于是我惊奇地发现警告统统不见了,也没有其他负面影响,于是我就这样用了大半年。
    然而,半年前我做的一个项目里面有个列表渲染需要试试获取用户头像和昵称,于是我之前的做法不管用了,每次获取到的用户信息跟当前内容不对应,并且会发生错乱。于是我重新理解了一遍 wx:key,结合下面的例子,我似乎明白了:

    <switch wx:for="{{objectArray}}" wx:key="unique" style="display: block;">
      {{item.id}}
    </switch>
    Page({
      data: {
        objectArray: [
          {id: 5, unique: 'unique_5'},
          {id: 4, unique: 'unique_4'},
          {id: 3, unique: 'unique_3'},
          {id: 2, unique: 'unique_2'},
          {id: 1, unique: 'unique_1'},
          {id: 0, unique: 'unique_0'},
        ]
      }
    })

    其实,wx:key 是用来绑定当前列表中的项目特征的,也就是说,如果列表是动态更新的,那么 wx:key 的作用是保持原有项目的整个状态不变。
    结合上面的例子,我们可以知道,对于列表数组是个对象数组,那么 wx:key 属性直接写对应的唯一的属性名就可以了,比如上面的 wx:key="unique", 或者 wx:key="id" 也是可以的,只要保持属性是唯一值就行了,有点类似页面标签里面的 id 属性在页面是唯一的。
    对于列表数组是个基本类型数组,那么直接写 wx:key="*this" 就可以了,如下:

    <block wx:for="{{[1, 2, 3]}}" wx:key="*this">
      <view>{{index}}:</view>
      <view>{{item}}</view>
    </block>

    巧用 wx:key 属性

    • 如果很明确自己的列表渲染是个静态列表,那么你可以像我一开始那样做,加个 wx:key="{{index}}" 就可以了
    • 反之,如果是个动态列表,那么就得在数组里找到唯一的键值,放在 wx:key 里面
    • 当然如果你无视警告,也不影响功能,不加也行

    二、 wx:if 和 hidden 有什么区别

    其实我们用条件渲染更多地在用 wx:if 而不是 hidden,因为前者可以拓展,后者缺乏一定的逻辑。然而他们到底有什么区别呢?
    官方文档 是这样描述的:

    clipboard.png

    上图中,我们大概可以了解到,如果需要频繁切换状态,用 hidden,否则用 wx:if。
    也就是说,wx:if 能够实时创建渲染组件或销毁组件,而且当他为真时才会创建,初始为假时什么也不做,由真变为假时则进行销毁。所以频繁切换他是一个比较耗性能举动。而 hidden 则代表页面初始渲染时就会把该组件渲染在页面上,值的真假只是控制其显示隐藏罢了。页面不销毁,则该组件也不会被销毁。
    明白了这一点,你会发现,从我们开发者的角度来说,灵活使用这两个条件判断会事半功倍。
    下面列举几种使用场景给开发者参考:

    <view class="load-event" hidden="{{!isAdd}}">加载中……</view>
    <view class="load-event" hidden="{{!isAdded && isMore}}">没有更多了</view>

    上面代码是一个上拉加载动画显示与隐藏组件,可以看到用的是 hidden,因为他是一个需要频繁切换的组件。

    <block wx:if="{{node.name === 'p'}}">
      <view class="{{node.attrs.class}}" data-index="{{index}}" bindtap="toText">
        <text selectable="true">{{node.children[0].text}}</text>
      </view>
    </block>
    <block wx:if="{{node.name === 'img'}}">
      <image class="{{node.attrs.class}}" data-original="{{node.attrs.src}}"></image>
    </block>
    <block wx:if="{{node.name === 'video'}}">
      <video class="{{node.attrs.class}}" data-original="{{node.attrs.src}}"></video>
    </block>

    上面代码展示的是渲染文字还是图片或者是视频,只展示其中的一个那么用 wx:if 最佳。

    下面是一个自定义 input 组件:

    <view wx:if="{{isInput}}" class="show-input">
      <view class="input-item">
        <input class="item-input" bindconfirm="onSend" bindinput="inputHandle" bindblur="hideInput"></input>
        <view class="send-btn" catchtap="onSend">发送</view>
      </view>
    </view>

    其功能是点击评论按钮能实时显示输入框,否则隐藏。这里为什么用 wx:if 呢?因为我希望它显示时是新的 input 组件,不是之前渲染好的,这样如果我刚输入完文字,再次评论不会出现上一次的文字残留。

    巧用 wx:if 和 hidden

    • 有时我们需要提前渲染好里面的子组件,那么要用 hidden,否则待显示时需要加上渲染的时间
    • 通常情况下,我在隐藏的时候都不需要该组件的话,那就用 wx:if
    • 如果需要在页面中点击切换的渲染,那么考虑小程序性能问题,还是用 hidden 为好

    三、思考(引伸)

    1、 <block> 这个元素在列表和条件渲染上是很好用的,不过要注意不要在这个标签上绑定其他属性,比如 data- 或者绑定事件 bindtap。下面是一个反例:

    <block wx:if="{{true}}" data-id="1" bindtap="tapName">
      <view>view1</view>
      <view>view2</view>
    </block>

    上面的代码里,在 js 中定义绑定事件后,你会发现不会执行。原因就在 <block> 元素在渲染页面后并不会存在,他不是个组件,不会渲染在页面树节点里面,所以在他上面绑定的事件或者属性也不会有效。

    2、 当 wx:for 的值为字符串时,会将字符串解析成字符串数组;另外,花括号和引号之间如果有空格,将最终被解析成为字符串,请看下面的例子:

    <view wx:for="array">
      {{item}}
    </view>

    等同于

    <view wx:for="{{['a','r','r','a','y']}}">
      {{item}}
    </view>
    <view wx:for="{{[1,2,3]}} ">
      {{item}}
    </view>

    等同于

    <view wx:for="{{[1,2,3] + ' '}}">
      {{item}}
    </view>

    四、参考链接

    (完)

    本文作者 Thinker

    本文如有错误之处,请留言,我会及时更正

    觉得对您有帮助的话就点个赞收藏吧!

    欢迎转载或分享,转载时请注明出处

    阅读原文

    查看原文

    赞 1 收藏 1 评论 1

    Thinker 发布了文章 · 2019-01-22

    微信小程序脚本语言 WXS 怎么用

    这是微信小程序踩坑系列的第二篇,想要了解更多关于微信小程序开发的那些事,欢迎关注我的《微信小程序》专栏。


    前言

    前几天有个同学问我 微信小程序支持管道过滤器 吗?
    用过 angular 或者 vue 的同学都应该会在项目里用到 filter,然而在小程序中是不支持的。但是也有一些解决办法,有一篇文章讲得还算不错,我在这里贴一下 微信小程序 使用filter过滤器几种方式
    但我只是关心 WXS 能不能实现 filter 以及还能做什么?带着这样的疑问,我重新看了一遍微信小程序官方的 WXS。

    clipboard.png

    下面举个简单的例子:

    <wxs module="m1">var msg = "hello world"; module.exports.message = msg;</wxs>
    <view>{{m1.message}}</view>

    上面的例子可以输出 hello world 页面,当你阅读完 官方文档,会发现小程序的脚本语言的功能很捉鳖,比如只支持 es5 语法,不支持外部引入 js 等等。但是我仍然期待它未来支持更多的能力。

    下面接入正题,探索文章题目的疑问

    一、用 WXS 实现 filter

    前端通常有一个需求,那就是把后台传过来的时间戳转为不同规格的日期后显示出来。以往的做法一般是用一个函数对数据进行包装,然后输出到页面。就像前面提到的那篇文章里面所说的第一种方法一样,但是有人考虑到性能问题,认为在js里面循环处理比较耗性能(这点我不做评价,毕竟自己没有真正测试过)
    关于日期格式化的例子在前面提到的文章已经有了,在这里我再举一个比较简单的例子。在我开发过的项目里面,后台返回的网路图片地址通常是相对地址,也就是说要把图片显示出来,还得加上配置好的域名前缀。而我通常是拿到数据后进行遍历操作,把需要前端展示的图片加上前缀。但是有了 WXS,我们可以这样做:

    <wxs module="filter">
        function getFullPath(url) {
            return "https://shiyuanjieyi.cn" + url
        }
        module.exports.getFullPath = getFullPath
    </wxs>
    <image data-original="{{filter.getFullPath(url)}}"></image>

    在上面这个例子中,可以看到整个过程基本类似于 vue 等框架自定义 filter 的做法。

    二、 WXS 还能做什么

    其实很多时候,我们并不了解 WXS 还能做更多条件渲染的一些东西。请看下面一个例子:

    <wxs module="filter">
      function getData(entry, type) {
        var imgUrl = '';
        var content = '';
        switch (entry) {
          case 'needs':
            imgUrl = '/images/goods_empty.png';
            content = '暂时没有需求';
            break;
          case 'goods':
            imgUrl = '/images/goods_empty.png';
            content = '暂时没有商品';
            break;
          case 'activity':
            imgUrl = '/images/activity_empty.png';
            content = '该专栏暂时没有活动';
            break;
          case 'channel':
            imgUrl = '/images/article_empty.png';
            content = '该专栏暂时没有资讯';
            break;
          case 'micro-circle':
            imgUrl = '/images/article_empty.png';
            content = '没有相关的话题哦';
            break;
          case 'needs-release':
            imgUrl = '/images/goods_release_empty.png';
            content = '你还没有发布任何需求哦';
            break;
          case 'goods-release':
            imgUrl = '/images/goods_release_empty.png';
            content = '你还没有发布任何商品哦';
            break;
          case 'goods-collection':
            imgUrl = '/images/goods_collect_empty.png';
            content = '你还没有收藏任何商品哦';
            break;
          case 'apply':
            imgUrl = '/images/activity_apply_empty.png';
            content = '你还没有报名任何活动哦';
            break;
          case 'activity-collection':
            imgUrl = '/images/activity_collect_empty.png';
            content = '你还没有收藏任何活动哦';
            break;
          default:
            break;
        }
        if (type === 'image') {
          return imgUrl;
        } else {
          return content;
        }
      }
      module.exports.getData = getData;
    </wxs>
    <template name="nodata">
      <view class="no-data">
        <image data-original="{{filter.getData(entry, 'image')}}" class="no-data-icon"></image>
        <view class="no-data-text">{{filter.getData(entry, 'content')}}</view>
      </view>
    </template>

    上例中,我使用了 WXS 的函数功能帮我做条件判断,拿到对应的模板输出,其功能就是一个空数据展示页面。或许你会问这样写的好处是什么?
    那我可以告诉你,它很容易扩展,比如新增一个页面需要对应的空数据模板,那么直接在 switch 语句中加多一个 case 即可。况且如果把逻辑写在 WXML 上代码会很复杂,不容易看懂。
    明白了这一点,你会发现,只要是在 WXML 上需要做一些逻辑判断的操作都可以 WXS 代替。
    也就是说,在开发中,我们都可以用 WXS 的函数功能帮助我们清晰有效地处理 WXML 上渲染的一些视图。

    三、思考(引伸)

    1、 对于需要做成 filter 形式的 WXS,最好把它写在一个.wxs文件里,需要使用时,直接在对应 WXML 上引用即可。

    var foo = "'hello world' from tools.wxs";
    var bar = function (d) {
      return d;
    }
    module.exports = {
      FOO: foo,
      bar: bar,
    };
    module.exports.msg = "some msg";
    <wxs data-original="./../tools.wxs" module="tools" />
    <view>{{tools.msg}}</view>
    <view>{{tools.bar(tools.FOO)}}</view>

    2、 在 .wxs 模块中引用其他 wxs 文件模块,可以使用 require 函数,但是不能引用其他 js 文件模块。

    四、参考链接

    (完)

    本文作者 Thinker

    本文如有错误之处,请留言,我会及时更正

    觉得对您有帮助的话就点个赞收藏吧!

    欢迎转载或分享,转载时请注明出处

    阅读原文

    查看原文

    赞 2 收藏 2 评论 0

    Thinker 关注了问题 · 2019-01-19

    如何解决微信小程序对于轮播图中高清图片加载很慢问题

    题目描述

    在微信小程序开发当中,我们难免会遇到需要渲染比较大的高清图,但是加载特别慢,并且会出现打印机那样一行一行出来的效果,体验不太好。
    我已经对静态资源加CDN了,但第一次加载依旧会很慢

    题目来源及自己的思路

    我想过几种方案:
    1.监听图片加载完成事件,等加载完成才显示整个图片,但是用了才发现图片加载监听事件并不能保证图片完全加载完毕才触发,所以效果不明显
    2.用骨架图先代替原图,但是问题还是没办法准确监听图片什么时候才能完整显示

    我希望做过比较大的项目或者有类似经验的大佬指导一下,我认为这也是性能优化的一个问题
    自己还是个个人开发者,开发小项目比较多,关于大项目的一些性能优化问题基本不了解
    因此,我想趁此机会学习一下前端性能优化的知识,请各位大佬指导一下

    关注 5 回答 4

    Thinker 提出了问题 · 2019-01-18

    如何解决微信小程序对于轮播图中高清图片加载很慢问题

    题目描述

    在微信小程序开发当中,我们难免会遇到需要渲染比较大的高清图,但是加载特别慢,并且会出现打印机那样一行一行出来的效果,体验不太好。
    我已经对静态资源加CDN了,但第一次加载依旧会很慢

    题目来源及自己的思路

    我想过几种方案:
    1.监听图片加载完成事件,等加载完成才显示整个图片,但是用了才发现图片加载监听事件并不能保证图片完全加载完毕才触发,所以效果不明显
    2.用骨架图先代替原图,但是问题还是没办法准确监听图片什么时候才能完整显示

    我希望做过比较大的项目或者有类似经验的大佬指导一下,我认为这也是性能优化的一个问题
    自己还是个个人开发者,开发小项目比较多,关于大项目的一些性能优化问题基本不了解
    因此,我想趁此机会学习一下前端性能优化的知识,请各位大佬指导一下

    关注 5 回答 4