解决 npm install 时的 permission denied 问题

问题场景

当我们在某个搭建好的项目中执行 npm install 时,有时会出现以下类似报错:

1
2
3
4
5
6
7
npm ERR! code 1
npm ERR! Command failed: /usr/local/bin/git clone --depth=1 -q -b fix/https-via-http-proxy git://github.com/contentful/axios.git /Users/xxx/.npm/_cacache/tmp/git-clone-e4d5168e
npm ERR! /Users/xxx/.npm/_cacache/tmp/git-clone-e4d5168e/.git: Permission denied
npm ERR!

npm ERR! A complete log of this run can be found in:
npm ERR! /Users/xxx/.npm/_logs/2017-11-30T21_24_50_080Z-debug.log

原因分析

npm 在执行 install 命令时,会尝试搜索本地缓存(位于 ~/.npm/cacache ),如果缓存命中,请求返回304,然后从本地缓存仓库解压缓存依赖包。

如果之前使用过 sudo 命令 install 过该依赖,则缓存的依赖压缩包的权限为 root ,当前用户权限不足,从而返回以上报错。

解决方案

提供当前用户权限

该方案属于修改用户权限的hack方案,如果对权限较敏感,请谨慎操作

执行以下命令,递归指定文件夹及内部文件权限到当前用户:

1
2
sudo chown -R $USER:$GROUP ~/.npm
sudo chown -R $USER:$GROUP ~/.config

尝试清除本地缓存

1
sudo npm cache clean -f

Nuxt.js 开发及部署

Nuxt.js 是什么

Nuxt.js 是一个基于 Vue.js 的通用应用框架,预设了利用 Vue.js 开发服务端渲染的应用所需要的各种配置,提供异步数据加载、中间件、布局支持等。

与传统的 csr (client-side-render) 相比,Nuxt.js封装的 ssr (server-side-render) 解决方案可以更好的支持 seo ,且白屏时间可减少大约 50% ,极大的提升用户的访问体验。

项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
·
├── README.md
├── nuxt.config.js # nuxt配置,文档地址:https://zh.nuxtjs.org/guide/configuration
├── package-lock.json
├── package.json
├── server # express服务
└── src # 开发目录
├── app.html # html模版
├── assets # 公共资源
├── components # 页面开发子组件
├── layouts # 组件布局(https://zh.nuxtjs.org/guide/views#%E5%B8%83%E5%B1%80)
│   ├── default.vue # 默认模版
│   └── error.vue # 错误页面模版
├── middleware # 中间件(https://zh.nuxtjs.org/guide/routing#%E4%B8%AD%E9%97%B4%E4%BB%B6)
├── pages # 页面开发主文件夹
├── plugins # 自定义插件(https://zh.nuxtjs.org/guide/plugins)
├── static # 静态资源,根路由'/'指向该文件夹
└── store # vuex

为了符合前端开发习惯,我们把通过 Nuxt.js cli 构建的目录结构做了一些微小的调整:前端相关开发内容放入了 src 文件夹,同时把 nuxt config 的开发默认路径改为 src 下:

1
2
3
4
5
// next.config.js

module.exports = {
srcDir: 'src/'
};

本地开发

注意,在 Vue 组件的生命周期中,beforeCreate 和 created 会在 客户端和服务端调用,其他生命周期仅在客户端调用。

server

默认 host 为 localhost , 端口号为 3000 ,可在 nuxt.config.js 中覆盖默认配置:

1
2
3
4
5
6
module.exports = {
server: {
port: 8080,
host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : 'local.xxx.com'
}
};

如果需要扩展/添加 express 中间件,可以修改 server/index.js :

1
2
3
4
const myMiddleWare = require('myMiddleWare');

const app = express();
app.use(myMiddleWare);

layouts

src/layouts 文件夹存放默认及自定义布局模版,default.vue 为默认通用模版,error.vue 为错误页面模版(接收上下文 error 对象)。

我们也可以自定义布局模版,通过 <nuxt/> 标签获取开发组件内容。比如我们新增 layouts/example.vue:

1
2
3
4
5
6
7
<template>
<div>
<div>example header</div>
<nuxt/>
<div>example footer</div>
</div>
</template>

在开发组件中使用自定义布局模版:

1
2
3
4
5
6
7
8
9
<template>
<!-- 开发组件内容 -->
</template>

<script>
export default {
layout: 'example' // 指定渲染的布局模版
}
</script>

error

我们可以通过编辑 layouts/error.vue 文件来定制自己的错误页面。

虽然此文件放在 layouts 文件夹中, 但应该将它看作是一个 页面(page)。

