基于gulp+webpack的"约定大于配置"的构建方案探讨
发布在gulp+webpack的前端构建方案探讨2015年10月26日view:4561自动化
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

这不到半年的时间,玩了很多东西。有些觉得不错的新技术,直接拿到公司的项目里去挖坑。感觉进步很大,但是看看工程,啥都有。单看模块管理,从遗留的requirejs,到我过来改用的browserify,以及现在的es6 module,都有,乱糟糟的感觉。然后有天老大发现:现在发布,前端的构建时间比后端还长。重新做构建吧。

示例代码

demo在github上,自取。

假设与前提

我非常推崇分治的开发方式。改了一个页面,对其他页面最好不要产生任何影响。开发环境也可以单独针对某个页面,不需要去编译打包其他页面的东西。这就要求,除了一些基本不会改变的公用js框架,js库,公用样式,以及logo图片之类的东西,其他的代码,每个页面的代码完全独立,最好从文件夹层面的分离。这样的话,比如有个页面,如果不再需要,直接把文件夹删就ok了。

demo的目录结构是这样的

dir

两个页面,一个index,一个contact。各自需要的所有代码,资源文件,全在自己的目录。公用的东西在lib里。

所以这里假设的构建方案是:

  1. 多个页面,每个页面相互独立,如果页面不要了,直接删了文件夹就ok。

  2. 开发时,只构建自己的东西,因为如果项目有20,30个页面,我现在只开发index,打包、watch其他页面的代码,会影响我的开发效率。

  3. 发布的时候,全量构建.

  4. 构建的文件路径映射,给出map.json(我命名为assets-map.json)文件,供路径解析用。

约定大于配置

有使用后端开发框架的同学,应该都知道这个说法。只要按照一定的约定去写代码,框架会帮你做一个自动的处理。比如以文件名以controller结尾的,是控制器,然后对应的路由会自动生成等。

很早之前我就在想,能不能前端也有约定大于配置的构建方案。我觉得各大公司肯定有相应的方案,但是我没见到。我希望一套方案,直接拿过去,npm一下,按照相应的约定去写,构建自动完成。

这里托webpack的福,能比较容易的做出我满意的方案。webpack以模块为设计出发点,所有资源都当成模块,css,js,图片,模版文件等等。

// webpack is a module bundler.

// This means webpack takes modules with dependencies

// and emits static assets representing those modules.

所以实际上,我需要知道每个页面的入口文件,就能自动构建每个页面的所有代码和资源。(这么一说,我好像什么也不用做-_-!)。然后配合gulp,去动态微调一些配置。gulp + webpack的基本玩法就是 配置一个基础的webpackConfig,gulp的task里,根据需要,动态微调基本的webpackConfig。

具体使用

首先看代码怎么写。既然分治了,那么先只看index文件夹。目录结构说明如下:

index
    img   -- 文件夹。是人都知道这个文件夹干嘛
    js    -- 文件夹。所以
    less  -- 文件夹。就不侮辱大家的智商
    test  -- 一些测试,这里偷懒,里面啥也没有
    tools -- 开发环境工具
    index.entry.js -- 入口文件

规定所以入口文件都是*.entry.js,这就是唯一的约定(后面构建时,会找出所以这样命名规则的文件)。当然主要是webpack做了太多的工作。

看一下index.entry.js的代码

import ReactDom from 'react-dom'
import IndexComponent from './js/IndexComponent.js'

import './less/index.less'

ReactDom.render(
    (
        <div>
            <IndexComponent/>
            <div className='avatar'/>
        </div>
    ),
    document.getElementById('mount-dom')
)

setTimeout(function(){
    require.ensure([],function(){
        require('./js/async.js')
    })
},1000)

使用es6语法,先各种import引入依赖(注意reactreact-dom会放到lib里,后面说)。包括js,less,setTimeout模拟按需异步加载js文件。其中index.less里有样式引用img里的图片

//index.less的代码
.avatar{
    background:url(../img/touxiang.jpg) no-repeat;
    height: 100px;
    width: 100px;
    background-size: 100%;
}

执行完构建后,assets\dist下,你会看到

//[hash]为文件的hash,这里写成占位符。
index.entry-[hash].js
index.entry-[hash].css  
img/[hash].jpg  

assets\assets-map.json,有路径的映射。(这里用的file-loader处理图片,实际url-loader更好,不过用法上,一般就加一个limit,这里就不赘述了)

单个页面实现

对webpack熟悉的同学,应该会觉得这很普通。类似下面的配置:

entry: {'/index.entry':"./assets/src/index/index.entry.js"},
output: {
    filename: "[name]-[chunkhash].js",
    chunkFilename:'[name].js',
    path: __dirname + "/dist",
    libraryTarget:'umd',
    publicPath:''
},
externals:{
    'react': {
        root: 'React',
        commonjs2: 'react',
        commonjs: 'react',
        amd: 'react'
    },
    'jquery': {
        root: 'jQuery',
        commonjs2: 'jquery',
        commonjs: 'jquery',
        amd: 'jquery'
    }
}
module: {
    loaders: [
        {
            test: /[\.jsx|\.js ]$/,
            exclude: /node_modules/,
            loader: "babel-loader?stage=0&optional[]=runtime"
        },
        {
            test: /\.css$/,
            loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
        },
        {
            test: /\.less$/,
            loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!less-loader')
        },
        { test: /\.(png|jpg|gif)$/, loader: 'file-loader?name=img/[hash].[ext]' }
    ]
},
devtool:'source-map',
plugins: [
    new ExtractTextPlugin("[name].css"),
    new webpack.optimize.UglifyJsPlugin({
        mangle: {
            except: ['$', 'exports', 'require']
        }
    }),
    assetsPluginInstance
],

都是些常用的配置和插件,有几点需要的注意的地方

  1. output的filename要有[chunkhash],使用[hash]的话,同一次构建,不同的entry文件,会是同一个hash.原因看文档
  2. 使用了assets-webpack-plugin生成文件的路径映射。
  3. externals把公用的库排除掉。公用库会去生成lib.js,lib.css

多个页面实现

那多个页面,怎么去实现一起构建呢。 上面的entry里配置项里只有一个index.entry,如果有两个当然就生成两个页面的代码和资源。类似这样

{ 
'/contact.entry': './assets/src/contact/contact.entry.js',
'/index.entry': './assets/src/index/index.entry.js'
}

还记得我们的约定吗(说着有点怪。。),选出来所有的*.entry.js文件,稍作处理就好了。

var entries = {}

var entryFiles = glob.sync('assets/src/**/*.entry.js');

for(var i = 0;i<entryFiles.length;i++){
    var filePath = entryFiles[i];
    key = filePath.substring(filePath.lastIndexOf(path.sep),filePath.lastIndexOf('.'))
    entries[key] = path.join(__dirname,filePath);
}

var config = _.merge({},webpackConfig)
config.entry=entries

