美文网首页
[攻略]nej regular环境使用es6的低成本方案

[攻略]nej regular环境使用es6的低成本方案

作者: 小人阳 | 来源:发表于2017-09-20 14:15 被阅读0次

    希望在生产环境中使用es6/7,babel应该是最普遍的选择。这是babel官网中,它对自己的定义:

    Babel 自带了一组 ES2015 语法转化器。这些转化器能让你现在就使用最新的 JavaScript 语法,而不用等待浏览器提供支持。

    babel就像一个javascript文件预处理器,你可以自由使用es6/7语法,不用当心兼容性问题,因为浏览器中运行是babel为你处理妥帖的代码。为了方便使用,它提供了许多使用方法:webpack、gulp、browserify、grunt......

    通过哪种方式来在当前技术栈(nej+regular+stateman)中使用babel,是一个值得深思熟虑问题,而没有经过深思熟虑就试图使用webpack的我,一度掉进了一个坑中:

    webpack + babel 的踩坑过程

    webpack应该是目前最流行的构建工具,关于它和babel的使用方法,网上的资料汗牛充栋,前人对各种可能发生的问题(比如ie8的兼容)基本都有了解决方法,换句话说就是这条路的坑比较少。

    但是对我们而言,webpack+babel的方式存在以下几个问题:

    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-module

    1. ftl注入,多入口。这两个问题可以从考拉的一篇文章中找到答案。

    除此之外,还遇到一个比较特殊的问题,webpack无法识别我们历史代码中的一个文件。

    综上所述,如果有着丰富的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的低成本方案

          本文链接:https://www.haomeiwen.com/subject/yphosxtx.html