专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
前端早读课  ·  【早阅】Leaflet地图库采用指南 ·  昨天  
前端大全  ·  深入了解Vite:依赖预构建原理 ·  2 天前  
前端早读课  ·  【早阅】如何使用Vike和Vite构建可扩展 ... ·  4 天前  
前端大全  ·  20个超好看的落地页/首页模板(附源码) ·  1 周前  
前端早读课  ·  【早阅】会话令牌和JWT结合的优势 ·  1 周前  
51好读  ›  专栏  ›  前端早读课

webpack 2 打包实战(下)

前端早读课  · 公众号  · 前端  · 2017-07-11 04:50

正文

继续下篇

配置favicon

在src目录中放一张favicon.png, 然后src/index.html的

中插入:

<link rel="icon" type="image/png" href="favicon.png">

修改webpack 配置:

{
 module: {
   rules: [
     {
       test: /.html$/,
       use: [
         {
           loader: 'html-loader',
           options: {
             /*
             html-loader接受attrs参数, 表示什么标签的什么属性需要调用webpack的loader进行打包.
             比如标签的src属性, webpack会把引用的图片打包, 然后src的属性值替换为打包后的路径.
             使用什么loader代码, 同样是在module.rules定义中使用匹配的规则.

             如果html-loader不指定attrs参数, 默认值是img:src, 意味着会默认打包标签的图片.
             这里我们加上标签的href属性, 用来打包入口index.html引入的favicon.png文件.
             */

             attrs: ['img:src', 'link:href']
           }
         }
       ]
     },

     {
       /*
       匹配favicon.png
       上面的html-loader会把入口index.html引用的favicon.png图标文件解析出来进行打包
       打包规则就按照这里指定的loader执行
       */

       test: /favicon.png$/,

       use: [
         {
           // 使用file-loader
           loader: 'file-loader',
           options: {
             // name: 指定文件输出名
             // [name]是源文件名, 不包含后缀. [ext]为后缀. [hash]为源文件的hash值,
             // 这里我们保持文件名, 在后面跟上hash, 防止浏览器读取过期的缓存文件.
             name: '[name].[ext]?[hash]'
           }
         }
       ]
     },

     // 图片文件的加载配置增加一个exclude参数
     {
       test: /.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(?.+)?$/,

       // 排除favicon.png, 因为它已经由上面的loader处理了. 如果不排除掉, 它会被这个loader再处理一遍
       exclude: /favicon.png$/,

       use: [
         {
           loader: 'url-loader',
           options: {
             limit: 10000
           }
         }
       ]
     }
   ]
 }
}

其实html-webpack-plugin接受一个favicon参数, 可以指定favicon文件路径, 会自动打包插入到html文件中. 但它有个bug, 打包后的文件名路径不带hash, 就算有hash, 它也是[hash], 而不是[chunkhash], 导致修改代码也会改变favicon打包输出的文件名. issue中提到的favicons-webpack-plugin倒是可以用, 但它依赖PhantomJS, 非常大.

开发环境允许其他电脑访问

webpack配置devServer.host为`0.0.0.0`即可.

打包时自定义部分参数

在多人开发时, 每个人可能需要有自己的配置, 比如说webpack-dev-server监听的端口号, 如果写死在webpack配置里, 而那个端口号在某个同学的电脑上被其他进程占用了, 简单粗暴的修改webpack.config.js会导致提交代码后其他同学的端口也被改掉.

还有一点就是开发环境/测试环境/生产环境的部分webpack配置是不同的, 比如publicPath在生产环境可能要配置一个CDN地址.

我们在根目录建立一个文件夹config, 里面创建3个配置文件:

default.js: 生产环境 

module.exports = {
 publicPath: 'http://cdn.example.com/assets/'
}

dev.js: 默认开发环境

module.exports = {
 publicPath: '/assets/',

 devServer: {
   port: 8100,
   proxy: {
     '/api/auth/': {
       target: 'http://api.example.dev',
       changeOrigin: true,
       pathRewrite: { '^/api': '' }
     },

     '/api/pay/': {
       target: 'http://pay.example.dev',
       changeOrigin: true,
       pathRewrite: { '^/api': '' }
     }
   }
 }
}

local.js: 个人本地环境, 在dev.js基础上修改部分参数 .

const config = require('./dev')
config.devServer.port = 8200
module.exports = config

package.json修改scripts:

{
 "scripts": {
   "local": "npm run dev --config=local",
   "dev": "webpack-dev-server -d --hot --env.dev --env.config dev",
   "build": "rimraf dist && webpack -p"
 }
}

webpack配置修改:

// ...
const url = require('url')

module.exports = (options = {}) => {
 const config = require('./config/' + (process.env.npm_config_config || options.config || 'default'))

 return {
   // ...
   devServer: config.devServer ? {
     host: '0.0.0.0',
     port: config.devServer.port,
     proxy: config.devServer.proxy,
     historyApiFallback: {
       index: url.parse(config.publicPath).pathname
     }
   } : undefined,
 }
}

这里的关键是npm run传进来的自定义参数可以通过process.env.npm_config_*获得. 参数中如果有-会被转成_

--env.*传进来的参数可以通过options.*获得. 我们优先使用npm run指定的配置文件. 这样我们可以在命令行覆盖scripts中指定的配置文件:

npm run dev --config=CONFIG_NAME

local命令就是这样做的.

这样, 当我们执行npm run dev时使用的是dev.js, 执行npm run local使用local.js, 执行npm run build使用default.js.

