blog's Issues
记有赞的两道编程题
题目一、生成Mock数据
设计一个生成mock对象的方法
输入为一种mock语法
mock语法长这样
{
'data.user.name': String,
'data.user.age': Number,
'data.isShow': Boolean
}
一条mock规则分为两部分,字段名和数据类型,字段名可嵌套,数据类型支持String
、Number
、Boolean
即可
输出为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
}
这段代码中有两处比较关键的部分
- babel插件的转化究竟做了些什么事儿,组件的依赖是怎么样的形式?
- 组件的realSrc是否真的为我所需要的路径
那么首先先看一下babel插件究竟做了什么
parseComponentsDeps babel插件
首先我在看这份源码的时候对于babel这块的知识是零基础,所以着实废了不少功夫。
在看babel插件之前最好可以先阅览这些资料
- Babel-handbook - 这份资料里面很详细地描述了如何写一个babel插件。
- Babel-types相关 - 这里会涉及到AST节点类型
接下来看一下核心的源码部分,这里声明了一个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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.