引言

最近给站里添加了表情包FacePack和图片模态框simple-img-modal两个插件,稍微丰富了下博客体验。然而,两个功能很简单的插件却都有160kb多大。经排查发现,ReactDOM这个东西,就占了100多k,而这两个插件都要用到React的组件(模态框其实可以不用但是我懒),要是能共用一个ReactDOM的话能显著减少客户端的下载用量,也为以后再添加基于React的组件带来便利。

使用Parcel.js?

Parcel是编写React组件最方便的打包工具了,和Webpack比起来,即开即用、快速编译、自动安装依赖的特性对新手开发者格外友好。Parcel也有“开箱即用”的代码分割功能,只要你使用动态的import('lib')来代替静态的import * as name from 'lib',剩下的事情就会由Parcel自己搞定。

先是试了一下能不能直接import()托管在公共CDN上的React,然而这样做就不能在VSCode里使用代码提示了,并且会遇到打包报错,或者无法require的问题。虽然可以引用UMD全局导入的React,但是因为没有代码提示,总感觉这不是我要的解决方案。

后面仔细地看了下Parcel的官方文档,感觉之前应该是对import()有些误解。我以为是只能引用外部Javascript文件,但是直接引用库也是可以的,或者说本应该这样。

于是就直接使用import('react-dom').then((ReactDOM)=>{})的方式来使用ReactDOM了。一开始编译的时候可以顺利分拆,然而在完善组件的时候不知道什么时候就突然拆不了了,嗯?

拜托了,Webpack!

之前的经验告诉我,Parcel搞不定的事,就拜托Webpack。虽然还没有真的用过Webpack,但是直觉上讲,Webpack会比高集成的Parcel更有自定义的空间。

也就是,虽然被Webpack的配置文件喂了很多X,但是,现在到了不得不吃X的地步了。

踩坑

