行业新闻

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

作者: 发布时间:2020-10-22

原标题:依据webpack建立前端工程处理计划探究

作者:dmyang

来历:SegmentFault 思否社区

本篇首要介绍webpack的基本原理以及依据webpack建立纯静态页面型前端项目工程化处理计划的思路。

关于前端工程

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

软件工程是一门研讨用工程化办法构建和保护有用的、有用的和高质量的软件的学科。

软件工程是一门研讨用工程化办法构建和保护有用的、有用的和高质量的软件的学科。

其间,工程化是办法,是将软件研制的各个链路串接起来的东西。

关于软件“工程化”,个人认为至少应当有如下特色:

  • 有IDE的支撑,担任初始化工程、工程结构安排、debug、编译、打包等作业

  • 有固定或许约好的工程结构,规则软件所依靠的不同类别的资源的寄存途径乃至代码的写法等

  • 软件依靠的资源或许来自软件开发者,也有或许是第三方,工程化需求集成对资源的获取、打包、发布、版别办理等才干

  • 和其他系统的集成,如CI系统、运维系统、监控系统等

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

< src= "app.js"></>

</body>

</html>

但随着webapp的杂乱程度不断在增加,前端也在变得很巨大和杂乱,依照传统的开发方法会让前端失控:代码巨大难以保护、功用优化难做、开发本钱变高。

感谢Node.js,使得Java这门前端的主力言语打破了浏览器环境的约束能够独立运转在OS之上,这让Java具有了文件IO、网络IO的才干,前端能够依据需求恣意定制研制辅助东西。

一时间呈现了以Grunt、Gulp为代表的一批前端构建东西,“前端工程”这个概念逐步被强谐和注重。可是由于前端的杂乱性和特别性,前端工程化一向很难做,构建东西有太多局限性。

诚如 张云龙@fouber 所言:

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

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

html、css和js的合作才干确保webapp的运转,增量装置是按需加载的需求。开发完结后输出三种以上不同格局的静态资源,静态资源之间有或许存在相互依靠联系,终究构成一个杂乱的资源依靠树(乃至网)。

所以,前端工程,最起码需求处理以下问题:

  • 供给开发所需的一整套运转环境,这和IDE效果相似

  • 资源办理,包括资源获取、依靠处理、实时更新、按需加载、公共模块办理等

  • 打通研制链路的各个环节,debug、mock、proxy、test、build、deploy等

供给开发所需的一整套运转环境,这和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:3005/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资源办理

资源的获取

ECMA 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项表明匹配的资源类型,loader或loaders项表明用来加载这种类型的资源的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.( '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.( '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经过标签引进

  • chunk:被entry所依靠的额定的代码块,相同能够包括一个或许多个文件

entry:进口,能够是一个或许多个资源兼并而成,由html经过标签引进

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别离输入不同的参数后得到瀑布图:

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供给了requireAPI(也能够经过增加bable插件来支撑ES6的import语法)。可是在开发阶段不或许改一次编译一次,webpack供给了强壮的热更新支撑,即HMR(hot module replace)。

HMR简略说便是webpack发动一个本地webserver(webpack-dev-server),担任处理由webpack生成的静态资源恳求。留意webpack-dev-server是把全部资源存储在内存的,所以你会发现在本地没有生成对应的chunk拜访却正常。

下面这张来自webpack官网的图片,能够很明晰地阐明module、entry、chunk三者的联系以及webpack怎么完结热更新的:

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>

...

< src= "http://localhost:3005/__build/vendors.js"></>

< src= "http://localhost:3005/__build/a.js"></>

</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,能够很直观地看到这一进程:

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

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'});

functionsomeMethod{}

exports.someMethod = someMethod;

运用:

// js/a.js

import {someMethod} from "./components/component-x";

someMethod;

正如最初所说,将三种言语、多种资源兼并成js来办理,大大降低了保护本钱。

关于新开发的组件或library,主张推送到npm库房进行同享。假如需求支撑其他加载方法(如RequireJS或标签直接引进),能够参阅webpack供给的externals项。

资源途径切换

由于进口文件是手动运用引进的,在webpack编译之后进口文件的称号和途径一般会改动,即开发环境和出产环境引证的途径不同:

// 开发环境

// a.html

< src= "/__build/vendors.js"></>

< src= "/__build/a.js"></>

// 出产环境

// a.html

< src= "http://cdn.site.com/js/460de4b8.vendors.min.js"></>

< src= "http://cdn.site.com/js/e7d20340.a.min.js"></>

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-seed : https://github.com/chemdemo/webpack-seed

SegmentFault 思否社区和文章作者打开更多互动和沟通。

- END -回来,检查更多

责任编辑: