熊D01001

熊D01001 查看完整档案

深圳编辑  |  填写毕业院校深圳互联网公司  |  Web 前端工程师 编辑 blog.beard.ink 编辑
编辑

SIMPLE BUT NOT SIMPLE.

个人动态

熊D01001 收藏了文章 · 2020-01-15

Go Module 工程化实践(一):基础概念篇

人的一切痛苦,本質上都是對自己的無能的憤怒。 -- 王小波

1. 基础概念篇

Go Module已经来了,默认Go Module模式将会在1.13版本发布。也就是说半年后,就会全面铺开。鉴于官方提供扫盲文档中的样例过于简单,提供一个更加贴近实际开发过程的例子也许是有必要的。

官方文档参考:Go Module Wiki

1.1 准备环境

按照官方的说明,Go Module是在 Go 的 1.11版本开始引入,但是默认该选项是关闭的,直到1.13版本将会默认开启。预计1.13将会在2019年8月份发布。所以在这之前,是必须手动开启Go Module支持。

必要条件:

  • Go语言版本 >= 1.11
  • 设置环境变量 GO111MODULE=on

在开启Go Module功能后,官方还提供了环境变量GOPROXY用于设置包镜像服务。此处暂不详细介绍了。

1.2 Go Module带来的改变

1.2.1 GOPATH作用的改变

引入Go Module后,环境变量GOPATH还是存在的。开启Go Module功能开关后,环境变量GOPATH的作用也发生了改变。

When using modules, GOPATH is no longer used for resolving imports. However, it is still used to store downloaded source code (in GOPATH/pkg/mod) and compiled commands (in GOPATH/bin).

翻译出来就是:

  • 环境变量GOPATH不再用于解析imports包路径,即原有的GOPATH/src/下的包,通过import是找不到了。
  • Go Module功能开启后,下载的包将存放与$GOPATH/pkg/mod路径
  • $GOPATH/bin路径的功能依旧保持。

1.2.2 新增go.mod文件配置

开始Go Module开发之前,首先是初始化一个正确的Go Module定义,即go.mod文件。
何为正确的Go Module定义。就是说mod包必须符合。

方法一

  • $GOPATH/src的目录下,创建合理的路径github.com/liujianping/foo路径。
  • 进入$GOPATH/src/github.com/liujianping/foo路径,执行go mod init即可。

或者

方法二

  • 创建foo路径,位置任意
  • 进入foo目录,执行go mod init github.com/liujianping/foo即可。

生成了go.mod文件后,就该文件的语法简单的学习一下。

  • module
    to define the module path;
  • go
    to set the expected language version;
  • require
    to require a particular module at a given version or later;
  • exclude
    to exclude a particular module version from use; and
  • replace
    to replace a module version with a different module version.

官方提供了一个简单全面的例子:

module my/thing
go 1.12
require other/thing v1.0.2
require new/thing/v2 v2.3.4
exclude old/thing v1.2.3
replace bad/thing v1.4.5 => good/thing v1.4.5

1.2.3 go get流程改变

引入Go Module之后,go get官方又重新实现了一套。具体实现代码可以参考:

  • 不开启Go Module功能,go get代码实现
$GOROOT/src/cmd/go/internal/get/get.go
  • 开启Go Module功能,go get代码实现
$GOROOT/src/cmd/go/internal/modget/get.go

简单说明一下主要区别,更详细的go get取包原理放到下篇讲解。最直接的区别是:

  • 老的go get取包过程类似:git clone + go install , 开启Go Module功能后go get就只有 git clone 或者 download过程了。
  • 新老实现还有一个不同是,两者存包的位置不同。前者,存放在$GOPATH/src目录下;后者,存放在$GOPATH/pkg/mod目录下。
  • 老的go get取完主包后,会对其repo下的submodule进行循环拉取。新的go get不再支持submodule子模块拉取。

1.3 一个完整的例子

官方的版本由于过于简单,连一个基础的本地第三方包的引入都没有,仅仅通过引入一个公开的第三方开源包,缺少了常规本地开发说明。所以,笔者特意提供一个完整的例子,分别从:

  • 本地仓库
  • 远程仓库
  • 私有仓库

三个维度阐释Go Module的在实际开发中的具体应用。

本例子的目录结构如下:

$GOPATH/src
├── github.com
    └── liujianping
        ├── demo
        │   └── go.mod
        └── foo
            └── go.mod

创建两个mod模块:demofoo, 其中 foo 作为一个依赖包,提供简单的 Greet 函数供 demo 项目调用。

1.3.1 本地仓库

本地仓库的意思,就是例子中的两个包: github.com/liujianping/demogithub.com/liujianping/foo 暂时仅仅存在于本地。无法通过 go get 直接从github.com 上获取。

通过以下命令,简单的创建项目代码:

$: mkdir -p $GOPATH/src/github.com/liujianping/foo
$: cd $GOPATH/src/github.com/liujianping/foo
$: go mod init
$: cat <<EOF > foo.go
package foo

import "fmt"

func Greet(name string) string {
    return fmt.Sprintf("%s, 你好!", name)
}
EOF

$: mkdir -p $GOPATH/src/github.com/liujianping/demo
$: cd $GOPATH/src/github.com/liujianping/demo
$: go mod init
$: cat <<EOF > main.go
package  main

import ( 
    "fmt"
    "github.com/liujianping/foo"
)

func main(){
    fmt.Println(foo.Greet("GoModule"))    
}
EOF

执行完以上命令以后,mod demofoo 的代码部分就完成了。现在来执行以下:

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
build github.com/liujianping/demo: cannot find module for path github.com/liujianping/foo

从输出可以看出,在demo 中调用 foo的依赖包,在编译过程就失败了。demo无法找到
github.com/liujianping/foo。为什么这样?

按照传统的$GOPATH引入包原则,只要在$GOPATH/src存在相应路径的包,就可以完成编译了。从现在的情形就可以解释$GOPATHGo Module功能开启后,对原有引入包的规则发生的改变。

既然,$GOPATH/src路径不再支持。那么如何解决这个无法找到包依赖的问题呢?方法有二:

  • 本地路径
  • 远程仓库

该小节提供本地路径方法。

$: cat $GOPATH/src/github.com/liujianping/demo/go.mod
module github.com/liujianping/demo

目前demo项目的go.mod仅仅一句话,因为无法找github.com/liujianping/foo,所以在go build过程中也不会修改go.mod,增加对包github.com/liujianping/foo的依赖关系。所以,只能是手动处理了。修改go.mod文件如下:

module github.com/liujianping/demo

require github.com/liujianping/foo v0.0.0
replace github.com/liujianping/foo => ../foo

再次执行demo程序:

$: go run main.go
go: finding github.com/liujianping/foo v0.0.0
GoModule, 你好!

对于项目中直接引用本地依赖包的官方文档中有段注意事项:

Note: for direct dependencies, a require directive is needed even when doing a replace. For example, if foo is a direct dependency, you cannot do replace foo => ../foo without a corresponding require for foo. (If you are not sure what version to use in the require directive, you can often use v0.0.0 such as require foo v0.0.0; see #26241).

意思就是,即使是本地依赖包,明确的require仍然是需要的。至于版本号,其实只要符合SemVer规范就可以。可以是v0.0.0,也可以是v0.1.2

Go Module最主要是引入了依赖包的版本控制。所以,我们不妨就本地版本测试一下。

对本地版本foo进行相应的git本地版本控制,增加几个版本,代码中相应的增加版本信息。

package foo

import "fmt"

func Greet(name string) string {
    return fmt.Sprintf("%s, 你好! Version 1.0.0", name)
}

增加了以下三个版本tag。

$: git tag
v0.1.0
v0.2.0
v1.0.0

demo项目中,设置foo版本, go.mod修改如下:

module github.com/liujianping/demo

require github.com/liujianping/foo v0.1.0
replace github.com/liujianping/foo => ../foo

执行demo程序,输出如下:

go run main.go
go: finding github.com/liujianping/foo v0.1.0
GoModule, 你好! Version 1.0.0

不难得出结论:go get是不会从本地仓库获取版本信息的,查看go get在module模式下工具链实现代码也可得出这个结论。

1.3.2 远程仓库

从上节可以大致了解Go Module的原理。现在我们将foo依赖包上传到github.com上,包括相应的版本tag。首先github.com创建相应的项目foo.再将本地仓库上传到远程仓库中。

$: git remote add origin git@github.com:liujianping/foo.git
$: git push -u origin master

上传版本tag信息:

$: git push origin --tags

现在完成了github.com/liujianping/foo依赖包的远程部署。看看具体实操demo项目,首先去掉本地的直接依赖。demo项目的go.mod如下

$: cat $GOPATH/src/github.com/liujianping/demo/go.mod
module github.com/liujianping/demo

重新执行demo项目

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
go: finding github.com/liujianping/foo v1.0.0
go: downloading github.com/liujianping/foo v1.0.0
GoModule, 你好! Version 1.0.0

查看变更后的go.mod,如下

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.0 // indirect

同时demo根目录下,增加了go.sum文件。

cat go.sum
github.com/liujianping/foo v1.0.0 h1:yYoUzvOwC1g+4mXgSEloF187GmEpjKAHEmkApDwvOVQ=
github.com/liujianping/foo v1.0.0/go.mod h1:HKRu+NgbfULQV4mqZOnCXpF9IwlhOOIwmns7gpwjZic=

修改foo版本号到 v0.2.0

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v0.2.0 // indirect

重新执行demo项目

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
go: finding github.com/liujianping/foo v0.2.0
go: downloading github.com/liujianping/foo v0.2.0
GoModule, 你好! Version 0.2.0

再看看go.sum文件发生的变化:

cat go.sum
github.com/liujianping/foo v0.2.0 h1:2JCV7mfUyneSksnWokX0kZoBbtWPoyL8s8iW80WHl/A=
github.com/liujianping/foo v0.2.0/go.mod h1:HKRu+NgbfULQV4mqZOnCXpF9IwlhOOIwmns7gpwjZic=
github.com/liujianping/foo v1.0.0 h1:yYoUzvOwC1g+4mXgSEloF187GmEpjKAHEmkApDwvOVQ=
github.com/liujianping/foo v1.0.0/go.mod h1:HKRu+NgbfULQV4mqZOnCXpF9IwlhOOIwmns7gpwjZic=

通过以上步骤,粗略可以了解针对Go Module对于远程仓库的版本选择。简单解释版本的选择过程下:

  • 检查远程仓库最新的tag版本,有就取得该版本
  • 远程仓库没有tag版本时,直接获取master分支的HEAD版本
  • 如果在go.mod文件中指定了具体版本,go get直接获取该指定版本
  • go.mod中除了可以指定具体版本号以外,还支持分支名

继续对远程版本foo增加新的版本v1.0.1。提交相应代码并推送版本标签v1.0.1到远端。并重新设置demo项目中的go.mod中的依赖版本为v1.0.0.如下:

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.0 // indirect

重新执行demo项目

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
GoModule, 你好! Version 1.0.0

这次执行没有输出go本身的提示信息,而是直接输出了结果。因为github.com/liujianping/foo v1.0.0已经存在于本地的缓存中了,不妨查看一下。

$: ls $GOPATH/pkg/mod/github.com/liujianping/foo@v1.0.0

虽然就demo项目而言,依赖项目foo有两个v1.0.0v1.0.1两个版本可用。按照GoModule版本选择最小版本的算法,demo项目依旧选择v1.0.0版本。

如何更新依赖包版本

更新依赖包的版本,最简单的方式,直接手动编辑go.mod设置依赖包版本即可。

另外一种方式就是通过go get -u的方式进行自动更新。具体操作步骤如下:

查看依赖包版本更新信息

$: go list -u -m all
go: finding github.com/liujianping/foo v1.0.1
github.com/liujianping/demo
github.com/liujianping/foo v1.0.0 [v1.0.1]

更新依赖包版本

$: go get -u 
go: downloading github.com/liujianping/foo v1.0.1

或者,制定更新patch版本

$: go get -u=patch github.com/liujianping/foo 
go: downloading github.com/liujianping/foo v1.0.1

此时,go.mod文件即被更新

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.1

重新执行程序

$: go run main.go
GoModule, 你好! Version 1.0.1

基于分支

GoModule除了支持基于标签tag的版本控制,可以直接利用远程分支名称进行开发。

所以本节,笔者就模块foo创建一个新的远程分支develop.具体代码,请直接参考github.com/liujianping/foo项目。

修改demo项目的go.mod文件:

module github.com/liujianping/demo

require github.com/liujianping/foo develop

再次执行demo, 结果如下:

$: go run main.go
go: finding github.com/liujianping/foo develop
go: downloading github.com/liujianping/foo v1.0.2-0.20190214080857-9c0018d55446
GoModule, 你好! Branch develop

查看,go.mod文件,发生如下变更:

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.2-0.20190214080857-9c0018d55446

按官方文档的说明,使用分支名,可以直接拉取该分支的最后一次提交。从实验来看, Go Module一旦发生编译就会针对分支名的依赖进行版本号固定。

1.3.3 私有仓库

对于私有仓库而言,其原理与1.3.2中的远程仓库是类似的。唯一不同之处是,go get取包的过程可能存在种种障碍,导致无法通过go get取到私有仓库包。主要原因可能是:

  • 权限问题
  • 路径问题

导致按照正常的go get过程取包失败。如果了解了go get取包原理,以上问题也就迎刃而解了。

更多文章可直接访问个人BLOG:GitDiG.com

微信公众号:一艘慢船,请扫描二维码,。
qrcode_for_gh_106a756d9c99_258.jpg

相关阅读:

查看原文

熊D01001 赞了文章 · 2020-01-15

Go Module 工程化实践(一):基础概念篇

人的一切痛苦,本質上都是對自己的無能的憤怒。 -- 王小波

1. 基础概念篇

Go Module已经来了,默认Go Module模式将会在1.13版本发布。也就是说半年后,就会全面铺开。鉴于官方提供扫盲文档中的样例过于简单,提供一个更加贴近实际开发过程的例子也许是有必要的。

官方文档参考:Go Module Wiki

1.1 准备环境

按照官方的说明,Go Module是在 Go 的 1.11版本开始引入,但是默认该选项是关闭的,直到1.13版本将会默认开启。预计1.13将会在2019年8月份发布。所以在这之前,是必须手动开启Go Module支持。

必要条件:

  • Go语言版本 >= 1.11
  • 设置环境变量 GO111MODULE=on

在开启Go Module功能后,官方还提供了环境变量GOPROXY用于设置包镜像服务。此处暂不详细介绍了。

1.2 Go Module带来的改变

1.2.1 GOPATH作用的改变

引入Go Module后,环境变量GOPATH还是存在的。开启Go Module功能开关后,环境变量GOPATH的作用也发生了改变。

When using modules, GOPATH is no longer used for resolving imports. However, it is still used to store downloaded source code (in GOPATH/pkg/mod) and compiled commands (in GOPATH/bin).

翻译出来就是:

  • 环境变量GOPATH不再用于解析imports包路径,即原有的GOPATH/src/下的包,通过import是找不到了。
  • Go Module功能开启后,下载的包将存放与$GOPATH/pkg/mod路径
  • $GOPATH/bin路径的功能依旧保持。

1.2.2 新增go.mod文件配置

开始Go Module开发之前,首先是初始化一个正确的Go Module定义,即go.mod文件。
何为正确的Go Module定义。就是说mod包必须符合。

方法一

  • $GOPATH/src的目录下,创建合理的路径github.com/liujianping/foo路径。
  • 进入$GOPATH/src/github.com/liujianping/foo路径,执行go mod init即可。

或者

方法二

  • 创建foo路径,位置任意
  • 进入foo目录,执行go mod init github.com/liujianping/foo即可。

生成了go.mod文件后,就该文件的语法简单的学习一下。

  • module
    to define the module path;
  • go
    to set the expected language version;
  • require
    to require a particular module at a given version or later;
  • exclude
    to exclude a particular module version from use; and
  • replace
    to replace a module version with a different module version.

官方提供了一个简单全面的例子:

module my/thing
go 1.12
require other/thing v1.0.2
require new/thing/v2 v2.3.4
exclude old/thing v1.2.3
replace bad/thing v1.4.5 => good/thing v1.4.5

1.2.3 go get流程改变

引入Go Module之后,go get官方又重新实现了一套。具体实现代码可以参考:

  • 不开启Go Module功能,go get代码实现
$GOROOT/src/cmd/go/internal/get/get.go
  • 开启Go Module功能,go get代码实现
$GOROOT/src/cmd/go/internal/modget/get.go

简单说明一下主要区别,更详细的go get取包原理放到下篇讲解。最直接的区别是:

  • 老的go get取包过程类似:git clone + go install , 开启Go Module功能后go get就只有 git clone 或者 download过程了。
  • 新老实现还有一个不同是,两者存包的位置不同。前者,存放在$GOPATH/src目录下;后者,存放在$GOPATH/pkg/mod目录下。
  • 老的go get取完主包后,会对其repo下的submodule进行循环拉取。新的go get不再支持submodule子模块拉取。

1.3 一个完整的例子

官方的版本由于过于简单,连一个基础的本地第三方包的引入都没有,仅仅通过引入一个公开的第三方开源包,缺少了常规本地开发说明。所以,笔者特意提供一个完整的例子,分别从:

  • 本地仓库
  • 远程仓库
  • 私有仓库

三个维度阐释Go Module的在实际开发中的具体应用。

本例子的目录结构如下:

$GOPATH/src
├── github.com
    └── liujianping
        ├── demo
        │   └── go.mod
        └── foo
            └── go.mod

创建两个mod模块:demofoo, 其中 foo 作为一个依赖包,提供简单的 Greet 函数供 demo 项目调用。

1.3.1 本地仓库

本地仓库的意思,就是例子中的两个包: github.com/liujianping/demogithub.com/liujianping/foo 暂时仅仅存在于本地。无法通过 go get 直接从github.com 上获取。

通过以下命令,简单的创建项目代码:

$: mkdir -p $GOPATH/src/github.com/liujianping/foo
$: cd $GOPATH/src/github.com/liujianping/foo
$: go mod init
$: cat <<EOF > foo.go
package foo

import "fmt"

func Greet(name string) string {
    return fmt.Sprintf("%s, 你好!", name)
}
EOF

$: mkdir -p $GOPATH/src/github.com/liujianping/demo
$: cd $GOPATH/src/github.com/liujianping/demo
$: go mod init
$: cat <<EOF > main.go
package  main

import ( 
    "fmt"
    "github.com/liujianping/foo"
)

func main(){
    fmt.Println(foo.Greet("GoModule"))    
}
EOF

执行完以上命令以后,mod demofoo 的代码部分就完成了。现在来执行以下:

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
build github.com/liujianping/demo: cannot find module for path github.com/liujianping/foo

从输出可以看出,在demo 中调用 foo的依赖包,在编译过程就失败了。demo无法找到
github.com/liujianping/foo。为什么这样?

按照传统的$GOPATH引入包原则,只要在$GOPATH/src存在相应路径的包,就可以完成编译了。从现在的情形就可以解释$GOPATHGo Module功能开启后,对原有引入包的规则发生的改变。

既然,$GOPATH/src路径不再支持。那么如何解决这个无法找到包依赖的问题呢?方法有二:

  • 本地路径
  • 远程仓库

该小节提供本地路径方法。

$: cat $GOPATH/src/github.com/liujianping/demo/go.mod
module github.com/liujianping/demo

目前demo项目的go.mod仅仅一句话,因为无法找github.com/liujianping/foo,所以在go build过程中也不会修改go.mod,增加对包github.com/liujianping/foo的依赖关系。所以,只能是手动处理了。修改go.mod文件如下:

module github.com/liujianping/demo

require github.com/liujianping/foo v0.0.0
replace github.com/liujianping/foo => ../foo

再次执行demo程序:

$: go run main.go
go: finding github.com/liujianping/foo v0.0.0
GoModule, 你好!

对于项目中直接引用本地依赖包的官方文档中有段注意事项:

Note: for direct dependencies, a require directive is needed even when doing a replace. For example, if foo is a direct dependency, you cannot do replace foo => ../foo without a corresponding require for foo. (If you are not sure what version to use in the require directive, you can often use v0.0.0 such as require foo v0.0.0; see #26241).

意思就是,即使是本地依赖包,明确的require仍然是需要的。至于版本号,其实只要符合SemVer规范就可以。可以是v0.0.0,也可以是v0.1.2

Go Module最主要是引入了依赖包的版本控制。所以,我们不妨就本地版本测试一下。

对本地版本foo进行相应的git本地版本控制,增加几个版本,代码中相应的增加版本信息。

package foo

import "fmt"

func Greet(name string) string {
    return fmt.Sprintf("%s, 你好! Version 1.0.0", name)
}

增加了以下三个版本tag。

$: git tag
v0.1.0
v0.2.0
v1.0.0

demo项目中,设置foo版本, go.mod修改如下:

module github.com/liujianping/demo

require github.com/liujianping/foo v0.1.0
replace github.com/liujianping/foo => ../foo

执行demo程序,输出如下:

go run main.go
go: finding github.com/liujianping/foo v0.1.0
GoModule, 你好! Version 1.0.0

不难得出结论:go get是不会从本地仓库获取版本信息的,查看go get在module模式下工具链实现代码也可得出这个结论。

1.3.2 远程仓库

从上节可以大致了解Go Module的原理。现在我们将foo依赖包上传到github.com上,包括相应的版本tag。首先github.com创建相应的项目foo.再将本地仓库上传到远程仓库中。

$: git remote add origin git@github.com:liujianping/foo.git
$: git push -u origin master

上传版本tag信息:

$: git push origin --tags

现在完成了github.com/liujianping/foo依赖包的远程部署。看看具体实操demo项目,首先去掉本地的直接依赖。demo项目的go.mod如下

$: cat $GOPATH/src/github.com/liujianping/demo/go.mod
module github.com/liujianping/demo

重新执行demo项目

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
go: finding github.com/liujianping/foo v1.0.0
go: downloading github.com/liujianping/foo v1.0.0
GoModule, 你好! Version 1.0.0

查看变更后的go.mod,如下

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.0 // indirect

同时demo根目录下,增加了go.sum文件。

cat go.sum
github.com/liujianping/foo v1.0.0 h1:yYoUzvOwC1g+4mXgSEloF187GmEpjKAHEmkApDwvOVQ=
github.com/liujianping/foo v1.0.0/go.mod h1:HKRu+NgbfULQV4mqZOnCXpF9IwlhOOIwmns7gpwjZic=

修改foo版本号到 v0.2.0

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v0.2.0 // indirect

重新执行demo项目

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
go: finding github.com/liujianping/foo v0.2.0
go: downloading github.com/liujianping/foo v0.2.0
GoModule, 你好! Version 0.2.0

再看看go.sum文件发生的变化:

cat go.sum
github.com/liujianping/foo v0.2.0 h1:2JCV7mfUyneSksnWokX0kZoBbtWPoyL8s8iW80WHl/A=
github.com/liujianping/foo v0.2.0/go.mod h1:HKRu+NgbfULQV4mqZOnCXpF9IwlhOOIwmns7gpwjZic=
github.com/liujianping/foo v1.0.0 h1:yYoUzvOwC1g+4mXgSEloF187GmEpjKAHEmkApDwvOVQ=
github.com/liujianping/foo v1.0.0/go.mod h1:HKRu+NgbfULQV4mqZOnCXpF9IwlhOOIwmns7gpwjZic=

通过以上步骤,粗略可以了解针对Go Module对于远程仓库的版本选择。简单解释版本的选择过程下:

  • 检查远程仓库最新的tag版本,有就取得该版本
  • 远程仓库没有tag版本时,直接获取master分支的HEAD版本
  • 如果在go.mod文件中指定了具体版本,go get直接获取该指定版本
  • go.mod中除了可以指定具体版本号以外,还支持分支名

继续对远程版本foo增加新的版本v1.0.1。提交相应代码并推送版本标签v1.0.1到远端。并重新设置demo项目中的go.mod中的依赖版本为v1.0.0.如下:

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.0 // indirect

重新执行demo项目

$: cd $GOPATH/src/github.com/liujianping/demo
$: go run main.go
GoModule, 你好! Version 1.0.0

这次执行没有输出go本身的提示信息,而是直接输出了结果。因为github.com/liujianping/foo v1.0.0已经存在于本地的缓存中了,不妨查看一下。

$: ls $GOPATH/pkg/mod/github.com/liujianping/foo@v1.0.0

虽然就demo项目而言,依赖项目foo有两个v1.0.0v1.0.1两个版本可用。按照GoModule版本选择最小版本的算法,demo项目依旧选择v1.0.0版本。

如何更新依赖包版本

更新依赖包的版本,最简单的方式,直接手动编辑go.mod设置依赖包版本即可。

另外一种方式就是通过go get -u的方式进行自动更新。具体操作步骤如下:

查看依赖包版本更新信息

$: go list -u -m all
go: finding github.com/liujianping/foo v1.0.1
github.com/liujianping/demo
github.com/liujianping/foo v1.0.0 [v1.0.1]

更新依赖包版本

$: go get -u 
go: downloading github.com/liujianping/foo v1.0.1

或者,制定更新patch版本

$: go get -u=patch github.com/liujianping/foo 
go: downloading github.com/liujianping/foo v1.0.1

此时,go.mod文件即被更新

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.1

重新执行程序

$: go run main.go
GoModule, 你好! Version 1.0.1

基于分支

GoModule除了支持基于标签tag的版本控制,可以直接利用远程分支名称进行开发。

所以本节,笔者就模块foo创建一个新的远程分支develop.具体代码,请直接参考github.com/liujianping/foo项目。

修改demo项目的go.mod文件:

module github.com/liujianping/demo

require github.com/liujianping/foo develop

再次执行demo, 结果如下:

$: go run main.go
go: finding github.com/liujianping/foo develop
go: downloading github.com/liujianping/foo v1.0.2-0.20190214080857-9c0018d55446
GoModule, 你好! Branch develop

查看,go.mod文件,发生如下变更:

$: cat go.mod
module github.com/liujianping/demo

require github.com/liujianping/foo v1.0.2-0.20190214080857-9c0018d55446

按官方文档的说明,使用分支名,可以直接拉取该分支的最后一次提交。从实验来看, Go Module一旦发生编译就会针对分支名的依赖进行版本号固定。

1.3.3 私有仓库

对于私有仓库而言,其原理与1.3.2中的远程仓库是类似的。唯一不同之处是,go get取包的过程可能存在种种障碍,导致无法通过go get取到私有仓库包。主要原因可能是:

  • 权限问题
  • 路径问题

导致按照正常的go get过程取包失败。如果了解了go get取包原理,以上问题也就迎刃而解了。

更多文章可直接访问个人BLOG:GitDiG.com

微信公众号:一艘慢船,请扫描二维码,。
qrcode_for_gh_106a756d9c99_258.jpg

相关阅读:

查看原文

赞 17 收藏 14 评论 4

熊D01001 评论了文章 · 2018-08-17

JavaScript ES6功能概述(ECMAScript 6和ES2015 +)

JavaScript在过去几年中发生了很大的变化。这些是您今天可以开始使用的12项新功能!

该语言的新增内容称为ECMAScript 6.它也称为ES6或ES2015 +。

自1995年JavaScript构思以来,它一直在缓慢发展。每隔几年就会发生新增事件。 ECMAScript于1997年成立,旨在指导JavaScript的发展方向。它已经发布了ES3,ES5,ES6等版本。

如您所见,ES3,ES5和ES6之间存在10年和6年的差距。此后每年​​进行小幅增量变更。而不是像ES6那样一次做大规模的改变。

clipboard.png

所有现代浏览器和环境都支持ES6!

clipboard.png

Chrome,MS Edge,Firefox,Safari,Node等等都支持JavaScript ES6的大多数功能。因此,您将在本教程中学习的所有内容都可以立即开始使用。

让我们开始使用ECMAScript 6!

您可以在浏览器控制台上测试所有这些代码段!

块级作用域

使用ES6,我们从使用var声明变量到使用let / const。

var出了什么问题?

var的问题是变量泄漏到其他代码块中,例如for循环或if块。

ES5
var x = 'outer';
function test(inner) {  
  if (inner) {    
    var x = 'inner';// scope whole function    
        return x;
  }  
      return x;// gets redefined because line 4 declaration is hoisted
    }
    test(false);// undefined 😱
    test(true);// inner

对于test(false)你会期望返回outer,但是你却得到undefined。

为什么?

因为即使没有执行if-block,第4行中的表达式“var x”也会被提升。

var是函数作用域。它甚至在被声明之前就可以在整个功能中使用。

声明已被挂载。因此,您可以在声明变量之前使用它。

初始化不会被提升。如果您使用var ,那么总会将变量声明在顶部。

在应用挂载规则后,我们可以更好地了解发生的情况:

ES5
var x = 'outer';
function test(inner) {  
  var x;// HOISTED DECLARATION  
  if (inner) {    
    x = 'inner';// INITIALIZATION NOT HOISTED    
    return x;
  }  
  return x;
}
ES6
let x = 'outer';
function test(inner) {  
  if (inner) {    
      let x = 'inner';
    return x;
  }  
      return x;// gets result from line 1 as expected
}
test(false);// outer
test(true);// inner

用let代替var会使事情按预期工作。如果未调用if块,则变量x不会从块中提升。

hoisting和 “temporal dead zone(暂时性死区)”

在ES6中,let将变量提升到块的顶部(不是像ES5那样位于函数的顶部)。

但是,在变量声明之前引用块中的变量会导致“ReferenceError(系统报错)”。

let被限制为块级作用域。在声明之前不能使用它。

“Temporal dead zone” 是从块开始到声明变量的区域。

IIFE(立即执行函数)

让我们在解释IIFE之前展示一个例子。 看看这里:

ES5
{  
  var private = 1;
}
console.log(private);// 1

如你所见,private漏掉了。 您需要使用IIFE(立即调用的函数表达式)来包含它:

ES5
(function(){  
  var private2 = 1;})();
console.log(private2);// Uncaught ReferenceError

如果你看过jQuery / lodash或其他开源项目,你会发现他们有IIFE来避免污染全局环境,只是在全局定义,如_,$或jQuery。

在ES6上更清洁,当我们只使用块和let时,我们也不需要再使用IIFE:

ES6
{  
  let private3 = 1;
}
console.log(private3);// Uncaught ReferenceError

Const

如果你根本不想改变变量,你也可以使用const。

用'let和const代替var`。

对所有引用使用const;避免使用var。

如果必须重新分配引用,请使用let而不是const。

模板文字(字符串拼接)

当我们有模板文字时,我们不需要做更多的嵌套连接。看一看:

ES5
var first = 'Adrian';
var last = 'Mejia';
console.log('Your name is ' + first + ' ' + last + '.');

现在你可以使用反引号和字符串插值$ {}:

const first = 'Adrian';
const last = 'Mejia';
console.log(`Your name is ${first} ${last}.`);

多行字符串

我们不必再连接字符串+ n了:

var template = '<li *ngFor="let todo of todos" [ngClass]="{completed: todo.isDone}" >\n' +
'  <div class="view">\n' +
'    <input class="toggle" type="checkbox" [checked]="todo.isDone">\n' +
'    <label></label>\n' +
'    <button class="destroy"></button>\n' +
'  </div>\n' +
'  <input class="edit" value="">\n' +
'</li>';
console.log(template);

在ES6上我们可以再次使用反引号来解决这个问题:

const template = `<li *ngFor="let todo of todos" [ngClass]="{completed: todo.isDone}" >
  <div class="view">
    <input class="toggle" type="checkbox" [checked]="todo.isDone">
    <label></label>
    <button class="destroy"></button>
  </div>
  <input class="edit" value="">
</li>`;
console.log(template);

两段代码都会有完全相同的结果。

解构分配

ES6解构非常有用和简洁。请遵循以下示例:

从数组中获取元素

ES5
var array = [1, 2, 3, 4];
var first = array[0];
var third = array[2];
console.log(first, third);// 1 3

es6的写法:

ES6
const array = [1, 2, 3, 4];
const [first,,third] = array;
console.log(first, third);// 1 3

交换 values

ES5
var a = 1;
var b = 2;
var tmp = a;
a = b;
b = tmp;
console.log(a, b);// 2 1

es6的写法:

ES6
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b);// 2 1

多次返回值的解构

ES5
function margin() {  
  var left=1, right=2, top=3, bottom=4;
  return { 
      left: left, right: right, top: top, bottom: bottom };
}
var data = margin();
var left = data.left;
var bottom = data.bottom;
console.log(left, bottom);// 1 4

在第3行中,您还可以将其返回到这样的数组中(并保存一些输入):

return [left, right, top, bottom];
但是,调用者需要考虑返回数据的顺序。

var left = data[0];
var bottom = data[3];

使用ES6,调用者只选择他们需要的数据(第6行):

ES6
function margin() {  
  const left=1, right=2, top=3, bottom=4;
  return { left, right, top, bottom };
}
const { left, bottom } = margin();
console.log(left, bottom);// 1 4
注意:第3行,我们还有其他一些ES6功能正在进行中。我们可以将{left:left}压缩为{left}。看看它与ES5版本相比有多简洁。那不是很酷吗?

参数匹配的解构

ES5
var user = {
  firstName: 'Adrian', lastName: 'Mejia'
};
function getFullName(user) {  
  var firstName = user.firstName;
  var lastName = user.lastName;
  return firstName + ' ' + lastName;
}
console.log(getFullName(user));// Adrian Mejia

es6(但更简洁)相同:

ES6
const user = {
  firstName: 'Adrian', lastName: 'Mejia'
};
function getFullName({ 
  firstName, lastName 
}) {  return ${firstName} ${lastName};}
console.log(getFullName(user));// Adrian Mejia

深拷贝

ES5
function settings() {  
  return { display: { color: 'red' }, keyboard: { layout: 'querty'} };}
var tmp = settings();
var displayColor = tmp.display.color;var keyboardLayout = tmp.keyboard.layout;
console.log(displayColor, keyboardLayout);// red querty

与es6(但更简洁)相同:

ES6
function settings() {  
  return { display: { color: 'red' }, keyboard: { layout: 'querty'} };
}
const { 
  display: { color: displayColor }, keyboard: { layout: keyboardLayout }
} = settings();
console.log(displayColor, keyboardLayout);// red querty

这也称为对象解构。

如您所见,这非常有用,并鼓励良好的编码风格。

最佳做法:

  • 使用数组解构来获取元素或交换变量。它可以帮助您避免创建临时引用。
  • 不要对多个返回值使用数组解构,而是使用对象解构

类和对象

使用ECMAScript 6,我们从“构造函数”到“类。

在JavaScript中,每个对象都有一个原型,这是另一个对象。所有JavaScript对象都从其原型继承其方法和属性。

在ES5中,我们使用构造函数来创建面向对象编程(OOP),以创建对象,如下所示:

ES5
var Animal = (function () {  
  function MyConstructor(name) {    this.name = name;
}  
MyConstructor.prototype.speak = function speak() {    console.log(this.name + ' makes a noise.');
};
return MyConstructor;})();
var animal = new Animal('animal');
animal.speak();// animal makes a noise.

在ES6中,我们有一些语法糖。我们可以用更少的样板和新的关键字来做同样的事情,比如class和constructor。另外,请注意我们如何定义方法

constructor.prototype.speak = function()vsspeed():
ES6
class Animal {  
  constructor(name) {    this.name = name;}  
    speak() {    console.log(this.name + ' makes a noise.');
 }
}
const animal = new Animal('animal');
animal.speak();// animal makes a noise.

正如我们所看到的,两种风格(ES5 / 6)在幕后产生相同的结果,并以相同的方式使用。

最佳做法:

  • 始终使用class语法并避免直接操作prototype。为什么?因为它使代码更简洁,更容易理解。
  • 避免使用空构造函数。如果未指定,则类具有默认构造函数。

继承

建立在之前的Animal类上。假设我们想要扩展它并定义一个“Lion”类

在ES5中,它更多地涉及原型继承。

ES5
var Lion = (function () {  
  function MyConstructor(name){    
      Animal.call(this, name);
}  
// prototypal inheritance  
MyConstructor.prototype = Object.create(Animal.prototype);
MyConstructor.prototype.constructor = Animal;
MyConstructor.prototype.speak = function speak() {    
  Animal.prototype.speak.call(this);
  console.log(this.name + ' roars 🦁');
};
return MyConstructor;})();
var lion = new Lion('Simba');lion.speak();// Simba makes a noise.
// Simba roars.

我不会详细介绍所有细节,但请注意:

  • 第3行,我们用参数显式调用Animal构造函数。
  • 第7-8行,我们将Lion原型分配给Animal的原型。
  • 第11行,我们从父类Animal中调用speak方法。

在ES6中,我们有一个新的关键字extends和super![superman shield](undefined)。

class Lion extends Animal {  
  speak() {    super.speak();
  console.log(this.name + ' roars 🦁');
}}
const lion = new Lion('Simba');
lion.speak();// Simba makes a noise.
// Simba roars.

看起来这个ES6代码与ES5相比看起来有多清晰,它们完全相同。

最佳做法:

使用内置的方式继承extends。

Promises

我们从回调地狱👹逃出来了。

ES5
function printAfterTimeout(string, timeout, done){  
  setTimeout(function(){    
      done(string);
}, timeout);}
printAfterTimeout('Hello ', 2e3, function(result){  
  console.log(result);// nested callback  
  printAfterTimeout(result + 'Reader', 2e3, function(result){    
    console.log(result);
 });
});

我们有一个函数接收回调,当done时执行。我们必须一个接一个地执行它。这就是我们在回调中第二次调用'printAfterTimeout`的原因。

如果您需要第3次或第4次回调,这可能会很快变得混乱。让我们看看我们如何通过Promises来做到这一点:

ES6
function printAfterTimeout(string, timeout){  
  return new Promise((resolve, reject) => {    
      setTimeout(function(){      
      resolve(string);
}, timeout);
});}
printAfterTimeout('Hello ', 2e3).then((result) => {  
  console.log(result);
  return printAfterTimeout(result + 'Reader', 2e3);
}).then((result) => {  
  console.log(result);
});

正如你所看到的,使用promises,我们可以使用then在另一个函数完成后执行某些操作。 不再需要保持嵌套功能。

箭头函数

ES6没有删除函数表达式,但它添加了一个名为箭头函数的新表达式。

在ES5中,我们对this有一些疑问:

ES5
var _this = this;// need to hold a reference
$('.btn').click(function(event){  
  _this.sendData();// reference outer this
});
$('.input').on('change',function(event){  
  this.sendData();// reference outer this
}.bind(this));// bind to outer this

你需要使用一个临时的this来引用一个函数或使用bind。在ES6中,您可以使用箭头函数来实现这个功能!

ES6
// this will reference the outer one
$('.btn').click((event) =>  this.sendData());
// implicit returns
const ids = [291, 288, 984];
const messages = ids.map(value => `ID is ${value}`);

For…of

我们从for转到forEach然后转到for ... of:

ES5
// for
var array = ['a', 'b', 'c', 'd'];
for (var i = 0;i < array.length;i++) {  
  var element = array[i];
  console.log(element);
}

// forEach
array.forEach(
  function (element) {  
    console.log(element);
});

ES6 for ...也允许我们进行迭代。

ES6
// for ...of
const array = ['a', 'b', 'c', 'd'];
for (const element of array) {    
 console.log(element); // a,b,c,d
}

默认参数

我们检查是否定义了变量以将值赋给“默认参数”。你之前做过这样的事吗?

ES5
function point(x, y, isFlag){
  x = x || 0;
  y = y || -1;
  isFlag = isFlag || true;
  console.log(x,y, isFlag);
}
point(0, 0) // 0 -1 true 😱
point(0, 0, false) // 0 -1 true 😱😱
point(1) // 1 -1 true
point() // 0 -1 true

检查的常见模式是变量具有值或指定默认值。然而,请注意有一些问题:

第8行,我们传递0,0并得到'0,-1`

第9行,我们传递false但得到'true`。

如果您将布尔值作为默认参数或将值设置为零,则它不起作用。你知道为什么吗???我会在ES6例子后告诉你;)

使用ES6,现在您可以用更少的代码做得更好!

ES6
function point(x = 0, y = -1, isFlag = true){
  console.log(x,y, isFlag);
}
point(0, 0) // 0 0 true
point(0, 0, false) // 0 0 false
point(1) // 1 -1 true
point() // 0 -1 true

注意第5行和第6行我们得到了预期的结果。ES5示例不起作用。我们必须首先检查undefined,因为false,null,undefined和0是假值。我们可以逃脱数字:

ES5
function point(x, y, isFlag){
  x = x || 0;
  y = typeof(y) === 'undefined' ? -1 : y;
  isFlag = typeof(isFlag) === 'undefined' ? true : isFlag;
  console.log(x,y, isFlag);
}
point(0, 0) // 0 0 true
point(0, 0, false) // 0 0 false
point(1) // 1 -1 true
point() // 0 -1 true

现在,当我们检查undefined时,它按预期工作。

Rest parameters

在ES5上,获取任意数量的参数是成熟的:

ES5
function printf(format) {
  var params = [].slice.call(arguments, 1);
  console.log('params: ', params);
  console.log('format: ', format);
}

我们可以使用rest运算符...来做同样的事情。

ES6
function printf(format, ...params) {
  console.log('params: ', params);
  console.log('format: ', format);
}

扩展运算符

我们从apply()转到了扩展运算符。

提醒:我们使用apply()将数组转换为参数列表。例如,Math.max()接受一个参数列表,但是如果我们有一个数组,我们可以使用apply来使它工作。

正如我们在前面所看到的,我们可以使用apply来传递数组作为参数列表:

ES5
Math.max.apply(Math, [2,100,1,6,43]) // 100

在ES6中,您可以使用扩展运算符:

ES6
Math.max(...[2,100,1,6,43]) // 100

另外,我们从concat数组开始使用spread(展开)运算符:

ES5
var array1 = [2,100,1,6,43];
var array2 = ['a', 'b', 'c', 'd'];
var array3 = [false, true, null, undefined];
console.log(array1.concat(array2, array3));

在ES6中,您可以使用spread运算符展平嵌套数组:

ES6
const array1 = [2,100,1,6,43];
const array2 = ['a', 'b', 'c', 'd'];
const array3 = [false, true, null, undefined];
console.log([...array1, ...array2, ...array3]);

JavaScript经历了很多变化。本文介绍了每个JavaScript开发人员应该了解的大多数核心功能。此外,我们还介绍了一些最佳实践,以使您的代码更简洁,更容易推理。

查看原文

熊D01001 发布了文章 · 2018-03-10

[笔记] 当在 Vue 中不得不用全局样式时...

有时候基于 Vue 的单文件组件开发项目时, 不得不使用全局样式, 这时有一些需要注意的地方.

当遇到需要使用全局样式时, 下列几种情况

  • 样式在项目各处均有使用;
  • 样式只在当前组件内的 DOM 上使用;
  • 样式需要应用到当前组件 DOM 的外部 DOM;

下面详细记录一下需要注意的问题:

样式在项目各处均有使用

如果样式需要在项目各处均有使用, 例如: reset.css, tiny-trim.css 等等.
这时推荐在项目入口文件中直接导入样式文件:

// src/main.js
import 'tiny-trim.css'
import 'asset/reset.css'
import 'asset/global.css'

当然, 也可以在顶层组件中没有设置 scoped 属性的 style 标签中导入:

@import url(asset/reset.css);
@import url(asset/global.css);

样式只在当前组件内的 DOM 上使用

当使用一些第三方 UI 库时, 有一些 UI 库生成的 DOM 在 template 中并不能直接添加 classstyle 来修改第三方 UI 库的组件样式, 这时我们可以通过当前组件没有 scoped 属性的 style 标签来添加全局样式.

但此时需要考虑一些问题:

  • 这个样式应该只影响当前组件内第三方 UI 库渲染出来的 DOM
  • 因为 DOM 不在 template 标签里 (DOM 由第三方 UI 库的 JS 在浏览器加载时构建或在编译打包过程中生成), 不能直接设置 classstyle 来修改样式, 故只能使用没有 scoped 属性的 style 标签

可以看出两点是有一定矛盾的. 不过可以采用如下方法解决或缓解:

  1. 为当前组件根元素设置自定义 data 属性

    <!-- src/components/MyComponent.vue -->
    <template>
      <div class="my-component" data-custom-mycomponent>
        <!-- ... -->
      </div>
    </tempalte>
  2. 在没有 scoped 属性的 style 标签中使用自定义 data 属性限定样式作用域

      .my-component[data-custom-mycomponent] {
        // ...
      }
    这里推荐使用 LessSass, 嵌套语法能减少许多代码冗余.

样式需要应用到当前组件 DOM 的外部 DOM

这种情况主要是针对上一种情况的补充, 有时候第三方 UI 库生成的 DOM 节点并不在当前组件的 DOM 内, 可能渲染到 body 中 (如 dialog, tooltip, message 等).

这些渲染到当前组件 DOM 之外的组件, 需要在上一种情况的处理基础上, 为它们的顶层元素再设置一个自定义的 data 属性:

  <!-- src/components/MyComponent.vue -->
  <template>
    <div class="my-component" data-custom-mycomponent>
      <!-- message 组件的 DOM 将被渲染到 body 中, 而不是当前组件 .my-component 中 -->
      <message
        class="my-component-message"
        msg="You got a message! "
        data-custom-mycomponent-message />
    </div>
  </tempalte>
  .my-component[data-custom-mycomponent] {
    // ...
  }
  .my-component-message[data-custom-mycomponent-message] {
    // ...
  }

-EOF

原文: [笔记] 当在 Vue 中不得不用全局样式时...
查看原文

赞 2 收藏 3 评论 0

熊D01001 赞了回答 · 2018-02-08

iframe跨域问题

文档看的不仔细呀……postMessage传送的这个message,是要能被The structured clone algorithm序列化的,你这直接把window怼进去了可不得报错么……
(手机发不了图,直接顺着原文找The structured clone algorithm点进去看看吧)

另外一个,有一个东西你得理解下,不管是BOM还是DOM,它们只是一套接口,面向被浏览器引擎渲染后的页面结构,就好像一扇门,装在墙上就能进屋,但拆下来运走就进不去这个屋了。

关注 5 回答 5

熊D01001 发布了文章 · 2018-01-12

深度定制团队自己的 Vue template

众所周知,使用 vue-cli 可以快速初始化一个基于 Vue.js 的项目,官方提供了 webpackpwabrowserify-simple常用 templates

当开发一个独立项目的时候,使用官方提供的 template 确实非常方便,省去了繁琐的依赖安装配置、webpack 配置,甚至连项目结构也不用多加考虑。

但是,当我们需要开发多个系统,每个系统相对独立但又有一些配置、依赖或逻辑相互通用的时候(例如集群的多后台系统),每次使用官方提供的 template 初始化项目之后,都需要进一步调整(添加依赖、修改配置、增加通用组件等等),这显然是十分麻烦的。
本着懒惰是第一生产力的初衷,我们需要定制一份自己的 template,以便我们...额...偷懒哈~
hehe

在开始定制我们自己的 Vue template 前,我们需要了解一些前置知识:

前置知识

  1. 模板结构
      首先我们先来了解模板的主要结构,模板结构很简单,主要包括两个部分:

    • template 该目录用于存放模板文件,初始化项目生成的文件来自于此。
    • meta.js / meta.json 用于描述初始化项目时命令行的交互动作。
  2. Metalsmith
      Metalsmith 在渲染项目文件流程中角色相当于 gulp.js,可以通过添加一些插件对构建文件进行处理,如重命名、合并等。
  3. download-git-repo
      使用 vue-cli 初始化项目时会使用该工具来下载目标仓库。默认的 webpack 等模板直接下载 vue-templates 中对应的模板仓库。
      自定义的模板也可以是一个 GitHub 仓库,使用如下命令来初始化项目:

    vue init username/repo my-project
    其中 username 为自定义模板仓库所在的 GitHub 用户或组织名,repo 为仓库名。
  4. Inquirer.js
      vue-cli 在模板仓库下载完成后,将通过 Inquirer.js 根据模板仓库中的 meta.jsmeta.json 文件中的设置,与用户进行一些简单的交互以确定项目的一些细节,如下图:
    cli-perview

    该交互配置是可选的,当项目中没有meta.jsmeta.json 文件时,模板仓库下载完成后将直接进入模板构建阶段。
  5. Handlebars.js
      在通过命令行交互确定了项目初始化的细节后,就该进入最后一道工序,按照模板初始化我们的项目啦!\(≧▽≦)/
      这里 vue-cli 选用的是 Handlebars.js —— 一个简单高效的语义化模板构建引擎。

  画了一张图,更有助于理清这些依赖在 vue-cli 初始化项目时的相互关联:
vue-cli-process

定制模板主要围绕着命令行交互Inquirer.js)与模板文件开发Handlebars.js)这两部分。

meta.js 配置文件(Inquirer.js)

  由于 meta.js 相当于模板项目的配置文件(虽然非必选),所以这里先看看它主要能干些啥。
  设置都在 meta.jsmeta.json 中配置,推荐使用 meta.js,更灵活一些。以下也将以 meta.js 进行展开说明。
  meta.js 一共可包含如下几个字段,简单列一下各字段功能:

  • helpers : 自定义 Handlebars.js 的辅助函数
  • prompts : 基于 Inquirer.js 的命令行交互配置
  • filters : 根据命令行交互的结果过滤将要渲染的项目文件
  • metalsmith : 配置 Metalsmith 插件,文件会像 gulp.js 中的 pipe 一样依次经过各个插件处理
  • completeMessage : 将模板渲染为项目后,输出一些提示信息,取值为字符串
  • complete : 与 completeMessage 功能相同,二选其一,取值为函数,函数最后需返回输出的字符串

命令行交互(Inquirer.js)

  命令行交互主要是 meta.jsprompts 字段的配置,详细的配置可以阅读 Inquirer.jsREADME.md,这里说一下常用的交互配置:

// meta.js
module.export = {
  // ...
  "prompts": {
    "isCustomName": {
      "type"   : "confirm",
      "message": "是否自定义系统名称?",
    },
    "sysName": {
      "type"    : "input",
      "when"    : "isCustomName",
      "default" : "默认系统名称",
      "message" : "请输入系统名称:",
      "required": true,
      "validate": function (val) {
        if (!val) return '(✘) 请输入系统名称,该名称将设为 index.html 的 title';
        return true;
      },
    },
    // ...
  },
}

字段说明:

  • isCustomNamesysName : 交互字段名称,可在后续条件交互或模板渲染时通过该字段读取到交互结果
  • type : 交互类型,有 input, confirm, list, rawlist, expand, checkbox, password, editor 八种类型
  • message : 交互的提示信息
  • when : 进行该条件交互的先决条件,在该例子中,sysName 这个交互动作只在 isCustomName 交互结果为真时才会出现
  • default : 默认值,当用户输入为空时,交互结果即为此值
  • required : 默认为 false,该值是否为必填项
  • validate : 输入验证函数
注:示例中 defaultrequiredvalidate 三个字段存在逻辑问题,仅为举例方便放到一起。

模板基本语法(Handlebars.js)

  在模板编写中,我们可以用 Mustache 语法在任何文本类型的文件中输出在命令行交互中得到的一些数据:

// dev.js
export default {
  //...
  token: '{{token}}',
  //...
};
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>{{sysName}}</title>
  </head>
  <!-- ... -->
</html>

  以 {%raw%}{{xxx}}{%endraw%} 即为一个 Mustache 句法标记。以上例子中 tokensysName 为匹配命令行交互数据对应的键名。

vue 有过了解的都知道,在模板标签中直接输出实例上的数据也是用的 Mustache 语法。
如果定制 vue template 模板时不对这些数据做相应处理,在最终输出由模板初始化的项目时,这些与命令行交互得到的数据无法匹配的 Mustache 句法标记会被移除。
此时我们需要使用反斜杠 \{%raw%}{{xxx}}{%endraw%} 或者 {%raw%}{{{xxx}}}{%endraw%} 来跳过 Handlebars 的处理,直接输出 {%raw%}{{xxx}}{%endraw%}

模板渲染时的辅助函数(Handlebars.js)

  vue-cli 中为 Handlebars 预置了 if_equnless_eq 辅助函数,用于使用交互所得数据来处理模板中是否渲染的两种逻辑关系,此外 Handlebars 中还内置了 ifunlesseach辅助函数

// sys.js
export default {
  {{#if_eq projType 'admin'}}
  id: {{#if_eq sysId ''}}undefined{{else}}{{sysId}}{{/if_eq}},
  {{/if_eq}}
  name: '{{sysName}}',
};

  如上,这里用了 if_eq 辅助函数,projType 代表将要匹配的键,'admin' 代表将要匹配的值。这个键值来自于在命令行界面与用户交互的操作结果。该栗子中,当命令行交互数据中 CLI[projType] == 'admin' 时,将在 sys.js 文件的导出数据中输出 id 字段;id 的值来自一个嵌套的 if_eq 辅助函数,当 CLI[sysId] == '' 时,id 将被设置为 undefined 否则 ({%raw%}{{else}}{%endraw%})输出 CLI[sysId] 命令行交互所得数据中的 sysId

辅助函数使用语法: {%raw%}{{{%endraw%}# + 函数名 + ' '(空格)+ 以空格分隔的参数列表 + }}
以空格分隔的参数列表:未用引号包裹的参数名将被将为自动取值为命令行交互结果中对应的数据

自定义辅助函数(Handlebars.js)

  有时候现有的辅助函数可能不能满足我们的需求,通过 mate.js 中的 helpers 字段我们可以自定义辅助函数:

// mate.js
module.exports = {
  "helpers": {
    "neither": function (k, v1, v2, options) {
      if (k !== v1 && k !== v2) {
        return options.fn(this);
      }
      return options.inverse(this);
    },
  },
  //...
}

  辅助函数可以接受若干的参数,最后一个参数 options 为辅助函数的钩子,调用 options.fn(this) 即输出该辅助函数运算结果为真时的内容,反之调用 options.inverse(this) 则输出 {%raw%}{{else}}{%endraw%} 的内容(如果有的话)。
  现在我们可以在模板中直接使用 neither 辅助函数了:

{{#neigher sysType 'admin' 'mobile'}}
isAdmin  = false
isMobile = false
{{else}}
isAdminOrMobile = true
{{/neigher}}

按条件过滤渲染文件

  辅助函数只可以控制文件内一部分内容的输出与否,有时候我们需要根据交互结果控制某些文件本身是否输出。
  在 mate.js 中的 filters 字段中进行相应的设置,就可以达到控制文件输出的效果:

module.exports = {
  //...
  "filters": {
    "project/config/test.env.js": "unit || e2e",
    "project/src/router/**/*": "router"
  },
  //...
}

  filters键名是要控制输出的文件的路径,可使用字面量,也可使用 简化的 glob 表达式。键名对应的值为命令行交互中得到的数据。

渲染时文件的操作

  在模板项目比较复杂或是有特殊需求的时候,比如:

  • 按照条件不同需要渲染两个文件名相同但内容完全不同的文件
  • 模板模块化,多个模板文件拼接渲染为一个项目文件
  • 使用 GZip 压缩一些非源码资源

  可以通过 mate.js 中的 metalsmith 字段配置相关插件来实现丰富的文件操作:

var renamer = require('metalsmith-renamer')
module.exports = {
  //...
  "metalsmith": function(metalsmith, opts, helpers) {
    metalsmith.use(renamer({
      index: {
        pattern: 'project/**/+(Mobile|Admin)Index.vue',
        rename: function(fileName) {
          return 'Index.vue';
        }
      },
      config: {
        pattern: 'project/src/+(mobile|admin)Config.js',
        rename: 'config.js'
      },
      //...
    }))
  },
  //...
}

  以上是 metalsmith-renamer 插件的简单使用,更多插件可以在这里查找

使用 metalsmith 插件请注意:由于 vue-cli 在下载完成模板仓库后并没有 npm install 安装模板的项目依赖这一操作,所以在打包模板仓库的时候也需要将依赖目录 node_modules 一同打包,metalsmith 的插件都很精简,一般不会有什么嵌套依赖。不过还是建议在使用前查看一下插件的相关 Github 仓库。

  关于 vue 项目模板的开发涉及到的问题差不多就介绍完了,为自己或团队开发一份专属的 Vue Template 吧!

原文: 深度定制团队自己的 Vue template
查看原文

赞 17 收藏 38 评论 1

熊D01001 赞了文章 · 2017-12-21

为何 ES Module 如此姗姗来迟

说明:本文发布之后,此问题的推进峰回路转,不停有新内容。文末新增一节 Updates,跟进本文发布之后的 ES Module 标准化进展情况。

浏览器大战多年了热度依旧高涨,大家终于在 JS 新特性的部署上达成一致纷纷追赶最新标准,然而 ES2015 中的 ES Module 这个万众期待的重要特性却始终迟迟未能实现。

等 2020 年回望历史,倘若我们错过了 ES Module 这艘船而 Node.js 死在汪洋大海之中,没有任何其他技术问题的重要性可以与此相比。
-- issac

Module 的规范是完工了的,只是对于模块如何加载和解析留给了“实现环境决定”——按历史经验,问题往往就出现在这一环。当然了不是烫手山芋 W3C 也不会就这么轻松甩开对吧,事实上这也不是 W3C 一家的事情,牵涉到 TC39、Node 技术委员会、Node 和前端两个开发社群,以及 npm 公司。

故事很长,我们从头说起。importexport 的语法规范很明确,模块的解析器 V8 早已实现,万事俱备只欠加载。区区加载能有多麻烦?

Module 的特性

在新规范下,JavaScript 程序划分成两种类型:脚本(我们以前写的传统JS)和模块(ES规范中新定义的 Module),模块有四项于脚本不同的特性:

  1. 强制严格模式(无法取消)
  2. 执行环境在一个非全局的作用域中
  3. 可以使用 import 导入其他 Module 的 binding
  4. 可以使用 export 导出本 Module 的 binding

看上去规则简单明白,但是要让一个解析器(parser)区分兼容这两种模式还挺复杂的。

解析器的难题

看看代码中是否包含 importexport 关键字不就可以判断它的类型了么?

不行。首先猜测用户意图是个危险行为,如果你猜对了,就更加掩盖了猜错可能会造成的风险。

而严格模式,除了运行时的一些要求之外还定义了几个语法错误:

  1. 使用 with 关键字;
  2. 使用八进制字面量(如 010);
  3. 函数参数重名;
  4. 对象属性重名(仅在 ES5 环境。ES6 取消了此错误);
  5. 使用 implementsinterfaceletpackageprivateprotectedpublicstaticyield 作为标识符。

这些语法错误需要在解析时就抛出来。所以如果以脚本模式解析到了文件末尾才发现有 export,就得从头重新解析整个文件来捕捉上述语法错误。

那我们换一条路,开始先假定为模块进行解析代码。既然 Module 语法相当于严格模式 + 导入导出 (importexport),我们可以用脚本模式 + 导入导出的语法来解析整个文件。然而这种解析规则已经超越了规范定义,这么扭曲的路线可以预见它成为 Bug 源泉的样子。

危险但不是不可能。OK 真正的麻烦来了:按照规范 importexport 都是可选的——你可以写一个 Module,既不导入也不导出任何东西,它只是对全局作用域做些小动作,比如这样:

// 一个合法的 Module
window.addEventListener("load", function() {
    console.log("Window is loaded");
});
// WAT!

总的来说,包含 importexport 表明它一定是个 Module,但没有这两个关键字却不能证明它不是 Module。 ╮(╯_╰)╭

区分 JavaScript 文件类型的任务没法放在解析器里自动完成,我们需要在解析文件之前就知道它的类型。

浏览器的办法

这就是为什么浏览器的模块引用是这个写法:

<script type="module" data-original="foo.js"></script>

当浏览器开始加载这个 foo.js,它会边加载边解析,碰到 import { bar } from './bar.js' 的第一时间开始加载依赖的 bar.js,加载完之后对其解析,检查其中是否导出了 bar。如此往复完成整个 Module 的解析。

Node.js 呢

到了 Node.js,新的问题来了。

作为世界上最大的软件包仓库,npm 中现有的软件包都是 CommonJS 规范。ES Module 需要能够与 CommonJS 模块共存,允许开发者们逐步转向新的语法。

所谓的共存,主要是指 import { foobar } from 'foobar' 语法要支持 CJS Module 和 ES Module 两种包格式——如果 import 只能用来导入 ES Module 而 require 可以导入任意模块,那么所有人都会用 require;如果 importrequire 各自负责导入各自的格式,那么开发者就需要知道所有依赖的库的格式,使用相应语法来导入它,并且在依赖的库们更换到新格式的时候修改自己的代码去兼容……在可预见的 CommonJS -> ES Module 漫长过渡期里这样的负担对社区而言不可接受。

为此社区提出了不少方案,(好消息)经过大量的讨论之后现在已经集中到两个选择还在讨论:

  1. 解析器自动检测。最大的好处是对用户而言透明,可惜原因如前所述,此方案已否定。
  2. 使用"use module"标注。一想到 JS 的未来永远都要在文件开头贴这么个膏药大家就不能忍了。否定。
  3. 新的文件后缀.jsm。主要问题是现有社区工具链全部需要更新才能支持,另外和浏览器实现的统一也要考虑。
  4. package.json上发挥。这个门类下的提议就更多了,比如添加一个 module 字段逐步替代掉 main
{
    // ...
    "module": "lib/index.js",
    "main": "old/index.js",
    // ...
}

这个方案只适用单入口的情况,对多文件(比如 require('foo/bar.js')的场景)就不行了。那就改成 modules 字段(复杂度陡升):

{
    // ...
    // files:
    "modules": ["lib/hello.js", "bin/hello.js"],

    // directories:
    "modules": ["lib", "bin"],

    // files and directories:
    "modules": ["lib", "bin", "special.js"],

    // if package never uses CJS Modules
    "modules": ["."],
}

这还没完,更多方案就不详述了,大家可以到 Node.js Wiki 上查看。

就个人偏好而言,尽管所有的方案都有利有弊,而 package.json 这条路为了兼容各种需求,修改版的提案已经越来越复杂,比较起来 .jsm 后缀倒是愈发显得简单清晰了。我更喜欢这个干净的解决方案。

现在的进展(2016.04.15)

<script type="module" /> 已经加入 HTML 规范,WhatWG 刚刚发了一篇文章讲述他们如何经过艰苦卓绝的努力达成这一目标,接下来就看浏览器厂商实现了。

除此之外 WhatWG 手上还有一个 ES Module loader 规范,用于指定 Module 的动态加载方式。它曾经是 ES6 草案的一部分,但因为 ES2015 “要赶着发布来不及了”不幸被砍,目前归属 WhatWG推进

Node.js 这边,在相当一段时间里我们还要借助 transpiler 来体验 ES Module。这件事需要 V8、Node.js、WhatWG 共同协调完成。

按计划本月 Node.js 发布 6.0,顺利的话可以 确定集成 V8 5.0(BTW,一天后 V8 发布了 5.1),对 ES2015 的特性支持达到 93%——看来 ES Module 很可能会成为 “The last ES2015 feature” 了。

关注 ES Module 的进展,还可以看看几个地方:

  1. Node 社区提案和讨论:https://github.com/nodejs/nod...
  2. V8 的实现:https://bugs.chromium.org/p/v...
  3. Blink 的实现:https://bugs.chromium.org/p/c...

愿 ES Module 早日到来。

Updates

关于 ES Module 在 Node.js 环境下的识别方案,从一月份 bmeck 提出提案开始社区就持续沟通和争论,以下是相关进展更新。

  • 2016.01.08
    bmeck 提出关于 ES Module 的提案(增加新后缀.mjs),社区讨论开始。
  • 2016.02.06
    社区提的方案归纳起来,有四个方向
  • 2016.04.15
    本文发布的日子。
  • 2016.04.20
    经过两个月的密集讨论,四个方向只剩下两个存活:.mjs 派和 package.json 派,然而这两派的争论非常激烈。
  • 2016.04.27
    鉴于 .mjs 已经在正式提案中,倘若讨论持续僵持不下,不出意外 .mjs 将会随着时间推移而正式成为规范。怀着这样的危机感,package.json 派发起了 In defense of dot js 来抗衡 .mjs 的提案,要求保持 .js 后缀不变而使用 package.json 来识别 ES Module。
  • 2016.06.14
    重大转折!bmeck 提出一个新的方案 UnambiguousJavaScriptGrammar:既然两边的纠结都是因为无法从文件本身识别 ES Module 而起,不妨调整一点语法细节(ES Module 中的 exports 语句不再是可选的,至少有一句 exports {} 来表明该文件是个 ES Module),两派的争论就这么迎刃而解了!
  • 2016.07.06
    经过 Node.js TSC 的讨论,Unambiguous JavaScript Grammar 方案正式加入提议(proposal)
  • 2016.07.07
    虽然 Unambiguous JavaScript Grammar 加入了 Node.js 的草案提案(5.1章),但是考虑到距离 TC39 的七月会议只剩下一周时间,而 Node.js 这边希望做更充分的调研和测试再进行讨论,所以从这次 TC39 的议程中拿掉了
  • 2016.09.06
    Domenic 提了 import() 作为动态加载的方案,有望取代 System.import()System.loader.import()
  • 2016.09.17
    ES Module 再次提上 TC39 的议事日程,相关的还有内建模块import()
  • 2016.09.30
    TC39 9月碰头会的与会者纷纷表示这次会议进展令人愉快,会议内容汇总在此,以及一些补充

    • Node.js 开发者想要提出一些修改规范的建议,也不知道合适不合适,沟通之后发现 TC39 是非常关心和在意每个社区的需求的(大家相谈甚欢)。
    • 原本的 ES 规范要求模块加载过程需要先完成静态 parse 然后再 evaluate,但是现在的 Node.js CommonJS 模块无法满足这个要求(CJS 模块必须 evaluate 之后才知道 exports 的是什么)。讨论下来规范将会改为允许 parse 过程在碰到 import CJS 模块时进入一个挂起的状态,等待依赖树中的 CJS 模块 evaluate 之后再完成 parse。
    • 对模块类型的检测目前是三个方案选项:

      1. Unambiguous JavaScript Grammar 看上去比较简单,但实现起来还是有不少坑;
      2. package.json 的办法比较累赘,局限也多;
      3. .mjs 的方案最简单,看来是最可行的,而且也跟 Node.js 现有方式一致(用后缀 .node.json.js来区分加载类型)。除非 Unambiguous JavaScript Grammar 的实现问题都解决掉,否则最终方案就是它了。
    • import() 大家都觉得没问题,稳步推进中。
    • 由于 ES Module 的静态特性,以前给 CJS 模块做动态 Mock、MonkeyPatch 的方式都不行了。不过解决办法也有,一是在加载阶段提供钩子,二是允许对已经加载的模块做热替换。
  • 2017.02.12
    Node.js CTC 和 TC39 的讨论:

    • 由于 ES6 模块的异步特性,require() 将无法加载 ES6 模块。
    • Babel 目前支持的 import { foo } from 'node-cjs-module' 也不符合规范,想 import 一个 NCJS 模块的话只能 import m from 'node-cjs-module' 然后 m.foo() 调用。
    • .mjs 是问题最少的选择。
    • (悲伤的消息来了)就目前剩余的工作内容估计,距离 ES6 Module 最终实现大约还有至少一年的时间(往好的一面想,终于看得到 timeline 了)。
  • 2017.05.10
    bmeck 在 Twitter 表示已经实现了 .mjs 加载器的原型,在 Node.js v9 中可以用 flag 的方式启用,(希望)在 v10 中正式推出。也就是还有一年的时间,一切顺利的话 2018 年 4 月就能看到 ES Module 正式加入 Node.js LTS。
  • 2017.05.11
    工具链对 .mjs 后缀的支持都在推进中:

  • 2018.03.30
    Node.js 项目中和 ES Module 实现相关的 Issue 和 PR
  • 2018.04.25
    Node.js 10.0.0 发布,加入了对 ES Module 的实验性支持(需要 --experimental-modules 开启)
    https://github.com/nodejs/nod...
  • 2019.03.28
    新版 ES Module 设计定案,PR 合进主干(https://github.com/nodejs/nod...),特性有变,仍然使用 --experimental-modules 开启。目前的计划是赶上 4 月份 Node.js 12 发布,最终在 2019 年 10 月进入 LTS。

参考资料

查看原文

赞 41 收藏 71 评论 8

熊D01001 回答了问题 · 2017-12-18

对输入文字的敏感词过滤以及 XSS 特殊字符过滤什么方案比较好?

前端产生的数据并不可靠,懂行的人可以轻易绕过前端设置的各种校验规则。
安全起见,后端在入库前应对前端产生的所有数据进行过滤处理。
这点性能开销,远比留下安全隐患划得来。

关注 7 回答 7

熊D01001 回答了问题 · 2017-08-11

解决css3 属性选择器 语法不生效

不是没有生效, 是因为选择器并没有匹配到目标节点.

[class^="pc-"] 匹配的是 class 属性值以 pc- 开头的, 但是你截图中的目标节点的 class 属性的值是 callapse pc-header, 很明显这个属性值并不是以 pc- 开头的.

可以把目标节点的 class 属性值改为 pc-header callapse, 这样 [class^="pc-"] 选择器就可以匹配到该节点了.

如果不更改目标节点 class 属性, 使用 [class*="pc-"] 也可以匹配到, 但是这个选择器匹配范围太广, 不太推荐使用.

关注 9 回答 10

熊D01001 回答了问题 · 2017-07-31

解决axios 可以挂在Vue原型上 为啥还有个vue-axios?

使用 Vue 的插件写法,更符合 Vue 整体生态环境。
P.S. 直接写原型链,感觉有些粗暴了,毕竟现在 ES8 都出来了。。。除非是很底层的实现,否则不太推荐这样写了

关注 7 回答 4

认证与成就

  • 获得 222 次点赞
  • 获得 7 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-06-27
个人主页被 1.7k 人浏览