Skip to content

a starter project wrote with angular using es6 and bundled with webpack.

Notifications You must be signed in to change notification settings

jonelovemira/weaGo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

前端工程开发方法

  • 需要具备的知识
  • 背景介绍
  • 安装使用
  • 开发配置
  • 开发
  • 生产配置

需要具备的知识

使用此项目之前,你需要掌握以下概念的一些入门级的知识,如果你还没有了解,可以通过推荐的链接进行学习。

背景介绍

一个前端工程基本上只需要HTML,JS,CSS三种类型的文件。其中html控制dom结构,CSS控制样式,JS控制逻辑。通过分工合作提供页面展示与交互满足功能需求。

传统的开发方式比较简单,直接按需开发HTML,CSS,JS即可。这样虽然自由度高,勉强满足需求,但是容易随着业务的增加,导致可用性低,难以维护等的问题。

随着技术的迭代,我们可以使用一些流行的开源框架,让我们能够专注于自己的业务,而不是基础代码的编写。

angular就是其中的佼佼者。这个MVVM的框架提供了很多特性可以让开发人员开发出高可用的代码。它将不同的业务分割成不同的模块,controller绑定视图与数据模型;service声明可复用的逻辑过程;directive定制可复用的组件,filter编写模型数据过滤器等等。当然最强大的,是广大的开源生态库,提供满足各种需求的开源组件。

我们使用angular作为前端开发框架,开发完成形成的源码放到后端http服务器内,浏览器就可以正常下载运行了。

如果对前端的代码进行一些优化(雅虎前端优化原则),我们可以在前端开发完成之后,使用开源库,例如任务流工具gulp, grunt以及 打包工具webpack等等,有针对性的做一些优化比如压缩代码,打包等可以使得前端代码获得更好的用户体验。

如果我们将开发和优化分为两个时间段,前端工作可以分为开发和编译两个阶段。我们可以在开发阶段按照自己的方式组织代码(webpack真自由),例如以文件为单位形成最小粒度的模块,交代文件(即模块)的依赖关系。然后在编译阶段,由工具分析依赖关系,将各个不同的文件模块使用不同的编译方法(其实就是使用webpack对不同的文件使用不同的loader),最后结合在一起组成配置好的入口文件,最终生成我们需要的HTML,CSS,JS等前端文件。

如果配置妥当,你可以不用担心代码逻辑一致性的问题。这个由使用的开源库控制,通常都已经经过了广泛的验证,可以放心使用。像前端的ES6规范的代码通过babel之后转换成ES5能够兼容更多的浏览器,babel在github上有22k+的star,可靠性比我们自己写的好。

同时由于分离成两个阶段,我们可以在开发阶段做更多的事情。前端界出现了很多开源库来辅助前端开发,帮助我们在真实部署前发现问题,这一点很重要,提前发现前端存在的问题,而不是在和后端联调时发现。一个有效的方法是在本地开设http服务器,这个服务器完全可以模拟生产环境,将前端与后端分离开来,做好代理,透明中转请求到后台服务地址(也可能是mock地址)。如果前后端都依赖的是接口,那么部署的时候就可以无缝切换了。

这一切都只需要安装一些开源工具,进行一次性的配置而已,意味着配置的代码可以项目间复用。

安装使用

1. 安装node

根据运行平台下载安装,地址在此

2. 全局安装gulp

全局安装最新版的gulp,支持ES6,使用方法在此

npm install gulp -g

可以使用以下命令更换成为淘宝源。

npm install gulp -g --registry=https://registry.npm.taobao.org

或者直接安装淘宝源使用工具cnpm,以后使用npm的时候都可以cnpm代替。教程在此

3. 安装开发依赖。

安装当前项目下的库依赖,都只是开发时使用。如果安装了cnpm,可以使用cnpm代替。

npm install

4. 开启本地服务器。

使用配置好gulp的任务,开启本地http服务器。在此步骤中使用webpack编译代码,同时附带热更新功能。

gulp serve

5. 开发-热更新

更改本地的前端源码,保存时可以触发热更新,无需重启服务器即可看到更改效果。注意:build下的配置文件的更改不能触发热更新,需要重启服务器。重启操作即关闭4. 中开启服务器,重新gulp serve即可。

6. 生成生产代码

开发完成,自测完毕之后,运行以下代码可以在target文件夹中生成生产用的代码。具体配置项可以看环境配置中的生成配置。

gulp build

7. 发布

拷贝target文件夹下的文件至生产http服务器根文件夹下开放访问。

开发时配置

这部分描述主要是辅助读懂build文件夹下的各个配置文件,如果想直接进入开发,可以直接跳过此小节,直接去开发部分。

使用gulp serve之后发生了什么事情?如何实现我们所有的想法?

1. 从抛弃ES5, 拥抱ES6开始。

前端开发代码要使用ES6规范,于是为了保证JS风格一致,我们在写gulp任务逻辑的时候也使用babel来进行转码。在项目根目录下保存以下内容为文件.babelrc,内容如下:

{
  "presets": ["es2015"]
}

可以在这个文件中指定转码规则和插件。具体看这里,又是阮老师的。

2. serve任务

接下来就是写serve任务的逻辑,根据背景描述,我们需要一个工具满足以下需求:

  • 本地服务器,提供自测环境
  • webpack 太优秀,我们想使用各种webpack模块化打包的各种优良特性。
  • 透明中转代理
  • 高效开发,文件更改自动触发浏览器更新,代码修改可以热更新。

看似复杂,其实业界已经有了成熟的开源工具,即browsersync。

3. browsersync配置

具体可以看./build/serve.js,代码中已经做了描述。具体的配置项可以看官网地址。有一个配置在这里解释一下, 里面的server选项:

server: {
    baseDir: config.PATH_SRC,
    routes: {
        // 本地打包测试
        '/local-build': 'target'
    }
}

这个配置主要是用来辅助从本地访问target文件夹。如果只指定baseDir,那么只能访问baseDir下的文件,不能访问baseDir兄弟目录下的文件。所以配置一个route选项,访问target目录下的index.html时可以使用http://localhost:8180/local-build/index.html来访问。

4.webpack开发配置

开启browsersync之后,会触发webpack使用配置对代码进行编译。围观整个编译流程,其实就是依赖分析的流程。通过入口文件E,获取E依赖的A,B,C,然后再分析A,B,C的依赖。所有的依赖均是以文件为单位的模块。编译的时候加载这些模块,webpack可以使用不同的loader来进行处理,例如js文件,我们使用babel进行转码,scss文件,我们使用sass编译器进行转换成为css。具体更加详细的可以查看以下两个文件。

./build/webpack.dev.config.js
./build/webpack.config.js

将公共部分配置提取出来形成一个基础配置。基于这个基础配置在不同使用场景再做详细的更改。更多资料可以看这里, 还有dev-tool

  • 垫片babel-polyfill

有关babel还需要做一些额外的配置,即垫片babel-polyfill,需要作为依赖引入,垫片的作用,请看这里, 主要是为了能够使用更多的ES6规范的API。方法如下:

// ./build/config.js 引入
WEBPACK_FRAMEWORK: ['babel-polyfill', 'framework_js'],

// ./build/webpack.config.js 声明成为入口文件
entry: {
    framework: config.WEBPACK_FRAMEWORK,
    paspui: config.WEBPACK_PASPUI,
}

// ./build/webpack.dev.config.js 将入口文件作为引用插入入口html
new HtmlWebpackPlugin({
    template: config.PATH_SRC + '/index.html',
    filename: config.PATH_SRC + '/index.html',
    inject: 'body',
    hash: true,
    chunks: ['framework', 'paspui', 'app'],
    chunksSortMode:  (a, b) => {
        if (config.WEBPACK_ENTRY_ORDER[a.names[0]] > config.WEBPACK_ENTRY_ORDER[b.names[0]]) {
            return 1;
        } else {
            return -1;
        }
    }
})
  • 全局声明替换变量

