DIYgod / RSSHub

🧡 Everything is RSSible
https://docs.rsshub.app
MIT License
32.6k stars 7.27k forks source link

掘金-标签下的文章无法获取 #7985

Closed 404LT closed 2 years ago

404LT commented 3 years ago

路由地址(不包含参数)

/juejin/tag/:tag

完整路由地址,包含所有必选与可选参数

https://rsshub.app/juejin/tag/架构

相关文档地址

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 环境

github-actions[bot] commented 3 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.

TonyRL commented 2 years ago

/test

/juejin/tag/架构
github-actions[bot] commented 2 years ago

Successfully generated as following:

http://localhost:1200/juejin/tag/架构 - Success ```rss <![CDATA[掘金 架构]]> https://juejin.cn/tag/%E6%9E%B6%E6%9E%84 RSSHub i@diygod.me (DIYgod) zh-cn Tue, 31 May 2022 12:48:31 GMT 5 <![CDATA[【架构师(第二十六篇)】编辑器开发之属性编辑同步渲染]]>

highlight: a11y-dark theme: smartblue

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

属性更新

  • 属性编辑通过 store 获取属性值
  • 通过发射事件触发 commit 修改属性值
  • 支持属性值的转换

image.png

propsMap.ts

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,
  },
};

PropsTable.vue

<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>

Editor.vue

// 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)
}

editor.ts

mutations: {
    // 更新组件属性
    updateComponent(state, { key, value }) {
      const updatedComponent = state.components.find(
        (c) => c.id === state.currentElement,
      );
      if (updatedComponent) {
        updatedComponent.props[key as keyof TextComponentProps] = value;
      }
    },
  },

最终实现如下

2.gif

优化需求

选择字体的下拉框可以直接显示当前的字体样式

image.png

h 函数

h 函数接收三个参数

  • type: 元素的类型
  • props: 数据对象
  • children: 子节点

使用 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

使用 tsx 改写

const fontFamilyOptions = fontFamilyArr.map((font) => {
  return {
    value: font.value,
    text: (<span style={{ fontFamily: font.text }}>{font.text}</span>) as VNode,
  };
});

使用 render 函数实现桥梁

// \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>

最终实现如下

image.png

阶段总结

业务组件

  • 创建编辑器 vuex store 结构,画布循环展示组件
  • 组件初步实现,使用 lodash 分离样式属性
  • 添加通用和特殊属性,转换为 props 类型
  • 抽取重用逻辑,style 抽取和点击跳转
  • 左侧组件库点击添加到画布的逻辑

组件属性对应表单组件的展示和更新

  • 获得正在被编辑的元素,通过 vuex getters
  • 创建属性和表单组件的对应关系
  • 使用 propsTable 将传入的属性渲染为对应的表单组件
  • 丰富对应关系字段支持更多自定义配置
  • 使用标准流程更新表单并实时同步单项数据流
  • 使用 h 函数以及 vnode 实现字体下拉框实时显示
]]>
Tue, 31 May 2022 00:54:34 GMT https://juejin.cn/post/7103697336534368263 https://juejin.cn/post/7103697336534368263
<![CDATA[Axios 的特殊应用:上传获取进度及下载文件]]>

theme: cyanosis highlight: zenburn

持续创作,加速成长!这是我参与「掘金日新计划 · 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>
]]>
Mon, 30 May 2022 15:15:15 GMT https://juejin.cn/post/7103548094460985380 https://juejin.cn/post/7103548094460985380
<![CDATA[多进程打包:thread-loader 源码(1)]]>

theme: fancy highlight: an-old-hope

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的6天,点击查看活动详情

一、背景

这不又到年初了,老板又拉了一大票成长计划(O(JB)KR),我头脑一热果断选择了一个我最不太熟悉的领域——并发程(多线程/进程)打包。

然而最近业务繁重,根本时间搞,连摸鱼的时间都没有了,咋办?

思虑再三,决定接着写小作文,一点一点磕,磕一点就写一点,这样也有动力,毕竟这种贡(刷)献(存)社(在)区(感)的摸鱼工作还是值得坚持下去的。

