分类 学习笔记 下的文章

2020-11-30T13:14:59.png

引言

开发过微信小程序的同学想必都对wx.showModal不陌生。用起来还是比较方便的,以api的形式挂载在全局对象wx上,只需调用一下这个api即可显示一个弹窗,还可以根据设置的参数做一些定制。一些知名的组件库,也实现了此类功能。比如element的$message。所以,我也来分享一个简单实现方式,以此来加深对Vue的理解。

代码结构

2020-11-30T15:17:32.png

在Message目录下,有着两个文件,vue文件负责组件内容,js负责处理服务APi。

实现原理

首先使用vue文件创建一个弹窗组件,然后按正常写组件方式写一个组件逻辑。唯一不同的是引入方式不一样,不在别的组件中引用该组件。使用js文件将其注册到全局api,调用该api。

实现过程

注意:本文使用的是Vue3.x版本。由于本人Vue3.x也正出于学习之处,如有理解/实现不当之处,恳请指正!

Vue文件

<template>
    <div v-if="visible" class="tm-message-wrapper">
        <div class="tm-message-dialog">
            <div class="tm-message-title">{{ title }}</div>
            <div class="tm-message-content">{{ content }}</div>
            <div class="tm-message-noRepeat" v-if="showNoRepeat"><input type="checkbox" v-model="noRepeat">不再提示</div>
            <div class="tm-message-operation-area">
                <div class="tm-message-cancel tm-message-operation" @click="action('cancel')">取消</div>
                <div class="tm-message-confirm tm-message-operation" @click="action('confirm')">确定</div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            uid: "default",
            visible: false,
            title: "提示",
            content: "消息内容",
            showNoRepeat: false,
            noRepeat: false,
            callBack: null
        }
    },
    methods: {
        action(action) {
            this.visible = false
            if (action == "confirm" && this.showNoRepeat && this.noRepeat) {
                window.localStorage.setItem(this.uid, true)
            }
            if (this.callBack instanceof Function) {
                this.callBack(action)
            }
        }
    }
}
</script>

该文件没有什么特别值得注意的地方,唯一一个跟普通组件写法不一样的就是多了一个callBack回调。

js文件

import { createApp } from 'vue'
import Message from "./index.vue"


const msg = options => {
    const NoticeInstance = createApp(Message)
    let msgNode = document.createElement('div');
    NoticeInstance.vm = NoticeInstance.mount(msgNode);
    if (options && Object.keys(options).length > 0) {
        Object.assign(NoticeInstance.vm, options)
    }
    console.log(NoticeInstance)
    NoticeInstance.vm.visible = true;
    NoticeInstance.dom = NoticeInstance.vm.$el;
    document.body.appendChild(NoticeInstance.dom)
    return NoticeInstance.vm
}
export default msg;

js文件首先创建了一个app实例,然后将其挂载到一个新的html元素,随后进行一些属性的赋值操作。最后将实例追加之文档的body末尾。

引入

main.js

import { createApp } from 'vue'
import App from './App.vue'

import Message from "../tm-ui/components/Message"

const app = createApp(App)
app.config.globalProperties.$message = Message;

app.mount('#app')

调用

test.vue

<template>
  <div @click="showModal" class="bg-blue text-lg">
      点我弹窗(wx.showModal)
  </div>
</template>

<script>
export default {
    methods:{
        showModal(){
            this.$message({
                title:"弹窗标题",
                content:"弹窗内容",
                callBack: (action) => {
                    if(action == 'confirm') {
                        console.log("点击了确定")
                    }
                }
            })
        }
    }
}
</script>

<style>

</style>

2020-11-30T15:18:35.png

后记

相比于Vue2.x,vue3.x的设计理念更偏向于函数式编程,比如不在默认导出Vue对象,而是使用createApp的方式创建一个实例。而这个showModal的实现虽然达到了效果,但用起来依旧缺少了函数式编程的美感。相比于微信小程序showModal的对象函数回调写法。我个人更偏向于element的messag实现。返回一个Promise对象,确认操作在then()调用里面,取消操作在catch()异常处理。感觉这样会更具有与Vue3统一的编程式美感。

笔者初学前端时,遇到一些具有padding/border的子元素铺满父元素时,常常采用人工计算width的方式来实现效果。知道后来了解到box-sizing,才知道那种实现方式是多么的尴尬!

box-sizing

box-sizing 属性允许你以某种方式定义某些元素,以适应指定区域。

box-sizing具有三个值

  1. content-box,默认值,指定盒模型为 W3C 标准模型,设置 border、padding 会增加元素 width与 height 的尺寸。
  2. border-box,指定盒模型为 IE模型(怪异模式),设置 border、padding 不会影响元素 width 与 height 的尺寸。
  3. inherit,继承

举个例子

<template>
<block>
  <div class="parent"></div>
 <div class="child"></div>
</block>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {
    msg: String
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
div{
  display: inline-block;
  height: 100px;
  width: 100px;
  padding: 10px;
  border-width: 10px;
  margin: 10px;
}
.parent {
  box-sizing: content-box;
}
 .child {
    box-sizing: border-box;
  }
</style>

效果展示

parent

2020-09-20T14:40:14.png

上图我们可以看到,content-box下,width的宽度只是content的宽度,元素的实际宽度等于width+border+padding

child

2020-09-20T14:42:40.png

而在border-box,width的宽度指的是边框盒子的宽度,元素的实际宽度就等于width

介绍

Vue中的指令形如v-*,如v-if,v-show,v-model等。同时,除了Vue自带的一些默认指令外,Vue同时运行用户自定义指令,来扩展指令功能。

使用场景

究竟什么情况下使用组件?而什么情况下使用指令呢?官方对此,给出了以下建议:

注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

也就是说,当你关注问题的抽象层级在DOM元素的时候,你就应该考虑使用自定义指令来完成需求了。

作用范围

自定义指令有两种作用范围,一种是申明在全局,全局内有效。另一种,则是局部指令。

钩子函数及相关参数见官网文档。

举个例子

需求

我需要设计一个自定义指令v-bug,该指令可将作用元素的所有内容都替换为烫烫烫烫,指令可以指定有几个烫,以及是否追加锟斤拷

1. 先将内容替换为一个

main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

Vue.directive('bug', {
  inserted: function (el) {
    el.innerHTML = "烫"
  }
})

new Vue({
  render: h => h(App),
}).$mount('#app')
App.vue
<template>
  <div id="app">
    <HelloWorld v-bug />
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

效果展示

2020-09-20T05:28:47.png

2. 生成指定个数的

main.js
...
Vue.directive('bug', {
  inserted: function (el, binding) {
    el.innerHTML = Array(binding.value).fill("烫").join("")
  }
})
...
App.vue
<template>
  <div id="app">
    <HelloWorld v-bug="12" />
  </div>
</template>

2020-09-20T05:29:25.png

3. 指定是否追加锟斤拷

main.js
...
Vue.directive('bug', {
  inserted: function (el, binding) {
    let html = Array(binding.value).fill("烫").join("")
    if(binding.modifiers.suffix){
      html += "锟斤拷"
    }
    el.innerHTML = html
  }
})
...
App.vue
<template>
  <div id="app">
    <HelloWorld v-bug.suffix="12" />
  </div>
</template>

2020-09-20T05:30:18.png

但是此时的代码有个非常严重的问题,从上文代码中可看出,内容的改变只在inserted的时候执行。如果我们将指令后面设置为变量,变量值的改变却不会影响到个数(这一点都不Vue),所有我们应再监听一下指令的updated钩子函数。

4. 数据驱动(动态改变)

App.vue
<script>
import HelloWorld from "./components/HelloWorld.vue";

let interval = null
export default {
    name: "App",
    components: {
        HelloWorld
    },
    data() {
        return {
            num: 12
        };
    },
    created() {
        interval = setInterval(() => {
            this.num++;
        }, 1000);
    },
    beforeDestroy(){
      clearInterval(interval)
    }
};
</script>
main.js
...
function generateHtml(binding){
  let html = Array(binding.value).fill("烫").join("")
  if (binding.modifiers.suffix) {
    html += "锟斤拷"
  }
  return html
}
Vue.directive('bug', {
  inserted: function (el, binding) {
    el.innerHTML = generateHtml(binding)
  },
  update: function (el, binding) {
    if (binding.oldValue !== binding.value) {
      el.innerHTML = generateHtml(binding)
    }
  },
})
...

2020-09-11T14:22:08.png
需求完成!

2020-08-24T14:58:17.png

引言

总所周知,js是单线程的。单线程意味着,js代码在执行的任何时候,都只有一个主线程来处理所有的任务(为了避免多线程可能产生的诸多问题,直接阉割多线程的可能性)。然而,随着硬件设备的快速发展,网页要做的事情会变得越来越多。人们慢慢意识到,不能让单线程限制了js的效率,然后web worker便应运而生。

栈,堆,队列

2020-08-24T14:58:35.png

基本数据结构知识略过~

讲讲它们在js语言层面的应用。

执行栈:函数调用形成了一个由若干帧(执行上下文)组成的栈。

消息队列:一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。

在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

任务队列

在js事件循环机制中,存在多种任务队列,它们可分为宏任务(macro-task)和微任务(micro-task)两种。

  • 宏任务包括:setTimeout、setInterval、I/O、UI rendering
  • 微任务包括:process.nextTick、Promise、Object.observe(已废弃)、MutationObserver(html5新特性)

事件循环

2020-08-24T14:58:17.png

如上图所示,事件循环就是在主线程清空执行上下文栈后空闲之时,先去微任务队列中读取待执行程序,并装载到主线程中。如果没有内容(各类微任务队列都清空),则将去宏任务队列中寻找。如此往复,周而复始,直至结束。

参考文档

并发模型与事件循环-MDN
详解JavaScript中的Event Loop(事件循环)机制-知乎
JS事件循环机制(event loop)之宏任务/微任务-掘金
JavaScript 事件循环机制-掘金

Vue环境变量和模式

Vue环境变量和模式

我最开始使用vue的判断环境的方法是

let BASE_URL = process.env.NODE_ENV == "development" ? "http:/localhost" : "xxx"

在这种情况下,需要先判断它的编译环境,进而根据环境适用对应的值。如果只有一个地方的变量跟环境有关,情况还好。但是若有多个,则需要复制多个判断或者封装成一个方法。但也只能使用于两个环境,如果存在多个环境,诸如开发,测试,预发布,正式等环境,这个方法就捉襟见肘了。所以,我们需要设置Vue环境变量和模式,好像webpack也可以实现?(好吧,这不是重点)

实现方式

vue允许你在项目根目录下添加.env.[mode]的文件来指定环境变量。

.env                # 在所有的环境中被载入
.env.local          # 在所有的环境中被载入,但会被 git 忽略
.env.[mode]         # 只在指定的模式中被载入
.env.[mode].local   # 只在指定的模式中被载入,但会被 git 忽略

比如我,我添加了.env.local,.env.development,.env.test,.env.production等文件,.env.local文件内容如下。

.env.local
VUE_APP_BUILD_MODE = 'development'
VUE_APP_BASE_URL = 'http://172.16.6.132:8002/threemiju/'

对应package.json的script如下:

"scripts": {
    "serve": "vue-cli-service serve --mode",
    "serve:test": "vue-cli-service serve --mode test",
    "build:test": "vue-cli-service build --mode test",
    "build:production": "vue-cli-service build --mode production",
    "deploy:test": "git push && git push gitlab dev && vue-cli-service build --mode test"
  },
请注意,此处有一个坑。当你使用yarn run build:test的时候,你会发现,假如你使用了异步组件,却会神奇的发现,它并没有把把代码切割,只编译成了一个app.js文件,这是由于你使用了--mode test, 导致它认为你并不是prodution环境。所以,你需要对.env.test做以下修改
  • .env.test
NODE_ENV = 'production'
VUE_APP_BUILD_MODE = 'test'
VUE_APP_BASE_URL = 'https://m.gileey.cn/threemiju/'

主动设置NODE_ENV即可。

环境变量的使用场景

  1. BASE_URL
export const baseURL = process.env.VUE_APP_BASE_URL;
  1. BUILD_MODE

在显示版本号时同时显示编译环境

<span :title="mode">{{ version }}</span>
data(){
return {mode: process.env.VUE_APP_BUILD_MODE}
}

本地变量(*.local)的一些应用场景

  1. BASE_URL: 在不同的开发机器下,请求不同内网地址
  2. APP_SECRET: 该字段不宜出现在共有仓库

2020-06-23T12:06:46.png

tips: 请注意区分vue3.0和vue cli 3.x/4.x

创建项目

vue create -r https://registry.npm.taobao.org vue-next-test

2020-06-23T12:01:58.png

选择你喜欢的配置即可,创建成功后,从Demo来看,跟vue2.0的typescript写法差别看起来不大的样子。。。

未完待续

typescript

本文环境

  1. @vue/cli 4.3.1
  2. typescript 3.9.5

相关配置文件

  1. vue.config.js
  2. tsconfig.json
  3. shims-vue.d.ts

问题汇总

导入 vue 文件报错

错误信息:Cannot find module './App.vue' or its corresponding type declarations
解决方案:修改shims-vue.d.ts

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

Vscode 报错,编译不报错

解决方案:重启 Vscode

挂载原型$api 报错

解决方案:在src目录下新增vue-property.d.ts

import Vue from 'vue'
declare module "vue/types/vue" {
  interface Vue {
    $api: any;
  }
}

无法使用@components别名 alias 路径

解决方案: 修改tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": ".",
    "types": ["webpack-env", "vuex"],
    "paths": {
      "@/*": ["src/*"],
      "@/components": ["src/components"] // 添加这一行
    },
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
  "exclude": ["node_modules"]
}

