公司前端开发架构改造

要看更多的文章,欢迎访问个人我的博客: http://oldli.netjavascript

如今的前端早已不是几年前的前端,不再是jQuery加一个插件就能解决问题的时代。php

最近对公司前端的开发进行了一系列的改造,初步达到了我想要的效果,可是将来还须要更多的改进。最终要完美的实现目标:工程化模块化组件化css

这是一个艰难的,持续的,不断进化的过程!html

先说下我司前端改造前的状况:前端

开始的时候,只有微信公众号开发,以及在APP中嵌入的Web页面,只须要考虑微信端的问题,以及跟原生APP的交互处理,一切都好像很完美。java

几个月后,要开发手机网页版,接着百度直达号也来了。原来微信端的功能已经比较多,不可能针对这2个端再从新开发,因而把微信端的代码拷贝2份,而后修改一下就上线了(初创公司功能要快速上线,有时不会考虑太多的技术架构,都懂的)。node

这样就出现了6个不一样的项目文件夹,为何会是6个呢?由于也分别拷贝出了各自的测试目录:webpack

/wap
/waptest
/m
/mtest
/baidu
/baidutest

因而,问题就来了,开发的时候,都在其中一个test目录下开发,好比waptest,开发测试没问题了,就拷贝修改的代码到其它的目录。这样开发的痛苦,可想而知了。git

不只仅是各个端的js,css,images文件是分别存放,还有各个端的页面模板也是在各自的目录下。 github

另外,一直以来,公司的前端美女会使用grunt作一些简单的前端构建,好比sass编译,css合并等,但离我想要的前端自动化/工程化仍是有点远。

为了提升前端的工做效率,最近终于有一点时间腾出手来处理这些问题。

PS:咱们团队组建一年多,项目也从0开始,到如今为止,产品/开发/项目管理等都在逐渐完善。走专业化道路,是我一直以来的目标,有兴趣的能够加我一块儿交流!

问题总结

先来总结一下改造前前端开发存在的问题:

  1. 同时存在多端,形成开发效率不高

  2. 项目没有模块化,组件化的概念,代码复用率低

  3. 部署困难,没有自动生成版本号,每次都要手动修改js的版本号

  4. 面条式的代码,开发任务重,没有作很好的规划

改进目标

有问题,那就想办法去解决它:

  1. 解决多端统一的问题,一处修改,多端同时生效

  2. 模块化开发,使代码逻辑更加清晰,更好维护

  3. 组件化开发,加强扩展性

  4. 按需打包,以及自动构建

  5. 自动更新js版本号,实现线上自动更新缓存资源

  6. 紧跟发展趋势,使用ES6进行开发

在改进的过程当中,会用到2个工具: GulpWebpack。用这2个工具,也是有缘由的。

原本我想在Grunt的基础上利用Browserify进行模块化打包,可是发现打包的速度太慢,个人Linux系统编译要4s以上,美女前端的Widnows系统通常都要7s以上,这简直不能忍受。在试用Gulp以后发现速度杠杠的,不用想了,马上替换Grunt。至于Webpack,是由于用browserify打包多个入口的文件配置比较麻烦,在试用了Webpack以后,发现Webpack的功能比browserify强大不少,因而就有了这2个工具的组合。Webpack的配置比较灵活,可是带来的结果就是比较复杂,在项目中,我也仅仅用到了它的模块化打包。

因而,最终初步实现前端构建的方案是:

Gulp进行JS/CSS压缩,代码合并,代码检查,sass编译,js版本替换等,Webpack只用来进行ES6的模块化打包。

webpack

如今前端的操做很简单:

开发的时候,执行如下命令,监听文件,自动编译:

$ gulp build:dev

开发测试完成,执行如下命令,进行编译打包,压缩,js版本替换等:

$ gulp build:prod

今后,前端开发能够专心地去写代码了!

方案实现

项目结构

整个项目是基于Yii2这个框架,相关的目录结构以下:

common/
    pages/
        user/
        index/
        cart/
wap/
    modules/
        user/
        index/
        cart/
    web/
        dev/
            index/
            user/
            cart/
            common/
            lib/
        dist/
        logs/
        gulp/
            tasks/
            utils/
            config.js
            config.rb
    node_modules/
    index.php
    package.json
    gulpfile.js
    webpack.config.js
    .eslintrc
  • common/pages存放公共模板,各个端统一调用

  • web/dev是开发的源码,包含了js代码,css代码,图片文件,sass代码

  • web/dist是编译打包的输出目录

