联系客服
客服二维码

联系客服获取更多资料

微信号:LingLab1

客服电话:010-82185409

意见反馈
关注我们
关注公众号

关注公众号

linglab语言实验室

回到顶部
JS SDK 构建实现 Webpack VS Rollup

377 阅读 2020-08-25 09:13:02 上传

以下文章来源于 西语语言学工作坊

零、前言


最近一段时间在开发 JavaScript SDK,讲究 原生、短小、快速、清晰、可读性、可测试性 等等。JavaScript SDK 开发思想参照 JavaScript SDK Design Guide.


而 SDK开发 关键的一环则是打包, 恰恰打包这一环决定了 SDK 的体积(短小)、引入方式(原生),一部分速度问题(快速)。


开发过程中分别使用 Webpack5 与 Rollup 打包。


本次 SDK 体积经历 88kb(webpack production 打包) -> 59kb(rollup 打包) -> 33kb (rollup 加上 plugin-transform-runtime 后打包)。


最后之所以能从 88kb 到 33kb,是因为 Rollup 和 Webpack5 的 terser 、 tree shaking 、 plugin-transform-runtime 拉开差距。


因为 Rollup 打包后体积更小,Webpack 在 dev 下热更效果较好, 最后决定使用 Rollup 作为 production 打包工具,Webpack5 作为 development 打包工具。


抛出最经典名言:Use webpack for apps, and Rollup for libraries


Webpack---拆得好,并得巧


Webpack 始于2012年,由 Tobias Koppers发起,用于解决当时现有工具未解决的的一个难题:构建复杂的单页应用程序(SPA)。特别是 webpack 的两个特性改变了一切:


  1. 代码拆分(Code Splitting)

    使你可以将应用程序分解成可管理的代码块,可以按需加载,这意味着你的用户可以快速获取交互性的网站,而不必等到整个应用程序下载和解析完成。当然你可以手动来完成这项工作,那么祝你好运。

  2. 静态资源(Static assets)

    如图像和 CSS 可以导入到你的应用程序中,而且还能够被作为依赖图中的另一个节点。再也不用关心你的文件是否放在正确的文件夹中,再也不用为文件 URL 增添 hash 而使用 hack 脚本,因为 webpack 会帮我们处理这些事情。


Rollup---打得小,跑得快


Rollup 是下一代 JavaScript 模块打包工具。开发者可以在你的应用或库中使用 ES2015 模块,然后高效地将它们打包成一个单一文件用于浏览器和Node.js使用。Rollup 最令人激动的地方,就是能让打包文件体积很小,Rollup总能打出更小,更快的包:


  1. Tree-shaking

    Rollup 最初推出时的一大特点。Rollup 通过对代码的静态分析,分析出冗余代码,在最终的打包文件中将这些冗余代码删除掉,进一步缩小代码体积。

  2. ES2015 模块打包支持

    ES2015 模块打包支持这个也是其他构建工具所不具备的。Rollup 直接不需要通过 babel 将 import 转化成 Commonjs 的 require 方式,极大地利用 ES2015 模块的优势。


一、配置


Webpack5 打包配置

由于一开始项目使用 Webpack 打包,所以配置了 development 与 production 两个config
ps: 下文可能会出现 babel ,由于非本文讨论重点,会略过。


