Skip to main content

2 posts tagged with "webpack5"

View All Tags

· 18 min read
石晓波

项目基础构建

本项目使用的是 MVC 基础机构模式

项目根目录下执行如下命令创建基础的 package.json:

npm init -y

根目录下创建 src 文件,src 下分别创建 client 和 server 文件夹,用来保存客户端代码和服务器代码。使用基础的 MVC 架构模式创建文件夹,完整主文件目录结构如下:

├── src
├──── web
├─────── assets
├─────── components
├─────── views
├──── server
├─────── assets
├─────── controllers
├─────── middlewares
├─────── models
├── node_modules
├── scripts
├── logs
├── dist
├── config
├── tests
├── README.md #帮助项目开发文件
├── app.js #项目启动文件
├── package-lock.json
└── package.json

基础代码实现

服务端代码

创建 app.js 为入口文件,引入 koa web 开发框架,初始化 app。编写配置文件,配置文件需要根据进程中的参数判断开发环境开始生产环境,配置文件代码如下:

/src/server/config/index.js
import { extend } from "lodash";
import { join } from "path";

let config = {
//配置后续资源路径和其他公共配置
viewDir: join(__dirname, "..", "views"),
staticDir: join(__dirname, "..", "assets"),
};

if (process.env.NODE_ENV === "development") {
let localConfig = {
port: 8081,
memoryFlag: false,
};

config = extend(config, localConfig);
}

if (process.env.NODE_ENV === "production") {
let localConfig = {
port: 8082,
memoryFlag: true,
};

config = extend(config, localConfig);
}

export default config;

初始化 koa,并引入 swig 模板引擎。

/src/server/app.js
import Koa from "koa";
import render from "koa-swig";
import { wrap } from "co";
import { port, viewDir, memoryFlag } from "./config";

const app = new Koa();

app.context.render = wrap(
render({
root: viewDir,
autoescape: true,
cache: memoryFlag,
ext: "html",
varControls: ["[[", "]]"],
writeBody: false,
})
);

app.listen(port, () => {
console.log("🍺服务器启动成功", port);
});

Controller

根据 MVC 架构模式定义 Controller,首先定义 Controller 基类,包含基础的 log 信息打印等业务信息,然后定义子类 IndexController、BooksController。

/src/server/controllers/Controller
class Controller {
constructor() {}

log() {
console.log("父类Controller");
}
}

BooksController 中整合了 Model 和 View

/src/server/controllers/BooksController.js
import Controller from "./Controller";
import Books from "../models/Books";
class BooksController extends Controller {
constructor() {
super();
}

//服务端渲染 直出
async actionIndex(ctx, next) {
const book = new Book();
const data = await book.getData();
ctx.body = await ctx.render("books/pages/list", data);
}

async actionCreate(ctx, next) {
ctx.body = await ctx.render("books/pages/create");
}
}
/src/server/controllers/IndexController.js
import Controller from "./Controller";
class IndexController extends Controller {
constructor() {
super();
}
async actionIndex(ctx, next) {
ctx.body = "石晓波🏮";
}
}
export default IndexController;

Router

定义了 Controller 和 Action 后需要将这些方法和路由进行整合,所以加入 koa-simple-router 的使用,代码如下:

/src/server/controllers/index.js
import router from "koa-simple-router";
import BooksController from "./BooksController";
import IndexController from "./IndexController";
const bookController = new BooksController();
const indexController = new IndexController();

export default (app) => {
app.use(
router((_) => {
_.get("/", indexController.actionIndex);
_.get("/books/list", bookController.actionIndex);
_.get("/books/create", bookController.actionCreate);
})
);
};

同时在 app.js 中添加对如上函数的引用并传入 app

/src/server/app.js
import controllers from './controllers';
...
controllers(app)
...

Model

model 定义数据,作为中间代理层,Model 层主要是做数据交互,数据获取、数据整合。如上 Controller 中使用了 Books,如下是 Books 的代码:

/src/server/Model/Books.js
import { get } from "axios";
class Books {
getData() {
return get("http://localhost/basic/web/index.php?r=books");
}
}

错误处理

程序运行难免会遇到错误,我们需要将错误信息收集并做持久化处理,所以自定义处理处理中间件并使用 log4j 对错误信息进行持久化处理。

首先在 app 文件中增加对 log4j 的配置以及引入错误处理中间件,需要注意的是错误处理中间件需要在业务代码前才能捕获到业务代码的错误。

/src/server/app.js
import { configure, getLogger } from "log4js";
import errorHandler from "./middlewares/errorHandler.js";
configure({
appenders: { cheese: { type: "file", filename: "logs/yd.log" } },
categories: { default: { appenders: ["cheese"], level: "error" } },
});
const logger = getLogger("cheese");

errorHandler.error(app, logger);

如下是自定义错误处理中间件代码:

/src/server/middleWares/errorHandler.js
class errorHandler{
static error(app,logger){
app.use(async (ctx, next)=>{
try{
await next();
} catch(e){
logger.error(e);
ctx.body = "500请求啦~恢复中.'"
}
})

app.use(async (ctx, next) => {
await next();
if(404 !== ctx.status){
return;
}
ctx.body = '<script type="text/javascript" src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js" charset="utf-8"></script>';
})
}
}

客户端代码

swig模板引擎

首先需要创建模板公共布局文件,后续所有页面都可以基于公共文件进行块内容填充,如下是构建公共布局模板:

/src/web/views/layouts/layout.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{% block title %}{% endblock %}</title>
{% block head %}{% endblock %}
</head>
<body>
<div>
{% block content %}{% endblock %}
</div>
{% block scripts %}{% endblock %}
</body>
</html>

swig使用了块占位符,以此模板为基础的页面可以只写填充标记相关的代码,模板会自行编译出对应的html文件。

创建模块页面

创建books模块继承自基础模块,并引入定义的组件

/src/web/views/books/pages/list.html
{% extends '@layouts/layout.html' %}

{% block title %} 图书列表页📚 {%endblock %}

{% block head %}
<!--injectcss-->
{% endblock %}

{% block content %}
{% include "@components/banner/banner.html" %}
<h1>展示图书</h1>
{% endblock %}

{% block scripts %}
<!--injectjs-->
{% endblock %}
/src/web/views/books/pages/create.html
{% extends '@layouts/layout.html' %}

{% block title %} 图书列表页📚 {%endblock %}

{% block head %}
<!--injectcss-->
{% endblock %}

{% block content %}
{% include "@components/banner/banner.html" %}
<h1>展示图书</h1>
{% endblock %}

{% block scripts %}
<!--injectjs-->
{% endblock %}

定义公共组件

定义公共组件banner

/src/web/components/banner/banner.html
<div class="baner">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/books/list">展示图书</a></li>
<li><a href="/books/create">添加图书</a></li>
</ul>
</div>
/src/web/components/banner/banner.js
const banner = {
init() {
console.log('banner');
},
};
export default banner;

webpack入口文件

webpack是以js为入口文件的,所以给每个页面都定义一个入口文件,这里设置一种规则模块下的入口文件定义为[模块名]-[页面名].entry.js,webpack打包时可以根据制定的规则解析入口js文件名,将打包后的文件插入到对应的html页面中。

/src/web/views/books/boosk-create.entry.js
import banner from '@/components/banner/banner.js';
banner.init();
/src/web/views/books/boosk-list.entry.js
import banner from '@/components/banner/banner.js';
banner.init();

编译环境构建

编译环境区分生产环境和开发环境webpack通过使用webpack-merge对不同环境的配置文件和基础文件进行合并,gulp通过使用process.env.NODE_ENV来区分开发环境和生产环境。

客户端代码编译

