文件切片上传(断点续传)

上传文件体积一大,要等很久。如果只是水管小,等一会也就算了。但是如果网络差,上传到 99% 突然断网了又重新开始,就比较难受了。所以又有了文件切片进行断点上传的方法。

上传的文件对象是一个 File 对象,然后它是 Blob 对象的一个子类。再然后它有一个 slice 方法,用这个方法就可以对文件进行切片,就和数组的这个方法一样。用法如示例。

话不多说直接上代码(里面代码是问了 chatgpt 然后改的,哈哈)。把下面两份代码分别保存为 index.htmlserver.js,安装好依赖,然后启动执行 node server.js,访问 http://localhost:3000

前端也大概美化了一下上传组件,支持点击选择文件或者拖拽文件上传。原生的 <input type="file" ></input> 组件的样式应该没人用吧,那么丑…。其中的 loading 效果出自网上大佬的《纯css实现117个Loading效果》,里面挺多效果不错的。

Read More

nginx 的基础用法 & linux(centos)下支持 https 和 http2

其实之前写过一篇相关的《记录下 nginx 使用配置》,关于 nginx 的一些稍微复杂的场景。然后发现日常的基础用法,反而记不住。这里就记录一下。

安装,mac 可以用 brew 下载。windows 的也很简单,去官网下载个压缩包就行了。至于 linux 的,就网上搜搜啦,我记得也很简单。

# 安装
brew install nginx

#查看
brew info nginx

查看信息,可以看到配置文件在 /opt/homebrew/etc/nginx/nginx.conf

命令

# 启动
nginx

# 刷新,改了 nginx.conf 文件,要重新生效
nginx -s reload

# 关闭
nginx -s stop

默认端口是 8080。所以直接访问本地地址 http://localhost:8080/,看到有显示 Welcome to nginx! 字样就是启动成功了。

其实 nginx.conf 已经写了例子和注释,这里只是稍微补充点。

root:资源文件夹
index: 默认的 index 文件

location / {
  root   /电脑地址/nginx;
  index  index.html index.htm;
}

如果要配置不同路由访问不同的文件,root 要改成 alias,比如访问 http://localhost:8080/page

location /page {
  alias /电脑地址/nginx;
  index page.html;
}

当然最好是写个兜底返回。在 vue 或者 react 使用 router 时,兜底返回默认 index.html。不然每增加一个路由就要写多一个配置很麻烦。

location ~* ^/* {
  root   /电脑地址/nginx;
  try_files $uri $uri/ /index.html;
}

注意
root 与 alias
两者区别在于 nginx 如何解释 location 后面的 url

root:
语法:root path
默认值:root html
配置段:http、server、location、if
处理结果:root 路径+ location 路径

alias:
语法:alias path
配置段:location
处理结果:使用 alias 路径替换 location 路径

所以用正则匹配写路径的要注意写好 root 或 alias,不然找不到资源就会出现 403!

可以写多个server,启动多个服务

server {
  listen       3000;
  server_name  0.0.0.0;

  location / {
    root   /电脑地址/nginx;
    index  3000.html 3000.htm;
  }
}

代理 ~ 为区分大小写,~*为不区分大小写,其他符号则请查询官网啦。

location ~ /api/* {
  proxy_pass   http://localhost:3000;
}

https服务,声明好证书即可。这里只是把其中证书配置列出来,实际上 nginx.conf 的例子还有挺多配置,一般我们不是运维,应该默认就够了。作为调试用,可能都不需要启动到 https server。

server {
  ssl_certificate      /电脑地址/nginx/cert/cert.pem;
  ssl_certificate_key  /电脑地址/nginx/cert/server.key;
}

开启文本压缩

http {
  gzip on;
  # 压缩比例,比例越大,压缩时间越长。默认是1
  gzip_comp_level 6;
  # 哪些文件可以被压缩
  gzip_types text/xml text/plain text/css application/javascript application/x-javascript application/rss+xml;
}

linux 下支持 https 和 http2,也是网上搜集的,实操了可以,这里记录一下。

# 安装依赖
yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel
# 解压缩
tar -zxvf nginx-1.23.4.tar.gz
cd nginx-1.23.4
# 执行配置
./configure --with-http_ssl_module --with-http_v2_module
# 编译安装
make
make install
# 默认安装在
/usr/local/nginx
# 启动
/usr/local/nginx/sbin/nginx
# 刷新配置启动
/usr/local/nginx/sbin/nginx -s reload
# 关闭
/usr/local/nginx/sbin/nginx -s stop
# 修改配置
vim  /usr/local/nginx/conf/nginx.conf

从零搭建前端工程(下)

文章内容太多,分为上下两部分,这里是下半部分。上半部分在《从零搭建前端工程(上)》,此篇的内容有:
4. 使用 eslint + prettier 让代码健壮和优雅
5. 使用 husky + lint-staged 强制增量各类检查
6. 使用 @commitlint/cli 规范提交信息
7. 使用 埋点(性能 + 错误) 让项目运行更好



四、使用 eslint + prettier 让代码健壮和优雅

到 eslint 了,是不是让人又爱又恨。刚接的时候应该很不爽吧,动辄就来个错,这也错那也错。满屏尽是红 error。其实都是没有配好,也有处理好。eslint 是非常必要的,可以尽早发现一些错误及不合理,也可以统一一些写法,减少冲突等。比如,vue template 的属性顺序,import 的顺序等等。

循例先上代码示例,在项目根目录创建一份 .eslintrc.js,记得前面有个点的,上书代码:

module.exports = {
  env: {
    // 关键字指定你想启用的环境
    browser: true,
    es2021: true,
  },
  extends: [
    // 一个配置文件可以被基础配置中的已启用的规则继承
    'plugin:vue/essential',
    'plugin:vue/recommended',
    'standard',
    'plugin:prettier/recommended',
  ],
  parserOptions: {
    // 允许你指定你想要支持的 JavaScript 语言选项
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: [
    // 支持使用第三方插件,检查自定义的语法
    'vue', // 省略了 eslint-plugin- 前缀,插件全称为 eslint-plugin-vue
    'prettier',
  ],
  rules: {
    // 直接声明的 eslint 规则
    semi: ['error', 'always'], // 规则为:需要结束分号,优先级为 error,即抛错
    // 当最后一个元素或属性与闭括号 ] 或 } 在 不同的行时,要求使用拖尾逗号
    // 当在 同一行时,禁止使用拖尾逗号。https://eslint.bootcss.com/docs/rules/comma-dangle
    'comma-dangle': ['error', 'always-multiline'],
    'no-console': ['error', { allow: ['info', 'warn', 'error'] }],
    'no-unused-vars': ['error', { args: 'after-used' }],
    // "plugin1/rule1": "error" 配置定义在插件中的一个规则的时候,必须使用 插件名/规则ID 的形式
    'prettier/prettier': 'error',
  },
  globals: {
    var1: 'writable', // 对 var1 这个全局变量允许重写
    var2: 'readonly', // 对 var2 这个全局变量只允许读取
    var3: 'off', // 不支持 var3 这个全局变量
  },
};

说一下可能配置较多的。

Read More

从零搭建前端工程(上)

文章内容太多,分为上下两部分,这里是上半部分。下半部分在《从零搭建前端工程(下)》,此篇的内容有:
1. 使用 webpack 打包(编译)vue
2. 使用 babel 处理 js
3. 使用 webpack-dev-sever 做热调试开发



产生这个的想法来自于某天接手一个项目,eslint 没配好,想着弄好格式化一下,一时之前忘了 cli 命令。
想一下,现在我们用的各种框架,都自带了脚手架工具,初始化后基本上不用怎么改造就可以上手开发。导致很多工程化的东西被人忽视了,就容易遇到问题时被卡住或者东找西找翻资料。
所以就想着自己从头搭建一个项目看看,遇到的一些配置、工具、命令都给记录下来,方便后面再遇到时候就不会生手了。

这里我就用 vue 来做基础框架,用 react 也一样,就一些编译工具的差别,大部分是一样的。要做的事有:

  1. 使用 webpack 打包(编译)vue
  2. 使用 babel 处理 js
  3. 使用 webpack-dev-sever 做热调试开发
  4. 使用 eslint + prettier 让代码健壮和优雅
  5. 使用 husky + lint-staged 强制增量各类检查
  6. 使用 @commitlint/cli 规范提交信息
  7. 使用 埋点(性能 + 错误) 让项目运行更好

零、文件初始化

准备一个和 vue 脚手架创建出来的初始工程一样。有:

src:项目源代码
src/main.js:工程入口文件,在这里做 vue 初始化,也是 webpack 的entry
src/App.vue:一个主页面
src/components/HelloWorld.vue:一个子组件
src/assets/logo.png:一张图片资源
src/public:公共资源文件夹,这里放的东西依样拷贝进打包里(除了index.html)
src/public/index.html:用来做单页面工程入口 html 的基础模板

然后初始化一下 npm 环境:

npm init -y

-y 就是创建一份默认的,不需要一步步问要填什么。直接生成空信息,有需要自己再去填就好了。

Read More

懒加载-虚拟列表-下拉菜单选择器

最近,遇到 select 表单下拉选项过多的问题。由于业务场景需要,选项是由接口请求回来,数量取决于线上数据的多少。

借此,顺便记录下目前遇到 select 组件有哪些优化选择。附上一些例子,使用 vue 写的。当然用 react 也是类似的做法,这个和用什么框架关系不大。

为了文章内容不要太长,例子里的共同样式放在了最后。

一、懒加载

懒加载这个词各位应该也很熟悉。在一个古老的例子,瀑布流显示图片就有这个词语了。就是第一屏只显示几条,用户有兴趣往下拉,再继续加载(或请求)新的数据。
以前的话应该是监听 scroll 事件,判断是否要见底了,来补充新数据。后面出了一个厉害的 IntersectionObserver 方法。具体怎么用就不赘述了,总之就是能监听一个 dom 的显隐,来触发回调。

例子可看代码,支持前端自己分批数据或者每次下拉由后端处理分批

<template>
  <div ref="lazy-load-select" class="lazy-load-select" :class="{ active: active }">
    <a class="display-label" href="javascript:;" @click="active = !active">{{
      displayLabel
    }}</a>
    <div class="list" v-show="active">
      <input
        type="text"
        class="select-input"
        @input="search"
        v-model="searchText"
      />
      <ul class="select-ul" ref="select-ul">
        <li
          v-for="item in displayData"
          ref="select-li"
          :class="{ 'select-li': true, selected: item.value === currentValue }"
          :key="item.value"
          @click="select(item.value)"
        >
          {{ item.label }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
function genData({ pageno, count, keyword }) {
  return new Array(count).fill('1').map((_, index) => {
    const value = (pageno - 1) * count + index + 1 + '';
    return {
      value,
      label: 'LazyLoad-' + value + (keyword || ''),
    };
  });
}

const originData = genData({ pageno: 1, count: 100 });

// 调试用的声明,true:使用全部数据返回,由前端分批,false:分批从后端获取数据
const defaultProps = false ? {
  originData: () => originData,
  getData: undefined,
} : {
  originData: () => [],
  getData: ({ pageno, count, keyword }) => {
    return Promise.resolve(genData({ pageno, count, keyword }));
  },
};

// 上面是调试示例用的代码

const ONE_LOAD = 10; // 每次加载10条

const LAZY_LOAD_COMP = 2; // 倒数第几个开始新的加载

export default {
  name: 'LazyLoadSelect', // 懒加载下拉菜单,支持前端分批处理或后端分批处理
  props: {
    originData: {
      type: Array,
      default: defaultProps.originData,
    },
    getData: {
      type: Function,
      default: defaultProps.getData,
    },
  },
  data() {
    return {
      currentValue: 0,
      active: false,
      searchText: '',
      displayData: [],
      filterData: [],
      intersectionObserver: null,
    };
  },
  computed: {
    displayLabel() {
      const selectd = this.originData.find(
        item => item.value === this.currentValue
      );
      return selectd ? selectd.label : '请选择';
    },
    useRemoteData() {
      return typeof this.getData === 'function';
    },
    count() {
      return ONE_LOAD;
    },
    pageno() {
      return this.displayData.length / this.count || 1;
    },
  },
  methods: {
    async initFilterData(keyword) {
      this.displayData = [];
      if (this.useRemoteData) {
        // 如果有外部方法,初始化则不需要过滤,直接更新接口来的数据即可
        const data = await this.loadRemoteData();
        this.displayData = data;
      } else {
        // 如果没有外部方法,初始化则先根据搜索词过滤得出过滤数据,然后再对过滤数据分批
        this.filterData = this.originData.filter(item => {
          return keyword ? item.label.indexOf(keyword) !== -1 : true;
        });
        this.displayData = this.filterData.slice(0, 0 + ONE_LOAD);
      }
      this.$selectUl && (this.$selectUl.scrollTop = 0);
      this.updateObserveLayzload();
    },
    search(event) {
      // 应防抖
      this.initFilterData(this.searchText);
    },
    select(value) {
      this.currentValue = value;
      this.active = false;
      this.$emit('change', value);
    },
    updateObserveLayzload() {
      this.$nextTick(() => {
        if (this.displayData.length >= this.filterData.length && !this.useRemoteData) {
          this.$lazyLoadLi &&
            this.intersectionObserver.unobserve(this.$lazyLoadLi);
          return;
        }
        const $liArr = this.$refs['select-li'];

        this.$lazyLoadLi &&
          this.intersectionObserver.unobserve(this.$lazyLoadLi);
        this.$lazyLoadLi = $liArr[$liArr.length - 1 - LAZY_LOAD_COMP];
        this.intersectionObserver.observe(this.$lazyLoadLi);
      });
    },
    async loadRemoteData() { // 从外部方法获取数据
      try {
        const data = await this.getData(
            { pageno: this.pageno, count: this.count, keyword: this.searchText }
        );
        return data;
      } catch(error) {
        console.error('get data error:', error);
      }
      return [];
    },
    async getNewBatchData() {
      if (this.useRemoteData) {
        // 数据(包括筛选)完全由后端处理
        const addData = await this.loadRemoteData();
        return addData;
      }
      // 由前端对数据分批
      const addData = this.filterData.slice(
        this.displayData.length,
        this.displayData.length + ONE_LOAD,
      );
      return addData;
    },
    async updateData() {
      const addData = await this.getNewBatchData();
      this.displayData.push(...addData);
      this.updateObserveLayzload();
    },
  },
  created() {
    this.initFilterData('');
  },
  mounted() {
    this.$selectUl = this.$refs['select-ul'];
    this.intersectionObserver = new IntersectionObserver(entries => {
      if (entries[0].intersectionRatio) {
        this.updateData();
      }
    });

    const $main = this.$refs['lazy-load-select'];
    window.addEventListener(
      'click',
      (ev) => {
        // 判断是否点击在 select 组件上
        const isSelectComp = ev.composedPath().find(dom => $main === dom);
        if (!isSelectComp) {
          this.active = false;
        }
      }
    );
  }
};
</script>

Read More