error.vue 接收上下文的 error 对象,改对象参数应该包含两个字段,分别为 statusCode (错误码)及 message (错误信息):

1
2
3
4
5
6
7
<template>
<div class="container">
<h1 v-if="error.statusCode === 404">页面不存在</h1>
<h1 v-else>应用发生错误异常:{{error.message}}</h1>
<nuxt-link to="/">首 页</nuxt-link>
</div>
</template>

router

项目支持常用的 vue router 路由跳转,使用方式也很简单,只需要使用 <nuxt-link to=""></nuxt-link> 标签代理 a 标签,to 属性内应放置项目结构内已有的相对路由。

同样的,我们也可以在不希望以 vue router 方式跳转的地方使用 a 标签。

plugin

插件放置在 src/plugin/ 目录,会在 Vue 应用程序之前执行,接收 context 上下文作为第一个参数。

插件的执行环境分三种:仅在客户端执行、仅在服务端执行、在客户端和服务端都执行。具体的置顶执行环境方式如下配置:

1、通过命名格式指定执行环境:

  • x.client.js 仅在客户端执行
  • x.server.js 仅在服务端执行
  • x.js 在客户端和服务端双端执行
1
2
3
4
5
6
7
// nuxt.config.js

module.exports = {
plugin: [
'~/plugins/plugin.client.js'
]
};

2、通过 ssr 字段置顶执行环境:

1
2
3
4
5
6
7
8
9
10
11
// nuxt.config.js

module.exports = {
plugin: [
{
src: '~/plugins/plugin.js',
// ssr: true 注意,ssr 字段即将弃用,改为 mode 字段
mode: 'server'
}
]
};

插件的具体使用方法:传送门

middleware

中间件放置在 src/middleware/ 目录,接收 context 上下文作为第一个参数。中间件会在路由改变时调用:

  • 在 nuxt.config.js 中配置,会在每个路由改变时调用
  • 也可不做全局配置,改为 layouts 或者自定义开发组件内调用

中间件的具体使用方法:传送门

store

Vuex 状态树在 Nuxt.js 内核做了实现,支持两种 store 方式:

  • 模块方式:src/store/ 目录下的每个 .js 文件会被转换为状态树 指定命名的子模块
  • Classic方式:传统 Vuex 语法使用,不建议,根据构建 warning 提示,会在3.0版本移除(目前为2.9.x)

参考地址:传送门

asyncData 方法

asyncData 方法会在组件(限于页面组件)每次加载之前被调用。它可以在 服务端 或 路由更新 之前被调用,接收 context 上下文作为第一个参数。Nuxt.js 会将 asyncData 返回的数据融合组件 data 方法返回的数据一并返回给当前组件。

该方法在 Vue 实例化之前调用,因此不能通过 this 获取 Vue 实例

fetch 方法

fetch 方法会在渲染页面前被调用,作用是填充状态树 (store) 数据,接收 context 上下文作为第一个参数。与 asyncData 方法类似,不同的是它不会设置组件的数据。例:

1
2
3
4
5
export default {
fetch ({ store, params, $axios }) {
store.commit('getUrl', 'url');
}
};

validate 方法

validate 可以在动态路由对应的页面组件中配置一个校验方法用于校验动态路由参数的有效性。接收 context 上下文作为第一个参数。例:

1
2
3
validate({ params, query }) {
return /^\d+$/.test(params.id);
}

context 上下文对象

在上面的介绍中,我们一直提到上下文对象,它包含 Vue 根实例,Vue Router 路由,Vuex 状态树,error 错误方法,redirect 跳转方法等等内容,可以参考这里。

Caching Components 组件缓存

Vue 由于创建组件实例和 Virtual DOM 节点的成本,它无法与纯粹基于字符串的模板(php)的性能相匹配。在 SSR 性能至关重要的情况下,合理地利用缓存策略可以大大缩短响应时间并减少服务器负载。

安装依赖:

1
npm i @nuxtjs/component-cache -D

新增组件缓存配置:

1
2
3
4
5
6
7
8
9
// nuxt.config.js
module.exports = {
modules: [
['@nuxtjs/component-cache', {
max: 10000,
maxAge: 1000 * 60 * 60
}]
]
};
  • 可缓存组件必须定义唯一 name 选项
  • 不应该缓存组件的情况
    • 可能拥有依赖 global 数据的子组件
    • 具有在渲染 context 中产生副作用的子组件

部署

静态应用部署

以下命令生成应用的静态目录和文件:

1
npm run generate

适合范围:spa 中小型应用,可控的动态路由页面。详细信息参考这里

服务端渲染应用部署