前面写的浅羲Vue源码这个大坑还没填完,但是又不得不开一个新坑,我保证这事儿干完接着填 Vue 源码的大坑。

多进程(线程)打包的实现有很多,比如大家都听过的子编译happypackthread-loader... happypack 作者对 js 兴趣退散了,最近不咋维护了,所以我们选择了 thread-loader 这个方案。

不能再多说背景了,再多说就被同事发现我了,这个活儿是记名的,否则这番吐槽就要到老板耳朵里面了

二、thread-loader 是个啥?

thread-loder 是个 loader,他不处理具体的转换模块到js的工作,而是把他后面的 loader 扔进一个工作线程池(worker pool)中并发运行,上传送门# thread-loader

2.1 thread-loader 的限制

运行在 worker pool 中的 loader 是受限制的,主要体现在以下几方面:

  1. 这些 loader 不能 通过 this.emitFile 生成一个新文件webpack 文档传送门 # this.emitFile
  2. 这些 loader 不能使用插件自定义的 loader 方法,所谓插件自定义就是通过插件向 loaderContext 扩展自定义的方法,loaderContextwebpack 提供的一个 loader 运行时的上下文对象;
  3. 这些 loader 同样也无法获取 webpack 打包的配置对象;

2.2 独立进程

thread-loader 虽然介绍时使用的是 worker pool 但是,它却不是真正的 worker 线程,而是实实在在的子进程(child_process);

官方说大约会有 600ms 左右的开销,并且进程间天然隔离,不能共享数据,只推荐在那些耗时比较长的 loader 中使用;

三、thread-loader 入口文件

3.1 搞源码

去 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
    ├── ....

3.2 入口文件

package.jsonmain 字段得知,这个包的入口文件为 dist/cjs.js

image.png

但是你会发现,上面的目录中压根没有 dist 目录。。。WHAT?

此时别着急,这种情况下都是作者在本地打包好,然后用本地文件发包,只不过 git 仓库是忽略掉 dist 目录。这就要求我们看看作者是用啥打包的,一般的不外乎:webpack、esbuild、rollup、babel、snowpack...

打开 package.jsonscripts 脚本,一般打包命令都写在这里面:

{
  "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 目录生成了:

image.png

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)看起就好了,这里算是分享一个我早期阅读源码的一个疑惑点。

四、loader.noraml & loader.pitch

4.1 入口模块的代码结构

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

在这个入口文件中定义并且导出了两个方法:pitchwarmup,我们先忽略方法中的具体逻辑,先说说这个结构。

值得一提的是 pitch 方法,这个名字不是瞎叫的,而是在 webpack 的整个生命周期中有这深远意义的名字;

4.2 pitch 方法

这需要你了解 webpackloader 的执行的全过程,我在前面的文章中写过一些关于 pitch 的内容

大致回顾一下,loader 的运行分为两个过程:pitchnormal 阶段,所谓 normal 就是实现具体功能的 loader 函数本身了,而 pitch 则是挂载 loader 上的一个特殊方法。

normal 阶段是大家熟知的按照 loader 添加的顺序倒序执行,但是在此之前还有一个 pitching 阶段,这个阶段会按照 loader 的添加顺如逐个执行 loaderpitch 方法;

pitch 是越过的意思,上官方文档### 越过 loader(Pitching loader)

也就是说你声明一个 loader 时,还可以声明一个 loader.pitch,用于跳过其余的 loader 进入到剩余 loadernormal 阶段;

比如我们对一个模块添加了三个 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

4.3 pitch 对于 thread-loader 的意义

结合前面的介绍,thread-loader 会把放在他后面的 loader 放到 worker pool 中并发执行。那么如何截住它后面的 loader 们呢?

正如你所料,thread loader 就是在 pitch loader 中把剩余的 loader 扔到线程池中运行;

五、总结

