精华内容
下载资源
问答
  • 如何自己的bug得清新脱俗,结构清楚则是需要我们不断努力的。在开始今天的话题之前,先抛出一个问题,代码结构好是好事吗?代码结构好事好事吗?该图是我的票圈里一位兄弟转发的。代码结构好了,别人接手容易...

    bug是不可避免。但如何让自己的bug写得清新脱俗,结构清楚则是需要我们不断努力的。

    在开始今天的话题之前,先抛出一个问题,代码结构好是好事吗?

    代码结构好事好事吗?

    699016fc635752d1b264a0e4f06157d6.png

    该图是我的票圈里一位兄弟转发的。代码结构好了,别人接手容易,反倒是写得烂了,却可以成为焦点。你咋一听觉得这是什么神逻辑,虽然听着有道理,但总感觉有点政治不正确。

    这个是一个问题,很值得思考的问题。

    写程序,就是写逻辑,逻辑最初的样子,就是用if else来表达,事实上这就是我们描述这个世界的基本方式,if else。这两个分支可以覆盖一切情况。你能告诉我们还有if else之外的场景吗?!

    4db5f9e086989e81b4b59cef98d41e4f.png

    可以这么说,if else可以描述这个世界上所有的逻辑。

    if else就是整个世界

    你手握if else 两个单词,心想,产品你尽管提需求吧,这个世界上还有我if else解决不了的问题吗。而且久而久之,你会发现写代码就是if else,整天就在if else两个代码块里盘旋。

    f6a163bdfc19a9f934851456df8819bd.png

    慢慢的,你开始彷徨,开始思考人生,甚至怀疑人生,难道程序这么无聊吗?if else 外加crud就可以解决一切?

    编程届两大流派

    这个还是要从理论说起,其实描述这个世界有两种方式。一种是函数算法派,一种面向对象派。

    709e108822de6fe0e509a4a40a8eff43.png

    而函数算法派其实就是if else派,这一派是一个古老的门派,他们围绕着一个方法体(或者叫函数)就可以一直写下去并且能解决问题。

    而面向对象派则主张通过结构和组合的方式来解决问题,而不是围绕着if else来搞事情。

    可以毫不夸张的说,我们现如今绝大多数人也包括我本人在内,我们都还处于函数算法派,也就是if else派。至于面向对象这些东西,充其量注入的时候体会一下,或者在new的时候体会一下。其余时候我们都是在安静的写着if else。

    然而if else最终让我们走向不归路。

    前面我们说过if else可以覆盖整个世界。但覆盖范围广并不等于它明了。

    eb8796016b7568e9e844d4257794cc18.png

    开始的时候,你发现自己的if else异常明了和清晰。

    11b00d393224a5ec839a92752ad17677.png

    直到后来,一步步,就变成下面这样了:

    d738f527a7a041c7d3edc47cbbfd094e.png

    直到后来,你开始怀疑人生,于是你决定去看一些技术书籍,让自己的心灵重新洗涤一下,这样第二天才能稍微平静的面对昨日遗留的if else。

    避免if else泛滥的四法则:一提二抽三组四模式

    那么我们如何避免if else的过渡泛滥呢?我总结了一个法则:一提二抽三组四模式。

    0416f515a3ce1e8df343c57352141e41.png

    1、一提

    以下的代码我是从真实的项目代码中摘取的。

    bad case:

    cca3146c6553f9bd4a02dea5a03e0476.png

    good case:

    0083abe2f11ab7013bda7f790e3e4073.png

    上面两段代码执行了相同的逻辑,但第一种层次更多,可读性也差,这还是刚开始的代码,随着需求的不断变化,这段代码的层次结构最后就会变成一堆乱麻。这里其实并没有用什么技术,就是简单的对代码逻辑路径进行重新的编排,从而实现了代码的整洁和更好的可读性。

    在if else的优化中,一个核心的思路就是:更少的缩进、更少的else

    上面重构后的代码,你可以看到缩进线由三条变成了两条,同时通过把异常场景的逻辑前提的方式去掉了else块。异常场景逻辑一般指负向的条件,比如==null,notExist,XxxException等。理想的代码,总是应该把这些异常情况,提前排除掉,然后才安心的去写主业务逻辑。这样你的代码就会显得层次分明。

    d208ca56a45fe78b3e1f2f28f5662671.png

    理想的代码应该有的样子:

    036fd03d8a080006f270387ad1e7527f.png

    二抽

    在有限代码行数内通过第一个法则可以让结构更加的清晰。当if中的代码行数过多时,这会就需要把可以独立成为方法的逻辑抽取成一个private的方法(也可以是public等),代码过长时,我们总是需要这样去做,这样可以让你的主方法就像一篇文章一样具有可读性。

    7524ef7ed479ebd5267b27511f9bd122.png

    依然是上面的forYes方法。我们把exist的逻辑单独抽取了一个方法,同时又把主逻辑代码也抽了一个方法,你会发现forYes1方法的代码长度并没有增加多少,依然保持的良好的可读性。

    三组

    上面的第二法则是抽取一个private方法,还没有出类。当我们抽取到一定地步,会发现适合单独成类的时候,应该把之前的这些private方法移动到一个新的类中。这个就是第三法则:组合,通过组合的方式来构建你的逻辑。

    681b16375bc1e084c6a414c2962602a5.png
    1d8ef32b73c5f408e56b547b476a90b9.png
    1b3164343d44a324b90519e1dac827f7.png

    四模式

    在用完组合后,我们的代码其实已经基本上比较清楚了。但为了让我们的代码更加的优雅,更上一个层次。有的场景下你需要使用到设计模式,设计模式被总结成为最佳实践,不是仅仅用来写框架用的,写日常纷繁复杂的业务逻辑代码也是需要设计模式的。

    接下来我就以自己正在开发的项目中的场景为例,来说说如何使用设计模式改善你的既有代码。

    在项目中我们需要为审批工作流提供一个回调(callback)接口。审批流有不同的状态,不同的状态回调会执行不同的逻辑。在重构前的代码大体是这样的:

    0e40852bcc94671d9c82d843c3b6444d.png

    这样的处理方式还不错。但当状态变多时,你就会一直扩张这个callback方法,这显然不是一种好的方案。为此你需要使用一个设计模式来改善现在的实现。

    如果你还记得设计模式,你一定还记得有一个状态模式(State Pattern)。

    现在我们就尝试使用状态模式来重构我们的代码。

    b854de2ffa1b1dec23092fd4149c1ea6.png

    我们希望最终的样子是这样的:

    81ba9ca0bb77b096bb8cbd8dc0fadbcf.png

    首先新建一个State接口类:

    32cfa35afd40ea41cf0320e0598a889e.png

    然后新建三个实现状态,分别是Yes,No和Cancel:

    25bbaf3eb042739bf9ae4d9eb3b4a582.png
    44ff86a6fb02f780e67cfef53b50dbf3.png
    380cd4937a66c698adcbf8f00116abf7.png

    然后新建一个Context类:

    40c73273ad1f55a5fa1e31a5e3372798.png

    然后,新建一个State工厂类:

    66bae94f849eab9d2320e58914f9e7f7.png

    改造完毕。通过上面的重构,我们使用了状态模式和一点点工厂模式。最终callback方法就只需通过newInstance就可以找到具体状态的回调逻辑,而以后即使状态在不断的增加的,你也只需新建一个新的实现状态,然后注入工厂类中,做到了可插拔。值得注意的是,这里我们的state其实并没有很好的被传递和持有,这很不“状态”,这通过这样的方式我们实现了对callback的重构,结构也更加的清晰了。总之,当你遇到业务需求的不断变化,你需要找到一种合适的设计模式来hold住它,即使GOLF不能满足你的需求,你也可以自己创造一个设计模式来让你的代码清晰易懂。

    总结

    本文一开始介绍了if else泛滥后的问题。然后为你介绍了“一提二抽三组四模式”法则。我们的项目也是万事万物中的一部分,套用《规模》一书中的一段话:耗散力一直在持续且不可避免地做着功,这使得所有系统都将退化。设计最为精巧的机器、最具创新组织力的公司、进化得最完美的生物都无法逃脱这一最为严酷的死神。耗散力就是一个系统运转所产生的摩擦力,系统复杂性所带来的无序热量,你的代码也一样,也需要通过不断的改进和重构来尽量的延缓和降低熵的增长。

    最终我们都将屈服于各种形式的磨损和衰竭,熵能杀人,你需要吃饭!

    展开全文
  • 这里是修真院后端小课堂...尝试自己编写一个简单脚本 】 大家好,我是IT修真院上海分院第02期学员,一枚正直善良的java程序员。 今天给分享一下修真院java任务三中关于脚本的相关知识。 背景 现在多数的服务器都是...

    这里是修真院后端小课堂,每篇分享文从

    【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】

    八个方面深度解析后端知识/技能,本篇分享的是:

    【如何写SHELL脚本?尝试自己编写一个简单脚本 】
    在这里插入图片描述

    大家好,我是IT修真院上海分院第02期学员,一枚正直善良的java程序员。

    今天给分享一下修真院java任务三中关于脚本的相关知识。

    背景

    现在多数的服务器都是Linux系统的,需要通过shell来进行操作,而利用shell脚本,可以大大提高开发维护的效率。

    知识剖析

    什么是shell

    shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便于运行程序的界面系统级程序,用户可以用shell来启动,挂起,停止甚至是编写一些程序。

    shell还是一个功能强大的编程语言,易编写,易调试。shell是解释执行的脚本语言,在shell中可以直接调用Linux命令。

    shell的两种主要语法有bourne和C,这两种语法彼此不兼容。bourne家族主要包括sh,ksh,Bash, psh,zsh,C家族主要用在unix,包括csh,tsh. 查看当前shell的类型的方法是:echo $SHELL

    概念辨析,shell是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。

    shell脚本(shell script),是一种为shell编写的脚本程序,业界所说的shell通常都是指shell脚本。

    shell有两种执行方式

    交互式:解释执行用户的命令,用户输入一条命令,shell就执行一条。

    批处理:用户事先写一个shell脚本,其中有很多命令,让shell一次把这些命令执行完,而不必一条一条地敲命令。

    编码

    首先,创建shell脚本文件:vim xxxx.sh ,后缀为.sh

    脚本执行有两种方式:1.赋予执行权限,chmod 755 xxxx.sh 2. 通过Bash调用执行脚本bash hello.sh

    特殊符号

    $ 调用变量的值

    反引号,引用系统命令

    $( ) 引用系统命令

    ’ ’ 单引号,在其中的特殊符号没有特殊含义。

    " " 双引号,在其中的特殊符号没有特殊含义,但有三个除外:$ , `` , \

    管道符

    命令1 | 命令2 命令1的正确输出作为命令2的操作对象。

    输入输出重定向

    命令 > 文件 ,以覆盖的方式,把命令输出到指定的文件中

    命令 >> 文件 ,以追加的方式,把命令输出到指定的文件中。

    输出重定向

    命令 < 文件

    命令 < 标识符

    以下是在任务三中统计ngiinx响应时间的脚本:

    #!/bin/bash

    #nginx logs

    H=100

    cd /usr/local/nginx/logs/

    tail -n $H access.log|awk ‘{print $25,$26,$7 }’

    echo “统计了 $H 条数据”

    echo “响应时间0.01秒以内”

    tail -n $H access.log|awk ‘BEGIN{sum=0}{if($26<0.01)sum++;}END{print sum}’

    echo “响应时间0.01~0.02秒”

    tail -n $H access.log|awk ‘BEGIN{sum=0}{if(($26>=0.01)&&($26<0.02))sum++;}END{print sum}’

    echo “响应时间0.02~0.03秒”

    tail -n $H access.log|awk ‘BEGIN{sum=0}{if(($26>=0.02)&&($26<0.03))sum++;}END{print sum}’

    echo “响应时间0.03~0.04”

    tail -n $H access.log|awk ‘BEGIN{sum=0}{if(($26>=0.03)&&($26<0.04))sum++;}END{print sum}’

    echo “响应时间0.04及以上”

    tail -n $H access.log|awk ‘BEGIN{sum=0}{if($26>=0.04)sum++;}END{print sum}’

    echo “访问量”

    tail -n $H access.log|awk ‘{print $7}’ | wc -l

    tail -n $H access.log|awk ‘{print $11}’ |sort | uniq -c |wc -l

    echo “#######统计完成######”

    展开全文
  • 尝试自己编写一个简单脚本 1.背景介绍 Shell是什么? Shell本身是一个用C语言编写的程序,它是用户使用Linux的桥梁。 Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和...

     

    大家好,
    今天给大家分享一下可能会使用到的知识点:

     如何写shell脚本?尝试自己编写一个简单脚本

     

     

     

    1.背景介绍

     

    Shell是什么?

    Shell本身是一个用C语言编写的程序,它是用户使用Linux的桥梁。

    Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。

     

     

    2.知识剖析

    Shell有两种执行命令的方式:

    •交互式(Interactive):解释执行用户的命令,用户输入一条命令,Shell就解释执行一条。

    •批处理(Batch):用户事先写一个Shell脚本(Script),其中有很多条命令,让Shell一次把这些命令执行完,而不必一条一条地敲命令。Shell脚本和编程语言很相似,也有变量和流程控制语句,但Shell脚本是解释执行的,不需要编译,Shell程序从脚本中一行一行读取并执行这些命令,相当于一个用户把脚本中的命令一行一行敲到Shell提示符下执行。

    Shell是一种脚本语言(即解释型语言),必须有解释器来执行这些脚本。

    bash:bash是Linux系统默认使用的shell。bash由Brian Fox和Chet Ramey共同完成,是BourneAgain Shell的缩写,内部命令一共有40个。

    ash:ash shell 是由Kenneth Almquist编写的,Linux中占用系统资源最少的一个小shell,它只包含24个内部命令,因而使用起来很不方便。

    sh:sh 由Steve Bourne开发,是Bourne Shell的缩写,各种UNIX系统都配有sh。

    3.常见问题

    语法都正确但是运行报错

     

    4.解决方案

    设置文件格式为Unix :set fileformat=unix

     

    5.编码实战

    这里主要通过实现几个小功能来间接体会shell的使用:

    统计访问多的ip

     

    为了对Nginx日志文件里访问ip数和响应时间进行分析,需要学习shell脚本和一些命令的语法。

    这里主要是通过Nginx的logs/access.log里的记录来分析,因为用户每一次访问都会在这里生成日志。

    我们到达该目录下,执行cat access.log 可以看到里面所有日志

     

    它们按照下面的格式排列

    #设定日志格式,其中upstream_response_time是响应时间,request_time是请求时间

     log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '

                          '"$http_user_agent" "$http_x_forwarded_for" $request_time $upstream_response_time';

    执行 cat access.log | awk '{print $1}' ,将上面数据第一列显示,也就是只显示ip。这条指令等价于awk '{print $1}'  access.log;

    执行awk '{print $NF}'  access.log | sort ,

    sort:会将 文件/文本 的每一行作为一个单位,相互比较,比较原则是从首字符向后,依次按ASCII码值进行比较,最后将他们按升序输出。

    注意到上面的参数换成了$NF,它指每一行最后一个字段,最后一个字段是响应时间,这样就得到了响应时间的升序排列。(因为我的日志了ip太少,对ip排序效果不明显)


     

    我上面只是对大量输出结果的少量截取,其实有很多重复的。

    执行  awk '{print $NF}' access.log|sort| uniq -c

    uniq会对前面的结果去重,加上 -c会在这一列前面显示它重复的次数,如上图。

    其实我的目的是统计访问最多的ip,只是因为ip太少这里用响应时间来充当ip,那么可以对上面的结果安照重复出现的次数进行升序排序,awk '{print $NF}' access.log|sort| uniq -c | sort -n

    加上 sort 后的 -n 表示按照数值的大小进行排序,如果不加,效果是以下:

    可以看出它是从首字符向后,依次按ASCII码值进行比较,最后将他们按升序输出。这样显然不符合我们的需求。

    好,用上awk '{print $NF}' access.log|sort| uniq -c | sort -n ,得到了按照访问量的升序排序(假设它是ip)。但是我们希望一降序形式展示这样可以直观的看出访问量最多的那个。那么在先前的基础上加上 -r就可以了 。

    总结:

    在sort里:

    # -n是按照数字大小排序,-r是以相反顺序,-k是指定需要排序的栏位。

    所以最终:

    awk '{print $NF}' access.log|sort| uniq -c | sort -n -k 1 -r |more

    more : 分页查看文件内容。 如果内容不多,可以去掉它。

     

     

    6.扩展思考

    $0 、$1-$n   的使用

     

     

    7.参考文献

    http://man.linuxde.net/read (Linux命令大全)

     

     

     

     

    展开全文
  • 而React则仅仅是一个轻量级的Library,官方社区只定义了一套组件的周期规则,而周边社区可以基于此规则实现自己的组件,React并不会提供给你一套开箱即用的方案,而需要自己在第三方市场挑选满意的组件形成“全家桶...

    前言

    玩过Angular的同学都知道Angular作为一个Framework,拥有一套完备的生态,还集成了强大的CLI。而React则仅仅是一个轻量级的Library,官方社区只定义了一套组件的周期规则,而周边社区可以基于此规则实现自己的组件,React并不会提供给你一套开箱即用的方案,而需要自己在第三方市场挑选满意的组件形成“全家桶”,这也是React社区活跃的原因之一。

    最近工作中在考虑使用monorepo对项目进行管理,发现了一套dev toolkit叫做Nx,Nx使用monorepo的方式对项目进行管理,其核心开发者vsavkin同时也是Angular项目的早期核心成员之一,他把Angular CLI这套东西拿到Nx,使其不仅可以支持Angular项目的开发,现在还支持React项目。

    Nx支持开发自己的plugin,一个plugin包括schematics和builders(这两个概念也分别来自Angular的schematics以及cli-builders),schematics按字面意思理解就是“纲要”的意思,也就是可以基于一些模板自动化生成所需的文件;而builders就是可以自定义构建流程。

    今天要讲的就是如何开发一个属于自己的Nx plugin (包含schematics),我会使用它来自动化创建一个页面组件,同时更新router配置,自动将其加入react router的config。

    关于Monorepo

    这篇文章不会详细介绍什么是monorepo,mono有“单个”的意思,也就是单个仓库(所有项目放在一个仓库下管理),对应的就是polyrepo,也就是正常一个项目一个仓库。如下图所示:

    25340180e2c4ece157c380455a8ee8d6.png

    更多关于monorepo的简介,可以阅读以下文章:

    1. Advantages of monorepos
    2. How to develop React apps like Facebook, Microsoft, and Google
    3. Misconceptions about Monorepos: Monorepo != Monolith

    关于Nx plugin

    先贴一张脑图,一个一个讲解schematic的相关概念:

    eb02cc1ad0d6e2eef1d987f08bacf2a1.png

    前面提到Nx plugin包括了builder(自动化构建)和schematic(自动化项目代码的增删改查)。一个成型的Nx plugin可以使用Nx内置命令执行。

    对于文章要介绍的schematics,可以认为它是自动化代码生成脚本,甚至可以作为脚手架生成整个项目结构。

    Schematics要实现的目标

    Schematics的出现优化了开发者的体验,提升了效率,主要体现在以下几个方面:

    1. 同步式的开发体验,无需知道内部的异步流程
      Schematics的开发“感觉”上是同步的,也就是说每个操作输入都是同步的,但是输出则可能是异步的,不过开发者可以不用关注这个,直到上一个操作的结果完成前,下一个操作都不会执行。
    2. 开发好的schematics具有高扩展性和高重用性
      一个schematic由很多操作步骤组成,只要“步骤”划分合理,扩展只需要往里面新增步骤即可,或者删除原来的步骤。同时,一个完整的schematic也可以看做是一个大步骤,作为另一个schematic的前置或后置步骤,例如要开发一个生成Application的schematic,就可以复用原来的生成Component的schematic,作为其步骤之一。
    3. schematic是原子操作
      传统的一些脚本,当其中一个步骤发生错误,由于之前步骤的更改已经应用到文件系统上,会造成许多“副作用”,需要我们手动FIX。但是schematic对于每项操作都是记录在运行内存中,当其中一项步骤确认无误后,也只会更新其内部创建的一个虚拟文件系统,只有当所有步骤确认无误后,才会一次性更新文件系统,而当其中之一有误时,会撤销之前所做的所有更改,对文件系统不会有“副作用”。

    接下来我们了解下和schematic有关的概念。

    Schematics的相关概念

    在了解相关概念前,先看看Nx生成的初始plugin目录:

    your-plugin
        |--.eslintrc
        |--builders.json
        |--collection.json
        |--jest.config.js
        |--package.json
        |--tsconfig.json
        |--tsconfig.lib.json
        |--tsconfig.spec.json
        |--README.md
        |--src
            |--builders
            |--schematics
                |--your-schema
                    |--your-schema.ts
                    |--your-schema.spec.ts
                    |--schema.json
                    |--schema.d.ts

    Collection

    Collection包含了一组Schematics,定义在plugin主目录下的collection.json

    {
      "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json",
      "name": "your-plugin",
      "version": "0.0.1",
      "schematics": {
        "your-schema": {
          "factory": "./src/schematics/your-schema/your-schema",
          "schema": "./src/schematics/your-schema/schema.json",
          "aliases": ["schema1"],
          "description": "Create foo"
        }
      }
    }

    上面的json文件使用@angular-devkit/schematics下的collection schema来校验格式,其中最重要的是schematics字段,在这里面定义所有自己写的schematics,比如这里定义了一个叫做"your-schema"的schematic,每个schematic下需要声明一个rule factory(关于rule之后介绍),该factory指向一个文件中的默认导出函数,如果不使用默认导出,还可以使用your-schema#foo的格式指定当前文件中导出的foo函数。

    aliases声明了当前schematic的别名,除了使用your-schema的名字执行指令外,还可以使用schema1description表示一段可选的描述内容。

    schema定义了当前schematic的schema json定义,nx执行该schematic指令时可以读取里面设置的默认选项,进行终端交互提示等等,下面是一份schema.json

    {
      "$schema": "http://json-schema.org/schema",
      "id": "your-schema",
      "title": "Create foo",
      "examples": [
        {
          "command": "g your-schema --project=my-app my-foo",
          "description": "Generate foo in apps/my-app/src/my-foo"
        }
      ],
      "type": "object",
      "properties": {
        "project": {
          "type": "string",
          "description": "The name of the project.",
          "alias": "p",
          "$default": {
            "$source": "projectName"
          },
          "x-prompt": "What is the name of the project for this foo?"
        },
        "name": {
          "type": "string",
          "description": "The name of the schema.",
          "$default": {
            "$source": "argv",
            "index": 0
          },
          "x-prompt": "What name would you like to use for the schema?"
        }
        "prop3": {
          "type": "boolean",
          "description": "prop3 description",
          "default": true
        }
      },
      "required": ["name", "project"]
    }

    properties表示schematic指令执行时的选项,第一个选项project表示项目名,别名p,使用$default表示Angular内置的一些操作,例如$source: projectName则表示如果没有声明project,会使用Angular workspaceSchema(nx中为workspace.json)中的defaultProject选项,而第二个选项的$default则表明使用命令时的第一个参数作为name

    x-prompt会在用户不键入选项值时的交互,用来提示用户输入,用户可以不用预先知道所有选项也能完成操作,更复杂的x-prompt配置请查阅官网。

    说了这么多,以下是几个直观交互的例子,帮助大家理解:

    nx使用generate选项来调用plugin中的schematic或者builder,和Angular的ng generate一致:

    # 表示在 apps/app1/src/ 下生成一个名为bar的文件
    
    $ nx g your-plugin:your-schema bar -p=app1
    # 或者
    $ nx g your-plugin:your-schema -name=bar -project app1

    如果使用交互(不键入选项)

    # 表示在 apps/app1/src/ 下生成一个名为bar的文件
    
    $ nx g your-plugin:your-schema
    ? What is the name of the project for this foo?
    $ app1
    ? What name would you like to use for the schema?
    $ bar

    接下来看看Schematics的两个核心概念:TreeRule

    Tree

    根据官方对Tree的介绍:

    The virtual file system is represented by a Tree. The Tree data structure contains a base (a set of files that already exists) and a staging area (a list of changes to be applied to the base). When making modifications, you don't actually change the base, but add those modifications to the staging area.

    Tree这一结构包含了两个部分:VFS和Staging area,VFS是当前文件系统的一个虚拟结构,Staging area则存放schematics中所做的更改。值得注意的是,当做出更改时,并不是对文件系统的及时更改,而只是将这些操作放在Staging area,之后会把更改逐步同步到VFS,知道确认无误后,才会一次性对文件系统做出变更。

    Rule

    A Rule object defines a function that takes a Tree, applies transformations, and returns a new Tree. The main file for a schematic, index.ts, defines a set of rules that implement the schematic's logic.

    Rule是一个函数,接收TreeContext作为参数,返回一个新的Tree,在schematics的主文件index.ts中,可以定义一系列的Rule,最后将这些Rule作为一个综合的Rule在主函数中返回,就完成了一个schematic。下面是Tree的完整定义:

    export declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree> | Rule | Promise<void> | Promise<Rule> | void;

    来看看一个简单的schematic主函数,我们在函数中返回一个RuleRule的操作是新建一个默认名为hello的文件,文件中包含一个字符串world,最后将这个Tree返回。

    // src/schematics/your-schema/index.ts
    
    import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
    
    // You don't have to export the function as default. You can also have more than one rule factory
    // per file.
    export function myComponent(options: any): Rule {
      return (tree: Tree, _context: SchematicContext) => {
        tree.create(options.name || 'hello', 'world');
        return tree;
      };
    }

    Context

    最后是Context,上面已经提到过,对于Schematics,是在一个名叫SchematicContext的Context下执行,其中包含了一些默认的工具,例如context.logger,我们可以使用其打印一些终端信息。

    如何开发一个Nx Schematic?

    下面的所有代码均可以在我的GitHub里下载查看,觉得不错的话,欢迎大家star。

    接下来进入正题,我们开发一个nx plugin schematic,使用它来创建我们的页面组件,同时更新路由配置。

    假设我们的项目目录结构如下:

    apps
        |...
        |--my-blog
            |...
            |--src
                |--components
                |--pages
                    |--home
                        |--index.ts
                        |--index.scss
                    |--about
                |--routers
                    |--config.ts
                    |--index.ts
                |...

    router/config.ts文件内容如下:

    export const routers = {
      // 首页
      '/': 'home',
      // 个人主页
      '/about': 'about'
    };

    现在我们要新增一个博客页,不少同学可能就直接新建一个目录,复制首页代码,最后手动添加一条路由配置,对于这个例子倒是还好,但是如果需要更改的地方很多,就很浪费时间了,学习了Nx plugin schematics,这一切都可以用Schematic实现。

    搭建Nx环境并使用Nx默认的Schematic创建一个plugin

    如果之前已经有了Nx项目,则直接在项目根目录下使用以下命令创建一个plugin:

    $ nx g @nrwl/nx-plugin:plugin [pluginName]

    如果是刚使用Nx,也可以使用下面的命令快速新建一个项目,并自动添加一个plugin:

    $ npx create-nx-plugin my-org --pluginName my-plugin

    设置好Schematic选项定义

    现在Nx为我们创建了一个默认的plugin,首先更改packages/plugin/collection.json,为schema取名叫做“page”

    {
      "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json",
      "name": "plugin",
      "version": "0.0.1",
      "schematics": {
        "page": {
          "factory": "./src/schematics/page/page",
          "schema": "./src/schematics/page/schema.json",
          "description": "Create page component"
        }
      }
    }

    接下来定义我们提供的schema option,这里需要修改src/schematics/page/schema.jsonsrc/schematics/page/schema.d.ts,前者作为JSON Schema被Nx plugin使用,后者作为类型定义,开发时用到。

    对于page,我们需要提供两个必须选项:name和对应的project,两个可选选项:connect(是否connect to redux)、classComponent(使用类组件还是函数组件)。

    下面分别是schema.jsonschema.d.ts

    {
      "$schema": "http://json-schema.org/draft-07/schema",
      "id": "page",
      "title": "Create page component",
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "The name of the page component",
          "$default": {
            "$source": "argv",
            "index": 0
          },
          "x-prompt": "What name would you like to use?"
        },
        "project": {
          "type": "string",
          "description": "The project of the page component",
          "$default": {
            "$source": "projectName"
          },
          "alias": "p",
          "x-prompt": "Which projcet would you like to add to?"
        },
        "classComponent": {
          "type": "boolean",
          "alias": "C",
          "description": "Use class components instead of functional component.",
          "default": false
        },
        "connect": {
          "type": "boolean",
          "alias": "c",
          "description": "Create a connected redux component",
          "default": false
        }
      },
      "required": ["name", "project"]
    }
    export interface PageSchematicSchema {
      name: string;
      project: string;
      classComponent: boolean;
      connected: boolean;
    }

    开发Schematic

    创建所需模板文件

    模板文件就是通过一些模板变量来生成真正的文件。每一个页面默认有两个文件,index.tsindex.scss,因此创建模板文件如下:

    index.ts.template

    <% if (classComponent) { %>
    import React, { Component } from 'react';
    <% } else { %>
    import React from 'react';
    <% } %>
    <% if (connect) { %>
    import { connect } from 'react-redux';
    import { IRootState, Dispatch } from '../../store';
    <% } %>
    
    import { RouteComponentProps } from 'react-router-dom';
    import './index.scss';
    
    <% if (connect) { %>
    type StateProps = ReturnType<typeof mapState>;
    type DispatchProps = ReturnType<typeof mapDispatch>;
    type Props = StateProps & DispatchProps & RouteComponentProps;
    <% } else { %>
    type Props = RouteComponentProps;
    <% } %>
    
    
    <% if (classComponent) { %>
    class <%= componentName %> extends Component<Props> {
      render() {
        return (
          <div className="<% className %>">
            Welcome to <%= componentName %>!
          </div>
        );
      }
    }
    <% } else { %>
    const <%= componentName %> = (props: Props) => {
      return (
        <div className="<%= className %>">
          Welcome to <%= componentName %>!
        </div>
      );
    };
    <% } %>
    
    <% if (connect) { %>
    function mapState(state: IRootState) {
      return {
    
      }
    }
    
    function mapDispatch(dispatch: Dispatch) {
      return {
    
      }
    }
    <% } %>
    
    <% if (connect) { %>
    export default connect<StateProps, DispatchProps, {}>(mapState, mapDispatch)(<%= componentName %>);
    <% } else { %>
    export default <%= componentName %>;
    <% } %>

    index.scss.template

    .<%= className %> {
    
    }

    我们将模板文件放到src/schematics/page/files/下。

    基于模板文件创建所需文件和目录

    我们一共需要做四件事:

    1. 格式化选项(把schematic默认的选项进行加工,加工成我们所需的全部选项)。
    2. 基于模板文件创建所需文件和目录。
    3. 更新app/src/routers/config.ts
    4. 使用eslint格式化排版。

    先来实现1和2:

    page.ts:

    import { PageSchematicSchema } from './schema';
    import { names } from '@nrwl/workspace';
    import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';
    
    interface NormalizedSchema extends PageSchematicSchema {
      /** element className */
      className: string;
      componentName: string;
      fileName: string;
      projectSourceRoot: Path;
    }
    
    /** 加工选项 */
    function normalizeOptions(
      host: Tree,
      options: PageSchematicSchema
    ): NormalizedSchema {
      const { name, project } = options;
      const { sourceRoot: projectSourceRoot } = getProjectConfig(host, project);
      // kebab-case fileName and UpperCamelCase className
      const { fileName, className } = names(name);
      return {
        ...options,
        // element className
        className: `${project}-${fileName}`,
        projectSourceRoot,
        componentName: className,
        fileName,
      };
    }

    接下来使用模板文件:

    page.ts

    import { join } from '@angular-devkit/core';
    import {
      Rule,
      SchematicContext,
      mergeWith,
      apply,
      url,
      move,
      applyTemplates,
    } from '@angular-devkit/schematics';
    import { PageSchematicSchema } from './schema';
    import { names } from '@nrwl/workspace';
    import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';
    
    interface NormalizedSchema extends PageSchematicSchema {
      /** element className */
      className: string;
      componentName: string;
      fileName: string;
      projectSourceRoot: Path;
    }
    
    /** 基于模板创建文件 */
    function createFiles(options: NormalizedSchema): Rule {
      const { projectSourceRoot, fileName } = options;
      const targetDir = join(projectSourceRoot, pagePath, fileName);
      return mergeWith(
        apply(url('./files'), [applyTemplates(options), move(targetDir)])
      );
    }

    原理是使用Angular Schematics自带的mergeWithRule,接收一个SourceSource的定义如下:

    A source is a function that generates a Tree from a specific context.

    也就是说Source()会生成一棵新的Tree。然后将其和原来的Tree合并。

    由于我们需要从模板文件中加载,首先需要使用url加载文件,url接收文件或文件夹的相对地址,返回一个Source,然后我们使用applyurl加载模板文件后的Source进行加工,apply接收一个Source和一个Rule的数组,将Rule[]应用后返回一个新的Source

    这里我们需要进行两种“加工”,首先使用options替换模板文件中的变量,最后将这些文件使用move移动到对应的目录下即可。

    更新router config

    来到了最重要也是比较难的一个步骤,我们还需要修改src/routers/config.ts中的routers变量,在里面增加我们刚加上的page component。

    由于这里是TS文件,所以需要分析TS的AST (Abstract Syntax Tree),然后修改AST,最后使用修改的AST对原来内容进行覆盖即可。

    修改AST可以使用TS官方的Compiler API结合TypeScript AST Viewer进行。不过由于AST的复杂结构,TS Compiler API也不太友好,直接使用API对AST进行操作非常困难。例如AST的每个节点都有position信息,做一个新的插入时,还需要对position进行计算,API并没有人性化的操作方式。

    由于上面的原因,我最终选择了ts-morph,ts-morph以前也叫做ts-simple-ast,它封装了TS Compiler API,让操作AST变得简单易懂。

    看代码之前,我们先使用TS AST Viewer分析一下routers/config.ts这段代码的AST:

    export const routers = {
      // 首页
      '/': 'home',
      // 第二页
      '/about': 'about'
    };

    AST如下(只含根节点信息):

    3769025f719e9e78c5cf5ea2b1cc33c3.png

    我们来层层分析:

    1. 从声明到赋值,整段语句作为Variable Statement
    2. 由于routers是被导出的,包含了ExportKeyword
    3. routers = xxx作为VariableDeclarationList中的唯一一个VariableDeclaration
    4. 最后是Identifier“routers”,再到字面量表达式作为它的value。
    5. ...

    由于下面代码用到了Initializer,上述的对象字面量表达式ObjectLiteralExpression就是routers这个VariableDeclarationInitializer

    469fcc41a447ac8bfc3af937a9b94cb2.png

    看懂AST后,更新router后的代码就容易理解了:

    import { join, Path } from '@angular-devkit/core';
    import {
      Rule,
      Tree,
      chain,
      SchematicContext,
      mergeWith,
      apply,
      url,
      move,
      applyTemplates,
    } from '@angular-devkit/schematics';
    import { PageSchematicSchema } from './schema';
    import { formatFiles, names } from '@nrwl/workspace';
    import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';
    import { Project } from 'ts-morph';
    
    /** 更新路由配置 */
    function updateRouterConfig(options: NormalizedSchema): Rule {
      return (host: Tree, context: SchematicContext) => {
        const { projectSourceRoot, fileName } = options;
        const filePath = join(projectSourceRoot, routerConfigPath);
        const srcContent = host.read(filePath).toString('utf-8');
    
        // 使用ts-morph的project对AST进行操作
        const project = new Project();
        const srcFile = project.createSourceFile(filePath, srcContent, {
          overwrite: true,
        });
        try {
          // 根据变量标识符拿到对应的VariableDeclaration
          const decl = srcFile.getVariableDeclarationOrThrow(
            routerConfigVariableName
          );
          // 获取initializer并转换成string
          const initializer = decl.getInitializer().getText();
          // 使用正则匹配对象字面量的最后一部分并做插入
          const newInitializer = initializer.replace(
            /,?s*}$/,
            `,'/${fileName}': '${fileName}' }`
          );
          // 更新initializer
          decl.setInitializer(newInitializer);
          // 获取最新的TS文件内容对源文件进行覆盖
          host.overwrite(filePath, srcFile.getFullText());
        } catch (e) {
          context.logger.error(e.message);
        }
      };
    }

    在如何对Initializer进行操作时,我最开始想到的是将其使用JSON.parse()转换成对象字面量,然后进行简单追加,后面发现这段内容里还可能包含注释,所以只能通过正则匹配确定字面量的“尾部部分”,然后进行匹配追加。

    使用eslint做好排版

    操作完成后我们可以使用Nx workspace提供的formatFiles将所有文件排版有序。最后我们只需要在默认导出函数里将上述Rule通过chain这个Rule进行汇总。来看看最终代码:

    import { join, Path } from '@angular-devkit/core';
    import {
      Rule,
      Tree,
      chain,
      SchematicContext,
      mergeWith,
      apply,
      url,
      move,
      applyTemplates,
    } from '@angular-devkit/schematics';
    import { PageSchematicSchema } from './schema';
    import { formatFiles, names } from '@nrwl/workspace';
    import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';
    import { Project } from 'ts-morph';
    
    interface NormalizedSchema extends PageSchematicSchema {
      /** element className */
      className: string;
      componentName: string;
      fileName: string;
      projectSourceRoot: Path;
    }
    
    // 页面组件目录
    const pagePath = 'pages';
    // 路由配置目录
    const routerConfigPath = 'routers/config.ts';
    // 路由配置文件中需要修改的变量名
    const routerConfigVariableName = 'routers';
    
    /** 加工选项 */
    function normalizeOptions(
      host: Tree,
      options: PageSchematicSchema
    ): NormalizedSchema {
      const { name, project } = options;
      const { sourceRoot: projectSourceRoot } = getProjectConfig(host, project);
      // kebab-case fileName and UpperCamelCase className
      const { fileName, className } = names(name);
      return {
        ...options,
        // element className
        className: `${project}-${fileName}`,
        projectSourceRoot,
        componentName: className,
        fileName,
      };
    }
    
    /** 基于模板创建文件 */
    function createFiles(options: NormalizedSchema): Rule {
      const { projectSourceRoot, fileName } = options;
      const targetDir = join(projectSourceRoot, pagePath, fileName);
      return mergeWith(
        apply(url('./files'), [applyTemplates(options), move(targetDir)])
      );
    }
    
    /** 更新路由配置 */
    function updateRouterConfig(options: NormalizedSchema): Rule {
      return (host: Tree, context: SchematicContext) => {
        const { projectSourceRoot, fileName } = options;
        const filePath = join(projectSourceRoot, routerConfigPath);
        const srcContent = host.read(filePath).toString('utf-8');
        const project = new Project();
    
        const srcFile = project.createSourceFile(filePath, srcContent, {
          overwrite: true,
        });
        try {
          const decl = srcFile.getVariableDeclarationOrThrow(
            routerConfigVariableName
          );
          const initializer = decl.getInitializer().getText();
          const newInitializer = initializer.replace(
            /,?s*}$/,
            `,'/${fileName}': '${fileName}' }`
          );
          decl.setInitializer(newInitializer);
          host.overwrite(filePath, srcFile.getFullText());
        } catch (e) {
          context.logger.error(e.message);
        }
      };
    }
    
    // 默认的rule factory
    export default function (schema: PageSchematicSchema): Rule {
      return function (host: Tree, context: SchematicContext) {
        const options = normalizeOptions(host, schema);
        return chain([
          createFiles(options),
          updateRouterConfig(options),
          formatFiles({ skipFormat: false }),
        ]);
      };
    }

    测试

    写好了schematic,别忘了进行测试,测试代码如下:

    page.spec.ts

    import { Tree, Rule } from '@angular-devkit/schematics';
    import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
    import { createEmptyWorkspace } from '@nrwl/workspace/testing';
    import { join } from 'path';
    import { PageSchematicSchema } from './schema';
    import { updateWorkspace, names } from '@nrwl/workspace';
    
    const testRunner = new SchematicTestRunner(
      '@plugindemo/plugin',
      join(__dirname, '../../../collection.json')
    );
    
    export function callRule(rule: Rule, tree: Tree) {
      return testRunner.callRule(rule, tree).toPromise();
    }
    
    export async function createFakeApp(tree: Tree, appName: string): Promise<Tree> {
      const { fileName } = names(appName);
    
      const appTree = await callRule(
        updateWorkspace((workspace) => {
          workspace.projects.add({
            name: fileName,
            root: `apps/${fileName}`,
            projectType: 'application',
            sourceRoot: `apps/${fileName}/src`,
            targets: {},
          });
        }),
        tree
      );
      appTree.create(
        'apps/app1/src/routers/config.ts',
        `
        export const routers = {
          // 首页
          '/': 'home',
          // 个人主页
          '/about': 'about'
        };
      `
      );
      return Promise.resolve(appTree);
    }
    
    describe('plugin schematic', () => {
      let appTree: Tree;
      const options: PageSchematicSchema = { name: 'myPage', project: 'app1' };
    
      beforeEach(async () => {
        appTree = createEmptyWorkspace(Tree.empty());
        appTree = await createFakeApp(appTree, 'app1');
      });
    
      it('should run successfully', async () => {
        const tree = await testRunner.runSchematicAsync('page', options, appTree).toPromise();
        // file exist
        expect(
          tree.exists('apps/app1/src/pages/my-page/index.tsx')
        ).toBeTruthy();
        expect(
          tree.exists('apps/app1/src/pages/my-page/index.scss')
        ).toBeTruthy();
    
        // router modified correctly
        const configContent = tree.readContent('apps/app1/src/routers/config.ts');
        expect(configContent).toMatch(/,s*'/my-page': 'my-page'/);
      });
    });

    测试这块能用的轮子也比较多,我这里简单创建了一个假的App(符合上面说的目录结构),然后进行了一下简单测试。测试可以使用如下指令对plugin中的单个schematic进行测试:

    $ nx test plugin --testFile page.spec.ts

    如果写的plugin比较复杂,建议再进行一遍end2end测试,Nx对e2e的支持也很好。

    发布

    最后到了发布环节,使用Nx build之后便可以自行发布了。

    $ nx build plugin

    上述所有代码均可以在我的GitHub里下载查看,同时代码里还增加了一个真实开发环境下的App-demo,里面将plugin引入了正常的开发流程,更能感受到其带来的便捷性。觉得不错的话,欢迎大家star。

    总结

    其实要写好这类对文件系统“增删改查”的工具,关键还是要理解文件内容,比如上面的难点就在于理解TS文件的AST。使用ts-morph还可以做很多事情,比如我们每增加一个文件,可能需要在出口index.ts中导出一次,使用ts-morph就一句话的事情:

    const exportDeclaration = sourceFile.addExportDeclaration({
        namedExports: ["MyClass"],
        moduleSpecifier: "./file",
    });

    当然,Nx和Angular提供了这一套生态,能用的工具和方法非常多,但是也需要我们耐心查阅,合理使用。目前来说Nx封装的方法没有详细的文档,可能用起来需要直接查阅d.ts文件,没那么方便。

    工欲善其事,必先利其器。Happy Coding!

    展开全文
  • 尝试自己编写一个简单脚本。 1 背景介绍 每次发布tomcat项目的时候都要反复敲一些命令,重启tomcat之前先检查tomcat进程有没有停掉,没有还要手动kill该进程,单独部署一个项目还好,如果一次多个部署,就比较费劲了...
  • 2018年的最后一个工作日,我从矿大研究...我给自己安排的首要任务就是完论文争取年底前拿到学位。本以为暂时没有了工作的繁忙,可以全心投入到文章的写作中。没想到随后发生的事情让我2018年的最后4个月如A股一般...
  • 记得之前一个读者和小鹿说去面试的时候,面试官让写一个冒泡排序,也写出来了,最后去没有通过面试。其实他的冒泡排序没有进行优化的,这也不是重点。在学数据结构和算法我有一个重大的发现,也包括我自己前...
  • 创建脚本文件的时候,必须将文件的第行指定要使用的shell。其格式为:#!/bin/bash运行脚本文件时要制定文件位置,还要赋予文件运行的权限:chmod u+x file运行:./file2. 显示消息:echo命令,比如:#!/bin/bash...
  • 脚本在最后,可以直接试用,相关的库可能需要自己安装第步,你需要获得网页上下载文件地址的列表用浏览器查看页面源文件得到可以看到,这格式是很清晰的,用xpath获得列表即可,这部分你可以去网上找相关教程,...
  • 很多人都有这疑问,包括我自己在学习新媒体运营之前,这也是我最大的困惑。现在是内容为王的时代,你的文章质量决定了你的KPI。后来我学习了文章的写作结构和技巧后,就能够篇5000+阅读量的文章,虽然比不上...
  • #echo "Start install now!...注意第二行末尾的,表示重定向输入,将我们输入的命令字符串作为一个执行程序的输入,这样,我们就不需要在那个程序环境中手工输入命令,以便自动执行我们需要的功能: ...
  • shell是用户和Linux内核之间的接口程序,如果把Linux内核想象成一个球体的中心,shell就是围绕内核的外层。当从shell或其他程序向Linux传递命令时,内核会做出相应的反应。Shell既是一种命令语言...
  • 近来使用的google翻译的插件有些不稳定, 时灵时不灵的,无奈的使用百度翻译了, 感觉体验也不好, 就想自己写一个翻译的脚本。整体思路简单, 就是调用百度翻译api再将结果呈现出来, 最后使用alias链接一下。大致...
  • 最近看到一篇关于爬虫的文章,而自己又正好在爬虫,于是就想写一篇分享下, 让我们一步一步来, 第一步:安装核心爬虫依赖puppeteer, 如果你打开googole.com是404,运行npm i puppeteer前,先运行set PUPPETEER_...
  • 如何检测谁ssh连接自己的次数最多? last命令 查看谁都ssh连过自己 last -i 查看并且显示ip 0.0.0.0是主机自己,172网段的才是别人 last -i | grep 0.0.0.0 -v过滤 last -i | grep 0.0.0.0 -v | cut -...
  • 用nodejs写一个yys挂机脚本

    千次阅读 2019-10-04 01:59:37
    用nodejs写一个yys外挂 为什么要用node来写 曾经用python写过一个自用御魂脚本,作为一个前端码农,就考虑能不能用js实现,js如何来使用天使插件(TSPlug.dll)实现后台操作呢?经google,github发现,已经有大佬实现...
  • 当你在自己的服务器一个脚本时,可以在命令行终端 输入命令运行, 如python 脚本 $python test.py 但是当你退出命令行终端或者关闭 ssh连接, 运行的脚本也会终止。那如何让脚本在后台一直运行呢?如何启动的...
  • 反正自己抢不到,想着写个脚本试试。几个关键步骤获取优惠券的url直接审查元素获取cookie通过本地代理,比如BurpSuite定时调用# coding=utf-8import requests, schedule, timedef do_request(_url, _cookie):...
  • 环境变量是一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。例如path,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到path
  • 最近一直在opengrok上看...最后没办法只好自己动手写一个脚本,从网卡找了个python脚本,修改了一下,运行良好。 代码如下,因为是我的环境,所以对于脚本中有些提供关键词要根据你的需求来修改。 #!/usr/bin/pyth...
  • 如何写shell脚本

    2018-11-18 09:37:25
    这里是修真院后端小课堂,每篇分享文从 【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】 八个方面深度解析后端知识/技能,本篇...尝试自己编写一个简单脚本...
  • 但是自己写的传参脚本,一般只传一个参数,如果传多个,也是固定的顺序,那么如何用python写出更专业的传参脚本呢? 答:使用python自带的getopt模块。 1、语法: import getopt getopt.getopt(args,shortop...
  • 如何用Autojs来写脚本赚零花钱(第课)

    万次阅读 多人点赞 2019-08-08 23:37:05
    我劝你把手机仍在一个角落里面,让它乖乖地给你去赚钱,而你呢,你应该去做你自己想做的事情,比如对于我来说,我喜欢程序,看书,喝茶,做家务,陪儿子玩。那么这个时间你就不要去玩手机了,极简主义者连手机这种...
  • 这两天用自己的数据来做训练,看到datasets库中的教程,进行翻译,记下如何使用本地/私有数据集。 文章目录HuggingFace Datasets上传本地文件作为训练数据前言编写数据集加载脚本(Writing a dataset loading script...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,119
精华内容 447
关键字:

如何自己写一个脚本