除此之外,有一个配置比较有用,即配置webpack 全局声明替换变量

因为开发阶段内的本地服务器代理的情况存在,我们在请求http接口时可能会带上某个prefix来触发使用代理,例如本项目中的如下配置:

// ./build/config.js
PROXY_PREFIX: 'local-proxy'

// ./build/serve.js
// 设置开发服务器的代理,请求中转至真实后台
proxyOptions = url.parse(config.API_URL);
proxyOptions.route = '/' + config.PROXY_PREFIX;

所以在开发阶段时可能某个接口路径为/local-proxy/login, 当我们真实部署使用时,可能不需要这个prefix,因为可能没有代理存在,是直接向本机请求,即/login。针对这种场景,我们需要使用webpack的全局声明替换变量,可以让使用者知道当前环境是开发环境还是生产环境。定义时如下:

// ./build/webpack.build.config.js
new webpack.DefinePlugin({ 'env':'"prod"' }),

// ./build/webpack.dev.config.js
new webpack.DefinePlugin({ 'env':'"dev"' }),

使用时可以这样:

url = env === 'dev' ? "/local-proxy" + '/login' : '/login';

于是在开发时,因为替换了env为'dev'(猜测是loader的时候替换),所以url取值是'/local-proxy/login', 生产环境中则env是'prod',所以url取值成为'/login'。

  • 一劳永逸的引入依赖

如何在统一入口文件中引入不同的子模块是一个问题。开发一个模块就更改一次入口文件显然比较麻烦。我们可以通过webpack的require.context来帮我们解决问题。如下:

/**
 * 子模块加载
 */
let dependencies = [];
const modules = require.context('./scripts/', true, /module.config.js$/);
modules.keys().forEach(key => {
    dependencies.push(modules(key).default);
});

/**
 * 页面加载
 */
let account = require.context('./scripts/pages/', true, /\.index.js$/);
account.keys().forEach(account);

/**
 * 应用初始化
 */
const app = angular.module('app', ['pasp.ui', ...dependencies]);

使用以上配置,我们可以引入不同的子模块,规则是需要在scripts下,文件名满足一个正则表达式。具体使用说明可以看这里,或者中文版

一切准备就绪,只等开发。

开发

在此简单介绍使用ES6风格代码写angular的app,同时介绍一些较好的开发实践。

子模块路由

不再是集中管理路由。由各个子模块配置路由, 在子模块的子页面(home.index.js)中,这就是高内聚,低耦合。一个例子如下:

import templateUrl from './template/home.html';
import controller from './home.controller';

angular.module('demo')
    .config(($stateProvider, $urlRouterProvider) => {
        "ngInject";
        
        $urlRouterProvider.otherwise('/');
        $urlRouterProvider.when('', '/');

        $stateProvider.state('main/home', {
            url: "/",
            templateUrl: templateUrl,
            controller: controller,
            controllerAs: 'vm',
            reloadOnSearch: false
        });
    });

controller

将controller封装成为一个类,然后导出

class Controller{
    constructor() {
        "ngInject";
    };
}

export default Controller;

这个类的成员变量都可以在页面视图html中通过vm.xxx来访问(vm为index中声明的controllerAs)。

directive

自定义组件同样使用类来封装,举例:

//download_tpl_button.directive.js
import templateUrl from './template/download_tpl_button.html';
import controller from './download_tpl_button.controller';

class DownloadTplButton {

    constructor($http) {
        this.restrict = "EA";
        this.replace = true;
        this.scope = {
            btnClass: "=",
            fileName: "=",
            processType: "="
        };
        this.templateUrl = templateUrl;
        this.controller = controller;
        this.controllerAs = "vm";
    }

    static factory($http) {
        "ngInject"; 

        //可以在此处注入依赖,然后从构造器中传入, 用$http示例
        return new DownloadTplButton($http);
    }

};