该文章为异教徒解决方案,各位看官看下即可,切勿模仿!!!

问题描述

原生的el-upload只支持上传图片时候的预览和回显,这是因为只针对img标签做了适配,如下图。

2020-06-03T05:42:34.png

而如果我们上传视频,则会出现一个白方框,用户体验不佳。

2020-06-03T05:45:07.png

解决思路

其实正确的解决思路应该是

  1. show-file-list属性设置为false。然后再自己循环显示file-list,以及追加对应的预览,删除按钮及功能。
  2. 设置自定义模板内容(推荐使用)

但是我觉得这样太麻烦了(时间问题)。

于是我突发奇想,如果我将img标签改成video标签呢?如下图

2020-06-03T05:46:56.png

发现居然完美契合,毫无违和感。

预览的实现

在做到把img改为video标签之前,还需要解决的另一个问题就是,如何让视频也支持预览。老规矩,我们先来看看官方Demo怎么实现预览的。

2020-06-03T05:54:03.png

官方的做法是增加一个dialog,然后在点击预览图片时将文件url传给dialog。我们先来实现一下改写一下dialog

<el-dialog :visible.sync="dialogVisible" :modal-append-to-body="true">
      <video width="100%" muted autoplay="autoplay" loop="loop" v-if="dialogImageUrl[dialogImageUrl.length - 1] == 4" :src="dialogImageUrl"></video>
      <img width="100%" v-else :src="dialogImageUrl" alt />
    </el-dialog>
