前不久把 sf 前端的构建工具进行了改进和优化,用上了目前很是火的 webpack 、babel 和 es6 等等新技术。javascript
sf 前端的构建工具最先使用的是当时很是流行的 grunt,接下来是 gulp,而后就是如今的 webpack。css
构建工具 | Browserify | Grunt | Gulp | Webpack |
---|---|---|---|---|
描述 | browser-side require() the node way | The JavaScript Task Runner | The streaming build system | Packs CommonJs/AMD modules for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand. Support loaders to preprocess files, i.e. json, jade, coffee, css, less, ... and your custom stuff. |
关键词 | browser, require, commonjs, commonj-esque, bundle, npm, javascript | task, async, cli, minify, uglify, build, lodash, unit, test, qunit, nodeunit, server, init, scaffold, make, jake, tool | ||
做者 | James Halliday | Grunt Development Team | Fractal | Tobias Koppers @sokra |
连接 | Homepage Bug Report Github | Homepage Bug Report Github | Homepage Bug Report Github | Homepage Bug Report Github |
比较 | ||||
Licenses | MIT | MIT | MIT | MIT |
Created | 5 years ago (Feb, 2011) | 5 years ago (Jan, 2012) | 3 years ago (Jul, 2013) | 4 years ago (Mar, 2012) |
版本数量 | 459 | 56 | 63 | 416 |
版本周期 | every 4 days | every a month | every 18 days | every 4 days |
依赖数 | 46 | 16 | 13 | 15 |
gulphtml
配置简单,插件丰富,上手快前端
解决了 grunt 配置繁琐,不易维护的问题,经过插件扩展进一步提升了开发效率vue
webpackjava
解决模块自由引入,打包node
webpack 解决了 sf 后台存在的历史问题,提升了开发效率react
html 模板、js 模块的自由引入,再也不依赖配置表jquery
每次添加新模块,再也不须要加入一堆的 script 标签webpack
代码压缩合并速度提高
coffee 换成了 js(es6)
Webpack 增长了项目开发的灵活性,优化了性能
提取公用 js 代码
只要有 loader ,能够自由使用多种语言
支持按需加载
去年在 SF 技术分享会安利过 gulp building width gulp
配置 webpack须要建一个 webpack.config.js 文件。建议搭配 webpack-dev-server 使用:webpack doc webpack-dev-server
咱们的配置文件大概长这个样子。下面来分析一下。
var webpack = require('webpack'); var reusePlugin = new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', _:'underscore', 'window.jQuery': 'jquery', 'root.jQuery': 'jquery' }); var config = { context: __dirname + '/src', devtool: 'source-map', entry:{ 'chart':[ './chart/ChartCtrl.js', './chart/ChartDirective.js', ], 'log':'./log/ctrl.js', 'dashboard':'./dashboard/ctrl.js', 'operation':'./operation/ctrl.js' }, output:{ path: __dirname + '/dist', filename:'./[name]/ctrl.js', publicPath:'/static/dist/', }, devServer: { hot: true, port: 3333, proxy: { '*': { target: 'http://xxadmin.domain.com', secure: false, changeOrigin: true }, }, }, module:{ loaders:[ { test: /\.js$/, exclude: /(node_modules|bower_components|3rd)/, loader: 'babel', // 'babel-loader' is also a legal name to reference query: { presets: ['es2015'] } }, { test: /\.html$/, loader: "html" }, { test: /\.css$/, exclude: /(node_modules|bower_components|3rd)/, loader: "style-loader!css-loader" }, { test: /\.scss$/, loaders: ["style", "css", "sass"] }, { test: /\.(png|woff|woff2|eot|ttf|svg)$/, loader: 'url-loader?limit=100000' } ] }, plugins: [ reusePlugin, ] }; module.exports = config;
处理的入口,配置须要处理的 js。entry 有三种写法,每一个入口称为一个chunk。
字符串 entry: "./index/index.js"
配置模块会被解析为模块,并在启动时加载。默认 chunk 名为 main, 具体打包文件名可在 output 中配置。
数组 entry: ['./src/mod1.js', [...,] './src/index.js']
全部的模块会在启动时 按照配置顺序 加载,合并到最后一个模块会被导出。默认 chunk 名也是 main。
对象 entry: {index: '...', login : [...] }
传入Object,则会生成多个入口打包文件, key 是 chunk 名,value能够是字符串,也但是数组。
很明显咱们采用的是第三种。
设置入口配置的文件的输出规则,经过output对象实现
output.path
:输出文件路径,一般设置为 __dirname + ‘/build’
output.filename
:输出文件名称,有下面列出的四种可选的变量,filename 配置能够是这几种的任意一种或多种的组合
[id] chunk的id
[name] chunk名
[hash] 编译哈希值
[chunkhash] chunk的hash值
output.publicPath
:设置为想要的资源访问路径。通常使用 webpack-dev-server 时,则须要经过相似 http://localhost:8080/asstes/index-1.js 来访问资源,若是没有设置,则默认从站点根目录加载。
有些时候,咱们用到的第三方库并无采用 CommonJS 或 AMD 规范。这样咱们没法经过 require() 来引用这些库。
Webpack 给出了解决方案,在项目根目录下,建立一个叫作 web_modules 的文件夹,将须要用到的第三方库存放到里面,就能够在逻辑代码中使用 require(‘xx-lib.js’)
来引用并使用了。
当咱们常用React、jQuery等外部第三方库的时候,一般在每一个业务逻辑JS中都会遇到这些库。
如咱们须要在各个文件中都是有jQuery的$对象,所以咱们须要在每一个用到jQuery的JS文件的头部经过require('jquery')来依赖jQuery。 这样作很是繁琐且重复。
webpack提供了咱们一种比较高效的方法,咱们能够经过在配置文件中配置使用到的变量名,那么webpack会自动分析,而且在编译时帮咱们完成这些依赖的引入。
这样,咱们在JS中,就不须要引入jQuery等经常使用模块了,直接使用配置的这些变量,webpack就会自动引入配置的库。
new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', _:'underscore', 'window.jQuery': 'jquery', 'root.jQuery': 'jquery' });
项目中有些代码咱们只为在开发环境(例如日志)或者是内部测试环境(例如那些没有发布的新功能)中使用,又不想让这些调试内容在发布的时候泄露出去,那就须要引入下面这些魔法全局变量(magic globals):
if (__DEV__) { console.warn('Extra logging'); } // ... if (__PRERELEASE__) { showSecretFeature(); }
同时还要在webpack.config.js中配置这些变量,使得 webpack 可以识别他们。
// webpack.config.js // definePlugin 会把定义的string 变量插入到Js代码中。 var definePlugin = new webpack.DefinePlugin({ __DEV__: JSON.stringify(JSON.parse(process.env.BUILD_DEV || 'true')), __PRERELEASE__: JSON.stringify(JSON.parse(process.env.BUILD_PRERELEASE || 'false')) }); module.exports = { entry: './main.js', output: { filename: 'bundle.js' }, plugins: [definePlugin] };
配置完成后,就可使用 BUILD_DEV=1 BUILD_PRERELEASE=1 webpack来打包代码了。 值得注意的是,webpack -p 会删除全部无做用代码,也就是说那些包裹在这些全局变量下的代码块都会被删除,这样就能保证这些代码不会因发布上线而泄露。
虽然CommonJS是同步加载的,可是webpack也提供了异步加载的方式。这对于单页应用中使用的客户端路由很是有用。当真正路由到了某个页面的时候,它的代码才会被加载下来。
指定你要异步加载的 拆分点。看下面的例子
if (window.location.pathname === '/feed') { showLoadingState(); require.ensure([], function() { // 这个语法痕奇怪,可是仍是能够起做用的 hideLoadingState(); require('./feed').show(); // 当这个函数被调用的时候,此模块是必定已经被同步加载下来了 }); } else if (window.location.pathname === '/profile') { showLoadingState(); require.ensure([], function() { hideLoadingState(); require('./profile').show(); }); }
剩下的事就能够交给webpack,它会为你生成并加载这些额外的 chunk 文件。
咱们能够在package.json中事先定义好命令:
"scripts": { "dev": "BUILD_DEV=1 webpack-dev-server --progress --colors", "build": "BUILD_PRERELEASE=1 webpack -p" }
那么就能够避免输入冗长的命令了
开发时输入 npm run dev
发布时输入 npm run build
项目中,对于一些经常使用的组件,站点公用模块常常须要与其余逻辑分开,而后合并到同一个文件,以便于长时间的缓存。要实现这一功能,配置参照:
var webpack = require('webpack'); var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; ... entry: { a: './index/a.js', b: './idnex/b.js', c: './index/c.js', d: './index/d.js' }, ... plugins: [ new CommonsChunkPlugin('part1.js', ['a', 'b']), new CommonsChunkPlugin('common.js', ['part1', 'c']) ]
简单的状况下能够这样写 newwebpack.optimize.CommonsChunkPlugin('common.js’);,这样就会提取全部模块的通用代码到 common.js。
能够经过在配置中加入devtool项,选择预设调试工具来提升代码调试质量和效率:
eval – 每一个模块采用eval和 //@ sourceURL 来执行
source-map – sourceMap是发散的,和output.sourceMapFilename协调使用
hidden-source-map – 和source-map相似,可是不会添加一个打包文件的尾部添加引用注释
inline-source-map – SourceMap以DataUrl的方式插入打包文件的尾部
eval-source-map – 每一个模块以eval方式执行而且SourceMap以DataUrl的方式添加进eval
cheap-source-map – 去除column-mappings的SourceMap, 来自于loader中的内容不会被使用。
cheap-module-source-map – 去除column-mappings的SourceMap, 来自于loader中的SourceMaps被简化为单个mapping文件
devtool | 构建速度 | 再次构建速度 | 支持发布版 | 质量 |
---|---|---|---|---|
eval | +++ | +++ | no | 生成代码 |
cheap-eval-source-map | + | ++ | no | 转换代码(lines only) |
cheap-source-map | + | o | yes | 转换代码(lines only) |
cheap-module-eval-source-map | o | ++ | no | 源代码 (lines only) |
cheap-module-source-map | o | – | yes | 源代码(lines only) |
eval-source-map | — | + | no | 源代码 |
source-map | — | — | yes | 源代码 |
来自官方文档:
Loaders allow you to preprocess files as you require() or “load” them. Loaders are kind of like “tasks” are in other build tools, and provide a powerful way to handle frontend build steps. Loaders can transform files from a different language like CoffeeScript to JavaScript, or inline images as data URLs. Loaders even allow you to do things like require() css files right in your JavaScript!
module.exports = { entry: ["./global.js" , "./app.js"], output: { filename: "bundle.js" }, module: { loaders: [ { test: /\.es6$/, exclude: /node_modules/, loader: 'babel-loader', query: { presets: ['react', 'es2015'] } } ] }, resolve: { extensions: ['', '.js', '.es6'] }, }
咱们的第一个loader 添加了3个键,下面分别作下解释。
test — 一个正则表达式,测试什么样的文件类型能够经过loader去执行。上面的例子意思是仅后缀为.es6的文件经过。
exclude — 表示loader 应该忽略/不包含的文件/文件路径。例如 node_modules 文件夹.
loader —表示咱们正在使用的loader 名称 (babel-loader).
query — 你能够传递一些选项参数到loader,写法相似一个 query string 或者像上面的例子那样使用 query 属性。
presets —让咱们可以使用早先安装好的 react 和 es2015 的 presets。
var webpack = require('webpack'); var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; var ExtractTextPlugin = require('extract-text-webpack-plugin'); //自定义"魔力"变量 var definePlugin = new webpack.DefinePlugin({ __DEV__: JSON.stringify(JSON.parse(process.env.BUILD_DEV || 'false')), __PRERELEASE__: JSON.stringify(JSON.parse(process.env.BUILD_PRERELEASE || 'false')) }); module.exports = { //上下文 context: __dirname + '/src', //配置入口 entry: { a: './view/index/index.js', b: './view/index/b.js', vender: ['./view/index/c.js', './view/index/d.js'] }, //配置输出 output: { path: __dirname + '/build/', filename: '[name].js?[hash]', publicPath: '/assets/', sourceMapFilename: '[file].map' }, devtool: '#source-map', //模块 module: { loaders: [ { //处理javascript test: /\.js$/, exclude: /node_modules/, loader: 'babel' }, { test: /\.css$/, loader: ExtractTextPlugin.extract( "style-loader", "css-loader?sourceMap" ) }, { test: /\.less$/, loader: ExtractTextPlugin.extract( "style-loader", "css-loader!less-loader" ) }, { test: /\.(png|jpg)$/, loader: 'url-loader?limit=1024' }, { //处理vue test: /\.vue$/, loader: 'vue-loader' }, { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&minetype=application/font-woff' }, { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10&minetype=application/font-woff' }, { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10&minetype=application/octet-stream' }, { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file' }, { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10&minetype=image/svg+xml' } ] }, plugins: [ //公用模块 new CommonsChunkPlugin('common.js', ['a', 'b']), //设置抽出css文件名 new ExtractTextPlugin("css/[name].css?[hash]-[chunkhash]-[contenthash]-[name]", { disable: false, allChunks: true }), //定义全局变量 definePlugin, //设置此处,则在JS中不用相似require('./base')引入基础模块, 只要直接使用Base变量便可 //此处一般可用作,对经常使用组件,库的提早设置 new webpack.ProvidePlugin({ Moment: 'moment', //直接从node_modules中获取 Base: '../../base/index.js' //从文件中获取 }) ], //添加了此项,则代表从外部引入,内部不会打包合并进去 externals: { jquery: 'window.jQuery', react: 'window.React', //... } };
工欲善其事,必先利其器。