Skip to content

Webpack #32

Description

@chengfengliu

CommonJS

var b = require('./b')
CommonJS在浏览器内并不适用. 因为require()的返回是同步的, 意味着有多个依赖的话需要一个一个依次下载, 堵塞了js脚本的执行.

深入浅出node.js

CommonJS的模块规范
1.模块引用
require()方法
用const
2.模块定义
对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式。
(function)
3.模块标识
Node的模块实现
1.路径分析
2.文件定位
(1)核心模块,如http、fs、path等。
(2).或者..开始的相对路径文件模块。
(3)以/开始的绝对路径文件模块。
(4)非路径形式的文件模块,通常是npm下载的模块,如connect模块。从node_modules目录里找

-g全局模式安装
全局模式并不是将一个模块包安装为一个全局包的意思,它并不意味着可以从任何地方通过require()来引用它。
-g是将一个包安装为全局可用的可执行命令。它根据包描述文件中(package.json)的bin字段配置,将实际脚本链接到与Node可执行文件相同的路径下:
“bin”: {
“express”: "./bin/express"
}
事实上,通过全局模式安装的所有模块包都被安装进了一个统一的目录下,如果Node可执行文件的位置是/user/local/bin/node,那么模块目录就是/user/local/lib/node_modules。最后,通过软链接的方式将bin字段配置的可执行文件链接到Node的可执行目录下。
例如环境变量的path中有E:\nodejs\,打开命令行工具任意文件下都可以使用node命令node -v,全局安装express后,将express模块的bin文件夹下的可执行文件的快捷方式会放进E:\nodejs\,以后任意文件下也可以使用express命令行。

3.编译执行
Node对获取的JavaScript文件内容进行了头尾包装。
(function(exports,require,module,__filename,__dirname){...}

前后端JavaScript分别搁置在HTTP的两端,它们扮演的角色并不同。浏览器端的JavaScript需要经历从同一个服务器端分发到多个客户端执行,而服务器端JavaScript则是相同的代码需要多次执行。前者的瓶颈在于带宽,后者的瓶颈则在于CPU和内存等资源。前者需要通过网络加载代码,后者从磁盘中加载,两者的加载速度不在一个数量级上。
纵观Node的模块引入过程,几乎全都是同步的。尽管与Node强调异步的行为有些相反,但它是合理的。但是如果前端模块也采用同步的方式引入,那将会在用户体验上造成很大的问题。UI在初始化过程中需要花费很多时间来等待脚本加载完成。
鉴于网络的原因,CommonJS为后端JavaScript制定的规范并不完全适合前端的应用场景。经过一段争执之后,AMD规范最终在前端应用场景中胜出。

Asynchronous Module Definition (AMD)

require(['./b', './c'], function(b, c) {
  var n = b.square(2)
  console.log(c) // 3.14159
})
define(['./d'], function(d) {
  return d.PI
})

webpack

1060770-20170604185938368-2068785469

├── dist                      打包输出目录, 只需部署这个目录到生产环境
├── package.json              项目配置信息
├── node_modules              npm安装的依赖包都在这里面
├── src                       我们的源代码
│   ├── components            可以复用的模块放在这里面
│   ├── index.html            入口html
│   ├── index.js              入口js
│   ├── shared                公共函数库
│   └── views                 页面放这里
└── webpack.config.js         webpack配置文件

npm init
帮助生成package.json文件
npm install webpack webpack-cli webpack-serve html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev
npm install可以一条命令同时安装多个包, 包之间用空格分隔. 包会被安装进node_modules目录中.
--save-dev会把安装的包和版本号记录到package.json中的devDependencies对象中, 还有一个--save, 会记录到dependencies对象中。
它们的区别, 我们可以先简单的理解为打包工具和测试工具用到的包使用--save-dev存到devDependencies, 比如eslint, webpack.
浏览器中执行的js用到的包存到dependencies, 比如jQuery等.
安装完webpack后运行
npx webpack --mode development //记得后面运行环境,npx:一个npm包执行器
提示你没有安装webpack-cli这是因为 webpack 在 webpack 4 里将命令行相关的都迁移至 webpack-cli 包。输入yes安装。
自动刷新浏览器的问题,webpack 提供 webpack-dev-server 来解决
npm install -D webpack-dev-server//-D就是--save-dev
给出项目运行地址和端口
Project is running at http://localhost:8080/

babel

npm install babel-core babel-preset-env babel-loader --save-dev

webpack.config.js

const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const history = require('connect-history-api-fallback')
const convert = require('koa-connect')

// 使用WEBPACK_SERVE环境变量检测当前是否是在webpack-server启动的开发环境中
const dev = Boolean(process.env.WEBPACK_SERVE)

module.exports = {
  /*
  webpack执行模式
  development: 开发环境, 它会在配置文件中插入调试相关的选项, 比如moduleId使用文件路径方便调试
  production: 生产环境, webpack会将代码做压缩等优化
  */
  mode: dev ? 'development' : 'production',

  /*
  配置source map
  开发模式下使用cheap-module-eval-source-map, 生成的source map能和源码每行对应, 方便打断点调试
  生产模式下使用hidden-source-map, 生成独立的source map文件, 并且不在js文件中插入source map路径, 用于在error report工具中查看 (比如Sentry)
  */
  devtool: dev ? 'cheap-module-eval-source-map' : 'hidden-source-map',

  // 配置页面入口js文件
  entry: './src/index.js',

  // 配置打包输出相关
  output: {
    // 打包输出目录
    path: resolve(__dirname, 'dist'),

    // 入口js的打包输出文件名
    filename: 'index.js'
  },

  module: {
    /*
    配置各种类型文件的加载器, 称之为loader
    webpack当遇到import ... 时, 会调用这里配置的loader对引用的文件进行编译
    */
    rules: [
      {
        /*
        使用babel编译ES6/ES7/ES8为ES5代码
        使用正则表达式匹配后缀名为.js的文件
        */
        test: /\.js$/,

        // 排除node_modules目录下的文件, npm安装的包不需要编译
        exclude: /node_modules/,

        /*
        use指定该文件的loader, 值可以是字符串或者数组.
        这里先使用eslint-loader处理, 返回的结果交给babel-loader处理. loader的处理顺序是从最后一个到第一个.
        eslint-loader用来检查代码, 如果有错误, 编译的时候会报错.
        babel-loader用来编译js文件.
        */
        use: ['babel-loader', 'eslint-loader']
      },

      {
        // 匹配.html文件
        test: /\.html$/,
        /*
        使用html-loader, 将html内容存为js字符串, 比如当遇到
        import htmlString from './template.html';
        template.html的文件内容会被转成一个js字符串, 合并到js文件里.
        */
        use: 'html-loader'
      },

      {
        // 匹配.css文件
        test: /\.css$/,

        /*
        先使用css-loader处理, 返回的结果交给style-loader处理.
        css-loader将css内容存为js字符串, 并且会把background, @font-face等引用的图片,
        字体文件交给指定的loader打包, 类似上面的html-loader, 用什么loader同样在loaders对象中定义, 等会下面就会看到.
        */
        use: ['style-loader', 'css-loader']
      },

      {
        /*
        匹配各种格式的图片和字体文件
        上面html-loader会把html中<img>标签的图片解析出来, 文件名匹配到这里的test的正则表达式,
        css-loader引用的图片和字体同样会匹配到这里的test条件
        */
        test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,

        /*
        使用url-loader, 它接受一个limit参数, 单位为字节(byte)

        当文件体积小于limit时, url-loader把文件转为Data URI的格式内联到引用的地方
        当文件大于limit时, url-loader会调用file-loader, 把文件储存到输出目录, 并把引用的文件路径改写成输出后的路径

        比如 views/foo/index.html中
        <img src="smallpic.png">
        会被编译成
        <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAA...">

        而
        <img src="largepic.png">
        会被编译成
        <img src="/f78661bef717cf2cc2c2e5158f196384.png">
        */
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10000
            }
          }
        ]
      }
    ]
  },

  /*
  配置webpack插件
  plugin和loader的区别是, loader是在import时根据不同的文件名, 匹配不同的loader对这个文件做处理,
  而plugin, 关注的不是文件的格式, 而是在编译的各个阶段, 会触发不同的事件, 让你可以干预每个编译阶段.
  */
  plugins: [
    /*
    html-webpack-plugin用来打包入口html文件
    entry配置的入口是js文件, webpack以js文件为入口, 遇到import, 用配置的loader加载引入文件
    但作为浏览器打开的入口html, 是引用入口js的文件, 它在整个编译过程的外面,
    所以, 我们需要html-webpack-plugin来打包作为入口的html文件
    */
    new HtmlWebpackPlugin({
      /*
      template参数指定入口html文件路径, 插件会把这个文件交给webpack去编译,
      webpack按照正常流程, 找到loaders中test条件匹配的loader来编译, 那么这里html-loader就是匹配的loader
      html-loader编译后产生的字符串, 会由html-webpack-plugin储存为html文件到输出目录, 默认文件名为index.html
      可以通过filename参数指定输出的文件名
      html-webpack-plugin也可以不指定template参数, 它会使用默认的html模板.
      */
      template: './src/index.html',

      /*
      因为和webpack 4的兼容性问题, chunksSortMode参数需要设置为none
      https://github.com/jantimon/html-webpack-plugin/issues/870
      */
      chunksSortMode: 'none'
    })
  ]
}

/*
配置开发时用的服务器, 让你可以用 http://127.0.0.1:8080/ 这样的url打开页面来调试
并且带有热更新的功能, 打代码时保存一下文件, 浏览器会自动刷新. 比nginx方便很多
如果是修改css, 甚至不需要刷新页面, 直接生效. 这让像弹框这种需要点击交互后才会出来的东西调试起来方便很多.

因为webpack-cli无法正确识别serve选项, 使用webpack-cli执行打包时会报错.
因此我们在这里判断一下, 仅当使用webpack-serve时插入serve选项.
issue: https://github.com/webpack-contrib/webpack-serve/issues/19
*/
if (dev) {
  module.exports.serve = {
    // 配置监听端口, 默认值8080
    port: 8080,

    // add: 用来给服务器的koa实例注入middleware增加功能
    add: app => {
      /*
      配置SPA入口

      SPA的入口是一个统一的html文件, 比如
      http://localhost:8080/foo
      我们要返回给它
      http://localhost:8080/index.html
      这个文件
      */
      app.use(convert(history()))
    }
  }
}

output是输出配置
path表示输出的硬盘路径,不过热替换中编译好的代码放在内存中,此时此项无用。
filename表示输出的文件名称。
publicPath则表示公共路径,即所有资源的URL前缀。比如,如果你的HTML页面中有一个
<script src="/static/bundle.js"></script>,那么应该将publicPath设为/static/。在热替换中,这个路径是虚拟的(即无法在硬盘中找到这个路径),打包编译后的脚本被保存到了内存中。

webpackDevMiddleware中间件
Webpack本身只有打包功能,但是把这个中间件连入Express服务器后,就可以将Webpack的打包功能与Express服务器的资源服务功能合二为一**(打包后再提供)**。Express服务器通过该中间件完成打包工作,并将打包好的资源读到内存中,接下来我们就可以通过Express服务器的端口访问这些资源了。不仅如此,该中间件可以监视代码变动,一旦代码变动,就会停止提供旧版的资源服务,等到编译完成后继续提供新的资源服务。

webpackHotMiddleware中间件
webpackDevMiddleware中间件已经完成了大部分自动化工作,但是它无法实现热替换,也就是无法在更新代码后保留程序状态。为了实现热替换,还应该在webpackDevMiddleware中间件的基础上加上webpackHotMiddleware中间件。

DllPlugin 和 DllReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度。
DllPlugin生成一个manifest.json文件(包含了从 require 和 import 的request到模块 id 的映射)和dll.js文件(第三方库都打包到这个js文件中)

const path = require('path')
const webpack = require('webpack')

const config = {
  mode: 'production',
  entry: {
    vendors: [
      'react',
      'react-dom',
      'react-router-dom',
      'antd'
    ],
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '..', 'static', 'scripts'),
    library: '[name]_lib',
  },
  plugins: [
    new webpack.DllPlugin({
      // context不能没有
      context: __dirname,
      name: '[name]_lib',
      path: path.resolve(__dirname, '[name].manifest.json'),
    })
  ],
}

module.exports = config

DllReferencePlugin通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions