尚华1

尚华1 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

尚华1 收藏了文章 · 4月16日

用了很多动效,介绍 4个很 Nice 的 Veu 路由过渡动效!

作者:Ahmad shaded
译者:前端小智
来源:sitepoint

有梦想,有干货,微信搜索 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq44924588... 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

Vue Router 过渡是向Vue应用程序添加个性的一种快速简便的方法。 它让我们可以在应用程序的不同页面之间添加平滑的动画/过渡效果。

如果使用得当,它可以让我们的应用程序更加现代和专业,从而增强用户体验。

在今天的文章中,我们介绍使用Vue Router过渡的基础知识,然后再介绍一些基本示例,希望能给大家一些启发和灵感。

下面我们要创建的四个过渡页面。

image

将 Vue 路由过渡添加到项目中

通常,Vue路由器设置如下所示

// default template
<template>
  <router-view />
</template>

在旧版本的Vue Router中,我们可以简单地用<transition>组件包装<router-view>

然而,在Vue Router的新版本中,我们必须使用v-slot来解构我们的 props ,并将它们传递到我们的内部插槽。这个slow包含一个被transition包围的动态组件。

<router-view v-slot="{ Component }">
  <transition>
    <component :is="Component" />
  </transition>
</router-view>

每个 Route 都有不同的过渡

默认情况下,用<transition>包装<component>将在我们使用的每条路由上添加相同的过渡。

有两种不同的方法可以为每个路由定制转场。

将过 transition 移到各个组件部分

首先,我们可以将<transition>移到每个单独的组件中,而不是用<transition>组件来包装我们的动态组件。 如下:

// app.vue
<template>
  <transition>
    <div class="wrapper">
      <!-- -->
    </div>
  </transition>
</template>

对于我们想要每个路由都有一个过渡效果,通过这种方式,我们可以通过过渡的名称来定制每个路由。

使用 v-bind 的动态过渡

另一种方法是将过渡的名称绑定到一个变量。然后,我们可以根据监听路由动态地改变这个变量。

<transition :name="transitionName">
  <component :is="Component" />
</transition>
watch: {
  '$route' (to, from) {
    const toDepth = to.path.split('/').length
    const fromDepth = from.path.split('/').length
    this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
  }
}

现在,我们了解了Vue Router Transition 的基础知识,下面我们来看一些 Nice 的示例。

1 – Fade Vue Router Transitions

添渐隐页面过渡可能是我们可以添加到Vue应用程序中最常用的动效之一。

我们可以通过更改元素的opacity 来实现此效果。

首先,我们创建一个带有fade名称的 Vue Router transition。 还要注意的另一件事是,我们将过渡模式设置为 out-in

有三种不同的过渡模式:

  • default – 进入和离开过渡同时发生
  • in-out – 新元素的过渡先进入。然后,当前元素过渡出去。
  • out-in - 当前元素先过渡出去。然后,新元素过渡进来。

为了让新元素平滑地淡入,我们需要在开始新的过渡之前删除当前元素。所以我们使用 mode="out-in"

<transition>为我们提供了几个CSS类,它们在动画周期中被动态添加/删除。

有6个不同的过渡类(3个用于进入,3个用于离开)。

  1. v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  2. v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  3. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  4. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  5. v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
  6. v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被删除),在过渡/动画完成之后移除。

注意:当我们为过渡提供一个name属性时,这是默认名称。类的格式是name-enter-fromname-enter-active,等等。

我们希望进入和离开状态的opacity 为0。然后,当我们的过渡处生效状态时,对 opacity 进行动画的处理。

// fade styles!
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}


.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

最后的效果 :

image

2 – Slide Vue Router Transitions

我们要构建的下一个过渡是幻灯片过渡。

模板如下所示。 由于我们希望进入和离开过渡同时发生,因此使用默认模式即可。

// slide transition
<router-view v-slot="{ Component }">
  <transition name="slide">
    <component :is="Component" />
  </transition>
</router-view>

为了让例子更好看,我们给每个页面加上下面的样式:

// component wrapper
.wrapper {
  width: 100%;
  min-height: 100vh;
}

最后,在过渡样式里为要滑动的组件设置相关的属性。如果需要不同的滑动方向,只需更改CSS属性(top, bottom, left, right)。

// slide styles!
.slide-enter-active,
.slide-leave-active {
  transition: all 0.75s ease-out;
}


.slide-enter-to {
  position: absolute;
  right: 0;
}


.slide-enter-from {
  position: absolute;
  right: -100%;
}


.slide-leave-to {
  position: absolute;
  left: -100%;
}


.slide-leave-from {
  position: absolute;
  left: 0;
}

最终的效果:

image

3 – Scale Vue Router Transitions

创建缩放过渡与我们的淡入过渡非常相似。 我们再次将模式设置为 out-in,以便我们可以确保动画的正确顺序。

// scale transition!

<router-view v-slot="{ Component }">
  <transition name="scale" mode="out-in">
    <component :is="Component" />
  </transition>
</router-view>
.scale-enter-active,
.scale-leave-active {
  transition: all 0.5s ease;
}


.scale-enter-from,
.scale-leave-to {
  opacity: 0;
  transform: scale(0.9);
}

这里给整个网页提供黑色的背景色会让过渡看上去更干净。

image

4 – Combining Vue Router Transitions

创建过渡的方式有很多很多但是,我认为不要过度过的,刻意的去做过渡。 过渡动效应该是很小的,微妙的增强功能,而不是会让应用产生干扰因素。

我认为实现较好过渡是将一些更基础的过渡结合在一起。

例如,让我们将幻灯片放大和缩小合并为一个过渡。

<router-view v-slot="{ Component }">
  <transition name="scale-slide">
    <component :is="Component" />
  </transition>
</router-view>
.scale-slide-enter-active,
.scale-slide-leave-active {
  position: absolute;
  transition: all 0.85s ease;
}


.scale-slide-enter-from {
  left: -100%;
}


.scale-slide-enter-to {
  left: 0%;
}


.scale-slide-leave-from {
  transform: scale(1);
}


.scale-slide-leave-to {
  transform: scale(0.8);
}

image

~完,我是刷碗智, 我要去刷碗了,我们下期见!

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://learnue.co/2021/01/4-...

交流

有梦想,有干货,微信搜索 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq44924588... 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

查看原文

尚华1 收藏了文章 · 4月16日

从0搭建自己的webpack开发环境(一)

上期文章:前端自动化测试

又一个连载来啦!这次我们将分四篇文章来介绍如何从0构建一个webpack开发环境,了解其内部机制和原理,从而让我们更准确的掌握和使用webpack,下面开始我们的起步:

1.什么是Webpack?

webpack是一个现代 JavaScript 应用程序的静态模块打包器(module bundler),当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

图片

使用Webpack作为前端构建工具:

  • 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等;
  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等;
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载;
  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件;
  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器;
  • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过;
  • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。

webpack应用中有两个核心:

  • 1) 模块转换器,用于把模块原内容按照需求转换成新内容,可以加载非 JS 模块;
  • 2) 扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。

2.初始化项目

├── src   # 源码目录
│   ├── a-module.js
│   └── index.js

编写 a-module.js

module.exports = 'hello';

编写 index.js

let a = require('./a-module');
console.log(a);
这里我们使用CommonJS模块的方式引入,这种方式默认在浏览器上是无法运行的,我们希望通过 webpack 来进行打包!

3.webpack快速上手

3.1 安装

npm init -y
npm install webpack webpack-cli --save-dev

webpack默认支持0配置,配置scripts脚本

"scripts": {
  "build": "webpack"
}

执行npm run build,默认会调用 node_modules/.bin下的webpack命令,内部会调用webpack-cli解析用户参数进行打包。默认会以 src/index.js 作为入口文件。

这里也可以使用npx webpacknpx 是 5.2版本之后npm提供的命令可以执行.bin下的可执行文件

图片

我们发现已经产生了dist目录,此目录为最终打包出的结果。main.js可以在html中直接引用,这里还提示我们默认modeproduction

3.2 webpack.config.js

我们打包时一般不会采用0配置,webpack在打包时默认会查找当前目录下的 webpack.config.js or webpackfile.js 文件。

通过配置文件进行打包:

const path = require('path');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename:'bundle.js', 
        // 打包出的结果文件
        path:path.resolve(__dirname,'dist')// 打包到dist目录下
    }
}

3.3 配置打包的mode

我们需要在打包时提供mode属性来区分是开发环境还是生产环境,来实现配置文件的拆分。

├── build
│   ├── webpack.base.js
│   ├── webpack.dev.js
│   └── webpack.prod.js

我们可以通过指定不同的文件来进行打包

配置scripts脚本:

"scripts": {
  "build": "webpack --config ./build/webpack.prod",
  "dev": "webpack --config ./build/webpack.dev"
}

可以通过 config 参数指定,使用哪个配置文件来进行打包。

通过env参数区分

"scripts": {
    "build": "webpack --env.production --config ./build/webpack.base",
    "dev": "webpack --env.development --config ./build/webpack.base"
}

改造webpack.base文件默认导出函数,会将环境变量传入到函数的参数中。

module.exports = (env)=>{
    console.log(env); // { development: true }
}

合并配置文件

我们可以判断当前环境是否是开发环境来加载不同的配置,这里我们需要做配置合并
安装webpack-merge:

npm install webpack-merge --save-dev

webpack.dev配置

module.exports = {
    mode:'development'
}

webpack.prod配置

module.exports = {
    mode:'production'
}

webpack.base配置

const path = require('path');
const merge = require('webpack-merge');
// 开发环境
const dev = require('./webpack.dev');
// 生产环境
const prod = require('./webpack.prod');
const base = { 
    // 基础配置
    entry:'./src/index.js',
    output:{
        filename:'bundle.js',
        path:path.resolve(__dirname,'../dist')
    }
}
module.exports = (env) =>{
    if(env.development){
        return merge(base,dev);
    }else{
        return merge(base,prod);
    }
}

后续开发中,我们会将公共的逻辑放到base中,开发和生产中的配置也分别进行存放!

4.webpack-dev-server

配置开发服务器,可以在实现在内存中打包,并且自动启动服务。

npm install webpack-dev-server --save-dev
"scripts": {
    "build": "webpack --env.production --config ./build/webpack.base",
    "dev": "webpack-dev-server --env.development --config ./build/webpack.base"
}

通过执行npm run dev来启启动开发环境:

webpack2

默认会在当前根目录下启动服务

配置开发服务的配置

const path = require('path')
module.exports = {
    mode:'development',
    devServer:{
        // 更改静态文件目录位置
        contentBase:path.resolve(__dirname,'../dist'),
        compress:true, // 开启gzip
        port:3000, // 更改端口号
    }
}

5.打包Html插件

5.1 单入口打包

自动产生html,并且引入打包后的文件

编辑webpack.base文件

const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins:[
    new HtmlWebpackPlugin({
        filename:'index.html', // 打包出来的文件名
        template:path.resolve(__dirname,'../public/index.html'),
        hash:true, // 在引用资源的后面增加hash戳
        minify:{
            removeAttributeQuotes:true // 删除属性双引号
        }
    })
]

5.2 多入口打包

根据不同入口生成多个js文件,引入到不同html中:

── src
    ├── entry-1.js
    └── entry-2.js

多入口需要配置多个entry

entry:{
    jquery:['jquery'], // 打包jquery
    entry1:path.resolve(__dirname,'../src/entry-1.js'),
    entry2:path.resolve(__dirname,'../src/entry-2.js')
},
output:{
    filename:'[name].js',
    path:path.resolve(__dirname,'../dist')
},

产生多个html文件

new HtmlWebpackPlugin({
    filename:'index.html',
    template:path.resolve(__dirname,'../public/template.html'),
    hash:true,
    minify:{
        removeAttributeQuotes:true
    },
    chunks:['jquery','entry1'], // 引入的chunk 有jquery,entry
}),
new HtmlWebpackPlugin({
    filename:'login.html',
    template:path.resolve(__dirname,'../public/template.html'),
    hash:true,
    minify:{
        removeAttributeQuotes:true
    },
    inject:false, // inject 为false表示不注入js文件
    chunksSortMode:'manual', // 手动配置代码块顺序
    chunks:['entry2','jquery']
})

以上的方式不是很优雅,每次都需要手动添加HtmlPlugin应该动态产生html文件,像这样:

let htmlPlugins = [
  {
    entry: "entry1",
    html: "index.html"
  },
  {
    entry: "entry2",
    html: "login.html"
  }
].map(
  item =>
    new HtmlWebpackPlugin({
      filename: item.html,
      template: path.resolve(__dirname, "../public/template.html"),
      hash: true,
      minify: {
        removeAttributeQuotes: true
      },
      chunks: ["jquery", item.entry]
    })
);
plugins: [...htmlPlugins]

6.清空打包结果

可以使用clean-webpack-plugin手动清除某个文件夹内容:

安装

npm install --save-dev clean-webpack-plugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
new CleanWebpackPlugin({
    // 清空匹配的路径
    cleanOnceBeforeBuildPatterns: [path.resolve('xxxx/*'),'**/*'],
})

这样就可以清空指定的目录了,我们可以看到webpack插件的基本用法就是 new Plugin并且放到plugins中。

7.小结

现在是不是对webpack的使用有了初步印象或更多的想法了呢?大神们的大刀是不是快要按捺不住了呢?下期介绍webpack不得不会的各种配置,敬请期待!


图片描述

查看原文

尚华1 收藏了文章 · 4月15日

手把手教会你小程序登录鉴权

导语

为了方便小程序应用使用微信登录态进行授权登录,微信小程序提供了登录授权的开放接口。乍一看文档,感觉文档上讲的非常有道理,但是实现起来又真的是摸不着头脑,不知道如何管理和维护登录态。本文就来手把手的教会大家在业务里如何接入和维护微信登录态

接入流程

这里官方文档上的流程图已经足够清晰,我们直接就该图展开详述和补充。

img

首先大家看到这张图,肯定会注意到小程序进行通信交互的不止是小程序前端和我们自己的服务端,微信第三方服务端也参与其中,那么微信服务端在其中扮演着怎样的角色呢?我们一起来串一遍登录鉴权的流程就明白了。

1. 调用wx.login生成code

wx.login()这个API的作用就是为当前用户生成一个临时的登录凭证,这个临时登录凭证的有效期只有五分钟。我们拿到这个登录凭证后就可以进行下一步操作:获取openidsession_key

wx.login({
    success: function(loginRes) {
        if (loginRes.code) {
            // example: 081LXytJ1Xq1Y40sg3uJ1FWntJ1LXyth
        }
    }
});

2. 获取openid和session_key

我们先来介绍下openid,用过公众号的童鞋应该对这个标识都不陌生了,在公众平台里,用来标识每个用户在订阅号、服务号、小程序这三种不同应用的唯一标识,也就是说每个用户在每个应用的openid都是不一致的,所以在小程序里,我们可以用openid来标识用户的唯一性。

那么session_key是用来干嘛的呢?有了用户标识,我们就需要让该用户进行登录,那么session_key就保证了当前用户进行会话操作的有效性,这个session_key是微信服务端给我们派发的。也就是说,我们可以用这个标识来间接地维护我们小程序用户的登录态,那么这个session_key是怎么拿到的呢?我们需要在自己的服务端请求微信提供的第三方接口https://api.weixin.qq.com/sns/jscode2session,这个接口需要带上四个参数字段:

参数
appid小程序的appid
secret小程序的secret
js_code前面调用wx.login派发的code
grant_type'authorization_code'

从这几个参数,我们可以看出,要请求这个接口必须先调用wx.login()来获取到用户当前会话的code。那么为什么我们要在服务端来请求这个接口呢?其实是出于安全性的考量,如果我们在前端通过request调用此接口,就不可避免的需要将我们小程序的appid和小程序的secret暴露在外部,同时也将微信服务端下发的session_key暴露给“有心之人”,这就给我们的业务安全带来极大的风险。除了需要在服务端进行session_key的获取,我们还需要注意两点:

  • session_key和微信派发的code是一一对应的,同一code只能换取一次session_key。每次调用wx.login(),都会下发一个新的code和对应的session_key,为了保证用户体验和登录态的有效性,开发者需要清楚用户需要重新登录时才去调用wx.login()
  • session_key是有失效性的,即便是不调用wx.login,session_key也会过期,过期时间跟用户使用小程序的频率成正相关,但具体的时间长短开发者和用户都是获取不到的
function getSessionKey (code, appid, appSecret) {
    var opt = {
        method: 'GET',
        url: 'https://api.weixin.qq.com/sns/jscode2session',
        params: {
            appid: appid,
            secret: appSecret,
            js_code: code,
            grant_type: 'authorization_code'
        }
    };
    return http(opt).then(function (response) {
        var data = response.data;
        if (!data.openid || !data.session_key || data.errcode) {
            return {
                result: -2,
                errmsg: data.errmsg || '返回数据字段不完整'
            }
        } else {
            return data
        }
    });
}

3. 生成3rd_session

前面说过通过session_key来“间接”地维护登录态,所谓间接,也就是我们需要自己维护用户的登录态信息,这里也是考虑到安全性因素,如果直接使用微信服务端派发的session_key来作为业务方的登录态使用,会被“有心之人”用来获取用户的敏感信息,比如wx.getUserInfo()这个接口呢,就需要session_key来配合解密微信用户的敏感信息。

那么我们如果生成自己的登录态标识呢,这里可以使用几种常见的不可逆的哈希算法,比如md5、sha1等,将生成后的登录态标识(这里我们统称为'skey')返回给前端,并在前端维护这份登录态标识(一般是存入storage)。而在服务端呢,我们会把生成的skey存在用户对应的数据表中,前端通过传递skey来存取用户的信息。

可以看到这里我们使用了sha1算法来生成了一个skey:

const crypto = require('crypto');

return getSessionKey(code, appid, secret)
    .then(resData => {
        // 选择加密算法生成自己的登录态标识
        const { session_key } = resData;
        const skey = encryptSha1(session_key);
    });
    
function encryptSha1(data) {
    return crypto.createHash('sha1').update(data, 'utf8').digest('hex')
}

4. checkSession

前面我们将skey存入前端的storage里,每次进行用户数据请求时会带上skey,那么如果此时session_key过期呢?所以我们需要调用到wx.checkSession()这个API来校验当前session_key是否已经过期,这个API并不需要传入任何有关session_key的信息参数,而是微信小程序自己去调自己的服务来查询用户最近一次生成的session_key是否过期。如果当前session_key过期,就让用户来重新登录,更新session_key,并将最新的skey存入用户数据表中。

