Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于webpack搭建前端工程解决方案探索 #10

Open
chemdemo opened this issue Aug 18, 2015 · 117 comments
Open

基于webpack搭建前端工程解决方案探索 #10

chemdemo opened this issue Aug 18, 2015 · 117 comments

Comments

@chemdemo
Copy link
Owner

本篇主要介绍webpack的基本原理以及基于webpack搭建前端项目工程化解决方案的思路。

下篇(还没写)探讨下对于Node.js作为后端的项目工程化、模块化、前后端共享代码、自动化部署的做法。

关于前端工程

下面是百科关于“软件工程”的名词解释:

软件工程是一门研究用工程化方法构建和维护有效的、实用的和高质量的软件的学科。

其中,工程化是方法,是将软件研发的各个链路串接起来的工具。

对于软件“工程化”,个人以为至少应当有如下特点:

  • 有IDE的支持,负责初始化工程、工程结构组织、debug、编译、打包等工作
  • 有固定或者约定的工程结构,规定软件所依赖的不同类别的资源的存放路径甚至代码的写法等
  • 软件依赖的资源可能来自软件开发者,也有可能是第三方,工程化需要集成对资源的获取、打包、发布、版本管理等能力
  • 和其他系统的集成,如CI系统、运维系统、监控系统等

广泛意义上讲,前端也属于软件工程的范畴。

但前端没有Eclipse、Visual Studio等为特定语言量身打造的IDE。因为前端不需要编译,即改即生效,在开发和调试时足够方便,只需要打开个浏览器即可完成,所以前端一般不会扯到“工程”这个概念。

在很长一段时间里,前端很简单,比如下面简单的几行代码就能够成一个可运行前端应用:

<!DOCTYPE html>
<html>
<head>
    <title>webapp</title>
    <link rel="stylesheet" href="app.css">
</head>
<body>
    <h1>app title</h1>
    <script src="app.js"></script>
</body>
</html>

但随着webapp的复杂程度不断在增加,前端也在变得很庞大和复杂,按照传统的开发方式会让前端失控:代码庞大难以维护、性能优化难做、开发成本变高。

感谢Node.js,使得JavaScript这门前端的主力语言突破了浏览器环境的限制可以独立运行在OS之上,这让JavaScript拥有了文件IO、网络IO的能力,前端可以根据需要任意定制研发辅助工具。

一时间出现了以Grunt、Gulp为代表的一批前端构建工具,“前端工程”这个概念逐渐被强调和重视。但是由于前端的复杂性和特殊性,前端工程化一直很难做,构建工具有太多局限性。

诚如 张云龙@fouber 所言:

前端是一种特殊的GUI软件,它有两个特殊性:一是前端由三种编程语言组成,二是前端代码在用户端运行时增量安装。

html、css和js的配合才能保证webapp的运行,增量安装是按需加载的需要。开发完成后输出三种以上不同格式的静态资源,静态资源之间有可能存在互相依赖关系,最终构成一个复杂的资源依赖树(甚至网)。

所以,前端工程,最起码需要解决以下问题:

  • 提供开发所需的一整套运行环境,这和IDE作用类似
  • 资源管理,包括资源获取、依赖处理、实时更新、按需加载、公共模块管理等
  • 打通研发链路的各个环节,debug、mock、proxy、test、build、deploy等

其中,资源管理是前端最需要也是最难做的一个环节。

注:个人以为,与前端工程化对应的另一个重要的领域是前端组件化,前者属于工具,解决研发效率问题,后者属于前端生态,解决代码复用的问题,本篇对于后者不做深入。

在此以开发一个多页面型webapp为例,给出上面所提出的问题的解决方案。

前端开发环境搭建

主要目录结构

- webapp/               # webapp根目录
  - src/                # 开发目录
    + css/              # css资源目录
    + img/              # webapp图片资源目录
    - js/               # webapp js&jsx资源目录
      - components/     # 标准组件存放目录
          - foo/        # 组件foo
            + css/      # 组件foo的样式
            + js/       # 组件foo的逻辑
            + tmpl/     # 组件foo的模板
            index.js    # 组件foo的入口
          + bar/        # 组件bar
      + lib/            # 第三方纯js库
      ...               # 根据项目需要任意添加的代码目录
    + tmpl/             # webapp前端模板资源目录
    a.html              # webapp入口文件a
    b.html              # webapp入口文件b
  - assets/             # 编译输出目录,即发布目录
    + js/               # 编译输出的js目录
    + img/              # 编译输出的图片目录
    + css/              # 编译输出的css目录
    a.html              # 编译输出的入口a
    b.html              # 编译处理后的入口b
  + mock/               # 假数据目录
  app.js                # 本地server入口
  routes.js             # 本地路由配置
  webpack.config.js     # webpack配置文件
  gulpfile.js           # gulp任务配置
  package.json          # 项目配置
  README.md             # 项目说明

这是个经典的前端项目目录结构,项目目结构在一定程度上约定了开发规范。业务开发的同学只需关注src目录即可,开发时尽可能最小化模块粒度,这是异步加载的需要。assets是整个工程的产出,无需关注里边的内容是什么,至于怎么打包和解决资源依赖的,往下看。

本地开发环境

我们使用开源web框架搭建一个webserver,便于本地开发和调试,以及灵活地处理前端路由,以koa为例,主要代码如下:

// app.js
var http = require('http');
var koa = require('koa');
var serve = require('koa-static');

var app = koa();
var debug = process.env.NODE_ENV !== 'production';
// 开发环境和生产环境对应不同的目录
var viewDir = debug ? 'src' : 'assets';

// 处理静态资源和入口文件
app.use(serve(path.resolve(__dirname, viewDir), {
    maxage: 0
}));

app = http.createServer(app.callback());

app.listen(3005, '0.0.0.0', function() {
    console.log('app listen success.');
});

运行node app启动本地server,浏览器输入http://localhost:8080/a.html即可看到页面内容,最基本的环境就算搭建完成。

如果只是处理静态资源请求,可以有很多的替代方案,如Fiddler替换文件、本地起Nginx服务器等等。搭建一个Web服务器,个性化地定制开发环境用于提升开发效率,如处理动态请求、dnsproxy(多用于解决移动端配置host的问题)等,总之local webserver拥有无限的可能。

定制动态请求

我们的local server是localhost域,在ajax请求时为了突破前端同源策略的限制,本地server需支持代理其他域下的api的功能,即proxy。同时还要支持对未完成的api进行mock的功能。

// app.js
var router = require('koa-router')();
var routes = require('./routes');
routes(router, app);
app.use(router.routes());
// routes.js
var proxy = require('koa-proxy');
var list = require('./mock/list');
module.exports = function(router, app) {
    // mock api
    // 可以根据需要任意定制接口的返回
    router.get('/api/list', function*() {
        var query = this.query || {};
        var offset = query.offset || 0;
        var limit = query.limit || 10;
        var diff = limit - list.length;

        if(diff <= 0) {
            this.body = {code: 0, data: list.slice(0, limit)};
        } else {
            var arr = list.slice(0, list.length);
            var i = 0;

            while(diff--) arr.push(arr[i++]);

            this.body = {code: 0, data: arr};
        }
    });

    // proxy api
    router.get('/api/foo/bar', proxy({url: 'http://foo.bar.com'}));
}

webpack资源管理

资源的获取

ECMAScript 6之前,前端的模块化一直没有统一的标准,仅前端包管理系统就有好几个。所以任何一个库实现的loader都不得不去兼容基于多种模块化标准开发的模块。

webpack同时提供了对CommonJS、AMD和ES6模块化标准的支持,对于非前三种标准开发的模块,webpack提供了shimming modules的功能。

受Node.js的影响,越来越多的前端开发者开始采用CommonJS作为模块开发标准,npm已经逐渐成为前端模块的托管平台,这大大降低了前后端模块复用的难度。

在webpack配置项里,可以把node_modules路径添加到resolve search root列表里边,这样就可以直接load npm模块了:

// webpack.config.js
resolve: {
    root: [process.cwd() + '/src', process.cwd() + '/node_modules'],
    alias: {},
    extensions: ['', '.js', '.css', '.scss', '.ejs', '.png', '.jpg']
},
$ npm install jquery react --save
// page-x.js
import $ from 'jquery';
import React from 'react';

资源引用

根据webpack的设计理念,所有资源都是“模块”,webpack内部实现了一套资源加载机制,这与Requirejs、Sea.js、Browserify等实现有所不同,除了借助插件体系加载不同类型的资源文件之外,webpack还对输出结果提供了非常精细的控制能力,开发者只需要根据需要调整参数即可:

// webpack.config.js
// webpack loaders的配置示例
...
loaders: [
    {
        test: /\.(jpe?g|png|gif|svg)$/i,
        loaders: [
            'image?{bypassOnDebug: true, progressive:true, \
                optimizationLevel: 3, pngquant:{quality: "65-80"}}',
            'url?limit=10000&name=img/[hash:8].[name].[ext]',
        ]
    },
    {
        test: /\.(woff|eot|ttf)$/i,
        loader: 'url?limit=10000&name=fonts/[hash:8].[name].[ext]'
    },
    {test: /\.(tpl|ejs)$/, loader: 'ejs'},
    {test: /\.js$/, loader: 'jsx'},
    {test: /\.css$/, loader: 'style!css'},
    {test: /\.scss$/, loader: 'style!css!scss'},
]
...

简单解释下上面的代码,test项表示匹配的资源类型,loaderloaders项表示用来加载这种类型的资源的loader,loader的使用可以参考using loaders,更多的loader可以参考list of loaders

对于开发者来说,使用loader很简单,最好先配置好特定类型的资源对应的loaders,在业务代码直接使用webpack提供的require(source path)接口即可:

// a.js
// 加载css资源
require('../css/a.css');

// 加载其他js资源
var foo = require('./widgets/foo');
var bar = require('./widgets/bar');

// 加载图片资源
var loadingImg = require('../img/loading.png');

var img = document.createElement('img');