客户端代码使用webpack进行编译,因为是MPA应用,所以需要定义多入口,使用glob使用正则匹配的方式获取多入口的js,获取的js为上面根据命名规则定义的入口文件,循环遍历这些入口文件解析出模块和文件名称,将获取的html文件为webpack打包插入的目标文件也就是template属性,并将以template为模板打包后的文件输出到dist下同等的目录下,这里需要注意两点,第一如果不设置html-webpack-plugin的chunk属性那么会将所有的js文件都插入到模板中,所以需要设置chunk属性['runtime', entryKey],entryKey为[模块名]-[文件名]的组合,为什么要使用这个名称的文件呢,因为多入口打包是以该名称为key,目标js文件为目标定义的。完成入口和循环生成html-webpack-plugin的实例配置后将插件的实例写入webpack的plugins中。如上插件中使用了chunks的配置那么我们就需要将webpack打包中的runtime单独提出来,配置optimization.runtimeChunk即可。

如下是webpack公共部分的配置:

const argv = require('yargs-parser')(process.argv.slice(2));
const _mode = argv.mode || 'development';
const _mergeConfig = require(`./config/webpack.${_mode}.js`);
const { merge } = require('webpack-merge');
const { sync } = require('glob');
const { resolve } = require('path');
const files = sync('./src/web/views/**/*.entry.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlAfterPlugin = require('./config/HtmlAfterPlugin');

let _entry = {};
let _plugins = [];
for (let item of files) {
// console.log(item)
if (/.+\/([a-zA-Z]+-[a-zA-Z]+)(\.entry\.js)/g.test(item) == true) {
const entryKey = RegExp.$1;
_entry[entryKey] = item;
const [dist, template] = entryKey.split('-');
_plugins.push(
new HtmlWebpackPlugin({
filename: `../views/${dist}/pages/${template}.html`,
template: `src/web/views/${dist}/pages/${template}.html`,
chunks: ['runtime', entryKey],
inject: false,
})
);
} else {
console.log('🐖', '项目配置匹配失败');
process.exit(-1);
}
}
const webpackConfig = {
entry: _entry,
optimization: {
runtimeChunk: {
name: 'runtime',
},
},
plugins: [..._plugins, new HtmlAfterPlugin()],
resolve: {
alias: {
'@': resolve('src/web'),
},
},
};
module.exports = merge(webpackConfig, _mergeConfig);

对于入口文件来说,开发环境和生产环境略微有些区别,生产环境建议给js文件名加hash的方式防止文件缓存问题,配置如下

//开发环境
output: {
path: join(__dirname, '../dist/assets'),
publicPath: '/',
filename: 'scripts/[name].bundle.js',
},
//生产环境
output: {
path: join(__dirname, '../dist/assets'),
publicPath: '/',
filename: 'scripts/[name].[contenthash:5].bundle.js',
},

配置运行命令后执行编译,发现虽然打包成功但是我们的components和layout在dist下没有,所以需要将文件拷贝到dist下,使用copy-wepbkac-plugin配置如下:

  new CopyPlugin({
patterns: [
{
from: join(__dirname, '../', 'src/web/views/layouts/layout.html'),
to: '../views/layouts/layout.html',
},
],
}),
new CopyPlugin({
patterns: [
{
from: 'src/web/components/**/*.html',
to: '../components',
transformPath(targetPath, absolutePath) {
//win环境
return targetPath.replace('src\\web\\components\\', '');
//mac环境
// return targetPath.replace('src/web/components/', '');
},
},
],
}),

自定义 html-after-plugin 插件

如上配置都完成后发现还是不能运行,查看html文件后发现打包虽然完成,但是js文件插入的位置有问题,未能按照我们在模板中定义的位置将js文件插入进去,而是将js文件都追加在了最后面,导致运行的时候生成html文件中没有引入js,所以需要我们自定义webpack插件,因为自定义的webpack插件是基于html-webpack-plugin的所以需要关闭该插件的默认插入操作,设置inject为false。具体的API可以查看npm.js中html-webpack-plugin中的介绍,自定义的webpack插件中主要使用到了两个生命周期beforeAssetTagGenerationbeforeEmit,第一个生命周期是资源生成前可以保存所有的资源路径,第二个生命周期是插入到html之前所以可以在该生命周期对html内容进行替换,具体实现如下:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const pluginName = 'HtmlAfterPlugin';

const assetsHelp = (data) => {
let js = [];
const getAssetsName = {
js: (item) => `<script src="${item}"></script>`,
};
for (let jsitem of data.js) {
js.push(getAssetsName.js(jsitem));
}
return {
js,
};
};

class HtmlAfterPlugin {
constructor() {
this.jsarr = [];
}
apply(compiler) {
compiler.hooks.compilation.tap(pluginName, (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
pluginName,
(htmlPligunData, cb) => {
const { js } = assetsHelp(htmlPligunData.assets);
this.jsarr = js;
cb(null, htmlPligunData);
}
);
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
pluginName,
(data, cb) => {
let _html = data.html;
_html = _html.replace('<!--injectjs-->', this.jsarr.join(''));
_html = _html.replace(/@components/g, '../../../components');
_html = _html.replace(/@layouts/g, '../../layouts');
data.html = _html;
cb(null, data);
}
);
});
}
}