本篇小作文开启了一个新坑,本文主要讨论了以下几个问题:

  1. thread-loader 作用;
  2. babel 打包 thread-loader 及打包的入口文件;
  3. 复习 loader.pitch & normal 以及两者的顺序;
  4. thread-loader 利用 pitch 截取后面的 loader 扔到线程池;
]]>
Mon, 30 May 2022 12:36:52 GMT https://juejin.cn/post/7103507305672474632 https://juejin.cn/post/7103507305672474632
<![CDATA[goFrame的gqueue详解 | 对比channel]]>

highlight: a11y-dark theme: smartblue

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

channel

首先明确一下channel的作用:用于go协程间的通信。

go语言最大的特点就是支持高并发:goroutine和channel是支持高并发的重要组成部分。

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

另外要明确知道go的并发哲学,铭记这句原则:用通信来共享内存,而不要用共享内存来通信。

在搞清楚channel的作用之后我们再来研究GoFrame框架(下文简称gf)中gqueue的特点。

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
      }
   }
}

打印结果

image.png

优势

为什么不用标准库的channel,要用gqueue呢?

  1. gqueue的使用比channel更灵活,channel有队列大小的限制,而gqueue队列支持动态大小

  2. 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一直在消费数据。

image.png

总结

通过这篇文章,我们知道了channel的概念和作用。也知道了gqueue的底层实现和特点,以及gqueue和channel的对比。两者各有什么特点。

最后

感谢阅读,欢迎大家三连:点赞、收藏、投币(关注)!!!

8e95dac1fd0b2b1ff51c08757667c47a.gif

]]>
Mon, 30 May 2022 12:18:33 GMT https://juejin.cn/post/7103502362429358116 https://juejin.cn/post/7103502362429358116
<![CDATA[货拉拉H5离线包原理与实践]]> 1.前言

货拉拉自研H5离线包SDK,能显著提升H5打开速度,已在多个业务中落地。车型介绍页使用离线包前后打开效果如下:

图1 使用离线包前 图2 使用离线包后

货拉拉自研离线包具有如下特点:

安全可靠:无hook,无私有API,三重降级策略。

容易维护:三层架构模式和模块化设计。

功能完备:功能可配置,数据埋点,开发者工具等功能一应俱全。

开源代码地址:https://github.com/HuolalaTech/HLLOfflineWebVC-iOS

团队介绍:货拉拉移动技术团队https://juejin.cn/user/1768489241815070

2.背景

Web 技术相信大家非常熟悉,它具有快速迭代发布的天然优势,在货拉拉App中广泛使用,比如金秋拉货节、余额、车型介绍页等,但也存在加载速度慢,体验差等问题。目前优化Web页面体验方案比较多,有Flutter、React Native、小程序、离线包,其中离线包技术对现有H5页面改造成本小,技术最为成熟和稳定。

3.行业方案

腾讯开源了离线包方案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拦截网络请求难度大的问题,开发和测试工作量大,暂不用。结合实现成本和技术可靠性最后采取加载本地路径的方案。

4. 技术实现

H5离线包的基本原理是将html、js、css、图片等静态资源打包成压缩包,然后下载到客户端并解压,H5加载时直接从本地读取静态资源文件,减少网络请求,提高速度。加载本地文件路径存在的问题和解决方法:

存在问题 解决方法
cgi请求跨域问题 在网关或者后端服务的跨域请求头增加null域支持
cookie跨域问题 目前静态js中无cookie操作,没有cookie跨域问题
localstorage跨域问题 暂时不涉及域名隔离问题,如果有需要,采取调用原生的方式解决
绝对路径问题离线模式不支持问题 改成相对路径

4.1 总体流程

如下图所示:

客户端启动后,先去远程配置服务器拉取离线包相关的功能配置,然后检查更新,如果有更新则下载离线包。webview加载时,如果本地缓存命中,则从本地磁盘加载html、js、css、图片等静态资源。

H5离线包和线上H5类似也能进行更新和升级,有三个更新时机:

1)webview容器打开时更新。开启离线包功能的H5页面每次打开时,会去检查对应的离线包是否有更新。如果有更新,则下载离线包到本地,绝大部分场景是下次打开时生效。