img.src = loadingImg;

注意,require()还支持在资源path前面指定loader,即require(![loaders list]![source path])形式:

require("!style!css!less!bootstrap/less/bootstrap.less");
// “bootstrap.less”这个资源会先被"less-loader"处理,
// 其结果又会被"css-loader"处理,接着是"style-loader"
// 可类比pipe操作

require()时指定的loader会覆盖配置文件里对应的loader配置项。

资源依赖处理

通过loader机制,可以不需要做额外的转换即可加载浏览器不直接支持的资源类型,如.scss.less.json.ejs等。

但是对于css、js和图片,采用webpack加载和直接采用标签引用加载,有何不同呢?

运行webpack的打包命令,可以得到a.js的输出的结果:

webpackJsonp([0], {
    /***/0:
    /***/function(module, exports, __webpack_require__) {

        __webpack_require__(6);

        var foo = __webpack_require__(25);
        var bar = __webpack_require__(26);

        var loadingImg = __webpack_require__(24);
        var img = document.createElement('img');

        img.src = loadingImg;
    },

    /***/6:
    /***/function(module, exports, __webpack_require__) {
        ...
    },

    /***/7:
    /***/function(module, exports, __webpack_require__) {
        ...
    },

    /***/24:
    /***/function(module, exports) {
        ...
    },

    /***/25:
    /***/function(module, exports) {
        ...
    },

    /***/26:
    /***/function(module, exports) {
        ...
    }
});

从输出结果可以看到,webpack内部实现了一个全局的webpackJsonp()用于加载处理后的资源,并且webpack把资源进行重新编号,每一个资源成为一个模块,对应一个id,后边是模块的内部实现,而这些操作都是webpack内部处理的,使用者无需关心内部细节甚至输出结果。

上面的输出代码,因篇幅限制删除了其他模块的内部实现细节,完整的输出请看a.out.js,来看看图片的输出:

/***/24:
/***/function(module, exports) {

    module.exports = "data:image/png;base64,...";

    /***/
}

注意到图片资源的loader配置:

{
    test: /\.(jpe?g|png|gif|svg)$/i,
    loaders: [
        'image?...',
        'url?limit=10000&name=img/[hash:8].[name].[ext]',
    ]
}

意思是,图片资源在加载时先压缩,然后当内容size小于~10KB时,会自动转成base64的方式内嵌进去,这样可以减少一个HTTP的请求。当图片大于10KB时,则会在img/下生成压缩后的图片,命名是[hash:8].[name].[ext]的形式。hash:8的意思是取图片内容hashsum值的前8位,这样做能够保证引用的是图片资源的最新修改版本,保证浏览器端能够即时更新。

对于css文件,默认情况下webpack会把css content内嵌到js里边,运行时会使用style标签内联。如果希望将css使用link标签引入,可以使用ExtractTextPlugin插件进行提取。

资源的编译输出

webpack的三个概念:模块(module)、入口文件(entry)、分块(chunk)。

其中,module指各种资源文件,如js、css、图片、svg、scss、less等等,一切资源皆被当做模块。

webpack编译输出的文件包括以下2种:

  • entry:入口,可以是一个或者多个资源合并而成,由html通过script标签引入
  • chunk:被entry所依赖的额外的代码块,同样可以包含一个或者多个文件

下面是一段entry和output项的配置示例:

entry: {
    a: './src/js/a.js'
},
output: {
    path: path.resolve(debug ? '__build' : './assets/'),
    filename: debug ? '[name].js' : 'js/[chunkhash:8].[name].min.js',
    chunkFilename: debug ? '[chunkhash:8].chunk.js' : 'js/[chunkhash:8].chunk.min.js',
    publicPath: debug ? '/__build/' : ''
}

其中entry项是入口文件路径映射表,output项是对输出文件路径和名称的配置,占位符如[id][chunkhash][name]等分别代表编译后的模块id、chunk的hashnum值、chunk名等,可以任意组合决定最终输出的资源格式。hashnum的做法,基本上弱化了版本号的概念,版本迭代的时候chunk是否更新只取决于chnuk的内容是否发生变化。

细心的同学可能会有疑问,entry表示入口文件,需要手动指定,那么chunk到底是什么,chunk是怎么生成的?

在开发webapp时,总会有一些功能是使用过程中才会用到的,出于性能优化的需要,对于这部分资源我们希望做成异步加载,所以这部分的代码一般不用打包到入口文件里边。

对于这一点,webpack提供了非常好的支持,即code splitting,即使用require.ensure()作为代码分割的标识。

例如某个需求场景,根据url参数,加载不同的两个UI组件,示例代码如下:

var component = getUrlQuery('component');

if('dialog' === component) {
    require.ensure([], function(require) {
        var dialog = require('./components/dialog');
        // todo ...
    });
}

if('toast' === component) {
    require.ensure([], function(require) {
        var toast = require('./components/toast');
        // todo ...
    });
}

url分别输入不同的参数后得到瀑布图:

code_splitting1

code_splitting2

