继续下篇
配置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: {
attrs: ['img:src', 'link:href']
}
}
]
},
{
test: /favicon.png$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
}
}
]
},
{
test: /.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(?.+)?$/,
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