The Ant’s Story

本文讲述了Ant工具的诞生故事。它最初是为解决Tomcat项目跨平台编译问题而开发,由James Duncan Davidson在试用多种工具无果后,用Java编写而成。Ant功能不断扩展,被纳入Apache,用户数量远超Tomcat,在Java社区广泛应用,甚至还有.NET实现。

 

Translate by guipei 200501

 

       Ant 工具估计应该不少朋友都在使用,ant也是一个不经意而成功的一个工具。它本身是为了方便tomcat项目编译出现的,最终的用户数量却远远大于了tomcat的用户,在整个的java社区广为应用。你知道它背后的故事么,这个故事来源于ant的原始发起人James Duncan Davidson的讲述,来让我们一起来了解一下吧。

 

       我必须承认我对Ant没有什么更多的想法,仅仅一个小小构建工具,可以走得这么远以及被众多的java社区的开发人员所使用。当我编写ant的第一个版本时候,它只是一个简单的工具用来帮助我解决跨平台的编译问题。现在它已经快速的成长,被成千上万的开发者用在不同的项目当中。这后面有什么魔力呢?这个小程序被众多的人们使用?也许ant来历的故事可以提供一些线索。

ant被纳入Apachecvs服务前,ant已经有了相当长的开发时间。在1998年中期,我在sun任职,当时负责创建java servlet2.1规范,以及涉及其实现的工作。这个实现的规范,就是后来的Tomcat,和以前的实现相比较,这是一个全新的系统,所以其实现必须是100%的纯java应用。为了取得100%的纯java认证,我们使用sunjava平台工作。你必须给Key Labs(一个中立的认证公司)证明它可以运行在三种不同的系统平台。为了证明servelt 规范的实现可以运行在任何的地方,我选择了SolarisWindows,和Mac 系统。然而我不仅仅想实现tomcat在不同的平台上面运行,而且还想让它可以在不同的平台上面构建和开发。我曾经试用过GNU Make工具,和系统脚本以及批处理操作。然而天知道怎么回事,不同的方式都存在不同的问题。所有的问题都可能来自于我所使用的工具使用C语言开发的。当使用它们在java方面的应用时候,可以工作,但是非常的慢。尽管java程序可以很好的实现,但是虚拟机的启动太过耗时。并且使用Make会在每一个java编译的时候创建一个虚拟几。编译一个项目的时间随着项目文件的增多,编译时间则直线上升。

我试验了很多方法来编写make 文件,来解决应该使用一个javac来编译项目中的不同源文件。但是,不管我如何努力尝试,如何多次的使用Make向导,我没有得到方法最终解决一个虚拟机编译的问题。另外,让我感到非常累的时处理make文件中的tab格式。后来我对Emacs提出过建议,希望它可以解决无意创建的空格字符。

       在一次去参加欧洲会议的飞行中,我最终无法忍受创建需要运行的不同环境的不同的make文件。我开始决定开发自己的工具:可以检查一个项目中的所有源文件,比较它们最新的编译文件是否存在,然后直接调用javac编译新的java源文件。另外,它应该具有可以把一些类文件打包到一个java文件,以及可以选择复制一些文件的功能,满足不同的软件发布版本。为了确保这些事情可以工作在不同的平台上面,我决定使用java编写这个工具。    几个小时候,我有了一个可以这样运行的工具。它相当的简单,甚至有些粗遭,仅仅有几个类文件组成。我使用java.util.Properties功能实现数据层操作,而且它可以完美的工作。我的编译时间有了数量级的减少。当我返回之后,我在SolarisLinux以及Mac系统上面做了测试,一切都非常完美。那个时候最大的问题时它仅仅有几个简单有限的功能,例如编译、复制一些文件-然而这正是它的核心功能。

在我展示这个工具的几个星期后,我命名这个工具为ant,因为这个小东西可以做大的事情。我的朋友Jason Huntero’reillyservlet程序作家),他认为这是一个十分有用的工具,但也没能预料到它后来的发展。后来,我想到使用java反射功能提供一个简单的方法来扩展ant的能力,以便于程序员自己可以开发他们自己的任务来扩展它。事情开始发展,我有了第一个用户。Jason总是拥有特殊的能力发现软件中的bug,帮助我修复了其中的不少问题。

在反射功能实现以后,我又编写了更多的一些任务,antsun成为了一个相当有用的工具。然而,随着build文件不断变大,随着目标(任务的集合)功能的引入,属性文件不能很好的实现层次功能。我尝试着使用不同的方法解决这个问题,在一次欧洲返回的飞行中突然找到方法。就是使用XML文档的层次结构。其中也用到了以前实现XML标记解析的经验。

最终在海洋上空的飞行过程中我很好的完成了代码工作。我非常惊奇是否在高空中会增加我的能力帮助我完成任务,或者欧洲旅行给我带来了更大的创造力。更多的试验也许可以知道最终的结果。

Ant,象我们知道的一样,就这样诞生了。你现在看到的ant版本(好的或许坏的)就是来自当时的那个决定。当然,目前ant已经有了很大的改变,但是原理还是那样。在2000年后期,ant被纳入apachetomcat旁边cvs资源库。我后来转移到其他的工作上去,主要集中在apache软件基金会的XML规范,例如sunJAXP,以及w3cDOM

很让人吃惊,很多人们都在讨论Ant。第一次人们发现它工作在tomcat下面。然后它们告诉它们的朋友,然后朋友又告诉他们的朋友,等等。一些时间后,人们知道它,使用他的人竟然多于了Tomcat。一些强大的开发人员和用户社区在apacheant下面开始成长,这个工具沿着这条路也做了很多修改。人们使用它构建各种不同的项目,从小的应用程序到巨大J2EE系统。

2001年的JavaOne大会上,我知道了ant进入了正规发展。当时我在演示一个新的数据库开发工具,一个推荐者展示了使用设计软件在方框之间画线有多么简单。随着控制窗口的闪过,ant的每一个用户熟悉的显示过程,完整的完成了任务。我被深深的震惊了。

Ant的用户数量继续在增长。这全部都来自当时我的一个小小渴望,现在被世界上的所有java程序员所共享。也不不仅仅时java开发员,我最近以外发现了NAnt,一个Ant.NET实现。如果我只到ant能够取得今天的巨大成功,我也许会花更多的时间,使它更加完美,更加复杂比当时发布。然而可能在易用性上面就会失败。也许ant会变得过于机械。如果我花费更多的时间让他可以完成多于工作所需要的功能,它也许变成了一个很大的工具,让它难以使用。就像我们看到的一些软件一样,例如目前的java api规范一样。这看起来有些奇怪,不想成功的ant达到了今天如此的成功。这是一个直接的答案对于这样的问题,许多人也许都发生过。我的确感到非常荣幸,幸运的发生了这个事情。

 

James Duncan Davidson

San Francisco, CA, April 2002

/* eslint-disable no-console */ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ const fs = require('fs'); const path = require('path'); const webpack = require('webpack'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const CopyPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); const { WebpackManifestPlugin, getCompilerHooks, } = require('webpack-manifest-plugin'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const parsedArgs = require('yargs').argv; const Visualizer = require('webpack-visualizer-plugin2'); const getProxyConfig = require('./webpack.proxy-config'); const packageConfig = require('./package'); // input dir const APP_DIR = path.resolve(__dirname, './'); // output dir const BUILD_DIR = path.resolve(__dirname, '../superset/static/assets'); const ROOT_DIR = path.resolve(__dirname, '..'); const { mode = 'development', devserverPort = 9000, measure = false, nameChunks = false, } = parsedArgs; const isDevMode = mode !== 'production'; const isDevServer = process.argv[1].includes('webpack-dev-server'); const ASSET_BASE_URL = process.env.ASSET_BASE_URL || ''; const output = { path: BUILD_DIR, publicPath: `${ASSET_BASE_URL}/static/assets/`, }; if (isDevMode) { output.filename = '[name].[contenthash:8].entry.js'; output.chunkFilename = '[name].[contenthash:8].chunk.js'; } else if (nameChunks) { output.filename = '[name].[chunkhash].entry.js'; output.chunkFilename = '[name].[chunkhash].chunk.js'; } else { output.filename = '[name].[chunkhash].entry.js'; output.chunkFilename = '[chunkhash].chunk.js'; } if (!isDevMode) { output.clean = true; } const plugins = [ new webpack.ProvidePlugin({ process: 'process/browser.js', ...(isDevMode ? { Buffer: ['buffer', 'Buffer'] } : {}), // Fix legacy-plugin-chart-paired-t-test broken Story }), // creates a manifest.json mapping of name to hashed output used in template files new WebpackManifestPlugin({ publicPath: output.publicPath, seed: { app: 'superset' }, // This enables us to include all relevant files for an entry generate: (seed, files, entrypoints) => { // Each entrypoint's chunk files in the format of // { // entry: { // css: [], // js: [] // } // } const entryFiles = {}; Object.entries(entrypoints).forEach(([entry, chunks]) => { entryFiles[entry] = { css: chunks .filter(x => x.endsWith('.css')) .map(x => `${output.publicPath}${x}`), js: chunks .filter(x => x.endsWith('.js') && x.match(/(?<!hot-update).js$/)) .map(x => `${output.publicPath}${x}`), }; }); return { ...seed, entrypoints: entryFiles, }; }, // Also write manifest.json to disk when running `npm run dev`. // This is required for Flask to work. writeToFileEmit: isDevMode && !isDevServer, }), // expose mode variable to other modules new webpack.DefinePlugin({ 'process.env.WEBPACK_MODE': JSON.stringify(mode), 'process.env.REDUX_DEFAULT_MIDDLEWARE': process.env.REDUX_DEFAULT_MIDDLEWARE, 'process.env.SCARF_ANALYTICS': JSON.stringify(process.env.SCARF_ANALYTICS), }), new CopyPlugin({ patterns: [ 'package.json', { from: 'src/assets/images', to: 'images' }, { from: 'src/assets/stylesheets', to: 'stylesheets' }, ], }), // static pages new HtmlWebpackPlugin({ template: './src/assets/staticPages/404.html', inject: true, chunks: [], filename: '404.html', }), new HtmlWebpackPlugin({ template: './src/assets/staticPages/500.html', inject: true, chunks: [], filename: '500.html', }), ]; if (!process.env.CI) { plugins.push(new webpack.ProgressPlugin()); } if (!isDevMode) { // text loading (webpack 4+) plugins.push( new MiniCssExtractPlugin({ filename: '[name].[chunkhash].entry.css', chunkFilename: '[name].[chunkhash].chunk.css', }), ); // Runs type checking on a separate process to speed up the build plugins.push(new ForkTsCheckerWebpackPlugin()); } const PREAMBLE = [path.join(APP_DIR, '/src/preamble.ts')]; if (isDevMode) { // A Superset webpage normally includes two JS bundles in dev, `theme.ts` and // the main entrypoint. Only the main entry should have the dev server client, // otherwise the websocket client will initialize twice, creating two sockets. // Ref: https://github.com/gaearon/react-hot-loader/issues/141 PREAMBLE.unshift( `webpack-dev-server/client?http://localhost:${devserverPort}`, ); } function addPreamble(entry) { return PREAMBLE.concat([path.join(APP_DIR, entry)]); } const babelLoader = { loader: 'babel-loader', options: { cacheDirectory: true, // disable gzip compression for cache files // faster when there are millions of small files cacheCompression: false, presets: [ [ '@babel/preset-react', { runtime: 'automatic', importSource: '@emotion/react', }, ], ], plugins: [ [ '@emotion/babel-plugin', { autoLabel: 'dev-only', labelFormat: '[local]', }, ], ], }, }; const config = { entry: { preamble: PREAMBLE, theme: path.join(APP_DIR, '/src/theme.ts'), menu: addPreamble('src/views/menu.tsx'), spa: addPreamble('/src/views/index.tsx'), embedded: addPreamble('/src/embedded/index.tsx'), }, cache: { type: 'filesystem', // Enable filesystem caching cacheDirectory: path.resolve(__dirname, '.temp_cache'), buildDependencies: { config: [__filename], }, }, output, stats: 'minimal', /* Silence warning for missing export in @data-ui's internal structure. This issue arises from an internal implementation detail of @data-ui. As it's non-critical, we suppress it to prevent unnecessary clutter in the build output. For more context, refer to: https://github.com/williaster/data-ui/issues/208#issuecomment-946966712 */ ignoreWarnings: [ { message: /export 'withTooltipPropTypes' \(imported as 'vxTooltipPropTypes'\) was not found/, }, { message: /Can't resolve.*superset_text/, }, ], performance: { assetFilter(assetFilename) { // don't throw size limit warning on geojson and font files return !/\.(map|geojson|woff2)$/.test(assetFilename); }, }, optimization: { sideEffects: true, splitChunks: { chunks: 'all', // increase minSize for devMode to 1000kb because of sourcemap minSize: isDevMode ? 1000000 : 20000, name: nameChunks, automaticNameDelimiter: '-', minChunks: 2, cacheGroups: { automaticNamePrefix: 'chunk', // basic stable dependencies vendors: { priority: 50, name: 'vendors', test: new RegExp( `/node_modules/(${[ 'abortcontroller-polyfill', 'react', 'react-dom', 'prop-types', 'react-prop-types', 'prop-types-extra', 'redux', 'react-redux', 'react-hot-loader', 'react-sortable-hoc', 'react-table', 'react-ace', '@hot-loader.*', 'webpack.*', '@?babel.*', 'lodash.*', 'antd', '@ant-design.*', '.*bootstrap', 'moment', 'jquery', 'core-js.*', '@emotion.*', 'd3', 'd3-(array|color|scale|interpolate|format|selection|collection|time|time-format)', ].join('|')})/`, ), }, // viz thumbnails are used in `addSlice` and `explore` page thumbnail: { name: 'thumbnail', test: /thumbnail(Large)?\.(png|jpg)/i, priority: 20, enforce: true, }, }, }, usedExports: 'global', minimizer: [new CssMinimizerPlugin(), '...'], }, resolve: { // resolve modules from `/superset_frontend/node_modules` and `/superset_frontend` modules: ['node_modules', APP_DIR], alias: { // TODO: remove aliases once React has been upgraded to v17 and // AntD version conflict has been resolved antd: path.resolve(path.join(APP_DIR, './node_modules/antd')), react: path.resolve(path.join(APP_DIR, './node_modules/react')), // TODO: remove Handlebars alias once Handlebars NPM package has been updated to // correctly support webpack import (https://github.com/handlebars-lang/handlebars.js/issues/953) handlebars: 'handlebars/dist/handlebars.js', /* Temporary workaround to prevent Webpack from resolving moment locale files, which are unnecessary for this project and causing build warnings. This prevents "Module not found" errors for moment locale files. */ 'moment/min/moment-with-locales': false, // Temporary workaround to allow Storybook 8 to work with existing React v16-compatible stories. // Remove below alias once React has been upgreade to v18. '@storybook/react-dom-shim': path.resolve( path.join( APP_DIR, './node_modules/@storybook/react-dom-shim/dist/react-16', ), ), }, extensions: ['.ts', '.tsx', '.js', '.jsx', '.yml'], fallback: { fs: false, vm: require.resolve('vm-browserify'), path: false, ...(isDevMode ? { buffer: require.resolve('buffer/') } : {}), // Fix legacy-plugin-chart-paired-t-test broken Story }, }, context: APP_DIR, // to automatically find tsconfig.json module: { rules: [ { test: /datatables\.net.*/, loader: 'imports-loader', options: { additionalCode: 'var define = false;', }, }, { test: /\.tsx?$/, exclude: [/\.test.tsx?$/], use: [ 'thread-loader', babelLoader, { loader: 'ts-loader', options: { // transpile only in happyPack mode // type checking is done via fork-ts-checker-webpack-plugin happyPackMode: true, transpileOnly: true, // must override compiler options here, even though we have set // the same options in `tsconfig.json`, because they may still // be overridden by `tsconfig.json` in node_modules subdirectories. compilerOptions: { esModuleInterop: false, importHelpers: false, module: 'esnext', target: 'esnext', }, }, }, ], }, { test: /\.jsx?$/, // include source code for plugins, but exclude node_modules and test files within them exclude: [/superset-ui.*\/node_modules\//, /\.test.jsx?$/], include: [ new RegExp(`${APP_DIR}/(src|.storybook|plugins|packages)`), ...['./src', './.storybook', './plugins', './packages'].map(p => path.resolve(__dirname, p), ), // redundant but required for windows /@encodable/, ], use: [babelLoader], }, { test: /ace-builds.*\/worker-.*$/, type: 'asset/resource', }, // react-hot-loader use "ProxyFacade", which is a wrapper for react Component // see https://github.com/gaearon/react-hot-loader/issues/1311 // TODO: refactor recurseReactClone { test: /\.js$/, include: /node_modules\/react-dom/, use: ['react-hot-loader/webpack'], }, { test: /\.css$/, include: [APP_DIR, /superset-ui.+\/src/], use: [ isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true, }, }, ], }, { test: /\.less$/, include: APP_DIR, use: [ isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true, }, }, { loader: 'less-loader', options: { sourceMap: true, lessOptions: { javascriptEnabled: true, modifyVars: { 'root-entry-name': 'default', }, }, }, }, ], }, /* for css linking images (and viz plugin thumbnails) */ { test: /\.png$/, issuer: { not: [/\/src\/assets\/staticPages\//], }, type: 'asset', generator: { filename: '[name].[contenthash:8][ext]', }, }, { test: /\.png$/, issuer: /\/src\/assets\/staticPages\//, type: 'asset', }, { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, issuer: /\.([jt])sx?$/, use: [ { loader: '@svgr/webpack', options: { titleProp: true, ref: true, // this is the default value for the icon. Using other values // here will replace width and height in svg with 1em icon: false, }, }, ], }, { test: /\.(jpg|gif)$/, type: 'asset/resource', generator: { filename: '[name].[contenthash:8][ext]', }, }, /* for font-awesome */ { test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource', }, { test: /\.ya?ml$/, include: ROOT_DIR, loader: 'js-yaml-loader', }, { test: /\.geojson$/, type: 'asset/resource', }, // { // test: /\.mdx?$/, // use: [ // { // loader: require.resolve('@storybook/mdx2-csf/loader'), // options: { // skipCsf: false, // mdxCompileOptions: { // remarkPlugins: [remarkGfm], // }, // }, // }, // ], // }, ], }, externals: { cheerio: 'window', 'react/lib/ExecutionEnvironment': true, 'react/lib/ReactContext': true, }, plugins, devtool: isDevMode ? 'eval-cheap-module-source-map' : false, }; // find all the symlinked plugins and use their source code for imports Object.entries(packageConfig.dependencies).forEach(([pkg, relativeDir]) => { const srcPath = path.join(APP_DIR, `./node_modules/${pkg}/src`); const dir = relativeDir.replace('file:', ''); if (/^@superset-ui/.test(pkg) && fs.existsSync(srcPath)) { console.log(`[Superset Plugin] Use symlink source for ${pkg} @ ${dir}`); config.resolve.alias[pkg] = path.resolve(APP_DIR, `${dir}/src`); } }); console.log(''); // pure cosmetic new line if (isDevMode) { let proxyConfig = getProxyConfig(); // Set up a plugin to handle manifest updates config.plugins = config.plugins || []; config.plugins.push({ apply: compiler => { const { afterEmit } = getCompilerHooks(compiler); afterEmit.tap('ManifestPlugin', manifest => { proxyConfig = getProxyConfig(manifest); }); }, }); config.devServer = { historyApiFallback: true, hot: true, port: devserverPort, proxy: [() => proxyConfig], client: { overlay: { errors: true, warnings: false, runtimeErrors: error => !/ResizeObserver/.test(error.message), }, logging: 'error', }, static: { directory: path.join(process.cwd(), '../static/assets'), }, }; } // To // e.g. npm run package-stats if (process.env.BUNDLE_ANALYZER) { config.plugins.push(new BundleAnalyzerPlugin({ analyzerMode: 'static' })); config.plugins.push( // this creates an HTML page with a sunburst diagram of dependencies. // you'll find it at superset/static/stats/statistics.html // note that the file is >100MB so it's in .gitignore new Visualizer({ filename: path.join('..', 'stats', 'statistics.html'), throwOnError: true, }), ); } // Speed measurement is disabled by default // Pass flag --measure=true to enable // e.g. npm run build -- --measure=true const smp = new SpeedMeasurePlugin({ disable: !measure, }); module.exports = smp.wrap(config); 分析webpack.config.js,确认一下路径应该如何修改
最新发布
10-24
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值