城南

城南 查看完整档案

北京编辑江西农业大学  |  软件工程 编辑OKCoin  |  进击的前端开发攻城狮 编辑 chanming.cc 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

城南 赞了回答 · 2020-03-30

解决react设置多个className

ES6 模板字符串 ``

className={`title ${index === this.state.active ? 'active' : ''}`}


classnames
参照:classnames

关注 7 回答 5

城南 赞了文章 · 2020-03-03

禁止浏览器自动填充

如何禁止浏览器自动填充

  • autocomplete 属性规定输入字段是否应该启用自动完成功能

方法一:设置autocomplete=“new-password”

 没错,autocomplete是可以控制浏览器自动完成功能,但文档里说autocomplete=“off”可以禁止浏览器自动填充,经过实际操作一试,然而并不能,autocomplete=“off”已经失效了,经过探索发现autocomplete=“new-password”(注意:在像vue,react等项目中应该用驼峰命名法autoComplete)加到对应的input[type=password]中就可以了,此方法在Chrome浏览器中有效,像Firefox又不起作用了    

图片描述

方法二:添加<input type="password" hidden>

Firefox浏览器可以在表单里添加<input type="password" hidden>,在添加<input type="password" hidden>后面的input都不会被自动填充,想要整个表单都不被填充,将它添加到表单最前面即可,但是此方法在Firefox浏览器中有效,Chrome又不起作用了

图片描述

终极方法:可以解决浏览器的兼容性的方法

将两种方法组合到一起,添加<input type="password" hidden>并设置autocomplete=“new-password”,查看一下,Chrome跟Firefox都生效了
图片描述

查看原文

赞 9 收藏 5 评论 1

城南 关注了用户 · 2020-03-03

barry_mr_杨 @barry_mr_yang

发表自己的想法跟工作的总结,不一定全都是对的,如有错还望指出。继续coding

关注 1

城南 回答了问题 · 2019-12-31

做一个chrome自动填充扩展。自动填充行为不会触发react的onChange函数,怎么解决呢?

看这篇文章,可以解决你的问题。

https://www.mobibrw.com/2018/...

关注 2 回答 2

城南 关注了问题 · 2019-12-31

做一个chrome自动填充扩展。自动填充行为不会触发react的onChange函数,怎么解决呢?

做一个chrome自动填充扩展。自动填充行为不会触发react的onChange函数,怎么解决呢

直接用js赋值:
document.querySelector("div.right-panel .input-item:nth-child(1) .name-input-class input").value = "testte"
可以赋值,但是点击提交就会显示为空

关注 2 回答 2

城南 回答了问题 · 2019-10-17

请求接口这个代码可以优化吗?

1、协调后端,让他从header中进行获取,然后在封装的get、post请求中,把token挂在header上
2、如果无法协调,拿到一次token之后,放到localStorage中,然后在封装的请求中把token放到参数里,至于token的有效性让后端自己判断。

目前你这每个请求都要先请求下token,成本太高了!!!

关注 3 回答 4

城南 收藏了文章 · 2019-10-17

手把手教会使用react开发日历组件

准备工作

提前需要准备好react脚手架开发环境,由于react已经不支持在页面内部通过jsx.transform来转义,我们就自己大了个简易的开发环境

创建一个文件夹,命名为react-canlendar

cd ./react-canlendar

运行

npm init

一路enter我们得到一个package.json的文件

安装几个我们需要的脚手架依赖包

npm install awesome-typescript-loader typescript webpack webpack-cli -D

安装几个我们需要的类库

npm install @types/react react react-dom --save

基础类库安装完毕,开始构建webpack配置

新建一个目录config,config下面新增一个文件,名字叫做webpack.js

var path = require('path')

module.exports = {
    entry: {
        main: path.resolve(__dirname, '../src/index.tsx')
    },
    output: {
        filename: '[name].js'
    },
    resolve: {
        extensions: [".ts", ".tsx", ".js", ".json"]
    },
    module: {
        rules: [
            {test: /\.tsx?$/, use: ['awesome-typescript-loader']}
        ]
    }
}

还需要创建一个index.html文件,这是我们的入口文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="root"></div>
    <script data-original="./dist/main.js"></script>
</body>
</html>

以上环境只是一个极简单的环境,真实环境要比这个复杂的多

好了,言归正传,我们还是聚焦到日历组件的开发中来吧

创建一个src文件夹,内部创建一个index.tsx文件。

这个入口文件很简单就是一个挂载

import * as React from 'react'
import * as ReactDOM from 'react-dom'

ReactDOM.render((
  <div>
    test
  </div>
), document.getElementById('root'))

ok,打开页面可以看到页面正常显示了test字样。

我们需要创建Calendar组件了。

创建一个components文件夹,内部创建一个Calendar.tsx文件。

import * as React from 'react'

export default class Calendar extends React.Component {
  render() {
   
    return (<div>
        日历
    </div>)
  }
}

在index.tsx中把Calendar.tsx引入,并使用起来。于是index.tsx变成这个样子。

import * as React from 'react'
import * as ReactDOM from 'react-dom'
import Calendar from './components/Calendar'

ReactDOM.render((
  <div>
    <Calendar/>
  </div>
), document.getElementById('root'))

可以看到页面显示了日历字样。

要显示日历,首先需要显示日历这个大框以及内部的一个个小框。实现这种布局最简单的布局就是table了

所以我们首先创建的是这种日历table小框框,以及表头的星期排列。

import * as React from 'react'

const WEEK_NAMES = ['日', '一', '二', '三', '四', '五', '六']
const LINES = [1,2,3,4,5,6]

export default class Calendar extends React.Component {
  render() {
    return (<div>
      <table cellPadding={0} cellSpacing={0} className="table">
        <thead>
        <tr>
          {
            WEEK_NAMES.map((week, key) => {
              return <td key={key}>{week}</td>
            })
          }
        </tr>
        </thead>
        <tbody>
        {
          LINES.map((l, key) => {
            return <tr key={key}>
              {
                WEEK_NAMES.map((week, index) => {
                  return <td key={index}>{index}</td>
                })
              }
            </tr>
          })
        }
        </tbody>
      </table>
    </div>)
  }
}

可以看到我们使用了一个星期数组作为表头,我们按照惯例是从周日开始的。你也可以从其他星期开始,不过会对下面的日期显示有影响,因为每个月的第一天是周几决定第一天显示在第几个格子里。

那为什么行数要6行呢?因为我们是按照最大行数来确定表格的行数的,如果一个月有31天,而这个月的第一天刚好是周六。就肯定会显示6行了。

为了显示好看,我直接写好了样式放置在index.html中了,这个不重要,不讲解。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style type="text/css">
        * {
            margin: 0;
            padding: 0;
        }
        .table {
            border-collapse:collapse;
            border-spacing:0;
        }
        .table td{
            border: 1px solid #ddd;
            padding: 10px;
        }
        .table caption .caption-header{
            border-top: 1px solid #ddd;
            border-right: 1px solid #ddd;
            border-left: 1px solid #ddd;
            padding: 10px;
            display: flex;
            justify-content: space-between;
        }
        .table caption .caption-header .arrow {
            cursor: pointer;
            font-family: "宋体";
            transition: all 0.3s;
        }
        .table caption .caption-header .arrow:hover {
            opacity:0.7;
        }
    </style>
</head>
<body>
    <div id="root"></div>
    <script data-original="./dist/main.js"></script>
</body>
</html>

下面就要开始显示日期了,首先要把当前月份的日期显示出来,我们先在组件的state中定义当前组件的状态

state = {
    month: 0,
    year: 0,
    currentDate: new Date()
}

我们定义一个方法获取当前年月,为什么不需要获取日,因为日历都是按月显示的。获取日现在看来对我们没有意义,于是新增一个方法,设置当前组件的年月

setCurrentYearMonth(date) {
    var month = Calendar.getMonth(date)
    var year = Calendar.getFullYear(date)
    this.setState({
      month,
      year
    })
}

static getMonth(date: Date): number{
    return date.getMonth()
}

static getFullYear(date: Date): number{
    return date.getFullYear()
}

创建两个静态方法获取年月,为什么是静态方法,因为与组件的实例无关,最好放到静态方法上去。

要想绘制一个月还需要知道一个月的天数吧,才好绘制吧

所以我们创建一个数组来表示月份的天数

const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]  // 暂定2月份28天吧

组件上创建一个函数,根据月份获取天数,也是静态的

static getCurrentMonthDays(month: number): number {
    return MONTH_DAYS[month]
}

下面还有一个重要的事情,就是获取当前月份第一天是周几,这样子就可以决定把第一天绘制在哪里了。首先要根据年月的第一天获得date,根据这个date获取周几。

static getDateByYearMonth(year: number, month: number, day: number=1): Date {
    var date = new Date()
    date.setFullYear(year)
    date.setMonth(month, day)
    return date
  }

这里获得每个月的第一天是周几了。

static getWeeksByFirstDay(year: number, month: number): number {
    var date = Calendar.getDateByYearMonth(year, month)
    return date.getDay()
  }

好了,开始在框子插入日期数字了。因为每个日期都是不一样的,这个二维数组可以先计算好,或者通过函数直接插入到jsx中间。

static getDayText(line: number, weekIndex: number, weekDay: number, monthDays: number): any {
    var number = line * 7 + weekIndex - weekDay + 1
    if ( number <= 0 || number > monthDays ) {
      return <span>&nbsp;</span>
    }

    return number
  }

看一下这个函数需要几个参数哈,第一个行数,第二个列数(周几),本月第一天是周几,本月天数。line * 7 + weekIndex表示当前格子本来是几,减去本月第一天星期数字。为什么+1,因为索引是从0开始的,而天数则是从1开始。那么<0 || >本月最大天数的则过滤掉,返回一个空span,只是为了撑开td。其他则直接返回数字。


import * as React from 'react'

const WEEK_NAMES = ['日', '一', '二', '三', '四', '五', '六']
const LINES = [1,2,3,4,5,6]
const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

export default class Calendar extends React.Component {
  state = {
    month: 0,
    year: 0,
    currentDate: new Date()
  }

  componentWillMount() {
    this.setCurrentYearMonth(this.state.currentDate)
  }

  setCurrentYearMonth(date) {
    var month = Calendar.getMonth(date)
    var year = Calendar.getFullYear(date)
    this.setState({
      month,
      year
    })
  }

  static getMonth(date: Date): number{
    return date.getMonth()
  }

  static getFullYear(date: Date): number{
    return date.getFullYear()
  }

  static getCurrentMonthDays(month: number): number {
    return MONTH_DAYS[month]
  }

  static getWeeksByFirstDay(year: number, month: number): number {
    var date = Calendar.getDateByYearMonth(year, month)
    return date.getDay()
  }

  static getDayText(line: number, weekIndex: number, weekDay: number, monthDays: number): any {
    var number = line * 7 + weekIndex - weekDay + 1
    if ( number <= 0 || number > monthDays ) {
      return <span>&nbsp;</span>
    }

    return number
  }

  static formatNumber(num: number): string {
    var _num = num + 1
    return _num < 10 ? `0${_num}` : `${_num}`
  }

  static getDateByYearMonth(year: number, month: number, day: number=1): Date {
    var date = new Date()
    date.setFullYear(year)
    date.setMonth(month, day)
    return date
  }

  checkToday(line: number, weekIndex: number, weekDay: number, monthDays: number): Boolean {
    var { year, month } = this.state
    var day = Calendar.getDayText(line, weekIndex, weekDay, monthDays)
    var date = new Date()
    var todayYear = date.getFullYear()
    var todayMonth = date.getMonth()
    var todayDay = date.getDate()

    return year === todayYear && month === todayMonth && day === todayDay
  }

  monthChange(monthChanged: number) {
    var { month, year } = this.state
    var monthAfter = month + monthChanged
    var date = Calendar.getDateByYearMonth(year, monthAfter)
    this.setCurrentYearMonth(date)
  }

  render() {
    var { year, month } = this.state
    console.log(this.state)

    var monthDays = Calendar.getCurrentMonthDays(month)
    var weekDay = Calendar.getWeeksByFirstDay(year, month)

    return (<div>
      {this.state.month}
      <table cellPadding={0} cellSpacing={0} className="table">
        <caption>
          <div className="caption-header">
            <span className="arrow" onClick={this.monthChange.bind(this, -1)}>&#60;</span>
            <span>{year} - {Calendar.formatNumber(month)}</span>
            <span className="arrow" onClick={this.monthChange.bind(this, 1)}>&gt;</span>
          </div>
        </caption>
        <thead>
          <tr>
            {
              WEEK_NAMES.map((week, key) => {
                return <td key={key}>{week}</td>
              })
            }
          </tr>
        </thead>
        <tbody>
        {
          LINES.map((l, key) => {
            return <tr key={key}>
              {
                WEEK_NAMES.map((week, index) => {
                  return <td key={index} style={{color: this.checkToday(key, index, weekDay, monthDays) ? 'red' : '#000'}}>
                    {Calendar.getDayText(key, index, weekDay, monthDays)}
                  </td>
                })
              }
            </tr>
          })
        }
        </tbody>
      </table>
    </div>)
  }
}

可以看到最终的代码多了一些东西,因为我加了月份的切换。

还记的上文我们把二月份天数写28天嘛?要不你们自己改改,判断一下闰年。

创建了一个程序员交流微信群,大家进群交流IT技术

图片描述

如果已过期,可以添加博主微信号15706211347,拉你进群

查看原文

城南 收藏了文章 · 2019-10-17

怎样实现一个datePicker(日期选择)组件

百度前端技术学院上有一个任务,要实现一个日期选择组件,本文由此而来~

  1. 看看需求

  • 组件默认一直呈显示状态

  • 通过某种方式选择年、月,选择了年月后,日期列表做相应切换

  • 通过单击某个具体的日期进行日期选择

  • 组件初始化时,可配置可选日期的上下限。可选日期和不可选日期需要有样式上的区别

  • 提供设定日期的接口,指定具体日期,日历面板相应日期选中

  • 日期选择面板默认隐藏,会显示一个日期显示框和一个按钮,点击这两个部分,会浮出日历面板。再点击则隐藏。

  • 点击选择具体日期后,面板隐藏,日期显示框中显示选取的日期

  • 增加一个接口,用于当用户选择日期后的回调处理

  • 增加一个参数及相应接口方法,来决定这个日历组件是选择具体某天日期,还是选择一个时间段

  • 当设置为选择时间段时,需要在日历面板上点击两个日期来完成一次选择,两个日期中,较早的为起始时间,较晚的为结束时间,选择的时间段用特殊样式标示

  • 增加参数及响应接口方法,允许设置时间段选择的最小或最大跨度,并提供当不满足跨度设置时的默认处理及回调函数接口

  • 在弹出的日期段选择面板中增加确认和取消按钮

先完成一个组件的基本结构

    (function(window,document){
       function Calendar(options){
          //传入配置的中的参数
          this.init();
       } 
       Calendar.prototype={
            init:function(){
               this.createDom();
               this.loadCss();
               this.cacheDom();
               this.bindEvents();
               this.render();
            },
            loadCss:function(){
               // 把组件所需的样式表动态加载进来
            },
            createDom:function(){
               // 创建dom对象或者创建html片段或者创建template
            },
            cacheDom:function(){
               // 存储dom 对象
            },
            bindEvents:function(){
               //事件绑定
            },
            render:function(){
              //渲染函数,更新数据或样式
            }
       }
       window.Calendar=Calendar;//把组件对象绑定到全局
    }(window,document));

通常我写组件时的基本结构如上,你可以根据组件的需要或者自己习惯进行编写。然后就可以在html里面添加以下的代码就可以调用我们的组件了,

<script data-original='calendar.js></script>
<script type='text/javascript'>
   var a=new Calendar({
      // 各种配置
      /* 类似于 id:'myCalendar'
         onSelected:function(){
                    alert('hello');
        }
     */
   });
</script>

下面再看一下我们的需求,我们来一 一分析

需求也不是很多嘛,手动斜眼~
先上图,根据图再慢慢分析

概要图
其实我们看了需求之后,每个人都会有一个大概的思路,下面说一下我的思路
首先,要实现一个日期选择器,最重要的就是要有一个日历,根据不同的年份和月份,日期面板上回显示每一天和对应的周几~
其实实现这一点的话就两点

  • 第一,要根据年份和月份算出每月有多少天

  • 第二,要计算出每月的第一天(1号)是周几
    伪代码如下:

 /**
     * @param  {string} year  年份
     * @param  {string} month 月份
     * @param  {string} day   号
     * @return {object}  message
     * message{
     * year   年份
     * month  月份
     * monthLen  那个月的天数
     * whichDay  1号是周几
     * day       号
     * }    
     */
     function calculate(year,month,day){
                 var date=year+'/'+month+'/'+'1';
                 var whichDay=new Date(date).getDay();
              var message={
                    year:year,
                    month:month,
                    monthLen:new Date(year,month,0).getDate(),
                    whichDay:whichDay,
                    day:day
              };
              return message;
     },

我想看完代码之后大家应该比较疑惑的是获取每个月天数的那句代码,这个比较优雅的做法是从这里看到的,
注意:在Date对象里month为0代表的是1月份,month为5代表6月份,所以我new Date(year,5,0)代表的六月份的第0天,即5月份的最后一天,所以还可以用getDate()获取5月份的长度,getDate方法是返回指定日期对象的月份中的第几天(1-31)。
所以当我们点击了月份加减/年份加减的按钮时,向calculate函数传入变化后的year,month参数,然后进行渲染,日历面板改变

其次,”选择时间段并且另处于开始时间和结束时间之间的日期添加特殊的样式“这一点也是花了不少时间来写,
伪代码如下:

// 初始化
var firstDate,secondDate=[0,0,0];
//点击日历面板上的日期的点击事件的执行函数的片段,每当点击事件被触发,就会执行该片段