以下命令进行项目编译构建,然后启动服务:

1
2
nuxt build
nuxt start

详细信息参考这里

部署方式在官方文档有详细描述,不做过多解释。

使用 pm2 进行服务进程管理

在我们实际开发环境中,直接 nuxt start 跑起来的项目可能并不能满足我们的需求,原因包含且不限于:无法后台运行,无法热重启等等。

使用 pm2 的原因:

  • 可以内建负载均衡(使用 Node cluster 集群模块)
  • 后台运行
  • 0 秒停机重载,我理解大概意思是维护升级的时候不需要停机.
  • 停止/重启不稳定的进程(避免无限循环或内存溢出)

安装

全局安装 pm2 :

1
sudo npm i -g pm2

pm2 管理配置

新增 process.json :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"apps": [{
"name": "nuxt",
"script": "./server/index.js",
"args": "--kill-timeout 10000",
"watch": false,
"ignore_watch": ["node_modules", "build", "logs"],
"out_file": "logs/nuxt_out.log",
"error_file": "logs/nuxt_error.log",
"max_memory_restart": "1G",
"env": {
"NODE_ENV": "production"
},
"log_date_format": "YYYY-MM-DD HH:mm Z",
"instance_var": "INSTANCE_ID",
"exec_mode": "cluster",
"instances": "0",
"autorestart": true
}]
}

大概介绍几个比较重要的配置项:

  • exec_mode:默认为 fork 模式,单服务进程,无法热重启。这里我们修改为 cluster 模式,内建负载均衡
  • instances:进程数,可手动设置,0 或 ‘max’ 会根据服务器核数分配进程数
  • autorestart:设置为 true , 在内存溢出或其他极端情况下重启服务

运行服务

我们可以执行以下命令运行我们的 Nuxt.js 项目:

1
pm2 start process.json

然后执行 pm2 list 可以看到各个服务进程的详细信息。

执行以下命令进行热重启:

1
pm2 reload nuxt # 使用process.json里的name配置进行服务管理

startOrGracefulReload 实现符合 node.js 特性的热重启

如果按照之前 reload 的方法进行热重启,且用户刷新的够快够频繁,还是会在重启过程中看到两三秒的502页面,原因在于 node.js 服务的单线程 异步 非阻塞I/O模型。异步的特性导致集群中的某一个服务并没有完全启动,但是代码已经执行结束,导致 pm2 直接进行下一线程的服务重启工作,因此我们选择使用 startOrGracefulReload 作为热重启手段。

首先对 server/index.js 做一些改造:

1
2
3
4
5
6
7
8
9
const express = require('express');
const app = express();

// ...

let server = app.listen(port, host);
process.on('SIGINT', () => {
server.close();
})

注意,express()本身不是一个server实例,但是express().listen()会返回server实例,因此我们在调用close()方法的时候要注意调用对象是否正确

执行以下命令以完成符合 node.js 特性的热重启:

1
pm2 startOrGracefulReload process.json

时隔几个月的一篇唠叨

最近一直在忙一些不知所谓的事情,发现无论是github还是当初决心要坚持的blog都停止了更新。

不知道从什么时候开始,每天都在像陀螺一样转。做不完的需求,讲不完的代码,举步维艰的新项目推进。负能量就像气泵里充足的气体一样每天都在疯狂打进自己的体内,在爆炸的边缘疯狂试探。

坐在工位就在想离职,躺进被子就希望天永远别亮。

甚至在想:不如回家吧,别敲代码了。

甚至似乎已经忘了自己这些年有多喜欢这个行业。


前些日子离职了一位同事,他说他不敲代码了,要回家跟着家人学做生意。

再前些日子,一位关系很好的搞设计的朋友说,明年准备回家了。我问回家干啥啊,不做设计了吗。他说随便找个小广告公司搞搞平面,也活得下去,北京太没有生活了。

我突然就不知道该怎么接话茬了。也许是羡慕他的决断吧。


今天面试了一个八年经验的前端,补之前离职同事的空缺。从业年限长,有大厂背景,翻来覆去却只有jquery和vue,突然就感觉很难受。我能感受到他非常需要这份工作,也许是因为房子,也许是因为孩子,或者是因为生活中其他的事情,但是这个年纪,匹配这种技术能力,实在是有点一言难尽。

我不知道该跟他再说些什么,但是我知道如果我到了他的年纪,无论是否还从事这个行业,都不希望让人感觉我这些年是在混日子。


打起精神。加油。

webpack优化二:打包分析

背景