webpack将require.ensure()包裹的部分单独打包了,即图中看到的[hash].chunk.js,既解决了异步加载的问题,又保证了加载到的是最新的chunk的内容。

假设app还有一个入口页面b.html,那麽就需要相应的再增加一个入口文件b.js,直接在entry项配置即可。多个入口文件之间可能公用一个模块,可以使用CommonsChunkPlugin插件对指定的chunks进行公共模块的提取,下面代码示例演示提取所有入口文件公用的模块,将其独立打包:

var chunks = Object.keys(entries);

plugins: [
    new CommonsChunkPlugin({
        name: 'vendors', // 将公共模块提取,生成名为`vendors`的chunk
        chunks: chunks,
        minChunks: chunks.length // 提取所有entry共同依赖的模块
    })
],

资源的实时更新

引用模块,webpack提供了require()API(也可以通过添加bable插件来支持ES6的import语法)。但是在开发阶段不可能改一次编译一次,webpack提供了强大的热更新支持,即HMR(hot module replace)

HMR简单说就是webpack启动一个本地webserver(webpack-dev-server),负责处理由webpack生成的静态资源请求。注意webpack-dev-server是把所有资源存储在内存的,所以你会发现在本地没有生成对应的chunk访问却正常。

下面这张来自webpack官网的图片,可以很清晰地说明moduleentrychunk三者的关系以及webpack如何实现热更新的:

HMR

enter0表示入口文件,chunk1~4分别是提取公共模块所生成的资源块,当模块4和9发生改变时,因为模块4被打包在chunk1中,模块9打包在chunk3中,所以HMR runtime会将变更部分同步到chunk1和chunk3中对应的模块,从而达到hot replace。

webpack-dev-server的启动很简单,配置完成之后可以通过cli启动,然后在页面引入入口文件时添加webpack-dev-server的host即可将HMR集成到已有服务器:

...
<body>
    ...
    <script src="http://localhost:8080/__build/vendors.js"></script>
    <script src="http://localhost:8080/__build/a.js"></script>
</body>
...

因为我们的local server就是基于Node.js的webserver,这里可以更进一步,将webpack开发服务器以中间件的形式集成到local webserver,不需要cli方式启动(少开一个cmd tab):

// app.js
var webpackDevMiddleware = require('koa-webpack-dev-middleware');
var webpack = require('webpack');
var webpackConf = require('./webpack.config');

app.use(webpackDevMiddleware(webpack(webpackConf), {
    contentBase: webpackConf.output.path,
    publicPath: webpackConf.output.publicPath,
    hot: true,
    stats: webpackConf.devServer.stats
}));

启动HMR之后,每次保存都会重新编译生成新的chnuk,通过控制台的log,可以很直观地看到这一过程:

HMR build

公用代码的处理:封装组件

webpack解决了资源依赖的问题,这使得封装组件变得很容易,例如:

// js/components/component-x.js
require('./component-x.css');

// @see https://github.com/okonet/ejs-loader
var template = require('./component-x.ejs');
var str = template({foo: 'bar'});

function someMethod() {}

exports.someMethod = someMethod;

使用:

// js/a.js
import {someMethod} from "./components/component-x";
someMethod();

正如开头所说,将三种语言、多种资源合并成js来管理,大大降低了维护成本。

对于新开发的组件或library,建议推送到npm仓库进行共享。如果需要支持其他加载方式(如RequireJS或标签直接引入),可以参考webpack提供的externals项。

资源路径切换

由于入口文件是手动使用script引入的,在webpack编译之后入口文件的名称和路径一般会改变,即开发环境和生产环境引用的路径不同:

// 开发环境
// a.html
<script src="/__build/vendors.js"></script>
<script src="/__build/a.js"></script>
// 生产环境
// a.html
<script src="http://cdn.site.com/js/460de4b8.vendors.min.js"></script>
<script src="http://cdn.site.com/js/e7d20340.a.min.js"></script>

webpack提供了HtmlWebpackPlugin插件来解决这个问题,HtmlWebpackPlugin支持从模板生成html文件,生成的html里边可以正确解决js打包之后的路径、文件名问题,配置示例:

// webpack.config.js
plugins: [
    new HtmlWebpackPlugin({
        template: './src/a.html',
        filename: 'a',
        inject: 'body',
        chunks: ['vendors', 'a']
    })
]

这里资源根路径的配置在output项:

// webpack.config.js
output: {
    ...
    publicPath: debug ? '/__build/' : 'http://cdn.site.com/'
}

其他入口html文件采用类似处理方式。

辅助工具集成

local server解决本地开发环境的问题,webpack解决开发和生产环境资源依赖管理的问题。在项目开发中,可能会有许多额外的任务需要完成,比如对于使用compass生成sprites的项目,因目前webpack还不直接支持sprites,所以还需要compass watch,再比如工程的远程部署等,所以需要使用一些构建工具或者脚本的配合,打通研发的链路。

因为每个团队在部署代码、单元测试、自动化测试、发布等方面做法都不同,前端需要遵循公司的标准进行自动化的整合,这部分不深入了。

对比&综述

前端工程化的建设,早期的做法是使用Grunt、Gulp等构建工具。但本质上它们只是一个任务调度器,将功能独立的任务拆解出来,按需组合运行任务。如果要完成前端工程化,这两者配置门槛很高,每一个任务都需要开发者自行使用插件解决,而且对于资源的依赖管理能力太弱。

在国内,百度出品的fis也是一种不错的工程化工具的选择,fis内部也解决了资源依赖管理的问题。因笔者没有在项目中实践过fis,所以不进行更多的评价。

webpack以一种非常优雅的方式解决了前端资源依赖管理的问题,它在内部已经集成了许多资源依赖处理的细节,但是对于使用者而言只需要做少量的配置,再结合构建工具,很容易搭建一套前端工程解决方案。

基于webpack的前端自动化工具,可以自由组合各种开源技术栈(Koa/Express/其他web框架、webpack、Sass/Less/Stylus、Gulp/Grunt等),没有复杂的资源依赖配置,工程结构也相对简单和灵活。

附上笔者根据本篇的理论所完成的一个前端自动化解决方案项目模板:
webpack-bootstrap

(完)。

@chemdemo chemdemo changed the title 基于webpack的前端工程解决方案探索(上篇) 基于webpack搭建前端工程解决方案探索 Aug 21, 2015
@luqin
Copy link

luqin commented Aug 27, 2015

先占位,学习学习

@FrendEr
Copy link

FrendEr commented Aug 27, 2015

赞!持续关注!敢问作者大概啥时候会出下篇?

@bluetomlee
Copy link

占位,学习

@fouber
Copy link

fouber commented Aug 31, 2015

一般webapp是有一个物理页面,然后根据hash或者url加载不同的“虚拟页面”来展现应用,大致的资源关系如下:

webpack

从下往上看,应用一般有一个入口模块,比如app,这个入口模块根据url来动态决定异步加载某个页面(P₁-P₄),而每个页面并不是孤岛资源,它们还会依赖其他组件,组件与组件之间可能还有共享的基础库依赖。

基于这样的资源依赖树结构,webpack是怎么解决资源合并和按需加载的?感觉静态分析打包不能很好的处理按需和请求合并问题,要么每个chunk冗余合并,要么多个chunk串行加载

@chemdemo
Copy link
Owner Author

@fouber

webpack可以通过CommonsChunkPlugin插件来对公共依赖模块进行提取:

{
    entry: {
        a: './js/pages/a.js',
        b: './js/pages/b.js',
        c: './js/pages/c.js',
        d: './js/pages/d.js'
    },
    ...
    plugins: [
        // 提取公共依赖模块,这种适合提取共享的基础库,如jquery、underscore等
        new CommonsChunkPlugin('lib', ['a', 'b', 'c', 'd'], 4),
        // 提取a、b、c中至少两个共同依赖的模块
        new CommonsChunkPlugin('common-abc', ['a', 'b', 'c'], 2),
        // 提取a、d共同依赖的模块
        new CommonsChunkPlugin('common-ad', ['a', 'd']);
    ]
}

更多可以参考multiple commons chunks

静态分析打包是事先生成chunk,如果需要消除模块冗余,只能自行配置来提取,所以webpack也是有局限性和学习成本。

对于按需加载,比如你提到的这种场景,p1-p4就当做chunk来加载了:

// app.js

switch(hash) {
    // index#p1
    case 'p1':
        require.ensure([], function() {
            require('p1');
        });
        break;

    // index#p2
    case 'p2':
        require.ensure([], function() {
            require('p2');
        });
        break;

    ...
    // 依次类推
}

打包完成之后,p1到p4会生成名为[hash].chunk.js的一系列文件。

@fouber
Copy link

fouber commented Aug 31, 2015

@chemdemo

我应该是理解webpack的做法的,只是你可能没有注意到我的例子是一个怎样的陷阱:

webpack可以通过CommonsChunkPlugin插件来对公共依赖模块进行提取

单独看我例子中的a、b依赖c的情况,这个说法是正确的。

对于按需加载,比如你提到的这种场景,p1-p4就当做chunk来加载了

单独看我例子中的app加载p1-p4,这个说法也是正确的。

但是,把二者结合起来,就不是那么回事了。。。

因为p1-p4各自为一个chunk,其结果就是p1-p4不用处理,就是文件本身就行了;而ab抽取公共依赖c,也等价于三个文件不用处理,自然就是“c为ab的公共chunk”,在静态分析的模式下,这个例子的最优合并结果居然是不合并!

现实中这样的例子其实更多,而且会更复杂,静态分析面对大工程最终的结果往往是要么因为其局限性而根本配不出来合理的方案,要么因为配置太多维护成本过高而变成一个bundle的情况,没有真正的优化空间,那些“有公共依赖抽取插件从而进行优化”的假设基本形同虚设。

@fouber
Copy link

fouber commented Aug 31, 2015

看过很多号称使用了webpack的项目,基本上无外乎这么几种最终运行效果:

  • 图省事,一个大bundle(呵呵)
  • 一个页面一个chunk,公共资源冗余重复合并在各个chunk中(呵呵)
  • 刚好页面彼此很独立,完全没有公共资源(算你走运)

