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

atool-build 解读 #32

Open
ghost opened this issue Sep 3, 2018 · 2 comments
Open

atool-build 解读 #32

ghost opened this issue Sep 3, 2018 · 2 comments

Comments

@ghost
Copy link

ghost commented Sep 3, 2018

诞生背景

早些时候,前端开发是没有「构建」这个步骤的,从写法的浏览器兼容到复用都很麻烦。如今前端高速发展及前端往工程化的进步,觉得主要有两个基石:

模块化

首先是「模块化」的推广和完善,npm 提供了规范的书写方式,从之前各具特色的写法困难的解读与适配,变成业界规范,正是因为达成共识的规范,车同轨,书同文,连接协作分享才成为可能,社区和生态也才能诞生。

构建

另一个基石是「构建」的趋于成熟,尤其是构建思想的成熟。在几年前还是 glup 这类把构建抽象为流任务,对 css,js 的 文件压缩合并合图等粗浅处理,如今发展到 webpack 提出的 web 资源都当作模块的设计思想,这个思想上的转变现在看来真算个里程碑,各种资源具备了统一的描述和加载方式,这样丰富灵活的统一操作和处理,组织成更细力度和更大规模的应用才成为可能,webpack 也成为如今的事实标准,这就是如今习以为常的「构建」步骤,它是各类模块的粘合剂,使得模块之间能顺利协作连接,形成各种功能实体,推进前端往工程化迈进。

矛盾与解法

事情都是有利有弊,因为各类模块的种类繁多以及处理方式多样,和 webpack 先进的思想与生俱来的就是它高昂的配置成本,它需要支持各类模块的编排构建,势必保持通用型非常灵活,有大量的配置可以灵活处理各类模块的编译方式。二八原则看,对于大部分项目来说,并不需要那么多配置,因此程序上一般处理方式就是加一层,收掉底层复杂的配置,透露简洁的使用方案给上层。 这就是 atool-build 诞生的原因和希望解决的问题。如今虽然已经不怎么更新,但作为 被 1425 个仓库,725个包依赖的模块(2018.09.01 统计),仍可以从它的设计里学习借鉴很多。

Webpack 概述

经过这么多年的发展,前端方面主要会面临的问题包括不同浏览器兼容,不同版本的 css,js 语言兼容,以及组件化方案等。早期大家还会写写原生的处理兼容问题,但如今各类浏览器+各个js、css版本,已经达到很难写完整的地步。

另外还有前端语言本身的写法问题,这在早期做页面时候问题不大,但随着前端的发展在规模和复杂度有了更高的要求,纯css,js的写法就变得相当繁琐。比如css作为描述型语言,容易上手写法简单,但是在做大型应用时候,纯手写会相当繁琐。js 因为弱类型的特性,灵活是一方面,但在大型应用的协作和描述上,不够透明成为一种负担。

因此趋势是手动变自动,通过预处理和构建编译去解决这些问题,其实这类也有很多方案。而 webpack 一切皆 pack 的统一处理特性,使得它成为承载以上各类问题处理方案的很好的载体。而事实上,基于 webpack 的 atool-build,确实也封装了这些年解决 web 开发问题的沉淀下来的各类处理插件,了解它们要解决的问题,也就基本看全了前端这几年的各个方向发展和沉淀,接下来我们会做大致介绍。

atool-build 解读

好,终于到了主角 ant-tool/atool-build 登场了, 基于之上背景,之所以去读 atool-build,是希望对以下有所了解

技术层面

  • node cli 的制作
  • npm 模块测试发布
  • webpack 常用配置

运营层面

  • npm 模块的维护和运营
  • 如何在大量的需求和问题,提取要素,规划后续发展

基本原理

atool-build 核心代码其实只有几行逻辑

  • 用户配置和默认配置合并,生成 webpackConfig
  • webpack 根据 webpackConfig 编译构建文件

src/build.js

// 根据配置,生成 webpack 编译器
const compiler = webpack(webpackConfig);
...
if (args.watch) {
  // 编译文件,并在文件变化时再次编辑
  compiler.watch(args.watch || 200, doneHandler);
} else {
  // 编译文件
  compiler.run(doneHandler);
}

接下来我们看一下 atool-build 默认集成了 webpack 哪些配置,解决了哪些问题。这里不可避免的需要涉及 webpack 的一些配置,但不需过多深入理解,这里大概理解 webpack 的两个概念:loader 和 plugin 就可以了,其中 loader 用于对模块的源代码进行转换,让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript),从而所有类型的文件转换为 webpack 能够处理的有效模块被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。

构建的基本输入输出,和第三方模块寻址

首先是构建环节基本的输入输出,主要配置在 src/getWebpackCommonConfig.js 这个文件

const pkgPath = join(args.cwd, 'package.json');
  const pkg = existsSync(pkgPath) ? require(pkgPath) : {};

  const jsFileName = args.hash ? '[name]-[chunkhash].js' : '[name].js';
  const cssFileName = args.hash ? '[name]-[chunkhash].css' : '[name].css';
  const commonName = args.hash ? 'common-[chunkhash].js' : 'common.js';
  
...

const config = {
  // 输入:在项目根目录 package.json 的 entry 配置要构建的文件
  entry: pkg.entry,
  ...
  // 输出:构建生成文件输出到 dist 目录
  output: {
    path: join(process.cwd(), './dist/'),
    filename: jsFileName,
    chunkFilename: jsFileName,
  },
};

然后是第三方模块的找寻方式

resolve: {
  // 配置第三方模块的找寻地址
  modules: ['node_modules', join(__dirname, '../node_modules')],
  // 当引入模块没有文件后缀,尝试根据这些文件后缀来找寻是否存在相应文件
  extensions: ['.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.ts', '.tsx', '.js', '.jsx', '.json'],
},

到这里,基本的构建阶段的输入输出配置就完成了。接下来是配置各类资源的处理。如上所说,webpack 对非 js 的文件处理是通过配置各类 loader 来做转换的. 需要注意的是,loader 的运行顺序是按数组倒序运行的。

JS

因为入口文件一般都是 js 文件,先看看 js 的编译

jsx 和 tsx

可以简单理解为都是 js 语言加上一些领域写法的变体,需要转换到原生js才能正常使用

    {
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loader: 'babel-loader',
      options: babelOptions,
    },
    {
      test: /\.tsx?$/,
      use: [
        {
          loader: 'babel-loader',
          options: babelOptions,
        },
        {
          loader: 'ts-loader',
          options: {
            transpileOnly: true,
            compilerOptions: tsQuery,
          },
        },
      ],
    },
babel 配置

babel 可以简单理解为,转换 js 成配置版本,使得一些浏览器尚未支持的特性,能降级为老版本实现,使得浏览器能够正常运行,在 atool-build 配置是

{
    cacheDirectory: tmpdir(),
    presets: [
      require.resolve('babel-preset-es2015-ie'),
      require.resolve('babel-preset-react'),
      require.resolve('babel-preset-stage-0'),
    ],
    plugins: [
      require.resolve('babel-plugin-add-module-exports'),
      require.resolve('babel-plugin-transform-decorators-legacy'),
    ],
  };
typeScript 配置

typeScript 可以简单理解为,有类型的 js,即在编写时候增加类型提示等辅助功能,但也不是原生的,需要做编译转化为原生 js,在 atool-build 配置是

{
    target: 'es6',
    jsx: 'preserve',
    moduleResolution: 'node',
    declaration: false,
    sourceMap: true,
  };

以上就是js 的基本构建处理了。接下来看看 css,即通过入口文件引入的css的 处理

CSS 处理

{
      test(filePath) {
        return /\.css$/.test(filePath) && !/\.module\.css$/.test(filePath);
      },
      use: ExtractTextPlugin.extract({
        use: [
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
            },
          },
          {
            loader: 'postcss-loader',
            options: postcssOptions,
          },
        ],
      }),
    },
    {
      test: /\.module\.css$/,
      use: ExtractTextPlugin.extract({
        use: [
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
              modules: true,
              localIdentName: '[local]___[hash:base64:5]',
            },
          },
          {
            loader: 'postcss-loader',
            options: postcssOptions,
          },
        ],
      }),
    },
    {
      test(filePath) {
        return /\.less$/.test(filePath) && !/\.module\.less$/.test(filePath);
      },
      use: ExtractTextPlugin.extract({
        use: [
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
            },
          },
          {
            loader: 'postcss-loader',
            options: postcssOptions,
          },
          {
            loader: 'less-loader',
            options: {
              sourceMap: true,
              modifyVars: theme,
            },
          },
        ],
      }),
    },
    {
      test: /\.module\.less$/,
      use: ExtractTextPlugin.extract({
        use: [
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
              modules: true,
              localIdentName: '[local]___[hash:base64:5]',
            },
          },
          {
            loader: 'postcss-loader',
            options: postcssOptions,
          },
          {
            loader: 'less-loader',
            options: {
              sourceMap: true,
              modifyVars: theme,
            },
          },
        ],
      }),
    },

postCSS

这里需要对 postCSS 有一定了解主要是处理 css 存在版本问题,以及各类浏览器写法问题。

  const postcssOptions = {
    sourceMap: true,
    plugins: [
      rucksack(),
      autoprefixer({
        browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 8', 'iOS >= 8', 'Android >= 4'],
      }),
    ],
  };
  

其它静态资源处理

通过入口文件 import 的其它非js类文件,也需配置对应的处理 loader

{
          test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
          loader: 'url-loader',
          options: {
            limit: 10000,
            minetype: 'application/font-woff',
          },
        },
        {
          test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
          loader: 'url-loader',
          options: {
            limit: 10000,
            minetype: 'application/font-woff',
          },
        },
        {
          test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
          loader: 'url-loader',
          options: {
            limit: 10000,
            minetype: 'application/octet-stream',
          },
        },
        { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
          loader: 'url-loader',
          options: {
            limit: 10000,
            minetype: 'application/vnd.ms-fontobject',
          },
        },
        {
          test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
          loader: 'url-loader',
          options: {
            limit: 10000,
            minetype: 'image/svg+xml',
          },
        },
        {
          test: /\.(png|jpg|jpeg|gif)(\?v=\d+\.\d+\.\d+)?$/i,
          loader: 'url-loader',
          options: {
            limit: 10000,
          },
        },
        {
          test: /\.html?$/,
          loader: 'file-loader',
          options: {
            name: '[name].[ext]',
          },
        },

插件

atool-build 内置了一些插件。注意插件的执行依赖于 webpack 的事件机制,并不是顺序执行。

// 打包出各个入口的共同文件 common.js
new webpack.optimize.CommonsChunkPlugin({
  name: 'common',
  filename: commonName,
}),

// 将样式文件单独打包
new ExtractTextPlugin({
  filename: cssFileName,
  disable: false,
  allChunks: true,
}),

// 大小写识别
new CaseSensitivePathsPlugin(),

// 错误提示增强
new FriendlyErrorsWebpackPlugin({
  onErrors: (severity, errors) => {
    ...
  },
}),

另外为了优化打包过程体验,还使用了 ProgressPlugin

new ProgressPlugin((percentage, msg, addInfo) => {
  const stream = process.stderr;
  if (stream.isTTY && percentage < 0.71) {
    stream.cursorTo(0);
    stream.write(`📦  ${chalk.magenta(msg)} (${chalk.magenta(addInfo)})`);
    stream.clearLine(1);
  } else if (percentage === 1) {
    console.log(chalk.green('\nwebpack: bundle build is now finished.'));
  }
}),

开发测试发布

// 使用 babel 转换代码为 es5
"build": "rm -rf lib && babel src --out-dir lib",

// 发布 npm 包和发布代码 
"pub": "npm run build && npm publish && rm -rf lib && git push origin"

// babel-node 和 babel-istanbul 
// $(npm bin) 本地命令行路径
// babel-node 和 babel-istanbul 
"test": "babel-node $(npm bin)/babel-istanbul cover $(npm bin)/_mocha -- --no-timeouts",

// 支持 es6 的 mocha
"debug": "$(npm bin)/mocha --require babel-core/register --no-timeouts",

// 使用 eslint 规范代码格式
"lint": "eslint --ext .js src",

// 现实覆盖率
"coveralls": "cat ./coverage/lcov.info | coveralls",

使用方式

使用文档

http://ant-tool.github.io/atool-build.html

API 设计

api 设计还是非常简洁,突出本质

program
  .version(require('../package').version, '-v, --version')
  .option('-o, --output-path <path>', 'output path')
  .option('-w, --watch [delay]', 'watch file changes and rebuild')
  .option('--hash', 'build with hash and output map.json')
  .option('--publicPath <publicPath>', 'publicPath for webpack')
  .option('--devtool <devtool>', 'sourcemap generate method, default is null')
  .option('--config <path>', 'custom config path, default is webpack.config.js')
  .option('--no-compress', 'build without compress')
  .option('--silent', 'close notifier')
  .option('--notify', 'activates notifications for build results')
  .option('--json', 'running webpack with --json, ex. result.json')
  .option('--verbose', 'run with more logging messages.')

自定义

通过配置 webpack.config.js 来扩展,这个好处是灵活,缺点是函数式过于灵活不受管控,容易变成坑, 比如去掉 common 的设置要这样写

webpackConfig.plugins.some(function(plugin, i){
  if(plugin instanceof webpack.optimize.CommonsChunkPlugin) {
    webpackConfig.plugins.splice(i, 1);
    return true;
  }
});

这个问题逐渐暴露难以收敛,构建的元能力没有得到很好的沉淀,作者在 这里 做了详细说明。

运营

模块运营非常不容易,首先是使用方式多样,需求多样。API 设计,模块定位,以及各类运行的问题都要处理,在各类问题和需求中保证一定的形态。

规划

尤其是处在变化的前端,工具的发展规划甚为不易,既要保持简洁,又要灵活,还要稳定,以及贴合趋势的发展,和在各种变化中保持本心, 可以看看这篇 pigcan: 支付宝前端构建工具的发展和未来的选择

附录

@acodercc
Copy link
Member

acodercc commented Sep 3, 2018

不错,文章写的很细致啊 💯

@rdmclin2
Copy link
Contributor

rdmclin2 commented Sep 3, 2018

顶👍

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

No branches or pull requests

2 participants