nej+regular環境使用es6的低成本方案

本文來自 網易雲社區 。javascript

 

但願在生產環境中使用es6/7,babel應該是最廣泛的選擇。這是babel官網中,它對本身的定義:html

Babel 自帶了一組 ES2015 語法轉化器。這些轉化器能讓你如今就使用最新的 JavaScript 語法,而不用等待瀏覽器提供支持。java

babel就像一個javascript文件預處理器,你能夠自由使用es6/7語法,不用小心兼容性問題,由於瀏覽器中運行是babel爲你處理妥帖的代碼。爲了方便使用,它提供了許多使用方法:webpack、gulp、browserify、grunt......webpack

經過哪一種方式來在當前技術棧(nej+regular+stateman)中使用babel,是一個值得深思熟慮問題,而沒有通過深思熟慮就試圖使用webpack的我,一度掉進了一個坑中:es6

 

webpack + babel 的踩坑過程

webpack應該是目前最流行的構建工具,關於它和babel的使用方法,網上的資料汗牛充棟,前人對各類可能發生的問題(好比ie8的兼容)基本都有了解決方法,換句話說就是這條路的坑比較少。web

可是對咱們而言,webpack+babel的方式存在如下幾個問題:npm

  1. 改變打包方式帶來的不肯定性。貿然在生產環境中替換打包工具,風險太大。
  2. 大量的歷史代碼中使用的nej改良後的amd語法,webpack沒法識別。webpack支持amd語法,也支持路徑別名,但僅限於以別名開頭的路徑
//webpack.config.js
...
alias: {
    pro: 'a/b/c'
}
...

//test.js
define(['pro/d']) //define(['a/b/c/d'])
define(['pro/e/{mode}/f']) //error

這個問題能夠經過編寫babel插件,根據nej的模塊語法定製修改:babel-plugin-transform-nej-modulegulp

  1. ftl注入,多入口。這兩個問題能夠從考拉的一篇文章中找到答案。

除此以外,還遇到一個比較特殊的問題,webpack沒法識別咱們歷史代碼中的一個文件。api

綜上所述,若是有着豐富的webpack使用經驗,可以承受改變打包方式帶來的風險,能夠考慮使用webpack來引入babel。數組

從webpack的踩坑過程當中,找到了作es6/7改造的兩個原則:

  1. 不改變歷史代碼
  2. 不改變打包方式

 

gulp + babel

根據上述的兩個原則,,gulp無疑是個很好的選擇:

  1. 當檢測到文件是用nej的語法引入模塊時,不處理; //不改變歷史代碼
  2. 當檢測到文件是用es6語法引入模塊時,把它改形成nej的語法,再進行es6/7的語法轉換; // 保證nej能夠識別,打包方式不變

所以,利用gulp來引入es6/7,過程應該是這樣的:

虛線方框裏是須要咱們來作的工做:

  1. import/export轉化爲nej的模塊語法
  2. 根據兼容需求,轉化es6/7語法

經過babel的插件能夠完成這兩個任務:

 

import/export轉化爲nej的模塊語法

babel自己會將import和export的語法轉化爲commonjs格式:

轉化前 轉化後 目標
import a from 'A' var a = require('A') define('A', function(a){...}
export {b} exports.b = b; {... return b}

這個轉化只是個簡寫,詳細的轉化後代碼能夠在這裏看,代碼解析能夠參考這篇文章

目前沒有前人的工做能夠直接實現的咱們的目標,因此必須本身編寫一個,babel插件的編寫能夠參考這篇文章。簡單的說,babel會把javascript代碼解析爲一棵語法樹,經過修改這棵樹來方便、準確的修改javascript代碼。

插件babel-plugin-transform-es2015-modules-nej將es6的模塊語法轉換爲nej的模塊語法,主要流程爲:

判斷是否爲nej模塊->解析import,生成路徑數組、文件名數組->解析export,生成return語句->將除了import和export的其它代碼生成內容數組,並將return語句放入->利用這三個數組構建amd格式的模塊語法

 

根據兼容需求,轉化es6/7語法

babel經過.baberc來配置

//.babelrc
{
    preset: '...',
    plugins: '...'
}

能夠看到,babel的配置由presetplugins構成,preset是插件的集合,選擇預設的插件集合配合一些解決比較特殊問題的插件,來完成babel的配置。

在npm中搜索babel-presetbabel-plugin可以得到3000+的結果,在預設的插件集合中,babel-preset-env是很是省心的選擇,它是一個動態的插件集合,經過指定你想要兼容的瀏覽器,它會幫你引入須要的插件。加上上一步中編寫的transform-es2015-modules-nej,配置就完成了。

babel轉化後的代碼會默認使用嚴格模式,若是歷史代碼中存在嚴格模式下報錯的問題,記得在插件中加上transform-remove-strict-mode

{
    "presets": [
        ["env", {
            "targets": {
                "browsers": ["last 2 versions", "IE 8-10"]
            }
        }]
    ],
    "plugins": [
        "transform-es2015-modules-nej"
    ]
}

API的轉化

最後一個問題是:babel只轉換語法,不轉換api,因此須要polyfill來保證generateasync這些喜聞樂見的api的正常使用。polyfill.min.js文件體積不算小:102kb,須要在每一個頁面中都引用,固然,加載一次事後,緩存能夠幫助節省大部分時間。

最優解是在打包的時候,根據每一個頁面使用的api來引入對應的polyfill。babel的官方插件babel-plugin-transform-runtime,能作到只引入文件用到的api的polyfill,可是它的引入方式爲commonjs。實際上,即便是amd方式,也難以和nej的模塊語法完美融合。並且,針對文件來引用polyfill仍然使得同一個頁面引用多個相同polyfill,加載重複數據。

所以,對於API的轉化,由以下三種解決方案

  • 每一個頁面引入polyfill.min.js;
  • 每一個文件引入對應polyfill;(修改插件babel-plugin-transform-runtime)
  • 打包時引入頁面所需polyfill;(利用打包來polyfill)

  

最終方案

graph TD
    A(gulp)-->|監視|B[raw/xxx/a.js]
    B-->|發生改變|C(babel)
    C-->|babel.rc|D(src/xxx/a.js)
    A-->|sourcemap|E(src/xxx/a.js.map)

gulp檢測文件的變更,經過babel轉化es6代碼,轉化過程當中,gulp生成對應文件的sourcemap:a.js.map

分爲四步:

  1. 配置gulp
  2. 配置babelrc
  3. 配置gulp+babel生成sourcemap
  4. 引入polyfill

 

1. 配置gulp

安裝gulp和babel

npm install --save-dev gulp;
npm install --save-dev gulp-babel

配置gulpfile.js:

const gulp = require('gulp');
const babel = require('gulp-babel');

gulp.task('babel', () =>
    gulp.src('./raw/**/*.js')
        .pipe(babel())
        .pipe(gulp.dest('./src'))
);

gulp.task('watch:babel', () => {
    gulp.watch('./raw/**/*.js', ['babel']);
});

2.配置babelrc

{
    "presets": [
        ["env", {
            "targets": {
                "browsers": ["last 2 versions", "IE 8-10"]
            }
        }]
    ],
    "plugins": [
        "transform-remove-strict-mode","transform-es2015-modules-nej"
    ]
}

提醒不熟悉babel的小夥伴一句,這些插件和預設須要安裝,babel包中並不提供:

npm install --save-dev babel-preset-env;
npm install --save-dev babel-plugin-transform-remove-strict-mode;
npm install --save-dev babel-plugin-transform-es2015-modules-nej;

3.配置gulp+babel生成sourcemap

修改gulpfile.js以下:

const gulp = require('gulp');
const babel = require('gulp-babel');
const sourcemaps = require('gulp-sourcemaps');

gulp.task('babel', () =>
    gulp.src('./raw/**/*.js')
        .pipe(sourcemaps.init())
        .pipe(babel())
        .pipe(sourcemaps.write('.',{sourceRoot: 'raw'}))
        .pipe(gulp.dest('./src'))
);

gulp.task('watch:babel', () => {
    gulp.watch('./raw/**/*.js', ['babel']);
});

生成sourcemap後,能夠在瀏覽器中運行轉換後代碼,調試轉換前代碼。

4. polyfill

<script src="/res/vendorjs/core/polyfill.min.js"></script>

總結及效果

目前測試的狀況,ie9及其以上環境有效,理論上支持ie8。

es6最大的優勢是給碼農帶來的快樂,一個不是很明顯的快樂對好比下:

原來這樣寫:

NEJ.define([
    'text!./app.html',
    'pro/cache/indexCache',
    'pro/util/userUtil',
    'pro/module/module',
    'pro/util/util'
],function(template,
           IndexCache,
           userUtil,
           Module,
           util
        ){
    var App = Module.extend({
        template: template,
        config: function(){
            this.supr();
            this.cache =new IndexCache();
            util.extend(this.data, {
                columns: [],
                columnConfig: [{
                    isShowIntroPic: false,
                    isVertical: false
                },{
                    isShowIntroPic: true,
                    isVertical: true
                },{
                    isShowIntroPic: true,
                    isVertical: true
                },{
                    isShowIntroPic: false,
                    isVertical: false
                }],
                courseUrlPrefix: userUtil.isUserLogin() ? this.$urlPrefix.termDetailPrefix : this.$urlPrefix.courseDetailPrefix
            });
            this.data.courseUrl = "/path/courses/";
        },
        init: function(){
            this.supr();
            this.getInitData();
        },
        enter: function(){
            this.supr();
        },
        getInitData: function() {
            this.cache.courseColumn( this.onGetCourseColumn._$bind(this));
        },
        onGetCourseColumn: function(data) {

            for(var i=0; i<data.length; i++ ) {
                var columnData = data[i];
                var columnConfig = this.data.columnConfig[i];
                var column = {
                    title: columnData.sectionName,
                    isShowIntroPic: columnConfig.isShowIntroPic,
                    isVertical: columnConfig.isVertical,
                    introPicSrc: columnData.photoUrl,
                    courseCards: []
                };

                var coursesData = columnData.termCardVos;

                for(var j=0; j<coursesData.length; j++) {
                    var courseData = coursesData[j];
                    column.courseCards.push({
                        title: courseData.courseName,
                        url: this.data.courseUrlPrefix.replace(':termid', courseData.termId),
                        src: courseData.bigPhoto,
                        price: courseData.price == 0 ? '免費' : courseData.price + '元'
                    })
                }

                this.data.columns.push(column);
            }

            this.$update();
        },
        leave: function(){
            this.supr();
        }
    });

    return App;
});

如今能夠這樣寫:

import template from './app.html';
import IndexCache from 'pro/cache/indexCache';
import userUtil from 'pro/util/userUtil';
import Module from 'pro/module/module';
import util from 'pro/util/util';

const App = Module.extend({
    template: template, 
    config: function () {
        this.supr();
        this.cache = new IndexCache();
        Object.assign(this.data, {
            columns: [],
            columnConfig: [{
                isShowIntroPic: false,
                isVertical: false
            }, {
                isShowIntroPic: true,
                isVertical: true
            }, {
                isShowIntroPic: true,
                isVertical: true
            }, {
                isShowIntroPic: false,
                isVertical: false
            }],
            courseUrlPrefix: userUtil.isUserLogin() ? this.$urlPrefix.termDetailPrefix : this.$urlPrefix.courseDetailPrefix
        });
        this.data.courseUrl = '/path/courses/';
    },
    init: function () {
        this.supr();
        this.getInitData();        
    },
    enter: function () {
        this.supr();
    },
    getInitData: async function () {
        const data = await this.cache.courseColumn();

        for(let [columnIdx, columnData] of data.entries()) {
            let columnConfig = this.data.columnConfig[columnIdx],
            column = {
                title: columnData.sectionName,
                isShowIntroPic: columnConfig.isShowIntroPic,
                isVertical: columnConfig.isVertical,
                introPicSrc: columnData.photoUrl,
                courseCards: []
            },
            coursesData = columnData.termCardVos;

            for(let courseData of coursesData) {
                column.courseCards.push({
                    title: courseData.courseName,
                    url: this.data.courseUrlPrefix.replace(':termid', courseData.termId),
                    src: courseData.bigPhoto,
                    price: courseData.price == 0 ? '免費' : `${courseData.price}元`
                });
            }

            this.data.columns.push(column);
        }

        this.$update();
    },
    leave: function () {
        this.supr();
    }
});

export {
    App
};

本文來自網易雲社區,經做者曹陽受權發佈。

原文地址:nej+regular環境使用es6的低成本方案

更多網易研發、產品、運營經驗分享請訪問網易雲社區

相關文章
相關標籤/搜索