jbrowse源码分析

782 阅读7分钟

1.先来看下目录结构

- .github
- bin
- browser
- build
- css
- docs
- img
- plugins
- sample_data
- src
  - JBrowse
  - perl5
  - jdataview-1.1-patch.X.js
  - lazyload.js
- tests
- utils
- website
- .editorconfig
- .eslintrc
- .gitattributes
- .gitignore // git配置文件
- .htaccess
- .npmignore
- travis.yml
- browserconfig.xml
- index.html // 项目入口页面
- INSTALL
- jbrowse_conf.json
- jbrowse.conf
- LICENSE
- Makefile.PL
- package.json
- README.md
- release.notes.md
- setup.sh
- site.webmanifest
- webpack.config.js
- yarn.lock

2.先看README.md

1. 初始化的时候要执行./setup.sh
2. 作为npm模块安装的时候要执行 .bin/jb_setup.js .bin/jb_run.js
3. tests是项目的测试模块
4. 执行build目录下的Makefile可以生成最小的代码库
5. 安装electron-packager包并执行Makeile release-electron-all可以生成electron app
6. 安装electron包,执行browser/main.js可以进electron的开发模式

3. 看package.json

"scripts": {
  "test": "npm run test-puppeteer",
  "test-all": "npm run build && npm run test-puppeteer && npm run test-perl && npm run test-electron && npm run test-selenium",
  "test-puppeteer": "node tests/js_tests/run-puppeteer.js http://localhost:8082/tests/js_tests/index.html",
  "test-electron": "ava --timeout=60s tests/electron_tests",
  "test-perl": "prove -Isrc/perl5 -j8 -lr tests/perl_tests/",
  "test-selenium": "SELENIUM_BROWSER=firefox JBROWSE_URL='http://localhost:8082/index.html' nosetests --verbose tests/selenium_tests/",
  "start": "utils/jb_run.js -p 8082",
  "watch": "webpack -w",
  "build": "webpack",
  "build-electron": "electron-packager . JBrowse --overwrite --icon=browser/icons/jbrowse",
  "hint": "npm run lint",
  "lint": "eslint src/JBrowse tests/js_tests/spec"
},
"bin": {
  "jb_run.js": "utils/jb_run.js",
  "jb_setup.js": "utils/jb_setup.js"
},

官网上说,你可以执行npm run start在本地开启一个express的服务器,但是每次修改了内容之后需要重新执行一次。也可以使用npm run watch来实现自动重启。

所以之后要留意一下jb_run.js和webpack配置文件。

4. 看webpack.config.js

摘一下比较重要的配置

entry: {
  main: "src/JBrowse/main",
  browser: "src/JBrowse/standalone"
},
plugins: [
  new CleanWebpackPlugin(['dist']),
  new DojoWebpackPlugin({
    loaderConfig: require("./build/dojo-loader-config"),
    environment: {
      dojoRoot: process.env.JBROWSE_PUBLIC_PATH || "./dist/"
    },
    buildEnvironment: {
      dojoRoot: "node_modules/"
    },
    locales: ["en"],
    loader: path.resolve('./build/dojo-webpack-plugin-loader/dojo/dojo.js')
  }),
  new CopyWebpackPlugin([{
    context: "node_modules",
    from: "dojo/resources/blank.gif",
    to: "dojo/resources"
  }]),
],
output: {
  filename: '[name].bundle.js',
  chunkFilename: '[name].bundle.js',
  path: path.resolve(__dirname, 'dist'),
  publicPath: process.env.JBROWSE_PUBLIC_PATH || 'dist/'
},

入口文件有两个src/JBrowse/mainsrc/JBrowse/standalone,输入为dist目录下的[name].bundle.js

5.看下setup.sh

值得注意的代码:

# if we are running in a development build, then run npm install and run the webpack build.
if [ -f "src/JBrowse/Browser.js" ]; then
  log_echo -n "Installing node.js dependencies and building with webpack ..."
    (
    set -e
    check_node
    [[ -f node_modules/.bin/yarn ]] || npm install yarn
    node_modules/.bin/yarn install
    JBROWSE_BUILD_MIN=$JBROWSE_BUILD_MIN node_modules/.bin/yarn build
    ) >>setup.log 2>&1;
  done_message "" "" "FAILURE NOT ALLOWED"
