蒋海云个人博客,日拱一卒。 2017-12-20T09:03:30+08:00 jiang.haiyun#gmail.com webpack 实践 13: Shimming 2017-12-20T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/webpack-practice-13-shimming Shimming

webpack 编译器能处理用 ES2015,CommonJS 或 AMD 编写的模块。

使用 Shimming 技术,使得能在 webpack 中处理 “非模块化” 的代码(例如 jQuery 库依赖一个全局的 $ 变量)。

Shimming 的另一个使用场景是用在 polyfill 浏览器的功能上。

Shimming 全局变量的场景

假设 lodash 包也和 jQuery 一样,导出为一个全局变量。

使用 ProvidePlugin 插件可使一个包能在每个通过 webpack 编译过的模块中作为一个变量使用。当 webpack 看到有使用该变量时,将在打包的 bundle 中加入相应的包体。

先在 src/index.js 中去除对 lodashimport 语句:

import _ from 'lodash';
function component() {
  var element = document.createElement('div');

  // Lodash, now imported by this script
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

  return element;
}

document.body.appendChild(component());

在 webpack.config.js 中提供 _ 变量的定义:

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

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new webpack.ProvidePlugin({
            _: 'lodash'
        })
    ]
};

还可以用 ProvidePlugin 导出模块中的多个属性和方法,参数形式为 [module, child1, child2, ...children?]

例如只导出 lodash 中的 join:

//webpack.config.js
    plugins: [
        new webpack.ProvidePlugin({
            join: ['lodash', 'join']
        })
    ]

此时 src/index.js 中直接使用 join 函数:

function component() {
  var element = document.createElement('div');

  // Lodash, now imported by this script
  element.innerHTML = join(['Hello', 'webpack'], ' ');

  return element;
}

document.body.appendChild(component());

运行 yarn run build 后等到的打包文件会变小,即未用 ProvidePlugin 导出的内容没有打包进来。

Granular Shimming

有些旧模块会将 this 作为 window 对象。

作为模拟,将 src/index.js 修改为:

function component() {
  var element = document.createElement('div');

  element.innerHTML = join(['Hello', 'webpack'], ' ');

  // Assume we are in the context of `window`
  this.alert("Hmmm, this probably isn't a great idea...");

  return element;
}

document.body.appendChild(component());

上面模块无法在 CommonJS 上下文中运行,因为此时的 this 指定 module.exports

需要在 webpack.config.js 中用 imports-loader 加载器重载 this:

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

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: require.resolve('./src/index.js'),
                use: 'imports-loader?this=>window'
            }
        ]
    },
    plugins: [
        new webpack.ProvidePlugin({
            join: ['lodash', 'join']
        })
    ]
};

之后 ./src/index.js 文件中的 this 都指向 window

安装 imports-loader:

$ yarn add imports-loader --dev

全局导出

假设有一个库,是以导出全局变量的形式实现的。假设库为 src/globals.js:

var file = 'blah.txt';
var helpers = {
  test: function() { console.log('test something'); },
  parse: function() { console.log('parse something'); }
}

此时需要在 webpack.config.js 中用 exports-loader 将全局变量导出转换为普通的模块导出,例如将模块中的 file 导出为 file, helpers.parse 导出为 pase:

    module: {
        rules: [
            {
                test: require.resolve('./src/globals.js'),
                use: 'exports-loader?file,parse=helpers.parse'
            }
        ]
    },

之后,在 src/index.js 中,可以用 import { file, parse } from './globals 的形式导入了。

安装 exports-loader:

$ yarn add exports-loader --dev

参考

]]>
webpack 实践 12: 打包发布自己的库 2017-12-19T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/webpack-practice-12-authoring-libraries 新建一个库

文件有:

  |- webpack.config.js
  |- package.json
  |- /src
    |- index.js
    |- ref.json

初始化:

$ npm init -y
$ npm install --save-dev webpack lodash

src/ref.json:

[{
  "num": 1,
  "word": "One"
}, {
  "num": 2,
  "word": "Two"
}, {
  "num": 3,
  "word": "Three"
}, {
  "num": 4,
  "word": "Four"
}, {
  "num": 5,
  "word": "Five"
}, {
  "num": 0,
  "word": "Zero"
}]

src/index.js:

import _ from 'lodash';
import numRef from './ref.json';

export function numToWord(num) {
  return _.reduce(numRef, (accum, ref) => {
    return ref.num === num ? ref.word : accum;
  }, '');
};

export function wordToNum(word) {
  return _.reduce(numRef, (accum, ref) => {
    return ref.word === word && word.toLowerCase() ? ref.num : accum;
  }, -1);
};

实现目标

  • 不打包进 lodash,让用户自行添加依赖
  • 库包设置为 webpack-numbers
  • 将库导出为一个叫 webpackNumbers 的变量
  • 能在 Node.js 中访问该库

同时用户能用如下方式访问库:

  • ES2015 模块中: import webpackNumbers from 'webpack-numbers'
  • CommonJS 模块中:require('webpack-numbers')
  • 使用 <script> 加载时通过全局变量访问

配置文件

//webpack.config.js
var path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'webpack-numbers.js'
  }
};

指定外部依赖

不打包进 lodash,只将它指定为是一个依赖,需要用户自行安装:

//webpack.config.js
var path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'webpack-numbers.js'
  },
  externals: {
    lodash: {
      commonjs: 'lodash',
      commonjs2: 'lodash',
      amd: 'lodash',
      root: '_'
    }
  }
};

也可以用正则表达式来指定多个外部依赖,例如对于依赖:

import A from 'library/one';
import B from 'library/two';
//...

可以在 webpack.config.js 中指定为:

externals: [
    'library/one',
    'library/two',
    // Everything that starts with "library/"
    /^library\/.+$/
]

导出库

导出库要能在多个环境下使用,如 CommonJS, AMD, Node.js 和作为全局变量等。

通过 output.library 属性设置导出的库名:

//webpack.config.js
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'webpack-numbers.js',
    library: 'webpackNumbers'
  }

这将我们的库作为一个名为 webpackNumbers 的全局变量导出。通过 output.libraryTarget 属性设置以何种方式导出库。

导出方式有:

  • libraryTarget: 'var': 通过 <script> 标识引用,作为一个全局变量使用,默认方式,通过 libraryTarget: { var: 'varname' },还同时指定导出的全局变量名
  • libraryTarget: 'this': 导出到 this,通过 this 访问
  • libraryTarget: 'window': 导出到浏览器的 window 对象中
  • libraryTarget: 'umd': 通过 require 加载使用

优化

可根据 webpack 实践 7: 生产环境 进行优化。

在 package.json 中设置:

{
    "main": "dist/webpack-numbers.js",
}

以指向库的最终的包文件。

同时根据 这篇文章 添加:

{
    "module": "src/index.js",
}

从而保持在非 ES2015 模块环境的兼容性。

运行 ` ./node_modules/.bin/webpack` 进行打包,将打包输出文件 作为一个 npm 包发布

参考

]]>
webpack 实践 11: 缓存 2017-12-18T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/webpack-practice-11-caching 输出文件中放入 hash

浏览器会根据资源的文件名进行缓存。

在 webpack 配置文件中,将 [hash] 替换变量设置在 output.filename 中,能实现针构建时相关的信息作为一个 hash 放入输出文件名中,例如:

output: {
    filename: '[name].[hash].js‘
}

[chunkhash] 替换变量可以将 chunk 内容相关的信息作为一个 hash 放入输出文件名中。

output: {
    filename: '[name].[chunkhash].js‘
}

src/index.js 文件:

import _ from 'lodash';
function component() {
  var element = document.createElement('div');

  // Lodash, now imported by this script
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

  return element;
}

document.body.appendChild(component());

webpack.config.js 文件:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: '[name].[chunkhash].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Caching'
    }),
    new CleanWebpackPlugin(['dist'])
  ]
};

运行 yarn run build 产生打包文件 main.15b52867804efb130452.js

Hash: 5b7a1b5a7365ee461d6a
Version: webpack 3.10.0
Time: 526ms
                       Asset       Size  Chunks                    Chunk Names