正如 @kerryChen95 在我的blog底下留言说到的

以Webpack目前对WebApp架构支持的完善程度,它只差一步就能实现WebApp和WebSite(暂且这么叫,就是传统的服务端模板生成页面的意思)的通吃,只要它实现了生成资源表的API(前提是Webpack认同 资源表+资源加载框架 这种理念),然后,如果业界出现了某种资源表的事实标准,那么就一切就完美了~

我觉得这种认识是早晚的,因为优势太明显了吧。。。

此外,我还想吐槽webpack的一些设计:

  1. require的返回结果只有三种情况:

    • 返回文件uri——资源定位
    • 返回文件内容(文本或base64)——资源内嵌
    • 什么都不返回,只是在表中记录资源依赖关系(如果认可)——依赖声明

    除了以上三种情况,前端构建不再需要多余的规则了。而返回文件内容时到底该返回文本还是文件base64是可以被唯一确定的,只要给出被require的文件就知道该返回哪种了。所以我觉得webpack还可以在精进一步概念。

  2. 这个require标识可以扩展到html和css。有些技术选型的情况下我们可能也需要在html或者css中标记依赖或者资源定位,比如模板中可能直接写一个img src定位资源(当然,你可以准换成js中定位然后模板传值给html)。

在三种语言中分别提供资源定位、资源内嵌、依赖声明的构建标识,采用表+资源加载框架优化加载,以上这两点构成了fis的核心设计理念,不可否认webpack是目前最接近fis的构建工具

@chemdemo
Copy link
Owner Author

@fouber
这个例子很特殊啊,webpack资源合并的最优结果是不合并,但是webpack内部已经自动“标识”了资源之间的依赖关系(包括加hash戳)。所以这并不矛盾,生成的chunk的最小单元就是一个资源文件。

想象在某种理想情况下,静态资源构建的结果不是合并文件(资源内嵌除外),而是分析资源之间的依赖并且有工具去识别这种依赖关系,在资源下载的时候支持combine......

所以“资源表+资源加载框架”这个概念我算是理解通了,看来要去学习下FIS。

不过关于webpack require 的设计:

1.require就是一个标识符,至于资源如何加载(或依赖标记)的,看对应的loader怎么实现。
2.凡是通过require标识加载的资源,对于资源中使用了浏览器直接可识别的外部资源加载API,webpack都会预先使用loader加载,比如某个模板里边使用img src引用了图片,编译时图片最终也是交给image loader去加载的,css同理。

@VaJoy
Copy link

VaJoy commented Sep 4, 2015

为方便同步、打包,都走的本地文件形式,在webpack里引用非本地文件只能通过externals来实现么,但这种情况下希望实现懒加载又不行了(require.ensure内还是必须使用的本地模块或提前下载好了的模块)。

像我司大部分移动端项目都会把公共模块(CMD模式)投放到分发的CDN上(服务器配置的资源过期时间很短也不建议修改之,webpack 的文件hash形式不太适用),客户端第一次则下载并存入localstorage,下次则直接从本地取,这种缓存模式走 webpack 是否有办法实现?

这样就忍痛舍弃了seajs的依赖就近模式(比如可以使用require.async在需要的时候才下载某模块),需要一口气先把全部需要的公共模块都下载下来(就是我第一段提及的问题)。但在旧项目我们先引入了seajs才下载模块,而在webpack 需要走externals配置(即在webpack打包后的bundle下载之前先下载),这时候是还没有模块加载器的,就悲剧了。。。

不知道对于我描述的问题是否有好的解决思路,我暂时也只想着留给后续的新项目用新的开发模式来走webpack,但这样新旧两种开发模式,开发和维护成本都有点高(无奈脸)

@chemdemo
Copy link
Owner Author

chemdemo commented Sep 6, 2015

@VaJoy

参考下这个issue

webpack是一个模块打包器,不是文件加载器,它只能加载本地磁盘上的文件。对于你们的情况,CDN的公共lib可以采用scriptjs加载,因为本地还需要判断localstorage,所以需要先判断local再加载,代码类似:

if(localStorage.getItem('lib-content')) {
    // todo
} else {
    var $script = require("scriptjs");
    $script('http://cdn.yourdomain.com/lib/jquery.js', function() {
        // todo
    });
}

而这段代码就可以放到入口文件里使用webpack打包。

对于公共业务module,个人觉得还是在开发时存放到本地比较好,开发完可以发布到npm或者公司内部模块管理平台。

@lilJay-lin
Copy link

谁能跟我说说,issue能收藏吗,小白白路过。。

@chshouyu
Copy link

@lilJay-lin 你在这里回复了之后,以后有新的回复时你会收到通知的

@FrendEr
Copy link

FrendEr commented Sep 18, 2015

大牛,有个问题请教。我正在使用webpack做构建,我有多个entry,这时output的目录能指定多个,达到一对一的效果吗?就是'/src/common/a.js' --> '/dist/common/a.bundle.js','/src/page/b.js' --> '/dist/page/a.bundle.js'。请问大牛这种现在webpack能实现吗?