webpack4.x新增了更加便利的splitChunks(文档翻译),可以帮助我们对项目的js进行提取分割,以便更好的长效缓存。但是提取规则是否合适,我们希望提取的模块是否按照预期进行,是否有可视化的插件可以明确的展示给我们?

工具

webpack-bundle-analyzer是一个可以把webpack打包输出的bundles以可视化的块状图展示每个modules的输出大小及来源路径。

安装

1
npm install --save-dev webpack-bundle-analyzer

或者

1
yarn add -D webpack-bundle-analyzer

配置

webpack配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
analyzerPort: 8889,
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info'
})
]
}

添加脚本:

1
2
3
scripts: {
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
}

在npm run build之后会展示资源的打包情况。

webpack优化一:构建测速

背景

随着项目体积的逐渐庞大,webpack的构建速度逐渐变得”年老体衰”,开发环境构建的时间都够打把斗地主了,实在是难以忍受。虽然文档上有那么大一个optimization,但是应该从哪儿着手优化却让我有点找不着北。所以想着如果有什么plugin可以打印出构建过程中每一步的耗时就好了。

工具

Speed Measure Plugin (speed-measure-webpack-plugin) 是为webpack量身定做的测速插件,它可以让你知道构建过程的每一步的耗时明细。Speed Measure Plugin需要node 6.0以上,支持所有webpack版本(1 - 4)。

安装

1
npm install --save-dev speed-measure-webpack-plugin

或者

1
yarn add -D speed-measure-webpack-plugin

使用

1
2
3
4
5
6
7
8
9
10
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
});

配置项

配置位置如下:

1
const smp = new SpeedMeasurePlugin(options);

options.disable

Type: Boolean

Default: false

值设置为true时该插件不工作。

options.outputFormat

Type: String|Function

Default: human

打印格式,可选值如下:

  • "json"
  • "human"
  • "humanVerbose"
  • Function

options.outputTarget

Type: String|Function
Default: console.log

options.pluginNames

Type: Object
Default: {}

默认情况下,SMP使用plugin.constructor.name打印plugin速度,但是如果某些plugin不生效或者你想通过指定字段代理plugin的名称,可以配置该项:

1
2
3
4
5
6
7
8
9
10
11
12
const uglify = new UglifyJSPlugin();
const smp = new SpeedMeasurePlugin({
pluginNames: {
customUglifyName: uglify
}
});

const webpackConfig = smp.wrap({
plugins: [
uglify
]
});

sublime启用vim模式

sublime 启用vim模式

什么是vim

Vim是从vi发展出来的一个文本编辑器。其代码补完、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。和Emacs并列成为类Unix系统用户最喜欢的编辑器。
–维基百科


sublime 启用标准vim模式方法

sublime默认关闭vim模式,打开方法如下:

1
2
菜单中打开:Preferences -> Setting - User
找到"ignored_packages"字段,将数组中的"Vintage"项删除

现在已经启用了vim模式,可以按“Esc”退出编辑模式,进入vim模式。

vim模式启用热键更改

如果你觉得esc启用vim模式不顺手(比如我),可以更改启动热键,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
菜单中打开:Preferences -> Key Bindings

不建议更改左侧默认热键,我们可以在右侧user文件内覆盖原配置:
[
{ "keys": ["j", "j"], "command": "exit_insert_mode",
"context":
[
{ "key": "setting.command_mode", "operand": false },
{ "key": "setting.is_widget", "operand": false }
]
}
]

上面的配置会把j+j(两下j)设置为vim的启动热键,这里你可以设置成自己顺手的启动键位。

常用vim功能键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
j: 下移一行
k: 上移一行
h: 左移一格
l: 右移一格
v: 配合hjkl等跳转键进行选中
a: 在当前字符后进入insert模式
A: 在当前行的结尾处进入insert模式
i: 在当前字符前进入insert模式
o: 向下插入空行并进入insert模式
O: 向上插入空行并进入insert模式
d: 删除选中字符(v)
dd: 删除行
3dd: 删除3行
dgg: 删除文件起始至当前行的内容
dG: 删除当前行至文件结束的内容
x: 删除当前字符
y: 复制
P: 当前字符前粘贴
p: 当前字符后粘贴
gu: 切换为小写
gU: 切换为大写
g~: 大小写转换
<: 向前缩进
>: 向后缩进
b: 回退一个单词
B: 回退到上一个空格
5b: 回退5个字符
w: 向前一个单词
W: 向前到下一个空格
5w: 向前5个字符
G: 跳到文件结尾处
gg: 跳到文件起始处
0: 跳到当前行的起始处
$: 跳到当前行的结尾处
{: 跳转到上一个段落
}: 跳转到下一个段落