上面的代码就是生成一个键值对(key-value pair),key形如 /*.entry,value是入口文件的路径。生成完了,设置给config.entry.

lib的处理

lib实际上就是把上面exteranls里的东西,统一打个包。 看gulpfile.js 里的lib task,就是把external设成{}.

lib.js的代码

import React from 'react'
import jQuery from 'jquery'
import ReactDOM from 'react-dom'

import './reset.less'

window.React = React
window.jQuery = jQuery
window.$ = jQuery
window.ReactDOM = ReactDOM

就是把之前排除掉公共的东西,都import进来,另外加点全局的样式。

为啥不用CommonsChunkPlugin?因为这些东西很明显是属于lib的,不用每次都去构建不需要构建的代码。

运行 gulp lib后,dist下,就会生成lib-[hash].jslib-[hash].css

====

实际上,基本的构建已经完成了。对照下上面说的4点

1.多个页面,每个页面相互独立,如果页面不要了,直接删了文件夹就ok。

index 和 contact的所有东西都是独立的。这点没问题。

2.开发时,只构建自己的东西,因为如果项目有20,30个页面,我现在只开发index,打包、watch其他页面的代码,会影响我的开发效率。

开发环境后面说

3.发布的时候,全量构建. 发布包括:发布lib.js,发布所有页面的静态文件。gulp的default task先执行lib task,然后自己打包所有页面的资源。

4.构建的文件路径映射,给出map.json(我命名为assets-map.json)文件,供路径解析用。 已经有了,在/assets/assets-map.json里。

关于assets-map.json

这里有个细节注意一下,assets-webpack-plugin这个插件,默认是把json文件覆盖掉的。对于本demo,lib和其他是分开,lib先执行,所以默认lib相关的路径映射会被覆盖。不覆盖有两个条件

  1. 设置属性{update:true}
  2. 同一个插件实例

代码如下:

var AssetsPlugin = require('assets-webpack-plugin');

var assetsPluginInstance = new AssetsPlugin({filename:'assets/assets-map.json',update: true,prettyPrint: true})

//然后配置里,plugins加入assetsPluginInstance,这样gulp lib task 和 default task里的assetsPluginInstance是同一个对象。

有了这个映射文件,就可以自动生成路径了。

//getStatic.js 可以直接执行node getStatic.js看结果。

//执行gulp后,生成assets/assets-map.json后,执行下面的命令
var fs =require('fs')
var path = require('path')
var fileContent = fs.readFileSync(path.join(__dirname,'assets/assets-map.json'))
var assetsJson = JSON.parse(fileContent);

function getStatic(resourcePath){
    var lastIndex = resourcePath.lastIndexOf('.')
    var name = resourcePath.substr(0,lastIndex),
        suffix = resourcePath.substr(lastIndex+1);
    if(name in assetsJson){
        return assetsJson[name][suffix]
    }else{
        return resourcePath;
    }
}

console.log(getStatic('/lib.js'))
console.log(getStatic('/index.entry.css'))  

以express + jade 为例

app.locals.getStatic = function(path){
    if(isProdction){
        return getStatic(path) 
    }else{
        //开发环境,return localPath..
    }
}

然后模板里这样使用。

script(src=getStatic('/lib.js'))

运行demo

说了半天,到现在没有任何可以看的效果。其实最大的效果,都在assets/dist里。不过为了大家看效果,多写点,能够运行。运行方式:

github上clone下来,然后

npm i
gulp
node index.js

浏览器打开 http://localhost:3333/contact.html http://localhost:3333/index.html

效果很简单(简直是粗糙),但是构建的很多方面都有涉及了。

看一下assets/dist里的index.htmlcontact.html,是完全静态的页面。如果不需要首屏数据,都是通过ajax生成的话,这就是一个完全静态化的方案。只需要nginx 指向dist文件夹下,就发布好了。这里为了大家运行方便,就express 去做静态文件服务器,道理是一样的。

构建完全静态化的东西,难点只有一个,就是路径的问题。找webpack的插件吧。这里使用:html-webpack-plugin.它会根据模板,自动在head里插入chunks的css,在body底部插入chunks的js。刚开始的时候,我使用模板文件(路径./assets/webpack-tpl.html’)去生成。

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>webpack coc</title>
    <!--lib是所有页面公用的。-->
    <!--需要自动生成一下-->
    <link href="/lib.css" rel="stylesheet">
</head>
<body>
    <div id="mount-dom"></div>
    <script src="/lib.js"></script>
</body>
</html>

生成后,index.htmlcontact.html会插入相应模块的js和css。但是lib呢,怎么把hash加上?我写了个简单的插件。webpack的插件,最简单的,就是一个function

//帮助函数
function getTplContent(libJs,libCss) {
    var str = `
<!DOCTYPE html>
    <html>
    <head lang="en">
        <meta charset="UTF-8">
        <title>webpack coc</title>
        <link href="${libCss}" rel="stylesheet">
    </head>
    <body>
        <div id="mount-dom"></div>
        <script src="${libJs}"></script>
    </body>
</html>
    `;
    return str
}

//插件,只在执行lib时插入。
function libPathPlugin(){
    this.plugin("done", function(stats) {
        var stats = stats.toJson()
        var chunkFiles = stats.chunks[0].files
        var libJs ='',libCss='';
        for(var i in chunkFiles){
            var fileName = chunkFiles[i]
            if(fileName.endsWith('.js')){
                libJs = fileName
            }
            if(fileName.endsWith('.css')){
                libCss = fileName
            }
        }
        globalTplContent = getTplContent(libJs,libCss)
    });
}

this.plugin 的第一个参数是构建的阶段,有after-emit,emit,done等。第二个就是负责执行构建逻辑的函数了。关键是这个stats参数,里面有大量丰富的信息,建议大家把它打印处理,好好看看。这里只需要知道最终生成了文件名是什么。stats.toJson().chunks里有。这里只有lib一个模块,所以简单处理一下,就能得到html-webpack-plugin需要的模板内容。

另外,一个HtmlWebpackPlugin,只能生成一个html,我们有多entry,有多个HtmlWebpackPlugin。相关的配置都有说明,另外可以看文档。

 for(var i in entries){
    config.plugins.push(new HtmlWebpackPlugin({
        filename:(i +'.html').replace('entry.',''),//index.entry => index.html
        //template:'./assets/webpack-tpl.html'
        templateContent:globalTplContent 
        inject: true,
        chunks:[i] //只注入当前的chunk,index.html注入index.entry
    }))
}

考虑前端分离

光有一个项目的方案还不行,实际上,我们现在已经有多个相对独立的前端项目.继续分治.

enter image description here

个人觉得,前后分离,大体上有两种方案:

1.模板 + ajax

首屏有loading提醒的情况下,用户体验尚可。而且理论上,甚至可以做到完全静态化,既html也是静态的。这样开完完成后,nginx直接指到相关目录,就ok了。我写的demo为了简单,就是全静态化的。

2.node做api中间层 最简单的情况就是node做个api代理,然后顺便可以简单的套个首屏页面。当然加这一层会给前端几乎无限的可能性。你可以实现自己的缓存策略,对感兴趣的数据进行统计(因为api转接,所以用户请求的数据以及返回的数据,都能拿到。)等。就是工作量略有上升,另外要肩负node运维的职责。node挂了怎么办;升级怎么保证不间断服务等。

通过上图能看出来,大致的架构是这样的:

前端分成多个小项目,都依赖于api server => 每一个前端,都使用上面的 gulp+ webpack的方案,把页面分开。这与分治思想相契合.对于一个工程,重写一个页面,代价太高;完全从写某一个项目,一般也可以接受。

补充:

这里没有对公用的东西进行说明,已经被团队的同学pk了。准备在写一篇,讲讲commonChunk。 另外没有就开发流程进行说明。还有单页的应用怎么办。 都再起一篇吧。不过大体思路,就是本篇的东西,大家可以根据自己的情况,去修改。

评论
发表评论
1年前
赞了此文章!
1年前

我在生产环境代码输出时,遇到了一些问题。我在将你的webpack-coc-demo检出执行后,也出现了一些问题。具体的描述我在github上给webpack的作者提了issue,不知道是webpack的问题还是其他问题。能否帮忙看下?https://github.com/webpack/docs/issues/58

1年前
赞了此文章!
1年前
赞了此文章!
1年前

@chenllos 得到成就:陈老师的赞~~

1年前

赞!

PUBLISHED IN
gulp+webpack的前端构建方案探讨

前端构建很重要,很装逼

我的收藏