export default DownloadTplButton;

// download_tpl_button.index.js
import directive from './download_tpl_button.directive';

angular.module("ctg-iot.account")
    .directive("downloadTplButton", directive.factory);

// download_tpl_button.controller.js 使用双向绑定变量

class Controller{

    constructor(BillingBatchService, $scope) {
        "ngInject";

        this._billingBatchService = BillingBatchService;
        this._$scope = $scope;

        this.downloadUrl = this._billingBatchService.getDownloadTemplateUrl(this._$scope.processType, this._$scope.fileName);
    };
};

export default Controller;

// 视图html使用双向绑定变量

成员属性就是传统angular写法的各种属性,包括双向绑定类型等。需要注意的是需要声明一个类方法来作为工厂,生产实例。视图HTML中使用directive双向绑定变量和controller中为:

<a class="btn btn-primary margin-left-10 {{btnClass}}" ng-href="{{vm.downloadUrl}}" download="{{fileName}}.xls">
    <span><i class="fa fa-download"></i> 下载模板</span>
</a>

filter

filter声明方法可以使用简单的函数生成器完成,如下:

//adjust.result.filter.js

export default function() {
    return (input) => {
        let map = {
            created: "未处理",
            updated: "修改争议账单",
            merged: "合并当期账单",
            delivered: "待下发调账文件",
            finished: "已完成",
            recharged: "为赠金充值",
            failed: "调账失败"
        };

        return map[input] || 'Unknown';
    }
};

//common.index.js
import adjustResultStateFilter from './filters/adjust.result.filter';
angular.module('ctg-iot.account')
    .filter('adjustResultStateFilter', adjustResultStateFilter);

在视图中的使用方法如下:

<td>{{result.adjustResult | adjustResultStateFilter}}</td>

service

service同样是封装成为类,使用方法如下:

// global.state.saver.service.js
class Service {
    constructor() {
        this.args = undefined;
    }

    set(args) {
        this.args = args;
    }

    get() {
        return this.args;
    }
};

export default Service;

// common.index.js
import globalStateSaver from './global.state.saver.service';

angular.module('ctg-iot.account')
    .service('GlobalStateSaver', globalStateSaver)

// controller.js

class someCtrl{
    constructor('GlobalStateSaver') {
        "ngInject";
        this._GlobalStateSaver = GlobalStateSaver;
    };

    test() {
        this._GlobalStateSaver.set('test');
        console.log(this._GlobalStateSaver.get()); // 'test'
    }
}
...

provider

provider与service一样,不同的是如果需要注入$scope, $rootScope时不能在constructor中注入,需要在$get中注入,如下:

// test.provider.js
class TestProvider {

    constructor() {}

    $get($scope) {
        "ngInject"; 

        this._$scope = $scope;
        return this;
    }
}

export default TestProvider;

// index.js
import provider from './test.provider';

export default
angular.module('test', [])
    .provider('TestProvider', provider)
    .name;

视图模板引用

angular特性使得模板可以简单复用,当然如果要复用逻辑与模板,使用自定义指令directive会更好。使用方法如下:

// controller.js

import commonTemplateUrl from '../common/template.html';

class ctrl {
    constructor() {
        this.template = commonTemplateUrl;
    }
}
...

使用时就可以把template当作一个双向绑定的变量(事实上就是绑定到了模板的相对位置).利用angular的ng-include,如下:

<ng-include src="vm.template"></ng-include>

生成配置

开发自测完成之后,我们需要webpack编译出来的文件作为生产版本文件。因为自测时使用的配置webpack只是生存文件在内存中(神奇不),所以需要专门定制新的配置将编译完成的(可以在浏览器中运行的代码)代码导出为文件。所以配置文件与开发时有所区别,具体可以关注文件./build/build.js。

运行脚本命令gulp build生成的文件存放在target文件夹,把这些文件放置在CDN或者http服务器中开放访问即完成了整个前端的工作。

About

a starter project wrote with angular using es6 and bundled with webpack.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published