checkSession这个步骤呢,我们一般是放在小程序启动时就校验登录态的逻辑处,这里贴个校验登录态的流程图:

img2

下面代码即校验登录态的简单流程:

let loginFlag = wx.getStorageSync('skey');
if (loginFlag) {
    // 检查 session_key 是否过期
    wx.checkSession({
        // session_key 有效(未过期)
        success: function() {
            // 业务逻辑处理
        },
    
        // session_key 过期
        fail: function() {
            // session_key过期,重新登录
            doLogin();
        }
    });
) else {
    // 无skey,作为首次登录
    doLogin();
}

5. 支持emoji表情存储

如果需要将用户微信名存入数据表中,那么就确认数据表及数据列的编码格式。因为用户微信名可能会包含emoji图标,而常用的UTF8编码只支持1-3个字节,emoji图标刚好是4个字节的编码进行存储。

这里有两种方式(以mysql为例):

1.设置存储字符集

在mysql5.5.3版本后,支持将数据库及数据表和数据列的字符集设置为utf8mb4,因此可在/etc/my.cnf设置默认字符集编码及服务端编码格式

// my.cnf

[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqld]
character-set-client-handshake = FALSE
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

设置完默认字符集编码及服务端字符集编码,如果是对已经存在的表和字段进行编码转换,需要执行下面几个步骤:

  • 设置数据库字符集为utf8mb4
ALTER DATABASE 数据库名称 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
  • 设置数据表字符集为utf8mb4
ALTER TABLE 数据表名称 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  • 设置数据列字段字符集为utf8mb4
ALTER TABLE 数据表名称 CHANGE 字段列名称 VARCHAR(n) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

这里的COLLATE指的是排序字符集,也就是用来对存储的字符进行排序和比较的,utf8mb4常用的collation有两种:utf8mb4_unicode_ciutf8mb4_general_ci,一般建议使用utf8mb4_unicode_ci,因为它是基于标准的Unicode Collation Algorithm(UCA)来排序的,可以在各种语言进行精确排序。这两种排序方式的具体区别可以参考:What's the difference between utf8_general_ci and utf8_unicode_ci

2.通过使用sequelize对emoji字符进行编码入库,使用时再进行解码

这里是sequelize的配置,可参考Sequelize文档

{
       dialect: 'mysql',    // 数据库类型
       dialectOptions: {    
         charset: 'utf8mb4',
         collate: "utf8mb4_unicode_ci"
      },
}

最后

前面讲了微信小程序如何接入微信登录态标识的详细流程,那么如何获取小程序中的用户数据以及对用户敏感数据进行解密,并保证用户数据的完整性,我将在下一篇文章给大家做一个详细地介绍。

腾讯IVWEB团队的工程化解决方案feflow已经开源:Github主页:feflow

如果对您的团队或者项目有帮助,请给个Star支持一下哈~

查看原文

尚华1 收藏了文章 · 4月10日

Git GUI 的基本操作

写在前面的话:
这并不是一篇讲解Git原理和命令的文章,本文旨在分享通过鼠标点击GUI界面使用Git实现日常开发的版本管理。尽管仅使用少量的Git命令就能够满足日常版本管理,一上手就展示命令行还让是不少想了解Git的人望而却步。
看完这篇文章,你应该对Git的工作流程有基本的了解,并能够使用Git GUI进行日常开发的版本管理。

准备:

一、Git GUI基本操作

1、版本库初始化

gitpractise文件夹就变成了Git可以管理的仓库,目录下多了一个.git文件夹,此目录是Git用于管理版本库的,不要擅自改动里面的文件,这样会破坏Git仓库。(.git文件夹默认是隐藏的,如果你没有看到它,不要慌。)

在想要初始化的文件夹的空白处右键,选择Git GUI Here,新建版本库时文件夹会自动定位到当前文件夹。

2、GUI说明

工作区:列出有改动的文件
暂存区:存放将要提交到版本库的文件,工作区中修改完成的文件应将放入暂存区
差异区:在工作区/暂存区选择文件会显示出改动前后的具体信息
提交的说明:提交时写入改动的相关说明

Rescan:扫描出改动的文件,显示在工作区。GUI并不会实时更新对仓库的修改,需要点击Rescan按钮重新扫描。
Stage Changed:将工作区中所有文件放入暂存区。
Sign off:在提交的说明后面附加上当前git账号的信息。多人协作时方便看到提交的编辑者。
Commit:将暂存区的文件提交到版本库。
Push:推送到远程版本库。

3、新增文件

让我们从新增一个文件开始,在gitpractise中新建hello.txt文件,然后点击Rescan,可以看到hello.txt出现在工作区

对提交过的文件的修改是可以撤销的,通过Commit -> Revert Changes

4、暂存

Commit -> Stage To Commit

