├── src ├── webpack.js ├── getTSCommonConfig.js ├── mergeCustomConfig.js ├── getBabelCommonConfig.js ├── transformLess.js ├── build.js └── getWebpackCommonConfig.js └── README.md /src/webpack.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | export default webpack; 3 | -------------------------------------------------------------------------------- /src/getTSCommonConfig.js: -------------------------------------------------------------------------------- 1 | /* 2 | (1)直接返回一个对象,这里是TypeScript的默认配置 3 | */ 4 | export default function ts() { 5 | return { 6 | target: 'es6', 7 | jsx: 'preserve', 8 | moduleResolution: 'node', 9 | declaration: false, 10 | sourceMap: true, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/mergeCustomConfig.js: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | 3 | /** 4 | * Merge custom config from `webpack.config.js`. 5 | * @param webpackConfig {Object} 6 | * @param customConfigPath {String} 7 | */ 8 | 9 | export default function mergeCustomConfig(webpackConfig, customConfigPath) { 10 | if (!existsSync(customConfigPath)) { 11 | return webpackConfig; 12 | } 13 | const customConfig = require(customConfigPath); 14 | if (typeof customConfig === 'function') { 15 | return customConfig(webpackConfig, ...[...arguments].slice(2)); 16 | } 17 | 18 | throw new Error(`Return of ${customConfigPath} must be a function.`); 19 | } -------------------------------------------------------------------------------- /src/getBabelCommonConfig.js: -------------------------------------------------------------------------------- 1 | import { tmpdir } from 'os'; 2 | /* 3 | (1)调用方式如下: 4 | getBabelCommonConfig(); 5 | (2)tmpdir方法作用 6 | The os.tmpdir() method returns a string specifying the operating system's default directory for 7 | temporary files. 8 | */ 9 | export default function babel() { 10 | return { 11 | cacheDirectory: tmpdir(), 12 | presets: [ 13 | require.resolve('babel-preset-es2015-ie'), 14 | require.resolve('babel-preset-react'), 15 | require.resolve('babel-preset-stage-0'), 16 | ], 17 | plugins: [ 18 | require.resolve('babel-plugin-add-module-exports'), 19 | require.resolve('babel-plugin-transform-decorators-legacy'), 20 | ], 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/transformLess.js: -------------------------------------------------------------------------------- 1 | import less from 'less'; 2 | import { readFileSync } from 'fs'; 3 | import path, { dirname } from 'path'; 4 | import postcss from 'postcss'; 5 | import rucksack from 'rucksack-css'; 6 | import autoprefixer from 'autoprefixer'; 7 | import NpmImportPlugin from 'less-plugin-npm-import'; 8 | 9 | function transformLess(lessFile, config = {}) { 10 | const { cwd = process.cwd() } = config; 11 | const resolvedLessFile = path.resolve(cwd, lessFile); 12 | 13 | let data = readFileSync(resolvedLessFile, 'utf-8'); 14 | data = data.replace(/^\uFEFF/, ''); 15 | 16 | return new Promise((resolve, reject) => { 17 | // Do less compile 18 | const lessOpts = { 19 | paths: [dirname(resolvedLessFile)], 20 | filename: resolvedLessFile, 21 | plugins: [ 22 | new NpmImportPlugin({ prefix: '~' }), 23 | ], 24 | }; 25 | less.render(data, lessOpts) 26 | .then(result => { 27 | // Do postcss compile 28 | const plugins = [ 29 | rucksack(), 30 | autoprefixer({ 31 | browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 8'], 32 | }), 33 | ]; 34 | const source = result.css; 35 | const postcssOpts = {}; 36 | 37 | postcss(plugins).process(source, postcssOpts) 38 | .then(r => { 39 | resolve(r.css); 40 | }) 41 | .catch(err => { 42 | reject(err); 43 | }); 44 | }) 45 | .catch(err => { 46 | reject(err); 47 | }); 48 | }); 49 | } 50 | 51 | export default transformLess; 52 | -------------------------------------------------------------------------------- /src/build.js: -------------------------------------------------------------------------------- 1 | import { join, resolve } from 'path'; 2 | import { writeFileSync } from 'fs'; 3 | import webpack, { ProgressPlugin } from 'webpack'; 4 | import chalk from 'chalk'; 5 | import mergeCustomConfig from './mergeCustomConfig'; 6 | import getWebpackCommonConfig from './getWebpackCommonConfig'; 7 | /* 8 | (1)调用方式如下: 9 | let webpackConfig = getWebpackConfig(args, {}); 10 | (2)可以通过命令传入的outputPath,publicPath,compress,hash,config进行配置 11 | 12 | */ 13 | function getWebpackConfig(args, cache) { 14 | let webpackConfig = getWebpackCommonConfig(args); 15 | webpackConfig.plugins = webpackConfig.plugins || []; 16 | 17 | // Config outputPath. 18 | if (args.outputPath) { 19 | webpackConfig.output.path = args.outputPath; 20 | } 21 | 22 | if (args.publicPath) { 23 | webpackConfig.output.publicPath = args.publicPath; 24 | } 25 | 26 | // Config if no --no-compress. 27 | //表示要对输出的内容进行压缩 28 | if (args.compress) { 29 | webpackConfig.UglifyJsPluginConfig = { 30 | output: { 31 | ascii_only: true, 32 | }, 33 | compress: { 34 | warnings: false, 35 | }, 36 | }; 37 | webpackConfig.plugins = [...webpackConfig.plugins, 38 | new webpack.optimize.UglifyJsPlugin(webpackConfig.UglifyJsPluginConfig), 39 | new webpack.DefinePlugin({ 40 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 41 | }), 42 | ]; 43 | } else { 44 | //https://cnodejs.org/topic/5785b3ef3b501f7054982f69 45 | if (process.env.NODE_ENV) { 46 | webpackConfig.plugins = [...webpackConfig.plugins, 47 | new webpack.DefinePlugin({ 48 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 49 | }), 50 | ]; 51 | } 52 | } 53 | 54 | webpackConfig.plugins = [...webpackConfig.plugins, 55 | new webpack.optimize.DedupePlugin(), 56 | new webpack.NoErrorsPlugin(), 57 | ]; 58 | 59 | // Output map.json if hash. 60 | if (args.hash) { 61 | const pkg = require(join(args.cwd, 'package.json')); 62 | //把output.filename和output.chunkFilename设置为加上hash的方式 63 | webpackConfig.output.filename = webpackConfig.output.chunkFilename = '[name]-[chunkhash].js'; 64 | webpackConfig.plugins = [...webpackConfig.plugins, 65 | require('map-json-webpack-plugin')({ 66 | assetsPath: pkg.name,//项目名称,会放置在项目根路径 67 | cache, 68 | }), 69 | ]; 70 | } 71 | 72 | //如果shell命令传入了config是函数,那么回调这个函数 73 | if (typeof args.config === 'function') { 74 | webpackConfig = args.config(webpackConfig) || webpackConfig; 75 | } else { 76 | webpackConfig = mergeCustomConfig(webpackConfig, resolve(args.cwd, args.config || 'webpack.config.js')); 77 | } 78 | return webpackConfig; 79 | } 80 | 81 | 82 | /* 83 | (1)调用方式有两种,分别如下: 84 | require('../lib/build')(program); 85 | require('../lib/build')(program, function () { 86 | process.exit(0); 87 | }); 88 | (2)其中args是进程传过来的参数,通过commander模块解析出来的 89 | */ 90 | export default function build(args, callback) { 91 | // Get config. 92 | let webpackConfig = getWebpackConfig(args, {}); 93 | //这里是把shell传入的options和默认的option进行配置后得到的最终options 94 | webpackConfig = Array.isArray(webpackConfig) ? webpackConfig : [webpackConfig]; 95 | let fileOutputPath; 96 | webpackConfig.forEach(config => { 97 | fileOutputPath = config.output.path; 98 | }); 99 | //获取最终的config.output.path属性表示最终的输出路径 100 | 101 | if (args.watch) { 102 | webpackConfig.forEach(config => { 103 | config.plugins.push( 104 | new ProgressPlugin((percentage, msg) => { 105 | const stream = process.stderr; 106 | if (stream.isTTY && percentage < 0.71) { 107 | stream.cursorTo(0); 108 | stream.write(`📦 ${chalk.magenta(msg)}`); 109 | stream.clearLine(1); 110 | } else if (percentage === 1) { 111 | console.log(chalk.green('\nwebpack: bundle build is now finished.')); 112 | } 113 | }) 114 | ); 115 | }); 116 | } 117 | //如果配置了watch,表示要监听,我们加入ProgressPlugin 118 | 119 | function doneHandler(err, stats) { 120 | //shell中配置了json参数,那么在fileOutputPath = config.output.path;也就是config.output.path 121 | //中输出我们的json文件 122 | if (args.json) { 123 | const filename = typeof args.json === 'boolean' ? 'build-bundle.json' : args.json; 124 | const jsonPath = join(fileOutputPath, filename); 125 | writeFileSync(jsonPath, JSON.stringify(stats.toJson()), 'utf-8'); 126 | console.log(`Generate Json File: ${jsonPath}`); 127 | } 128 | //如果出错,那么退出码是1 129 | const { errors } = stats.toJson(); 130 | if (errors && errors.length) { 131 | process.on('exit', () => { 132 | process.exit(1); 133 | }); 134 | } 135 | // if watch enabled only stats.hasErrors would log info 136 | // otherwise would always log info 137 | if (!args.watch || stats.hasErrors()) { 138 | const buildInfo = stats.toString({ 139 | colors: true, 140 | children: true,//添加子模块的信息,https://github.com/webpack/extract-text-webpack-plugin/issues/35 141 | chunks: !!args.verbose, 142 | modules: !!args.verbose, 143 | chunkModules: !!args.verbose, 144 | hash: !!args.verbose,//如果verbose为true表示有日志,那么我们会输出这部分内容 145 | version: !!args.verbose, 146 | }); 147 | if (stats.hasErrors()) { 148 | console.error(buildInfo); 149 | } else { 150 | console.log(buildInfo); 151 | } 152 | } 153 | if (err) { 154 | process.on('exit', () => { 155 | process.exit(1); 156 | }); 157 | console.error(err); 158 | } 159 | 160 | if (callback) { 161 | callback(err); 162 | } 163 | } 164 | 165 | // Run compiler. 166 | //webpack返回的是一个Compiler实例对象 167 | const compiler = webpack(webpackConfig); 168 | // Hack: remove extract-text-webpack-plugin log 169 | //verbose: 是否输出过程日志 170 | if (!args.verbose) { 171 | compiler.plugin('done', (stats) => { 172 | stats.stats.forEach((stat) => { 173 | //compilation.children是他所有依赖的模块信息 174 | stat.compilation.children = stat.compilation.children.filter((child) => { 175 | return child.name !== 'extract-text-webpack-plugin'; 176 | }); 177 | }); 178 | }); 179 | } 180 | //调用compiler对象的核心方法watch和run方法 181 | if (args.watch) { 182 | compiler.watch(args.watch || 200, doneHandler); 183 | } else { 184 | compiler.run(doneHandler); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/getWebpackCommonConfig.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import getBabelCommonConfig from './getBabelCommonConfig'; 4 | import getTSCommonConfig from './getTSCommonConfig'; 5 | import { existsSync } from 'fs'; 6 | import { join, resolve } from 'path'; 7 | import rucksack from 'rucksack-css'; 8 | import autoprefixer from 'autoprefixer'; 9 | /* 10 | (1)调用方式如下: 11 | let webpackConfig = getWebpackCommonConfig(args);其中args是通过进程传过来的,也就是解析process.argv 12 | (2)如果传入的参数有hash,那么我们会对css/js和普通文件名进行修改,而且这里采用的是chunkHash 13 | const jsFileName = args.hash ? '[name]-[chunkhash].js' : '[name].js'; 14 | const cssFileName = args.hash ? '[name]-[chunkhash].css' : '[name].css'; 15 | const commonName = args.hash ? 'common-[chunkhash].js' : 'common.js'; 16 | (3)获取babel特定的配置信息,cacheDirectory,presets,plugins 17 | (4)getTSCommonConfig返回如下内容: 18 | return { 19 | target: 'es6', 20 | jsx: 'preserve', 21 | moduleResolution: 'node', 22 | declaration: false,//设置为false 23 | sourceMap: true, 24 | }; 25 | (5)获取package.json中配置的theme,如果theme是string,那么表示是路径,如果是相对路径那么转化为绝对路径并获取 26 | 该路径的文件内容,并调用默认theme文件默认导出的方法!如果配置的是一个对象,那么表示这个对象就是theme! 27 | (6)读取package.json中的browser对象 28 | 29 | 30 | */ 31 | export default function getWebpackCommonConfig(args) { 32 | const pkgPath = join(args.cwd, 'package.json'); 33 | const pkg = existsSync(pkgPath) ? require(pkgPath) : {}; 34 | const jsFileName = args.hash ? '[name]-[chunkhash].js' : '[name].js'; 35 | const cssFileName = args.hash ? '[name]-[chunkhash].css' : '[name].css'; 36 | const commonName = args.hash ? 'common-[chunkhash].js' : 'common.js'; 37 | const babelQuery = getBabelCommonConfig(); 38 | const tsQuery = getTSCommonConfig(); 39 | tsQuery.declaration = false; 40 | 41 | let theme = {}; 42 | if (pkg.theme && typeof(pkg.theme) === 'string') { 43 | let cfgPath = pkg.theme; 44 | // relative path 45 | if (cfgPath.charAt(0) === '.') { 46 | cfgPath = resolve(args.cwd, cfgPath); 47 | } 48 | const getThemeConfig = require(cfgPath); 49 | theme = getThemeConfig(); 50 | } else if (pkg.theme && typeof(pkg.theme) === 'object') { 51 | theme = pkg.theme; 52 | } 53 | 54 | const emptyBuildins = [ 55 | 'child_process', 56 | 'cluster', 57 | 'dgram', 58 | 'dns', 59 | 'fs', 60 | 'module', 61 | 'net', 62 | 'readline', 63 | 'repl', 64 | 'tls', 65 | ]; 66 | 67 | const browser = pkg.browser || {}; 68 | const node = emptyBuildins.reduce((obj, name) => { 69 | //如果browser里面没有这个模块,那么我们会把obj对象上这个模块的信息设置为'empty'字符串 70 | if (!(name in browser)) { 71 | return { ...obj, ...{ [name]: 'empty' } }; 72 | } 73 | return obj; 74 | }, {}); 75 | 76 | 77 | return { 78 | babel: babelQuery, 79 | ts: { 80 | transpileOnly: true, 81 | compilerOptions: tsQuery, 82 | }, 83 | 84 | output: { 85 | path: join(process.cwd(), './dist/'), 86 | filename: jsFileName, 87 | chunkFilename: jsFileName, 88 | }, 89 | 90 | devtool: args.devtool,//source-map 91 | 92 | resolve: { 93 | modulesDirectories: ['node_modules', join(__dirname, '../node_modules')], 94 | //本层级的node_modules和上一级node_modules 95 | extensions: ['', '.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.ts', '.tsx', '.js', '.jsx', '.json'], 96 | //扩展名 97 | }, 98 | 99 | resolveLoader: { 100 | modulesDirectories: ['node_modules', join(__dirname, '../node_modules')], 101 | }, 102 | 103 | entry: pkg.entry, 104 | //package.json中配置的entry对象 105 | 106 | node, 107 | 108 | module: { 109 | noParse: [/moment.js/],//不解析moment.js 110 | loaders: [ 111 | { 112 | test: /\.js$/, 113 | exclude: /node_modules/, 114 | loader: require.resolve('babel-loader'), 115 | query: babelQuery, 116 | }, 117 | { 118 | test: /\.jsx$/, 119 | loader: require.resolve('babel-loader'), 120 | query: babelQuery, 121 | }, 122 | { 123 | test: /\.tsx?$/, 124 | loaders: [require.resolve('babel-loader'), require.resolve('ts-loader')], 125 | }, 126 | { 127 | test(filePath) { 128 | return /\.css$/.test(filePath) && !/\.module\.css$/.test(filePath); 129 | }, 130 | loader: ExtractTextPlugin.extract( 131 | `${require.resolve('css-loader')}` + 132 | `?sourceMap&-restructuring&-autoprefixer!${require.resolve('postcss-loader')}` 133 | ), 134 | }, 135 | { 136 | test: /\.module\.css$/, 137 | loader: ExtractTextPlugin.extract( 138 | `${require.resolve('css-loader')}` + 139 | `?sourceMap&-restructuring&modules&localIdentName=[local]___[hash:base64:5]&-autoprefixer!` + 140 | `${require.resolve('postcss-loader')}` 141 | ), 142 | }, 143 | { 144 | test(filePath) { 145 | return /\.less$/.test(filePath) && !/\.module\.less$/.test(filePath); 146 | }, 147 | loader: ExtractTextPlugin.extract( 148 | `${require.resolve('css-loader')}?sourceMap&-autoprefixer!` + 149 | `${require.resolve('postcss-loader')}!` + 150 | `${require.resolve('less-loader')}?{"sourceMap":true,"modifyVars":${JSON.stringify(theme)}}` 151 | ), 152 | }, 153 | { 154 | test: /\.module\.less$/, 155 | loader: ExtractTextPlugin.extract( 156 | `${require.resolve('css-loader')}?` + 157 | `sourceMap&modules&localIdentName=[local]___[hash:base64:5]&-autoprefixer!` + 158 | `${require.resolve('postcss-loader')}!` + 159 | `${require.resolve('less-loader')}?` + 160 | `{"sourceMap":true,"modifyVars":${JSON.stringify(theme)}}` 161 | ), 162 | }, 163 | { 164 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 165 | loader: `${require.resolve('url-loader')}?` + 166 | `limit=10000&minetype=application/font-woff`, 167 | }, 168 | { 169 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 170 | loader: `${require.resolve('url-loader')}?` + 171 | `limit=10000&minetype=application/font-woff`, 172 | }, 173 | { 174 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 175 | loader: `${require.resolve('url-loader')}?` + 176 | `limit=10000&minetype=application/octet-stream`, 177 | }, 178 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: `${require.resolve('file-loader')}` }, 179 | { 180 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 181 | loader: `${require.resolve('url-loader')}?` + 182 | `limit=10000&minetype=image/svg+xml`, 183 | }, 184 | { 185 | test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i, 186 | loader: `${require.resolve('url-loader')}?limit=10000`, 187 | }, 188 | { test: /\.json$/, loader: `${require.resolve('json-loader')}` }, 189 | { test: /\.html?$/, loader: `${require.resolve('file-loader')}?name=[name].[ext]` }, 190 | ], 191 | }, 192 | 193 | postcss: [ 194 | rucksack(), 195 | autoprefixer({ 196 | browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 8', 'iOS >= 8', 'Android >= 4'], 197 | }), 198 | ], 199 | 200 | plugins: [ 201 | new webpack.optimize.CommonsChunkPlugin('common', commonName), 202 | //公共模块名字 203 | new ExtractTextPlugin(cssFileName, { 204 | disable: false, 205 | allChunks: true, 206 | }), 207 | //css文件名字 208 | new webpack.optimize.OccurenceOrderPlugin(), 209 | //顺序触发的插件 210 | ], 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 1.说在前面的话 2 | 这篇文章来自于我的[github文章全集](https://github.com/liangklfangl/react-article-bucket)。atool-build本身是基于webpack1的,如果你使用的是webpack2,可以试试[wcf](https://github.com/liangklfangl/wcf)。这是在atool-build基础上开发的,集成了webpack-dev-server(启动了webpack-dev-server打包), webpack watch(webpack自身的watch模式,监听文件变化重新打包), webpack(打包一次然后退出)三种打包方式。因为webpack2更新后我们下面描述的很多plugin都已经移除而内置了,同时很多配置项都已经失效了,以前出现的问题都已经解决了。所以我还是强烈建议更新到webpack2的。对于该打包工具,我将它用于[React全家桶完整实例详解](https://github.com/liangklfangl/react-universal-bucket)的开发中,并与Babel插件,React常用组件结合,实现了服务端渲染与同构,代理于与反代理服务器,自定义bootstrap,高阶组件与逻辑复用一系列的功能 3 | 4 | ### 2.atool-build的简单说明 5 | 废话不多说,请看下面内容: 6 | 7 | atool-build脚手架只是对webpack进行了简单的封装。 8 | 9 | 首先,webpack/babel/TypeScript那些基本配置信息都有了默认信息,并内置了很多默认的`loader`来处理文件;然后,他是自己调用`compiler.run`方法开始编译的,并通过compiler.watch来监听文件的变化,生产[build-bundle.json](https://github.com/liangklfangl/commonchunkplugin-source-code)表示编译的信息; 10 | 11 | 然后,里面通过一个hack来解决extract-webpack-text-plugin打印log的问题;Babel的缓存目录会使用操作系统默认的缓存目录来完成,使用os模块的tmpdir方法;其中devtool采用的是如下的方式加载: 12 | 13 | ```js 14 | webpack --devtool source-map 15 | ``` 16 | 17 | (1)如果在 package.json 中 browser 没有设置,则设置 child_process, cluster, dgram, dns, fs, module, net, readline, repl, tls 为 `empty`! 18 | 19 | (2)对于自定义添加的 loader,plugin等依赖,需要在项目文件夹中npm install 这些依赖。但不需要再安装 webpack 依赖,因为可以通过 require('atool-build/lib/webpack') 得到; 20 | 21 | ### 3.atool-build官方配置项与内部处理 22 | 23 | 下面是atool-build给出的那些可以允许配置信息: 24 | 25 | (1): --verbose:是否在shell中传入verbose参数(表示是否输出过程日志) 26 | ```js 27 | //如果没有指定verbose 28 | if (!args.verbose) { 29 | compiler.plugin('done', (stats) => { 30 | stats.stats.forEach((stat) => { 31 | stat.compilation.children = stat.compilation.children.filter((child) => { 32 | return child.name !== 'extract-text-webpack-plugin'; 33 | }); 34 | }); 35 | }); 36 | } 37 | ``` 38 | 如果没有传入verbose,那么表示不允许输出日志。至于为什么是移除'extract-text-webpack-plugin'可以参见这个[hack](https://github.com/webpack/extract-text-webpack-plugin/issues/35) 39 | 40 | (2):--json 是否生成bundle.json文件 41 | ```js 42 | if (args.json) { 43 | const filename = typeof args.json === 'boolean' ? 'build-bundle.json' : args.json; 44 | const jsonPath = join(fileOutputPath, filename); 45 | writeFileSync(jsonPath, JSON.stringify(stats.toJson()), 'utf-8'); 46 | console.log(`Generate Json File: ${jsonPath}`); 47 | } 48 | ``` 49 | 表示是否在shell中配置了json参数,在doneHandle,也就是说每次修改都会调用这个方法,然后写一个默认为build-bundle.json文件: 50 | 51 | (3)-o, --output-path 指定构建后的输出路径。 52 | 处理如下: 53 | ```js 54 | //对应于webpack的output.path选项 55 | if (args.outputPath) { 56 | webpackConfig.output.path = args.outputPath; 57 | } 58 | ``` 59 | 60 | (4)-w, --watch [delpay] 是否监控文件变化,默认为不监控。内部处理如下: 61 | ```js 62 | if (args.watch) { 63 | compiler.watch(args.watch || 200, doneHandler); 64 | //启动compiler.watch监听文件变化 65 | } else { 66 | compiler.run(doneHandler); 67 | } 68 | ``` 69 | 也用于监控编译的过程 70 | ```js 71 | if (args.watch) { 72 | webpackConfig.forEach(config => { 73 | config.plugins.push( 74 | new ProgressPlugin((percentage, msg) => { 75 | const stream = process.stderr; 76 | if (stream.isTTY && percentage < 0.71) { 77 | stream.cursorTo(0); 78 | stream.write(`📦 ${chalk.magenta(msg)}`); 79 | stream.clearLine(1); 80 | } else if (percentage === 1) { 81 | console.log(chalk.green('\nwebpack: bundle build is now finished.')); 82 | } 83 | }) 84 | ); 85 | }); 86 | } 87 | ``` 88 | 89 | (5)--public-path 90 | 91 | 具体可以查看[该文档](https://github.com/webpack/docs/wiki/configuration#outputpublicpath),内部处理如下: 92 | ```js 93 | //对应于webpack的虚拟路径 94 | if (args.publicPath) { 95 | webpackConfig.output.publicPath = args.publicPath; 96 | } 97 | ``` 98 | 99 | (6)--no-compress 不压缩代码。 100 | 101 | ```js 102 | if (args.compress) {//配置为--no-compress表示不压缩 103 | webpackConfig.UglifyJsPluginConfig = { 104 | output: { 105 | ascii_only: true, 106 | }, 107 | compress: { 108 | warnings: false, 109 | }, 110 | }; 111 | webpackConfig.plugins = [...webpackConfig.plugins, 112 | new webpack.optimize.UglifyJsPlugin(webpackConfig.UglifyJsPluginConfig), 113 | new webpack.DefinePlugin({ 114 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), 115 | }), 116 | ]; 117 | } else { 118 | //https://cnodejs.org/topic/5785b3ef3b501f7054982f69 119 | if (process.env.NODE_ENV) { 120 | webpackConfig.plugins = [...webpackConfig.plugins, 121 | new webpack.DefinePlugin({ 122 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 123 | }), 124 | ]; 125 | } 126 | } 127 | ``` 128 | 如果压缩代码,那么我们添加UglifyJsPlugin。 129 | 130 | (7)--config [userConfigFile] 指定用户配置文件。默认为根目录下的 webpack.config.js文件。这个配置文件不是必须的。处理方式如下: 131 | ```js 132 | if (typeof args.config === 'function') { 133 | webpackConfig = args.config(webpackConfig) || webpackConfig; 134 | } else { 135 | webpackConfig = mergeCustomConfig(webpackConfig, resolve(args.cwd, args.config || 'webpack.config.js')); 136 | } 137 | ``` 138 | 也就说,如果config参数是一个函数,那么直接调用这个函数,否则获取路径并调用这个路径引入的文件的默认导出函数,传入参数为webpackConfig,下面是内置的mergeCustomConfig内部逻辑: 139 | ```js 140 | export default function mergeCustomConfig(webpackConfig, customConfigPath) { 141 | if (!existsSync(customConfigPath)) { 142 | return webpackConfig; 143 | } 144 | const customConfig = require(customConfigPath); 145 | if (typeof customConfig === 'function') { 146 | return customConfig(webpackConfig, ...[...arguments].slice(2)); 147 | } 148 | throw new Error(`Return of ${customConfigPath} must be a function.`); 149 | } 150 | ``` 151 | 注意,也就说如果我们传入的是config为file,*那么这个config必须导出的是一个函数*!但是在[wcf](https://github.com/liangklfangl/wcf)中我们采用了webpack-merge来合并配置项,更加灵活多变 152 | 153 | (8)--devtool 生成 sourcemap 的方法,默认为空,这个参数和 webpack 的配置一致。表示sourceMap的等级。 154 | 155 | (9)--hash 使用hash模式的构建, 并生成映射表map.json。内部的处理如下: 156 | ```js 157 | //如果指定了hash,那么我们的生成的文件名称为[name]-[chunkhash]这种类型 158 | if (args.hash) { 159 | const pkg = require(join(args.cwd, 'package.json')); 160 | webpackConfig.output.filename = webpackConfig.output.chunkFilename = '[name]-[chunkhash].js'; 161 | webpackConfig.plugins = [...webpackConfig.plugins, 162 | require('map-json-webpack-plugin')({ 163 | assetsPath: pkg.name,//项目名称,会放置在项目根路径 164 | cache, 165 | }), 166 | ]; 167 | } 168 | ``` 169 | 也就是说如果指定了hash,那么我们必须修改输出的文件名,即webpackConfig.output.filename 和webpackConfig.output.chunkFilename并添加hash。而且这里使用的是chunkhash,同时这里使用了map-json-webpack-plugin这个插件生成map.json映射文件。 170 | 171 | ### 4.atool-build中内置的那些插件 172 | 173 | (1)ProgressPlugin学习 174 | ```js 175 | config.plugins.push(new ProgressPlugin(percentage,msg)=>{ 176 | const stream=process.stderr; 177 | if(stream.isTTY&&percentage<0.71){ 178 | stream.cursorTo(0); 179 | stream.write(`📦 ${chalk.magenta(msg)}`); 180 | stream.clearLine(1); 181 | }else if(percentate==1){ 182 | console.log(chalk.green('webpack: bundle build is now finished.')); 183 | } 184 | }) 185 | 186 | ``` 187 | 该插件表示编译的进度。插件详见官方网站[阅读](https://webpack.github.io/docs/list-of-plugins.html) 188 | 189 | (2)NoErrorsPlugin 190 | 191 | 表示如果编译的时候有错误,那么我们跳过emit阶段,因此包含错误信息的资源都不会经过emit阶段也就是没有文件产生。这时候所有资源的emitted都是false。如果你使用CLI,那么当你使用这个插件的时候不会退出并产生一个error code,如果你想要CLI退出,那么使用bail选项。 192 | 193 | (3)DefinePlugin 194 | 表示允许你定义全局变量,可以用于在编译阶段和开发阶段进行不同的处理。用法如下: 195 | ```javascript 196 | new webpack.DefinePlugin({ 197 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 198 | }), 199 | ``` 200 | 201 | (4)DedupePlugin 202 | 查找相等或者相似的文件并从输出去剔除。在watch模式下不要使用,要在production下使用。 203 | ```js 204 | export default function mergeCustomConfig(webpackConfig, customConfigPath) { 205 | if (!existsSync(customConfigPath)) { 206 | return webpackConfig; 207 | } 208 | const customConfig = require(customConfigPath); 209 | //必须返回函数,也就是我们通过shell配置的args.config必须是返回一个函数! 210 | if (typeof customConfig === 'function') { 211 | return customConfig(webpackConfig, ...[...arguments].slice(2)); 212 | } 213 | throw new Error(`Return of ${customConfigPath} must be a function.`); 214 | } 215 | ``` 216 | 217 | ### 5.atool-build源码分析 218 | ```js 219 | export default function build(args, callback) { 220 | // Get config. 221 | let webpackConfig = getWebpackConfig(args, {}); 222 | //这里是把shell传入的options和默认的option进行配置后得到的最终options 223 | webpackConfig = Array.isArray(webpackConfig) ? webpackConfig : [webpackConfig]; 224 | let fileOutputPath; 225 | webpackConfig.forEach(config => { 226 | fileOutputPath = config.output.path; 227 | }); 228 | //获取最终的config.output.path属性表示最终的输出路径 229 | if (args.watch) { 230 | webpackConfig.forEach(config => { 231 | config.plugins.push( 232 | new ProgressPlugin((percentage, msg) => { 233 | const stream = process.stderr; 234 | if (stream.isTTY && percentage < 0.71) { 235 | stream.cursorTo(0); 236 | stream.write(`📦 ${chalk.magenta(msg)}`); 237 | stream.clearLine(1); 238 | } else if (percentage === 1) { 239 | console.log(chalk.green('\nwebpack: bundle build is now finished.')); 240 | } 241 | }) 242 | ); 243 | }); 244 | } 245 | //如果配置了watch,表示要监听,我们加入ProgressPlugin 246 | function doneHandler(err, stats) { 247 | //shell中配置了json参数,那么在fileOutputPath = config.output.path;也就是config.output.path 248 | //中输出我们的json文件 249 | if (args.json) { 250 | const filename = typeof args.json === 'boolean' ? 'build-bundle.json' : args.json; 251 | const jsonPath = join(fileOutputPath, filename); 252 | writeFileSync(jsonPath, JSON.stringify(stats.toJson()), 'utf-8'); 253 | console.log(`Generate Json File: ${jsonPath}`); 254 | } 255 | //如果出错,那么退出码是1 256 | const { errors } = stats.toJson(); 257 | if (errors && errors.length) { 258 | process.on('exit', () => { 259 | process.exit(1); 260 | }); 261 | } 262 | // if watch enabled only stats.hasErrors would log info 263 | // otherwise would always log info 264 | if (!args.watch || stats.hasErrors()) { 265 | const buildInfo = stats.toString({ 266 | colors: true, 267 | children: true,//添加子模块的信息,https://github.com/webpack/extract-text-webpack-plugin/issues/35 268 | chunks: !!args.verbose, 269 | modules: !!args.verbose, 270 | chunkModules: !!args.verbose, 271 | hash: !!args.verbose,//如果verbose为true表示有日志,那么我们会输出这部分内容 272 | version: !!args.verbose, 273 | }); 274 | if (stats.hasErrors()) { 275 | console.error(buildInfo); 276 | } else { 277 | console.log(buildInfo); 278 | } 279 | } 280 | if (err) { 281 | process.on('exit', () => { 282 | process.exit(1); 283 | }); 284 | console.error(err); 285 | } 286 | 287 | if (callback) { 288 | callback(err); 289 | } 290 | } 291 | // Run compiler. 292 | //webpack返回的是一个Compiler实例对象 293 | const compiler = webpack(webpackConfig); 294 | // Hack: remove extract-text-webpack-plugin log 295 | //verbose: 是否输出过程日志,这里是取消'extract-text-webpack-plugin'所有的日志信息 296 | if (!args.verbose) { 297 | compiler.plugin('done', (stats) => { 298 | stats.stats.forEach((stat) => { 299 | //compilation.children是他所有依赖的plugin信息 300 | stat.compilation.children = stat.compilation.children.filter((child) => { 301 | return child.name !== 'extract-text-webpack-plugin'; 302 | }); 303 | }); 304 | }); 305 | } 306 | //调用compiler对象的核心方法watch和run方法 307 | if (args.watch) { 308 | compiler.watch(args.watch || 200, doneHandler); 309 | } else { 310 | compiler.run(doneHandler); 311 | } 312 | } 313 | ``` 314 | 上面的代码是很容易看懂的,其实我们最重要的代码就是如下的内容: 315 | ```js 316 | function doneHandler(err, stats) { 317 | if (args.json) { 318 | const filename = typeof args.json === 'boolean' ? 'build-bundle.json' : args.json; 319 | const jsonPath = join(fileOutputPath, filename); 320 | writeFileSync(jsonPath, JSON.stringify(stats.toJson()), 'utf-8'); 321 | console.log(`Generate Json File: ${jsonPath}`); 322 | } 323 | //如果出错,那么退出码是1 324 | const { errors } = stats.toJson(); 325 | if (errors && errors.length) { 326 | process.on('exit', () => { 327 | process.exit(1); 328 | }); 329 | } 330 | // if watch enabled only stats.hasErrors would log info 331 | // otherwise would always log info 332 | if (!args.watch || stats.hasErrors()) { 333 | const buildInfo = stats.toString({ 334 | colors: true, 335 | children: true,//添加子模块的信息,https://github.com/webpack/extract-text-webpack-plugin/issues/35 336 | chunks: !!args.verbose, 337 | modules: !!args.verbose, 338 | chunkModules: !!args.verbose, 339 | hash: !!args.verbose,//如果verbose为true表示有日志,那么我们会输出这部分内容 340 | version: !!args.verbose, 341 | }); 342 | if (stats.hasErrors()) { 343 | console.error(buildInfo); 344 | } else { 345 | console.log(buildInfo); 346 | } 347 | } 348 | if (err) { 349 | process.on('exit', () => { 350 | process.exit(1); 351 | }); 352 | console.error(err); 353 | } 354 | if (callback) { 355 | callback(err); 356 | } 357 | } 358 | ``` 359 | 360 | 因为我们调用compiler.watch方法,在webpack中,其会调用Watching对象的watch方法监听文件的变化,每次变化的时候我们只是重新生成我们的'build-bundle.json'文件表示本次编译的信息!而且在webpack的watch的回调函数,也就是doneHandler中每次都会传入Stats对象,如果你还不知道可以查看下面这个[文章](https://github.com/liangklfangl/webpack-compiler-and-compilation) 361 | 362 | ### 6.TypeScript默认配置项 363 | ```js 364 | export default function ts() { 365 | return { 366 | target: 'es6', 367 | jsx: 'preserve', 368 | moduleResolution: 'node', 369 | declaration: false, 370 | sourceMap: true, 371 | }; 372 | } 373 | ``` 374 | 375 | ### 7.Babel配置项 376 | ```js 377 | export default function babel() { 378 | return { 379 | cacheDirectory: tmpdir(),//临时文件存放位置 380 | presets: [//presets字段设定转码规则 381 | require.resolve('babel-preset-es2015-ie'), 382 | require.resolve('babel-preset-react'), 383 | require.resolve('babel-preset-stage-0'), 384 | ], 385 | plugins: [ 386 | require.resolve('babel-plugin-add-module-exports'), 387 | require.resolve('babel-plugin-transform-decorators-legacy'), 388 | ], 389 | }; 390 | } 391 | ``` 392 | 上面tmpdir的作用如下: 393 | 394 | The *os.tmpdir()* method returns a string specifying the operating system's default directory for temporary files. 395 | 396 | 关于Babel的其他内容,你可以查看[React全家桶完整实例](https://github.com/liangklfangl/react-universal-bucket),其详述了如Babel操作AST抽象语法树等一系列高级内容。 397 | 398 | ### 8.Webpack默认配置项 399 | 直接上源码部分,再分开分析下: 400 | ```js 401 | export default function getWebpackCommonConfig(args) { 402 | const pkgPath = join(args.cwd, 'package.json'); 403 | const pkg = existsSync(pkgPath) ? require(pkgPath) : {}; 404 | const jsFileName = args.hash ? '[name]-[chunkhash].js' : '[name].js'; 405 | const cssFileName = args.hash ? '[name]-[chunkhash].css' : '[name].css'; 406 | const commonName = args.hash ? 'common-[chunkhash].js' : 'common.js'; 407 | //如果传入hash,那么输出文件名要修改。 408 | const babelQuery = getBabelCommonConfig(); 409 | const tsQuery = getTSCommonConfig(); 410 | //获取TypeScript配置 411 | tsQuery.declaration = false; 412 | let theme = {}; 413 | if (pkg.theme && typeof(pkg.theme) === 'string') { 414 | let cfgPath = pkg.theme; 415 | // relative path 416 | if (cfgPath.charAt(0) === '.') { 417 | cfgPath = resolve(args.cwd, cfgPath); 418 | } 419 | const getThemeConfig = require(cfgPath); 420 | theme = getThemeConfig(); 421 | } else if (pkg.theme && typeof(pkg.theme) === 'object') { 422 | theme = pkg.theme; 423 | } 424 |  //为什么引入下面的部分可以参考官方https://webpack.js.org/configuration/node/ 425 |  //目的是为了解决如果是打包为浏览器的bundle的时候却有nodejs的核心模块,导致打包出错的问题。一般当你用webpack打包node模块才会抛出这个错误 426 |  const emptyBuildins = [ 427 | 'child_process', 428 | 'cluster', 429 | 'dgram', 430 | 'dns', 431 | 'fs', 432 | 'module', 433 | 'net', 434 | 'readline', 435 | 'repl', 436 | 'tls', 437 | ]; 438 | const browser = pkg.browser || {}; 439 | const node = emptyBuildins.reduce((obj, name) => { 440 | //如果browser里面没有这个模块,那么我们会把obj对象上这个模块的信息设置为'empty'字符串 441 | if (!(name in browser)) { 442 | return { ...obj, ...{ [name]: 'empty' } }; 443 | } 444 | return obj; 445 | }, {}); 446 | return { 447 | babel: babelQuery, 448 | ts: { 449 | transpileOnly: true, 450 | compilerOptions: tsQuery, 451 | }, 452 | output: { 453 | path: join(process.cwd(), './dist/'), 454 | filename: jsFileName, 455 | chunkFilename: jsFileName, 456 | }, 457 | devtool: args.devtool,//source-map 458 | resolve: { 459 | modulesDirectories: ['node_modules', join(__dirname, '../node_modules')], 460 | //本层级的node_modules和上一级node_modules 461 | extensions: ['', '.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.ts', '.tsx', '.js', '.jsx', '.json'], 462 | //扩展名 463 | }, 464 | resolveLoader: { 465 | modulesDirectories: ['node_modules', join(__dirname, '../node_modules')], 466 | }, 467 | entry: pkg.entry, 468 | //package.json中配置的entry对象 469 | node, 470 | module: { 471 | noParse: [/moment.js/],//不解析moment.js 472 | loaders: [ 473 | { 474 | test: /\.js$/, 475 | exclude: /node_modules/, 476 | loader: require.resolve('babel-loader'), 477 | query: babelQuery, 478 | }, 479 | { 480 | test: /\.jsx$/, 481 | loader: require.resolve('babel-loader'), 482 | query: babelQuery, 483 | }, 484 | { 485 | test: /\.tsx?$/, 486 | loaders: [require.resolve('babel-loader'), require.resolve('ts-loader')], 487 | }, 488 | { 489 | test(filePath) { 490 | return /\.css$/.test(filePath) && !/\.module\.css$/.test(filePath); 491 | }, 492 | loader: ExtractTextPlugin.extract( 493 | `${require.resolve('css-loader')}` + 494 | `?sourceMap&-restructuring&-autoprefixer!${require.resolve('postcss-loader')}` 495 | ), 496 | }, 497 | { 498 | test: /\.module\.css$/, 499 | loader: ExtractTextPlugin.extract( 500 | `${require.resolve('css-loader')}` + 501 | `?sourceMap&-restructuring&modules&localIdentName=[local]___[hash:base64:5]&-autoprefixer!` + 502 | `${require.resolve('postcss-loader')}` 503 | ), 504 | }, 505 | { 506 | test(filePath) { 507 | return /\.less$/.test(filePath) && !/\.module\.less$/.test(filePath); 508 | }, 509 | loader: ExtractTextPlugin.extract( 510 | `${require.resolve('css-loader')}?sourceMap&-autoprefixer!` + 511 | `${require.resolve('postcss-loader')}!` + 512 | `${require.resolve('less-loader')}?{"sourceMap":true,"modifyVars":${JSON.stringify(theme)}}` 513 | ), 514 | }, 515 | { 516 | test: /\.module\.less$/, 517 | loader: ExtractTextPlugin.extract( 518 | `${require.resolve('css-loader')}?` + 519 | `sourceMap&modules&localIdentName=[local]___[hash:base64:5]&-autoprefixer!` + 520 | `${require.resolve('postcss-loader')}!` + 521 | `${require.resolve('less-loader')}?` + 522 | `{"sourceMap":true,"modifyVars":${JSON.stringify(theme)}}` 523 | ), 524 | }, 525 | { 526 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 527 | loader: `${require.resolve('url-loader')}?` + 528 | `limit=10000&minetype=application/font-woff`, 529 | }, 530 | { 531 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 532 | loader: `${require.resolve('url-loader')}?` + 533 | `limit=10000&minetype=application/font-woff`, 534 | }, 535 | { 536 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 537 | loader: `${require.resolve('url-loader')}?` + 538 | `limit=10000&minetype=application/octet-stream`, 539 | }, 540 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: `${require.resolve('file-loader')}` }, 541 | { 542 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 543 | loader: `${require.resolve('url-loader')}?` + 544 | `limit=10000&minetype=image/svg+xml`, 545 | }, 546 | { 547 | test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i, 548 | loader: `${require.resolve('url-loader')}?limit=10000`, 549 | }, 550 | { test: /\.json$/, loader: `${require.resolve('json-loader')}` }, 551 | { test: /\.html?$/, loader: `${require.resolve('file-loader')}?name=[name].[ext]` }, 552 | ], 553 | }, 554 | postcss: [ 555 | rucksack(), 556 | autoprefixer({ 557 | browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 8', 'iOS >= 8', 'Android >= 4'], 558 | }), 559 | ], 560 | plugins: [ 561 | new webpack.optimize.CommonsChunkPlugin('common', commonName), 562 | //公共模块名字 563 | new ExtractTextPlugin(cssFileName, { 564 | disable: false, 565 | allChunks: true, 566 | }), 567 | //css文件名字 568 | new webpack.optimize.OccurenceOrderPlugin(), 569 | //顺序触发的插件 570 | ], 571 | }; 572 | } 573 | ``` 574 | 我们是如下调用的: 575 | ```js 576 | let webpackConfig = getWebpackCommonConfig(args); 577 | ``` 578 | 而我们的args表示从shell控制台传入的参数,这些参数会被原样传入到上面的getWebpackCommonConfig方法中。但是,我们依然要弄清楚下面的内容: 579 | ```js 580 | let theme = {}; 581 | if (pkg.theme && typeof(pkg.theme) === 'string') { 582 | let cfgPath = pkg.theme; 583 | // relative path 584 | if (cfgPath.charAt(0) === '.') { 585 | cfgPath = resolve(args.cwd, cfgPath); 586 | } 587 | const getThemeConfig = require(cfgPath); 588 | theme = getThemeConfig(); 589 | } else if (pkg.theme && typeof(pkg.theme) === 'object') { 590 | theme = pkg.theme; 591 | } 592 | ``` 593 | 我们可以在package.json中配置theme选项,如果配置为对象,那么就是theme内容,否则如果是文件那么我们require进来,然后调用默认的方法!这也就是告诉我们,我们配置的这个文件名导出的内容`必须是一个函数`!那么这个theme有什么用呢?其实这是less为我们提供的覆盖less文件默认配置的变量的方法!我们在package.json中配置的theme会被传入到以下的插件中: 594 | ExtractTextPlugin 595 | 596 | ```js 597 | { 598 | test(filePath) { 599 | return /\.less$/.test(filePath) && !/\.module\.less$/.test(filePath); 600 | }, 601 | loader: ExtractTextPlugin.extract( 602 | `${require.resolve('css-loader')}?sourceMap&-autoprefixer!` + 603 | `${require.resolve('postcss-loader')}!` + 604 | `${require.resolve('less-loader')}?{"sourceMap":true,"modifyVars":${JSON.stringify(theme)}}` 605 | ), 606 | } 607 | ``` 608 | 首先:一种文件可以使用多个loader来完成;然后:我们可以使用?为不同的loader添加参数并且注意哪些参数是变量哪些参数是字符串!比如对于less-loader来说,我们使用了modifyVars来覆盖原来的样式,因为在loader里面会通过query读取查询字符串,然后做相应的覆盖(因为less里面使用了变量)。 609 | ```less 610 | less.modifyVars({ 611 | '@buttonFace': '#5B83AD', 612 | '@buttonText': '#D9EEF2' 613 | }); 614 | ``` 615 | 详见链接:[modifyVars](http://lesscss.org/usage/#using-less-in-the-browser-modify-variables) 616 | 617 | ### 9.webpack/TypeScript/Babel基本配置的含义 618 | 为什么说getWebpackCommonConfig返回的是一个webpack的common配置信息,这些信息都是什么意思?为何说getBabelCommonConfig.js得到的是babel的基本配置,配置是什么意思?getTSCommonConfig得到的又是什么配置?这些内容不再一一赘述,读者可自行google. 619 | 620 | ### 10.wcf vs atool-build 621 | 最后打一个小广告说一下[wcf](https://github.com/liangklfangl/wcf)与atool-build的区别,如果你有兴趣,也欢迎star,issue,贡献代码: 622 | 623 | (1)wcf集成了三种打包模式 624 | 625 | 上面已经说过了,我们的wcf集成了三种打包模式,而且功能是逐渐增强的。*webpack模式*只是打包一次,然后退出,和webpack自己的打包方式是一样的。*webpack watch模式*会自动监听文件是否发生变化,然后重新打包。*webpack-dev-server模式*天然支持了HMR,支持无刷新更新数据。具体你可以[阅读文档](https://github.com/liangklfangl/wcf) 626 | 627 | (2)很好的扩展性 628 | 629 | atool-build提供一个mergeCustomConfig函数来合并用户自定义的配置与默认的配置,并将用户配置作为参数传入函数进行修改,但是当要修改的配置项很多的时候就比较麻烦。wcf自己也集成了很多loader对文件进行处理,但是很容易进行拓展,只要你配置自己的扩展文件就可以了,内部操作都会自动完成。你可以通过两种方式来配置: 630 | 631 | cli模式: 632 | ```js 633 | wcf --dev --devServer --config "Your custom webpack config file path" 634 | //此时会自动合并用户自定义的配置与默认配置,通过webpack-merge完成,而不用逐项修改 635 | ``` 636 | Nodejs模式: 637 | ```js 638 | const build = require("webpackcc/lib/build"); 639 | const program = { 640 | onlyCf : true, 641 | //不启动打包,只是获取最终配置信息 642 | cwd : process.cwd(), 643 | dev : true, 644 | //开发模式,不启动如UglifyJs等 645 | config :"Your custom webpack config file path" 646 | }; 647 | const finalConfig = build(program); 648 | //得到最终的配置,想干嘛干嘛 649 | ``` 650 | 通过nodejs模式,你可以获取webpack配置项用于其他地方。 651 | 652 | 下面给出一个完整的例子(假如下面给出的是我们*自定义的配置文件*): 653 | ```js 654 | module.exports = { 655 | entry:{ 656 | 'main': [ 657 | 'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr', 658 | "bootstrap-webpack!./src/theme/bootstrap.config.js", 659 | './src/client.js' 660 | ] 661 | }, 662 | output: { 663 | path: assetsPath, 664 | filename: '[name]-[hash].js', 665 | chunkFilename: '[name]-[chunkhash].js', 666 | publicPath: 'http://' + host + ':' + port + '/dist/' 667 | }, 668 | plugins:[ 669 | new webpack.DefinePlugin({ 670 | __CLIENT__: true, 671 | __SERVER__: false, 672 | __DEVELOPMENT__: true, 673 | __DEVTOOLS__: true 674 | // <-------- DISABLE redux-devtools HERE 675 | }), 676 | new webpack.IgnorePlugin(/webpack-stats\.json$/), 677 | webpackIsomorphicToolsPlugin.development() 678 | ], 679 | module:{ 680 | rules:[ 681 | { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url-loader?limit=10000&mimetype=application/font-woff" }, 682 | { 683 | test: webpackIsomorphicToolsPlugin.regular_expression('images'), 684 | use: { 685 | loader: require.resolve("url-loader"), 686 | options:{} 687 | } 688 | }, 689 | { 690 | test: /\.jsx$/, 691 | exclude :path.resolve("node_modules"), 692 | use: [{ 693 | loader:require.resolve('babel-loader'), 694 | options:updateCombinedBabelConfig() 695 | }] 696 | }, 697 | { 698 | test: /\.js$/, 699 | exclude :path.resolve("node_modules"), 700 | use: [{ 701 | loader:require.resolve('babel-loader'), 702 | options:updateCombinedBabelConfig() 703 | }] 704 | }] 705 | } 706 | } 707 | ``` 708 | 注意:我们的wcf没有内置的entry,所以你这里配置的entry将会作为合并后的最终webpack配置的entry项。对于output来说,用户自定义的配置将会覆盖默认的配置(其他的也一样,除了module,plugins等)。对于plugin来说,我们会进行合并,此时不仅包含用户自定义的plugin,同时也包含内置的plugin。对于loader来说,如果有两个相同的loader,那么用户自定义的loader也会原样覆盖默认的loader。这样就很容易进行扩展。只要用户*配置一个自定义配置文件的路径即可*! 709 | 710 | (3)dedupe 711 | 712 | atool-build并没有对我们的plugin和loader进行去重,这样可能导致同一个plugin被添加了两次,这就要求用户必须了解内置那些plugin,从而不去添加它。同时也会导致某一个js文件的loader也添加了两次,得到如下的内容: 713 | ```js 714 | [ { test: { /\.jsx$/ [lastIndex]: 0 }, 715 | exclude: 716 | { [Function: exclude] 717 | [length]: 1, 718 | [name]: 'exclude', 719 | [arguments]: null, 720 | [caller]: null, 721 | [prototype]: exclude { [constructor]: [Circular] } }, 722 | use: [ { loader: 'babel-loader', options: {} }, [length]: 1 ] }, 723 | { test: { /\.jsx$/ [lastIndex]: 0 }, 724 | //对于jsx的loader又添加了一次 725 | exclude: 726 | { [Function: exclude] 727 | [length]: 1, 728 | [name]: 'exclude', 729 | [arguments]: null, 730 | [caller]: null, 731 | [prototype]: exclude { [constructor]: [Circular] } }, 732 | use: [ { loader: 'after', options: {} }, [length]: 1 ] }, 733 | [length]: 2 ] 734 | ``` 735 | 736 | 这个问题你可以查看我给webpack-merge提出的[issue](https://github.com/survivejs/webpack-merge/issues/75)。但是这些工作wcf已经做了,所以当你有两个相同的插件,或者两个相同的loader的情况下,都只会留下一个,并且用户自定义的优先级要高于默认配置的优先级。 737 | 738 | (4)打包前进行钩子设置 739 | 740 | 如果在打包前,或者获取到最终配置之前,你要对最终配置做一个处理,比如删除某个plugin/loader,那么我们提供了一个钩子函数: 741 | ```js 742 | const program = { 743 | onlyCf : true, 744 | //此时不打包,只是为了获取最终配置用于nodejs 745 | cwd : process.cwd(), 746 | dev : true, 747 | //不启动压缩 748 | //下面这个hook用于去掉commonchunkplugin 749 | hook:function(webpackConfig){ 750 | const commonchunkpluginIndex = webpackConfig.plugins.findIndex(plugin => { 751 | return plugin.constructor.name == "CommonsChunkPlugin" 752 | }); 753 | webpackConfig.plugins.splice(commonchunkpluginIndex, 1); 754 | return webpackConfig; 755 | } 756 | }; 757 | ``` 758 | 759 | (5)其他功能 760 | 761 | 请[参考这里](https://github.com/liangklfangl/wcf/blob/master/changelog.md) 762 | 763 | ### 11.关于webpack+babel打包的更多文章 764 | #### 11.1 webpack相关 765 | 766 | [webpack-dev-server原理分析](https://github.com/liangklfangl/webpack-dev-server) 767 | 768 | [webpack热加载HMR深入学习](https://github.com/liangklfangl/webpack-hmr) 769 | 770 | [集成webpack,webpack-dev-server的打包工具](https://github.com/liangklfangl/wcf) 771 | 772 | [prepack与webpack对比](https://github.com/liangklfangl/prepack-vs-webpack) 773 | 774 | [webpack插件书写你需要了解的知识点](https://github.com/liangklfangl/webpack-common-sense) 775 | 776 | [CommonsChunkPlugin深入分析](https://github.com/liangklfangl/commonchunkplugin-source-code) 777 | 778 | [CommonsChunkPlugin配置项深入分析](https://github.com/liangklfangl/commonsChunkPlugin_Config) 779 | 780 | [webpack.DllPlugin提升打包性能](https://github.com/liangklfangl/webpackDll) 781 | 782 | [webpack实现code splitting方式分析](https://github.com/liangklfangl/webpack-code-splitting) 783 | 784 | [webpack中的externals vs libraryTarget vs library](https://github.com/liangklfangl/webpack-external-library) 785 | 786 | [webpack的compiler与compilation对象](https://github.com/liangklfangl/webpack-compiler-and-compilation) 787 | 788 | [webpack-dev-middleware原理分析](https://github.com/liangklfang/webpack-dev-middleware) 789 | 790 | #### 11.2 Babel相关 791 | 792 | [Babel编译class继承与源码打包结果分析](https://github.com/liangklfangl/babel-compiler-extends) 793 | 794 | [使用babel操作AST来完成某种特效](https://github.com/liangklfangl/astexample) 795 | 796 | [babylon你了解多少](https://github.com/liangklfangl/babylon) 797 | 798 | 799 | 更加深入的问题,您可以继续阅读[react+webpack+babel全家桶完整实例](https://github.com/liangklfangl/react-universal-bucket) 800 | 801 | 802 | 803 | 804 | 805 | 参考资料: 806 | 807 | [atoolo-build官方文档](http://ant-tool.github.io/atool-build.html) 808 | 809 | [webpack配置文档](https://github.com/webpack/docs/wiki/configuration#outputpublicpath) 810 | 811 | [Babel入门教程](http://www.ruanyifeng.com/blog/2016/01/babel.html) 812 | --------------------------------------------------------------------------------