module.exports = HtmlAfterPlugin;

在wepbpack中引入我们自定义的插件,发现js插入到了理想中的位置。

服务端代码编译

gulp小巧且容易使用,因为我们这里只对ES Module进行编译,编译目标是Commonjs的规范,使用gulp不会产生其他冗余的代码,代码也比较简洁,需要注意的是我们使用的编译插件是@babel/plugin-transform-modules-commonjs所以这里不能使用wepback中使用的babel配置,所以需要设置babelrc属性为false,开发环境需要使用gulp-watch对文件进行监听,当文件发生变化时自动编译。为了优化运行环境引入gulp-rollup的tree-shaking,对无用代码进行清洗,编译环境自动删除无用代码。完整配置如下:

gulpfile.js
const gulp = require('gulp');
const watch = require('gulp-watch');
const entry = './src/server/**/*.js';
const plumber = require('gulp-plumber');
const cleanEntry = './src/server/config/index.js';
const rollup = require('gulp-rollup');
const babel = require('gulp-babel');
const replace = require('@rollup/plugin-replace');
const prepack = require('gulp-prepack');
function builddev() {
return watch(entry, { ignoreInitial: false }, () => {
gulp
.src(entry)
.pipe(plumber())
.pipe(
babel({
babelrc: false,
plugins: ['@babel/plugin-transform-modules-commonjs'],
})
)
.pipe(gulp.dest('dist'));
});
}

function buildprod() {
return gulp
.src(entry)
.pipe(
babel({
babelrc: false,
ignore: [cleanEntry],
plugins: ['@babel/plugin-transform-modules-commonjs'],
})
)
.pipe(gulp.dest('dist'));
}

//清理环境变量
function buildconfig() {
return (
gulp
.src(entry)
.pipe(
rollup({
input: cleanEntry,
output: {
format: 'cjs',
},
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
})
)
// .pipe(prepack({}))
.pipe(gulp.dest('./dist'))
);
}

let build = gulp.series(builddev);
if (process.env.NODE_ENV == 'production') {
build = gulp.series(buildprod, buildconfig);
}

gulp.task('default', build);

落地页和切页优化

完成所有的配置后页面可以正常显示也可以正常的切换页面以及刷新。接下来我们给layout中增加一些公共库的使用比如jquery,发现每次刷新切换都会重新加载jquery

pjax

修改公共的layout.html,增加如下代码:

  <div id="app">
{% block content %}{% endblock %}
</div>
<script>
<script src="https://cdn.staticfile.org/jquery/3.5.1/jquery.js"></script>
<script src="https://cdn.staticfile.org/jquery.pjax/2.0.1/jquery.pjax.min.js"></script>
$(document).pjax('a', '#app');
</script>

为了优化如上这种情况,避免静态公共资源被重复加载,造成资源的浪费,这里选择加入pjax,pjax=ajax+pushState实现不刷新页面而修改页面内容。首先需要做的就是拦截所有<a>标签的默认事件,然后传入一个容器,容器就是需要替换的内容展示的位置。

修改代码重新切换页面发现http://localhost:8081/books/list?_pjax=%23app多了这么一条请求,因为服务器没有对这条请求进行响应所以还是走原来的路线,发起默认的list请求,所以需要增加服务器对pjax请求的响应处理。需要增加对header头中的x-pjax为判断条件,返回内容,注意要主动设置ctx.status=200ctx.type = 'html',那么到底要返回什么内容呢,这里就需要注意页面内容的加载分为落地页站内切,落地页需要返回整体所有的内容,站内切则只需要返回部分内容,到底返回那些内容呢,我们可以给需要动态返回的内容都增加一个classpjaxcontent,通常会给所有的组件都会添加。这里为了服务器端对html进行解析和DOM操作引入一个库cheerio类似服务器端的jquery,动态获取到所有的pjaxcontent元素通过流的方式分片发送给客户端。代码如下:

async actionIndex(ctx, next) {
// const book = new Book();
// const { data } = await book.getData();
const data = '123';
// console.log('🐻', data);
// ctx.body = {
// data,
// };
const html = await ctx.render('books/pages/list', {
data,
});
if (ctx.request.header['x-pjax']) {
console.log('站内切');
const $ = cheerio.load(html);
ctx.status = 200;
ctx.type = 'html';
//每个组件都需要加 否则切页动态替换的时候不能加载所有的组件
$('.pjaxcontent').each(function () {
ctx.res.write($(this).html());
});
//因为有缓存的原因原地切虽然没有重新加载js但是已经在栈中,当两个页面来回切就会发现js没有运行所以要加如下代码
$('.lazyload-js').each(function () {
ctx.res.write(
`<script class="lazyload-js" src="${$(this).attr('src')}"></script>`
);
});
ctx.res.end();
} else {
function createSSRStreamPromise() {
console.log('落地页');
return new Promise((resolve, reject) => {
const htmlStream = new Readable()
htmlStream.push(html);
htmlStream.push(null);
ctx.type = 'html';
ctx.status = 200;
htmlStream.on('error', (err) => {
reject(err)
})
.pipe(ctx.res)
})
}

await createSSRStreamPromise();
}
ctx.body = await ctx.render('books/pages/list');
}

总结

BFF基础架构已经全部实现也实现了简单的性能优化,站内切只加载部分资源,刷页全部加载,即保证了落地页的直出又保证了切页速度,结合了SSR的优点和SPA的优点。如上文章中使用了分片传输到客户端使用了bigpie,如需了解bigpie的详细信息可以查看bigpie初探

· 13 min read
石晓波

ie兼容一直是个让人头疼的问题,很多新的API都不能运行在IE上,但是IE有时候又不得不去兼容他,接下里是解决ie兼容的踩坑过程。

本文使用的是wepback5去编译工程代码,如果是单纯的使用babel或者typescript编译器也可以参考后续文章对应的部分。

webpack5编译分为两部分,webpack5的 运行时代码和我们编写的业务代码,webpack5自身可以通过配置将自身的运行时代码编译成可兼容ie运行的代码,但是业务代码webpack5只会处理模块和依赖关系代码并将业务代码本身不会优化为兼容性较高的版本,并且不能直接给ie运行,这时候就需要我们去配置loader去解决代码的编译问题了。

webpack5运行时代码兼容

默认情况下不配置webpack,生成的runtime使用的箭头函数,如果要讲webpack5运行时代码编译为兼容低版本的代码,需要配置target,target可配置为['web', 'es5'],target默认使用的是package.json中的browserslinst可以配置browserslists属性也能达到目的

babel编译

使用babel编译源代码需要配置babel-loader,babel可以将新版本的JavaScript编译为稳定版本以提高兼容性。首先需要安装babel-loader

npm install babel-loader @babel/core @babel/preset-env -D

webpack需要使用babel-loader来调用babel,而@babel/core和@babel/preset-env是核心插件集

danger

babel配置文件需要命名为.babelrc,千万不要命名为babel.config.json,后续配置babel-runtime会报错,模块解析失败的问题,如果添加了模块解析的plugin同样不能实现根据需要引入babel-runtime

首先配置webpack,使用babel-loader解析js文件,babel的配置单独配置到.babelrc中

webpack.common.js
            {
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'babel-loader',
}
]
}
{
"presets": [
[
"@babel/preset-env"
]
]
}

编写一些测试代码

const a = 1;
let b = 2;


class myClass{

}

const mine = new myClass();
console.log('mine',mine);

export const aFunc = () => {
return a + b;
}

export const arrInclude = () => {
const arr = [1, 2, 3, 4];
return arr.includes(a)
}

export const promiseFunc = () => {
return new Promise((resolve) => {
setTimeout(() => {
document.body.style.backgroundColor = 'green';
}, 3000)
})
}

const asyncFunc = async () => {
await promiseFunc();
console.log('success!!!!!!!!!!')
}



console.log('aFunc', aFunc())

console.log('arrInclude', arrInclude())

document.body.style.backgroundColor = 'red';

asyncFunc();

启动编译,查看bundle.js文件发现babel默认只对Syntax做转换,对于Promise它未转换,可以使用@babel/runtime@babel/polyfill

info

babel默认只针对Syntal做转换,例如箭头函数、es6、class语法糖等,而自带的API需要原生内置的方法需要透过polyfill才能在浏览器正常运行。

@babel/runtime使用

@babel/runtime是由Babel提供的polyfill套件,由core.js和regenerator组成,core.js是用于JavaScript的组合标准化库,它包含了各种版本的polyfill的实现,而regenerator是来自facebook的一个函数库,主要用户实现generator/yeild,async.await等特性。

安装@babel/runtime

yarn add @babel/runtime -D

使用@babel-runtime还需要安装依赖的@babel/plugin-transform-runtime

yarn add @babel/plugin-transform-runtime -D

修改.babelrc

{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false
}
]
]
}

从编译结果来看Class语法糖全局污染的问题已经结果,通过@babel/plugin-transform-runtime插件,它会帮助分析是否有polyfill的需求,并自动通过require的方式向@babel-runtime拿去polyfill,简单来说@babe/runtime提供了丰富的polyfill,开发者可自行使用require来导入自己需要的polyfill,但是谈过与繁琐和麻烦所以需要使用@babel/plugin-transform-runtime自动分析添加@babel/runtime中的polyfill,仔细观察编译结果来看Promise includes,接下来解锁@babel/runtime的所有功能配置。

corejs选项安装命令
falseyarn add @babel/runtime
2yarn add @babel/runtime-corejs2
3yarn add @babel/runtime-corejs3

修改babelrc文件

{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2
}
]
]
}

查看编译结果

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "aFunc": function() { return /* binding */ aFunc; },
/* harmony export */ "arrInclude": function() { return /* binding */ arrInclude; },
/* harmony export */ "promiseFunc": function() { return /* binding */ promiseFunc; }
/* harmony export */ });
/* harmony import */ var _babel_runtime_corejs2_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime-corejs2/helpers/asyncToGenerator */ "./node_modules/@babel/runtime-corejs2/helpers/esm/asyncToGenerator.js");
/* harmony import */ var _babel_runtime_corejs2_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime-corejs2/helpers/classCallCheck */ "./node_modules/@babel/runtime-corejs2/helpers/esm/classCallCheck.js");
/* harmony import */ var _babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @babel/runtime-corejs2/regenerator */ "./node_modules/@babel/runtime-corejs2/regenerator/index.js");
/* harmony import */ var _babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @babel/runtime-corejs2/core-js/promise */ "./node_modules/@babel/runtime-corejs2/core-js/promise.js");
/* harmony import */ var _babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_3__);