if(self.isSelectRange){
             var date=[self.year.innerHTML,self.month.innerHTML,ele.innerHTML];            
             if(self.firstDate[0]===0){// 
                if(self.secondDate[0]===0){//两个日期都没有被设置
                     self.firstDate=date;
                }else{//firstDate没有被设置,secondDate已经被设置,
                     
                }
             }else{
                if(self.secondDate[0]===0){//firstDate已经设置,
                    self.secondDate=date;
                    if(compareDate(self.firstDate.join('/'),self.secondDate.join('/'))){//如果第一个选择的日期大于第二次选择的日期,进行交换
                        self.firstDate=[self.secondDate,self.secondDate=self.firstDate][0];
                    } 
                }else{//两个日期都已经被设置,已经选择了两个元素,再次选择则都
                   self.secondDate=[0,0,0];
                   self.firstDate=date;
                   self.clearDayInRangeStyle();
                }
             }
             self.day.innerHTML=ele.innerHTML;
             self.render();

firstDate,secondDate分别代表开始时间和结束时间。每次触发日期的点击事件时,就会执行以上的代码片段,对firstDate和secondDate进行更改,这样的话,无论是我对日历面板进行更新或者对开始时间和结束时间之间的日期显示不同的样式,都可以通过firstDate和secondDate来实现。

显示不同的样式就判断日期是否在开始时间和结束时间之间,每次重新render的时候就给选择过的firstDate和secondDate添加样式。

包括计算开始时间和结束时间之间的跨度是否在设定的跨度内,我们点击按钮后进行判断。
最后,看看render函数怎么实现
关于render函数,有以下几点需要注意:

  • 清除日历面板上的所有内容和样式,样式通过清除每个单元格上的类实现

  • 根据每月1号是周几和每月的长度生成每月的日历

  • 根据记录的fisrtDate和secondDate来显示已经选择过的选择的样式

以上大概是我的思路,我也实现了一个组件,有兴趣的朋友可以点这里,欢迎找bug~
ps:文笔还是不行,文章写的好烂。。

查看原文

城南 收藏了文章 · 2019-10-17

react模仿antd手写一个多选日期日历组件

业务需求

  1. 多选近三个月的日期。
  2. 不能选择当日之前的日期。

因为antd的日期组件都是选择单个日期或者日期范围。不符合需求,所以自己就实现了一个。写的不好的地方大家请指教

效果展示

在这里插入图片描述
测试组件

<CheckCalendar
      visible={this.state.showCalendar}
       onClose={()=>{
            this.setState({
                 showCalendar:false
            })
       }}
       onConfirm={(isCheck)=>{
            console.log(isCheck)
            this.setState({
                 showCalendar:false
            })
       }}
       />

CheckCalendar.jsx

import React, { Component, Fragment } from "react";
import { cloneDeep, chunk } from "lodash";
import PropTypes from 'prop-types';
import "animate.css";
import "./index.scss"

class CheckCalendar extends Component {
    constructor(props) {
        super(props)
        this.state = {
            dateTable: [],
            isCheck: [],
        }
        this.calendar = React.createRef();
        this.mask = React.createRef();
    }

    componentWillMount() {
        this.initDateTable()
    }

    initDateTable() {
        let temp = []
        for (let i = 0; i < 2; i++) {  // 取近三个月内的日期
            let obj = this.getDateTable(i);
            temp.push(obj);
        }
        this.setState({
            dateTable: temp
        });
    }

    getDateTable(plus) {
        let curDate = new Date()  //现在时间
        let curYear = curDate.getFullYear();
        let curMonth = curDate.getMonth() + 1;
        let curDay = curDate.getDate();
        if (curMonth + plus > 12) {
            curYear++
            curMonth = curMonth + plus - 12
        } else {
            curMonth = curMonth + plus
        }
        let date = new Date(curYear, curMonth, 0);
        let year = date.getFullYear(); // 当前年
        let month = date.getMonth() + 1; // 当前月
        // console.log(`${year}年${month}月.`);

        let date2 = new Date(year, month, 0);
        let days = date2.getDate(); // 当月有多少天
        // console.log(`当月有${days}天.`);

        date2.setDate(1);
        let day = date2.getDay(); // 当月第一天是星期几
        // console.log(`当月第一天是星期${day}.`);

        let list = [];

        for (let i = 0; i < days + day; i++) {
            if (i < day) {  // 头部补零
                list.push({
                    isActive: false,
                    number: 0
                });
            } else {
                if (plus === 0) {
                    if ((i - day + 1) < curDay) {
                        list.push({
                            disable: true,
                            isActive: false,
                            number: i - day + 1
                        });
                    } else {
                        list.push({
                            isActive: false,
                            number: i - day + 1
                        });
                    }
                } else {
                    list.push({
                        isActive: false,
                        number: i - day + 1
                    });
                }
            }
        }
        let hlist = chunk(list, 7); // 转换为二维数组
        let len = hlist.length;
        let to = 7 - hlist[len - 1].length;

        // 循环尾部补0
        for (let i = 0; i < to; i++) {
            hlist[len - 1].push({
                isActive: false,
                number: 0
            });
        }
        if (month < 10) {
            month = "0" + month
        }
        const str = `${year}-${month}`
        return {
            "list": hlist,
            "desc": str
        }
    }

    handleItemClick(desc, number, index, index1, index2) {
        let temp = cloneDeep(this.state.dateTable)
        const flag = !temp[index].list[index1][index2].isActive
        temp[index].list[index1][index2].isActive = flag
        this.setState({
            dateTable: temp,
        })
        const arr = desc.split("-");
        if (number < 10) {
            number = "0" + number
        }
        if (flag) {
            let temp = cloneDeep(this.state.isCheck);
            temp.push(arr[0] + "-" + arr[1] + "-" + number)
            this.setState({
                isCheck: temp
            })
        } else {
            let temp = cloneDeep(this.state.isCheck);
            let filted = temp.filter((item) => {
                return item !== arr[0] + "-" + arr[1] + "-" + number
            })
            this.setState({
                isCheck: filted
            })
        }
    }

    onExit = () => {
        const { onCancel } = this.props;

        onCancel && onCancel();
    }

    onConfirm = () => {
        const { onConfirm } = this.props;

        onConfirm && onConfirm(this.state.isCheck);
    }

    render() {
        return this.props.visible ? (
            <div className="calendar-mask">
                <div className="calendar-wrap animated fadeInUp">
                    <RenderCalendarHeader
                        onExit={this.onExit}
                    />
                    <RenderChineseWeek />
                    <RenderDateTemp
                        dateTable={this.state.dateTable}
                        handleItemClick={this.handleItemClick}
                        self={this}
                    />
                    <div className="fake-area"></div>
                </div>
                <RenderConfirm
                    onConfirm={this.onConfirm}
                />
            </div>
        ) : (<span></span>)
    }
}

/**
 * 渲染表格每个item
 * 
 */
const RenderDateItem = (props) => {
    const { number, active } = props;

    return number === 0 ?
        (
            <div className="date-wrap">
                <span className="left"></span><div className="item"></div><span className="right"></span>
            </div>
        ) : props.disable ?
            (
                <div className="date-wrap">
                    <span className="left"></span>
                    <div className="item disable">{number}</div>
                    <span className="right"></span>
                </div>
            ) :
            (
                <div className="date-wrap">
                    <span className="left"></span>
                    <div className={`item ${active ? 'active' : ''}`} onClick={props.itemClick} >
                        <span>{number}</span>
                    </div>
                    <span className="right"></span>
                </div>
            )
}

/**
 * 日历顶部
 * @param props.onExit 退出事件 
 */
const RenderCalendarHeader = (props) => {
    const { onExit } = props;
    return (
        <div className="header">
            <span>日期多选</span>
            <div className="exit" onClick={onExit}></div>
        </div>
    )
}

/**
 * 渲染中文日期
 */
const RenderChineseWeek = () => {
    const weeks = ["日", "一", "二", "三", "四", "五", "六"];
    return (
        <div className="week-wrap">
            {
                weeks.map((item, index) => (
                    <div className="week-item" key={index}>{item}</div>
                ))
            }
        </div>
    )
}

/**
 * 
 * @param props.dateTable 模板数组
 * @param prop.handleItemClick item点击事件
 * @param prop.self 父组件作用域
 */
const RenderDateTemp = (props) => {
    const { dateTable, handleItemClick, self } = props;
    return (
        <Fragment>
            {
                dateTable.map((item, index) => {
                    const arr = item.desc.split("-");
                    return (
                        <div className="date-table" key={index}>
                            <span className="desc">
                                {arr[0] + "年" + arr[1] + "月"}
                            </span>
                            {
                                item.list.map((item2, index2) => {
                                    return (
                                        <div className="row" key={index2}>
                                            {
                                                item2.map((item3, index3) => {
                                                    return (
                                                        <RenderDateItem
                                                            itemClick={handleItemClick.bind(self, item.desc, item3.number, index, index2, index3)}
                                                            active={item3.isActive}
                                                            disable={item3.disable ? item3.disable : false}
                                                            number={item3.number}
                                                            key={index3}
                                                        />
                                                    )
                                                })
                                            }
                                        </div>
                                    )
                                })
                            }
                        </div>
                    );
                })
            }
        </Fragment>
    )
}

const RenderConfirm = (props) => {
    return (
        <div className="confirm-wrap">
            <div className="confirm" onClick={props.onConfirm}>
                确定
            </div>
        </div>
    )
}

/**
 * @param onCancel 关闭事件回调
 * @param onConfirm 确认事件回调
 * @param visible 组件显示状态
 */
CheckCalendar.propTypes = {
    onCancel: PropTypes.func,
    onConfirm: PropTypes.func,
    visible: PropTypes.bool
}

export default CheckCalendar;

checkCalendar.scss

.calendar-mask {
    position: fixed;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    background-color: rgba(0, 0, 0, 0.5);

    .calendar-wrap {
        width: 100%;
        height: 100%;
        background-color: #ffffff;
        overflow: auto;
        animation-duration: .3s;

        .header {
            color: black;
            font-size: 17px;
            font-weight: bold;
            height: 30px;
            line-height: 30px;

            .exit {
                width: 20px;
                height: 20px;
                position: relative;
                float: left;
                left: 20px;
                top: 5px;
            }

            .exit::before,
            .exit::after {
                content: "";
                position: absolute;
                height: 20px;
                width: 1.5px;
                left: 8.5px;
                background: #098fef;
            }

            .exit::before {
                transform: rotate(45deg);
            }

            .exit::after {
                transform: rotate(-45deg);
            }

        }

        .week-wrap {
            display: flex;
            font-size: 16px;
            border-bottom: 1px solid rgb(221, 221, 221);

            .week-item {
                height: 30px;
                line-height: 30px;
                width: 14.28571429%;
            }
        }

        .date-table {
            margin-top: 20px;

            .desc {
                text-align: left;
                text-indent: 12px;
                font-size: 18px;
            }

            .row {
                display: flex;
                margin: 8px 0px;

                .date-wrap {
                    height: 35px;
                    width: 14.28571429%;
                    line-height: 30px;

                    .left {
                        width: 100%;
                    }

                    .item {
                        display: inline-block;
                        width: 35px;
                        height: 35px;
                        font-size: 15px;
                        font-weight: bold;
                        line-height: 35px;
                        border-radius: 50%;
                    }

                    .disable {
                        background-color: rgb(238, 238, 238);
                        color: rgb(187, 187, 187);
                    }

                    .active {
                        background-color: #108ee9;
                        color: #ffffff;
                    }

                    .right {
                        width: 100%;
                    }
                }
            }
        }

        .fake-area {
            height: 53px;
            width: 100%;
        }
    }

    .confirm-wrap {
        position: fixed;
        bottom: 0;
        height: 54px;
        width: 100%;
        box-sizing: border-box;
        border-top: 1px solid rgb(221, 221, 221);
        background-color: rgb(247, 247, 247);
        display: flex;
        align-items: center;
        justify-content: center;

        .confirm {
            border-radius: 5px;
            width: 90%;
            background-color: #108ee9;
            font-size: 18px;
            color: #ffffff;
            padding: 8px 0;

            &:active {
                background-color: rgb(14, 128, 210);
                color: rgb(85, 166, 223)
            }
        }
    }
}
查看原文

城南 收藏了文章 · 2019-09-26

如何写一个自己的脚手架 - 一键初始化项目

介绍

脚手架的作用:为减少重复性工作而做的重复性工作

即为了开发中的:编译 es6,js 模块化,压缩代码,热更新等功能,我们使用webpack等打包工具,但是又带来了新的问题:初始化工程的麻烦,复杂的webpack配置,以及各种配置文件,所以就有了一键生成项目,0 配置开发的脚手架

本文项目代码地址

本系列分 3 篇,详细介绍如何实现一个脚手架:

  • 一键初始化项目
  • 0 配置开发环境与打包
  • 一键上传服务器

首先说一下个人的开发习惯

在写功能前我会先把调用方式写出了,然后一步一步的从使用者的角度写,现将基础功能写好后,慢慢完善

例如一键初始化项目功能

我期望的就是 在命令行执行输入 my-cli create text-project,回车后直接创建项目并生成模板,还会把依赖都下载好

我们下面就从命令行开始入手

创建项目 my-cli,执行 npm init -y快速初始化

bin

my-cli

package.json 中加入:

{
  "bin": {
    "my-cli": "bin.js"
  }
}

bin.js

#!/usr/bin/env node

console.log(process.argv);

#!/usr/bin/env node这一行是必须加的,就是让系统动态的去PATH目录中查找node来执行你的脚本文件。

命令行执行 npm link ,创建软链接至全局,这样我们就可以全局使用my-cli命令了,在开发 npm 包的前期都会使用link方式在其他项目中测试来开发,后期再发布到npm

命令行执行 my-cli 1 2 3

输出:[ '/usr/local/bin/node', '/usr/local/bin/my-cli', '1', '2', '3' ]

这样我们就可以获取到用户的输入参数

例如my-cli create test-project

我们就可以通过数组第 [2] 位判断命令类型create,通过第 [3] 位拿到项目名称test-project

commander

node的命令行解析最常用的就是commander库,来简化复杂cli参数操作

(我们现在的参数简单可以不使用commander,直接用process.argv[3]获取名称,但是为了之后会复杂的命令行,这里也先使用commander

#!/usr/bin/env node

const program = require("commander");
const version = require("./package.json").version;

program.version(version, "-v, --version");

program
  .command("create <app-name>")
  .description("使用 my-cli 创建一个新的项目")
  .option("-d --dir <dir>", "创建目录")
  .action((name, command) => {
    const create = require("./create/index");
    create(name, command);
  });

program.parse(process.argv);

commander 解析完成后会触发action回调方法

命令行执行:my-cli -v

输出:1.0.0

命令行执行: my-cli create test-project

输出:test-project

创建项目

拿到了用户传入的名称,就可以用这么名字创建项目
我们的代码尽量保持bin.js整洁,不将接下来的代码写在bin.js里,创建create文件夹,创建index.js文件

create/index.js中:

const path = require("path");
const mkdirp = require("mkdirp");

module.exports = function(name) {
  mkdirp(path.join(process.cwd(), name), function(err) {
    if (err) console.error("创建失败");
    else console.log("创建成功");
  });
};

process.cwd()获取工作区目录,和用户传入项目名称拼接起来

(创建文件夹我们使用mkdirp包,可以避免我们一级一级的创建目录)

修改bin.jsaction方法:

// bin.js
.action(name => {
    const create = require("./create")
    create(name)
  });

命令行执行: my-cli create test-project

输出:创建成功

并在命令行所在目录创建了一个test-project文件夹

模板

首先需要先列出我们的模板包含哪些文件

一个最基础版的vue项目模板:

|- src
  |- main.js
  |- App.vue
  |- components
    |- HelloWorld.vue
|- index.html
|- package.json

这些文件就不一一介绍了

我们需要的就是生成这些文件,并写入到目录中去

模板的写法后很多种,下面是我的写法:

模板目录:

|- generator
  |- index-html.js
  |- package-json.js
  |- main.js
  |- App-vue.js
  |- HelloWorld-vue.js

generator/index-html.js 模板示例:

module.exports = function(name) {
  const template = `
{
  "name": "${name}",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {},
  "devDependencies": {
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "vue": "^2.6.10"
  }
}
  `;
  return { template, dir: "", name: "package.json" };
};

dir就是目录,例如main.jsdir就是src

create/index.jsmkdirp中新增:

const path = require("path");
const mkdirp = require("mkdirp");
const fs = require("fs");

module.exports = function(name) {
  const projectDir = path.join(process.cwd(), name);
  mkdirp(projectDir, function(err) {
    if (err) console.error("创建失败");
    else {
      console.log(`创建${name}文件夹成功`);
      const { template, dir, name: fileName } = require("../generator/package")(name);
      fs.writeFile(path.join(projectDir, dir, fileName), template.trim(), function(err) {
        if (err) console.error(`创建${fileName}文件失败`);
        else {
          console.log(`创建${fileName}文件成功`);
        }
      });
    }
  });
};

这里只写了一个模板的创建,我们可以用readdir来获取目录下所有文件来遍历执行

下载依赖

我们平常下载npm包都是使用命令行 npm install / yarn install
这时就需要用到 nodechild_process.spawn api 来调用系统命令

因为考虑到跨平台兼容处理,所以使用 cross-spawn 库,来帮我们兼容的操作命令

我们创建utils文件夹,创建install.js

utils/install.js

const spawn = require("cross-spawn");

module.exports = function install(options) {
  const cwd = options.cwd || process.cwd();
  return new Promise((resolve, reject) => {
    const command = options.isYarn ? "yarn" : "npm";
    const args = ["install", "--save", "--save-exact", "--loglevel", "error"];
    const child = spawn(command, args, { cwd, stdio: ["pipe", process.stdout, process.stderr] });

    child.once("close", code => {
      if (code !== 0) {
        reject({
          command: `${command} ${args.join(" ")}`
        });
        return;
      }
      resolve();
    });
    child.once("error", reject);
  });
};

然后我们就可以在创建完模板后调用install方法下载依赖

install({ cwd: projectDir });

要知道工作区为我们项目的目录

至此,解析 cli,创建目录,创建模板,下载依赖一套流程已经完成

基本功能都跑通之后下面就是要填充剩余代码和优化

优化

当代码写的多了之后,我们看上面create方法内的回调嵌套回调会非常难受

node 7已经支持async,await,所以我们将上面代码改成Promise

utils目录下创建,promisify.js

module.exports = function promisify(fn) {
  return function(...args) {
    return new Promise(function(resolve, reject) {
      fn(...args, function(err, ...res) {
        if (err) return reject(err);
        if (res.length === 1) return resolve(res[0]);
        resolve(res);
      });
    });
  };
};

这个方法帮我们把回调形式的Function改成Promise

utils目录下创建,fs.js

const fs = require(fs);
const promisify = require("./promisify");
const mkdirp = require("mkdirp");

exports.writeFile = promisify(fs.writeFile);
exports.readdir = promisify(fs.readdir);
exports.mkdirp = promisify(mkdirp);

fsmkdirp方法改造成promise

改造后的create.js

const path = require("path");
const fs = require("../utils/fs-promise");
const install = require("../utils/install");

module.exports = async function(name) {
  const projectDir = path.join(process.cwd(), name);
  await fs.mkdirp(projectDir);
  console.log(`创建${name}文件夹成功`);
  const { template, dir, name: fileName } = require("../generator/package")(name);
  await fs.writeFile(path.join(projectDir, dir, fileName), template.trim());
  console.log(`创建${fileName}文件成功`);
  install({ cwd: projectDir });
};

结语

关于进一步优化:

  • 更多功能与健壮 例如指定目录创建项目,目录不存在等情况
  • chalkora优化log,给用户更好的反馈
  • 通过inquirer问询用户得到更多的选择:模板vue-routervuex等更多初始化模板功能,eslint

更多的功能:

  • 内置 webpack 配置
  • 一键发布服务器

其实要学会善用第三方库,你会发现我们上面的每个模块都有第三方库的身影,我们只是将这些功能组装起来,再结合我们的想法进一步封装

虽然有vue-clicreate-react-app这些已有的脚手架,但是我们还是可能在某些情况下需要自己实现脚手架部分功能,根据公司的业务来封装,减少重复性工作,或者了解一下内部原理

【青团社】招聘前端方面: 高级/资深/技术专家,欢迎投递 lishixuan@qtshe.com

查看原文

认证与成就

  • 获得 1766 次点赞
  • 获得 20 枚徽章 获得 2 枚金徽章, 获得 4 枚银徽章, 获得 14 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • 一个轻量级的js 时间处理器

    一个轻量级的js 时间处理器,也是在开发中遇到时间处理比较麻烦,所以就自己写了一套。 dateformat.js 是一个非常简洁、轻量级、不到 5kb 的很简洁的 Javascript 库, 它是一个时间的处理工具类。 支持常用的时间格式化 得到当前星期,时间对比大小,是否为闰年 增加日期,增加月份,增加年份等等 支持自动实时更新; 支持浏览器script方式; 测试用例完善,执行良好;

注册于 2016-10-20
个人主页被 7.8k 人浏览