vim还有很多需要记忆的功能键位,以上只是开发中比较常用的一部分,所以学习(记忆)曲线比较陡峭。如果能熟练使用,对于高(tong)效(shi)开(zhuang)发(bi)有很大的帮助。

以上,感谢阅读。

Webpack Optimization

Optimization (webpack文档翻译)

从webpack4开始,webpack会根据你的mode选项来决定运行哪些优化项,但是所有的优化项仍然可以手动配置及覆盖。

optimization.minimize

boolean

是否使用UglifyjsWebpackPlugin进行压缩。

在production模式下默认为true。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
minimize: false
}
};

更多关于mode

optimization.minimizer

[UglifyjsWebpackPlugin]

这个选项允许你使用其他的压缩插件,或者覆盖UglifyjsWebpackPlugin的默认配置项。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
//...
optimization: {
minimizer: [
new UglifyJsPlugin({ /* your config */ })
]
}
};

optimization.splitChunks

object

从webpack4开始,我们提供了默认的chunk代码分割插件(CommonsChunkPlugin推出历史舞台),更多配置信息参考SplitChunksPlugin(翻译文档已上传github)。

optimization.runtimeChunk

object string boolean

如果想给每个entrypoint添加一个包含runtime代码的额外chunk,我们可以把optimization.runtimeChunk设置为true或者multiple。该值等同于:

webpack.config.js

1
2
3
4
5
6
7
8
module.exports = {
//...
optimization: {
runtimeChunk: {
name: entrypoint => `runtime~${entrypoint.name}`
}
}
};

single 创建一个runtime文件,这个文件会被所有chunks公用。该值等同于:

1
2
3
4
5
6
7
8
module.exports = {
//...
optimization: {
runtimeChunk: {
name: 'runtime'
}
}
};

如果我们把optimization.runtimeChunk设置为object,该对象仅支持name值作为打包生成的runtime代码块的name。

默认为false:runtime打包进各个entry chunk。

引入的模块代码将分别在每个chunk运行时初始化,因此如果在页面上包含多个entry point,请注意此行为。这种情况下,你可能希望设置single项,或者使用其他配置项,使runtime代码分割为同一个公用chunk。

webpack.config.js

1
2
3
4
5
6
7
8
module.exports = {
//...
optimization: {
runtimeChunk: {
name: entrypoint => `runtimechunk~${entrypoint.name}`
}
}
};

optimization.noEmitOnErrors

boolean

optimization.noEmitOnErrors会在编译发生错误时跳过输出。该选项可以确保错误的资源不被打包进chunk,并且在打印信息中不再包含错误信息。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
noEmitOnErrors: true
}
};

如果你使用webpack CLI进行开发,webpack不会再遇到错误代码时结束进程。如果你希望webpack CLI在编译遇到错误时不退出,请检查CLI的bail option。

optimization.namedModules

boolean: false

为了方便调试,使用更易读的模块ids。该配置项可以在development模式下使用(默认),不可以在production模式下使用。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
namedChunks: true
}
};

optimization.moduleIds

bool:false string: natural, named, hashed, size, total-size

该配置项可以指定使用某一种算法来生成模块ids。如果设置optimization.moduleIds为false,则webpack在生成ids时不使用任何一种算法,这一项可以用其他plugin来替代。

默认为false。

支持以下值:

Option Description
natural 数字下标
named 方便调试的高可读性ids
hashed 短hash ids,长期缓存表现更好
size 根据请求到的初始资源size计算的ids
total-size 根据请求到的解析资源size计算的ids

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
moduleIds: 'hashed'
}
};

optimization.nodeEnv

string bool: false

给定一个process.env.NODE_ENV值。optimization.nodeEnv会使用DefinePlugin,除非设置值为false。optimization.nodeEnv的默认值为mode的值,如果没有设置的话,默认为production。

可以设置的值:

  • 任意字符串
  • false: 不修改process.env.NODE_ENV的值

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
nodeEnv: 'production'
}
};

optimization.mangleWasmImports

bool: false

如果设置为true,webpack会通过把引入资源转换为更短的字符串的方式,减少WASM的大小。它会打乱module并输出names。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
mangleWasmImports: true
}
};

optimization.removeAvailableModules

bool: true

该配置项会检测modules是否已经被父级chunk打包过,如果已经被打包过,则会在该chunk中删除该modules。设置为false来阻止该优化。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
removeAvailableModules: false
}
};

optimization.removeEmptyChunks

bool: true

该配置项会检测并删除空的chunks。设置为false来阻止该优化。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
removeEmptyChunks: false
}
};

optimization.mergeDuplicateChunks

bool: true

该配置项会合并包含相同modiles的trunks。设置为false来阻止该优化。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
mergeDuplicateChunks: false
}
};

optimization.flagIncludedChunks

bool

该配置项会使webpack确认当前标记的chunk是另外一个chunk的子集并且已经加载完成时,当前标记的chunk将不会再次加载(包含关系)。默认并且仅允许生产环境使用。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
flagIncludedChunks: true
}
};

optimization.occurrenceOrder

bool

使webpack可以按照顺序排列模块,从而可以得到最小的初始包。生产环境默认为true,其他环境下为false。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
occurrenceOrder: false
}
};

optimization.providedExports

bool

使webpack可以高效的导出来自export * from ...的代码。默认开启。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
providedExports: false
}
};

optimization.usedExports

bool

使webpack确定每个模块导出项(exports)的使用情况。依赖于optimization.providedExports的配置。optimization.usedExports收集到的信息会被其他优化项或产出代码使用到(模块未用到的导出项不会被导出,在语法完全兼容的情况下会把导出名称混淆为单个char)。为了最小化代码体积,未用到的的导出项目(exports)会被删除。生产环境默认开启。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
usedExports: true
}
};

optimization.concatenateModules

bool

使webpack可以把代码片段安全的合并为一个模块。依赖于optimization.providedExports和optimization.usedExports。生产环境默认开启。

webpack.comfig.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
concatenateModules: true
}
};

optimization.sideEffects

bool

使webpack可以识别package.json中的sideEffects项。可以删除多余的导出项(如import {a} from 'module',只会打包'module/lib/a',而不会把整个’module’打包进去)。

package.json

1
2
3
4
5
{
"name": "awesome npm module",
"version": "1.0.0",
"sideEffects": false
}

注意,sideEffects应该在package.json中,但并不意味着你需要在你的项目中把它设置为false(可能你的项目会需要用到某一个大的依赖)。

optimization.sideEffects依赖于optimization.providedExports。这会产生额外的构建时间,但是总体来说还是有更好的表现,因为代码量会大大减少。该优化项的优化效果取决于你的项目的第三方依赖情况。

生产环境默认开启。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
sideEffects: true
}
};

optimization.portableRecords

bool

该优化项会使webpack生成依赖项的相对路径。

默认关闭。如果recordsPath,recordsInputPath,recordsOutputPath中至少一项被配置,则开启。

webpack.config.js

1
2
3
4
5
6
module.exports = {
//...
optimization: {
portableRecords: true
}
};

从零开始搭建多入口webpack4项目

简介

  • 搭建基础项目架构
  • 开发中(dev-server)
  • 模块(module)配置
  • 生产环境的资源打包

搭建基础项目架构

首先,webpack4对于Node.js版本是有要求的:

webpack4要求Node.js的最低版本为6.11.5

如果你的Node.js版本过低并进行了升级,然后在项目运行时出现了茫茫多的关于node-sass的报错,则需要重建node-sass环境。运行指令:

1
npm rebuild node-sass

创建一个多入口项目,按照我们的现有项目,目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
└ your-project
├ src
├ css
├ js
├ project1
├ detail.js
└ index.js
└ project2
└ index.js
└ tpl
├ project1
├ detail.ejs
└ index.ejs
└ project2
└ index.ejs
├ webpack.config.js
├ webpack.production.config.js
└ package.json

安装webpack和webpack-cli(webpack4需要配合webpack-cli使用,社区也有相关的绕过cli的解决方案,但不是很推荐):

1
cnpm i webpack webpack-cli -D

新建默认配置入口webpack.config.js,虽然能看到webpack4在往0配置的方向发展,但是在大多数项目中的配置三板斧还是不能少的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let config = {
entry: {
// webpack4默认src/index.js为入口文件
// 本次针对多入口项目做配置,第二节会去做入口文件的获取
},
output: {
// 文件输出默认为dist/bundle.js
// 第二节会去重新覆盖默认的输出项
},
module: {
rules: [
// module.rules在大多数情况仍然需要配置
// 我们需要合适的loader去解析对应的文件格式(*.css, *.ejs, ...)
]
},
mode: 'development'
};

module.exports = config;

mode为webpack4新增配置,默认值为production,根据字面意思可以猜到分别对应开发环境和生产环境,设置的模式会有对应的内部配置。该参数可以在cli中追加,也可以在config中配置(如上)。该参数在webpck4中为必需参数。

具体可以参考模式(mode)。

开发中(dev-server)

webpack4的开发环境搭建有两个推荐:webpack-dev-server(node + express)和webpack-serve(node + koa2)。