@luqin
Copy link

luqin commented Sep 18, 2015

可以的,参考官方文档吧

@chemdemo
Copy link
Owner Author

@FrendEr 这种好像做不到,filename项只支持字符串。

@chemdemo
Copy link
Owner Author

@io3 有例子吗 我没这样做过哎 一般无需关注webpack输出的结果。

@FrendEr
Copy link

FrendEr commented Sep 18, 2015

@io3 对啊,有没有线上的例子或者教程呀?有点急,在线等哈!

@luqin
Copy link

luqin commented Sep 18, 2015

{
    entry: {
        'common/a': "./a",
        'page/b': "./b"
    },
    output: {
        path: path.join(__dirname, "dist"),
        filename: "[name].entry.js"
    }
}

具体还可参考 http://webpack.github.io/docs/multiple-entry-points.html 页面底部的三种示例

@FrendEr
Copy link

FrendEr commented Sep 18, 2015

@io3 这里最终打包的都是到了 /yourpath/dist/下面而已吧,都是同级的。我需要的是根据入口文件的路径来确定输出文件的路径哦

@luqin
Copy link

luqin commented Sep 18, 2015

我发的code中,/就会自动生成文件夹目录。我项目都是这么用的

@chemdemo
Copy link
Owner Author

@io3 你的示例明显达不到他的需求啦 filename如果支持函数就可以

@luqin
Copy link

luqin commented Sep 18, 2015

我这也是多个entry,每个entry自己指定目录,达不到他需求?

@FrendEr
Copy link

FrendEr commented Sep 18, 2015

@io3 恩。多个entry是可以达到的,但是output不行。我是希望output可以为每个文件指定不同的目录

@luqin
Copy link

luqin commented Sep 18, 2015

{
    entry: {
        'common/a': "./a",
        'page/b': "./b"
    },
    output: {
        path: path.join(__dirname, "dist"),
        filename: "[name].entry.js"
    }
}

输出的文件:

/dist
   /common
      a.entry.js
   /page
      b.entry.js

我不知道你们还有啥需求

@chemdemo
Copy link
Owner Author

额 好吧 看出来了

@FrendEr
Copy link

FrendEr commented Sep 18, 2015

@chemdemo @io3 好腻害!!谢谢两位!

@daifee
Copy link

daifee commented Sep 19, 2015

你好,请原谅在这里问一个关于webpack的问题。

需求是“单页应用&按需加载”。实践过程中遇到了问题。下面是问题描述:

目录结构:

/app
  |-- components/
  |           |-- header.js
  |-- pages/
  |        |-- index.js
  |        |-- about.js
  |        |-- login.js
  |-- entry.js

描述:

  • pages/index.jspages/about.js都依赖components/header.js
  • pages/login.js不依赖components/header.js
  • entry.js是入口文件,根据URL按需加载pages/*.js
// 监听path路径,按需加载
window.addEventListener('popstate', function () {
  switch (path) {
    case '/':
      require.ensure(['./pages/index.js']);
      break;
    case '/about':
      require.ensure(['./pages/about.js']);
      break;
  }
});

结果:生成了两个按需加载的chunks,但两个chunks都包含了components/header.js。即存在两份。

期望结果:components/header.js作为一个独立的chunk。这样可以减少代码量(代价是多了请求数量)

webpack能实现我期望结果的需求吗?

@chemdemo
Copy link
Owner Author

@epooren

可以在打包的时候将components/header.js作为独立的入口:

{
    entry: {
        'components/header': 'components/header.js',
    }
}

在html里需要手动引用。

@wyntau
Copy link

wyntau commented Jul 21, 2016

@Mmzer 如果有这种需求, 建议去看下 FIS3. 这样的需求, 使用 FIS3 可以算是 so easy

@zhangbg
Copy link

zhangbg commented Jul 26, 2016

学习了

@vegawong
Copy link

问题: 如何处理服务端模板的img中的src指向?

我看楼上已经有提过类似的, 也有回答, 不过只限于纯前端模板解析, 类似用loader去过一遍html之类的, 并不能满足需求, 我这里处理的是服务端的模板, 而家不一定是html, 有可能是ejs, jade之类, 这种情况下img的src好尴尬啊...

@chemdemo
Copy link
Owner Author

如果是html或者类html模板(ejs、jade),应该有相应的loader可以做。
不过其他语言的模板如smarty、jsp之类的目前好像没法这么做。

@geraldlrh
Copy link

geraldlrh commented Aug 2, 2016

@treri @chemdemo 关于chunk id的问题,使用manifest和NamedModulesPlugin,是可以解决的。
参考 webpack/webpack#1315https://github.com/kevinrenskers/chunkhash-problem

@db-murphy
Copy link

mark

@Meqn
Copy link

Meqn commented Oct 13, 2016

mark

@avilang
Copy link

avilang commented Oct 22, 2016

mark
顺便想提一个问题,你们是怎么解决的?
项目的目录结构大致如下:
---b2b #b2b目录
---crm #crm目录
---cms #cms目录
........... #其他子系统
package.json
gulpfile.b2b.js
gulpfile.crm.js
gulpfile.cms.js
.......................
webpack.config.b2b.js

上面目录可以看出,各项目的 gulp 文件和 webpack 配置文件都存放在根目录下,但是这样有一个问题就是 package.json 只能有一个,这么引起的问题就是,各个子系统的依赖都在一个 package.json 文件里,好像不太利用管理各自的依赖,怎么能很好的区分各自的依赖呢?
尝试过,每个子系统里,存放各自的 package.json , gulp , webpack 的文件,不存放在根目录中,但打包构建的时候,碰到坑,没有直接存放在根目录下,方便直接。
大神们是怎么管理,这种项目下有多个子应用,子系统,的依赖的了

@chemdemo
Copy link
Owner Author

@avilang 这种纯粹是物理路径的区分 算不上前端工程吧 为什么不拆开成多个project存放呢

@cc258
Copy link

cc258 commented Nov 9, 2016

精彩,看10遍

@zhenyong
Copy link

zhenyong commented Nov 11, 2016

@treri 稳定 module ID 相关的坑: http://zhenyong.site/2016/10/27/webpack-long-term-hash/

对于 webpack 1.x 输出稳定的 module id 有多种方案,比较便捷的一种方式:
https://github.com/zhenyong/webpack-stable-module-id-and-hash

@wyntau
Copy link

wyntau commented Nov 11, 2016

@zhenyong 赞, 有时间看一下. 目前在使用 fis3

@yumo-mt
Copy link

yumo-mt commented Nov 26, 2016

mark

@ImHype
Copy link

ImHype commented Jan 7, 2017

看了开头,忍不住想要评论下,和我的构思几乎一模一样,知己

@CntChen
Copy link

CntChen commented Jan 15, 2017

mark

@SimpleZn
Copy link

SimpleZn commented Feb 7, 2017

@chemdemo 想请教一个问题,在local server , dev环境中访问 router.get('/', () => { }) , 是怎样读取 html-webpack-plugin 生成在内存中的html文件的。

@chemdemo
Copy link
Owner Author

chemdemo commented Feb 7, 2017

@SimpleZn dev环境,文件是存储在内存然后由webpack-dev-server进行serve,而node渲染模板又要求文件是落地的,所以目前使用到html-webpack-plugin想再使用server-render是不行的。

早有人提过这个问题,http://stackoverflow.com/questions/36269456/render-file-generated-by-html-webpack-plugin-whithin-router ,貌似还是无解。

@SimpleZn
Copy link

SimpleZn commented Feb 8, 2017

@chemdemo 我在你的代码中看到,node server-render时是提供了一个home.html , 这个文件是一个载体,当访问其中的a.html 时,实际上是由webpack-dev-server进行serve的,是这样的吧?

我之前是想在dev环境直接通过route来访问html-webpack-plugin生成在内存中的html文件,后来我在html-webpack-plugin的issues中找到了答案: jantimon/html-webpack-plugin#145

var express = require('express');
var app = express();
var webpack = require('webpack');
var path = require('path');

var compiler = webpack(require('./webpack.config.js'));

app.use(require('webpack-dev-middleware')(compiler, {
  noInfo: true,
  publicPath: '/'
}));

app.use('*', function (req, res, next) {
  var filename = path.join(compiler.outputPath,'index.html');
  compiler.outputFileSystem.readFile(filename, function(err, result){
    if (err) {
      return next(err);
    }
    res.set('content-type','text/html');
    res.send(result);
    res.end();
  });
});

app.listen(3000);

@muxiaobai
Copy link

Sit onlookers

@zhaoqize
Copy link

确实让我更加明晰了一些概念 但是有的还是需要写demo来测试一下

@cxinixn
Copy link

cxinixn commented Mar 30, 2017

我有问题
用new webpack.optimize.CommonsChunkPlugin,提取公共的jquery,但是我没有需要引用jquery的页面出现webpackJsonp is not defined

@wyntau
Copy link

wyntau commented Apr 7, 2017

@chemdemo 请看使用 fis3 + typescript 做的 vue-hackernews-2.0

https://github.com/Treri/fis3-typescript-vue-hackernews-2.0

fis3-hook-node_modules 还是挺好用的.

@2ue
Copy link

2ue commented May 8, 2017

请问博主一个问题,我现在有这么一个需求:

//a.css

@import "./reset.css" 
@import "./common-ui.css"

//enter.js

require('a.css')

//webpack.config.js

enter:{
index:'enter.js'
},
output:{
       filename:'[name].js',
},

这样配置后,最终会生成一个index.css文件,index.css文件中包含了reset.css和common-ui.css。
单页应用这样处理没什么毛病,如果是复杂的多页应用,每个页面的css文件都把rest.css和common-ui.css打包进去就显得太冗余,请问有没有办法让他们单独分离出来,能实现自动插入到html文件更好~

请问,这样能实现么?

@AquariusBaby
Copy link

评论太精彩,插个眼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests