完美融合 nextjs 和 antd
相信大家在使用nextjs的时候,难免遇到一些坑。其实可能大部分原因在于 nextjs 做了很多封装,我们可能不能第一时间搞清楚包括它相关的所有配置,比如其中的webpack配置。我前面也写过 SSR 实现的文章和简单的轮子《实现ssr服务端渲染》,也知道 SSR 要实现为 nextjs 这样的三方框架,还是会需要经历很复杂编码的。
总归有时候遇到问题,在网上也查不到一个正确的解决方案。比如,我为此头痛几天的 antd-mobile 按需加载,最开始我无法正常使用,就只能全局引入 antd-mobile的min.css,这导致我要在页面加载 164k 的 css 文件,我们使用 nextjs 就是为了提升加载速度,这种情况不能忍啊!
言归正传,先说说我遇到的问题,我使用了 antd-mobile 并且需要对它进行按需加载,下面是官方的文档,推荐我们使用 babel-plugin-import。
按照 nextjs 官方文档我们在 .babelrc 中添加插件
{
"presets": ["next/babel"],
"plugins": [
["import", { "libraryName": "antd-mobile", "style": true }]
]
}
可当我运行的时候报错了,报错如下。最开始感到奇怪,我并没有引入这个包,后来发现其实是 antd-mobile 中有引入它。但是为什么会报错呢, 便想到应该是 webpack loader 的问题,我认为是 loader 排除了node_modules。(这里解释一下,node_modules 中的包本应该都是打包好的,但是一些情况下,我们是直接引入源代码中的模块的,那这样的话我们就需要让我们的 loader 解析 node_modules 中的代码,然而有些默认配置就是排除了 node_modules 的,这样就会导致无法解析)。
然后我在 next.config.js 中,定义 webpack 方法,打印出 webpack 配置。 nextjs 中的 webpack 配置大致是引入了一个 next-babel-loader 这样的 loader,而我们使用next-css、next-less或者next-sass等插件,相关的 loader 会被 push 到 rules 中。 核心的loader 就是 next-babel-loader。然而我在其参数中并没有发现 exclude, 到是有 include,而后我往 include 里添加 node_modules 下需要的组件正则,发现并没有效果。而后我经历了各种痛苦,尝试过各种方面的办法,网上也查不出解决方案。好,跳过心酸的部分。
再后来我开始仔细的一个个看官方的插件,我找到了它:next-transpile-modules,从名称上来看似乎和我想要的有点关系。https://github.com/martpie/next-transpile-modules。
一看文档果然,它就是我要找的,它就是解决 node_modules 中代码不被 loader 解析的问题。我使用了它,这时报错信息变了(其实后来我弄比较清楚以后就没有报错了,可能当时配置改的比较多,哪里影响到了),我觉得似乎起到作用了,但是还是会报错。于是我便看了一下它的代码,我终于发现了 webpack.externals 这个配置,原来是这个地方排除了解析外部依赖。如果我们使用插件 transpile 并配置好 transpileModules: ["antd-mobile"],transpile 内部会生成 includes 正则,在 externals 执行时,会排除掉我们配置的 node_modules 模块,因此 antd-mobile 就能被正常解析了,代码如下
if (config.externals) {
config.externals = config.externals.map(external => {
if (typeof external !== 'function') return external;
return (ctx, req, cb) => {
return includes.find(include =>
req.startsWith('.')
? include.test(path.resolve(ctx, req))
: include.test(req)
)
? cb()
: external(ctx, req, cb);
};
});
}
而后它又添加了一个 next-babel-loader 到 rules 中,现在其实有两个 next-babel-loader 在 webpack 配置中。我认为这个配置是多余的,并且就是之前我可能哪里没配置对,这个多余的 loader 让我编译报错了,我把它生成的多余 loader 删除才没有报错的。
最后在我完全能正常运行的时候,还是尝试删除了它,发现并没有报错,因为从理论上来说,这个重复的loader本身也没有用,因此我给作者提了一个建议,建议去掉这个新loader, 对方说再认真看看。这里:https://github.com/martpie/next-transpile-modules/issues/32。 (事实证明我理解错了,请看文章后文详情)
// Add a rule to include and parse all modules
config.module.rules.push({
test: /\.+(js|jsx|ts|tsx)$/,
loader: options.defaultLoaders.babel,
include: includes
});
我当前使用的 next 是8.x,在6.x里,我看了下它确实是用的 exclude 来排除的 node_modules,到 8 以后改为 externals 了,一定有它官方的道理吧。如果你用的是6.x,你可以尝试修改 exclude,不过建议大家都升级为 8 吧,很平滑的。
第二个问题,可能也是大家比较常见的,那就是 cssModules。官方代码是这样的
// next.config.js
const withCSS = require('@zeit/next-css')
module.exports = withCSS({
cssModules: true,
cssLoaderOptions: {
importLoaders: 1,
localIdentName: "[local]___[hash:base64:5]",
}
})
完全没有问题,可以正常使用。只是 antd-mobile 的 class 名称也被 cssModules 给改了,但是组件 dom 中的 class 名称并没有被修改,这样样式就不起作用了。ok,没有问题,这个简单,我们使用 css-loader api 中的 options.getLocalIdent,来控制修改 class 名称。代码大致如下
const cssLoaderGetLocalIdent = require("css-loader/lib/getLocalIdent.js");
/*.....*/
cssLoaderOptions: {
localIdentName: "[local]___[hash:base64:5]",
getLocalIdent: (context, localIdentName, localName, options) => {
let hz = context.resourcePath.replace(context.rootContext, "");
if (/node_modules/.test(hz)) {
return localName;
} else {
return cssLoaderGetLocalIdent(
context,
localIdentName,
localName,
options
);
}
}
},
通过阅读 css-loader 源码,发现其内部运行过程,它内部有一个 css-loader/lib/getLocalIdent.js 方法,如果用户自定义了 getLocalIdent 方法,它在编译 cssmodules 时,便会用用户定义的方法,否则使用自带的方法。我的想法就是通过自定义 getLocalIdent, 正则判断 node_modules,也就是当前样式如果是来自于 node_modules 中文件的话,我返回它本身的名称,就是不改动它,而它是我们的源码的话,我执行 css-loader 本身的 getLocalIdent 方法。这样就既使我们自己的代码能被 cssmodules,而三方库的代码不被 cssmodules 影响。
最后附上两个配置文件 .babelrc 、 next.config.js 和 postcss.config.js
//.babelrc
{
"presets": ["next/babel"],
"plugins": [
["import", { "libraryName": "antd-mobile", "style": true }]
]
}
//next.config.js
const withLess = require("@zeit/next-less");
const withCss = require("@zeit/next-css");
const withPlugins = require("next-compose-plugins");
const withTM = require('next-transpile-modules');
const cssLoaderGetLocalIdent = require("css-loader/lib/getLocalIdent.js"); module.exports = withPlugins([withCss, withLess,withTM], {
transpileModules: ["antd-mobile"],
cssModules: true,
cssLoaderOptions: {
localIdentName: "[local]___[hash:base64:5]",
getLocalIdent: (context, localIdentName, localName, options) => {
let hz = context.resourcePath.replace(context.rootContext, "");
if (/node_modules/.test(hz)) {
return localName;
} else {
return cssLoaderGetLocalIdent(
context,
localIdentName,
localName,
options
);
}
}
}
});
//postcss.config.js
const pxtorem = require("postcss-pxtorem");
module.exports = {
plugins: [
pxtorem({
rootValue: 50,
unitPrecision: 5,
propList: ["*"],
selectorBlackList: [/^\.nop2r/, /^\.am/],//排除antd样式
replace: true,
mediaQuery: false,
minPixelValue: 0
})
]
}
pxtorem是转换px为rem,有的需要的自取,如果此方案解决了你的问题,点个赞吧~ 注意: 如果还会存在 antd 的报错,在 next.config.js 中添加 webpack 配置方法去掉 next-transpile-modules 额外添加的 loader,清空其 include。 这个多余的 loader 确实会导致 bug,或许你在使用的时候此包的代码已经更新。
//next.config.js
const withLess = require("@zeit/next-less");
const withCss = require("@zeit/next-css");
const withPlugins = require("next-compose-plugins");
const withTM = require('next-transpile-modules');
const cssLoaderGetLocalIdent = require("css-loader/lib/getLocalIdent.js"); module.exports = withPlugins([withCss, withLess,withTM], {
lessLoaderOptions: {//如果是antd就需要,antd-mobile可以不用
javascriptEnabled: true
},
transpileModules: ["antd-mobile"],
cssModules: true,
cssLoaderOptions: {
localIdentName: "[local]___[hash:base64:5]",
getLocalIdent: (context, localIdentName, localName, options) => {
let hz = context.resourcePath.replace(context.rootContext, "");
if (/node_modules/.test(hz)) {
return localName;
} else {
return cssLoaderGetLocalIdent(
context,
localIdentName,
localName,
options
);
}
}
},
webpack(config){
config.module.rules.forEach(item=>{
if(item.loader&&item.loader.loader){
item.include = []
}
})
return config
}
});
终解:
后来我终于想清楚了,首先 next-transpile-modules 的目的就是让 node_modules 中的包可以使用 next-babel-loader ,它的文档第一句就是这个意思,我当时理解错误了。
其次我们再来说说 webpack.externals 这个配置,比如 nextjs 默认就是如下这样配置的,它把 node_modules 下的 js 作为一个公共的js来处理,当这样配置以后,webpack 就不会去分析 node_modules 下的 js 的依赖了。
比如我自己在 node_modules 里写一个文件夹 @test,里面是一个 index.js,index.js require了同级的 b.js,然后我们在 nextjs 的项目代码里引入 @test/index.js ,编译时就会报错,报错的行就在 require('b.js') 这里。
再来说说 next-transpile-modules, 它做了两个事情,第一是从 nextjs 默认的 externals 中,排除掉我们定义的 transpileModules: ["antd-mobile"],这样 antd-mobile 中的 js 就会被 webpack 正常解析依赖了。而后新建了一个 next-babel-loader ,include 的值是 transpileModules 配置的 ["antd-mobile"]。 由于我们的 antd-mobile 中的代码不需要被 next-babel-loader 解析,甚至如果使用 next-babel-loader 解析就会报错,因此我前面的配置把它添加的 loader 的 include 给清空了,这样所有的配置就 ok 了。因此我们只需要它其中的 externals 功能,ok, next.config.js 最终代码如下( .babelrc 和 postcss.config.js 参照上面不变
const withLess = require("@zeit/next-less");
const withCss = require("@zeit/next-css");
const withPlugins = require("next-compose-plugins");
const cssLoaderGetLocalIdent = require("css-loader/lib/getLocalIdent.js");
const path = require('path'); module.exports = withPlugins([withLess,withCss], {
lessLoaderOptions : {//如果是antd就需要,antd-mobile不需要
javascriptEnabled : true
},
cssModules: true,
cssLoaderOptions: {
camelCase: true,
localIdentName: "[local]___[hash:base64:5]",
getLocalIdent: (context, localIdentName, localName, options) => {
let hz = context.resourcePath.replace(context.rootContext, "");
if (/node_modules/.test(hz)) {
return localName;
} else {
return cssLoaderGetLocalIdent(
context,
localIdentName,
localName,
options
);
}
}
},
webpack(config){
if(config.externals){
const includes = [/antd-mobile/];
config.externals = config.externals.map(external => {
if (typeof external !== 'function') return external;
return (ctx, req, cb) => {
return includes.find(include =>
req.startsWith('.')
? include.test(path.resolve(ctx, req))
: include.test(req)
)
? cb()
: external(ctx, req, cb);
};
});
}
return config;
}
});
如果没有实现按需加载,最终打包出来的文件会很大,因为包含了整个antd库。
一点重要提示
需要安装less,npm i --save less。本以为 next/less 应该有 less 依赖,结果还是需要安装一下 less,否则 loader 会报错!
发现的一些问题记录
1.页面切换样式问题
开发环境页面 A 切换到 B 后,B 没有样式。这个情况是在开发模式下才有。
比如我初次启动应用之后,访问 A,A 发现没登录访 B,这个时候 B 样式加载不出来,页面没样式。如果我在 B 页面刷新一次,让服务端渲染一次,然后 A 再跳到 B 就有样式了。我发现在第一次从 A 跳到 B 的时候,有一个类似这样的一个请求:/_next/static/chunks/styles.js?ts=1557217006063,就是 B 样式的热更新文件。但是实际 _next/static/css/styles.chunk.css 这个文件里没有成功载入 B 的样式。而当我们用服务端渲染一次 B 页面,也就是在 B 的路由下刷新一次。而后的 chunk.css 就有样式了。
我们再看看生产环境,生产环境,nextjs 会把所有依赖的 css 打包到一个 chunk.css 文件中,在首次渲染的时候,整个应用的所有样式都已经被载入了,比如 A 和 B 的样式都有了。所以在切换页面的时候,样式都没问题。
依照这个情况看来,开发环境下,样式是被加载到运行时的内存中的,一旦有用服务端渲染 A 页面,A 的样式就会被添加进服务端内存中,再用服务端渲染一次 B 页面,而后请求 chunk.css 就才会有两个页面的样式。问题在于开发环境下的热更新没有起到作用,应该是一个官方的bug。
此 issue 说不是 next 核心的 bug,是三方插件的问题,那么问题应该在next-css, https://github.com/zeit/next.js/issues/4732 。
完美融合 nextjs 和 antd的更多相关文章
- MacOSX和Windows 8的完美融合
MacOSX和Windows8的完美融合 一般情况下我们要在MACOS系统下运行Windows软件怎么办呢?一种方法我们可以装CrossOver这款软件,然后在configuration->in ...
- SpringBoot专题1----springboot与mybatis的完美融合
springboot大家都知道了,搭建一个spring框架只需要秒秒钟.下面给大家介绍一下springboot与mybatis的完美融合: 首先:创建一个名为springboot-mybatis的ma ...
- 再也不担心写出臃肿的Flink流处理程序啦,发现一款将Flink与Spring生态完美融合的脚手架工程-懒松鼠Flink-Boot
目录 你可能面临如下苦恼: 接口缓存 重试机制 Bean校验 等等...... 它为流计算开发工程师解决了 有了它你的代码就像这样子: 仓库地址:懒松鼠Flink-Boot 1. 组织结构 2. 技术 ...
- 懒松鼠Flink-Boot(Flink+Spring):一款将Flink与Spring生态完美融合的脚手架工程
目录 你可能面临如下苦恼: 接口缓存 重试机制 Bean校验 等等...... 它为流计算开发工程师解决了 有了它你的代码就像这样子: 仓库地址:懒松鼠Flink-Boot 1. 组织结构 2. 技术 ...
- Spring Cloud与Dubbo的完美融合之手「Spring Cloud Alibaba」
很早以前,在刚开始搞Spring Cloud基础教程的时候,写过这样一篇文章:<微服务架构的基础框架选择:Spring Cloud还是Dubbo?>,可能不少读者也都看过.之后也就一直有关 ...
- 光谱郑匡移动互联网O2O完美融合
移动互联网尽管市场颇大,前景广阔,可是由于数据过于密集,非常难精准的定位所谓的目标客户群,然而O2O的线下市场却与互联网市场有极大的反差.一直认为高校周边的小商家是最幸福的生意人,客户明白(就是本校学 ...
- XCode与Git的完美融合,不再依赖其它Git客户端
Git源代码管理工具的出现,使得我们开发人员对于源码的管理更加方便快捷.至于Git的优点以及与其他源代码管理工具有何区别,不是本文的重点,如果想深入了解可以搜索一下这方面的文章.下面直接进入主题,如何 ...
- WordPress腾讯云存储搭建教程,完美解决
写在前面的话: 为什么会有今天的话题:WordPress+腾讯云存储? 因为博主不想使用七牛云,也不想使用又拍云,所以才有了今天的话题. 在使用腾讯云存储的过程中是很不顺利的,万幸的是现在终于完美融合 ...
- hexo next主题深度优化(十),博文加密,不需要插件,极简模式,相对安全,融合pjax。
文章目录 效果: 代码: 注意: 背景: 思路: https://www.jianshu.com/p/90c0a15c6f36 http://zhailiange.com/2017/07/06/hex ...
随机推荐
- ApplicationContext(三)BeanFactory 初始化
ApplicationContext(三)BeanFactory 初始化 上节我们提到容器初始化的第一步首先进行了属性的检验,下面就要开始第二步:进行 beanFactory 的初始化工作了. App ...
- linux fedora 的备份小技巧
大家都知道,在fedora中,是没有默认安装带有GUI的备份软件的. 我们可以去软件中心搜索“备份”或者“dup”来安装deja-dup来进行备份,这个软件就是ubuntu中设置的“备份”,只不过ub ...
- NotificationMangerService处理显示通知
设置——>应用——>点击“已下载”列表中的任一APP,如图: 代码位置:Settings\src\com\android\settings\applications\InstalledA ...
- Ubuntu服务器如何搭建PPTPD(原创保证可用)
Ubuntu是一款基于linux的操作系统,无需许可和订购的费用,Ubuntu Server可以帮助您高效地扩展您的数据中心.它精简的架构和自动化部署的能力让您只需花费更少的运算能力和资源,便可提供更 ...
- 销售vs技术岗,做技术的方法思考
销售甚至比技术岗位挣得还多,当然,做技术的比较好的拿到的自然也多. 我在想个问题,技术的天然优势是可以不断地积累,包括写code,写博客,做流程,完善流程,自动化流程,或者把某些工作流程化,自动化,托 ...
- Java类加载机制及自定义加载器
转载:https://www.cnblogs.com/gdpuzxs/p/7044963.html Java类加载机制及自定义加载器 一:ClassLoader类加载器,主要的作用是将class文件加 ...
- HDU 6185(打表代码
/** @xigua */ #include <cstdio> #include <cmath> #include <iostream> #include < ...
- hdu-1033(格式)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1033 参考文章:https://blog.csdn.net/curson_/article/detai ...
- WPF中的路由事件(转)
出处:https://www.cnblogs.com/JerryWang1991/archive/2013/03/29/2981103.html 最近因为工作需要学习WPF方面的知识,因为以前只关注的 ...
- 解决css3不支持同时缩放和旋转的办法
设置两个div,外层scale,内层rotate.