Tuesday, February 22, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


组员重构代码千奇百怪,直接JS、ES6和Vue规范给一梭子

Posted: 21 Feb 2022 04:00 PM PST

前言

近期组员接手了一个"古老"的初始由后端大佬写的前端项目,业务层面的组件复用,全靠是 copy 相同代码咱不说,经过不同大佬们的维护,代码风格更是千奇百怪。该前端项目还在正常迭代更新,又不可能重写,面对 💩 一样的代码,两个接手的小前端抱着欲哭无泪,瑟瑟发抖。见状,只能安慰之,暂时发挥啊 Q 精神,规范自己的新代码,然后每次迭代开发任务重构一两个旧组件,此过程持续 2-3 个月后,上 eslint 和 prettier 自动化检测语法和格式化代码。
本着"代码不规范,新人两行泪"的警示,总结出如下 JavaScrip、ES6 和 Vue 单文件组件相关代码风格案例,供大家参考。

Javascript 代码风格

使用有意义的变量名称

变量的名称应该是可描述,有意义的, JavaScript 变量都应该采用驼峰式大小写 ( camelCase) 命名。

// bad ❌ const foo = 'JDoe@example.com' const bar = 'John' const age = 23 const qux = true  // good ✅ const email = 'John@example.com' const firstName = 'John' const age = 23 const isActive = true

布尔变量通常需要回答特定问题,例如:

isActive didSubscribe hasLinkedAccount

避免添加不必要的上下文

当对象或类已经包含了上下文的命名时,不要再向变量名称添加冗余的上下文。

// bad ❌ const user = {   userId: '296e2589-7b33-400a-b762-007b730c8e6d',   userEmail: 'JDoe@example.com',   userFirstName: 'John',   userLastName: 'Doe',   userAge: 23 }  user.userId  //good ✅ const user = {   id: '296e2589-7b33-400a-b762-007b730c8e6d',   email: 'JDoe@example.com',   firstName: 'John',   lastName: 'Doe',   age: 23 }  user.id

避免硬编码值

// bad ❌ setTimeout(clearSessionData, 900000)  //good ✅ const SESSION_DURATION_MS = 15 * 60 * 1000  setTimeout(clearSessionData, SESSION_DURATION_MS)

使用有意义的函数名称

函数名称需要描述函数的实际作用,即使很长也没关系。函数名称通常使用动词,但返回布尔值的函数可能是个例外 — 它可以采用 是或否 问题的形式,函数名也应该是驼峰式的。

// bad ❌ function toggle() {   // ... }  function agreed(user) {   // ... }  //good ✅ function toggleThemeSwitcher() {   // ... }  function didAgreeToAllTerms(user) {   // ... }

限制参数的数量

尽管这条规则可能有争议,但函数最好是有 3 个以下参数。如果参数较多可能是以下两种情况之一:

  • 该函数做的事情太多,应该拆分。
  • 传递给函数的数据以某种方式相关,可以作为专用数据结构传递。
// bad ❌ function sendPushNotification(title, message, image, isSilent, delayMs) {   // ... }  sendPushNotification('New Message', '...', 'http://...', false, 1000)  //good ✅ function sendPushNotification({ title, message, image, isSilent, delayMs }) {   // ... }  const notificationConfig = {   title: 'New Message',   message: '...',   image: 'http://...',   isSilent: false,   delayMs: 1000 }  sendPushNotification(notificationConfig)

避免在一个函数中做太多事情

一个函数应该一次做一件事,这有助于减少函数的大小和复杂性,使测试、调试和重构更容易。

// bad ❌ function pingUsers(users) {   users.forEach((user) => {     const userRecord = database.lookup(user)     if (!userRecord.isActive()) {       ping(user)     }   }) }  //good ✅ function pingInactiveUsers(users) {   users.filter(!isUserActive).forEach(ping) }  function isUserActive(user) {   const userRecord = database.lookup(user)   return userRecord.isActive() }

避免使用布尔标志作为参数

函数含有布尔标志的参数意味这个函数是可以被简化的。

// bad ❌ function createFile(name, isPublic) {   if (isPublic) {     fs.create(`./public/${name}`)   } else {     fs.create(name)   } }  //good ✅ function createFile(name) {   fs.create(name) }  function createPublicFile(name) {   createFile(`./public/${name}`) }

避免写重复的代码

如果你写了重复的代码,每次有逻辑改变,你都需要改动多个位置。

// bad ❌ function renderCarsList(cars) {   cars.forEach((car) => {     const price = car.getPrice()     const make = car.getMake()     const brand = car.getBrand()     const nbOfDoors = car.getNbOfDoors()      render({ price, make, brand, nbOfDoors })   }) }  function renderMotorcyclesList(motorcycles) {   motorcycles.forEach((motorcycle) => {     const price = motorcycle.getPrice()     const make = motorcycle.getMake()     const brand = motorcycle.getBrand()     const seatHeight = motorcycle.getSeatHeight()      render({ price, make, brand, nbOfDoors })   }) }  //good ✅ function renderVehiclesList(vehicles) {   vehicles.forEach((vehicle) => {     const price = vehicle.getPrice()     const make = vehicle.getMake()     const brand = vehicle.getBrand()      const data = { price, make, brand }      switch (vehicle.type) {       case 'car':         data.nbOfDoors = vehicle.getNbOfDoors()         break       case 'motorcycle':         data.seatHeight = vehicle.getSeatHeight()         break     }      render(data)   }) }

避免副作用

JavaScript 中,你应该更喜欢函数式模式而不是命令式模式。换句话说,大多数情况下我们都应该保持函数纯洁。副作用可能会修改共享状态和资源,从而导致一些奇怪的问题。所有的副作用都应该集中管理,例如你需要更改全局变量或修改文件,可以专门写一个 util 来做这件事。

// bad ❌ let date = '21-8-2021'  function splitIntoDayMonthYear() {   date = date.split('-') }  splitIntoDayMonthYear()  // Another function could be expecting date as a string console.log(date) // ['21', '8', '2021'];  //good ✅ function splitIntoDayMonthYear(date) {   return date.split('-') }  const date = '21-8-2021' const newDate = splitIntoDayMonthYear(date)  // Original vlaue is intact console.log(date) // '21-8-2021'; console.log(newDate) // ['21', '8', '2021'];

另外,如果你将一个可变值传递给函数,你应该直接克隆一个新值返回,而不是直接改变该它。

// bad ❌ function enrollStudentInCourse(course, student) {   course.push({ student, enrollmentDate: Date.now() }) }  //good ✅ function enrollStudentInCourse(course, student) {   return [...course, { student, enrollmentDate: Date.now() }] }

使用非负条件

// bad ❌ function isUserNotVerified(user) {   // ... }  if (!isUserNotVerified(user)) {   // ... }  //good ✅ function isUserVerified(user) {   // ... }  if (isUserVerified(user)) {   // ... }

尽可能使用简写

// bad ❌ if (isActive === true) {   // ... }  if (firstName !== '' && firstName !== null && firstName !== undefined) {   // ... }  const isUserEligible = user.isVerified() && user.didSubscribe() ? true : false  //good ✅ if (isActive) {   // ... }  if (!!firstName) {   // ... }  const isUserEligible = user.isVerified() && user.didSubscribe()

避免过多分支

尽早 return 会使你的代码线性化、更具可读性且不那么复杂。

// bad ❌ function addUserService(db, user) {   if (!db) {     if (!db.isConnected()) {       if (!user) {         return db.insert('users', user)       } else {         throw new Error('No user')       }     } else {       throw new Error('No database connection')     }   } else {     throw new Error('No database')   } }  //good ✅ function addUserService(db, user) {   if (!db) throw new Error('No database')   if (!db.isConnected()) throw new Error('No database connection')   if (!user) throw new Error('No user')    return db.insert('users', user) }

优先使用 map 而不是 switch 语句

既能减少复杂度又能提升性能。

// bad ❌ const getColorByStatus = (status) => {   switch (status) {     case 'success':       return 'green'     case 'failure':       return 'red'     case 'warning':       return 'yellow'     case 'loading':     default:       return 'blue'   } }  //good ✅ const statusColors = {   success: 'green',   failure: 'red',   warning: 'yellow',   loading: 'blue' }  const getColorByStatus = (status) => statusColors[status] || 'blue'

使用可选链接

const user = {   email: 'JDoe@example.com',   billing: {     iban: '...',     swift: '...',     address: {       street: 'Some Street Name',       state: 'CA'     }   } }  // bad ❌ const email = (user && user.email) || 'N/A' const street = (user && user.billing && user.billing.address && user.billing.address.street) || 'N/A' const state = (user && user.billing && user.billing.address && user.billing.address.state) || 'N/A'  //good ✅ const email = user?.email ?? 'N/A' const street = user?.billing?.address?.street ?? 'N/A' const street = user?.billing?.address?.state ?? 'N/A'

避免回调

回调很混乱,会导致代码嵌套过深,使用 Promise 替代回调。

// bad ❌ getUser(function (err, user) {   getProfile(user, function (err, profile) {     getAccount(profile, function (err, account) {       getReports(account, function (err, reports) {         sendStatistics(reports, function (err) {           console.error(err)         })       })     })   }) })  //good ✅ getUser()   .then(getProfile)   .then(getAccount)   .then(getReports)   .then(sendStatistics)   .catch((err) => console.error(err))  // or using Async/Await ✅✅  async function sendUserStatistics() {   try {     const user = await getUser()     const profile = await getProfile(user)     const account = await getAccount(profile)     const reports = await getReports(account)     return sendStatistics(reports)   } catch (e) {     console.error(err)   } }

处理抛出的错误和 reject 的 promise

// bad ❌ try {   // Possible erronous code } catch (e) {   console.log(e) }  //good ✅ try {   // Possible erronous code } catch (e) {   // Follow the most applicable (or all):   // 1- More suitable than console.log   console.error(e)    // 2- Notify user if applicable   alertUserOfError(e)    // 3- Report to server   reportErrorToServer(e)    // 4- Use a custom error handler   throw new CustomError(e) }

只注释业务逻辑

// bad ❌ function generateHash(str) {   // Hash variable   let hash = 0    // Get the length of the string   let length = str.length    // If the string is empty return   if (!length) {     return hash   }    // Loop through every character in the string   for (let i = 0; i < length; i++) {     // Get character code.     const char = str.charCodeAt(i)      // Make the hash     hash = (hash << 5) - hash + char      // Convert to 32-bit integer     hash &= hash   } }  // good ✅ function generateHash(str) {   let hash = 0   let length = str.length   if (!length) {     return hash   }    for (let i = 0; i < length; i++) {     const char = str.charCodeAt(i)     hash = (hash << 5) - hash + char     hash = hash & hash // Convert to 32bit integer   }   return hash }

ES6 优化原生 JS(ES5) 代码风格

使用默认参数

// bad ❌ function printAllFilesInDirectory(dir) {   const directory = dir || './'   //   ... }  // good ✅ function printAllFilesInDirectory(dir = './') {   // ... }

对象结构取值

const obj = {   a: 1,   b: 2,   c: 3,   d: 4,   e: 5 }  // bad ❌ const f = obj.a + obj.d const g = obj.c + obj.e  // good ✅ const { a, b, c, d, e } = obj const f = a + d const g = c + e

ES6 的解构赋值虽然好用。但是要注意解构的对象不能为 undefinednull。否则会报错,故要给被解构的对象一个默认值。

const { a, b, c, d, e } = obj || {}

拓展运算符合并数据

合并数组或者对象,用 ES5 的写法有些冗余

const a = [1, 2, 3] const b = [1, 5, 6] const obj1 = {   a: 1 } const obj2 = {   b: 1 }  // bad ❌ const c = a.concat(b) //[1,2,3,1,5,6] const obj = Object.assign({}, obj1, obj2) // {a:1, b:1}  // good ✅ const c = [...new Set([...a, ...b])] //[1,2,3,5,6] const obj = { ...obj1, ...obj2 } // {a:1, b:1}

拼接字符

const name = '小明' const score = 59  // bad ❌ let result = '' if (score > 60) {   result = `${name}的考试成绩及格` } else {   result = `${name}的考试成绩不及格` }  // good ✅ const result = `${name}${score > 60 ? '的考试成绩及格' : '的考试成绩不及格'}`

includes 替代多条件判断

// bad ❌ f(     type == 1 ||     type == 2 ||     type == 3 ||     type == 4 || ){    //... }  // good ✅ const condition = [1,2,3,4];  if( condition.includes(type) ){    //... }

列表查找某一项

const a = [1, 2, 3, 4, 5]  // bad ❌ const result = a.filter((item) => {   return item === 3 })  // good ✅ const result = a.find((item) => {   return item === 3 })

数组扁平化

// bad ❌ const deps = {   采购部: [1, 2, 3],   人事部: [5, 8, 12],   行政部: [5, 14, 79],   运输部: [3, 64, 105] } let member = [] for (let item in deps) {   const value = deps[item]   if (Array.isArray(value)) {     member = [...member, ...value]   } } member = [...new Set(member)]  // good ✅ const member = Object.values(deps).flat(Infinity)

可选链操作符获取对象属性值

// bad ❌ const name = obj && obj.name  // good ✅ const name = obj?.name

动态对象属性名

// bad ❌ let obj = {} let index = 1 let key = `topic${index}` obj[key] = '话题内容'  // good ✅ obj[`topic${index}`] = '话题内容'

判断非空

// bad ❌ if (value !== null && value !== undefined && value !== '') {   //... }  // good ✅ if ((value ?? '') !== '') {   //... }

Vue 组件风格

Vue 单文件组件风格指南内容节选自 Vue 官方风格指南

组件数据

组件的 data 必须是一个函数。

// bad export default {   data: {     foo: 'bar'   } };  // good export default {   data() {     return {       foo: 'bar'     };   } };

单文件组件文件名称

单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)。

// bad mycomponent.vue myComponent.vue  // good my - component.vue MyComponent.vue

紧密耦合的组件名

和父组件紧密耦合的子组件应该以父组件名作为前缀命名。

// bad components/ |- TodoList.vue |- TodoItem.vue └─ TodoButton.vue  // good components/ |- TodoList.vue |- TodoListItem.vue └─ TodoListItemButton.vue

自闭合组件

在单文件组件中没有内容的组件应该是自闭合的。

<!-- bad --> <my-component></my-component>  <!-- good --> <my-component />

Prop 名大小写

在声明 prop 的时候,其命名应该始终使用 camelCase,而在模板中应该始终使用 kebab-case。

// bad export default {   props: {     'greeting-text': String   } };  // good export default {   props: {     greetingText: String   } };
<!-- bad --> <welcome-message greetingText="hi" />  <!-- good --> <welcome-message greeting-text="hi" />

指令缩写

指令缩写,用 : 表示 v-bind: ,用 @ 表示 v-on:

<!-- bad --> <input v-bind:value="value" v-on:input="onInput" />  <!-- good --> <input :value="value" @input="onInput" />

Props 顺序

标签的 Props 应该有统一的顺序,依次为指令、属性和事件。

<my-component   v-if="if"   v-show="show"   v-model="value"   ref="ref"   :key="key"   :text="text"   @input="onInput"   @change="onChange" />

组件选项的顺序

组件选项应该有统一的顺序。

export default {   name: '',    components: {},    props: {},    emits: [],    setup() {},    data() {},    computed: {},    watch: {},    created() {},    mounted() {},    unmounted() {},    methods: {} }

组件选项中的空行

组件选项较多时,建议在属性之间添加空行。

export default {   computed: {     formattedValue() {       // ...     },      styles() {       // ...     }   },    methods: {     onInput() {       // ...     },      onChange() {       // ...     }   } }

单文件组件顶级标签的顺序

单文件组件应该总是让顶级标签的顺序保持一致,且标签之间留有空行。

<template> ... </template>  <script>   /* ... */ </script>  <style>   /* ... */ </style>

种类丰富的材质库,让开发者建模轻松高效

Posted: 15 Feb 2022 05:23 PM PST

材质,是图形学中描述光线如何在物体表面和内部进行交互的一种性质,由材质模型和一组控制参数来定义表面外观。如下图所示,左边的图添加各种材料的纹理后,真实感立显。材质贴图目前已经广泛应用在游戏、建筑、设计等领域。

在材质制作和应用的过程中,用户在纹理采集制作、贴图效果处理等方面都面临一些问题和痛点,如制作过程耗时费力,成本高,且用户可能还需要花费大量时间和精力去寻找合适的贴图资源等等。

如何解决这些痛点呢?我们提供三种思路:一是利用深度学习推理网络,一键生成符合PBR标准的纹理要素,提升纹理制作效率和质量;二是将技术美术的经验和制作规范固化为遵循PBR标准的数据,通过数据在不同项目和渲染器间复用素材,传承美术制作经验;三是充分结合上述两种思路,基于HMS Core 3D建模服务的材质生成能力,建立种类丰富的材质库,避免用户把时间浪费在寻找材质贴图上,通过材质库的高效筛选和应用让用户的模型更加美观。

一、材质生成能力

HMS Core 3D建模服务的材质生成能力,可将RGB图像转换为PBR材质,用户只需输入RGB图片,便可一键生成包括diffuse map, normal map, specular map, roughness map,height map的五种贴图类型。材质类型包括混凝土、大理石、岩石、碎石、砖、石膏、黏土、金属、木材、树皮、皮革、织物、漆面、塑料、合成材料等,并且目前已经能支持1k-8k贴图的输出,帮助开发者生成更符合实际需要的材质。

二、种类丰富、高清、快速、好用的材质库

基于强大的材质生成能力,3D建模服务为用户提供种类丰富的材质库。材质库能够依据用户需求筛选场景和种类并提供PBR材质贴图,且支持查询、预览和下载等功能,方便用户根据需要高效使用,提高工作效率。用户只需在AGC(AppGallery Connect)开通3D建模服务,进入"我的项目",点击"构建 > 3D建模服务",选择"材质库",便可进入材质库页面。

1、节约时间成本,快速精准获取材质资源

在首页提供筛选搜索功能,在搜索框输入要检索的材质名称,点击查询即可获取相关材质。

点击分辨率、场景可筛选符合条件的材质。

应用场景选择。

筛选出符合指定条件的材质,可以灵活选择默认按上传时间或按预览量进行排序,帮助用户迅速高效地筛选出想要获取的材质。

2、1k-4k的高清材质贴图,想用哪里点哪里

很多墙体、造型、木制,都需要用材质来表达结构、空间、预期效果等等。模型加入材质库的高清材质贴图后,可以看出明显差距。

点击图片即可进行材质预览,目前预览提供该材质的球体、正方体和平面三种形态的预览效果,并支持鼠标拖拽,环绕展示。

3、30000+海量材质,种类丰富,持续更新,可直接下载

目前材质库提供覆盖16个种类的32066个材质,如果该材质满足用户需求,可在预览页面点击右下角直接下载。下载结果为包含材质贴图的zip压缩包,解压后输出为jpg格式,可直接使用,方便快捷。后续材质库也会根据用户需求不断更新升级,敬请期待。

了解更多详情>>

访问华为开发者联盟官网
获取开发指导文档
华为移动服务开源仓库地址:GitHubGitee

关注我们,第一时间了解 HMS Core 最新技术资讯~

疑难杂症:运用 transform 导致文本模糊的现象探究

Posted: 20 Feb 2022 06:40 PM PST

在我们的页面中,经常会出现这样的问题,一块区域内的文本或者边框,在展示的时候,变得特别的模糊,如下(数据经过脱敏处理):

正常而言,应该是这样的:

emmm,可能大图不是很明显,我们取一细节对比,就非常直观了:

何时触发这种现象?

那么?什么时候会触发这种问题呢?在 Google 上,其实我们能搜到非常多类似的案例,总结而言:

  1. 当文本元素的某个祖先容器存在 transform: translate() 或者 transform: scale()transform 操作时,容易出现这种问题

当然,这只是必要条件,不是充分条件。继续深入探究,会发现,必须还得同时满足一些其它条件:

  1. 元素作用了 transform: translate() 或者 transform: scale() 后的计算值产生了非整数

譬如,上述案例触发的 CSS 代码如下:

.container {     position: absolute;     width: 1104px;      height: 475px;     top: 50%;     transform: translateY(-50%);     // ... }

由于元素的高度为 475pxtranslateY(-50%) 等于 237.5px,非整数,才导致了内部的字体模糊。

但是,需要注意的是,并非所有产生的非整数都会导致了内部的字体模糊。

这里有个简单的示意:

还是上述的例子,当高度从 477px 一直调整到 469px 的过程中,只有 477px475px 导致了模糊,而 473, 471, 469 则没有。所以,这也只是引发模糊的一个必要条件。

  1. 文本内容是否模糊还与屏幕有关,高清屏(dpr > 2)下不容易触发,更多发生在普通屏幕下(dpr = 1)

在我实测的过程中还发现,这个现象基本只会发生在 dpr 为 1 的普通屏幕下。

类似于 MAC 的高清屏幕则不太会触发这个问题。

dpr = 物理像素 / 设备独立像素,表示设备像素比。这个与我们通常说的视网膜屏(多倍屏,Retina屏)有关。设备像素比描述的是未缩放状态下,物理像素和设备独立像素的初始比例关系。
  1. 并非所有浏览器都是这个表现,基本发生在 chromium 内核。

为何发生这种现象呢?

那么,为何会发生这种现象?针对这个问题,没有找到特别官方的回答,普遍的认为是因为:

由于浏览器将图层拆分到 GPU 以进行 3D 转换,而非整数的像素偏移,使得 Chrome 在字体渲染的时候,不是那么的精确

关于这个问题,感兴趣的可以再看看这两个讨论:

如何解决?

那么针对这个问题,我们该如何解决呢?社区里给出的一种方案:

  1. 给元素设置 -webkit-font-smoothing: antialiased

font-smooth CSS 属性用来控制字体渲染时的平滑效果,该特性是非标准的,我们应该尽量不要在生产环境中使用它。并且在我的实测中,这个方法不太奏效。

  1. 保证运用了 transform: translate() 或者 transform: scale() 的元素的高宽为偶数

如果你赋予给元素的 transform 的值非常明确,譬如我上文例子中的利用其来对元素进行水平垂直居中 -- transform: translate(-50%, -50%),让元素的高宽为偶数这个方法是可行的,但如果当你无法确定transform 的值,譬如 transform: translateX(-31.24%) 或者是 transform: scale(1.05),那这个方法依旧无法奏效。

  1. 弃用 transform

如果这个问题对你的页面非常致命,那么只能弃用 transform,寻找替代方案。大部分的时候,我们还是可以找到不使用 transform 的替代方案的。

总结一下,本文简单探究了在 Chromium 内核下,使用了 transform 导致内部文本模糊的现象,并且给出了一些可尝试的解决方案,实际遇到,需要多加调试,尝试最优的解决方案。

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

vscode语音注释, 让信息更丰富(中)

Posted: 20 Feb 2022 03:43 AM PST

vscode语音注释, 让信息更丰富 (中)

前言

     上一篇我们做完了最基础的功能"识别语音注释", 本篇我们要一起开发语音'播放'等相关功能。

一、mac电脑获取音频文件(后期有坑,到时会填)

     要开发音频'播放'功能, 那我们首先需要一份音频文件, 上网找mp3文件下载多数都需要注册, 那索性直接使用电脑自带的录音功能生成mp3就好了,这种方式有bug后期我们再解决。

这里演示mac电脑的录音功能:

第一步: 找到软件:

image.png

第二步: 将录制好的音频分享到某个app上

image.png

image.png

image.png

m4a文件: (我们可以手动修改后缀名)

"m4a是MPEG-4音频标准文件的扩展名,与大家熟悉的mp3一样,也是一种音频格式文件,苹果公司用此命名来区分mpeg4视频。"

二、播放音频插件的选择

     这里的播放指的是"鼠标悬停"即可播放音频, 那么就不能是web意义上的播放, 因为我们无法利用audio标签实现, vscode是基于Electron开发的, 所以其内的插件也是处于node环境里的, 那我们就可以利用node音频流输入到音频输出设备从而达到播放的目的。

     在网上用node播放音频的插件不多, 在这里推荐其中两款:play.js&node-wav-player

  1. play.js: github地址
  2. node-wav-player: github地址

play.js有个瑕疵, 就是无法暂停播放这个问题可能是开发者无法忍受的, 所以我最终选择了node-wav-player, 别看它叫wav播放器, mp3也是能播的。

安装走起:
yarn add 
使用播放:(这里暂时使用绝对地址)

在`hover.ts文件内添加:

import * as vscode from 'vscode'; import * as player from 'node-wav-player'; import { getVoiceAnnotationDirPath, targetName, testTargetReg } from './util' let stopFn: () => void;  function playVoice(id: string) {     const voiceAnnotationDirPath = getVoiceAnnotationDirPath()     if (voiceAnnotationDirPath) {         player.play({             path: `/xxx/xxxx/xx.mp3`         }).catch(() => {             vscode.window.showErrorMessage('播放失败')         })         stopFn = () => {             player.stop()         }     } }  export default vscode.languages.registerHoverProvider("*", {     provideHover(documennt: vscode.TextDocument, position: vscode.Position) {         stopFn?.()         const word = documennt.getText(documennt.getWordRangeAtPosition(position));         const testTargetRes = testTargetReg.exec(word);         if (testTargetRes) {             playVoice(testTargetRes[1])             return new vscode.Hover('播放中 ...')         }     } }) 

     player.playpath 播放地址暂时写死, 你会发现当前可以正常播放音频, 如果此时你认为播放功能ok了那就大错特错了。

三、node-wav-player的核心原理

     node-wav-player的代码十分简易, 远比我想象的简练, 下面都是我将代码化简后的样子, 是不是清爽很多:

初始化的play方法, 只负责整理数据, 真正播放是靠_play方法

image.png

_play方法

image.png
    node child_process.spawn 用来启动一个新的'子进程', 这个就是用来启动音频播放的'子进程' 第一个参数是命令语句, 第二个参数是数组的话就是执行命令的位置。

     spawn的使用方法我演示一下:
image.png

     比如说 afplay 音频地址 就可以在mac上面播放声音。

监听报错

image.png

如果不是code0亦或是this._called_stop === true人为手动调用停止的情况, 则报错"播放失败", 如果500毫秒内并未报错, 则removeAllListeners("close")移除关闭的监听。

如何终止播放

     在stop方法中直接kill掉'子进程'即可:
image.png

四、'何处'录制音频?

     我们做这个插件的体验宗旨就是方便快捷, 所以录制音频的"链路一定要短", 最好用户一键就可以进行'录制', 一键就可以生成音频注释。

     我最开始的想法是尽可能在vscode内部完成, 也就是不要新开一个 h5页面, 让用户的不要越出vscode这个层级。

     录制音频就不能像播放音频一样, 因为录制涉及到录音结束后的重播, 音频文件的保存, 以及开始+暂停+结束等等状态的操作, 所以最好要有个操作界面而不是靠node单打独斗。

五、webview

创建webview

     vscode内部提供了webview的能力, 看到它的第一眼我就'心动'了, 我们使用下面的代码就可以增加一个webview页。

  const panel = vscode.window.createWebviewPanel(     "类型xxx",     "标题xxx",     vscode.ViewColumn.One,     {}   );

image.png

定义内容

     需要用到panel.webview.html属性, 类似innerHTML:

  const panel = vscode.window.createWebviewPanel(     "类型xxx",     "标题xxx",     vscode.ViewColumn.One,     {}   );   panel.webview.html = `<div>123</div>`;

image.png

局限性

     阅读了官方文档也查看了ts类型文件, 但是遗憾没能发现为音频授权的方法, 所以导致无法使用audio标签来采集到用户的音频信息, 只能选择换种方式实现。

六、右键录音

     我参考了一些音乐播放软件, 发现大家播放功能几乎都是通过打开h5页面实现的, 那咱们的录音功能也可以尝试这种方式, 原理当然是利用node启动一个web服务, 然后帮助用户打开类似http://localhost:8830/这种地址, 这个地址返回给用户一段html, 这里就是录音的地方。

定义右键导航

package.json文件内增加

  "contributes": {     "menus": {       "editor/context": [         {           "when": "editorFocus",           "command": "vn.recording",           "group": "navigation"         }       ]     },     "commands": [       {         "command": "vn.recording",         "title": "此工程内录制语音注释"       }     ] }
  1. editor/context里面定义了右键呼出的菜单栏的内容。
  2. when在什么生命周期激活这个功能定义, 这里选择了当获得编辑焦点时。
  3. command定义了命令名称。
  4. title就是显示在菜单中的名称。

image.png

打开h5页面

extension.ts新增navigation模块:

import * as vscode from 'vscode'; import hover from './hover'; import initVoiceAnnotationStyle from './initVoiceAnnotationStyle'; import navigation from './navigation' // 新增  export function activate(context: vscode.ExtensionContext) {     initVoiceAnnotationStyle()     context.subscriptions.push(hover);     context.subscriptions.push(navigation); // 新增     context.subscriptions.push(         vscode.window.onDidChangeActiveTextEditor(() => {             initVoiceAnnotationStyle()         })     ) }  export function deactivate() { } 

navigation,ts文件, 负责启动服务并且打开浏览器跳到对应页面:

yarn add open
import * as vscode from 'vscode'; import * as open from 'open'; import server from './server'; import { serverProt } from './util'; import { Server } from 'http';  let serverObj: Server; export default vscode.commands.registerCommand("vn.recording", function () {     const voiceAnnotationDirPath = getVoiceAnnotationDirPath()     if (voiceAnnotationDirPath) {         if (!serverObj) {             serverObj = server()         }         open(`http://127.0.0.1:${serverProt()}`);     } })
启动server

     因为咱们的插件要尽可能的小, 这里当然不使用任何框架, 手撸原生即可:

新建server.ts文件:

import * as fs from 'fs'; import * as http from 'http'; import * as path from 'path'; import * as url from 'url'; import { targetName, getVoiceID } from './util';  export default function () {     const server = http.createServer(function (       req: http.IncomingMessage, res: http.ServerResponse) {             res.write(123)             res.end()     }).listen(8830)      return server }

七、返回页面, 定义api

     server光启动不行, 现在开始定义接口能力, 在server.ts内:

import * as fs from 'fs'; import * as http from 'http'; import * as path from 'path'; import * as url from 'url'; import { serverProt, targetName, getVoiceID } from './util';  const temp = fs.readFileSync(     path.join(__dirname, "./index.html") )  export default function () {     const server = http.createServer(function (req: http.IncomingMessage, res: http.ServerResponse) {         if (req.method === "POST" && req.url === "/create_voice") {             createVoice(req, res)         }else {             res.writeHead(200, {                 "content-type": 'text/html;charset="fs.unwatchFile-8"'             })             res.write(temp)             res.end()         }     }).listen(serverProt())      return server }
  1. src/html/index.html文件是我们的录音的h5界面文件。
  2. 我们定义上传为"POST"请求, 并且请求地址为/create_voice
createVoice方法

     此方法用于接收音频文件, 并将音频文件保存在用户指定的位置:

 function createVoice(req: http.IncomingMessage, res: http.ServerResponse) {     let data: Uint8Array[] = [];     req.on("data", (chunck: Uint8Array) => {         data.push(chunck)     })     req.on("end", () => {         let buffer = Buffer.concat(data);         const voiceId = getVoiceID()         try {             fs.writeFileSync(`保存音频的位置`,                 buffer,             )         } catch (error) {             res.writeHead(200)             res.end()         }         res.writeHead(200)         res.end(JSON.stringify({ voiceId: `// ${targetName}_${voiceId}` }))     }) } 
  1. 因为前端会使用formData的形式进行音频文件的传递, 所以需要这种接收方式。
  2. 将最后生成的这种// voice_annotation_20220220153713111音频注释字符串返回给前端, 方便前端直接放入用户剪切板。

end

     接下来是音频的录制与上传(涉及到webRTC相关知识) , 以及如何定义储存音频文件的路径, 并且附加vscode插件的发布, 这次就是这样, 希望与你一起进步。

urlcat:JavaScript的URL构建器库

Posted: 16 Feb 2022 05:24 AM PST

urlcat 是一个小型的 JavaScript 库,它使构建 URL 非常方便并防止常见错误。

特性:

  • 友好的 API
  • 无依赖
  • 压缩后0.8KB大小
  • 提供TypeScript类型

为什么用?

在调用 HTTP API 时,通常需要在 URL 中添加动态参数:

const API_URL = 'https://api.example.com/';  function getUserPosts(id, blogId, limit, offset) {   const requestUrl = `${API_URL}/users/${id}/blogs/${blogId}/posts?limit=${limit}&offset=${offset}`;   // send HTTP request }

正如你所看到的,这个最小的例子已经很难阅读了。这也是不正确的:

  • 我忘记了 API_URL 常量末尾有一个斜杠,所以这导致了一个包含重复斜杠的 URL(https://api.example.com//users)
  • 嵌入的值需要使用 encodeURIComponent 进行转义

我可以使用内置的 URL 类来防止重复的斜杠和 URLSearchParams 来转义查询字符串。但我仍然需要手动转义所有路径参数。

const API_URL = 'https://api.example.com/';  function getUserPosts(id, blogId, limit, offset) {   const escapedId = encodeURIComponent(id);   const escapedBlogId = encodeURIComponent(blogId);   const path = `/users/${escapedId}/blogs/${escapedBlogId}`;   const url = new URL(path, API_URL);   url.search = new URLSearchParams({ limit, offset });   const requestUrl = url.href;   // send HTTP request }

如此简单的任务,却又很难读,写也很乏味!这是这个小型库可以帮助您的地方:

const API_URL = 'https://api.example.com/';  function getUserPosts(id, limit, offset) {   const requestUrl = urlcat(API_URL, '/users/:id/posts', { id, limit, offset });   // send HTTP request }

这个库会这样处理:

  • 转义所有参数
  • 将所有部分连接起来(它们之间总是正好有一个 /?

如何使用?

目前,该软件包通过 npm 分发。 (Zip 下载和 CDN 即将推出)。

npm install --save urlcat

在Node.js中使用

官方支持 Node 10 及更高版本。由于代码在内部使用 URL 和 URLSearchParams 类,它们在 v10 以下不可用,因此我们无法支持这些版本。

要构建完整的 URL(最常见的用例):

const urlcat = require('urlcat').default;

要使用任何一个实用函数:

const { query, subst, join } = require('urlcat');

要使用所有导出的函数:

const { default: urlcat, query, subst, join } = require('urlcat');

在Typescript中使用

官方支持 TypeScript 2.1 及更高版本。

要构建完整的 URL(最常见的用例):

import urlcat from 'urlcat';

要使用任何一个实用函数:

import { query, subst, join } from 'urlcat';

要使用所有导出的函数:

import urlcat, { query, subst, join } from 'urlcat';

在Deno中使用

import urlcat from 'https://deno.land/x/urlcat/src/index.ts';  console.log(urlcat('https://api.foo.com', ':name', { id: 25, name: 'knpwrs' }));

API

ParamMap:具有字符串键的对象

例如,{ firstParam: 1, 'second-param': 2 } 是一个有效的 ParamMap。

urlcat:构建完整的 URL

function urlcat(baseUrl: string, pathTemplate: string, params: ParamMap): string function urlcat(baseUrl: string, pathTemplate: string): string function urlcat(baseTemplate: string, params: ParamMap): string

例如:

  • urlcat('https://api.example.com', '/users/:id/posts', { id: 123, limit: 10, offset: 120 })
    → 'https://api.example.com/users/123/posts?limit=10&offset=120'
  • urlcat('http://example.com/', '/posts/:title', { title: 'Letters & "Special" Characters' })
    → 'http://example.com/posts/Letters%20%26%20%22Special%22%20Characters'
  • urlcat('https://api.example.com', '/users')
    → 'https://api.example.com/users'
  • urlcat('https://api.example.com/', '/users')
    → 'https://api.example.com/users'
  • urlcat('http://example.com/', '/users/:userId/posts/:postId/comments', { userId: 123, postId: 987, authorId: 456, limit: 10, offset: 120 })
    → 'http://example.com/users/123/posts/987/comments?authorId=456&limit=10&offset=120'

query:构建查询字符串

使用指定的键值对构建查询字符串。键和值被转义,然后由 '&' 字符连接。

例如:

paramsresult
{}''
{ query: 'some text' }'query=some%20text'
{ id: 42, 'comment-id': 86 }'id=42&comment-id=86'
{ id: 42, 'a name': 'a value' }'id=42&a%20name=a%20value'

subst:替换路径参数

用模板字符串中的值替换参数。模板可能包含 0 个或多个参数占位符。占位符以冒号 (:) 开头,后跟只能包含大写或小写字母的参数名称。在模板中找到的任何占位符都将替换为 params 中相应键下的值。

例如

templateparamsresult
':id'{ id: 42 }'42'
'/users/:id'{ id: 42 }'/users/42'
'/users/:id/comments/:commentId'{ id: 42, commentId: 86 }'/users/42/comments/86'
'/users/:id'{ id: 42, foo: 'bar' }'/users/42'

join:使用一个分隔符连接两个字符串

仅使用一个分隔符连接两个部分。如果分隔符出现在 part1 的末尾或 part2 的开头,则将其删除,然后使用分隔符连接两个部分。

例如:

part1separatorpart2result
'first'',''second''first,second'
'first,'',''second'
'first'','',second'
'first,'','',second'

Github库地址:https://github.com/balazsboto...

怎么说服领导,能让我用DDD架构肝项目?

Posted: 20 Feb 2022 06:14 PM PST

作者:小傅哥
博客:https://bugstack.cn
原文:https://mp.weixin.qq.com/s/ezd-6xkRiNfPH1lGwhLd8Q

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

领导:为什么要使用DDD?

我也苦思冥想,怎么跟领导说咱们从 MVC 升级到 DDD 吧,因为 DDD 代码结构更加清晰、领域驱动比测试驱动开发更加先进、研发的兄弟们也更想用用新框架等。

不过这么聊被喷一顿不说,还得说你是过度设计瞎折腾,咋回事呢?因为没聊到重点呀,你MVC升级DDD;给业务带来了什么提升了交付效率吗降低了公司研发成本吗,都没有?不仅没有,你还说为了后期的迭代维护,前期会需要更多的设计和开发时间。咋?你是想这一个Q就把我送走吗,我刚来咱们部门KPI在那悬着,压的我头发都白了!别瞎搞,求稳!

那就不搞了吗?搞哇,不让搞换领导!但搞之前,要考虑清楚,DDD 不是 Silver Bullet,你有一腔热血虽好,可是也得知晓 DDD 的设计原则是什么、它更适合的场景是什么、与 MVC 对比有什么云泥之别。

二、开发成本

使用 DDD 模式开发代码的成本到底在哪?是因为使用 DDD 四层分层结构就比MVC 三层分层结构 更浪费时间吗?其实并不是,因为四层结构相对于三层结构,反而更好的区分了代码所属职责,在熟悉模块功能职责后,开发起来也会更加顺畅。

那这里的 DDD 领域驱动设计开发的成本在哪呢?这个成本在于对于一个复杂系统又尚未在开发前期就有非常充足的经验来拆分职责边界划分功能领域明确编排逻辑对未知流程扩展的把控上,所带来的风暴模型设计成本

而通常使用的 MVC 结构基本不会出现这样的问题,因为在实际的代码中,DAO、PO、VO等都是共用的,大家在开发代码的时候,像堆泥球一样面向过程写代码,直接串联出产品的PRD功能节点即可,不用过多的思考解耦和内聚。

那不是可以设计模式吗,这就需要看你是站在哪个维度去思考问题。设计模式在这里是战术问题的,DDD和MVC是确定战略问题的,有点像是说:"方向不对,努力白费一样"

那么现在我们再来看这条开发成本曲线:

架构模式,开发成本曲线

  • 与其他两种分层结构相对比,使用 DDD 的时候,需要在前期投入较多的时间成本来设计领域建模,所以前期成本会更高一些。
  • 但随着业务不断迭代后的逻辑的复杂性增加,DDD 系统架构所开发的代码稳定性会更好,也就说明 DDD 更容易扩容和维护。
  • 所以框架结构的更换,不是最终增加开发成本的地方,如果你不做领域建模也不做更多的设计思考,那么即使是 DDD 的四层架构,也能让你写出 MVC 的效果。而那些对业务场景经验丰富的架构师或者研发人员,已经非常明确了各个业务功能的职责边界,要实现一个系统需求需要完成哪些核心领域服务,再这样的情况用 DDD 也不会带来多少开发成本,反而更加游刃有余了!这就是为什么说,需要领域专家,因为专家已经积累了很多的战略设计经验
  • 此外使用 DDD 领域驱动设计的模式进行开发,除了解决需求的迭代成本,更多的时候是要面对公司战略调整后,系统的交接、人员的更替和新增,都要在原有的工程架构下继续迭代开发,否则就要推翻重新做,那样所面临的更替成本将更大,同时又是开发了一个与人员绑定不易于交接维护的工程代码。

三、架构对比

在了解和掌握 DDD 领域驱动设计的路上,你一定会碰到两个抽象的钉子 —— "贫血模型"、"充血模型":

  • 贫血模型:事务脚本模式,最早起源于 EJB2,到 Spring 进入开"春"盛世。
  • 充血模型:领域模型模式,2003年提出,一直到《实现领域驱动设计》的问世,才开启了 DDD 的大门。但国内直到微服务、低代码的兴起,才开始 DDD 热

1. MVC

MVC 分层结构将:"状态"(数据,成员对象)、"行为"(逻辑、过程),分离到不同的对象中,只有状态的对象(VO -> Value Object) 被称为贫血模型,只有行为的对象,就是框架分层中常见的Logic/Service/Manager层(对应到EJB2中的Stateless Session Bean)

MVC 分层结构

  • 以应用层 Service 使用 DAO、PO 基础设施包装业务逻辑的开发方式,乍一看以为应用层是在对领域建模的实现,"领域层"有着丰富的对象链接,和真正的领域模型也非常类似,但当我们代码随着业务功能逻辑的逐步实现中会慢慢发现,我们写了一堆的 get/set 对象,而他们被反复交叉使用,没有与任何领域聚合,也就是不具有任何的行为动作,只是一堆贫血模型对象。
  • 这种反模式的设计,其实完全与面向对象的设计是背道而驰的,面向对象的设计更希望行为和数据绑定在一起,与之对比的贫血模型更像是面向过程设计。
  • 在 MVC 分层结构下,所有的行为都被写入到 Service 对象中,最终你会得到一组事务处理的过程脚本,从而完美的避开了领域模型设计所带来的好处(清晰的职责边界、聚合的功能服务、清晰的面向对象)。

2. DDD

DDD 的分层结构也是面向对象编程的本质:"一个对象拥有行为和数据",在领域层包括了:对象、聚合对象、仓储和Service实现。

DDD 分层结构

  • DDD 的分层结构更注重 Domain 领域层的实现,由很薄的应用层定义接口和编排接口,由领域层做具体的实现。
  • 所有的业务逻辑都按照各自的职责边界拆分成一块块的功能领域,每一个功能领域都是充血模型的结构的具体实现。
  • 那么这样的代码最终实现以后,无论在迭代、维护、人员更替,都能很好按照领域设计文档找到对应的代码实现进行开发。

四、设计原则

首先 DDD 的设计分为战略和战术;

  • 战略设计:从业务视角出发,建立业务领域模型、划分职责边界,建立通用语言的界限上下文。顶层战略设计构建的领域模型结构,是整个服务后期编排的重点,它确定了功能的职责边界、聚合、对象等,也就绝对了后期服务战术实现的开发和交付质量。重视战略,才能落地好战术!
  • 战术设计:从技术视角出发,侧重于领域模型的技术实现,完成功能开发和交付落地。领域设计的重点包括:实体、聚合对象、值对象、领域服务、仓储,还有一个非常重点的设计模式。任何一个较为复杂的领域模型实现都需要考虑设计模式的使用,否则即使战略优秀,战术也能干回 MVC 去。

在以DDD领域驱动设计落地的过程中,要依靠领域驱动设计的设计思想,通过事件风暴建立领域模型,合理划分领域逻辑和物理边界,建立领域对象及服务矩阵和服务架构图,定义符合DDD分层架构思想的代码结构模型,保证业务模型与代码模型的一致性。通过上述设计思想、方法和过程,指导团队按照DDD设计思想完成微服务设计和开发。

  1. 拒绝泥球小单体、拒绝污染功能与服务、拒绝加功能排期一个月
  2. 架构出高可用极易符合互联网高速迭代的应用服务
  3. 物料化、组装化、可编排的服务,提高人效
  4. 要领域驱动设计,而不是数据驱动设计,也不是界面驱动设计
  5. 要职能清晰的分层,而不是什么都放的大箩筐

DDD 的领域模型设计,界限内的上下文,可以拆分为独立的微服务。但不仅要从业务视角看问题,也要考虑非业务的技术因素,包括:高性能、安全、团队、技术异构等,这些非业务的技术因素,也会决定领域模型落地的具体落地。

五、举个例子

你说我 MVC 不好,你说我 MVC 贫血模型,PO 类不断的膨胀,但让我用 DDD 又都是理论,程序员更喜欢看的是已经落地的代码,告诉我怎么干。

为什么这么难落地呢?因为从 MVC 过度到 DDD 描述对比只是积累了 MVC 失败的教训,但没有 DDD 成功的经验,所以更多的时候想落地 DDD 除了有理论支撑,更需要一份案例摆在面前。

1. 工程结构

所以为了让更多的码农看到在 DDD 上一条能走的路,专门折腾了个 DDD 分布式抽奖系统,来告诉大家怎么使用 DDD 开发业务需求;

DDD 分布式抽奖系统,工程分布

整体系统架构设计包含了6个工程:

  1. Lottery:分布式部署的抽奖服务系统,提供抽奖业务领域功能,以分布式部署的方式提供 RPC 服务。
  2. Lottery-API:网关API服务,提供;H5 页面抽奖、公众号开发回复消息抽奖。
  3. Lottery-Front:C端用户系统,vue H5 lucky-canvas 大转盘抽奖界面,讲解 vue 工程创建、引入模块、开发接口、跨域访问和功能实现
  4. Lottery-ERP:B端运营系统,满足运营人员对于活动的查询、配置、修改、审核等操作。
  5. DB-Router:分库分表路由组件,开发一个基于 HashMap 核心设计原理,使用哈希散列+扰动函数的方式,把数据散列到多个库表中的组件,并验证使用。
  6. Lottery-Test:测试验证系统,用于测试验证RPC服务、系统功能调用的测试系统。

2. 流程拆解

当我们拿到产品的 RPD 以后,并不是直接上手开发,而是需要从流程中拆解出一份面向对象设计的领域服务,举例;

DDD 分布式抽奖系统,流程拆解

  • 拆解功能流程,提炼领域服务,一步步教会你把一个业务功能流程如何拆解为各个职责边界下的领域模块,在通过把开发好的领域服务在应用层进行串联,提供整个服务链路。
  • 通过这样的设计和落地思想,以及在把流程化的功能按照面向对象的思路使用设计模式进行设计,让每一步代码都变得清晰易懂,这样实现出来的代码也就更加易于维护和扩展了。
  • 所以,你在这个过程中学会的不只是代码开发,还有更多的落地思想实践在这里面体现出来。也能为你以后开发这样的一个项目或者在面试过程中,一些实际复杂场景问题的设计思路,打下不错的基础。

3. 一起实践

如果你对 DDD 实践学习的事情感兴趣,也可以一起加入DDD 分布式抽奖系统的实践,来吸收一份能落地的经验。PS给自己花点钱,做有价值的投资,就当少买个皮肤了

学习链接:https://bugstack.cn/md/project/lottery/introduce/Lottery%E6%8A%BD%E5%A5%96%E7%B3%BB%E7%BB%9F.html

  • 具备 Java 编程基础的研发人员,想提升自己的技术能力
  • 希望提升编码思维,剔除到代码中的坏味道
  • 有意愿成为架构师,但还处在一定瓶颈期
  • 想加入大厂做码农,但总感觉找不到门路

六、总结

  • DDD 并不是 Silver Bullet,你并不能指望换个了个框架结构,就能改变堆屎山⛰似的开发代码,所带来坏味道问题。MVC 结构一样可以开发出好的代码,只是它的稳定性更差,不利于长期维护和迭代。
  • DDD 的复杂性是因为缺少领域建模的经验,如果同一个需求你已经在 MVC 的中嚯嚯的吸收了足够的边界上下文总结,现在换 DDD 可以让你更快的开发代码。
  • DDD 也并不是所有工程模型结构都复杂,DDD 是指导思想,你可以在 DDD 四层架构中因为引入 RPC 拆解各个模块的分层,也可以因业务规模在中等及复杂度时不引入 RPC 框架,这样的 DDD 会更加短小精干,与 MVC 相比只是在领域层定义接口,把代码放到 domain 层做实现,数据放到仓储层处理。参考代码:https://github.com/fuzhengwei/CodeGuide

No comments:

Post a Comment