首页>>前端>>Vue->玩起来,如何使用vue

玩起来,如何使用vue

时间:2023-11-30 本站 点击:1

上个月有网友看我之前用vite搭建的vue3.0服务端渲染demo之后,就在评论区问我有没有不是vite的vue3.0服务端渲染教程。闻此,我心中窃喜(ps:兄弟们来活了),沉睡了很长时间的我,终于又开始鼓捣了。

记得上一篇vite的文章是去年3月份发布的,一晃居然一年过去了,不由得感叹光阴似箭,日月如梭啊。在去年调研vite的时候,其实刚开始是调研的webpack和vue-cli去做构建工具,但是当时这方面的生态太差了,一些关键的地方进行不下去,无奈只能弃之,转用当时风头正盛的大明星-vite,不得不说vite由于尤大的大力支持,在当时来说生态已经是很好了,用来做ssr,基本稍加改动即可,想了解的同学可以看之前的vite帖子,不知不觉废话有多了,接下来进入我们的正题吧。

不行,还得说一句,太难了,实在是有点脑壳疼,文章有点长,各位看客先准备袋瓜子,看我慢慢道来。

从我们平时对vue-cli的使用知道,其实vue-cli已经帮我们做了很多底层构建的封装,但是它的这些封装都是基于csr模式去做的,并不一定适合ssr。所以我们在转为ssr的时候,毫无疑问要去改装它的vue-config.js文件。这里是官方文档给出的实例,喜欢循序渐进的同学可以去看看。

cli-service 命令

对cli-service注册不是很了解的同学,可以查看下下面官方文档: 添加一个新的 cli-service 命令 项目本地的插件 下面我用到的是本地注册的插件,当然你们也可以按上面的方法,独立成一个插件包来使用,奇怪的知识是不是又多了?

与官方实例比,这样通过自定义命令实现比较清晰明了,对原有架构没有太多的入侵,即可实现ssr。相信看了上一篇对cli-service的介绍,应该都了解的差不多了,下面不再做太多的赘叙,直接就入正题了,不然通篇看下来全是废话,浪费你们的时间。

注册ssr:build

首选我们注册ssr:build命令,用于生产打包,开发思路可以借鉴上面官方文档给出的代码,具体如下:

const webpackConfig = (api) =>// 根据不用的构建任务,实例化不同的wepack配置实例  api.chainWebpack((webpackConfig) => {    const { ClientWebpack, ServerWebpack } = require("./webpack");    const { VUE_CLI_SSR_TARGET } = process.env;    if (!VUE_CLI_SSR_TARGET || VUE_CLI_SSR_TARGET === "client")      return new ClientWebpack(webpackConfig);    return new ServerWebpack(webpackConfig);  });// vue-cli提供的注册指令api.registerCommand(    "ssr:build",    {      description: "build for production (SSR)",    },    async (args) => {      const webpack = require("webpack");      // 把vue-cli自带的webpack配置和当前指令的配置进行合并      webpackConfig(api);      const rimraf = require('rimraf');      const formatStats = require("@vue/cli-service/lib/commands/build/formatStats");      // 删除构建产物      rimraf.sync(api.resolve(config.distPath));      const { getWebpackConfigs } = require("./webpack");      // 提取css      api.service.projectOptions.css.extract = true;      // 文件名添加hash      api.service.projectOptions.filenameHashing = true;      // 获取合并后的webpack配置      const [clientConfig, serverConfig] = getWebpackConfigs(api.service);      // 生成编译器      const compiler = webpack([clientConfig, serverConfig]);      // 开始构建      compiler.run();    }  );

webpack

接下来我们来看看webpack配置文件

const webpack = require('webpack');const { WebpackManifestPlugin } = require('webpack-manifest-plugin') // 形成服务端manifest文件const nodeExternals = require('webpack-node-externals')const WebpackBar = require('webpackbar');const { config: baseConfig } = require('./config');const HtmlFilterPlugin = require('./plugins/HtmlFilterPlugin');const RemoveUselessAssetsPlugin = require('./plugins/RemoveUselessAssetsPlugin');const VueSSRClientPlugin = require('./plugins/VueSSRClientPlugin');const CssContextLoader = require.resolve('./loaders/css-context');class BaseWebpack {    constructor(config) {        const isProd = process.env.NODE_ENV === 'production';        const isBuild = process.env.RUN_TYPE === 'build';        config.plugins.delete('hmr');        // 禁用 cache loader,否则客户端构建版本会从服务端构建版本使用缓存过的组件        config.module.rule('vue').uses.delete('cache-loader');        config.module.rule('js').uses.delete('cache-loader');        config.module.rule('ts').uses.delete('cache-loader');        config.module.rule('tsx').uses.delete('cache-loader');        // 一些报错的友好提示        config.stats(isProd ? 'normal' : 'none');        // 构建js文件添加hash        isBuild && config.output.filename('js/[name].[hash].js').chunkFilename('js/[name].[hash].js');        // 一些报错的友好提示        config.devServer            .stats('errors-only')            .quiet(true)            .noInfo(true);    }}// 客户端构建配置class ClientWebpack extends BaseWebpack {    constructor(config) {        super(config);        config            .entry('app')            .clear()            .add('./src/entry-client');        config            .plugin('loader')            .use(WebpackBar, [{ name: 'Client', color: 'green' }]);        // 过滤掉index.html模板文件里面的js和css注入        config.plugin('html-filter').use(HtmlFilterPlugin);        // block clear comments in template        config.plugin('html').tap((args) => {            args[0].minify && (args[0].minify.removeComments = false);            return args;        });        // 生成客户端文件映射        config.plugin('VueSSRClientPlugin')            .use(VueSSRClientPlugin);    }}class ServerWebpack extends BaseWebpack {    constructor(config) {        super(config);        config            .entry('app')            .clear()            .add('./src/entry-server');        config            .output            .libraryTarget('commonjs2');        // 这允许 webpack 以适合于 Node 的方式处理动态导入,        // 同时也告诉 `vue-loader` 在编译 Vue 组件的时候抛出面向服务端的代码。        config.target('node');        // 生成客户端资源清单        config            .plugin('manifest')            .use(new WebpackManifestPlugin({ fileName: 'ssr-manifest.json' }));        // server-side remove public file        config.plugins.delete('copy');        // 由于共用的vue-cli配置会生产一些无用文件,则进行清除        config.plugin('RemoveUselessAssetsPlugin')              .use(new RemoveUselessAssetsPlugin());        // 忽略掉没有必要的构建依赖        config.externals(nodeExternals({ allowlist: baseConfig.nodeExternalsWhitelist }));        // 不需要代码分割,合成一个文件即可        config.optimization.splitChunks(false).minimize(false);        // 删除服务端不支持的plugins        config.plugins.delete('preload');        config.plugins.delete('prefetch');        config.plugins.delete('progress');        config.plugins.delete('friendly-errors');        const isExtracting = config.plugins.has('extract-css');        if (isExtracting) {            // Remove extract            const langs = ['css', 'postcss', 'scss', 'sass', 'less', 'stylus'];            const types = ['vue-modules', 'vue', 'normal-modules', 'normal'];            for (const lang of langs) {                for (const type of types) {                    const rule = config.module.rule(lang).oneOf(type);                    rule.uses.delete('extract-css-loader');                    // Critical CSS                    rule.use('css-context')                        .loader(CssContextLoader)                        .before('css-loader');                }            }            config.plugins.delete('extract-css');        }        config.plugin('limit').use(            new webpack.optimize.LimitChunkCountPlugin({                maxChunks: 1            })        );        config            .plugin('loader')            .use(WebpackBar, [{ name: 'Server', color: 'orange' }]);        config.node.clear();    }}const getWebpackConfigs = (service) => {    process.env.VUE_CLI_SSR_TARGET = 'client';    // Override outputDir before resolving webpack config    service.projectOptions.outputDir = `${baseConfig.distPath}/client`;    const clientConfig = service.resolveWebpackConfig();    process.env.VUE_CLI_SSR_TARGET = 'server';    // 重写outputDir,使客户端和服务端打包产物隔离    service.projectOptions.outputDir = `${baseConfig.distPath}/server`;    const serverConfig = service.resolveWebpackConfig();    return [clientConfig, serverConfig];};

写到这里,敲过官方实例的同学就会发现,把它的代码原封不动的copy下来,构建出来的产物,服务端会多出很多无用的文件,运行之后也会发现在页面首次加载的同时,也会把一些暂时不需要的js,css文件也一并加载了,这是没必要的。所以上面手写了几个插件用来避免这些问题。

HtmlFilterPlugin

阻止vue-cli自带的html-webpack-plugin插件向模板文件注入js和css文件。

const ID = 'vue-cli-plugin-ssr:html-filter';module.exports = class HtmlFilterPlugin {    apply(compiler) {        compiler.hooks.compilation.tap(ID, (compilation) => {            compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(                ID,                (data, cb) => {                    data.head = data.head.filter(                        (tag) => !this.isCssOrJs(tag)                    );                    data.body = data.body.filter(                        (tag) => !this.isCssOrJs(tag)                    );                    cb(null, data);                }            );        });    }    isCssOrJs(tag) {        const { href, src } = tag.attributes;        return /.(css|js)$/.test(href || src);    }};

RemoveUselessAssetsPlugin

移除掉服务端生成的无用文件

class RemoveUselessAssetsPlugin {    apply(compiler) {        compiler.hooks.emit.tapAsync('webpack', (compilation, callback) => {            Object.keys(compilation.assets).forEach(k => {                if (k.match('precache-manifest'))                delete compilation.assets[k];            })            delete compilation.assets['index.html'];            delete compilation.assets['service-worker.js'];            delete compilation.assets['manifest.json'];            callback();        });    }}

既然我们用到上述插件移除了html-webpack-plugin对index.html模板文件的资源注入,那么问题就来了,我们在请求页面的时候,要如何的去正确的注入当面路由匹配的页面所需要的资源呢?就在百思不得其解的时候,突然想起来vue2.0 ssr,那么它又是如何去做的呢?我们来打开vue2.0用到的ssr插件vue-server-renderer的仓库,可以很清晰的看到表层就有一个client-plugin.js文件,这个就是生成客户端资源对应清单的关键所在,我们可以点进去借鉴一下源码的思路即可实现,即上面代码中使用的VueSSRClientPlugin插件,文件地址:https://github.com/Vitaminaq/cfsw-vue-cli3.0/blob/ssr-vue3.0-cli/plugins/ssr/plugins/VueSSRClientPlugin.js。

既然生成了客户端资源清单,那么问题又来了,我们如何在请求到达服务器的时候去动态按需注入到ssr模板中去呢?可以说问题环环相扣,非常之烧脑。这个时候我又满脸奸笑的把目光瞄上了vue-server-renderer插件,那么它是怎么来做资源的匹配按需加载的呢。我们把鼠标点向它的源码处:https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/build.dev.js同样也是动动小手,即可改装成我们所需功能,觉得麻烦的同学也可以直接装它这个插件来用用,不得不说。

用前朝的剑,斩本朝的官,你好大的胆子啊

到了这里生产构建也就差不多了,接下来开始啃本地开发的配置,一个字:麻烦!

注册ssr:serve

用于本地开发,首先看下注册代码

 api.registerCommand(    "ssr:serve",    { description: "Run the included server." },    async (args) => {      webpackConfig(api);      const { createServer } = require("./server");      const port = args.port || config.port || process.env.PORT;      // 防止端口冲突      if (!port) {        const portfinder = require("portfinder");        port = await portfinder.getPortPromise();      }      await createServer({ port, api });    }  );

看起来比上面的ssr:build简单点太多,事实并非如此,createServer创建本地开发服务器只是个开始。

createServer

启动ssr服务器核心代码如下,看过之前vite那篇的同学应该不会太陌生,换汤不换药:

module.exports = async (app) => {    const isBuild = process.env.RUN_TYPE === 'build';    try {        let createApp; // entry-server导出的构建函数        let template; // 模板文件        let clientManifest; // 客户端资源清单        // 经过构建的,直接读取dist目录下相应文件即可        if (isBuild) {            const manifest = require(resolveSource('server/ssr-manifest.json'));            const appPath = resolveSource(`server/${manifest['app.js']}`);            createApp = require(appPath).default            template = fs.readFileSync(resolveSource('client/index.html'), 'utf-8');            clientManifest = require(resolveSource('client/vue-ssr-client-manifest.json'));        } else {            // 开发环境后续讲解            const { setupDevServer } = require('./dev-server');            await setupDevServer({                server: app,                onUpdate: ({ca, tl, cm}) => {                    createApp = ca;                    template = tl;                    clientManifest = cm;                }            });        }        app.use(compression({ threshold: 0 }));        // Serve static files        if (isBuild) {            const serve = (filePath) =>            express.static(filePath, {                maxAge: config.maxAge,                index: false            });            // 把打包好的文件转成静态资源            const serveStaticFiles = serve(resolveSource('client'));            // 拒绝访问index.html模板文件            app.use((req, res, next) => {                if (/index\.html/g.test(req.path)) {                    next();                } else {                    serveStaticFiles(req, res, next);                }            });        }        app.get('*', async(req, res, next) => {            if (config.skipRequests(req)) return next();            // 读取配置文件,注入给客户端            const envConfig = require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` }).parsed;            const { app, store } = await createApp(req.originalUrl, envConfig);            const appContent = await renderToString(app);            const state =                '<script>window.__INIT_STATE__=' +                serialize(store, { isJSON: true }) + ';' +                'window.__APP_CONFIG__=' + serialize(envConfig, { isJSON: true }) +                '</script>';            // 调用从vue-server-render插件里面提取的模板渲染函数,来进行模板静态资源按需加载            const render = new TemplateRenderer({                template,                inject: true,                clientManifest            });            // Load resources on demand            const html = render.render('')                .replace('<div id="app">', `<div id="app">${appContent}`)                .replace(`<!--app-store-->`, state);            res.setHeader('Content-Type', 'text/html');            res.send(html)        });        return createApp;    } catch (e) {        console.error(e);    }};

setupDevServer

module.exports.setupDevServer = ({ server, onUpdate }) =>    new Promise((resolve, reject) => {        const { getWebpackConfigs } = require('./webpack');        const [clientConfig, serverConfig] = getWebpackConfigs(config.api.service);        let createApp;        let template;        let clientManifest;        // 触发更新函数        const update = () => {            if (createApp && template && clientManifest) {                onUpdate({ ca: createApp, tl: template, cm: clientManifest });                resolve();            }        };        // modify client config to work with hot middleware        clientConfig.entry.app = [            'webpack-hot-middleware/client',            ...clientConfig.entry.app        ];        clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin());        // dev middleware        const clientCompiler = webpack(clientConfig);        const clientMfs = new MFS();        // watch file update        const devMiddleware = require('webpack-dev-middleware')(            clientCompiler,            {                outputFileSystem: clientMfs, // 改写编译输出文件配置,写入内存                publicPath: clientConfig.output.publicPath,                stats: 'none',                index: false            }        );        server.use(devMiddleware);        clientCompiler.hooks.done.tap('cli ssr', async (stats) => {            // 读取内存里面的模板文件以及客户端资源清单            template = clientMfs.readFileSync(path.join(clientConfig.output.path, 'index.html'), 'utf8');            clientManifest = JSON.parse(clientMfs.readFileSync(path.join(clientConfig.output.path, 'vue-ssr-client-manifest.json'), 'utf8'));            // 编译完毕,触发更新            update();        });        // hot module replacement middleware - refresh page        server.use(            require('webpack-hot-middleware')(clientCompiler, {                heartbeat: 5000            })        );        // watch and update server renderer        const serverCompiler = webpack(serverConfig);        // 服务端逻辑同客户端类似        const serverMfs = new MFS();        serverCompiler.outputFileSystem = serverMfs;        serverCompiler.watch({}, (err, stats) => {            // 读取内存里面的文件            const appFile = serverMfs.readFileSync(path.join(serverConfig.output.path, 'js/app.js'), 'utf-8');            createApp = eval(appFile).default;            update();        });    });

综上所述,其实就在于三个点,服务端导出的createApp,客户端编译的template,客户端构建时形成的clientManifest。利用crateApp生成当前路由匹配的dom节点,插入template中,再根据clientManifest动态按需加载当前页面所需要的资源文件。 对于ssr的改造做了上述这些,还有些项目优化,比如模块化,ts,store的按需注册,以及一些自定义插件等,就不一一道来了,喜欢的同学可以download源码或者fork过去玩玩。 有需要交流的同学,也欢迎评论区交流交流。

项目仓库:https://github.com/Vitaminaq/cfsw-vue-cli3.0/tree/ssr-vue3.0-cli 注册插件源码:https://github.com/Vitaminaq/cfsw-vue-cli3.0/tree/ssr-vue3.0-cli/plugins/ssr 项目中用到的插件仓库:https://github.com/Vitaminaq/plugins-vue(喜欢的同学可以自取,欢迎同学们加入开发)

原文:https://juejin.cn/post/7094641120633683982


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Vue/3735.html