统一多端的问题

因为多端的存在,致使开发一个功能,要开发人员去手动拷贝代码到不一样的目录,同时还要针对不一样的端再作修改。

js文件,css文件,图片文件,还有相关的控制器文件,模板文件都分散在不一样的目录,要拷贝,耗时间不说,并且容易出错遗漏。

要解决这个问题,有2种方法:

  • 全部端调用公共的文件

  • 在某个端开发,开发完成以后,用工具自动拷贝文件,而且自动替换相关调用

在综合考虑了以后,这2种方法同时使用,模板文件多端公共调用,其它的文件,经过命令自动拷贝到其它端的目录。

公共模板放到目录common/pages,按模块进行划分,重写了下Yii2的View类,各个端均可以指定是否调用公共模板。

public function actionIndex()
{
    $this->layout = '/main-new';
    return $this->render('index', [
        '_common' => true,    // 经过该值的设置,调用公共模板
    ]);
}

模板一处修改,多端生效。

另外,其它文件经过gulp去拷贝到不一样的目录,例如:

$ gulp copy:dist -f waptest -t wap

这里有一个前提就是,全部编译打包出来的文件都是在dist文件夹,包含了js代码,css代码,图片文件等。

组件化开发

这个只能说是将来努力的一个目标。现阶段还没能很好地实现。这里单独列出这一点,是但愿给你们一点启发,或者有哪路高手给我一点建议。

看了网上诸路大神的言论,总结了下前端的组件化开发思想:

  • 页面上的每一个独立的可视/可交互区域视为一个组件

  • 每一个组件对应一个工程目录,组件所需的各类资源都在这个目录下就近维护

  • 组件与组件之间能够 自由组合

  • 页面只不过是组件的容器,负责组合组件造成功能完整的界面

  • 当不须要某个组件,或者想要替换组件时,能够整个目录删除/替换

其中,各个组件单独的目录,包含了js代码,样式,页面结构。这样就把各个功能单元独立出来了。不只维护方便,并且通用性高。

最终,整个Web应用自上而下是这样的结构:

图片描述

模块化开发

前端开发的代码从开始到如今,经历了3个阶段:

  • 第一阶段,面条式代码,在每一个模板页面写上一堆js代码,执行的代码跟函数代码交替出现,多重复代码

  • 第二阶段,进化到了Object,每一个模板页面的js都用Object进行封装一次

  • 第三阶段,引入ES6的模块化写法

在以前,前端都按下面的目录存放文件:

js/
    goods-order.js
    package-order.js
    index.js
    user.js
    zepto.js
    crypto-js.js
images/
    goods.jpg
    logo.png
    footer.png
css/
    header.css
    footer.css
    goods-order-index.css

这样会致使一个目录下会有不少文件,找起来很是不方便,并且结构不清晰,比较杂乱。

如今,在目录web/dev分开不一样的目录存放各个模块的代码以及相关文件,web/dist是编译打包出来的文件存放的目录。如:

dev/
    lib/
        zepto.js
        crypto-js.js
    common/
        js/
            request.js
            url.js
        scss/
            head.scss
    order/
        goods-order/
            index.js
            index.scss
            a.png
        package-order/
            index.js
            index.scss
dist/
    lib/
        lib.js
    order/
        goods-order/
            index.50a80dxf.js
            a.png

其中,有2个重要的目录:

  • lib/目录存放第三方库,编译的时候合并并压缩为一个文件,在页面上直接引入,服务端开启gzip以及缓存

  • common/目录存放公共的模块,包括js的,还有sass等,其它模块目录的js,sass能够调用这些公共的模块

其它的目录都是单独的一个模块目录,根据业务的状况划分,每一个模块目录把js,sass,图片文件都放一块儿。

这样的结构清晰明了,极大地提升了可维护性。

至于JS代码的模块化,恰好去年发布了ES6,并且各大框架/类库/工具等都支持ES6的特性,显然这是将来的一种趋势,相比之前的CMD/AMD/CommonJS规范,选择ES6会更加的符合时代的发展。

ES6支持Class以及继承,以及使用import来引入其余的模块,使用起来很简单。

至于CSS的模块化,以前是使用Compass来写CSS,在本次改造中,还没作太多的处理,只是由原来的grunt编译该为用gulp编译。可是compass已经好久没有更新了,并且不建议使用它。之后会逐渐替换掉。

模块化打包

因为使用了ES6的模块化写法,须要引入Webpack进行编译打包,我是gulp与webpack配合使用。

var gulp = require('gulp');
var webpack = require('webpack-stream');
var changed = require('gulp-changed');
var handleError = require('../utils/handleError');
var config = require('../config');
 
gulp.task('webpack', function() {
    return gulp.src(config.paths.js.src)
        .pipe(changed(config.paths.js.dest))
        .pipe(webpack( require('./../../webpack.config.js') ))
        .on('error', handleError)
        .pipe(gulp.dest(config.paths.js.dest));
});

webpack的配置以下:

var webpack = require('webpack');
var path = require('path');
var fs = require('fs');
var assetsPlugin = require('assets-webpack-plugin');
var config = require('./gulp/config');
var webpackOpts = config.webpack;
 
var assetsPluginInstance = new assetsPlugin({
    filename: 'assets.json',
    path: path.join(__dirname, '', 'logs'),
    prettyPrint: true
});
 
var node_modules_dir = path.resolve(__dirname, 'node_modules');
var DEV_PATH = config.app.src;      // 模块代码路径,开发的时候改这里的文件
var BUILD_PATH = config.app.dest;   // webpack打包生成文件的目录
 
/**
 * get entry files for webpack
 */
function getEntry()
{
    var entryFiles = {};
    readFile(DEV_PATH, entryFiles);
    return entryFiles;
}
 
function readFile(filePath, fileList)
{
    var dirs = fs.readdirSync(filePath);
    var matchs = [];
    dirs.forEach(function (item) {
        if(fs.statSync(filePath+'/'+item).isDirectory()){
            readFile(filePath+'/'+item, fileList);
        }else{
            matchs = item.match(/(.+)\.js$/);
            if (matchs) {
                key = filePath.replace(DEV_PATH+'/', '').replace(item, '');
                if(!key.match(/^lib(.*)/) && !key.match(/^common(.*)/)){
                    fileList[key+'/'+matchs[1]] = path.resolve(filePath, '', item);
                }
            }
        }
    });
}
 
var webpackConfig = {
    cache: true,
    node: {
        fs: "empty"
    },
    entry: getEntry(),
    output: {
        path: BUILD_PATH,
        filename: '',
        // publicPath: '/static/assets/',
    },
  
    externals : webpackOpts.externals,
 
    resolve: {
        extensions: ["", ".js"],
        modulesDirectories: ['node_modules'],
        alias: webpackOpts.alias,
    },
 
    plugins: [
        assetsPluginInstance,
        new webpack.ProvidePlugin(webpackOpts.ProvidePlugin),
    ],
 
    module: {
        noParse: webpackOpts.noParse,
        loaders: [
            {
                test: /\.js$/,
                loader: 'babel',
                exclude: [node_modules_dir],
                query: {
                    presets: ['es2015'],
                }
            },
        ]
    }
};
 
if(process.env.BUILD_ENV=='prod'){
    webpackConfig.output.filename = '[name].[chunkhash:8].js';
}else{
    webpackConfig.output.filename = '[name].js';
    webpackConfig.devtool = "cheap-module-eval-source-map";
}

module.exports = webpackConfig;

入口文件

项目的入口文件都放在/web/dev下面,根据业务特色来命名,好比:index.jspay.js

webpack.config.js文件,能够经过getEntry函数来统一处理入口,并获得entry配置对象。若是你是多页面多入口的项目,建议你使用统一的命名规则,好比页面叫index.html,那么你的js和css入口文件也应该叫index.jsindex.css

资源映射记录

因为编译出来的文件是带有版本号的,如select-car.b9cdba5e.js,每次更改JS发布,都必需要替换模板页面的script包含的js文件名。

我用到了assets-webpack-plugin这个插件,webpack在编译的时候,会生成一个assets.json文件,里边记录了全部编译的文件编译先后的关联。如:

{
  "store/select-store": {
    "js": "store/select-store.54caf1d3.js"
  },
  "user/annual2/index": {
    "js": "user/annual2/index.2ff2c11d.js"
  },
  "user/user-car/select-car": {
    "js": "user/user-car/select-car.cd0f5f41.js"
  }
}

这个插件只是生成映射文件,还须要用这个文件去执行js版本替换。看下面的自动更新缓存。

