k6 集成goja 的部分集成说明
k6 对于goja 的集成还是比较强大的,支持了es6(基于babel 的编译能力),同时对于默认的js engine 进行了扩展(基于core-js)
同时对于require以及module ,exports 也是支持的,只是对于exports 是自己定义了变量,同时对于一些内置的模块(k6 开头的进行了
特殊处理,直接内置),对于require 的支持是基于了 github.com/spf13/afero 一个很不错的通用文件系统抽象以及自定义了一个require
函数,同时暴露到goja js engine 的全局对象中,同时k6 扩展了console的支持
es6编译处理
会将babel 的Transform 映射为一个golang 函数,编译就是利用了bable 的能力,为了防止多起加载,使用了sync.Once
同时会通过goja 的parser 解析代码为ast然后在编译(为了兼容,支持了多种模式的编译)
func (c *Compiler) Compile(src, filename, pre, post string,
strict bool, compatMode lib.CompatibilityMode) (*goja.Program, string, error) {
code := pre + src + post
ast, err := parser.ParseFile(nil, filename, code, 0)
if err != nil {
if compatMode == lib.CompatibilityModeExtended {
code, _, err = c.Transform(src, filename)
if err != nil {
return nil, code, err
}
// the compatibility mode "decreases" here as we shouldn't transform twice
return c.Compile(code, filename, pre, post, strict, lib.CompatibilityModeBase)
}
return nil, code, err
}
pgm, err := goja.CompileAST(ast, strict)
return pgm, code, err
}
babel 浏览器集成方法(一个参考)
var output = Babel.transform(content, { presets: ['env'] }).code;
require 支持
实际上就是利用了js hack 的模式
https://github.com/loadimpact/k6/blob/master/js/initcontext.go#L117
func (i *InitContext) Require(arg string) goja.Value {
switch {
case arg == "k6", strings.HasPrefix(arg, "k6/"):
// Builtin or external modules ("k6", "k6/*", or "k6/x/*") are handled
// specially, as they don't exist on the filesystem. This intentionally
// shadows attempts to name your own modules this.
v, err := i.requireModule(arg)
if err != nil {
common.Throw(i.runtime, err)
}
return v
default:
// Fall back to loading from the filesystem.
v, err := i.requireFile(arg)
if err != nil {
common.Throw(i.runtime, err)
}
return v
}
}
spf13/afero 参考使用
package main
import (
"io/ioutil"
"log"
"net/http"
"github.com/spf13/afero"
)
func httpFs() {
var appFs = afero.NewMemMapFs()
appFs.Mkdir("/", 0755)
fh, _ := appFs.Create("/demoapp.js")
fh.WriteString("This is a test")
fh.Close()
httpFs := afero.NewHttpFs(appFs)
fileserver := http.FileServer(httpFs.Dir("/"))
http.Handle("/", fileserver)
http.ListenAndServe(":8090", nil)
}
func main() {
httpFs()
}
为了方便跨平台的支持,提供了一个通用的基于afero 的文件系统实现,同时支持网络文件系统
func CreateFilesystems() map[string]afero.Fs {
// We want to eliminate disk access at runtime, so we set up a memory mapped cache that's
// written every time something is read from the real filesystem. This cache is then used for
// successive spawns to read from (they have no access to the real disk).
// Also initialize the same for `https` but the caching is handled manually in the loader package
osfs := afero.NewOsFs()
if runtime.GOOS == "windows" {
// This is done so that we can continue to use paths with /|"\" through the code but also to
// be easier to traverse the cachedFs later as it doesn't work very well if you have windows
// volumes
osfs = fsext.NewTrimFilePathSeparatorFs(osfs)
}
return map[string]afero.Fs{
"file": fsext.NewCacheOnReadFs(osfs, afero.NewMemMapFs(), 0),
"https": afero.NewMemMapFs(),
}
}
网络文件加载模式也是支持的,首先第一次是进行http 请求缓存,然后进行读取(基于NewMemMapFs)
if scheme == "https" {
var finalModuleSpecifierURL = &url.URL{}
switch {
case moduleSpecifier.Opaque != "": // This is loader
finalModuleSpecifierURL, err = resolveUsingLoaders(logger, moduleSpecifier.Opaque)
if err != nil {
return nil, err
}
case moduleSpecifier.Scheme == "":
logger.Warningf(`The moduleSpecifier "%s" has no scheme but we will try to resolve it as remote module. `+
`This will be deprecated in the future and all remote modules will `+
`need to explicitly use "https" as scheme.`, originalModuleSpecifier)
*finalModuleSpecifierURL = *moduleSpecifier
finalModuleSpecifierURL.Scheme = scheme
default:
finalModuleSpecifierURL = moduleSpecifier
}
var result *SourceData
result, err = loadRemoteURL(logger, finalModuleSpecifierURL)
if err == nil {
result.URL = moduleSpecifier
// TODO maybe make an afero.Fs which makes request directly and than use CacheOnReadFs
// on top of as with the `file` scheme fs
_ = afero.WriteFile(filesystems[scheme], pathOnFs, result.Data, 0644)
return result, nil
}
if moduleSpecifier.Scheme == "" || moduleSpecifier.Opaque == "" {
// we have an error and we did remote module resolution without a scheme
// let's write the coolest error message to try to help the lost soul who got to here
return nil, noSchemeRemoteModuleResolutionError{err: err, moduleSpecifier: originalModuleSpecifier}
}
return nil, errors.Errorf(httpsSchemeCouldntBeLoadedMsg, originalModuleSpecifier, finalModuleSpecifierURL, err)
}
自定义模块的开发
k6已经内置了一些以k6开头的模块,使用了类似databasedriver 的开发模式,一个简单参考
func init() {
modules.Register("k6", New())
}
type K6 struct{}
// ErrGroupInInitContext is returned when group() are using in the init context
var ErrGroupInInitContext = common.NewInitContextError("Using group() in the init context is not supported")
// ErrCheckInInitContext is returned when check() are using in the init context
var ErrCheckInInitContext = common.NewInitContextError("Using check() in the init context is not supported")
func New() *K6 {
return &K6{}
}
说明
通过学习k6集成goja 的模式,发现比goja 社区的模块模式简单点(但是核心没变),提供k6支持es6 的模式设计的挺不错的
(以前自己有基于browserify +babel 编译模式集成的模式,发现不是很好,而且没有k6集成模式支持的特性多,而且简单)
参考资料
https://github.com/loadimpact/k6/blob/master/js/common/bridge.go
https://github.com/loadimpact/k6/blob/master/js/initcontext.go#L117
https://godoc.org/github.com/dop251/goja#Runtime.ToValue
https://github.com/spf13/afero
https://github.com/loadimpact/k6/blob/master/js/compiler/compiler.go#L74
https://github.com/zloirock/core-js
https://babeljs.io/docs/en/babel-standalone.html
https://github.com/loadimpact/k6/blob/master/loader/loader.go#L205