Giter Site home page Giter Site logo

blog's People

Contributors

lyh2668 avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar

blog's Issues

记有赞的两道编程题

题目一、生成Mock数据

设计一个生成mock对象的方法
输入为一种mock语法
mock语法长这样

{
    'data.user.name': String,
    'data.user.age': Number,
    'data.isShow': Boolean
}

一条mock规则分为两部分,字段名和数据类型,字段名可嵌套,数据类型支持StringNumberBoolean即可
输出为mock对象,按照mock规则随机生成对象

{
    data: {
        user: {
            name: 'asdfghjkl',
            age: 20
        },
        isShow: true
    }
}

解题思路

  • 先将字符串根据.拆分为数组,然后通过递归数组的方式完成对象的生成
  • 递归过程中,对于不存在的key值生成一个空对象,对于存在的key值则需要做对象的合并,使用Object.assign进行合并
  • 递归到最后一个字段需要根据类型随机生成数据,这里使用typeof type.prototype.valueOf的方式判断类型,随机数据可以先预设一个规则(简单略过)

代码实现

const obj = {
  'data.user.name': String,
  'data.user.age': Number,
  'data.isShow': Boolean
}

let mock = (obj) => {
  let mockObj = {}
  
  for (let key in obj) {
    let keys = key.split('.')
    mockData(mockObj, keys, obj[key])
  }
  return mockObj
}

let mockData = (mockObj, arr, type) => {
  let key = arr[0]
  let tmpObj = {}
  if (arr.length === 1) {
    if (typeof type.prototype.valueOf() === 'string') {
      // string, number, boolean
      // 根据类型随机生成数据 
    }
    // 这里先用123模拟
    tmpObj[key] = '123'
    mockObj = Object.assign(mockObj, tmpObj)
    return mockObj
  }
  if (!(key in mockObj)) {
    mockObj[key] = {}
  }
  tmpObj[key] = mockData(mockObj[key], arr.slice(1, arr.length), type)
  mockObj[key] = Object.assign(mockObj[key], tmpObj[key])
  return mockObj
}
mock(obj)

总结

这题本身思路并不难,难度在于递归和对象的合并,但是这道题目花费了我挺长时间的,需要注意的是几个坑点:

  • codepen & jsbin 的在线编辑器打印对象只会打印两层,导致一直以为代码层面存在的问题
  • 由于Object.assign是浅拷贝,所以需要逐层合并
  • 类型的判断一时还真拿捏不准,如此判断未必是更好的方式

题目二、时间分段

给定一个时间段和步长,枚举该时间段内步长的划分
例如:时间段6:00-9:00,步长为45分钟
那么返回的数组为
['6:00-6:45', '6:45-7:30', '7:30-8:15', '8:15-9:00']

解题思路

这题相对第一题简单的多,略微思考一下就有解题思路了

  • 写一个时间转化为分钟的转换函数
  • 写一个分钟到时间的转换函数
  • 写一个循环,直到当前时间+步进值 > 结束时间为止
  • 注意跨日的处理

代码实现

let time2min = (time) => {
  let arr = time.split(':')
  return arr[0] * 60 + parseInt(arr[1])
}

let min2time = (min) => {
  let hour = parseInt(min / 60)
  let minute = min - hour * 60
  if (hour >= 24) {
    hour = hour - 24
  }
  if (minute < 10) {
    minute = '0' + minute
  }
  if (hour < 10) {
    hour = '0' + hour
  }
  return `${hour}:${minute}`
}

let calStep = (start, end, step) => {
  let startTime = time2min(start)
  let endTime = time2min(end)
  if (startTime > endTime) {
    endTime += 24 * 60
  }
  let arr = []
  while (startTime < endTime) {
    let right = startTime + step > endTime ? endTime : startTime + step
    arr.push(`${min2time(startTime)}-${min2time(right)}`)
    startTime += step
  }
  return arr
}

calStep('22:10', '0:30', 35)

总结