main.15b52867804efb130452.js     544 kB       0  [emitted]  [big]  main
                  index.html  197 bytes          [emitted]         
   [0] ./src/index.js 255 bytes {0} [built]
   [2] (webpack)/buildin/global.js 509 bytes {0} [built]
   [3] (webpack)/buildin/module.js 517 bytes {0} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
       [2] (webpack)/buildin/global.js 509 bytes {0} [built]
       [3] (webpack)/buildin/module.js 517 bytes {0} [built]
        + 2 hidden modules
Done in 1.07s.

运行 yarn run build 产生打包文件 main.15b52867804efb130452.js

Hash: 5b7a1b5a7365ee461d6a
Version: webpack 3.10.0
Time: 535ms
                       Asset       Size  Chunks                    Chunk Names
main.15b52867804efb130452.js     544 kB       0  [emitted]  [big]  main
                  index.html  197 bytes          [emitted]         
   [0] ./src/index.js 255 bytes {0} [built]
   [2] (webpack)/buildin/global.js 509 bytes {0} [built]
   [3] (webpack)/buildin/module.js 517 bytes {0} [built]
    + 1 hidden module
Child html-webpack-plugin for "index.html":
     1 asset
       [2] (webpack)/buildin/global.js 509 bytes {0} [built]
       [3] (webpack)/buildin/module.js 517 bytes {0} [built]
        + 2 hidden modules
Done in 1.07s.

可见虽然代码没有修改,但是打包文件中的 hash 变了。

这是因为 webpack 会将运行时和 manifest 等一些样板信息放入 entry chunk 中,而这些信息在每次构建时会有变动。

抽出样板信息

利用 CommonsChunkPlugin 插件可以将 manifest 等样板信息抽出,生成一个独立的 bundle。

webpack.config.js:

const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
        name: 'manifest'
    })
  ]
}

再次运行 yarn run build 会看到抽出会生成了独立的 manifest.e949286afc215d459d57.js

最好将第三方库 loadash, react 等代码统一打包在一个独立的 vendor 包,因为这些内容一般不会变动。设置如下:

//webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  entry: {
      main: './src/index.js',
      vendor: [
          'lodash'
      ]
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Caching'
    }),
    new CleanWebpackPlugin(['dist']),
    new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
        name: 'manifest'
    })
  ]
};

配置文件中的 CommonsChunkPlugin 实例的放置位置很重要,vendor 的必须放在 manifest 之上。

模块 标识

添加另一个模块 src/print.js:

export default function print(text) {
  console.log(text);
}

src/index.js 修改为:

import _ from 'lodash';
import Print from './print';

function component() {
  var element = document.createElement('div');

  // Lodash, now imported by this script
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  element.onclick = Print.bind(null, 'Hello webpack!');

  return element;
}

document.body.appendChild(component());

这次只是新增加了一个模块,并且只修改了 index.js,再次运行 yarn run build,会发现 manifest, vendor, main 这 3 个包文件名都变动了,与预期只有 main 会变动不符。

这是因为 module.id 是基于使用该模块的时间,其值是递增的,即删减模块后,其它模块的 ID 也可能会变化。

  • main 包名变动是因为它的内容有修改。
  • vendor 包名变动是因为它内部模块的 module.id 有变动。
  • manifest 包名变动是因为它包含了一个新的模块的引用。

添加 NamedModulesPlugin 插件后,webpack 会使用模块的路径名作为其标识,因而删减模块后,模块标识不会变动。但因其性能原因,最好只用在开发环境下。而生产环境中可使用 HashedModuleIdsPlugin 插件:

//webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  entry: {
      main: './src/index.js',
      vendor: [
          'lodash'
      ]
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Caching'
    }),
    new CleanWebpackPlugin(['dist']),
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
        name: 'manifest'
    })
  ]
};

之后运行 yarn run build 的结果会如预期。

参考

]]>
webpack 实践 10: 环境变量 2017-12-16T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/webpack-practice-10-env-variables webpack 命令可以通过 --env 加入多个环境变量值,例如 webpack --env.production --env.NODE_ENV=local 等,这些环境变量值可以在 webpack.config.js 中引用。

传入的环境变量没有用 = 给定值时,默认值为 true,例如 webpack --env.production

webpack 的配置文件中的 module.exports 通过是指向一个 {} 对象,但是要引用环境变量 env 时,需要将 module.exports 指向一个函数,例如:

//webpack.config.js
module.exports = env => {
  // Use env.<YOUR VARIABLE> here:
  console.log('NODE_ENV: ', env.NODE_ENV) // 'local'
  console.log('Production: ', env.production) // true

  return {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  }
}

参考

]]>
webpack 实践 9: 按需加载 2017-12-14T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/webpack-practice-9-lazy-loading 本文在代码分割的基础上,实现当用户交互时,进行按需加载。

创建新的模块 src/print.js:

console.log('The print.js module has loaded! See the network tab in dev tools...');

export default () => {
  console.log('Button Clicked: Here\'s "some text"!');
}

src/index.js 调整为根据用户交互进行动态加载:

import _ from 'lodash';

function component() {
    var element = document.createElement('div');
    var button = document.createElement('button');
    var br = document.createElement('br');

    button.innerHTML = 'Click me and look at the console!';
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    element.appendChild(br);
    element.appendChild(button);

    // Note that because a network request is involved, some indication
    // of loading would need to be shown in a prod-level site/app.
    button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
        var print = module.default;
        print();
    });

    return element;
}

document.body.appendChild(component());

注意当 import() ES6 模块时,必须使用 module 对象的 default 属性值,它是当 promise resolved 时的返回值。

webpack.conf.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
  },
  output: {
    filename: '[name].bundle.js',
    chunkFilename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Lazy Loading'
    }),
    new CleanWebpackPlugin(['dist'])
  ]
};

运行 yarn run build 进行打包测试。

React 等框架都有自己的动态加载方法,见 Code Splitting and Lazy Loading

参考

]]>
webpack 实践 8: 代码分割 2017-12-13T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/webpack-practice-8-code-splitting webpack 的代码分割功能能将代码分成多个 bundle,从而实现按需加载或并行加载。

有 3 种常用的代码分割方法:

  • 入口点:在配置文件中通过 entry 手动指定分割
  • 去重:使用 CommonsChunkPlugin 插件抽取重复模块
  • 动态加载:通过模块中的内联函数调用实现

入口点方法

这是最直观最简单的方法,缺点是需要手动配置分割,并且重复的模块可能会打包进多个 bundle 中,无法去重。

添加另一个模块 src/another-module.js

import _ from 'lodash';

console.log(
    _.join(['Another', 'module', 'loaded!'], ' ')
);

webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Code Splitting'
    }),
    new CleanWebpackPlugin(['dist'])
  ]
};

src/index.js:

import _ from 'lodash';

function component() {
  var element = document.createElement('div');

  // Lodash, now imported by this script
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

  return element;
}

document.body.appendChild(component());

运行 yarn run build 会打包出两个文件 dist/index.bundle.jsdist/another.bundle.js。由于 src/index.jssrc/another-module.js 这两个源文件中都加载了 lodash,两个结果包中也都有包含了 lodash,没有去重。

去重

CommonsChunkPlugin 插件可以将通用的依赖模块抽取出来,保存到现有的 entry chunk 或一个新的 chunk 中。

先在 webpack.config.js 中使用该插件:

const webpack = require('webpack');

module.exports = {
  //...
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
        name: 'common' // Specify the common bundle's name.
    })
  ]
};

运行 yarn run build 可以多出了一个打包文件: dist/common.bundle.js。即将原来两个包文件中重复的 lodash 模块抽取了出来并单独保存为了一个新的打包文件。

使用该插件可以将第三方库 (vendor) 代码抽取并打包为一个独立包。

可用于分割代码的其它插件和加载器:

动态加载

这里会有 2 种方法:

  • 使用 import() 语法(内部使用了 promises 实现,因此老浏览器上需使用 es6-promisepromise-polyfill
  • 另一个老方法,即使用 webpack 的 require.ensure

先在 webpack.config.js 中去除 CommonsChunkPlugin 插件,入口设置为一个,output 中添加 chunkFilename 配置:

//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
  },
  output: {
    filename: '[name].bundle.js',
    chunkFilename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Code Splitting'
    }),
    new CleanWebpackPlugin(['dist'])
  ]
};

chunkFilename 配置项用来确定 non-entry chunk 文件的名字,详细见 output doc

src/index.js 修改成动态加载 lodash

function getComponent() {
    return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
        var element = document.createElement('div');
        element.innerHTML = _.join(['Hello', 'webpack'], ' ');
        return element;
    }).catch(error => 'An error occurred while loading the component');
}

getComponent().then(component => {
    document.body.appendChild(component);
});

注意 import 语句中的 webpackChunkName: "lodash" 注释,它将使动态加载的模块打包为 lodash.bundle.js。关于 webpackChunkName 和其它选项,见 import doc

打包后可看到生成了 dist/lodash.bundle.jsdist/index.bundle.js

Bundle 分析工具

  • 官方工具
  • webpack-chart: 以交互式的饼图显示 webpack stat
  • webpack-visualizer:可视化并分析你的 bundle,以查看模块都占用多少空间,是否有重复
  • webpack-bundle-analyzer: 即是一个插件,也是一个命令行工具,能以便捷的交互式的可绽放的 treemap 方式呈现 bundle 的内容。

参考

]]>
在本机以 docker 方式运行 github pages 及添加评论系统 isso 2017-12-11T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/github-pages-in-docker-add-comments 在 docker 中运行 github-pages

使用 docker hub 中现存的映像文件 starefossen/github-pages,这是它对应的 Dockerfile

需要将 github page 乃至的各种插件 gem 及 github pages 的仓库地址都配置在 _config.yml 文件中,例如:

# in _config.yml
repository: haiiiiiyun/haiiiiiyun.github.io
plugins:
- jekyll-paginate
- jekyll-github-metadata
- jekyll-mentions
- jekyll-redirect-from
- jekyll-sitemap
- jemoji

在本机运行的例子:

$ docker run \
  --name atjiang \
  -t \
  --restart always \
  -v "/home/hy/workspace/haiiiiiyun.github.io":/usr/src/app \
  -e JEKYLL_GITHUB_TOKEN=your_github_token \
  -p "9900:4000" starefossen/github-pages &

运行评论系统 isso

isso 是一个类似于 Disqus 的评论系统,支持匿名评论,可以将它集成到静态网站中。它的 github 地址是 github.com/posativ/isso

使用 docker hub 上现有的映像文件 wonderfall/isso,这是它对应的 Dockerfile

配置

为 isso 创建配置文件 isso.conf:

[general]
; database location, check permissions, automatically created if not exists
dbpath = /db/comments.db
; your website or blog (not the location of Isso!)
; you can add multiple hosts for local development
; or SSL connections. There is no wildcard to allow
; any domain.
host = 
    http://www.atjiang.com/
    http://atjiang.com/
    http://fullstackpython.atjiang.com/
    http://localhost:8080
log-file = /db/logs

[server]
listen = http://0.0.0.0:8080

[guard]
enabled = true
ratelimit = 2
direct-reply = 3
reply-to-self = true
require-author = true
require-email = false

配置文件中指定了数据库文件(sqlite 3) 及日志文件的位置。同时指定的需要使用该评论系统的域名。

在本机上为 isso 创建目录:

$ mkdir  -p ~/workspace/isso_comments
$ cd ~/workspace/isso_comments

将上面的配置文件 isso.conf 移到 ~/workspace/isso_comments 目录下。

运行:

$ docker run \
  --name isso_comments \
  --restart always \
  -v "/home/hy/workspace/isso_comments":/config \
  -v "/home/hy/workspace/isso_comments":/db \
  -p "9902:8080" \
  wonderfall/isso &

访问 http://localhost:9902/js/embed.min.js 检测是否运行 isso 成功。

将上面的 isso 系统运行到服务器上,假设绑定的域名 “comments.my_isso.com”。

集成

在 github pages 的模板中加入:

<script data-isso="//comments.my_isso.com/"
        data-isso-css="true"
        data-isso-lang="zh"
        data-isso-reply-to-self="false"
        data-isso-require-author="true"
        data-isso-require-email="false"
        data-isso-max-comments-top="10"
        data-isso-max-comments-nested="5"
        data-isso-reveal-on-click="5"
        data-isso-avatar="true"
        data-isso-avatar-bg="#f0f0f0"
        data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
        data-isso-vote="true"
        data-vote-levels=""
        src="//comments.my_isso.com/js/embed.min.js"></script>
<section id="isso-thread"></section>
<noscript>请开启 JavaScript 查看 <a href="https://posativ.org/isso/" rel="nofollow">isso 评论系统的内容</a></noscript>

完成。

参考

]]>
webpack 实践 7: 生产环境 2017-12-10T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/webpack-practice-7-production 将 webpack 配置文件分成 3 个文件,共用部分 webpack.common.js,开发环境用 webpack.dev.js,生产环境下用 webpack.prod.js,然后用 webpack-merge 工具组合。

先安装 webpack-merge:

$ yarn add webpack-merge --dev
//webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  entry: {
      app: './src/index.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
          title: 'Production'
      })
  ],
  module: {
      rules: [
          {
              test: /\.css/,
              use: [
                  'style-loader',
                  'css-loader'
              ]
          },
          {
              test: /\.(png|svg|jpg|gif|)$/,
              use: [
                  'file-loader'
              ]
          },
          {
              test: /\.(woff|woff2|eot|ttf|otf)$/,
              use: [
                  'file-loader'
              ]
          },
          {
              test: /\.(csv|tsv)$/,
              use: [
                  'csv-loader'
              ]
          },
          {
              test: /\.xml$/,
              use: [
                  'xml-loader'
              ]
          }
      ]
  }
};
//webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist'
    }
});
//webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = merge(common, {
    plugins: [
        new UglifyJSPlugin()
    ]
});

自定义命令

package.json 中定义 start 命令用于开发环境下的测试,而 build 命令用于生产环境下的构建:

"scripts": {
    "start": "webpack-dev-server --open --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
},

混淆和最小化代码

可以用 UglifyJSPlugin,还有一些更加流行的工具,如:

Source Mapping

建议在生产环境中也启用 source map, 以便于定位错误。在开发环境中一般用 inline-source-map,而在生产环境中避免用 inline-***eval-*** 的 source-map,它们会增大打包后的文件大小,并影响性能。

生产环境中可以使用 source-map,并在 UglifyJSPlugin 中也启用 souce map:

module.exports = merge(common, {
    devtool: 'source-map',
    plugins: [
        new UglifyJSPlugin({
            sourceMap: true
        })
    ]
});

指定环境

process.env.NODE_ENV 这个变量值可以用来检测当前是生产环境还是开发环境。

可以使用 webpack 内置的 DefinePlugin 插件来定义这个变量值:

//webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const webpack = require('webpack');

module.exports = merge(common, {
    devtool: 'source-map',
    plugins: [
        new UglifyJSPlugin({
            sourceMap: true
        }),
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('production')
        })
    ]
});

如果使用 react,可以看到这样定义后,打包文件会显著变小。

可以在源码的任何地方使用该变量:

//src/index.js
if (process.env.NODE_ENV !== 'production') {
    console.log('Looks like we are in devel mode!');
}

用命令行传入

上面提到的这些插件也可以用 webpack 的命令行选项传入。

  • --optimize-minimize 将自动加入 UglifyJSPlugin 插件。
  • --define process.env.NODE_ENV="'production'" 会自动加入 DefinePlugin 实例并定义值。

参考

]]>
webpack 实践 6: 去除无用代码 Tree Shaking 2017-12-10T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/webpack-practice-6-tree-shaking webpack 2 内置对 unused module export 的检测。而 Tree Shaking 就是在打包时将没有用到的导出对象排除到包外。

先添加一个工具库 src/math.js,并导出两个函数:

export function square(x) {
    return x*x;
}

export function cube(x) {
    return x*x*x;
}

src/index.js 修改为:

import { cube } from './math.js';

function component() {
  var element = document.createElement('pre');

  element.innerHTML = [
      'Hello webpack!',
      '5 cubed = ' + cube(5)
  ].join('\n\n');

  return element;
}

document.body.appendChild(component());

由于没有从 src/math.js 中导入 square 方法,因此该函数为 dead code

运行 yarn run build 后检查 dist/bundle.js

/***/ "./src/math.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function square(x) {
    return x*x;
}

function cube(x) {
    return x*x*x;
}


/***/ })

/******/ });

square 函数上加入了 unused harmony export square 的注释,并且该函数在打包后没有导出。

使用 UglifyJSPlugin 混淆并去除无用代码

先安装:

$ yarn add uglifyjs-webpack-plugin --dev

在 webpack.config.js 中使用:

const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

//...
plugins: [
    new UglifyJSPlugin()
]

此后构建的包中将不再包含 square 代码。

在运行 webpack 时,添加 --optimize-minimize 命令行选项也能插入 UglifyJSPlugin 插件。

webpack 使用第三方工具实现 tree-shaking, 工具有 UglifyJSwebpack-rollup-loaderBabel Minify Webpack Plugin

参考

]]>
webpack 实践 5:模块热更新 HMR 2017-12-09T00:00:00+08:00 Haiiiiiyun haiiiiiyun.github.io/webpack-practice-5-hot-module-replacement HRM 适合在开发环境中使用,不适合不生产中使用。

开启 HMR

只需在 webpack.config.js 中为 webpack-dev-server 添加 hot: true,并使用 webpack 的内置 HMR 插件即可,同时修改为一个 entry:

const webpack = require("webpack");

module.exports = {
  entry: {
      app: './src/index.js'
  },
  devServer: {
    contentBase: './dist',
    hot: true
  },
  plugins: [
      new webpack.NamedModulesPlugin(),
      new webpack.HotModuleReplacementPlugin()
  ]
}

使用 NamedModulesPlugin 插件可以看出哪个依赖库打了补丁。

使用 yarn run start 开启开发服务器。在命令行中使用 webpack-dev-server --hotOnly 开启开发服务器也可以传入参数。

src/index.js 中添加接收模块更新的代码,从而当 print.js 有更新时,运行相关的回调函数:

if (module.hot) {
    module.hot.accept('./print.js', function(){
        console.log("Accepting the updated printMe module!");
        printMe();
    })
}

src/print.js 修改为:

export default function printMe() {
    console.log("Updating print.js...");
}

可在浏览器的 console 中看到:

[HMR] Waiting for update signal from WDS...
main.js:4395 [WDS] Hot Module Replacement enabled.
+ 2main.js:4395 [WDS] App updated. Recompiling...
+ main.js:4395 [WDS] App hot update...
+ main.js:4330 [HMR] Checking for updates on the server...
+ main.js:10024 Accepting the updated printMe module!
+ 0.4b8ee77….hot-update.js:10 Updating print.js...
+ main.js:4330 [HMR] Updated modules:
+ main.js:4330 [HMR]  - 20
+ main.js:4330 [HMR] Consider using the NamedModulesPlugin for module names.

通过 Node.js API 使用 Webpack Dev Server

此时不要把开发服务器的配置项放在 webpack 的配置对象中,而要在创建时将它作为第 2 个参数传入:

new WebpackDevServer(compiler, options)

在开启 HMR,需要修改 webpack 配置对象,以加入 HMR 入口点,而 webpack-dev-server 包中的一个叫 addDevServerEntrypoints 的方法能添加 HMR 入口点。下面是一个例子:

//dev-server.js
const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');

const config = require('./webpack.config.js');
const options = {
  contentBase: './dist',
  hot: true,
  host: 'localhost'
};

webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);

server.listen(5000, 'localhost', () => {
  console.log('dev server listening on port 5000');
});

使用 HMR 时需注意

上例中,当通过 HMR 更新完 printMe 函数后,button 上绑定的 onclick 处理函数还是旧的 printMe。要绑定更新后的函数, src/index.js 需修改为:

let element = component(); // Store the element to re-render on print.js changes
document.body.appendChild(element);

if (module.hot) {
    module.hot.accept('./print.js', function(){
        document.body.removeChild(element);
        element = component(); // re-render the "component" to update the click handler
        document.body.appendChild(element);
    });
}

样式的 HMR

style-loader 加载器会通过 module.hot.accept 自动更新依赖的 CSS。

因此修改 css 样式后,会在页面上立即体现。

参考

]]>