定义环境变量

在开发的时候,编译打包的文件跟发布编译打包出来的文件确定不同,具体能够参考构建优先的原则

在gulp的build:devbuild:prod命令里边,会设置一个环境变量:

gulp.task('build:dev', function(cb){
 
    // 设置当前应用环境为开发环境
    process.env.BUILD_ENV = 'dev';
 
    //... ...
});

gulp.task('build:prod', function(cb){

    // 设置当前应用环境为生产环境
    process.env.BUILD_ENV = 'prod';

    //... ...
});

而后在webpack里边根据不一样的环境变量,来进行不一样的配置:

if(process.env.BUILD_ENV=='prod'){
    webpackConfig.output.filename = '[name].[chunkhash:8].js';
}else{
    webpackConfig.output.filename = '[name].js';
    webpackConfig.devtool = "cheap-module-eval-source-map";
}

自动更新缓存

一直以来,咱们修改js提交发布的时候,都须要手动去修改一下版本号,如:

当前线上版本:

<script src="/js/user-car.js?v=1.0.1"></script>

待发布版本:

<script src="/js/user-car.js?v=1.1.0"></script>

这样如今看起来好像没有什么问题,惟有的问题就是每次都要手动改版本号。

可是,若是之后要对静态资源进行CDN部署的时候,就会有问题。通常动态页面会部署在咱们的服务器,静态资源好比js,css,图片等会使用CDN,那这时候是先发布页面呢,仍是先发布静态资源到CDN呢?不管哪一个前后,都会有个时间间隔,会致使用户访问的时候拿到的静态资源会跟页面有不一致的状况。

以上这种是覆盖式更新静态资源带来的问题,要解决这个问题,可使用非覆盖式更新,也就是每次发布的文件都是一个新的文件,新旧文件同时存在。这时能够先发布静态资源,再发布动态页面。能够完美地解决这个问题。

那么,咱们要实现的就是每次开发修改js文件,都会打包出一个新的js,而且带上版本号。

webpack中能够经过配置output的filename来生成不一样的版本号:

webpackConfig.output.filename = '[name].[chunkhash:8].js';

有了带版本号的js,同时也生成了资源映射记录,那就能够执行版本替换了。

在网上看了下别人的解决方案,基本上都说是用到webpack的html-webpack-plugin这个插件来处理,或者用gulp的gulp-revgulp-rev-collector这2个插件处理。可是我感受都不是很符合咱们项目的状况,并且这个应该不难,就本身写了一个版本替换的代码去处理。这些插件后续有时间再研究研究。

在页面模板上,咱们经过下面的方式来注册当前页面的使用的js文件到页面底部:

<?php
$this->registerJsFile('/dist/annual2/index/index.js');
?>

每次用gulp执行版本替换的时候, 会先读取资源映射文件assets.json,拿到全部js的映射记录。

var assetMap = config.app.root + '/logs/assets.json';
var fileContent = fs.readFileSync(assetMap);
var assetsJson = JSON.parse(fileContent);
function getStaticMap(suffix){
    var map = {};
    for(var item in assetsJson){
        map[item] = assetsJson[item][suffix];
    }
    return map;
}
var mapList = getStaticMap('js');

而后再读取模板文件,用正则分析出上面注册的js文件,最后执行版本替换就好了。

一些要点

使用externals

项目通常会用到第三方库,好比咱们会用到zeptojsart-templatecrypto-js等。

单独把这些库打包成一个文件lib.js,在页面上用script标签引入。

这能够经过在webpack中配置externals来处理。

sourcemap

在开发环境下,能够设置webpack的sourcemap,方便调试。 可是webpack的sourcemap模式很是多,哪一个比较好,还没什么时间去细看。 能够参考官方文档

最后

至此,前端项目的第一阶段的改造算是完成了。 我不是前端开发,gulpwebpack都是第一次接触而后使用,中间踩了很多的坑,为了解决各类问题,差很少把google都翻烂了。不过庆幸的是,如今前端开发能够比较顺畅地去写代码了。整个结构看起来比之前赏心悦目了很多。

我以为此次改造最大的变化不是使用了2个工具使到开发更自动化,而是整个开发的思想与模式都从根本上发生了变化。将来还会继续去作更多的探索与改进。

各位看官对前端开发有更好的建议或者作法,欢迎随时跟我交流。

相关文章
相关标签/搜索