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