4
最近我们小组内打算推进一个内部私有库的项目,实现内部组件、模块、类库的中心化管理,方便小组内成员使用。私有库是基于第三方库sinopia搭建的,但本文不涉及该库的构建,主要讨论内容是自定义npm模块包从打包到发布的一些心得。

为什么要使用webpack对组件或者模块进行打包?因为可复用库的模块化,需要适合在任何场景中进行引用,比如AMD/CMD、CommonJs、ES6、ES5等环境。从webpack打包之后的头文件来看:

(function webpackUniversalModuleDefinition(root, factory) {
  if (typeof exports === 'object' && typeof module === 'object')
    module.exports = factory(); // node
  else if (typeof define === 'function' && define.amd)
    define([], factory);    //  AMD/CMD
  else if (typeof exports === 'object')
    exports["Url"] = factory(); 
  else
    root["Url"] = factory();
})(this, function () {
    // somecode
}

从代码可以看出,webpack打包出来的文件是支持多场景的引用方式的。


笔者使用了一个测试包来分析。先上代码:

这是package文件

//  package.json
{
  "name": "Url",
  "version": "1.0.0",
  "description": "测试模块",
  "main": "./build/Url.min.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {},
  "keywords": [],
  "author": "nardo.li",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.25.0",
    "babel-loader": "^7.1.1",
    "babel-preset-es2015": "^6.24.1",
    "webpack": "^3.1.0"
  }
}

这是webpack配置文件(babelcopy的插件其实没用到)

其中output的配置项里需要写入libraryTarget: 'umd'

const webpack = require('webpack')
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  context: path.resolve(__dirname, './lib'),
  entry: {
    Url: './Url.js'
  },
  output: {
    path: path.resolve(__dirname, './build'),
    filename: '[name].min.js',
    libraryTarget: 'umd'
  },
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader',
      query: {
        presets: ['es2015']
      }
    }]
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, './lib'),
        to: path.resolve(__dirname, './build'),
        force: true,
        toType: 'dir',
        ignore: ['.*']
      }
    ])
  ]
}

这是模块文件(随便写了一个)

//  Url.js
function Url () {}
Url.prototype.getParams = function (URL, key) {
  if (key) {
    let reg = new RegExp(key + '=([^&]+)')
    let val = (URL.match(reg) || ['', undefined])[1]
    return val
  } else {
    let _options = {}, urlStr = ''
    if (URL.indexOf('?') > 0) {
      urlStr = /\?([\S]*)/.exec(URL)[1]
      urlStr.split('&').forEach(function (item, i) {
        _options[item.split('=')[0]] = item.split('=')[1]
      })
    }
    return _options
  }
}
module.exports = new Url()

webpack内部模块的引用和输出使用的是CommonJs规范(也就是node的模块),所以模块输出方式遵照默认规范module.exports = new Url()

接下来,打包编译。执行$ webpack命令,打包成功。
为了方便测试,直接将编译好的文件复制一份放到笔者选择最近手上在做的项目中,项目正好使用的是vue框架。

启动服务,项目中引用方式如下:

import Url from './modules/Url'

控制台打印:

Url undefined

笔者当时的疑问是,难道打包好的文件没有成功输出吗?于是换用了node模块引入的方式:

控制台打印:

Url Object{Url: Url, __esModule: true}

可以看到,这种方式输出有值,但似乎是挂载到输出对象的Url属性上,这种输出形式与es6export Url有点类似。于是乎,笔者猜想,是否像es6未指定default对象时,需要采用{ Url }解构赋值的方式从输出对象的引用中获取到需要的值。

接下来再换了两种方式:

不出所料,控制台打印:

Url Url{}

看到这,有点搞清楚了,再回到打包好后的文件头部:

添加输出日志,发现控制台打印:

似乎明白了,最终输出方式的不同,应该是webpack切割文件时导致的。在这个项目中,打包好的Url.js文件里的自调用函数传入的this指向了webpack定义的一个全局对象,用来挂载输出。而引入未编译好的模块包,则不会有这种问题。

为什么会出现这个问题?笔者决定先将引入的Url模块包换成未编译时的文件Url2.js,然后将整个项目打包:

//  Url2.js
function Url() { }
Url.prototype.getParams = function (URL, key) {
  if (key) {
    let reg = new RegExp(key + '=([^&]+)')
    let val = (URL.match(reg) || ['', undefined])[1]
    return val
  } else {
    let _options = {}, urlStr = ''
    if (URL.indexOf('?') > 0) {
      urlStr = /\?([\S]*)/.exec(URL)[1]
      urlStr.split('&').forEach(function (item, i) {
        _options[item.split('=')[0]] = item.split('=')[1]
      })
    }
    return _options
  }
}
module.exports = new Url()

然后看打包后的app.js

可以看到webpack切割之后的对应的Url输出方式仍然为module.exports,与其他模块包无异。

然后换成编译后的Url.js,再次打包:

是不是一目了然了,最终输出该文件时没有传入exports对象,所以采用了挂载全局的形式。

简单来看,是因为webpack对已编译过的模块和未编译的模块的切割方式不同。而根本原因是,webpack在打包时能识别依赖模块是否符合UMD规范,如果不是UMD规范模块(比如注明的jQuery),或编译过的模块,都会被判断为非规范模块,以全局挂载的方式输出。


如果这个编译后的Url.js直接在一个静态html文件中采用如下方式引用:

<script src="../src/modules/Url.js"></script>
<script>
    console.log('Url', Url)
</script>

控制台打印:

可以看到,this指向window,所以输出值挂载在了window对象。

结论

综上所述,经过webpack打包后的模块,可以通过多种方式引用,这也是因为webpack的编译过程跟编译后如何被其他文件引用没有关系,只是跟js执行时的环境有关。一般来说Server端node场景不需要引入编译好的模块,而在使用vue等框架开发业务时,通也会结合webpack,所以引入的模块其实也不需要提前编译好。

之所以需要对这些业务组件进行打包编译,首先是因为在小组提供的公用组件中有不少是依赖于第三方库,其次是因为有不少项目应用场景还是沿用的以script标签加载js资源的方式。


NardoLi
31 声望1 粉丝

前端开发