首先要从Parceljs完全迁移到Webpack,因此要先实现Parceljs有的功能(

多入口

Parcel要实现多入口特别地直接暴力,直接在parcel build 后面跟入口文件名就行了。Webpack也相对简单,entry部分修改一下就行。键名是输出时给bundle起的名字,值是入口文件的地址,能改名这点比Parcel厉害一些。

详细可以参见这篇文档

代码压缩

不要用Uglifyjs

不知道是哪个地方给我推荐用Uglifyjs压缩,导致输出代码的时候拿到了一串红字。

[danger]ERROR in display.js from UglifyJs
Unexpected token: name «Template», expected: punc «;» [./node_modules/ts-loader??ref--4-0!./node_modules/face-pack/src/util/template.ts:4,0][display.js:97,6]
ERROR in imgmodal.js from UglifyJs
Unexpected token: keyword «const» [./node_modules/ts-loader??ref--4-0!./src/imgmodal.ts:3,0][imgmodal.js:123,0]
ERROR in selector.js from UglifyJs
Unexpected token: name «Template», expected: punc «;» [./node_modules/ts-loader??ref--4-0!./node_modules/face-pack/src/util/template.ts:4,0][selector.js:107,6][/danger]

搜索发现堆栈溢出说Uglify不支持ES6,我还以为这是当年那个版本的问题。结果我去Github的repo一看:

[card]

UglifyJS 3

UglifyJS is a JavaScript parser, minifier, compressor and beautifier toolkit.
Note:

  • uglify-js@3 has a simplified API and CLI that is not backwards compatible with uglify-js@2.
  • Documentation for UglifyJS 2.x releases can be found here.
  • uglify-js only supports JavaScript (ECMAScript 5).
  • To minify ECMAScript 2015 or above, transpile using tools like Babel.

[/card]

打扰了。

万能的巴别塔倒下了

(不是指15cm巴别塔

Babel可以说是目前最流行的Javascript转换器了,巴别塔这个名字是真的取得好。然而,把minifier换成babel-minify-webpack-plugin后发现巴别塔的错误更离谱↓

[danger]ERROR in unknown: Unexpected token (277:80)

ERROR in unknown: Unexpected token (2333:54)[/danger]

不仅出错了,甚至不告诉你错在哪里,这也配叫巴别塔?

等等,万一是这个minify plugin的问题呢?为了分析问题,先讲一下这时整个脚本的编译流程:

[code lang="javascript"]const MinifyPlugin = require("babel-minify-webpack-plugin")
module.exports = {
module: {
rules: [
//Typescript
{
test: /\.tsx?$/,
use: [
{
loader: "ts-loader",
options: {
allowTsInNodeModules: true
}
}
]
},
//...
]
}, optimization: {
minimizer: new MinifyPlugin({ keepFnName: false, keepClassName: false },
{ test: /\.js$/, exclude: /node_modules/, })
}
}[/code]

可以看出,tsx首先会通过微软自家的ts-loader转换成js,然后再交给minimizer做压缩。其实就是有三步,tsx转换jsx语法变成ts、ts删除类型注记变成js、处理ES模块解析、压缩。从之前Uglify的报错中能看出tsc已经完成它的任务了,所以既然babel放在minimizer这边一直出错,那就试试直接全程babel吧。

于是把ts-loader更换成babel-loader:

[code lang="javascript"]{
test: /\.tsx?$/,
use: [
{
loader: "babel-loader",
options:{
presets:[['@babel/preset-typescript',
{isTSX:true,allExtensions:true}]]
}
}
]
}, [/code]

然后也报错了:

[danger]ERROR in ./node_modules/face-pack/src/FaceSelector/FaceSelectorDeployer.tsx 19:13
Module parse failed: Unexpected token (19:13)
File was processed with these loaders:
* ./node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
| }
|
> _displayed = true;
| /**
| *渲染FaceSelector
@ ./src/fpselector.ts 3:0-83 10:6-26

ERROR in ./node_modules/face-pack/src/FaceDisplay/FaceDisplay.ts 3:11
Module parse failed: Unexpected token (3:11)
File was processed with these loaders:
* ./node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
| import { processTemplate } from "../util/template";
| export default class FaceDisplay {
> _faceMap = new Map();
|
| constructor(facePackages, imgClassName = '', imgInlineStyle = '', leftBracket = ':', rightBracket = ':') {
@ ./src/fpdisplay.ts 3:0-64 5:22-33

ERROR in ./node_modules/simple-img-modal/src/deploy.tsx 44:20
Module parse failed: Unexpected token (44:20)
File was processed with these loaders:
* ./node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
| export function updateModal(opacity, imgSrc) {
| Promise.all([import('react'), import('react-dom')]).then(([React, ReactDOM]) => {
> ReactDOM.render(, container);
| });
| }
@ ./src/imgmodal.ts 1:0-73 2:0-15 3:0-9[/danger]

这里主要是两种错误,一个是不接受下划线变量名,一个是没有处理包含在Promise内的jsx。要知道,Parcel同样是用babel作为核心的。同样的核心,Parcel可是一遍过的。问题就变得特别神秘。

没办法,再换一个minimizer罢。

Terser能解决问题吗?

能,现在立刻npm i --save-dev terser-webpack-plugin,请。(当然loader换回了ts-loader

压缩效率上和Parcel有一定差距,虽然Parcel用的也是terser,但是两边的require不大一样,感觉会稍微大个0.几kb。至于两边的require有什么区别我就没有研究了,毕竟以Webpack的可拓展性应该可以随便换吧。

代码解释

如果你像我一样使用Typescript写React插件的话就会遇到如何把tsx转换成js的问题。

首先建议开发环境就直接用Parceljs了,用的babel,编译快,热替换服务器什么的都不用配置。只有像我一样打包的时候想搞点花样才会用到Webpack。

Webpack+Typescript+React的环境实测就ts-loader(tsc)比较好用了(指能用),具体可以看上面代码压缩的巴别塔那节。同时,非常推荐阅读Typescript官方的指南

代码分拆

代码分拆是这次搞这么久首要要解决的问题了。就我翻阅过的文档看,Webpack分拆第三方库有两个思路,一个是直接外链依赖,一个是使用SplitChunk。

外链依赖

最简单粗暴的方式。仅需要在module.exports这一层加一个externals:

[code lang="javascript"]module.exports = {
//...
,externals: {
"react": "React",
"react-dom": "ReactDOM"
},
//...
}[/code]

对于React来说,webpack.config.js里做完这样的设置后,index.html直接引入UMD版的生产版本React就能正常工作了。因此我就研究到这里了,更进阶的使用方法可以阅读官方文档

这次更改就用的是这种方式,应该能为文章页减少100多k的脚本(和一次额外建立的链接),希望能带来一定PageSpeed的分数提升(

SplitChunk

如果你看过Webpack代码分割(或者说代码拆分,无所谓了)的教程的话,可能能看到一个叫webpack.optimize.CommonsChunkPlugin的东西。这是很早前的Webpack的插件了,所以说网上Webpack教程不标版本还是有点麻烦。还好你要是用了这个插件会收到警告要你换到SplitChunk。

因为外链依赖已经解决了本次遇到的问题了,所以就没有具体研究SplitChunk。等有需要的时候再说罢!

结语

原本都准备好被Webpack喂X了的,想不到external这东西这么好用,反而更多时间是用在处理代码压缩那块。

这就是Webpack本身的自由度带来的弊端。你要一个个地去查插件的文档,知道应该怎么用它们,然后最后可能还是用不了被迫换其他的插件。一个配置好的Webpack流程甚至值得代代相传。这可能就是为什么大公司比较喜欢Webpack吧,毕竟配置基本都有前人写好,功能也多。对于小开发者而言Parceljs就特别方便了,最主要是它不会消耗你宝贵的学习时间,打消你入坑的欲望。对于什么东西都是这样哦,入门的工具一定要简单又能满足最开始的学习需求。等到你发现入门的工具已经满足不了你以后,再去入手高端的工具也不迟。:parrots.node: