Closed 404LT closed 2 years ago
👍 感谢提交该 Issue,请确保仔细阅读 Issue 模板,RSS 提案模板 或 Feature 提案模板 以便于测试。不符合模板将导致 Issue 被直接关闭。
👍 Thanks for the issue, please follow the Issue template, RSS proposal template or Feature proposal template. Otherwise the issue will be closed.
/test
/juejin/tag/架构
Successfully generated as following:
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
store
获取属性值commit
修改属性值import type { TextComponentProps } from './defaultProps';
// 属性转化成表单 哪个属性使用哪个类型的组件去编辑
export interface PropsToForm {
component: string;
subComponent?: string;
extraProps?: { [key: string]: any };
text?: string;
options?: {
text: string;
value: any;
}[];
initalTransform?: (v: any) => any;
afterTransform?: (v: any) => any;
valueProp?: string;
eventName?: string;
}
// 属性列表转化成表单列表
export type PropsToForms = {
[p in keyof TextComponentProps]?: PropsToForm;
};
// 属性转化成表单的映射表 key:属性 value:使用的组件
export const mapPropsToForms: PropsToForms = {
// 比如: text 属性,使用 a-input 这个组件去编辑
text: {
component: 'a-textarea',
extraProps: {
rows: 3,
},
text: '文本',
afterTransform: (e: any) => e.target.value,
},
fontSize: {
text: '字号',
component: 'a-input-number',
initalTransform: (v: string) => parseInt(v),
afterTransform: (e: any) => (e ? `${e}px` : ''),
},
lineHeight: {
text: '行高',
component: 'a-slider',
extraProps: {
min: 0,
max: 3,
step: 0.1,
},
initalTransform: (v: string) => parseFloat(v),
afterTransform: (e: number) => e.toString(),
},
textAlign: {
component: 'a-radio-group',
subComponent: 'a-radio-button',
text: '对齐',
options: [
{
value: 'left',
text: '左',
},
{
value: 'center',
text: '中',
},
{
value: 'right',
text: '右',
},
],
afterTransform: (e: any) => e.target.value,
},
fontFamily: {
component: 'a-select',
subComponent: 'a-select-option',
text: '字体',
options: [
{
value: '',
text: '无',
},
{
value: '"SimSun","STSong',
text: '宋体',
},
{
value: '"SimHei","STHeiti',
text: '黑体',
},
],
afterTransform: (e: any) => e,
},
};
<template>
<div class="props-table">
<div v-for="(item, index) in finalProps"
class="prop-item"
:key="index">
<span class="label">{{ item.text }}</span>
<div class="prop-component">
<!-- 使用 antd 组件库中的组件 -->
<component v-if="item.valueProp"
:[item.valueProp]="item?.value"
v-bind="item?.extraProps"
v-on="item.events"
:is="item?.component">
<template v-if="item.options">
<component :is="item.subComponent"
v-for="(option, key) in item.options"
:key="key"
:value="option.value">
{{ option.text }}
</component>
</template>
</component>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, computed, defineEmits } from 'vue';
import { mapPropsToForms } from '../propsMap'
import { reduce } from 'lodash-es'
import type { PartialTextComponentProps } from '../defaultProps'
export interface Props {
props: PartialTextComponentProps;
}
export interface FormProps {
component: string;
subComponent?: string;
value: string;
extraProps?: { [key: string]: any };
text?: string;
options?: {
text: string;
value: any;
}[];
initalTransform?: (v: any) => any;
valueProp: string;
eventName: string;
events: { [key: string]: (e: any) => void };
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'change', data: any): void;
}>()
// 获取属性表单映射列表
const finalProps = computed(() => {
return reduce(props.props, (result, value, key) => {
const newKey = key as keyof PartialTextComponentProps
const item = mapPropsToForms[newKey]
if (item) {
const { valueProp = 'value', eventName = 'change', initalTransform, afterTransform } = item
const newItem: FormProps = {
...item,
value: initalTransform ? initalTransform(value) : value,
valueProp,
eventName,
events: {
[eventName]: (e: any) => {
emit('change', { key, value: afterTransform ? afterTransform(e) : e })
}
}
}
result[newKey] = newItem
}
return result
}, {} as { [key: string]: FormProps })
})
</script>
// template
<!-- 右侧组件属性编辑 -->
<a-layout-sider width="300"
style="background:#fff"
class="setting-container">
组件属性
<props-table v-if="currentElement"
@change="handleChange"
:props="currentElement?.props"></props-table>
</a-layout-sider>
// script
// 更新组件的属性值事件
const handleChange = (e: any) => {
store.commit('updateComponent', e)
}
mutations: {
// 更新组件属性
updateComponent(state, { key, value }) {
const updatedComponent = state.components.find(
(c) => c.id === state.currentElement,
);
if (updatedComponent) {
updatedComponent.props[key as keyof TextComponentProps] = value;
}
},
},
选择字体的下拉框可以直接显示当前的字体样式
h
函数接收三个参数
使用 h
函数改写
import { h, VNode } from 'vue';
const fontFamilyArr = [
{
value: '"SimSun","STSong',
text: '宋体',
},
{
value: '"SimHei","STHeiti',
text: '黑体',
},
];
const fontFamilyOptions = fontFamilyArr.map((font) => {
return {
value: font.value,
text: h('span', { style: { fontFamily: font.value } }, font.text),
};
});
// 属性转化成表单的映射表 key:属性 value:使用的组件
export const mapPropsToForms: PropsToForms = {
fontFamily: {
component: 'a-select',
subComponent: 'a-select-option',
text: '字体',
options: [
{
value: '',
text: '无',
},
...fontFamilyOptions,
],
afterTransform: (e: any) => e,
},
};
使用 tsx
改写
const fontFamilyOptions = fontFamilyArr.map((font) => {
return {
value: font.value,
text: (<span style={{ fontFamily: font.text }}>{font.text}</span>) as VNode,
};
});
// \src\components\RenderVnode.ts
import { defineComponent } from 'vue';
const RenderVnode = defineComponent({
props: {
vNode: {
type: [Object, String],
required: true,
},
},
render() {
return this.vNode;
},
});
export default RenderVnode;
// \src\components\PropsTable.vue
<!-- 使用 antd 组件库中的组件 -->
<component v-if="item.valueProp"
:[item.valueProp]="item?.value"
v-bind="item?.extraProps"
v-on="item.events"
:is="item?.component">
<template v-if="item.options">
<component :is="item.subComponent"
v-for="(option, key) in item.options"
:key="key"
:value="option.value">
<render-vnode :vNode="option.text"></render-vnode>
</component>
</template>
</component>
业务组件
vuex store
结构,画布循环展示组件lodash
分离样式属性props
类型style
抽取和点击跳转组件属性对应表单组件的展示和更新
vuex getters
propsTable
将传入的属性渲染为对应的表单组件h
函数以及 vnode
实现字体下拉框实时显示持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
废话只说一句:码字不易求个👍,收藏 === 学会,快行动起来吧!🙇🙇🙇。
import { ref } from 'vue'
import axios from 'axios'
const fileRef = ref(null)
const progress = ref(0);
const handleUpload = () => {
let tempFile = fileRef.value.files[0]
if (tempFile.size > 10 * 1024 * 1024) {
// 文件大小超限了
alert('请上传小于10M的图片');
fileRef.value.value = '' // 清空内容
return
}
let file = new FormData()
file.append('file', tempFile)
<template>
<div class="process">
<div class="process-bar":style="{width:progress+'%}"/>
</div>
</template>
<style lang="scss">
.process{
height: 7px;
color:#ccc;
margin: 10px 70px 0;
border-radius: 100px;
background-color: #CCC;
position: relative;
.process-bar{
height: 7px;
background-color: #1890ff;
border-radius: 100px;
position: absolute;
left: 0;
}
}
</style>
axios({
method: 'post',
url,
data: file,
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: function(progressEvent) {
const complete = parseInt((progressEvent.loaded / progressEvent.total * 100 | 0))
// 上传的进度
progress.value = complete
}
}).then(res => {
// 上传成功后续处理
const { data } = res
if (data.success) {
console.log('上传成功')
}
}).catch(err => {
// 捕获异常并处理
console.log(err)
})
Vue 3 下上传文件并获取时时进度
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const fileRef = ref(null)
const progress = ref(0);
// 上传方法
const handleUpload = () => {
let tempFile = fileRef.value.files[0]
if (tempFile.size > 10 * 1024 * 1024) {
// 文件大小超限了
alert('请上传小于10M的图片');
fileRef.value.value = '' // 清空内容
return
}
let file = new FormData()
file.append('file', tempFile)
fileRef.value.value = '' // 清空内容
axios({
method: 'post',
url,
data: file,
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: function(progressEvent) {
const complete = parseInt((progressEvent.loaded / progressEvent.total * 100 | 0))
// 上传的进度
progress.value = complete
}
}).then(res => {
// 上传成功后续处理
const { data } = res
if (data.success) {
console.log('上传成功')
}
}).catch(err => {
// 捕获异常并处理
console.log(err)
})
upload(forms).then((res: any) => {
console.log(res)
}).catch((err:any)=>{
console.log(err)
});
}
</script>
<template>
<div class="process">
<div class="process-bar":style="{width:progress+'%}"/>
</div>
<input
type="file"
accept="video/*"
ref="fileRef"
@change="handleUpload"
/>
</template>
<style lang="scss">
.process{
height: 7px;
color:#ccc;
margin: 10px 70px 0;
border-radius: 100px;
background-color: #CCC;
position: relative;
.process-bar{
height: 7px;
background-color: #1890ff;
border-radius: 100px;
position: absolute;
left: 0;
}
}
</style>
<div @click="downLoad()">下载</div>
<script setup>
const downLoad = () => {
var params={
name: xxx //额外需要携带的参数
}
Axios.get('/api/export',{
params: params,
responseType: 'blob' //设置responseType字段格式为 blob
}).then(res => {
console.log(res);
// 为blob设置文件类型,这里以.xlsx为例
let blob = new Blob([res], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;"});
// 创建一个临时的url指向blob对象
let url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = '自定义文件名';
a.click();
// 释放这个临时的对象url
window.URL.revokeObjectURL(url);
});
}
</script>
还可以借助 js-file-download
插件完成下载功能
<script setup>
import axios from 'axios'
import fileDownload from 'js-file-download';
const download =()=> {
axios.get('下载地址', {
responseType: 'blob',
}).then(res => {
fileDownload(res.data, '下载的文件名字');
});
}
</script>
]]>持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的6天,点击查看活动详情
这不又到年初了,老板又拉了一大票成长计划(O(JB)KR
),我头脑一热果断选择了一个我最不太熟悉的领域——并发程(多线程/进程)打包。
然而最近业务繁重,根本时间搞,连摸鱼的时间都没有了,咋办?
思虑再三,决定接着写小作文,一点一点磕,磕一点就写一点,这样也有动力,毕竟这种贡(刷)献(存)社(在)区(感)的摸鱼工作还是值得坚持下去的。
前面写的浅羲Vue源码这个大坑还没填完,但是又不得不开一个新坑,我保证这事儿干完接着填 Vue
源码的大坑。
多进程(线程)打包的实现有很多,比如大家都听过的子编译
、happypack
、thread-loader
... happypack 作者对 js
兴趣退散了,最近不咋维护了,所以我们选择了 thread-loader 这个方案。
不能再多说背景了,再多说就被同事发现我了,这个活儿是记名的,否则这番吐槽就要到老板耳朵里面了
thread-loder
是个 loader
,他不处理具体的转换模块到js
的工作,而是把他后面的 loader
扔进一个工作线程池(worker pool)中并发运行,上传送门# thread-loader
运行在 worker pool
中的 loader
是受限制的,主要体现在以下几方面:
loader
不能 通过 this.emitFile
生成一个新文件webpack 文档传送门 # this.emitFileloader
不能使用插件自定义的 loader
方法,所谓插件自定义就是通过插件向 loaderContext
扩展自定义的方法,loaderContext
是 webpack
提供的一个 loader
运行时的上下文对象;loader
同样也无法获取 webpack
打包的配置对象;thread-loader
虽然介绍时使用的是 worker pool
但是,它却不是真正的 worker
线程,而是实实在在的子进程(child_process
);
官方说大约会有 600ms
左右的开销,并且进程间天然隔离,不能共享数据,只推荐在那些耗时比较长的 loader
中使用;
去 github 上克隆下来就行:
$ git clone git@github.com:webpack-contrib/thread-loader.git
大致文件结构如下:
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.js
├── commitlint.config.js
├── example
.....
│ └── webpack.config.js
├── husky.config.js
├── lint-staged.config.js
├── package-lock.json
├── package.json
├── src
│ ├── WorkerError.js
│ ├── WorkerPool.js
│ ├── cjs.js
│ ├── index.js
│ ├── readBuffer.js
│ ├── serializer.js
│ ├── worker.js
│ └── workerPools.js
└── test
├── ....
从 package.json
的 main
字段得知,这个包的入口文件为 dist/cjs.js
但是你会发现,上面的目录中压根没有 dist
目录。。。WHAT
?
此时别着急,这种情况下都是作者在本地打包好,然后用本地文件发包,只不过 git
仓库是忽略掉 dist
目录。这就要求我们看看作者是用啥打包的,一般的不外乎:webpack、esbuild、rollup、babel、snowpack...
打开 package.json
看 scripts
脚本,一般打包命令都写在这里面:
{
"name": "thread-loader",
"main": "dist/cjs.js",
"engines": {
"node": ">= 10.13.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel src -d dist --copy-files"
},
"files": [
"dist"
],
}
所以这个包使用 babel
打包的了,并且是打包一个目录(src
)输出到 dist
目录,并且吧 src
目录下的文件复制到 dist
(--copy-files
),这部分内容属于 babel cli
执行下面的依赖安装和打包命令:
$ npm ci
$ npm run build
这样 dist
目录生成了:
dist 目录下的文件结构:
.
├── WorkerError.js
├── WorkerPool.js
├── cjs.js
├── index.js
├── readBuffer.js
├── serializer.js
├── worker.js
└── workerPools.js
这看起来和 src 下的文件别无二致,为啥还要打包?
打包的原因是作者用 ESModule
开发的,通过 Babel
打包成 CommonJS
;但是我们阅读,就从打包的入口文件(thread-loader/src/index.js
)看起就好了,这里算是分享一个我早期阅读源码的一个疑惑点。
import loaderUtils from 'loader-utils';
import { getPool } from './workerPools';
function pitch() {
// ...
}
function warmup(options, requires) {
// ....
}
export { pitch, warmup }; // eslint-disable-line import/prefer-default-export
在这个入口文件中定义并且导出了两个方法:pitch
、warmup
,我们先忽略方法中的具体逻辑,先说说这个结构。
值得一提的是 pitch
方法,这个名字不是瞎叫的,而是在 webpack
的整个生命周期中有这深远意义的名字;
这需要你了解 webpack
对 loader
的执行的全过程,我在前面的文章中写过一些关于 pitch 的内容。
大致回顾一下,loader
的运行分为两个过程:pitch
和 normal
阶段,所谓 normal
就是实现具体功能的 loader
函数本身了,而 pitch
则是挂载 loader
上的一个特殊方法。
normal
阶段是大家熟知的按照 loader
添加的顺序倒序执行,但是在此之前还有一个 pitching
阶段,这个阶段会按照 loader
的添加顺如逐个执行 loader
的 pitch
方法;
pitch
是越过的意思,上官方文档### 越过 loader(Pitching loader)。
也就是说你声明一个 loader
时,还可以声明一个 loader.pitch
,用于跳过其余的 loader
进入到剩余 loader
的 normal
阶段;
比如我们对一个模块添加了三个 loader: [a, b, c]
,这些 loader
的整个执行过程如下:
a.pitch
b.pitch
c.pitch
request modeule 被拾取成为依赖
c.normal
b.normal
a.normal
那么 pitch
如何发挥作用实现跳过呢?只要让 loader.pitch
方法返回一个非 undefined
的值就可以了。比如 b.pitch
返回了一段新的 request
;
module.exports = function loaderB (content) {
return someSyncOperation(content, this.data.value);
};
// loaderB.pitch
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
return 'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');';
};
此时这些 loader
顺序变成 c.pitch/c.module/b.normal
都被跳过,如下:
a.pitch
b.pitch return 一个新的request
a.normal
结合前面的介绍,thread-loader
会把放在他后面的 loader
放到 worker pool
中并发执行。那么如何截住它后面的 loader
们呢?
正如你所料,thread loader
就是在 pitch loader
中把剩余的 loader
扔到线程池中运行;
本篇小作文开启了一个新坑,本文主要讨论了以下几个问题:
thread-loader
作用;babel
打包 thread-loader
及打包的入口文件;loader.pitch
& normal
以及两者的顺序;thread-loader
利用 pitch
截取后面的 loader
扔到线程池;持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
首先明确一下channel的作用:用于go协程间的通信。
go语言最大的特点就是支持高并发:goroutine和channel是支持高并发的重要组成部分。
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
如果说 goroutine 是Go程序并发的执行体,channel
就是它们之间的连接。channel
是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。
另外要明确知道go的并发哲学,铭记这句原则:用通信来共享内存,而不要用共享内存来通信。
在搞清楚channel的作用之后我们再来研究GoFrame框架(下文简称gf)中gqueue的特点。
队列 gqueue 动态大小的并发安全队列
gqueue也可以设置为固定大小的队列,固定大小时和标准库channel没区别。
简单来说channel实现的功能gqueue也能实现。
gqueue是并发安全的,常用于多个goroutine数据通信且支持动态队列大小的场景
package main
import (
"fmt"
"github.com/gogf/gf/container/gqueue"
"github.com/gogf/gf/os/gtimer"
"time"
)
func main() {
//实例化gqueue
q := gqueue.New()
//数据生产者 每隔1秒想队列写入1条数据
gtimer.SetInterval(time.Second, func() {
nowStr := time.Now().String()
q.Push(nowStr)
})
//3秒后关闭队列
gtimer.SetTimeout(time.Second*3, func() {
fmt.Println("关闭队列")
q.Close()
})
// 消费者 不停的从队列中取值输出到终端中
for {
if v := q.Pop(); v != nil {
fmt.Println("消费者接收:", v)
} else {
break
}
}
}
为什么不用标准库的channel,要用gqueue呢?
gqueue的使用比channel更灵活,channel有队列大小的限制,而gqueue队列支持动态大小
channel的读写性能确实非常高,但是channel创建的时候需要初始化内存,初始化操作效率非常低;而gqueue的创建效率非常高,gqueue是动态创建内存。
gqueue的底层实现是基于glist实现动态大小的特性,在队列满或者队列空时读取数据会产生阻塞。
glist是一个并发安全的链接,支持关闭并发安全的特性,当关闭并发安全的特性时和普通链表无异,在存储和读取数据时,不会产生阻塞。
select{}的作用可以阻止进程销毁
package main
import (
"fmt"
"github.com/gogf/gf/container/gqueue"
"github.com/gogf/gf/os/gtime"
"github.com/gogf/gf/os/gtimer"
"time"
)
func main() {
//实例化队列
queue := gqueue.New()
// 生产者每隔1秒钟向队列写入一条数据
gtimer.SetInterval(time.Second, func() {
queue.Push(gtime.Now().String())
})
//消费者 常驻内存一直接收生产者的数据
for {
select {
case v := <-queue.C: //C是 chan interface{}
if v != nil {
fmt.Println("消费者:", v)
} else {
return
}
}
}
}
如下图所示,select{}可以阻止进程销毁,gtimer一直在生产数据,而for循环中的select一直在消费数据。
通过这篇文章,我们知道了channel的概念和作用。也知道了gqueue的底层实现和特点,以及gqueue和channel的对比。两者各有什么特点。
感谢阅读,欢迎大家三连:点赞、收藏、投币(关注)!!!
货拉拉自研H5离线包SDK,能显著提升H5打开速度,已在多个业务中落地。车型介绍页使用离线包前后打开效果如下:
图1 使用离线包前 | 图2 使用离线包后 |
---|---|
货拉拉自研离线包具有如下特点:
安全可靠:无hook,无私有API,三重降级策略。
容易维护:三层架构模式和模块化设计。
功能完备:功能可配置,数据埋点,开发者工具等功能一应俱全。
开源代码地址:https://github.com/HuolalaTech/HLLOfflineWebVC-iOS
团队介绍:货拉拉移动技术团队https://juejin.cn/user/1768489241815070
Web 技术相信大家非常熟悉,它具有快速迭代发布的天然优势,在货拉拉App中广泛使用,比如金秋拉货节、余额、车型介绍页等,但也存在加载速度慢,体验差等问题。目前优化Web页面体验方案比较多,有Flutter、React Native、小程序、离线包,其中离线包技术对现有H5页面改造成本小,技术最为成熟和稳定。
腾讯开源了离线包方案VasSonic(https://github.com/Tencent/VasSonic),但截至目前,开源代码的iOS工程基于废弃的UIWebview实现,无法用于实际生产。通过继续调研,业界离线包现有实现方案如下:
方案名 | 优点 | 缺点 | 备注 |
---|---|---|---|
加载本地路径 | 简单可靠,无需hook和调用私有API | 有跨域问题,影响cookie和localstorage,H5需做少量改动 | 货拉拉方案 |
请求拦截 | 不修改加载URL,没有跨域问题,且支持网页部分资源离线化,灵活性和兼容性好 | iOS端目前提供的NSURLProtocal和WKSURLSchemehandler拦截方案有缺陷,前期实现成本高 | https://www.jianshu.com/p/8f5e1082f5e0 网易云音乐方案https://zhuanlan.zhihu.com/p/347592487 |
本地Web Server | 兼容性好 | 对客户端耗电和CPU性能有影响 | 暂未发现有公司采用,https://juejin.cn/entry/6844903492537024525 |
Service Worker | 前端兼容性好 | iOS端WKWebView不提供官方支持,实现技术难度大 | 爱奇艺方案https://zhuanlan.zhihu.com/p/148931732 |
上述几种方案,本地搭建Web Server方式对客户端性能影响大,先排除,Service Worker方案由于iOS端不支持,不考虑。资源拦截方案兼容性好,但需要解决WKWebview拦截网络请求难度大的问题,开发和测试工作量大,暂不用。结合实现成本和技术可靠性最后采取加载本地路径的方案。
H5离线包的基本原理是将html、js、css、图片等静态资源打包成压缩包,然后下载到客户端并解压,H5加载时直接从本地读取静态资源文件,减少网络请求,提高速度。加载本地文件路径存在的问题和解决方法:
存在问题 | 解决方法 |
---|---|
cgi请求跨域问题 | 在网关或者后端服务的跨域请求头增加null域支持 |
cookie跨域问题 | 目前静态js中无cookie操作,没有cookie跨域问题 |
localstorage跨域问题 | 暂时不涉及域名隔离问题,如果有需要,采取调用原生的方式解决 |
绝对路径问题离线模式不支持问题 | 改成相对路径 |
如下图所示:
客户端启动后,先去远程配置服务器拉取离线包相关的功能配置,然后检查更新,如果有更新则下载离线包。webview加载时,如果本地缓存命中,则从本地磁盘加载html、js、css、图片等静态资源。
H5离线包和线上H5类似也能进行更新和升级,有三个更新时机:
1)webview容器打开时更新。开启离线包功能的H5页面每次打开时,会去检查对应的离线包是否有更新。如果有更新,则下载离线包到本地,绝大部分场景是下次打开时生效。
2)启动查询离线包更新。对于实时性要求比较高的页面,可配置在启动时更新离线包。
3)长连接推送实时更新。更新实时性要求极高的场景使用。
之前货拉拉webview容器(后续简称WebVC)采用的是两层结构,每个业务方从基础WebVC派生出自己的业务WebVC容器。两层结构在接入离线包时遇到如下问题:
离线包接入方案 | 优点 | 缺点 |
---|---|---|
集成到业务WebVC容器 | 不影响基础WebVC功能,影响范围小 | 通用性差,每个业务WebVC都需要修改,接入成本高。 |
集成到基础WebVC容器 | 业务方接入简单,升级基础WebVC即可 | 基础WebVC会变得臃肿,增加了离线包,调试工具,埋点功能,后续维护成本高。 |
为了既不造成基础容器臃肿,维护成本高,也要方便业务方快速接入,将webview改成了三层结构:
增强WebVC和基础WebVC 功能独立,已分成独立的仓库,由不同的开发维护,维护性大大提高。货拉拉基础WebVC容器代码暂未开源,开源代码中改成简化版实现,可自行定制和修改。
1)查询并行化
目前已有多个H5页面采用离线包,为了提高查询效率,多个业务离线包检查的请求采取并行请求的方式。由于HTTP2支持多路复用,所以查询时共用一个NSURLSession对象,多次查询操作自动复用一个TCP连接,减少建立连接次数。考虑到后端改造成本问题,目前不支持聚合查询,计划在后续版本中完善。
2)下载任务去重
下载过程也是并行进行,可同时下载多个离线包。由于查询操作有多个触发时机,存在重复下载的情况。为了节省流量,采用下载URL作为key,不重复下载正在下载或已下载的文件。
3)启动预下载
大部分离线包查询和下载的时机为打开H5页面时,由于离线包查询、下载、解压总体耗时超过1秒,导致首次打开无法命中离线包。所以货拉拉离线包支持配置部分离线包在启动时检查和下载离线包,提升首次打开H5页面离线包命中率。配置json格式为:
{
"predownloadlist": //选填。需要启动就下载的离线包列表清单
["test-offline"]
}
4)断点续传
移动端网络稳定性不如固定网络,会出现因为无网络而导致下载中断的情况。货拉拉离线包接入了自研的下载SDK,支持断点续传功能,节省了用户下载流量和时长。由于下载SDK代码暂时未开源,离线包对外开源代码中采用简化版本代替,没有实现断点续传功能。
5)解压异步化和串行化
解压过程是CPU和IO密集型操作,放在子线程中处理,有多个文件需要解压时,采用串行队列的方式,一次只解压一个文件,避免同时解压多个文件影响客户端性能。
1)解压操作可靠性设计
文件解压耗时较长(大约30ms),如果程序异常退出可能会出现解压操作完成一半的情况,影响后续离线包功能。所以文件解压操作采取先解压,然后重命名,保证最后得到的文件完整性。同时当离线包正在使用时,一般情况下采取先解压,下次生效的策略,极端情况下立刻生效,但会导致页面强刷,影响用户体验,使用较少。离线包存放时使用tmp、new、cur三个文件夹,不采用版本号命名,简化离线包缓存管理逻辑。解压细节如下:
2)三重降级策略
客户端自动降级。本地没有离线包时,客户端会自动将启用离线包的H5页面降级为线上H5页面。
远程配置降级。可以设置局部降级,即临时将某个使用离线包的H5页面降级为线上,也可设置全局降级,关闭所有页面的离线包功能。配置json为:
{
"switch": 1, //总开关,1开启,0关闭
"disablelist": //选填,配置禁用某个离线包
["act-test"],
}
服务端接口降级。 服务端提供的离线包查询接口可设置将某个页面降级为线上H5,亦可配置客户端更新离线包后强制刷新。
降级策略流程图如下:
3)性能监控
货拉拉对webview的加载成功率,错误码、耗时进行了统计上报,通过监控面板查看。
此外离线包SDK还有离线包下载,请求,解压的耗时、结果数据上报。监控和上报采取的接口扩展方式,接入方根据业务特点选用具体的数据上报SDK。
1)开发者工具
为了方便调试和查看,离线包中包含了开发者工具,仅在Debug模式下才能使用,可查看离线包版本和清除离线包缓存。
2)离线包和URL映射配置化
客户端webview通过URL是否有offweb参数判断该页面走线上请求还是加载本地离线包。URL来源广泛,有客户端硬编码、运营系统下发、JS action调用多种来源,手动修改URL效率太低,因此通过远程配置,自动给指定URL添加离线包参数并命中对应的H5离线包。
配置json格式如下:主要通过host,path配置匹配规则。
{
"rules" :[
{
"host" :[
"test1.zzz.cn" ,
"test2.zzz.cn"
],
"path" :[
"/testapp"
],
"offweb" : "test-offline1"
},
{
"host" :[
"test3.xxx.cn" ,
"test4.xxx.cn"
],
"path" :[
"test/offweb" ,
"test/abc"
],
"offweb" : "test-offline2"
}
]
}
H5离线包经过多个业务上线验证,性能稳定,收益明显。
5.1 加载速度提升
改造完成的H5页面,平均加载速度从2s提升至1s。部分页面详细数据如下:
5.2 加载成功率提升
页面主框架(不考虑动态数据)加载成功率从96%提升到100%。
1)扩大开源范围。断点续传下载SDK,日志SDK后续会考虑开源。
2)单元测试用例完善。增加更多单元测试,保证稳定性。
货拉拉离线包开源地址https://github.com/HuolalaTech/HLLOfflineWebVC-iOS
携程增量更新https://zhuanlan.zhihu.com/p/34125968
Service worker https://zhuanlan.zhihu.com/p/148931732
https://github.com/yangfangkuo/WKWebViewHandleDemo
WKWebView完美网络请求拦截 https://www.jianshu.com/p/7b28cbd8f92a
腾讯vassonic https://blog.csdn.net/oqzuser1587576/article/details/87975943
]]>
class Demo {
//成员变量
public int num = 100;
//静态成员变量
public static int count = 200;
//静态方法
public static void method(){
System.out.println(count);
}
}
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
废话只说一句:码字不易求个👍,收藏 === 学会, 快行动起来吧!🙇🙇🙇。2022.05.30
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。虽然,Axios是个优秀的 HTTP 库,但是,直接在项目中使用并不是那么方便,会存在大量的重复性方法和代码,所以,我们需要对其进行一定程度上的配置封装,减少重复代码,精简调用方式。
本次使用 axios 版本为 ^0.21.4
先设计下我想要这个通用请求能达到什么样的效果:
下面👇是 JS 版本
// src/api/axios.js
import axios from "axios";
import Qs from 'qs'
export const Axios = (url,method='get',params={},headers={})=>{
// 根据 process.env.NODE_ENV 区分状态,切换不同的 baseURL 开发环境使用代理, 生产环境可以直接使用域名全拼
const BaseUrl = process.env.NODE_ENV==='development'? '' : process.env.BASEURL;
let defaultHeaders = {
'Content-Type': 'application/json;charset=UTF-8',
// 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', // 指定提交方式为表单提交 或上传
// 'Content-Type' :'multipart/form-data;charset=UTF-8',
'Accept': 'application/json', // 通过头指定,获取的数据类型是JSON 'application/json, text/plain, */*',
// 'Access-Control-Allow-Origin': 'true',
// 'Access-Control-Allow-Credentials': 'true',
}
if(headers){
for (let i in headers) {
defaultHeaders[i] = headers[i];
}
}
const showResState = (state) => {
let message = ''
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}
// 添加请求拦截器
axios.interceptors.request.use( (config) => {
// 在发送请求之前做些什么
console.log(config)
// header 配置 Token 判断Token是否过期 没过期则正常处理 过期则发起刷新Token的请求 拿到新的Token保存
config.headers.Authorization = null;
// const token = !localStorage.getItem('__auth_provider_token__')?localStorage.setItem('__auth_provider_token__',''):localStorage.getItem('__auth_provider_token__');
// let navigate = useNavigate();
// if(sessionStorage.getItem("__auth_provider_isLogin__") !== '1' && isAuth ){//&& !token
// alert('token失效');
// navigate('/login');
// return new Promise((resolve, reject) => {});
// }
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use((res) => {
// 对响应数据做点什么
const status = res.status
let msg = ''
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
msg = showResState(status)
if (typeof res.data === 'string') {
res.data = { msg }
} else {
res.data.msg = msg
}
}
return res;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
// 1. 执行异步ajax请求
const instance = axios({
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: BaseUrl,
// `url` 是用于请求的服务器 URL
url: url,
// `method` 是创建请求时使用的方法
method: method || 'get',
// mode: 'cors',
// cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
// `headers` 是即将被发送的自定义请求头
headers: {...defaultHeaders},
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
transformRequest: [function (data, headers) {
// 对 data 进行任意转换处理
return data;
}],
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 对 data 进行任意转换处理
return data;
}],
// `params` 是即将与请求一起发送的 URL 参数
// 必须是一个无格式对象(plain object)或 URLSearchParams 对象
params: method === 'get' ? params || {} : {},
// `paramsSerializer` 是一个负责 `params` 序列化的函数
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function(params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
// `data` 是作为请求主体被发送的数据
// 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
// 在没有设置 `transformRequest` 时,必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属:FormData, File, Blob
// - Node 专属: Stream
data: method === 'post' ? params || {} : {},
// `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
// 如果请求话费了超过 `timeout` 的时间,请求将被中断
timeout: 0,
// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // default 为true则产生跨域,跨域携带cookie
// `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
responseType: 'json', // default
});
return new Promise((resolve, reject) => {
instance.then(response => {
// 2. 如果成功了, 调用resolve(value)
resolve(response);
})
.catch(error => {
// 3. 如果失败了, 不调用reject(reason), 而是提示异常信息
reject(error)
// message.error('请求出错了: ' + error.message).then(r => {});
}).finally(() => {
})
});
}
// GET 请求 get 下 params 为查询参数
export const Get = (url,params={},headers={}) => {
return Axios(url,'get',params,headers)
}
// POST 请求 post 下 params 为body参数, 如果 post 下既需要传查询参数也需要传实体参数,则查询参数配置在 url 中
export const Post = (url,params={},headers={}) => {
return Axios(url,'post',params,headers)
}
在请求或响应被 then
或 catch
处理前拦截它们。
// 添加请求拦截器
axios.interceptors.request.use( (config) => {
// 在发送请求之前做些什么
console.log(config)
// header 配置 Token 判断Token是否过期 没过期则正常处理 过期则发起刷新Token的请求 拿到新的Token保存
config.headers.Authorization = null;
// const token = !localStorage.getItem('__auth_provider_token__')
//?localStorage.setItem('__auth_provider_token__','')
//:localStorage.getItem('__auth_provider_token__');
// let navigate = useNavigate();
// if(sessionStorage.getItem("__auth_provider_isLogin__") !== '1' && isAuth && !token){
// alert('token失效');
// navigate('/login');
// return new Promise((resolve, reject) => {});
// }
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use((res) => {
// 对响应数据做点什么
return res;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
在响应拦截中,如果有需要还可以对状态码提示进行处理。现在项目一般后端都会给处理好,这个就根据自己的项目情况进行配置吧。
// 根据不同的状态码,生成不同的提示信息
const showResState = (state) => {
let message = ''
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}
// 添加响应拦截器
axios.interceptors.response.use(function (res) {
// 对响应数据做点什么
const status = res.status
let msg = ''
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
msg = showResState(status)
if (typeof res.data === 'string') {
res.data = { msg }
} else {
res.data.msg = msg
}
}
return res;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
// GET 请求 get 下 params 为查询参数
export const Get = (url,params={},headers={}) => {
return Axios(url,'get',params,headers)
}
// POST 请求 post 下 params 为body参数, 如果 post 下既需要传查询参数也需要传实体参数,则查询参数配置在 url 中
// post 请求支持上传文件
export const Post = (url,params={},headers={}) => {
return Axios(url,'post',params,headers)
}
// src/api/index.js
import {Axios,Post,Get} from './axios'; // Axios 数据请求方法
/**
* 使用说明:
* import { login } from '@/api/index'
* 使用:
* login(params).then((res)=>{
* // 业务处理
* }).catch((err)=>{
* console.log(err)
* });
*/
/**
* 请求示例--Get 示例
* @params title string
* {title: 'cp'}
*/
export const getArticles = (params={}) => {
return Get('/api/v1/blog/getArticles', params);
}
/**
* 请求示例--Post 示例
* @params code string
* {code: 'xxxxxxx'}
*/
export const saveArticles = (params={}) => {
return Post('/api/v1/blog/saveArticles', params);
}
/**
* Axios 示例
* @params title string
* {title:'标题'}
*/
export const getAxiosDemo = (params={}) => {
return Axios('/api/v1/blog/getArticles','get', params);
}
/**
* 请求示例--Post 上传示例
* @params file object
* file
*/
export const upload = (params={}) => {
return Post('/api/v1/article/upload', params);
}
// 直接在页面中使用封装的方法
import { Axios, Get, Post } from '@/api'
// 使用 Axios 配置
Axios(
'/api/v1/blog/getArticles',
'get',
{id:123}
)
.then((res: any) => {
console.log(res)
}).catch((err:any)=>{
console.log(err)
});
// 使用 Get 请求
Get(
'/api/v1/blog/getArticles',
'get',
{id:123}
)
.then((res) => {
console.log(res)
}).catch((err)=>{
console.log(err)
});
// 使用 Post 请求
Post(
'/api/v1/blog/getArticles',
{id:123}
)
.then((res) => {
console.log(res)
}).catch((err)=>{
console.log(err)
});
对于在实际业务系统中,可以 对 Api 进行封装,放在一个或一组文件中,然后在页面中通过 API 接口名称进行调用,这样也便于管理 api 地址。对比下这种方式是不是很方便呢。
// 在页面中使用封装好的 API
import { getArticles } from '@/api'
getArticles({id:123}).then((res: any) => {
console.log(res)
}).catch((err:any)=>{
console.log(err)
});
这里单独把上传文件的代码也贴出来,供参考
<script setup>
import { ref } from 'vue';
const fileRef = ref(null);
const handleUploadBtn = () => {
fileRef.value.click();
}
const handleUpload = () => {
let file = fileRef.value.files[0];
if (file.size > 10 * 1024 * 1024) {
// 文件大小超限了
alert('请上传小于10M的图片');
fileRef.value.value = ''; // 清空内容
return;
}
let forms = new FormData();
forms.append('file', file);
// forms.append('filePath', `pc/client-${moment().format('YYYY-MM-DD')}/`);
fileRef.value.value = ''; // 清空内容
upload(forms).then((res) => {
console.log(res);
}).catch((err)=>{
console.log(err);
});
}
</script>
<template>
<input
type="file"
accept="image/*"
ref="fileRef"
@change="handleUpload"
/>
<button @click="handleUploadBtn">选择文件</button>
</template>
]]>持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
前段时间整理的文章:gtoken替换jwt实现sso登录 | 带你读源码 收到了大家积极的反馈,还有幸上了【掘金一周】。
在此感谢掘友们,感谢掘金平台。
gtoken替换jwt实现sso登录
的开发过程是比较稳健的,但是在我们测试联调的过程中暴露出了很多问题。
如果大家也想使用gtoken替换jwt实现sso登录,那么这篇文章可以减少很多大家debug的时间,分享一下我的踩坑之旅。
服务端出于优化项目体验的考虑,替换了之前校验登录状态的方式,由JWT替换为 Gtoken。
有效的避免了jwt服务端无法退出问题;
解决jwt无法作废已颁布的令牌,只能等到令牌过期问题;
通过用户扩展信息存储在服务端,有效规避了jwt携带大量用户扩展信息导致降低传输效率问题;
gtoken替换jwt实现sso登录在前后端通信上是能做到兼容JWT的。
我们服务端的替换操作对前端同学应该是无感的,因为后端做了兼容处理,不需要前端同学修改任何东西。
gtoken的实现原理以及如何使用建议大家读我这篇文章: gtoken替换jwt实现sso登录 | 带你读源码。
在本篇文章中就不赘述了,下面重点介绍踩坑之旅:
当大家遇到登录问题时可以从这几个方向定位问题:
如果我们使用的版本是gf1.x.x,只能使用gtokenv1.4.X相关版本。
而gtoken v1.5.0
版本全面适配GoFrame v2.0.0。
如果遇到版本不一致的问题,比如提示这种:
可以通过指定gtoken版本解决,比如这样:
go get github.com/goflyfox/gtoken@v1.4.1
如果我们是团队多人协作,碰到需要指定依赖版本的问题,我们可以考虑把go.mod提交到git中。
在遇到这个问题之前,我的习惯是把go.mod添加的gitignore中。
大家有没有更好的办法来解决需要指定依赖版本的问题呢?大家可以在评论区留言指教。
如果你们的项目是集群应用,gtoken的存储就需要使用gredis模式,而不是单机的gcache模式了。
这就需要我们生成token和获取token的各个项目连接的redis是一致的。
如果你是集群应用,千万要确保涉及到gtoken生成和验证的各个项目连接的redis是一致的。
所以,大家遇到token校验不通过时,可以首先排查一下配置文件,是不是连接redis库的问题。
正如上面提到的,如果gtoken的存储是使用redis中来实现集群项目的共享。
那我们是不能跨环境使用token的,因为我们的本机、开发、测试、预发布、生产等环境往往连接的是不同的redis。
如果测试时多个用户登录同一个账号,可能会出现奇葩问题。
究其原因是这样的:
gtoken是允许多点登录的,所以支持大家使用同一个账号登录。
但是!如果其中一个人做了退出登录的操作,那么其他人的登录态也会失效,需要重新登录。
比如设置的token有效期是2个小时,且2小时内有请求操作,会刷新token的有效期。但是如果有多人登录同一个账号,其中一个人退出,那么其他人的登录态也会失效的。
上面这些是我在开发中踩的坑,大家如果在集成gtoken时遇到登录态问题可以从这几个角度排查问题。
感谢阅读,欢迎大家三连:点赞、收藏、投币(关注)!!!
Hello,掘友们好,「掘金·日新计划」6月更文挑战来了!
相信经过第一次「掘金·日新计划」,大家对于这个活动有了更深刻的了解。这个6月,「掘金·日新计划」再次起航,让我们一起迎接新挑战吧!
5 月 25 日 0:00 - 6 月 30 日 23:59
获奖条件 | 奖品 | 图片 | |
---|---|---|---|
第一关 | 更文天数≥7天 | 太空人小风扇 | |
第二关 | 更文天数≥14天 | 太空人小风扇 +JUEJIN FRIENDS 「码赛克」拖鞋 注:(仅有39、41、43三个码数) |
|
第三关 | 更文天数≥21天 | 太空人小风扇 +JUEJIN FRIENDS 「码赛克」拖鞋 注:(仅有39、41、43三个码数) +米家便携随行榨汁机 快速鲜榨 |
|
全勤奖 | 更文天数≥30天 | 太空人小风扇 +JUEJIN FRIENDS 「码赛克」拖鞋 注:(仅有39、41、43三个码数) +米家便携随行榨汁机 + 小米(MI)米家电饭煲C1 智能预约多功能电饭锅 |
|
创作先锋奖 (可与其他奖项叠加) | 设置三个技术方向,分别评选积分排名前 10 的同学,共 30 位: 1. 前端(“前端”分类) 2. 服务端("后端"、“人工智能”分类) 3. 移动端(“Android”、“iOS”分类) 创作先锋奖不限制上榜作者在活动期间的更文篇数、天数,写一篇也有机会获奖,奖项评选包括两个大的维度:内容质量和数据情况,其中内容质量由掘金内容评审团来评选,数据情况通过积分高低来评选,以掘金后台统计为准;若作者有多篇文章上榜,仅按寄送 1 份礼物。 1、 上榜的文章要求为原创技术文章且 首发 在掘金。注意:面试、代码人生、产品经理相关文章不能参与评选。 2、文章首句关键词没有差别。 |
小米手表 Redmi Watch 2 典雅黑 +【实体证书】掘金日新计划-创作先锋(2022.6) |
|
新人奖 | 首次参与更文挑战活动并完成第一关 | 掘金马克杯 | |
老司机激励奖 | 之前参与过更文挑战,并完成第一关 | 限定款式小黄鸭2个(皮卡丘+钢铁侠) | |
幸运奖 | 将参赛文章发到本文的评论区,带上标题+链接,每周三我们会在评论区揪 2 位参赛的幸运作者,送出掘金随机周边 | 随机~ |
基本要求 | 描述 |
---|---|
文章类型 | 要求必须为技术类文章,像读后感类、人生经历、面试经历、年中/终总结等与技术不是直接相关的,都不计数。 |
正文第一句带模板关键词👉(否则不会被后台统计在内) | 持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第N天,点击查看活动详情 >>PS:已经更文多少天,N就写几。一定要写对文案,否则文章不计入在内;模板句子需要带超链接。 |
文章分类 | 请根据文章内容选择,可优先选择前端、后端、iOS、Android、人工智能 |
特别说明:
更多 QA 可以查看飞书文档 掘金日新计划 · 6 月更文挑战 QA ,有疑问可以在⬅️飞书文档底部评论,运营同学会进行解答。
参加活动的掘友添加掘金酱为好友,回复关键词「更文挑战」入群。重要消息不错过,大家互相鼓励,也可以在群内咨询哦~
如果你是参与往期更文活动的老用户且已经在更文活动群(任一群内),请勿重复进群。
如果你是新手,不知道从哪里下笔写作,可以参考以下文章方向:
实践总结: 实践总结可以是最近自己积累一些心得体会:最近使用 xx 实践一些项目的总结了一些使用心得体会等。
技术细节: 技术细节型的文章侧重于解决某个具体的问题,比如某个开源库中存在什么bug?如何解决这个bug?如何使用某个具体的技术?等等
技巧干货: 这类文章是作者对于某一块的内容进行的总结,具有较高的收藏价值,是属于较为受欢迎的一类文章,也更加容易在社区内和社区外传播。
开源工具介绍: 优秀的开源工具可以帮助你事陪功半,可以是对某个开源工具的详使用细介绍,也可以是某个技术方向涉及的开源工具的认真总结,更可以安利介绍自己的开源作品。
翻译优质英文技术文章: 举例
行业见解及职业选择: (一定要是程序员的行业见解及职业选择) 通过自己在行业的理解和沉淀,加以事实的依据,帮助他人更完整地理解行业的发展情况以及作为开发者的职业选择、职业成长,让读者在技术之外全面提升
预热活动规则:
在本文评论区发布「坚持,持续创作;思考,加速成长;挑战6月更文!」即可参与抽奖,我们将在 5 月 25 日抽取 3 位幸运同学,分别送出 掘金鼠标垫 / 掘金抱枕 / 空调毯。
预热活动开奖:
恭喜以下小伙伴获得掘金周边:
]]>OOP
面向对象程序设计(Object Oriented Programming,OOP)是一种计算机编程架构。OOP 的一条基本原则是计算机程序由单个能够起到子程序作用的单元或对象组合而成。OOP 达到了软件工程的三个主要目标:重用性、灵活性和扩展性。面向对象编程的三大特点:封装性、继承性和多态性。
TCC
动态配置中心 TCC(ToutiaoConfigCenter)是提供给业务方的一套平台+SDK 的配置管理解决方案,提供的功能有权限管理、配置管理、版本管理、灰度发布、多地区多环境支持等。与百度开源的“百度分布式配置中心 BRCC”功能类似。
APIX
Golang 实现的 web 框架,可参考开源项目 Gin。
GORM
Golang 编写的热门数据库 ORM 框架。
大力智能学习灯于 2019 年 10 月份上线,截止 2021 年底,台灯出货量已超过 100w 台,完成了从 0 到 1 的探索。在成立之初,很多方向的产品为了尽早拿到用户反馈,要求快速迭代,研发在代码实现上相对快糙猛,早期阶段这无可厚非,但慢慢地,自习室、系统工具、知识宇宙等应用已经变成灯上核心基建,如果还按之前的野蛮生长的方式将会为台灯的成长埋下隐患。
在这样的背景下,大力智能服务端推动 OOP 技术专项的落地,希望能够:提升团队成员自身的编码水平;统一团队内部编程风格;支撑业务快速迭代。
TCC、APIX、GORM 都是日常项目中经常会依赖到的外部包,本文从这些项目的源码出发,在学习的过程中,解读良好的代码设计在其中的应用,希望能帮忙大家更好的理解和应用 OOP 思想,写出更优秀的代码。
一个类只负责一个职责(功能模块)。
一个类、方法或模块的扩展性要保持开放,可扩展但不影响源代码(封闭式更改)
子类可以替换父类,并且不会导致程序错误。
一个类对另一个类的依赖应该建立在最小的接口上。
高层次的模块不应该依赖于低层次的模块,它们应该依赖于抽象。
解决问题:在设计一个函数时,当存在配置参数较多,同时参数可选时,函数式选项模式是一个很好的选择,它既有为不熟悉的调用者准备好的默认配置,还有为需要定制的调用者提供自由修改配置的能力,且支持未来灵活扩展属性。
TCC 在创建BConfigClient
对象时使用了该模式。BConfigClient
是用于发送 http 请求获取后端服务中 key 对应的 value 值,其中getoptions
结构体是 BConfigClient 的配置类,包含请求的 cluster、addr、auth 等信息,小写开头,属于内部结构体,不允许外部直接创建和修改,但同时对外提供了GetOption
的方法去修改getoptions
中的属性,其中WithCluster
、
```
/juejin/tag/架构
works as of replying.
路由地址(不包含参数)
完整路由地址,包含所有必选与可选参数
相关文档地址
https://docs.rsshub.app/programming.html#jue-jin
预期是什么
返回正常的xml结构
实际发生了什么
Route requested: /juejin/tag/%E6%9E%B6%E6%9E%84 Error message: Timeout awaiting 'request' for 30000ms
部署相关信息
rsshub.app 环境