config.devServer.proxy用来配置后端api的反向代理, ajax /api/auth/*的请求会被转发到http://api.example.dev/auth/*, /api/pay/*的请求会被转发到http://api.example.dev/pay/*.

changeOrigin会修改HTTP请求头中的Host为target的域名, 这里会被改为api.example.dev

pathRewrite用来改写URL, 这里我们把/api前缀去掉.

还有一点, 我们不需要把自己个人用的配置文件提交到git, 所以我们在.gitignore 中加入:

config/*
!config/default.js
!config/dev.js

把config目录排除掉, 但是保留生产环境和dev默认配置文件.



webpack-dev-server处理带后缀名的文件的特殊规则

当处理带后缀名的请求时, 比如 http://localhost:8100/bar.do , webpack-dev-server会认为它应该是一个实际存在的文件, 就算找不到该文件, 也不会fallback到index.html, 而是返回404. 但在SPA应用中这不是我们希望的. 幸好webpack-dev-server有一个配置选项disableDotRule: true可以禁用这个规则, 使带后缀的文件当不存在时也能fallback到index.html

historyApiFallback: {
 index: url.parse(config.publicPath).pathname,
 disableDotRule: true
}

代码中插入环境变量

在业务代码中, 有些变量在开发环境和生产环境是不同的, 比如域名, 后台API地址等. 还有开发环境可能需要打印调试信息等.

我们可以使用DefinePlugin插件在打包时往代码中插入需要的环境变量 ,

// ...
const pkgInfo = require('./package.json')

module.exports = (options = {}) => {
 const config = require('./config/' + (process.env.npm_config_config || options.config || 'default')).default

 return {
   // ...
   plugins: [
     new webpack.DefinePlugin({
       DEBUG: Boolean(options.dev),
       VERSION: JSON.stringify(pkgInfo.version),
       CONFIG: JSON.stringify(config.runtimeConfig)
     })
   ]
 }
}

DefinePlugin插件的原理很简单, 如果我们在代码中写:

console.log('Debug');

它会做类似这样的处理:

'console.log(DEBUG)'.replace('DEBUG', true)

最后生成:

console.log(true)

这里有一点需要注意, 像这里的VERSION, 如果我们不对pkgInfo.version做JSON.stringify(),

console.log(VERSION)

然后做替换操作:

'console.log(VERSION)'.replace('VERSION', '1.0.0')

最后生成:

console.log(1.0.0)

这样语法就错误了. 所以, 我们需要JSON.stringify(pkgInfo.version)转一下变成'"1.0.0"', 替换的时候才会带引号.

还有一点, webpack打包压缩的时候, 会把代码进行优化, 比如:

if (DEBUG) {
 console.log('debug mode')
} else {
 console.log('production mode')
}

会被编译成:

if (false) {
 console.log('debug mode')
} else {
 console.log('production mode')
}

然后压缩优化为:

console.log('production mode')

简化import路径

文件a引入文件b时, b的路径是相对于a文件所在目录的. 如果a和b在不同的目录, 藏得又深, 写起来就会很麻烦:

import b from '../../../components/b'

为了方便, 我们可以定义一个路径别名(alias):

resolve: {
 alias: {
   '~': resolve(__dirname, 'src')
 }
}

这样, 我们可以以`src`目录为基础路径来`import`文件:

import b from '~/components/b'

html中的标签没法使用这个别名功能, 但html-loader有一个root参数, 可以使 / 开头的文件相对于root目录解析.

{
 test: /\.html$/,
 use: [
   {
     loader: 'html-loader',
     options: {
       root: resolve(__dirname, 'src'),
       attrs: ['img:src', 'link:href']
     }
   }
 ]
}

那么, 就能顺利指向到src目录下的favicon.png文件, 不需要关心当前文件和目标文件的相对路径.

PS: 在调试标签的时候遇到一个坑, html-loader会解析注释中的内容, 之前在注释中写的

之前因为没有加root参数, 所以`/`开头的文件名不会被解析, 加了root导致编译时报错, 找不到该文件. 大家记住这一点.

优化babel编译后的代码性能

babel编译后的代码一般会造成性能损失, babel提供了一个loose选项, 使编译后的代码不需要完全遵循ES6规定, 简化编译后的代码, 提高代码执行效率:

package.json:

{
 "babel": {
   "presets": [
     [
       "env",
       {
         "loose": true
       }
     ],
     "stage-2"
   ]
 }
}

但这么做会有兼容性的风险, 可能会导致ES6源码理应的执行结果和编译后的ES5代码的实际结果并不一致. 如果代码没有遇到实际的效率瓶颈, 官方不建议使用loose模式.

使用webpack 2自带的ES6模块处理功能

我们目前的配置, babel会把ES6模块定义转为CommonJS定义, 但webpack自己可以处理import和export, 而且webpack处理import时会做代码优化, 把没用到的部分代码删除掉. 因此我们通过babel提供的modules: false选项把ES6模块转为CommonJS模块的功能给关闭掉.

package.json:

{
 "babel": {
   "presets": [
     [
       "env",
       {
         "loose": true,
         "modules": false
       }
     ],
     "stage-2"
   ]
 }
}

使用autoprefixer自动创建css的vendor prefixes

css有一个很麻烦的问题就是比较新的css属性在各个浏览器里是要加前缀的, 我们可以使用autoprefixer工具自动创建这些浏览器规则, 那么我们的css中只需要写:

:fullscreen a {
   display: flex
}

autoprefixer会编译成:

:-webkit-full-screen a {
   display: -webkit-box;
   display: flex
}
:-moz-full-screen a {
   display: flex
}
:-ms-fullscreen a {
   display: -ms-flexbox;
   display: flex
}
:fullscreen a {
   display: -webkit-box;
   display: -ms-flexbox;
   display: flex
}

首先, 我们用npm 安装它:

npm install postcss-loader autoprefixer --save-dev

autoprefixer是postcss的一个插件, 所以我们也要安装postcss的webpack loader.

修改一下webpack的css rule:

{
 test: /\.css$/,
 use: ['style-loader', 'css-loader', 'postcss-loader']
}

然后创建文件postcss.config.js:

module.exports = {
 plugins: [
   require('autoprefixer')()
 ]
}

编译前清空dist目录

不清空的话上次编译生成的文件会遗留在dist目录中, 我们最好先把目录清空一下. macOS/Linux下可以用rm -rf dist搞定, 考虑到跨平台的需求, 我们可以用rimraf:

npm install rimraf --save-dev

package.json修改一下:

{
 "scripts": {
   "build": "rimraf dist && webpack -p --env.config production"
 },
}

传统的多页面网站(MPA)能否用webpack打包?

对于多页面网站, 我们最多的是用Grunt或Gulp来打包, 因为这种简单的页面对模块化编程的需求不高. 但如果你喜欢上使用import来引入库, 那么我们仍然可以使用webpack来打包.

MPA意味着并没不是一个单一的html入口和js入口, 而是每个页面对应一个html和多个js. 那么我们可以把项目结构设计为:

├── dist
├── package.json
├── node_modules
├── src
│   ├── components
│   ├── libs
|   ├── favicon.png
|   ├── vendor.js             所有页面公用的第三方库
│   └── pages                 页面放这里
|       ├── foo               编译后生成 http://localhost:8100/foo.html
|       |    ├── index.html
|       |    ├── index.js
|       |    ├── style.css
|       |    └── pic.png
|       └── bar               http://localhost:8100/bar.html
|           ├── index.html
|           ├── index.js
|           ├── style.css
|           └── baz           http://localhost:8100/bar/baz.html
|               ├── index.html
|               ├── index.js
|               └── style.css
└── webpack.config.js

这里每个页面的index.html是个完整的从开头到结束的页面, 这些文件都要用html-webpack-plugin处理. index.js是每个页面的业务逻辑, 全部作为入口js配置到entry中. 页面公用的第三方库仍然打包进vendor.js. 这里我们需要用glob库来把这些文件都筛选出来批量操作 .

npm install glob --save-dev

webpack.config.js修改的地方:

// ...
const glob = require('glob')

module.exports = (options = {}) => {
 // ...

 const entries = glob.sync('./src/**/index.js')
 const entryJsList = {}
 const entryHtmlList = []
 for (const path of entries) {
   const chunkName = path.slice('./src/pages/'.length, -'/index.js'.length)
   entryJsList[chunkName] = path
   entryHtmlList.push(new HtmlWebpackPlugin({
     template: path.replace('index.js', 'index.html'),
     filename: chunkName + '.html',
     chunks: ['manifest', 'vendor', chunkName]
   }))
 }

 return {
   entry: Object.assign({
     vendor: './src/vendor'
   }, entryJsList),

   // ...

   plugins: [
     ...entryHtmlList,
     // ...
   ]
 }
}