如果要沿用我们比较熟悉的webpack-dev-server(传送门)进行开发环境的搭建,需要安装对应的beta版本:

1
cnpm i webpack-dev-server@next -D

而webpack-serve(传送门)作为升级版本,功能更加强大(也许也会有更多的坑,待踩),使用WebSockets做HMR(Hot Module Replacement 模块热替换)。这次我们就来体验一下webpack-serve,安装依赖:

1
cnpm i webpack-serve -D

新建serve.config.js,安装依赖项:

1
cnpm i glob yargs koa-router -D

警告!如果需要用cli命令启用服务的话,必需使用CommonJS规范语法。以下实例因为未使用CommonJS规范的原因只能使用node语法启动,因为我懒得改了。

配置我们需要的配置项,然后为了便利考虑在根路由搭建目录列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// serve.config.js

const path = require('path');
// glob用更方便的规则匹配需要的文件
// 参考地址 https://github.com/isaacs/node-glob
const glob = require('glob');
const serve = require('webpack-serve');
const Router = require('koa-router'); // 引入koa-router,配置根路由的展示内容
const WebpackConfig = require('./webpack.config.js');

// 更多配置项参考 https://github.com/webpack-contrib/webpack-serve
const config = {
open: true // 构建完成后是否自动在浏览器打开
};

// 新建路由,输出目录结构
let directory = new Router();
directory.get('/', async ctx => {
// 拼接根路由的html DOM结构
let html = `<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><ul>`;
// 获取webpack入口文件,遍历生成目录结构
let entries = WebpackConfig.entry;

for (let key in entries) {
if (key !== 'commons') {
html += `<li><a href="/pages/${key}.html">${key}</a></li>`;
}
}

html += `</ul>`;

// 输出html结构
ctx.body = html;
});

// 装载子路由
let router = new Router();
router.use('/', directory.routes(), directory.allowedMethods());

serve(config, {
WebpackConfig,
add: (app, middleware) => {
middleware.webpack();
middleware.content();

// 加载路由中间件,路由**必需**作为最后一个中间件添加
app.use(router.routes());
}
})
.then((result) => {
// to do ...
});

在目录页的搭建时用到了webpack配置中的entry项(入口文件),接下来我们需要在webpack.config.js中获取我们项目的所有入口文件,并指定输出规则(output):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// webpack.config.js

// path参考 http://nodejs.cn/api/path.html
const path = require('path');
const glob = require('glob');

const SRC_PATH = path.resolve(__dirname, './src');

...

// 多入口项目获取入口对象
let entries = (entryPath => {
let files = {},
filesPath;

// 传入的entryPath为src/js文件夹路径
// 如果有不需要作为入口js获取的文件/文件夹,在options项添加ignore配置来进行筛除
filesPath = glob.sync(entryPath + '/**/*.js', {
// options ...
});

filesPath.forEach((entry, index) => {
// 获取并简化入口文件对象的键名
let chunkName = path.relative(entryPath, entry).replace(/\.js$/i, '');

files[chunkName] = entry;
});

// 返回多入口文件对象
return files;
})(path.join(SRC_PATH, 'js'));

let config = {
entry: entries,
output: {
path: path.resolve(__dirname, './dist'),
filename: 'js/[name].[hash:7].js'
},
...
};

...

在package.json中写入启动脚本(再次友情提示!建议使用CommonJS规范书写serve.config.js,以便使用推荐的cli命令!):

1
2
3
4
5
{
"scripts": {
"dev": "node ./serve.config.js"
}
}

模块(module)配置

webpack4依然不支持把css和html作为模块,相关格式的loader配置仍然是必需项。
根据社区的开发计划,webpack5也许会解决这个问题(把css和html视作模块读取)。

处理scss/css文件

在webpack4之前我们都是用extract-text-webpack-plugin(传送门)来抽离已经被sass-loader、css-loader解析完成的css为独立的外部css文件,它的beta版本也对webpack4做了支持,但是仅支持4.2.0以下版本,因此我们应尽量选择版本向上兼容度更好的其他依赖。

解决方案:使用官方新推荐的mini-css-extract-plugin(传送门)作替代。

注意:loader的解析顺序为从后往前,顺序混乱可能会引起一些报错。

安装依赖项目:

1
2
cnpm i mini-css-extract-plugin css-loader postcss-loader node-sass sass-loader -D
cnpm i autoprefixer -D

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

...
let config = {
...
module: {
rules: [
{
// 如果不希望把已经解析完成的css文件单独抽离引入,而是希望以style标签对的形式插入页面
// 可以把MiniCssExtractPlugin.loader替换为style-loader
test: /\.(c|sa|sc)ss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')({
'browsers': ['> 1%', 'last 10 versions']
})
]
}
},
'sass-loader'
]
},
...
]
},
...
plugins: [
// 指定抽离出的css文件的输出规则
// 如果使用style-loader,不需要以下配置
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:7].css'
})
]
};
...

module模块不再支持loader配置项,需要使用use项做升级

ejs模版语言处理

正常情况下,使用我们之前的ejs-compiled-loader仍然可以正常解析ejs模版,但是在一些未知的条件下会出现this指针相关的报错(如pc_v1项目)。目前没有找到合适的替代依赖。

解决方案:根据热心网友提供的PR,可以暂时使用ejs-zdm-loader做应急策略。

安装依赖项:

1
cnpm i ejs-zdm-loader -D

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js

...
let config = {
...
module: {
...
{
test: /\.ejs$/i,
use: 'ejs-zdm-loader'
},
},
...
};
...

js公共模块抽离

在webpack4之前,对于公共依赖的抽离使用的是CommonsChunkPlugin,webpack4对于该插件已经做了废弃处理,相对的,提供了更便捷的SplitChunksPlugin作为新的解决方案。相关api在官方文档可以查看。

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// webpack.config.js
...
let config = {
...
optimization: {
splitChunks: {
chunks: 'all',
minChunks: 1,
name: true,
cacheGroups: {
commons: {
name: 'commons',
minChunks: 2,
// minSize设置为0只为demo体现抽离效果,建议保持默认的30000
// 该设置过小会可能导致网络请求无必要的增加一次
minSize: 0
}
}
}
},
...
};
...

其他plugins

到目前为止我们已经做了入口文件的获取,各种文件格式的解析抽离,并指定了输出规则,但是并没有实际的输出html文件。

仍然使用html-webpack-plugin,升级到支持webpack4的最新版本。

安装依赖:

1
cnpm i html-webpack-plugin -D

注意:html-webpack-plugin每次只能生成一个.html文件,所以在多入口项目中需要对入口文件进行遍历。

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// webpack.config.js
const webpack = require('webpack');
...
const HtmlWebpackPlugin = require('html-webpack-plugin');
...
let config = {
...
plugins: [
...
// 把$变量暴露到全局
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery'
})
]
};

let pages = Object.keys(entries);
pages.forEach(item => {
config.plugins.push(new HtmlWebpackPlugin({
showErrors: false,
filename: path.join(__dirname, `/dist/pages/${item}.html`),
template: path.join(__dirname, `/src/tpl/${item}.ejs`),
chunks: ['commons', item]
}));
});
...

到目前为止,我们已经完成了开发环境的所有配置,可以使用npm run dev来看看项目是否已经正常启动。完整配置可以参考这里。

生产环境的资源打包

万里长征最后一步,配置生产环境,压缩静态资源,用chunk hash代替编译hash。

chunk hash可以根据文件内容生成hash,在文件内容无更改时不会生成新hash。而编译hash会在每次项目编译的时候重新生成,所以不建议使用在生产环境。

为了方便书写,免去变量区分,我们新建一个webpack.production.config.js来配置生产环境。在webpack4下,我们不再需要UglifyJsPlugin来压缩js代码,只需要配置模式(mode)为production即可。

配置如下:

1
2
3
4
5
6
7
8
// webpack.production.config.js
...
let config = {
...
mode: 'production',
...
};
...

由于静态资源的编译顺序为js依赖编译 > 编译css > 抽离css资源为单独文件 > 编译并压缩js,所以以上配置并不会压缩css文件,所以我们需要单独处理一下生产环境的css资源。

webpack4下我们尝试使用optimize-css-assets-webpack-plugin来压缩css文件。

首先安装依赖:

1
cnpm i optimize-css-assets-webpack-plugin -D

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// webpack.production.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
...
let config = {
...
optimization: {
minimizer: [
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
// postcss-loader解析css过程中已经添加了autoprefixer兼容
// 此处需要设置为false
autoprefixer: false
}
})
]
},
...
};
...

最后,我们需要用chunk hash代替编译hash。

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.production.config.js
...
let config = {
...
output: {
path: path.resolve(__dirname, './dist'),
filename: 'js/[name].[chunkhash:7].js',
publicPath: './'
},
...
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:7].css'
}),
...
]
};
...

添加webpack打包命令:

1
2
3
4
5
6
{
"scripts": {
"dev": "node ./serve.config.js",
"build": "rm -rf dist && npx webpack --config webpack.production.config.js"
}
}

完整的生产环境配置可以参考这里。