tips: 我这种判断MP4格式的方式实属异端,不建议模仿。

替换img标签

一到标签节点的操作,我第一想到的就是document操作(异端+1),直接上代码。

 changeVideoTag(){
      let videoTag = document.querySelector('.video img')
      console.log('检测到应为video的img标签', videoTag)
      if(videoTag){
        let parentNode = videoTag.parentNode
        let newElement = document.createElement('video')
        newElement.setAttribute('class', videoTag.getAttribute('class'))
        newElement.setAttribute('src', videoTag.getAttribute('src'))
        parentNode.insertBefore(newElement, videoTag)
      }
    },

该函数负责寻找video类下的img标签,然后在img标签之前,添加一个同样的videos元素节点,此处你可以选择是否移除原img标签。

最终实现效果

gif.gif

最后的话

  1. 这种方法非常不推荐使用,强烈建议使用自定义模板缩略图
  2. 这种方法非常不推荐使用,强烈建议使用自定义模板缩略图
  3. 这种方法非常不推荐使用,强烈建议使用自定义模板缩略图

u=3276836620,1954497454&fm=26&gp=0.jpg

remove-files-webpack-plugin

原理就是在项目中添加一个webpack插件,然后配置插件
项目根目录新增vue.config.js

const path = require('path')
const RemovePlugin = require('remove-files-webpack-plugin')