这里的操作是将选定的单个文件放入暂存区(快捷键是ctrl+t)。多个文件可以按住shift/ctrl进行选定。暂存全部直接点击Stage Changed

4.1、撤销暂存

提交前还想再编辑。Commit -> Unstage From Commit

也可以不撤销暂存,直接编辑,再次暂存,效果是一样的。

5、提交

6、版本节点和回滚

6.1、 查看版本节点

为了演示版本的回滚,我对hello.txt做了修改并提交。此时我们有两个版本。
Repository -> Visualize master's history

6.2、回滚,回到第一个版本

reset有三种模式:soft | mixed | hard ,不同的模式对工作区、暂存区和版本库的影响不同,具体如下图:

soft:回退版本提交历史,暂存区和工作区不变。

mixed:回退版本提交历史,暂存区文件与该版本一致,工作区不变。

hard:回退版本提交历史,暂存区和工作区文件与该版本一致。

二、分支与合并

在版本节点和回滚中,我可以看到,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master分支。
在GIt中,可以以任一分支为原型再创建分支,此时新的分支与原型拥有一致的版本库,对新分支的任何改动都不会影响原型分支,新旧分支开始维护各自的版本。
最后,可以通过合并分支,将不同分支的内容结合到一起。
分支让每个人拥有独立的工作空间,而合并能将所有人的工作成果归一,协同工作因此变得简单、高效。

1、创建一个分支

Branch -> Create

新建并切换工作仓库至branch1,接下来就可以修改、暂存、提交。跟在master分支的操作是一样的。

分支的切换:
Branch -> Checkout

切换前要保持当前分支的工作区和暂存区是干净的(clean),即没有未提交的修改。如果你修改了一半,这时由于各种原因,需要切换到其他分支工作,可以使用git stash命令,它会记录当前工作区和暂存区中文件的状态,把工作现场“储藏”起来,这样工作区就是干净的了。再次回到此分支,使用git stash pop命令,就能恢复现场。Git GUI不提供相关的操作,需要去Git bash执行这两个命令。

2、合并分支

为了演示,我还以master为原型新建了分支branch2
(1)、branch1中修改hello.txt的第一行为hi~world.,并新增文件world.txt
(2)、branch2中修改hello.txt的第一行为work hard pls.

我们看一下现在的分支情况:

2.1、合并branch1到master

在合并之前,我们思考一下,由于branch1是在master版本上进行了修改,我们期望的合并结果应该是master中hello.txt第一行被修改为hi~world.,并新增文件world.txt,接下来就是验证。
首先切换到主分支master,然后点击菜单栏的 Merge -> Local Merge

结果符合预期。再看一下此时的分支情况:

可以看到此时 master跟branch1指向了同一个版本节点。

2.2、合并branch2到master

接下来我们合并branch2到master。

合并失败!让我们仔细看一下错误提示:

提示框中有一句:Automatic merge failed,即自动合并失败。我们与合并branch1的提示做一下对比

我们看到了Fast-forward的提示。 由于master 是要并入的branch1分支的直接上游,master顺着走下去可以到达branch1分支,这种单线的历史分支不存在任何需要解决的分歧,合并过程称为快进(Fast forward)。

而合并了branch1之后的master不再是branch2的直接上游,此时合并branch2就会要求解决分歧(如果存在的话)。所以才有了Automatic merge failed: fix conflicts and then commit the result的提示。(自动合并失败,请解决冲突后再提交)。Git将文件中有冲突的地方都写入文件,同时显示在差异区,我们可以按图索骥,到指定文件中删除不想要的部分,然后暂存,再次提交。(这里的暂存是不可以通过点击Stage Changed实现的)

branch1和branch2完成了它们的使命,可以通过Branch -> Delete进行删除,需要注意一点:不能删除工作中的分支。

差异区的查看
前面的"-1,2"分成三个部分:减号表示改动前的文件,"1"表示第1行,"2"表示连续2行。合在一起,就表示下面是修改前的文件从第1行开始的连续2行。同样的,"+1,6"表示变动后,成为改动后的文件从第1行开始的连续6行。

三、远程协作

现在,我们以一个托管在GitHub上的ASP.NET MVC项目来进行远程协作演示。

1、 克隆远程仓库到本地

2、忽略文件

细心的你会发现根目录下有一个名为.gitignore的文件。它用于定义忽略规则(ignore rules),每一次暂存/提交之前,Git会根据该文件内容忽略指定文件/文件夹。 在实际的开发过程中,总有一些情况是我们不想让Git跟踪某些文件的。Git提供三种忽略文件的方式

2.1、创建局部.gitignore文件

在仓库的根目录下创建.gitignore文件,忽略规则仅作用于当前仓库。一般情况下,应该将.gitignore文件提交到版本库,这样就可以与克隆项目仓库的人共享忽略规则