这题由于讲究一个速度,所以没有太多的考虑,从理思路到编写大概用了20分钟时间,所以可能有些写法上还不够严谨。
这题从看到题目的开始我就注意到了可以用转化成分钟来做,然后注意到了可能会有跨天的情况,这个应该算是这道题目的应该坑点吧。

记一次mpvue-loader源码探究

本人技术栈偏向vue一些,所以之前写小程序的时候会考虑使用wepy,但是期间发现用起来有很多问题,然后又没有什么更好的替代品,直到有mpvue的出现,让我眼前一亮,完全意义上的用vue的语法写小程序,赞👍

踩坑之旅

起因

根据官网的文档,可以很迅速的完成quick start,之后很愉快地把自己写的tabbar组件搬了过来,首先先引入组件...

// script
import { LTabbar, LTabbarItem } from '@/components/tabbar'

export default {
  components: {
    LTabbar,
    LTabbarItem
  },
...

// file path
components
    |----tabbar
        |----tabbar.vue
        |----tabbar-item.vue
        |----index.js
...

在vue上很常规的引入方式,然后使用...然后看效果...结果没有任何东西被渲染出来,查看console发现有一条警告

有问题肯定得去解决是吧,然后就开始作死的mpvue源码探究之旅

定位问题

由于是基于实际问题出发的源码探究,所以本质是为了解决问题,那么就得先定位出该问题可能会产生的原因,并带着这个问题去阅读源码。从warning可以很明确的看出,是vue组件转化为wxml时发生的问题,而这件事应当是在loader的时候处理的,所以可以把问题的原因定位到mpvue-loader,先看一眼mpvue-loader的构成

├── component-normalizer.js
├── loader.js // loader入口
├── mp-compiler // mp script解析相关文件夹
│   ├── index.js
│   ├── parse.js // components & config parse babel插件
│   ├── templates.js // vue script部分转化成wxml的template
│   └── util.js // 一些通用方法
├── parser.js // parseComponent & generateSourceMap
├── selector.js
├── style-compiler // 样式解析相关文件夹
├── template-compiler // 模板解析相关文件夹
└── utils

首先找到loader.js这个文件,找到关于script的解析部分,从这里看到调用了一个compileMPScript方法来解析components

  • script参数即为vue单文件的<script></script>包含部分
  • mpOptions mp相关配置参数
  • moduleId 用于模块唯一标识 moduleId = 'data-v-' + genId(filePath, context, options.hashKey)
// line 259
// <script>
output += '/* script */\n'
var script = parts.script
  if (script) {
    // for mp js
    // 需要解析组件的 components 给 wxml 生成用
    script = compileMPScript.call(this, script, mpOptions, moduleId)
...

接下来看一下mp-compiler目录下的compileMPScript具体做了哪些事情

function compileMPScript (script, optioins, moduleId) {
  // 获得babelrc配置
  const babelrc = optioins.globalBabelrc ? optioins.globalBabelrc : path.resolve('./.babelrc')
  // 写了一个parseComponentsDeps babel插件来遍历组件从而获取到组件的依赖(关键)
  const { metadata } = babel.transform(script.content, { extends: babelrc, plugins: [parseComponentsDeps] })

  // metadata: importsMap, components
  const { importsMap, components: originComponents } = metadata
  // 处理子组件的信息
  const components = {}
  if (originComponents) {
    const allP = Object.keys(originComponents).map(k => {
      return new Promise((resolve, reject) => {
        // originComponents[k] 为组件依赖的路径,格式如下: '@/components/xxx'
        // 通过this.resolve得到realSrc
        this.resolve(this.context, originComponents[k], (err, realSrc) => {
          if (err) return reject(err)
          // 将组件名由驼峰转化成中横线形式
          const com = covertCCVar(k)
          // 根据真实路径获取到组件名(关键)
          const comName = getCompNameBySrc(realSrc)
          components[com] = { src: comName, name: comName }
          resolve()
        })
      })
    })
    Promise.all(allP)
      .then(res => {
        components.isCompleted = true
      })
      .catch(err => {
        console.error(err)
        components.isCompleted = true
      })
  } else {
    components.isCompleted = true
  }

  const fileInfo = resolveTarget(this.resourcePath, optioins.mpInfo)
  cacheFileInfo(this.resourcePath, fileInfo, { importsMap, components, moduleId })
  
  return script
}

这段代码中有两处比较关键的部分

  1. babel插件的转化究竟做了些什么事儿,组件的依赖是怎么样的形式?
  2. 组件的realSrc是否真的为我所需要的路径
    那么首先先看一下babel插件究竟做了什么

parseComponentsDeps babel插件

首先我在看这份源码的时候对于babel这块的知识是零基础,所以着实废了不少功夫。
在看babel插件之前最好可以先阅览这些资料

接下来看一下核心的源码部分,这里声明了一个components访问者:

Visitors(访问者)
当我们谈及“进入”一个节点,实际上是说我们在访问它们,
之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。.

访问者是一个用于 AST 遍历的跨语言的模式。
简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法

// components 的遍历器
const componentsVisitor = {
  ExportDefaultDeclaration: function (path) {
    path.traverse(traverseComponentsVisitor)
  }
}

traverseComponentsVisitor里面主要是对结构的一个解析,最后获取到importsMap,然后组装成一个components对象并返回

// 解析 components
const traverseComponentsVisitor = {
  Property: function (path) {
    // 只对类型为components的进行操作
    if (path.node.key.name !== 'components') {
      return
    }
    path.stop()

    const { metadata } = path.hub.file
    const { importsMap } = getImportsMap(metadata)

    // 找到所有的 imports
    const { properties } = path.node.value
    const components = {}
    properties.forEach(p => {
      const k = p.key.name || p.key.value
      const v = p.value.name || p.value.value

      components[k] = importsMap[v]
      // Example: components = { Card: '@/components/card' } 
    })

    metadata.components = components
  }
}

对于import Card from '@/components/card'
component就应该为{ Card: '@/components/card' }
对于import { LTabbar, LTabbrItem } from '@/components/tabbar'
则会被解析为{ LTabbar: '@/components/tabbar', LTabbarItem: '@/components/tabbar' }
而我们期望的显然是 { LTabbar: '@/components/tabbar/tabbar', LTabbarItem: '@/components/tabbar/tabbar-item' }

然后我就得到这样一个思路:

  • 从path中解析出LTabbar和LTabbarItem真实的路径,或者关联的部分
  • 找到以后替换这里的importsMap

感觉想法并没有错,但是我花费了大量的精力去解析path最后得出一个结论...解析不出来!!,期间尝试了ImportDeclaration 从中得到过最接近期望的一段path,然而它是被写在LeadingComments这个字段当中的,除非没有办法的办法,否则就不应该通过这个字段去进行正则匹配
然后看了一部分Rollup的Module部分的源码,感觉这个源码写得是真的好,非常清晰。从中的确收获了一些启迪,不过感觉这目前的解析而言没有什么帮助。
既然从babel插件这条路走不通了,所以想着是否可以从其他路试试,然后就到了第二个关键点部分

组件的realSrc

既然在babel组件当中的importsMap不是我真正想要的依赖文件,那究竟依赖文件怎么获取到呢?首先我再compileMPScript里面打印了一下this.resourcePath,得到了以下输出

resource:  /Users/linyiheng/Code/wechat/my-project/src/App.vue
resource:  /Users/linyiheng/Code/wechat/my-project/src/pages/counter/index.vue
resource:  /Users/linyiheng/Code/wechat/my-project/src/pages/index/index.vue
resource:  /Users/linyiheng/Code/wechat/my-project/src/pages/logs/index.vue
resource:  /Users/linyiheng/Code/wechat/my-project/src/components/card.vue
resource:  /Users/linyiheng/Code/wechat/my-project/src/components/tabbar/tabbar.vue
resource:  /Users/linyiheng/Code/wechat/my-project/src/components/tabbar/tabbar-item.vu

这个其实就是文件的一个加载顺序,由于LTabbar、LTabbarItem这两个组件是在pages/index/index.vue被引入的,所以相应的解析操作会被放在这里进行,但是从babel组件无法得到这两个组件的realSrc,那么是否可以从最后加载进来的两个vue组件着手考虑呢,这个resourcePath显然就是我们想要的realSrc
简单的给traverseComponentsVisitor加上这样的一个代码段

// traverseComponentsVisitor
if (path.node.key.name === 'component') {
  path.stop()
  const k = path.node.value.value
  const components = {}
  const { metadata } = path.hub.file
  components[k] = ''
  metadata.components = components
  return
}

然后稍微改造一下this.resolve的处理

// 如果originComponents[k]不存在的情况下,则使用当前的resourcePath
this.resolve(this.context, originComponents[k] || this.resourcePath, (err, 

感觉一切就绪了,尝试发现仍然是不行的,虽然我的确得到了组件的realSrc,但是对于pages/index/index.vue而言,已经完成了wxml模板的输出了,而后面进行的主体是components/tabbar/tabbar.vue和components/tabbar/tabbar-item.vue,显然这个时候是无法输出wxml的。看一下生成Wxml的核心代码

function createWxml (emitWarning, emitError, emitFile, resourcePath, rootComponent, compiled, html) {
  const { pageType, moduleId, components, src } = getFileInfo(resourcePath) || {}
  // 这儿一个黑魔法,和 webpack 约定的规范写法有点偏差!
  if (!pageType || (components && !components.isCompleted)) {
    return setTimeout(createWxml, 20, ...arguments)
  }

  let wxmlContent = ''
  let wxmlSrc = ''

  if (rootComponent) {
    const componentName = getCompNameBySrc(rootComponent)
    wxmlContent = genPageWxml(componentName)
    wxmlSrc = src
  } else {
    // TODO, 这儿传 options 进去
    // {
    //   components: {
    //     'com-a': { src: '../../components/comA$hash', name: 'comA$hash' }
    //   },
    //   pageType: 'component',
    //   name: 'comA$hash',
    //   moduleId: 'moduleId'
    // }
    // 以resourcePath为key值,从cache里面获取到组件名,组件名+hash形式
    const name = getCompNameBySrc(resourcePath)
    const options = { components, pageType, name, moduleId }
    // 将所有的配置相关传入并生成Wxml Content
    wxmlContent = genComponentWxml(compiled, options, emitFile, emitError, emitWarning)
    // wxml的路径
    wxmlSrc = `components/${name}`
  }
  // 上抛
  emitFile(`${wxmlSrc}.wxml`, wxmlContent)
}

这部分代码主要的工作其实就是根据之前获取的组件 & 组件路径相关信息,通过genComponentWxml生成对应的wxml,但是由于没办法一次性拿到realSrc,所以我觉得这里的代码存在着一些小问题,理想的效果应该是完成所有的components解析以后再进行wxml的生成,那么这件问题就迎刃而解了。其实作者用尝试通过components.isCompleted来实现异步加载的问题,但是除非是把所有的compileMPScript给包含在一个Promise里面,否则的话感觉这步操作似乎没有起到作用。(也有可能是我理解不到位)

总结

虽然这个需求并不是优先级很高的一个需求

// 其实只要把 import { LTabbar, LTabbarItem } from '@/components/tabbar' 拆分为以下两段就可以了
import LTabbar from '@/components/tabbar'
import LTabbarItem from '@/components/tabbar-item'

但是从这个需求出发看源码,的确是有发现源码中的一些瑕疵(当然换我我还写不出来...所以还是得支持一下大佬的),顺带也了解了一下Babel插件实现的原理,了解了loader大概的一个实现原理,所以还是收获颇丰的。
经过了那么久时间的尝试我还是没有解决这个问题,说实话我是心有不甘的,我把这次经验整理出来也希望大家能够给我提供一些思路,或是如何解析babel插件,或是如何实现wxml的统一解析,或是还有其他的解决方案。最后希望mpvue能够越来越棒👍

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.