安装Webpack及其相关:

    yarn add -D webpack@5.0.0-beta.22yarn add -D webpack-cliyarn add -D webpack-dev-serveryarn add -D webpack-merge

    在项目中添加以下配置文件: webpack.config.js

      const {merge} = require('webpack-merge')const productionConfig = require('./webpack.prod.conf.js') // 引入生产环境配置文件const developmentConfig = require('./webpack.dev.conf.js') // 引入开发环境配置文件const webpack = require('webpack')const path = require('path')
      const baseConfig = {  module: {    rules: [      {        test: /\.(js|ts|tsx)$/,        exclude: /(node_modules)/,        use: {          loader: 'babel-loader',        },      },      {        test: /\.(less|css)$/,        use: [          {            loader: 'style-loader',          },          {            loader: 'css-loader',          },          {            loader: 'less-loader',            options: {              lessOptions: {                javascriptEnabled: true,              },            },          },        ],      },    ],  },  resolve: {    extensions: ['.ts', '.js', '.tsx', '.jsx'],    alias: {      '@': path.resolve(__dirname, './src'),    },  },  plugins: [    new webpack.ProvidePlugin({      h: ['dom-chef', 'h'],    }),  ],}
      module.exports = env => {  // mode: 'production' || 'development',  let config = env === 'production' ? productionConfig : developmentConfig  console.log('当前模式:', env, config)  return merge(baseConfig, config) // 合并 公共配置 和 环境配置}

      (混入 babel)

      安装babel及其相关:

        yarn add -D @babel/coreyarn add -D @babel/preset-envyarn add -D @babel/preset-typescriptyarn add -D @babel/plugin-syntax-typescriptyarn add -D @babel/plugin-transform-react-jsx

        babel.config.js

          const presets = [  [    '@babel/preset-env',    {      targets: {        edge: '17',        firefox: '60',        chrome: '67',        safari: '11.1',        ie: '11',      },      useBuiltIns: 'usage',      corejs: 3,    },  ],  ['@babel/preset-typescript'],]const plugins = [  ['@babel/plugin-syntax-typescript'],  [    '@babel/plugin-transform-react-jsx',    {      pragma: 'h',      pragmaFrag: 'DocumentFragment',    },  ],]module.exports = {presets, plugins}

           webpack.prod.conf.js

            const path = require('path')
            module.exports = {  mode: 'production',  entry: {    index: path.resolve(__dirname, './src/app.ts'),  },  output: {    libraryTarget: 'umd',    path: __dirname + '/dist',    filename: 'index.js',    library: '@efox/pay',  },  performance: {    hints: 'error',  },  plugins: [],  optimization: {    minimize: true,  },}


            pack.dev.conf.js

              const path = require('path')
              module.exports = {  mode: 'development',  entry: {    index: path.resolve(__dirname, './src/app.ts'), //入口  },  output: {    libraryTarget: 'umd',    path: __dirname + '/dist',    filename: 'index.js',    library: '@efox/pay',  },  devtool: 'source-map',  performance: {    hints: 'warning',  },}

              启动

              dev 命令:

              webpack --env development --watch

              build命令:

              cp src/index.d.ts dist && webpack --env production

              使用上述配置 Webpack5 打包本 SDK ,包体积为 88kb

              Rollup 打包配置

              安装 Rollup 及其相关:

              yarn add -D rollup
              yarn add -D rollup-plugin-commonjs
              yarn add -D rollup-plugin-inject
              yarn add -D rollup-plugin-terser
              yarn add -D rollup-plugin-typescript2

              rollup.config.js

                import typescript from 'rollup-plugin-typescript2'import babel from 'rollup-plugin-babel'import {DEFAULT_EXTENSIONS} from '@babel/core'import commonjs from 'rollup-plugin-commonjs'import nodeResolve from 'rollup-plugin-node-resolve'import json from 'rollup-plugin-json'import inject from 'rollup-plugin-inject'import {terser} from 'rollup-plugin-terser'
                export default {  input: 'src/app.ts', // 入口文件  output: {    name: 'efoxPay', // umd 模式必须要有 name  此属性作为全局变量访问打包结果    file: `dist/index.js`,    format: 'umd',    sourcemap: true,  },  plugins: [    json({      // All JSON files will be parsed by default,      // but you can also specifically include/exclude files      include: 'node_modules/**',      exclude: ['node_modules/foo/**', 'node_modules/bar/**'],
                     // for tree-shaking, properties will be declared as      // variables, using either `var` or `const`      preferConst: true, // Default: false
                     // specify indentation for the generated default export —      // defaults to '\t'      indent: '  ',
                     // ignores indent and generates the smallest code      compact: true, // Default: false
                     // generate a named export for every property of the JSON object      namedExports: true, // Default: true    }),    nodeResolve({      jsnext: true,      main: true,    }),    commonjs({      include: 'node_modules/**', // Default: undefined      namedExports: {        'node_modules/dom-chef/index.js': ['dom-chef'],        'node_modules/dom-chef/index.json': ['svg-tag-names/'],      },    }),    typescript({      tsconfigOverride: {        compilerOptions: {          declaration: false, // 输出时去除类型文件        },      },    }),    babel({      extensions: [...DEFAULT_EXTENSIONS, 'ts', 'tsx', 'js'],      runtimeHelpers: true,      exclude: 'node_modules/**',      babelrc: false,      presets: ['@babel/preset-env'],      plugins: [['@babel/plugin-transform-runtime', {useESModules: false}]],    }),    inject({      h: ['dom-chef', 'h'],    }),    terser(),  ],}

                启动

                打包命令:

                rollup -c

                使用上述配置 Rollup 打包本 SDK ,包体积为 33kb

                接下来,本文将讨论打包过程中最重要的插件和算法。


                二、 terser


                Terser 是 ES6+ 的 JavaScript 解析器 和 mangler/compressor 工具包, 它可能是最有效的。Terser 建议搭配 Rollup 打包使用,这样会产生更小的代码。


                如果要在 Rollup 上使用 Terser ,需要安装 rollup-plugin-terser

                yarn add -D rollup-plugin-terser

                并在 rollup.config.js 上引入

                  import {terser} from 'rollup-plugin-terser'export default { plugins: [    terser(), ]}

                  Rollup 是一个不错的选择,但如果使用 webpack 大于 v4,默认情况下会使用 Terser。可以通过 optimization.minimize 来启用 Terser ,如下所示:

                      optimization: {    minimize: true,  },


                    三、 tree shaking


                    Webpack5 和 Rollup 都自带了 tree shaking。静态分析代码中的 import,并将排除任何未实际使用的代码。这允许架构于现有工具和模块之上,而不会增加额外的依赖或使项目的大小膨胀。

                    例如,在使用 CommonJS 时,必须导入(import)完整的工具(tool)或库(library)对象。

                      // 使用 CommonJS 导入(import)完整的 utils 对象var utils = require( 'utils' );var query = 'Rollup';// 使用 utils 对象的 ajax 方法utils.ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

                      但是在使用 ES6 模块时,无需导入整个 utils 对象,我们可以只导入(import)我们所需的 ajax 函数:

                        // 使用 ES6 import 语句导入(import) ajax 函数import { ajax } from 'utils';var query = 'Rollup';// 调用 ajax 函数ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

                        tree shaking 只引入最基本最精简代码,所以可以生成轻量、快速,以及低复杂度的 library 和应用程序。因为这种基于显式的 import 和 export 语句的方式,它远比「在编译后的输出代码中,简单地运行自动 minifier 检测未使用的变量」更有效。


                        可能是 rollup 的 tree shaking 算法更好的原因, webpack5 和rollup 同样经过 tree shaking 之后,rollup 打出的包体积更小。


                        四、 plugin-transform-runtime

                        A plugin that enables the re-use of Babel's injected helper code to save on codesize.

                        为了浏览器向下兼容,会使用 @babel/preset-env 对每个文件的 ES6 语法进行转换,我们称之为辅助函数。


                        但样这做存在一个问题。在正常的前端工程开发的时候,少则几十个js文件,多则上千个。如果每个文件里都使用了class类语法,那会导致每个转换后的文件上部都会注入这些相同的函数声明。这会用构建工具打包出来的包非常大,包含大量重复代码。


                        一个思路就是,把这些函数声明都放在一个 npm 包里,需要使用的时候直接从这个包里引入到我们的文件里。这样即使上千个文件,也会从相同的包里引用这些函数。通过 webpack 这一类的构建工具打包的时候,只会把使用到的 npm 包里的函数引入一次,这样就做到了复用,减少了体积。


                        安装 @babel/runtime 包提供辅助函数模块,安装Babel插件 @babel/plugin-transform-runtime 来自动替换辅助函数。

                           yarn add -D @babel/preset-env   yarn add -D @babel/runtime   yarn add -D @babel/plugin-transform-runtime


                          并在 rollup.config.js 上引入

                            import babel from 'rollup-plugin-babel'export default { plugins: [    babel({      extensions: [...DEFAULT_EXTENSIONS, 'ts', 'tsx', 'js'],      runtimeHelpers: true,      exclude: 'node_modules/**',      babelrc: false,      presets: ['@babel/preset-env'],      plugins: [['@babel/plugin-transform-runtime', {useESModules: false}]],    }),    ]}



                            五、 总结

                            • 在不配置第三方插件的情况, Rollup 打包本 SDK 依然比 Webpack5 小,证实了:在开发应用时使用 Webpack,开发库时使用 Rollup(打包应用,Webpack 对静态资源的打包很完美)。

                            • Rollup 比 Webpack5 的 tree shaking 效果好

                            • 本 SDK 把原有 css文件剔除了集成进 tsx 文件,因为 Rollup 对支持静态资源支持不佳,有得有失,包体更小了。而 Webpack 对于代码分割和静态资源导入有着“先天优势”,但是自然会导致包体变大。

                            • plugin-transform-runtime 是打包必须引入的插件,可以将重复的辅助函数自动替换,节省大量体积。


                            点赞
                            收藏
                            表情
                            图片
                            附件