Github上维护有一个适用于多种现流行的操作系统/环境/开发语言的官方推荐.gitignore文件的项目。还可以通过gitignore.io生成对应操作系统/开发语言/IDE的.gitignore文件。

2.2、创建全局.gitignore文件

我们还可以通过全局.gitignore文件将忽略规则应用到本机上所有Git仓库。
(1)、git config --global core.excludesfile ~/.gitignore_global。
(2)、在用户根目录下创建.gitignore文件。

2.3、显式排除出仓库

如果不想使用.gitignore文件,还可以通过往仓库根目录下.git/info/exclude文件添加忽略规则达到忽略本仓库指定文件/文件夹的目的。

1、如果一个文件已经添加到版本库,之后添加对应的忽略规则到gitignore/exclude文件并不会生效。这种情况下,需要通过以下命令先解除对文件的跟踪:
git rm --cached FILENAME
2、全局和局部的.gitignore文件会共同作用。

3、pull和push

pull和push分别表示从远程仓库获取信息和推送本地更新到远程仓库两个操作。

多人协作时,大家都会往master分支上推送各自的修改,小伙伴已经向master推送了他的提交,而碰巧我们对同样的文件作了修改,并试图推送:

得到错误提示:

推送失败,因为小伙伴的最新提交和我们的推送存在冲突,解决办法也很简单,Git已经提示我们,先用git pull把最新的提交从master抓下来,然后,在本地合并,解决冲突,在Git GUI中的Pull操作需要两个步骤来完成:

1、获取远程仓库版本记录 Remote -> Fetch

2、本地合并Merge -> Local Merge

解决冲突后,再推送:

写在最后的话
如果你认真看完了整篇文章,那么Git日常的操作应该能够使用Git GUI完成了,这也是本文的初衷。Git作为当前最强大的版本管理工具(没有之一)拥有的功能比本文写到的要多得多,然而很多功能是通过命令行实现的,GUI只是封装了一部分常用的功能,方便入门和日常操作。如果你想深入了解Git,这是Git的官方网站

THE END

原文链接

查看原文

尚华1 收藏了文章 · 2月10日

实现Lazyman

前言

之前在掘金上到一篇文章关于微信面试的文章,其中提到了手动实现Lazyman的问题。刚开始
看到Lazyman我是一脸懵逼的,这是什么鬼,后来查了查了一下,才发现,其实就是手动实现
以下功能:

实现一个LazyMan,可以按照以下方式调用:
LazyMan(“Hank”)输出:
Hi! This is Hank!
 
LazyMan(“Hank”).sleep(10).eat(“dinner”)输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~
 
LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出
Hi This is Hank!
Eat dinner~
Eat supper~
 
LazyMan(“Hank”).sleepFirst(5).eat(“supper”)输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
 
以此类推。

看起来好像挺有趣的,我们来分析以下

分析

首先我们看到例子有几个特点,一个是我们调用Lazyman的时候不需要用到new关键字,这意味着我们需要使用工厂函数;另一个是要我们实现链式调用。
我们看到LazyMan(“Hank”).eat(“dinner”).eat(“supper”)这样的形式,无疑是链式调用了;还有一个难点就是 LazyMan(“Hank”).sleepFirst(5).eat(“supper”)
当存在sleepFirst时,我们还要先等待一段时间,然后再开始报名字,这就说明sleepFirst优先级更高,不管何时注册,都要第一个执行,仔细想想
有什么可以实现这个呢?明显我们需要一个任务队列,而且sleepFirst放在最前面,然后等所有任务都安排好了,才开始执行任务队列
恩?那说明执行任务不能紧跟在插入任务全程的后面,那我们见他们分进两个事件队列就好了,这就需要借助setTimeout函数了;
除此之外,一个任务完成了,我们怎么通知任务队列去取下一个任务呢?这就需要一个尾调用。

编码

经过上面的分析,我们可以开始编码了:
首先,我们先写工厂函数

function Lazyman ( name ) {
    return new _Lazyman ( name );
}

接着我们开始实现Lazyman:

constructor ( name ) {
        this.tasks = [];//设置任务队列
        let task = (name => () => {
            console.log ( `Hi! This is ${name} !` );
            this.next ();
        }) ( name );
        this.tasks.push ( task );
        //通过settimeout的方法,将执行函数放入下一个事件队列中,从而达到先注册事件,后执行的目的

        setTimeout ( () => {
            this.next ();
        }, 0 );

    }
    //尾调用函数,一个任务执行完然后再调用下一个任务
    next () {
        let task = this.tasks.shift ();
        task && task ();
    }

    eat ( food ) {
        let task = (food => () => {
            console.log ( `Eat ${food}` );
            this.next ();
        }) ( food );
        this.tasks.push ( task );
        return this;
    }

    sleep ( time ) {
        let task = (time => () => {
            setTimeout ( () => {
                console.log ( `Wake up after ${time} s!` );
                this.next ();
            }, time * 1000 )
        }) ( time );
        this.tasks.push ( task );
        return this;
    }

    sleepFirst ( time ) {
        let task = (time => () => {
            setTimeout ( () => {
                console.log ( `Wake up after ${time} s!` );
                this.next ();
            }, time * 1000 )
        }) ( time );
        this.tasks.unshift ( task );//sleepFirst函数需要最先执行,所以我们需要在任务队列前面放入,然后再执行后面的任务
        return this;
    }

}

通过上面的步骤,我们就实现了一个简单的Lazyman了

改进

上面明明实现了一个Lazyman了呀,还有什么可以改进的?当然有,如果我们调用eat的时候,想输出的是Eaaaaaaaaaat ${food}!!!!,然后输出好饱啊好饱啊
这意味着每次改变,我们都要去修改eat这个函数,这就耦合度太高了,这时候,我们可以采用发布订阅的方式,在调用eat的时候,我们注册一个监听函数,然后当任务
执行的时候再发布这个事件,让对应的监听函数执行,这样就实现了解耦了

总结

一个小小的Lazyman,竟然有如此多的考点,是在让人受益匪浅,当然,Lazyman还可以使用promise的方式实现,当然实现一个手写的Promise实在有点难(逃),有机会再用promise
实现一次哈

查看原文

尚华1 提出了问题 · 2月9日

从github上clone代码后文件不显示绿勾是什么原因呢?

在github上新建了一个respositories,然后通过git clone、提交代码都没问题,但是本地的文件没有绿勾或者红勾,每次改动不知道改动了哪些,按照网上搜索的修改注册表后重启试了也没用:

按Win+R键打开运行对话框,输入 regedit.exe ,准备修改注册表;
找到 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer;
新建一个键名称为 “Max Cached Icons” 数据设置为 “2000”;
重启一下电脑,图标就可以显示了。
如果还不显示
1.win+r,regedit.exe,打开注册表 按照文件的层次关系依次找到“HKEY_LOCAL_MACHINE–>SOFTWARE–>Microsoft–>Windows–>CurrentVersion–>Explorer–>ShellIconOverlayIdentifiers”这一项
2.将Tortoise相关的项都提到靠前的位置(使其靠前的方法:重命名,在名称之前加几个空格)
3.重启资源管理器即可(任务管理器-->资源管理器(重新启动))
之后再去查看你的git文件夹,就发现绿色、红色图标可以正常显示了。

然后github的用户名和邮箱也全局配置了,但是就是没有绿勾,用svn拉取就有,请问是怎么回事,该怎么解决呢?

关注 2 回答 2

尚华1 回答了问题 · 2月6日

vue-cli的Library模式打包生成的umd.js引入到html后不能传参是什么原因呢?

终于找到原因了,props名称驼峰改为-就行了,浏览器把大写解析为小写的了,而在cli中template会通过js处理后再渲染,草率了

关注 1 回答 1

尚华1 收藏了问题 · 2月5日

求不使用vue-cli直接html引入的vue多页面项目

要求很奇葩,有没有像这样直接引入来做项目的

<script data-original="js/vue.js" type="text/javascript" charset="utf-8"></script>
<script data-original="//cdn.bootcss.com/vue-router/2.0.3/vue-router.js" type="text/javascript" charset="utf-8"></script>
<script data-original="//cdn.bootcss.com/vue-resource/1.0.3/vue-resource.js" type="text/javascript" charset="utf-8"></script>

尚华1 提出了问题 · 2月5日

vue-cli的Library模式打包生成的umd.js引入到html后不能传参是什么原因呢?

用vue-cli写了个button组件,想着利用Library模式打包umd.js已供其他传统页面使用,在vue-cli使用是正常的,发现打包的umd.js引入后,传给组件的参数无效了:
image.png
上图是button组件
<br/>
image.png
上图是Library模式入口文件
<br/>
image.png
上图是在cli中引用是没问题的
<br/>
image.png
上图是打包后的使用
<br/>
image.png
上图是渲染后的,发现transferData并没有被解析,而是当成了属性
请问这是什么原因呢?

关注 1 回答 1

尚华1 提出了问题 · 2月1日

解决vue template中使用ts import子组件后会提示子组件里的变量未定义,是什么原因?

第一次在vue中用ts,发现import一个子组件后,子组件就报错了,如图image
父组件这样写的:
image
子组件这样写的:
image
请问是什么原因呢?

关注 2 回答 1

认证与成就

  • 获得 9 次点赞
  • 获得 53 枚徽章 获得 2 枚金徽章, 获得 15 枚银徽章, 获得 36 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-10-11
个人主页被 1.5k 人浏览