else
  log_echo "Minimal release, skipping node and Webpack build (note: this version will not allow using plugins. Use a github clone or a dev version of JBrowse to use plugins"
fi

根据src/JBrowse/Browser.js是否存在判断是否是开发版代码,如果是develop build,要安装node yarn然后执行yarn install yarn build

然后会安装一些perl的依赖。

生成Volvox的实例数据:

# ...
# format volvox
rm -rf sample_data/json/volvox;
bin/prepare-refseqs.pl --fasta docs/tutorial/data_files/volvox.fa --out sample_data/json/volvox;
if [ $SUPPRESS_BIODB_TO_JSON -eq 1 ]; then
    echo "Not running biodb-to-json.pl for Volvox";
else
    bin/biodb-to-json.pl -v --conf docs/tutorial/conf_files/volvox.json --out sample_data/json/volvox;
fi

cat \
    docs/tutorial/data_files/volvox_microarray.bw.conf \
    docs/tutorial/data_files/volvox_sine.bw.conf \
    docs/tutorial/data_files/volvox-sorted.bam.conf \
    docs/tutorial/data_files/volvox-sorted.bam.coverage.conf \
    docs/tutorial/data_files/volvox-paired.bam.conf \
    docs/tutorial/data_files/volvox.vcf.conf \
    docs/tutorial/data_files/volvox_fromconfig.conf \
    docs/tutorial/data_files/volvox.gff3.conf \
    docs/tutorial/data_files/volvox.gtf.conf \
    docs/tutorial/data_files/volvox.sort.gff3.gz.conf \
    docs/tutorial/data_files/volvox.sort.gff3.gz.htmlfeatures.conf \
    docs/tutorial/data_files/volvox.sort.bed.gz.conf \
    docs/tutorial/data_files/gvcf.vcf.gz.conf \
    docs/tutorial/data_files/bookmarks.conf \
    docs/tutorial/data_files/volvox.subsubparts.gff3.conf \
    docs/tutorial/data_files/volvox-long-reads.fastq.sorted.bam.conf \
    docs/tutorial/data_files/volvox-long-reads.fastq.sorted.cram.conf \
    docs/tutorial/data_files/volvox.bb.conf \
    docs/tutorial/data_files/volvox-sorted.cram.conf \
    docs/tutorial/data_files/volvox-sv.bam.conf \
    docs/tutorial/data_files/volvox-sv.cram.conf \
>> sample_data/json/volvox/tracks.conf

bin/add-json.pl '{ "dataset_id": "volvox", "include": [ "../../raw/volvox/functions.conf" ] }' sample_data/json/volvox/trackList.json
bin/add-json.pl '{ "dataset_id": "volvox", "plugins": [ "HideTrackLabels", "NeatCanvasFeatures", "NeatHTMLFeatures" ] }' sample_data/json/volvox/trackList.json
bin/flatfile-to-json.pl --bed docs/tutorial/data_files/volvox_segment.bed --out sample_data/json/volvox --trackLabel ChromHMM --trackType CanvasFeatures --clientConfig '{"color": "{chromHMM}", "strandArrow": false}' --config '{"displayMode": "collapsed", "enableCollapsedMouseover": true, "category": "Miscellaneous" }';
bin/generate-names.pl --safeMode -v --out sample_data/json/volvox;

mkdir -p sample_data/raw;
if [ ! -e sample_data/raw/volvox ]; then
    ln -s ../../docs/tutorial/data_files sample_data/raw/volvox;
fi;
ln -sf ../../docs/tutorial/conf_files/volvox.json sample_data/raw/;

touch sample_data/json/volvox/successfully_run;

上面的代码总结下来有这么几步:

1.使用perl脚本(bin/prepare-refseqs.pl, biodb-to-json.pl)处理.fa .json文件,生成初始化实例数据
2.生成配置数据tracks.conf
3.使用perl脚本(add-json.pl)生成trackList.json,还有个bin/flatfile-to-json.pl不清楚是什么作用

生成yeast示例数据的过程类似。

6. 看下webpack中的两个入口文件src/JBrowse/mainsrc/JBrowse/standalone

main.js:

require([
    'JBrowse/Browser',
    'JBrowse/Util',
    'JBrowse/QueryParamConfigMapper',
    'dojo/io-query',
    'dojo/json',
    'css!../../css/genome.scss',
],
function (
    Browser,
    Util,
    QueryParamConfigMapper,
    ioQuery,
    JSON,
) {
    // 根据url的参数来生成一个queryParams
    var queryParams = ioQuery.queryToObject( window.location.search.slice(1) );
    // 声明一个全局配置变量参数config,这个参数用来生成全局jbrowse
    var config = {
        containerID: "GenomeBrowser",
        ...
    };
    // 这里使用的是dojo,一个DHTML框架?
    dojo.addOnLoad(() => {
        // 这里依然是对config进行一下参数配置
        if( queryParams.addFeatures ) {
            config.stores.url.features = JSON.parse( queryParams.addFeatures );
        }
        ...
        
        QueryParamConfigMapper().handleQueryParams(config,queryParams);

        // 创建一个包含JBrowse实例的JBrowse全局变量
        window.JBrowse = new Browser( config );
        
        window.JBrowse.afterMilestone('loadRefSeqs', function() { dojo.destroy(dojo.byId('LoadingScreen')); });
    })
});

这个项目应该是使用dojo作为基础框架,去查了一下是个比较早的框架,项目也使用了dojo配套的叫做dijit的widget组件库。

而且看到这里创建了一个Browser的实例,命名为JBrowse挂在windows下。这个实例生成了在浏览器中显示的数据图形页面。所以之后我们要去看下Browser的构造函数是如何实现的。

standalone.js:

import 'babel-polyfill'
require([
    'JBrowse/Browser',
    'css!../../css/genome.scss',
], function (Browser) {
    window.Browser = Browser;
});

7. 看下src/JBrowse/Browse.js

我们先将src/JBrowse/目录下的文件夹和重要文件展示出来:

- src
  - JBrowse
    - ConfigAdaptor
    - Digest
    - Model
    - Store
    - Util
    - View
    - .eslintrc
    - BehaviorManager.js
    - Browser.js // jbrowse前端页面的主文件
    - CodonTable.js
    - Component.js
    - ConfigManager.js
    - Errors.js
    - FeatureFiltererMixin.js
    - Finisher.js
    - GenomeView.js
    - has.js
    - main.js
    - package.json
    - Plugin.js
    - QueryParamConfigMapper.js
    - standalone.js
    - Store.js
    - TouchScreenSupport.js
    - Track.js
    - Util.js

再看Browse.js文件,Browse.js有3471行,是jbrowser项目前端页面的构造函数,使用了dojo框架搭配widget组件以AMD模块加载方式开发。

重要代码:

define([
    'dojo/_base/declare',
    'dojo/_base/lang',
    'dojo/on',
    'dojo/html',
    'dojo/query',
    'dojo/dom-construct',
    'dojo/keys',
    'dojo/Deferred',
    'dojo/DeferredList',
    'dojo/topic',
    'dojo/aspect',
    'dojo/request',
    'dojo/io-query',
    'JBrowse/has',
    'dojo/_base/array',
    'dijit/layout/ContentPane',
    'dijit/layout/BorderContainer',
    'dijit/Dialog',
    'dijit/form/ComboBox',
    'dojo/store/Memory',
    'dijit/form/Button',
    'dijit/form/Select',
    'dijit/form/ToggleButton',
    'dijit/form/DropDownButton',
    'dijit/DropDownMenu',
    'dijit/CheckedMenuItem',
    'dijit/MenuItem',
    'dijit/MenuSeparator',
    'dojox/form/TriStateCheckBox',
    'dojox/html/entities',
    'JBrowse/Util',
    'JBrowse/Store/LazyTrie',
    'JBrowse/Store/Names/LazyTrieDojoData',
    'dojo/store/DataStore',
    'JBrowse/FeatureFiltererMixin',
    'JBrowse/GenomeView',
    'JBrowse/TouchScreenSupport',
    'JBrowse/ConfigManager',
    'JBrowse/View/InfoDialog',
    'JBrowse/View/FileDialog',
    'JBrowse/View/FastaFileDialog',
    'JBrowse/Model/Location',
    'JBrowse/View/LocationChoiceDialog',
    'JBrowse/View/Dialog/SetHighlight',
    'JBrowse/View/Dialog/Preferences',
    'JBrowse/View/Dialog/OpenDirectory',
    'JBrowse/View/Dialog/SetTrackHeight',
    'JBrowse/View/Dialog/QuickHelp',
    'JBrowse/View/StandaloneDatasetList',
    'JBrowse/Store/SeqFeature/ChromSizes',
    'JBrowse/Store/SeqFeature/UnindexedFasta',
    'JBrowse/Store/SeqFeature/IndexedFasta',
    'JBrowse/Store/SeqFeature/BgzipIndexedFasta',
    'JBrowse/Store/SeqFeature/TwoBit',
    'dijit/focus',
    '../lazyload.js', // for dynamic CSS loading
    
    // extras for webpack
    'dojox/data/CsvStore',
    'dojox/data/JsonRestStore'
], function(
    declare,
    lang,
    on,
    html,
    query,
    domConstruct,
    keys,
    Deferred,
    DeferredList,
    topic,
    aspect,
    request,
    ioQuery,
    has,
    array,
    dijitContentPane,
    dijitBorderContainer,
    dijitDialog,
    dijitComboBox,
    dojoMemoryStore,
    dijitButton,
    dijitSelectBox,
    dijitToggleButton,
    dijitDropDownButton,
    dijitDropDownMenu,
    dijitCheckedMenuItem,
    dijitMenuItem,
    dijitMenuSeparator,
    dojoxTriStateCheckBox,
    dojoxHtmlEntities,
    Util,
    LazyTrie,
    NamesLazyTrieDojoDataStore,
    DojoDataStore,
    FeatureFiltererMixin,
    GenomeView,
    Touch,
    ConfigManager,
    InfoDialog,
    FileDialog,
    FastaFileDialog,
    Location,
    LocationChoiceDialog,
    SetHighlightDialog,
    PreferencesDialog,
    OpenDirectoryDialog,
    SetTrackHeightDialog,
    HelpDialog,
    StandaloneDatasetList,
    ChromSizes,
    UnindexedFasta,
    IndexedFasta,
    BgzipIndexedFasta,
    TwoBit,
    dijitFocus,
    LazyLoad
){
return declare(FeatureFiltererMixin, {
    constructor: function(){},
    _initialLocation: function(){},
    ...
    initPlugins: function(){},
    initView: function(){},
    ...
})
})

然后这里的实现已经非常接近页面的具体细节了,现在我们希望的是如何将页面每个部分的具体模块与代码对应上,方便理解开发模式已经将来对页面自己需要的地方进行修改。不妨单独把constructor函数单独拿出来分析,不重要的代码直接省略了:

constructor: function(params) {
    // 新建Browser实例时,传入的参数作为配置数据
    this.config = params || {};

    // 如果是在执行单元测试,代码会在这里停止
    if( this.config.unitTestMode ) return;

    // 外部应用初始化时的钩子,可以在index.html中设置
    if (typeof this.config.initExtra === 'function')
        this.config.initExtra(this,params);

    this.startTime = new Date();

    // 开始初始化
    var thisB = this;
    
    // dojo 初始化
    dojo.addOnLoad( function() {
        // util工具函数的集合,判断如果是electron应用
        if(Util.isElectron() && !thisB.config.dataRoot) {
            dojo.addClass(document.body, "jbrowse");
            dojo.addClass(document.body, thisB.config.theme || "tundra");
            thisB.welcomeScreen(document.body);
            return;
        }
        
        // loadConfig函数,根据配置数据加载所有的文件
        thisB.loadConfig().then( function() {
            // 初始化container
            thisB.container = dojo.byId( thisB.config.containerID );
            thisB.container.onselectstart = function() { return false; };

            // 如果在配置中设置了突出显示,则初始化突出显示
            if( thisB.config.initialHighlight && thisB.config.initialHighlight != "/" ) {
                thisB.setHighlight( new Location( thisB.config.initialHighlight ) );
            }
            // 初始化所有在config中定义的插件
            thisB.initPlugins().then( function() {
                thisB.loadNames();
                thisB.loadUserCSS().then( function() {
                    // 初始化异步获取的track元数据
                    thisB.initTrackMetadata();
                    thisB.loadRefSeqs().then( function() {

                       // 计算出初始化的坐标
                       var initialLocString = thisB._initialLocation();
                       //  util工具中提供的parseLocString()函数
                       var initialLoc = Util.parseLocString( initialLocString );
                       if (initialLoc && initialLoc.ref && thisB.allRefs[initialLoc.ref]) {
                           thisB.refSeq = thisB.allRefs[initialLoc.ref];
                       }

                       // 确认container的宽和高非零
                       thisB.ensureNonzeroContainerDimensions()
                       // 初始化试图
                       thisB.initView().then( function() {
                           Touch.loadTouch(); // 初始化对触屏设备的支持
                           if( initialLocString ) {
                               thisB.navigateTo( initialLocString, true );
                           }

                           // 计算出我们使用的初始化track list
                           // tracksToShow就是我们将要绘制的图形的参数信息
                           var tracksToShow = [];
                           // 总是添加始终轨道,不管任何其他轨道参数
                           if (thisB.config.alwaysOnTracks) { 
                            tracksToShow = tracksToShow.concat(thisB.config.alwaysOnTracks.split(","));
                           }
                           // 在URL track参数中添加tracks指定的规则
                           if (thisB.config.forceTracks)   { 
                            tracksToShow = tracksToShow.concat(thisB.config.forceTracks.split(",")); 
                           } else if (thisB.cookie("tracks")) { 
                           // 如果没有URL track参数,通过tracks cookie添加上次查看的tracks
                            tracksToShow = tracksToShow.concat(thisB.cookie("tracks").split(",")); 
                           } else if (thisB.config.defaultTracks) {
                             // 如果没有URL参数和也没有tracks cookie,那么使用defaultTracks
                             // 在少数情况下defaultTracks已经包括了一个数组,在先前的函数中已经被分割调用了。
                             // 所以加入他没有分割的话 我们只能分割尝试
                             if (!(thisB.config.defaultTracks instanceof Array)) {
                                tracksToShow = tracksToShow.concat(thisB.config.defaultTracks.split(","));
                             }
                           }
                           // 目前只能在没有其他要显示的指南下,才会显示DNA
                           // 这里也可以改为强制迫使DNA显示
                           if (tracksToShow.length == 0) { tracksToShow.push("DNA"); }
                           tracksToShow = Util.uniq(tracksToShow);
                           //消除轨道重复(可能同时在alwaysOnTracks和defaultTracks中指定)
                           thisB.showTracks( tracksToShow );
                           thisB.passMilestone( 'completely initialized', { success: true } );
                       });
                       thisB.reportUsageStats();
                    });
                });
            });
        });
    });
},

上面的代码可以看到,constructor构造函数使用了很多定义在this上的方法。先根据配置文件加载所有需要的文件,然后初始化视图框,计算出初始化坐标的位置,判断视图框的宽和高不为零。初始化视图view,定义一个空数组tracksToShow作为要展示的数据,根据url,cookie等把不同的值赋给tracksToShow,最后将数据绘制出来。

但是很明显的可以看到,现在只能对图形初始化做一个大概的了解,生成的数据来源,如何初始化视图框之类的细节还需要找到对应的函数中了解。

8. jbrowse加载数组的方式

src/JBrowse/Model文件夹中有有个异步获取文件的js文件XHRBlob.js。这个文件应该是jbrowse异步获取数据的关键文件。在src下搜索XHRBlob,发现src/JBrowse/Store/SeqFeature里几乎每个以文件格式命名的文件都引用了XHRBlob.js。那么SeqFeature里面是什么文件呢,来看下该文件的文件目录(只摘去部分):

- src
  - JBrowse
    - Store
      - SeqFeature
        - BED
        - BigWig
        - GTF
        - UnindexedFasta
        - BAM.js
        - BED.js
        - BEDTabix.js
        - BigWig.js
        - CRAM.js
        - GFF3.js
        - GTF.js
        ...
        

我们从上面的目录可以得出一个结论,这个目录下是对应着不同文件格式数据的处理代码,不同的js处理不同格式的数据,其中处理步骤包括获取相应格式的数据。所以jbrose是确定本次要展示的是哪一种数据然后调用对应的js文件再进行处理的。