module.exports = {
    configureWebpack: {
        plugins: [
            new RemovePlugin({
                after: {
                    root: path.join(__dirname, './unpackage'),
                    include: [
                        path.join(__dirname, 'unpackage/dist', process.env.NODE_ENV === 'production' ?
                            'build' : 'dev', process.env
                            .UNI_PLATFORM, './mp-weixin/static/APPPIC')
                    ],
                    trash: false
                }
            })
        ]
    }
}

部分webpack类似的webpack插件

  1. copy-webpack-plugin
  2. clean-webpack-plugin

持续集成

Gitlab的持续集成

我们可以将整个运行机制,看作一个赏金猎人接任务,执行任务,并完成任务的过程。

GitLab-CI

简单来说,这就是一个任务发布平台。运行在gitlab服务器,监听代码状态变化,并发布对应的任务。

GitLab-Runner

而每个runner就是一位赏金猎人,是任务的执行者。

2020-05-22T06:04:52.png

.gitlab-ci.yml

任务的发布者,规定什么时候触发任务,任务的具体内容。

配置流程

经过前面的解释,整个思路就很清晰了。我们需要做的有三件事。

  1. 编写.gitlab-ci.yml文件,设置对应的任务
  2. 部署Runner,激活赏金猎人
  3. 配置ci,邀请赏金猎人加入系统

部署Runner

这一步需要一个服务器,能run起来赏金猎人。

安装

请务必安装最新版,不然会出现很多未知的问题

  1. 下载二进制文件
# Linux x86-64
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# Linux x86
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386

# Linux arm
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm

# Linux arm64
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64
  1. 授予执行权限
sudo chmod +x /usr/local/bin/gitlab-runner
  1. Create a GitLab CI user:
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
  1. Install and run as service:
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

加入任务系统

注册

sudo gitlab-runner register

然后就是一些简单的配置,配置完成后就将该Runner注册到任务发布平台了,然后就可以接任务了。详细见参考文献【1】

编写.gitlab-ci.yml任务

本机部署版本
.gitlab-ci.yml

stages:
  - deploy

cache:
  paths:
    - node_modules/
    - public/

deployJob:
  stage: deploy 
  script:
    - npm install 
    - npm run build
    - rm -rf /home/data/three_miju_shopper_manager_system_front/*
    - cp -rf ./dist/* /home/data/three_miju_shopper_manager_system_front/
    - sh ./bot.sh ${CI_COMMIT_REF_SLUG} ${CI_COMMIT_SHA:0:8} ${CI_COMMIT_MESSAGE}
  tags:
    - shared_test_machine_runner
  only:
    - dev

这个版本具有企业微信群机器人推送功能,需要配置./bot.sh

#!/usr/bin/env bash
curl '群机器人地址' \
      -H 'Content-Type: application/json' \
      -d '
      {
        "msgtype": "markdown",
        "markdown": {
          "content": "商户端代码已更新,分支:'$1' 提交:'$2'
          更新:'$3'
          已发布,[点击测试](http://test.shop.gileey.cn)"
        }
      }'

远程推送版本

stages:
  - deploy

cache:
  paths:
    - node_modules/
    - public/

deployJob:
  stage: deploy 
  script:
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_dsa
    - chmod 600 ~/.ssh/id_dsa
    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
    - rsync -avzu --progress ./dist/* root@thinkmoon.cn:/www/wwwroot/3ju.psyannabel.cn/
  tags:
    - shared_test_machine_runner
  only:
    - dev

该版本在gitlab-runner机器上执行编译等工作,编译完成后使用rsync同步到云服务器,需要配置私钥变量$SSH_PRIVATE_KEY

2020-05-30T14:20:12.png

遇到的问题

导入自定义组件时一直报错:This dependency was not found:

出现背景:由于以前命名组件是"clickImg",后改成"ClickImg",由于linux的区分大小写,所以会一直没找到。

解决方案:换个名字???

参考文献

  1. 前端的gitlab的ci初尝试
  2. Install GitLab Runner manually on GNU/Linux