代码在examples/mpa目录.

其他问题

为什么不使用webpack.config.babel.js

部分同学可能知道webpack可以读取webpack.config.babel.js, 它会先调用babel将文件编译后再执行. 但这里有两个坑:

1. 由于我们的package.json中的babel配置指定了modules: false, 所以babel并不会转码import, 这导致编译后的webpack配置文件仍然无法在node.js中执行, 解决方案是package.json不指定modules: false, 而在babel-loader中的options中配置babel. 这样webpack.config.babel.js会使用package.json的babel配置编译, 而webpack编译的js会使用babel-loader指定的配置编译.

{
 test: /\.js$/,
 exclude: /node_modules/,
 use: [
   {
     loader: 'babel-loader',
     options: {
       presets: [
         ['env', {
           loose: true,
           modules: false
         }],
         'stage-2'
       ]
     }
   },

   'eslint-loader'
 ]
}

2. postcss的配置不支持先用babel转码, 这导致了我们的配置文件格式的不统一.

综上, 还是只在src目录中的文件使用ES6模块规范会比较方便一点.

总结

通过这篇文章, 我想大家应该学会了webpack的正确打开姿势. 虽然我没有提及如何用webpack来编译React和vue.js, 但大家可以想到, 无非是安装一些loader和plugin来处理jsx和vue格式的文件, 那时难度就不在于webpack了, 而是代码架构组织的问题了. 具体的大家自己去摸索一下. 以后有时间我会把脚手架整理一下放到github上, 供大家参考 .

关于本文

作者:@华尔街见闻技术团队

原文:https://zhuanlan.zhihu.com/p/27046322