// import "core-js/stable";
// import "regenerator-runtime/runtime";
var a = 1;
var b = 2;

var myClass = function myClass() {
(0,_babel_runtime_corejs2_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_1__["default"])(this, myClass);
};

var mine = new myClass();
console.log('mine', mine);
var aFunc = function aFunc() {
return a + b;
};
var arrInclude = function arrInclude() {
var arr = [1, 2, 3, 4];
return arr.includes(a);
};
var promiseFunc = function promiseFunc() {
return new (_babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_3___default())(function (resolve) {
setTimeout(function () {
document.body.style.backgroundColor = 'green';
}, 3000);
});
};

var asyncFunc = /*#__PURE__*/function () {
var _ref = (0,_babel_runtime_corejs2_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/_babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee() {
return _babel_runtime_corejs2_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return promiseFunc();

case 2:
console.log('success!!!!!!!!!!');

case 3:
case "end":
return _context.stop();
}
}
}, _callee);
}));

return function asyncFunc() {
return _ref.apply(this, arguments);
};
}();

console.log('aFunc', aFunc());
console.log('arrInclude', arrInclude());
document.body.style.backgroundColor = 'red';
asyncFunc();

修改babelrc文件

{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}

编译结果如下

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "aFunc": function() { return /* binding */ aFunc; },
/* harmony export */ "arrInclude": function() { return /* binding */ arrInclude; },
/* harmony export */ "promiseFunc": function() { return /* binding */ promiseFunc; }
/* harmony export */ });
/* harmony import */ var _babel_runtime_corejs3_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime-corejs3/helpers/asyncToGenerator */ "./node_modules/@babel/runtime-corejs3/helpers/esm/asyncToGenerator.js");
/* harmony import */ var _babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @babel/runtime-corejs3/helpers/classCallCheck */ "./node_modules/@babel/runtime-corejs3/helpers/esm/classCallCheck.js");
/* harmony import */ var _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @babel/runtime-corejs3/regenerator */ "./node_modules/@babel/runtime-corejs3/regenerator/index.js");
/* harmony import */ var _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/instance/includes */ "./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/promise */ "./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_4__);
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_set_timeout__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @babel/runtime-corejs3/core-js-stable/set-timeout */ "./node_modules/@babel/runtime-corejs3/core-js-stable/set-timeout.js");
/* harmony import */ var _babel_runtime_corejs3_core_js_stable_set_timeout__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_babel_runtime_corejs3_core_js_stable_set_timeout__WEBPACK_IMPORTED_MODULE_5__);






// import "core-js/stable";
// import "regenerator-runtime/runtime";
var a = 1;
var b = 2;

var myClass = function myClass() {
(0,_babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_1__["default"])(this, myClass);
};

var mine = new myClass();
console.log('mine', mine);
var aFunc = function aFunc() {
return a + b;
};
var arrInclude = function arrInclude() {
var arr = [1, 2, 3, 4];
return _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_3___default()(arr).call(arr, a);
};
var promiseFunc = function promiseFunc() {
return new (_babel_runtime_corejs3_core_js_stable_promise__WEBPACK_IMPORTED_MODULE_4___default())(function (resolve) {
_babel_runtime_corejs3_core_js_stable_set_timeout__WEBPACK_IMPORTED_MODULE_5___default()(function () {
document.body.style.backgroundColor = 'green';
}, 3000);
});
};

var asyncFunc = /*#__PURE__*/function () {
var _ref = (0,_babel_runtime_corejs3_helpers_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__["default"])( /*#__PURE__*/_babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2___default().mark(function _callee() {
return _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_2___default().wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return promiseFunc();

case 2:
console.log('success!!!!!!!!!!');

case 3:
case "end":
return _context.stop();
}
}
}, _callee);
}));

return function asyncFunc() {
return _ref.apply(this, arguments);
};
}();

console.log('aFunc', aFunc());
console.log('arrInclude', arrInclude());
document.body.style.backgroundColor = 'red';
asyncFunc();

从上面的结果可知corejs2版本主要针对底层API做编译如:Promise、Fetch等等;corejs3版本主要针对底层API和一些方法如Array.prototype.filter、Array.prototype.includes,简单来说如果要彻底解决兼容性的问题就需要使用corejs3的版本。

note

使用@babel/runtime能够在不污染全局环境的情况下提供相对的的polyfil,拥有自动识别的功能,一般情况下编译出来的文件要比使用@babel/polyfill要小,适合开发组件库或者对环境较为严格的方案。

@babel/polyfill

当babel版本小于7.0.4可以使用@babel/polyfill,最新版本已经废弃,@babel/polyfill是由stable版本的core-js和regenerator-runtime组成,可以直接下在这两个组件库当做@babel/polyfill来使用官方也推荐这样使用。还需要注意的是regenerator-runtime为@babel/runtime的相依套件,检查是否正确安装

修改.babelrc如下所示

{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": false,
"corejs": 3 // 当前 core-js 版本
}
]
]
}

使用同样的JavaScript源码进行测试结果发现和单纯使用babel一样,只针对语法(Syntax)做编译,那是因为尚未开启polyfill的功能,可通过useBuniltIns来修改模式,模式可选为false usage entry,如果使用usage模式结果和@babel/runtime-corejs3一样,自动识别需要require的语法,将兼容性问题彻底解决,不同的地方在于@babel/runtime在不污染全局变量环境的情况下提供了polyfill,而@babel/polyfill则是将需要兼容的新语法挂在到全局对象上,这样的做法造成了所谓的全局污染,如果使用entry模式。就比较简单了,没有使用任何主动识别,直接将整个关于ES环境代码挂在到全局对象,确保浏览器可以兼容所有的新特性,但这样缺陷也显而易见,会导致源代码体积变大,所以为什么要有entry模式,其实最主要的原因是babel默认不会检测第三方的依赖库,所以使用usage选项时可能会因为第三方库的源代码问题导致不能兼容,这事就有了使用entry模式的必要。

note

使用entry选项记得在代码的最前面添加import core-js/stableregenerator-runtime/runtime组件库

@babel/polyfill具有一次性导入或自动和别导入polyfill的功能,使用挂在全局对象的方法兼容新特性,适合开发应用。不太适合开发组件库或者第三方工具包,存在污染全局环境的问题。

小总结

  1. Babel版本<7.4.0>
    • 开发组件库、工具包选择@babel/runtime
    • 开发应用类项目选择@babel/polyfill
  2. Babel版本>=7.4.0
    • 配置较简单,会污染全局环境,选择@babel/polyfill
    • 配置较繁琐,不会污染全局环境选择@babel/runtime

ts-loader

首先修改webpack的配置文件

webpack.common.js
  module: {
rules: [{
test: /\.ts$/,
use: ['ts-loader'],
}],
},

修改tsconfig.json

{
"compilerOptions":{
"target": "es3",
"strict":true,
"lib": [
"dom",
"es2015",
"scripthost"
],
}
}

async await为例,首先需要安装Promise的polyfill

yarn add es6-promise -D

源代码最前面需要手动引入polyfill

import "es6-promise/auto";
...

编译结果中添加了promise的polyfill,所有的类似需要兼容的API都需要添加对应的polyfill,比较繁琐。

总结

Babel的生态和编译提供的polyfill更为丰富,配置更方便,推荐使用Babel,后续文章中也会完成在React中使用typescript并且使用babel编译源代码。