2)启动查询离线包更新。对于实时性要求比较高的页面,可配置在启动时更新离线包。

3)长连接推送实时更新。更新实时性要求极高的场景使用。

4.2 架构改进

之前货拉拉webview容器(后续简称WebVC)采用的是两层结构,每个业务方从基础WebVC派生出自己的业务WebVC容器。两层结构在接入离线包时遇到如下问题:

离线包接入方案 优点 缺点
集成到业务WebVC容器 不影响基础WebVC功能,影响范围小 通用性差,每个业务WebVC都需要修改,接入成本高。
集成到基础WebVC容器 业务方接入简单,升级基础WebVC即可 基础WebVC会变得臃肿,增加了离线包,调试工具,埋点功能,后续维护成本高。

为了既不造成基础容器臃肿,维护成本高,也要方便业务方快速接入,将webview改成了三层结构:

image.png

增强WebVC和基础WebVC 功能独立,已分成独立的仓库,由不同的开发维护,维护性大大提高。货拉拉基础WebVC容器代码暂未开源,开源代码中改成简化版实现,可自行定制和修改。

4.3 性能优化

1)查询并行化

目前已有多个H5页面采用离线包,为了提高查询效率,多个业务离线包检查的请求采取并行请求的方式。由于HTTP2支持多路复用,所以查询时共用一个NSURLSession对象,多次查询操作自动复用一个TCP连接,减少建立连接次数。考虑到后端改造成本问题,目前不支持聚合查询,计划在后续版本中完善。

image.png

2)下载任务去重

下载过程也是并行进行,可同时下载多个离线包。由于查询操作有多个触发时机,存在重复下载的情况。为了节省流量,采用下载URL作为key,不重复下载正在下载或已下载的文件。

3)启动预下载

大部分离线包查询和下载的时机为打开H5页面时,由于离线包查询、下载、解压总体耗时超过1秒,导致首次打开无法命中离线包。所以货拉拉离线包支持配置部分离线包在启动时检查和下载离线包,提升首次打开H5页面离线包命中率。配置json格式为:

{
   "predownloadlist": //选填。需要启动就下载的离线包列表清单
    ["test-offline"]
}

4)断点续传

移动端网络稳定性不如固定网络,会出现因为无网络而导致下载中断的情况。货拉拉离线包接入了自研的下载SDK,支持断点续传功能,节省了用户下载流量和时长。由于下载SDK代码暂时未开源,离线包对外开源代码中采用简化版本代替,没有实现断点续传功能。

5)解压异步化和串行化

解压过程是CPU和IO密集型操作,放在子线程中处理,有多个文件需要解压时,采用串行队列的方式,一次只解压一个文件,避免同时解压多个文件影响客户端性能。

4.4 可靠性设计

1)解压操作可靠性设计

文件解压耗时较长(大约30ms),如果程序异常退出可能会出现解压操作完成一半的情况,影响后续离线包功能。所以文件解压操作采取先解压,然后重命名,保证最后得到的文件完整性。同时当离线包正在使用时,一般情况下采取先解压,下次生效的策略,极端情况下立刻生效,但会导致页面强刷,影响用户体验,使用较少。离线包存放时使用tmp、new、cur三个文件夹,不采用版本号命名,简化离线包缓存管理逻辑。解压细节如下:

image.png

2)三重降级策略

客户端自动降级。本地没有离线包时,客户端会自动将启用离线包的H5页面降级为线上H5页面。

远程配置降级。可以设置局部降级,即临时将某个使用离线包的H5页面降级为线上,也可设置全局降级,关闭所有页面的离线包功能。配置json为:

{
    "switch": 1, //总开关,1开启,0关闭
    "disablelist": //选填,配置禁用某个离线包
     ["act-test"],
}

服务端接口降级。 服务端提供的离线包查询接口可设置将某个页面降级为线上H5,亦可配置客户端更新离线包后强制刷新。

降级策略流程图如下:

image.png

3)性能监控

货拉拉对webview的加载成功率,错误码、耗时进行了统计上报,通过监控面板查看。

