• nextjs:如何将静态资源发布到 CDN


    nextjs 是基于 react 的服务端同构指出框架,在使用的过程中也多多少少遇到过几个问题,其中最大的问题就是静态资源的发布了。

    1. 如何基于文件内容进行 hash 命名

    Next.js uses a constant generated at build time to identify which version of your application is being served. This can cause problems in multi-server deployments when next build is ran on every server. In order to keep a static build id between builds you can provide the generateBuildId function:

    按照官网上的说法,每次发布都会生成新的 hash 路径,即使当前没有任何的变动。例如某次发布的路径是/_next/static/tZonUgEY-GPCEExGbFapL/pages/index.js,那么下次的 hash 必然不是这个值。这样导致的一个问题是:如果在多台机器上发布并 build 时,会导致每次 build 产生的值不同。如果想固定某个值或者使用某个值,一个是可以先 build 完成后后再分发,或者,可以在next.config.js中自定义generateBuildId

    // 来自官网上的例子
    // next.config.js
    module.exports = {
        generateBuildId: async () => {
            return 'my-build-id';
        }
    };
    

    npm 上也有提供相应的安装包,可以使用当前 git 提交的 hash 值作为 buildId:next-build-id

    可是这种存在的一个问题就是:即使文件没有发生变动,或者我只修改了首页的代码,发布完成后,pages 下所有的资源都需要重新加载,有用户建议使用内容的 hash 值作为每个资源的路径,但官方好像好像不太情愿,说实现起来比较困难,详情可以看这个 issue: use content hash in pages chunk name。在这条 issue 中,有用户自己实现一个插件,不过我还没用过,有兴趣的同学可以尝试下。

    2. 路径的拼接规则

    静态资源上传到 CDN,这是存在目前存在的最大的问题,虽然在next.config.js中可以配置assetPrefix字段,但实际使用起来还是非常困难。

    打包后的 js 和 css,引用路由均为/_next/static开头:

    nextjs中静态资源上传到CDN

    如图片中所示,带有 data-next-page 属性的,实际上访问的是.next/server/static/[hash]/pages/_app.js;不带这个属性的,访问的路径是.next/static/runtime/webpack-[hash].js

    我们以 2019/09/16 提交的 nextjs 源码为例:pages_document,里面有全局脱水数据的注入,页面相关的 js 和静态资源的 js 的拼接:

    // 页面相关的js
    // assetPrefix为我们在next.config.js中配置的前缀
    // ${buildId}即为每次打包生成的hash值,在本地环境下值为development
    // _devOnlyInvalidateCacheQueryString: 变动的时间戳,正式环境中为空, _devOnlyInvalidateCacheQueryString: process.env.NODE_ENV !== 'production' ? '?ts=' + Date.now() : ''
    src={assetPrefix + encodeURI(`/_next/static/${buildId}/pages${getPageFile(page)}`) + _devOnlyInvalidateCacheQueryString}
    
    // 静态资源的js
    src={`${assetPrefix}/_next/${file}${_devOnlyInvalidateCacheQueryString}`}
    

    全局脱水数据的注入

    <script
        id="__NEXT_DATA__"
        type="application/json"
        nonce={this.props.nonce}
        crossOrigin={this.props.crossOrigin || process.crossOrigin}
        dangerouslySetInnerHTML={{
            __html: NextScript.getInlineScriptSource(this.context._documentProps)
        }}
        data-ampdevmode
    />
    

    上面的页面编译后的路径是.next/server/static/{hash}/pages/_document.js,这些 js 读取的路径是分别由 2 个 json 文件控制的。

    • sever/pages-manifest.json:加载页面相关的 js,nextjs 是服务端渲染+客户端渲染两种方式,刷新页面时使用的服务端渲染(使用server/static/{hash}/pages/中的文件),切换路由时使用的是客户端渲染(使用static/{hash}/pages/中的文件),这里加载的 js,是用于在路由切换时使用客户端渲染的方式;
    • static/build-manifest.json:加载静态资源的 js,使用static/里除 hash 路径外的资源;

    我们了解这些,主要是为了理解 js 的路径是怎样拼接完成的。

    3. 如何发布静态资源到 CDN

    静态资源发布到 CDN 其实很简单,只要把.next/static下目录的资源上传上去即可。最困难的是如何替换代码中的路径。把这个目录下的静态文件上传到 CDN 后,生成的地址会变成:

    <!-- 假设我们的CDN地址是 http://static.qq.com -->
    <script src="http://static.qq.com/runtime/webpack-4b444dab214c6491079c.js"></script>
    

    从第 2 部分中能看到,代码中使用assetPrefix作为静态资源的前缀时,只是单纯的拼接到了最前面而已,拼接后的地址是:

    <script src="http://static.qq.com/_next/static/runtime/webpack-4b444dab214c6491079c.js"></script>
    

    中间多出了/_next/static的路径,最后的结果是页面需要加载的资源和上传的资源路径不一致,就会各种 404。这里我的解决方案很简单粗暴,读取编译后的文件,然后执行 node 程序,将里面的字符替换掉:

    const fs = require('fs');
    
    // 获取文件夹中所有的文件
    function readDirAll(path) {
        // 获取字符串的最后一个字符
        var getLastCode = function(str) {
            return str.substr(str.length - 1, 1);
        };
    
        var result = []; // 存储获取到的文件
        var stats = fs.statSync(path); // 获取当前文件的状态
        if (stats.isFile()) {
            result.push(path);
        } else if (stats.isDirectory()) {
            // 若当前路径是文件夹,则获取路径下所有的信息,并循环
            var files = fs.readdirSync(path);
    
            for (var i = 0, len = files.length; i < len; i++) {
                var item = files[i],
                    itempath =
                        getLastCode(path) == '/' ? path + item : path + '/' + item; // 拼接路径
                var st = fs.statSync(itempath);
    
                if (st.isFile()) {
                    result.push(itempath);
                } else if (st.isDirectory() && item !== 'cache') {
                    // 当前是文件夹,则递归检索,将递归获取到的文件列表与当前result进行拼接
                    var s = readDirAll(itempath);
                    result = result.concat(s);
                }
            }
        }
        return result;
    }
    
    const list = readDirAll('.next');
    
    list.forEach(file => {
        let data = fs.readFileSync(file, 'utf8');
    
        if (file.indexOf('_document.js') > -1) {
            data = data
                .replace(//_next//g, '/')
                .replace(/static/" + buildId/g, '" + buildId');
            fs.writeFileSync(file, data);
            console.log(file, 'success');
        } else if (file.indexOf('build-manifest.json') > -1) {
            data = data.replace(/static//g, '');
            fs.writeFileSync(file, data);
            console.log(file, 'success');
        } else if (data.indexOf('/_next/static') > -1) {
            data = data.replace(//_next/static//g, '/');
            fs.writeFileSync(file, data);
            console.log(file, 'success');
        }
    });
    

    这样就能就可以保证项目的 CDN 地址和真正上传的地址是一致的了。

    欢迎访问蚊子的前端博客: https://www.xiabingbao.com
    欢迎关注蚊子的公众号:
    蚊子的前端公众号

  • 相关阅读:
    微信小程序如何获取UnionId详细版---Crypto.js解密
    微信小程序获取用户unionId
    pymysql的使用
    OpenCV 磨皮-Python
    python 识别图片中的人脸,并裁剪人脸,将图片转化成固定大小,并将图片再按照自己的后缀名存储起来
    python遍历文件夹中的所有jpg文件
    numpy matrix 与ndarray的区别
    netty: 解决粘包拆包: 分隔符DelimiterBasedFrameDecoder,定长消息FixedLengthFrameDecoder
    commons-dbutils工具栏的编写
    commons-dbutils 字段名称转换,支持驼峰字段名
  • 原文地址:https://www.cnblogs.com/xumengxuan/p/11693381.html
Copyright © 2020-2023  润新知