此外离线包SDK还有离线包下载,请求,解压的耗时、结果数据上报。监控和上报采取的接口扩展方式,接入方根据业务特点选用具体的数据上报SDK。

4.5 效能优化

1)开发者工具

为了方便调试和查看,离线包中包含了开发者工具,仅在Debug模式下才能使用,可查看离线包版本和清除离线包缓存。

2)离线包和URL映射配置化

客户端webview通过URL是否有offweb参数判断该页面走线上请求还是加载本地离线包。URL来源广泛,有客户端硬编码、运营系统下发、JS action调用多种来源,手动修改URL效率太低,因此通过远程配置,自动给指定URL添加离线包参数并命中对应的H5离线包。

image.png

配置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"
        }
    ]
}

5. 收益分析

H5离线包经过多个业务上线验证,性能稳定,收益明显。

5.1 加载速度提升

改造完成的H5页面,平均加载速度从2s提升至1s。部分页面详细数据如下:

5.2 加载成功率提升

页面主框架(不考虑动态数据)加载成功率从96%提升到100%。

6.后期工作与展望

1)扩大开源范围。断点续传下载SDK,日志SDK后续会考虑开源。

2)单元测试用例完善。增加更多单元测试,保证稳定性。

7.参考资料

货拉拉离线包开源地址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

]]>
Mon, 30 May 2022 02:21:05 GMT https://juejin.cn/post/7103348563479887885 https://juejin.cn/post/7103348563479887885
<![CDATA[java中的static关键字说清楚还得靠JVM]]> 持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

前言

  • Java中Static想必大家一定使用过吧。他是用来修饰类或者成员变量或者方法的。
  • 关于Static的用法还是很简单的,因为他就是一个修饰词。但是如果不理解他修饰的作用原理的话,可能会闹出bug来

变量

image-20220509191420226.png

  • 上图是一个简化的JVM内存结构模型。学习过JVM的都知道我们创建的对象正常情况下都是在堆中的。那么我们访问对象中的属性自然也就存放在堆中的。
  • 但是当static修饰属性之后他就发生了变化了。
 ​
 class Demo {
   //成员变量
   public int num = 100;
   //静态成员变量
   public static int count = 200;
   //静态方法
   public static void method(){
     System.out.println(count);
   }
 }
  • num属性属于常规属性,count属性属于静态变量。他们不仅仅是名称上的不同。从JVM的角度看他的存放位置也不同。
  • 首先num依赖于具体的对象,所以他和对象存放在一起都是堆中。
  • 而count独立于对象。JVM中专门有一块空间用于存放静态变量。这个空间我们叫做方法区。

方法

  • 除了修饰变量外,static还可以修饰方法。被修饰的方法我们叫做静态方法 。静态方法的特点和静态变量一样都属于类而不是对象。静态方法内部只能访问静态变量而无法通过this对象进行访问对象属性。
  • 总结下来就是静态方法内部只能访问静态变量无法访问非静态变量。
  • 除了静态方法外,还有一个特殊的方法叫做静态代码块。这个方法不需要我们准备方法名,入参,出参等等。只需要准备方法体。关于方法体内部和静态方法内部要求是一样的。

image-20220514140752124.png

  • 关于静态代码块和静态方法他们和普通方法还有一个重要的区别就是执行时机。静态变量与普通变量的区别就是内存分布位置,而方法是在栈中操作的,不涉及内存的存储,所以区别就是方法执行的时机。这里需要我们提前了解点类加载机制。
  • 首先我们一个类的加载分为五个过程。首先是加载class元信息,最后一步是进行初始化。至于前面三步我们这里可以不理解。重点知道在类加载的最后阶段会进行初始化,而初始化的操作就是执行静态方法和静态代码块。
  • 从类加载过程中我们也能够看的出来静态方法是不依赖于对象的调用的。因为静态方法中只能使用到静态属性。也就是说静态属性使用时还没有创建对象。这也佐证了静态变量不依赖对象的说法。

总结

  • 本文主要讲解Java基础,请原谅我没有华丽的词藻渲染出色的文章。虽然基础但往往是我们容易忽略的知识点。
  • 只有不断的学习,才能不断的进步,关于static的进一步使用场景,目前我能想到的就是单例模式中会使用。
]]>
Mon, 30 May 2022 01:17:53 GMT https://juejin.cn/post/7103332427287756831 https://juejin.cn/post/7103332427287756831
<![CDATA[关于 Axios 的再封装...]]>

highlight: zenburn theme: cyanosis

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

废话只说一句:码字不易求个👍,收藏 === 学会, 快行动起来吧!🙇‍🙇‍🙇‍。2022.05.30

简介

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。虽然,Axios是个优秀的 HTTP 库,但是,直接在项目中使用并不是那么方便,会存在大量的重复性方法和代码,所以,我们需要对其进行一定程度上的配置封装,减少重复代码,精简调用方式。

本次使用 axios 版本为 ^0.21.4

设计

先设计下我想要这个通用请求能达到什么样的效果:

  • 优化配置,设置默认配置项(responseType、跨域携带cookie、token、超时设置)
  • 统一设置请求头
  • 根据环境设置 baseURL
  • 通过 Axios 方法直接发起请求
  • 添加请求拦截器
  • 添加响应拦截器
  • 导出 Primise 对象
  • 封装 Post 方法,精简 post 请求方式
  • 封装 Get 方法,精简 get 请求方式
  • 请求成功,配置业务状态码
  • 全局的loading配置

下面👇是 JS 版本

封装主体 Axios

// 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 请求 get 下 params 为查询参数
export const Get = (url,params={},headers={}) => {
    return Axios(url,'get',params,headers)
}

封装 Post 请求

// POST 请求 post 下 params 为body参数, 如果 post 下既需要传查询参数也需要传实体参数,则查询参数配置在 url 中
// post 请求支持上传文件
export const Post = (url,params={},headers={}) => {
    return Axios(url,'post',params,headers)
}

封装 API

// 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>
]]>
Sun, 29 May 2022 15:59:34 GMT https://juejin.cn/post/7103188409039978504 https://juejin.cn/post/7103188409039978504
<![CDATA[gtoken替换jwt实现sso登录 | 排雷避坑]]>

highlight: a11y-dark theme: smartblue

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

前段时间整理的文章:gtoken替换jwt实现sso登录 | 带你读源码 收到了大家积极的反馈,还有幸上了【掘金一周】。

在此感谢掘友们,感谢掘金平台。

gtoken替换jwt实现sso登录的开发过程是比较稳健的,但是在我们测试联调的过程中暴露出了很多问题。

如果大家也想使用gtoken替换jwt实现sso登录,那么这篇文章可以减少很多大家debug的时间,分享一下我的踩坑之旅。

gtoken

服务端出于优化项目体验的考虑,替换了之前校验登录状态的方式,由JWT替换为 Gtoken

gtoken替换jwt解决的问题

  1. 有效的避免了jwt服务端无法退出问题;

  2. 解决jwt无法作废已颁布的令牌,只能等到令牌过期问题;

  3. 通过用户扩展信息存储在服务端,有效规避了jwt携带大量用户扩展信息导致降低传输效率问题;

兼容JWT

gtoken替换jwt实现sso登录在前后端通信上是能做到兼容JWT的。

我们服务端的替换操作对前端同学应该是无感的,因为后端做了兼容处理,不需要前端同学修改任何东西。

gtoken实现原理

gtoken的实现原理以及如何使用建议大家读我这篇文章: gtoken替换jwt实现sso登录 | 带你读源码

在本篇文章中就不赘述了,下面重点介绍踩坑之旅:

踩坑之旅

当大家遇到登录问题时可以从这几个方向定位问题:

1 gtoken版本

如果我们使用的版本是gf1.x.x,只能使用gtokenv1.4.X相关版本。

gtoken v1.5.0版本全面适配GoFrame v2.0.0。

如果遇到版本不一致的问题,比如提示这种:

image.png

可以通过指定gtoken版本解决,比如这样:

go get github.com/goflyfox/gtoken@v1.4.1

如果我们是团队多人协作,碰到需要指定依赖版本的问题,我们可以考虑把go.mod提交到git中。

在遇到这个问题之前,我的习惯是把go.mod添加的gitignore中。

大家有没有更好的办法来解决需要指定依赖版本的问题呢?大家可以在评论区留言指教。

2 gtoken存储问题

如果你们的项目是集群应用,gtoken的存储就需要使用gredis模式,而不是单机的gcache模式了。

这就需要我们生成token和获取token的各个项目连接的redis是一致的。

如果你是集群应用,千万要确保涉及到gtoken生成和验证的各个项目连接的redis是一致的。

所以,大家遇到token校验不通过时,可以首先排查一下配置文件,是不是连接redis库的问题。

3 不能跨环境使用token

正如上面提到的,如果gtoken的存储是使用redis中来实现集群项目的共享。

那我们是不能跨环境使用token的,因为我们的本机、开发、测试、预发布、生产等环境往往连接的是不同的redis。

4 测试账号不规范问题

如果测试时多个用户登录同一个账号,可能会出现奇葩问题。

究其原因是这样的:

gtoken是允许多点登录的,所以支持大家使用同一个账号登录。

但是!如果其中一个人做了退出登录的操作,那么其他人的登录态也会失效,需要重新登录。

比如设置的token有效期是2个小时,且2小时内有请求操作,会刷新token的有效期。但是如果有多人登录同一个账号,其中一个人退出,那么其他人的登录态也会失效的。

总结

  1. gtoken版本问题
  2. 连接的redis库不一致问题
  3. 是否跨环境使用了token,导致校验不过的问题
  4. 多人登录同一个账号,有退出操作,导致登录态失效的问题

上面这些是我在开发中踩的坑,大家如果在集成gtoken时遇到登录态问题可以从这几个角度排查问题。

最后

感谢阅读,欢迎大家三连:点赞、收藏、投币(关注)!!!

8e95dac1fd0b2b1ff51c08757667c47a.gif

]]>
Fri, 27 May 2022 12:17:14 GMT https://juejin.cn/post/7102389025050361864 https://juejin.cn/post/7102389025050361864
<![CDATA[持续创作,加速成长,6月更文活动来啦!| 掘金·日新计划]]>

theme: vuepress

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、人工智能

💫 具体要求(投稿前请仔细阅读)

  • 如果发现有一篇非原创文章,立刻取消该作者的所有获奖资格。注:全文超过 50% 与他人创作的内容重复都视为非原创 (包括直接抄袭搬运、大篇幅摘抄书籍、网络文章、产品官方文档等)
  • 特别说明:

    • 技术类笔记文章可以参加活动,但自己原创的思考、总结、观点段落篇幅必须大于 50% ,如果不符合 50% 的篇幅要求,也视作非原创,直接取消该作者所有获奖资格。
    • 文章不能只贴代码,要有自己的分析思考,代码文字比不得超过70%,否则当天更文不计数。
    • 关于创作先锋奖:官方将在活动结束后,筛选 首发 在掘金的文章进行评选,非首发不能获奖;创作先锋最终评选包括两个大的维度:内容质量和数据情况,其中内容质量由掘金内容评审团来评选,数据情况通过积分高低来评选,积分计算方式:阅读量/100 * 0.3 + 评论数 * 0.3+点赞数 * 0.2+收藏数 * 0.2,以掘金后台统计为准;若作者有多篇文章上榜,仅按寄送 1 份礼物。
    • 活动期间,作者如果有用脚本刷阅读量、注册僵尸号刷赞等严重违反社区规范的行为,直接取消所有获奖资格。
    • 优质文章有机会获得官方加量曝光,低质文章将会被系统打压、减少曝光,详情可以阅读这两个文档 (1)掘金签约作者品控文档(优质内容标杆); (2)【掘金社区规则更新】什么样的文章不会被推荐?

❓活动须知

  • 一篇文章只能参加一个活动,当天参与站内其他活动的文章,不计入更文天数。
  • 不需要连续更文,在活动期间 累计天数 达到就行。比如可以不连续、累计更新7天的文章,就能拿到第一关奖励。
  • 让大家写“第N天”是为了方便大家创作时自己统计天数,做到心中有数、不用每次都往回翻。所以,大家自己写的“第N天”与后台统计更文天数无关。
  • 活动统计更文的“天数”,而不是“篇数”,即,1 天发 N 篇,仅计 1 天;天数统计以“文章创建时间”为准,而非“修改时间”,即,1 篇文章修改 N 次,不会重复计算天数。
  • 允许搬运自己发在个人博客或者其他平台的文章,但是仅支持搬运自己的原创文章,搬运他人的文章当抄袭处理。
  • 每个阶段奖品不会单独发放,会在活动结束后根据各自发文情况统一发放所获得的奖品。
  • 如某项奖品库存不足,运营同学将根据实际库存更换为同等价值的其他奖品,或者通知部分同学延期发货。

更多 QA 可以查看飞书文档 掘金日新计划 · 6 月更文挑战 QA ,有疑问可以在⬅️飞书文档底部评论,运营同学会进行解答。

👥 加群交流

参加活动的掘友添加掘金酱为好友,回复关键词「更文挑战」入群。重要消息不错过,大家互相鼓励,也可以在群内咨询哦~

如果你是参与往期更文活动的老用户且已经在更文活动群(任一群内),请勿重复进群。

👀 如果你是新手,可以看看这些文章

如果你是新手,不知道从哪里下笔写作,可以参考以下文章方向:

  • 行业见解及职业选择: (一定要是程序员的行业见解及职业选择) 通过自己在行业的理解和沉淀,加以事实的依据,帮助他人更完整地理解行业的发展情况以及作为开发者的职业选择、职业成长,让读者在技术之外全面提升

小提醒:🎁 记得在本文评论区发参赛文章~ 从下周开始,每周三会抽幸运奖哦

预热活动开奖

预热活动规则:

在本文评论区发布「坚持,持续创作;思考,加速成长;挑战6月更文!」即可参与抽奖,我们将在 5 月 25 日抽取 3 位幸运同学,分别送出 掘金鼠标垫 / 掘金抱枕 / 空调毯。

预热活动开奖:

恭喜以下小伙伴获得掘金周边:

企业微信20220525-162109@2x.png

]]>
Fri, 20 May 2022 06:33:20 GMT https://juejin.cn/post/7099702781094674468 https://juejin.cn/post/7099702781094674468
<![CDATA[OOP 思想在 TCC/APIX/GORM 源码中的应用]]> 名词解释

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 思想,写出更优秀的代码。

OOP 原则

单一职责原则(SRP)

一个类只负责一个职责(功能模块)。

开放封闭原则(OCP)

一个类、方法或模块的扩展性要保持开放,可扩展但不影响源代码(封闭式更改)

替换原则(LSP)

子类可以替换父类,并且不会导致程序错误。

接口隔离原则(ISP)

一个类对另一个类的依赖应该建立在最小的接口上。

依赖倒置原则(DIP)

高层次的模块不应该依赖于低层次的模块,它们应该依赖于抽象。

参数可选,开箱即用—函数式选项模式

解决问题:在设计一个函数时,当存在配置参数较多,同时参数可选时,函数式选项模式是一个很好的选择,它既有为不熟悉的调用者准备好的默认配置,还有为需要定制的调用者提供自由修改配置的能力,且支持未来灵活扩展属性。

TCC 在创建BConfigClient对象时使用了该模式。BConfigClient是用于发送 http 请求获取后端服务中 key 对应的 value 值,其中getoptions结构体是 BConfigClient 的配置类,包含请求的 cluster、addr、auth 等信息,小写开头,属于内部结构体,不允许外部直接创建和修改,但同时对外提供了GetOption的方法去修改getoptions中的属性,其中WithCluster ```

TonyRL commented 2 years ago

/juejin/tag/架构 works as of replying.